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