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