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