ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/vim/plugin/mcp-vim.vim
Revision: 1.1
Committed: Sun Oct 11 08:05:52 2026 UTC (2 weeks, 3 days ago) by root
Branch: MAIN
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 " mcp-vim.vim - Vim client for the mcp-vim broker (protocol version 1.0).
2     "
3     " The broker exposes live Vim buffers to MCP clients and also supports
4     " browser-initiated editing sessions. Vim itself uses a private protocol
5     " to talk to the broker; it is not an MCP client.
6     "
7     " Installation:
8     " Distribution URL: http://dist.schmorp.de/mcp-vim/1/mcp-vim.vim
9     " Copy the file to ~/.vim/plugin/mcp-vim.vim to load it at startup,
10     " or load it manually with :source /path/to/mcp-vim.vim.
11     "
12     " Usage:
13     " :Mcp http://HOST:PORT/COOKIE/TARGET
14     "
15     " The broker supplies the connection URL, including its authentication
16     " cookie and the target name. To avoid typing the common prefix, set
17     " this in your vimrc:
18     "
19     " let g:mcp_broker_url = 'http://HOST:PORT/COOKIE/'
20     "
21     " Then connect with :Mcp TARGET. Disconnected sessions are retried.
22     " McpHasTarget() reports whether a target URL has been configured.
23     "
24     " The /1/ in the distribution URL denotes the private Vim/broker
25     " protocol major version, not the MCP protocol version.
26     "
27     " Restart Vim after updating this file; re-sourcing does not reload it.
28    
29     if exists ('g:loaded_mcp_vim')
30     finish
31     endif
32     let g:loaded_mcp_vim = 1
33    
34     let s:prot_major = 1
35     let s:prot_minor = 0
36    
37     let s:instance_id = hostname () . ':' . getpid () . ':' . localtime ()
38     if has ('reltime')
39     let s:instance_id .= ':' . reltimestr (reltime ())
40     endif
41    
42     let s:channel = v:null
43     let s:sessions = {}
44     let s:popup = 0
45     let s:url = ''
46     let s:target = ''
47     let s:retry_timer = -1
48     let s:incoming = v:null
49    
50     command! -nargs=1 Xpcse call s:connect (<f-args>)
51     command! -nargs=1 Mcp call s:connect (<f-args>)
52    
53     function! McpHasTarget () abort
54     return !empty (s:url)
55     endfunction
56    
57     function! s:stop_retry () abort
58     if s:retry_timer != -1
59     call timer_stop (s:retry_timer)
60     let s:retry_timer = -1
61     endif
62     endfunction
63    
64     function! s:retry (timer) abort
65     if type (s:channel) != v:t_channel
66     echom 'mcp-vim: reconnecting to ' . s:target
67     call s:connect (s:url)
68     endif
69     endfunction
70    
71     function! s:quit () abort
72     if type (s:channel) == v:t_channel
73     if ch_status (s:channel) ==# 'open'
74     call ch_sendexpr (s:channel, ['quit'])
75     endif
76     call ch_close (s:channel)
77     let s:channel = v:null
78     endif
79     endfunction
80    
81     function! s:connect (url) abort
82     let url = a:url
83    
84     if url !~# '^http://'
85     let url = get (g:, 'mcp_broker_url', '') . url
86     endif
87    
88     let s:url = url
89    
90     let m = matchlist (url, '^http://\([^:/]*\):\([0-9]\+\)/\([^/]\+\)/\(.*\)$')
91    
92     if empty (m)
93     echoerr "Bad mcp-vim broker URL"
94     return
95     endif
96    
97     call s:quit ()
98    
99     let host = m[1]
100     let port = m[2]
101     let cookie = m[3]
102     let id = m[4]
103     let s:target = id
104    
105     let s:channel = ch_open (host . ':' . port, {
106     \ 'callback': function ('s:receive'),
107     \ 'close_cb': function ('s:disconnected'),
108     \ })
109    
110     if ch_status (s:channel) ==# 'open'
111     call s:stop_retry ()
112    
113     call ch_sendexpr (s:channel, ['mcp-vim-connect', s:prot_major, s:prot_minor, cookie, id, s:instance_id])
114    
115     for session in values (s:sessions)
116     call ch_sendexpr (s:channel, ['session', session.session, session.state])
117     endfor
118    
119     call ch_sendexpr (s:channel, ['sessions-done'])
120     else
121     let s:channel = v:null
122     echom 'mcp-vim: connection failed'
123     endif
124     endfunction
125    
126     function! s:disconnected (channel) abort
127     echom 'mcp-vim: disconnected from ' . s:target
128     let s:channel = v:null
129    
130     if s:retry_timer == -1
131     let s:retry_timer = timer_start (1000, function ('s:retry'), { 'repeat': 60 })
132     endif
133     endfunction
134    
135     """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
136     " RPC support.
137     "
138     " broker -> vim: ['call', id, method, args...]
139     " vim -> broker: ['reply', id, result]
140     " vim -> broker: ['error', id, exception, throwpoint]
141     "
142     " Methods:
143     " state ()
144     " read_lines (buffer, ranges)
145     " grep (buffers, pattern, mode, max_results)
146     " replace_lines (buffer, line, old_lines, new_lines, expected_changedtick)
147     " apply_patch (buffer, patch, max_offsets, expected_changedtick) " requires ordinary unified diff; MCP wrapper may relax @@ headers
148     " substitute (buffer, range, pattern, replacement, flags, expected_changedtick)
149     " set_cursor (buffer, line, col [, expected_changedtick])
150     "
151     " Explicit buffer ids are loaded on demand. In state, changedtick and line_count
152     " are v:null until a buffer has been loaded.
153    
154     let s:rpc_sid = expand ('<SID>')
155    
156     function! s:rpc_bufinfo (buf) abort
157     let info = getbufinfo (a:buf)
158    
159     if empty (info)
160     throw 'rpc/no_such_buffer: ' . a:buf
161     endif
162    
163     if !info[0].loaded
164     call bufload (a:buf)
165     let info = getbufinfo (a:buf)
166    
167     if empty (info) || !info[0].loaded
168     throw 'rpc/cannot_load_buffer: ' . a:buf
169     endif
170     endif
171    
172     return info[0]
173     endfunction
174    
175     function! s:rpc_expect_tick (expected) abort
176     if b:changedtick != a:expected
177     throw printf ('rpc/stale_buffer: expected %d, got %d', a:expected, b:changedtick)
178     endif
179     endfunction
180    
181     function! s:rpc_break_undo () abort
182     let &g:undolevels = &g:undolevels
183     endfunction
184    
185     " Execute with {buf} current, while disturbing the user's window/buffer/cursor state as little as possible.
186     function! s:rpc_in_buffer (buf, fun, ...) abort
187     let restoreview = !a:0 || a:1
188    
189     let info = s:rpc_bufinfo (a:buf)
190     let oldwin = win_getid ()
191     let oldbuf = bufnr ('%')
192     let oldview = winsaveview ()
193    
194     try
195     if a:buf != oldbuf
196     if !empty (info.windows)
197     noautocmd call win_gotoid (info.windows[0])
198    
199     if win_getid () != info.windows[0]
200     throw 'rpc/cannot_enter_window: ' . info.windows[0]
201     endif
202     else
203     execute 'noautocmd keepalt keepjumps hide buffer ' . a:buf
204     endif
205     endif
206    
207     let targetwin = win_getid ()
208     let targetview = winsaveview ()
209    
210     try
211     return call (a:fun, [])
212     finally
213     if restoreview
214     \ && win_getid () == targetwin
215     \ && bufnr ('%') == a:buf
216     call winrestview (targetview)
217     endif
218     endtry
219     finally
220     noautocmd call win_gotoid (oldwin)
221    
222     if win_getid () != oldwin
223     throw 'rpc/cannot_restore_window: ' . oldwin
224     endif
225    
226     if bufnr ('%') != oldbuf
227     execute 'noautocmd keepalt keepjumps hide buffer ' . oldbuf
228     endif
229    
230     " Restore the caller's original view unless that is the buffer
231     " we deliberately modified and want Vim's adjustment to survive.
232     if restoreview || oldbuf != a:buf
233     call winrestview (oldview)
234     endif
235     endtry
236     endfunction
237    
238     " Validate a small Ex-range subset, then let Ex resolve it.
239     " A range-function gets the resolved addresses in a:firstline/a:lastline.
240     "
241     " range := % | address ([,;] address)?
242     " address := base offset* | offset+
243     " base := . | $ | [1-9][0-9]* | /regexp/ | ?regexp?
244     " offset := [+-][0-9]*
245     " regexp := one or more non-delimiter characters; backslash escapes
246     " the following character, including the delimiter
247     let s:rpc_range_result = v:null
248    
249     function! s:rpc_capture_range () range abort
250     let s:rpc_range_result = [a:firstline, a:lastline]
251     endfunction
252    
253     function! s:rpc_validate_range (range) abort
254     if a:range =~# '[\r\n]'
255     throw 'rpc/bad_range: ' . a:range
256     endif
257    
258     let fwd_char = '\%([^/\\]\|\\.\)'
259     let bwd_char = '\%([^?\\]\|\\.\)'
260     let fwd = '/' . fwd_char . '\+/'
261     let bwd = '?' . bwd_char . '\+?'
262     let base = '\%([.$]\|[1-9][0-9]*\|' . fwd . '\|' . bwd . '\)'
263     let offset = '[+-][0-9]*'
264     let address = '\%(' . base . '\%(' . offset . '\)*\|\%(' . offset . '\)\+\)'
265     let range = '\%(%\|' . address . '\%([,;]' . address . '\)\?\)'
266    
267     if a:range !~# '^' . range . '$'
268     throw 'rpc/bad_or_unsupported_range: ' . a:range
269     endif
270     endfunction
271    
272     function! s:rpc_range (range, line_count) abort
273     call s:rpc_validate_range (a:range)
274    
275     let range = a:range
276     let start = matchlist (range, '^\([1-9][0-9]*\)\([,;]\)')
277     let end = matchlist (range, '\([,;]\)\([1-9][0-9]*\)$')
278    
279     if !empty (start) && str2nr (start[1]) > a:line_count
280     throw printf ('rpc/range_start_beyond_eof: %d > %d', str2nr (start[1]), a:line_count)
281     endif
282    
283     if !empty (end) && str2nr (end[2]) > a:line_count
284     let range = substitute (range, '\([,;]\)[1-9][0-9]*$', '\1' . a:line_count, '')
285     elseif range =~# '^[1-9][0-9]*$' && str2nr (range) > a:line_count
286     throw printf ('rpc/range_start_beyond_eof: %d > %d', str2nr (range), a:line_count)
287     endif
288    
289     let pos = getpos ('.')
290     let s:rpc_range_result = v:null
291    
292     try
293     execute 'silent keepjumps keeppatterns ' . range . ' call ' . s:rpc_sid . 'rpc_capture_range ()'
294     finally
295     call setpos ('.', pos)
296     endtry
297    
298     if type (s:rpc_range_result) != v:t_list
299     throw 'rpc/bad_range: ' . a:range
300     endif
301    
302     return s:rpc_range_result
303     endfunction
304    
305     function! s:rpc_call_state () abort
306     let buffers = []
307    
308     for info in getbufinfo ()
309     call add (buffers, {
310     \ 'buffer': info.bufnr,
311     \ 'name': info.name,
312     \ 'listed': info.listed ? v:true : v:false,
313     \ 'modified': info.changed ? v:true : v:false,
314     \ 'changedtick': info.loaded ? info.changedtick : v:null,
315     \ 'line_count': info.loaded ? info.linecount : v:null,
316     \ 'buftype': getbufvar (info.bufnr, '&buftype'),
317     \ 'filetype': getbufvar (info.bufnr, '&filetype'),
318     \ })
319     endfor
320    
321     return {
322     \ 'cwd': getcwd (),
323     \ 'current_buffer': bufnr ('%'),
324     \ 'cursor': {'line': line ('.'), 'column': col ('.')},
325     \ 'buffers': buffers,
326     \ }
327     endfunction
328    
329     function! s:rpc_call_apply_patch (buf, patch, max_offsets, expected_changedtick) abort
330     return s:rpc_in_buffer (a:buf, {-> s:rpc_apply_patch (
331     \ a:patch,
332     \ a:max_offsets,
333     \ a:expected_changedtick
334     \ )}, v:false)
335     endfunction
336    
337     function! s:rpc_apply_patch (patch, max_offsets, expected_changedtick) abort
338     call s:rpc_expect_tick (a:expected_changedtick)
339    
340     if !&modifiable
341     throw 'rpc/not_modifiable'
342     endif
343    
344     let old_lines = getline (1, '$')
345     let old_eol = &endofline
346     let oldfile = tempname ()
347     let newfile = tempname ()
348    
349     try
350     " Write exactly the logical Vim buffer to the patch input file.
351     " writefile(..., 'b') adds no final NL; an extra empty list item does.
352     let disk_lines = copy (old_lines)
353    
354     if old_eol
355     call add (disk_lines, '')
356     endif
357    
358     if writefile (disk_lines, oldfile, 'b')
359     throw 'rpc/patch_write_failed'
360     endif
361    
362     " patch does all unified-diff parsing/matching. It never modifies the
363     " Vim buffer. Keep rejects out of the filesystem.
364     let cmd = 'LC_ALL=C patch --unified --batch --forward --get=0 --fuzz=0'
365     \ . ' --reject-file=- --no-backup-if-mismatch'
366     \ . ' -o ' . shellescape (newfile)
367     \ . ' ' . shellescape (oldfile)
368     \ . ' 2>&1'
369    
370     let output = system (cmd, a:patch)
371     let status = v:shell_error
372    
373     if status
374     throw printf (
375     \ 'rpc/patch_failed: exit=%d %s',
376     \ status,
377     \ substitute (trim (output), "\n", ' | ', 'g')
378     \ )
379     endif
380    
381     " --fuzz=0 still permits relocating whole hunks. The MCP wrapper rejects
382     " ambiguous pure insertions and limits relocation by distinct old-side lines.
383     for line in split (output, "\n")
384     let match = matchlist (line, '^Hunk #\(\d\+\).*\<offset\s\+\([+-]\=\d\+\)\s\+lines\?\>')
385    
386     if !empty (match)
387     let hunk = str2nr (match[1])
388     let offset = str2nr (match[2])
389    
390     if hunk < 1 || hunk > len (a:max_offsets)
391     throw 'rpc/patch_bad_output: ' . substitute (trim (output), "\n", ' | ', 'g')
392     endif
393    
394     if abs (offset) > a:max_offsets[hunk - 1]
395     throw printf ('rpc/patch_needs_more_context: hunk=%d offset=%d max=%d %s',
396     \ hunk, offset, a:max_offsets[hunk - 1],
397     \ substitute (trim (output), "\n", ' | ', 'g'))
398     endif
399     endif
400     endfor
401    
402     " Binary read preserves whether the result had a final newline:
403     " readfile(..., 'b') adds a final empty item iff the file ends in NL.
404     let new_lines = readfile (newfile, 'b')
405     let new_eol = !empty (new_lines) && new_lines[-1] ==# ''
406    
407     if new_eol
408     call remove (new_lines, -1)
409     endif
410    
411     " Vim always has at least one logical line.
412     if empty (new_lines)
413     let new_lines = ['']
414     endif
415    
416     " Keep this first implementation line-oriented.
417     if new_eol != old_eol
418     throw 'rpc/patch_eol_change'
419     endif
420    
421     let hunks = diff (old_lines, new_lines, {'output': 'indices'})
422    
423     if empty (hunks)
424     return {
425     \ 'changedtick': b:changedtick,
426     \ 'hunks': 0,
427     \ 'noop': v:true,
428     \ }
429     endif
430    
431     " Nothing has touched the buffer yet. Recheck immediately before commit.
432     call s:rpc_expect_tick (a:expected_changedtick)
433     call s:rpc_break_undo ()
434    
435     " diff() indices refer to the original buffer. Apply bottom-up so earlier
436     " line numbers never move underneath later hunks.
437     let changed = 0
438    
439     for hunk in reverse (copy (hunks))
440     let first = hunk.from_idx + 1
441     let old_count = hunk.from_count
442     let new_count = hunk.to_count
443     let common = min ([old_count, new_count])
444    
445     let replacement = new_count
446     \ ? new_lines[hunk.to_idx : hunk.to_idx + new_count - 1]
447     \ : []
448    
449     " Pure insertion: from_idx is the zero-based insertion position.
450     if !old_count
451     if changed
452     undojoin
453     endif
454    
455     if append (first - 1, replacement)
456     throw 'rpc/append_failed'
457     endif
458    
459     let changed = 1
460     continue
461     endif
462    
463     if common
464     if changed
465     undojoin
466     endif
467    
468     if setline (first, replacement[:common - 1])
469     throw 'rpc/setline_failed'
470     endif
471    
472     let changed = 1
473     endif
474    
475     if new_count < old_count
476     if changed
477     undojoin
478     endif
479    
480     if deletebufline (
481     \ bufnr ('%'),
482     \ first + new_count,
483     \ first + old_count - 1
484     \ )
485     throw 'rpc/deletebufline_failed'
486     endif
487    
488     let changed = 1
489    
490     elseif new_count > old_count
491     if changed
492     undojoin
493     endif
494    
495     if append (
496     \ first + old_count - 1,
497     \ replacement[old_count:]
498     \ )
499     throw 'rpc/append_failed'
500     endif
501    
502     let changed = 1
503     endif
504     endfor
505    
506     return {
507     \ 'changedtick': b:changedtick,
508     \ 'hunks': len (hunks),
509     \ }
510    
511     finally
512     call delete (oldfile)
513     call delete (newfile)
514     endtry
515     endfunction
516    
517     function! s:rpc_read_ranges (buf, ranges) abort
518     let result = []
519     let line_count = s:rpc_bufinfo (a:buf).linecount
520    
521     for range in a:ranges
522     let [first, last] = s:rpc_range (range, line_count)
523     call add (result, {'range': range, 'start': first, 'end': last, 'lines': getline (first, last)})
524     endfor
525    
526     return {'changedtick': b:changedtick, 'ranges': result}
527     endfunction
528    
529     function! s:rpc_call_read_lines (buf, ranges) abort
530     return s:rpc_in_buffer (a:buf, {-> s:rpc_read_ranges (a:buf, a:ranges)})
531     endfunction
532    
533     " :global does the actual scan; this function merely collects matching lines.
534     let s:rpc_grep_result = []
535     let s:rpc_grep_pattern = ''
536     let s:rpc_grep_max = 0
537    
538     function! s:rpc_grep_collect () abort
539     if len (s:rpc_grep_result) >= s:rpc_grep_max
540     return
541     endif
542    
543     let text = getline ('.')
544    
545     call add (s:rpc_grep_result, {
546     \ 'buffer': bufnr ('%'),
547     \ 'changedtick': b:changedtick,
548     \ 'line': line ('.'),
549     \ 'column': match (text, s:rpc_grep_pattern) + 1,
550     \ 'text': text,
551     \ })
552     endfunction
553    
554     function! s:rpc_grep_buffer () abort
555     let save_search = @/
556    
557     try
558     let @/ = s:rpc_grep_pattern
559     execute 'silent keepjumps keeppatterns global//call ' . s:rpc_sid . 'rpc_grep_collect()'
560     finally
561     let @/ = save_search
562     endtry
563     endfunction
564    
565     function! s:rpc_call_grep (buffers, pattern, mode, max_results) abort
566     if a:max_results <= 0
567     throw 'rpc/bad_max_results: ' . a:max_results
568     endif
569    
570     if a:mode ==# 'regex'
571     let s:rpc_grep_pattern = a:pattern
572     elseif a:mode ==# 'word'
573     let s:rpc_grep_pattern = '\V\<' . escape (a:pattern, '\') . '\>'
574     else
575     throw 'rpc/bad_grep_mode: ' . a:mode
576     endif
577    
578     let s:rpc_grep_result = []
579     let s:rpc_grep_max = a:max_results
580     let buffers = empty (a:buffers) ? map (filter (getbufinfo (), 'v:val.loaded'), 'v:val.bufnr') : a:buffers
581    
582     for buf in buffers
583     call s:rpc_in_buffer (buf, {-> s:rpc_grep_buffer ()})
584    
585     if len (s:rpc_grep_result) >= s:rpc_grep_max
586     break
587     endif
588     endfor
589    
590     return {'matches': s:rpc_grep_result, 'truncated': len (s:rpc_grep_result) >= s:rpc_grep_max}
591     endfunction
592    
593     " Quote / as an Ex :substitute delimiter without changing replacement backslash semantics.
594     " A / preceded by an even-length run of backslashes needs one more backslash;
595     " with an odd-length run it is already quoted for the command parser. An odd
596     " trailing run also needs one more backslash so it cannot quote the closing /.
597     function! s:rpc_quote_substitute_replacement (text) abort
598     let result = ''
599     let backslashes = 0
600    
601     for char in split (a:text, '\zs')
602     if char ==# '\'
603     let backslashes += 1
604     else
605     if char ==# '/' && backslashes % 2 == 0
606     let result .= '\'
607     endif
608    
609     let backslashes = 0
610     endif
611    
612     let result .= char
613     endfor
614    
615     if backslashes % 2
616     let result .= '\'
617     endif
618    
619     return result
620     endfunction
621    
622     function! s:rpc_insert_lines (line, lines, expected_changedtick) abort
623     if a:line < 0 || a:line > line ('$')
624     throw printf ('rpc/bad_insert_line: %d', a:line)
625     endif
626    
627     call s:rpc_expect_tick (a:expected_changedtick)
628     call s:rpc_break_undo ()
629    
630     if append (a:line, a:lines)
631     throw 'rpc/append_failed'
632     endif
633    
634     return {
635     \ 'changedtick': b:changedtick,
636     \ 'start': a:line + 1,
637     \ 'end': a:line + len (a:lines),
638     \ }
639     endfunction
640    
641     function! s:rpc_call_insert_lines (buf, line, lines, expected_changedtick) abort
642     return s:rpc_in_buffer (a:buf, {-> s:rpc_insert_lines (
643     \ a:line,
644     \ a:lines,
645     \ a:expected_changedtick
646     \ )})
647     endfunction
648    
649     function! s:rpc_substitute (range, pattern, replacement, flags, expected_changedtick) abort
650     call s:rpc_expect_tick (a:expected_changedtick)
651     call s:rpc_validate_range (a:range)
652    
653     if a:flags !~# '^[egiInp#lr]*$'
654     throw 'rpc/bad_substitute_flags: ' . a:flags
655     endif
656    
657     if strpart (a:replacement, 0, 2) ==# '\='
658     throw 'rpc/expression_replacement_not_allowed'
659     endif
660    
661     let save_search = @/
662     let replacement = s:rpc_quote_substitute_replacement (a:replacement)
663    
664     call s:rpc_break_undo ()
665    
666     try
667     let @/ = a:pattern
668     execute 'silent keepjumps keeppatterns ' . a:range . 's//' . replacement . '/' . a:flags
669     finally
670     let @/ = save_search
671     endtry
672    
673     return {'changedtick': b:changedtick, 'changed': b:changedtick != a:expected_changedtick}
674     endfunction
675    
676     function! s:rpc_call_substitute (buf, range, pattern, replacement, flags, expected_changedtick) abort
677     return s:rpc_in_buffer (a:buf, {-> s:rpc_substitute (a:range, a:pattern, a:replacement, a:flags, a:expected_changedtick)})
678     endfunction
679    
680     function! s:rpc_call_set_cursor (buf, line, col, ...) abort
681     let info = s:rpc_bufinfo (a:buf)
682    
683     if a:0 && a:1 isnot v:null && info.changedtick != a:1
684     throw printf ('rpc/stale_buffer: expected %d, got %d', a:1, info.changedtick)
685     endif
686    
687     if a:line < 1 || a:line > info.linecount || a:col < 1
688     throw printf ('rpc/bad_cursor: %d,%d', a:line, a:col)
689     endif
690    
691     if a:buf != bufnr ('%')
692     if !empty (info.windows)
693     if !win_gotoid (info.windows[0])
694     throw 'rpc/cannot_enter_window: ' . info.windows[0]
695     endif
696     else
697     execute 'keepalt keepjumps hide buffer ' . a:buf
698     endif
699     endif
700    
701     call cursor (a:line, a:col)
702    
703     return {'buffer': bufnr ('%'), 'changedtick': b:changedtick, 'line': line ('.'), 'column': col ('.')}
704     endfunction
705    
706     """""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
707     " command receiver
708    
709     function! s:receive (channel, msg) abort
710     if type (a:msg) != v:t_list || len (a:msg) == 0
711     return
712     endif
713    
714     let cmd = a:msg[0]
715    
716     if cmd ==# 'call'
717     let id = a:msg[1]
718     let method = a:msg[2]
719     let args = a:msg[3:]
720    
721     try
722     let result = call (function('s:rpc_call_' . method), args)
723     call ch_sendexpr (s:channel, ['reply', id, 1, result])
724     catch
725     call ch_sendexpr (s:channel, ['reply', id, 0, v:exception]) " originallky also: v:throwpoint
726     endtry
727    
728     elseif cmd ==# 'open'
729     let session = a:msg[1]
730     let filename = a:msg[2]
731     let extension = a:msg[3]
732     let state = a:msg[4]
733    
734     let path = tempname() . '-' . filename . extension
735     let s:incoming = { 'path': path, 'session': session, 'state': state }
736     call writefile ([], path)
737    
738     elseif cmd ==# 'data'
739     let data = base64_decode (a:msg[1])
740     call ch_sendexpr (s:channel, ['ack', len (data)])
741     call writefile (data, s:incoming.path, 'ab')
742    
743     elseif cmd ==# 'ack'
744     " nop
745    
746     elseif cmd ==# 'eof'
747     if a:msg[1]
748     execute 'edit ' . fnameescape (s:incoming.path)
749     let s:sessions[bufnr('%')] = { 'session': s:incoming.session, 'state': s:incoming.state, 'path': s:incoming.path }
750     else
751     call delete (s:incoming.path)
752     endif
753    
754     let s:incoming = v:null
755    
756     elseif cmd ==# 'ex'
757     execute a:msg[1]
758    
759     elseif cmd ==# 'echo'
760     if s:popup
761     call popup_close (s:popup)
762     endif
763     echom 'mcp-vim: ' . a:msg[1]
764    
765     elseif cmd ==# 'error'
766     if s:popup
767     call popup_close (s:popup)
768     endif
769     let s:popup = popup_create (split (a:msg[1], "\n", 1), { 'line': &lines - 2, 'col': 1, 'pos': 'botleft', 'maxheight': 10, 'maxwidth': &columns - 4, 'scrollbar': 1, 'padding': [0, 1, 0, 1] })
770    
771     elseif cmd ==# 'goto'
772     for b in keys (s:sessions)
773     if s:sessions[b].session ==# a:msg[1]
774     execute 'buffer ' . b
775     call cursor (a:msg[2], a:msg[3])
776     break
777     endif
778     endfor
779    
780     else
781     echom 'mcp-vim: unknown command ' . string (cmd)
782     endif
783     endfunction
784    
785     function! s:save (file) abort
786     if type (s:channel) != v:t_channel
787     return
788     endif
789    
790     let b = bufnr ('%')
791    
792     if has_key (s:sessions, b) && a:file ==# s:sessions[b].path
793     call ch_sendexpr (s:channel, ['save', s:sessions[b].session])
794    
795     let data = readblob (s:sessions[b].path)
796     call ch_sendexpr (s:channel, ['data', base64_encode (data)])
797     call ch_sendexpr (s:channel, ['eof', 1])
798    
799     endif
800     endfunction
801    
802     function! s:close (buf) abort
803     if has_key (s:sessions, a:buf)
804     call delete (s:sessions[a:buf].path)
805     if type (s:channel) == v:t_channel
806     call ch_sendexpr (s:channel, ['close', s:sessions[a:buf].session])
807     endif
808     call remove (s:sessions, a:buf)
809     endif
810     endfunction
811    
812     augroup mcp_vim
813     autocmd!
814    
815     autocmd BufWritePost * call s:save (expand ('<afile>:p'))
816     autocmd BufUnload * call s:close (str2nr (expand ('<abuf>')))
817     autocmd VimLeavePre * call s:quit ()
818     augroup END
819