ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/AnyEvent-MCP/vim/plugin/mcp-vim.vim
Revision: 1.11
Committed: Sat Oct 24 12:35:50 2026 UTC (2 days, 15 hours ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.10: +7 -0 lines
Log Message:
*** empty log message ***

File Contents

# Content
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 " 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 "
9 " Installation:
10 " In the Perl source distribution: vim/plugin/mcp-vim.vim
11 " Published URL: https://dist.schmorp.de/mcp-vim/1/mcp-vim.vim
12 " 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 call ch_sendraw (s:channel, json_encode (['mcp-vim', cookie, s:prot_major, s:prot_minor, id, s:instance_id]) . "\n")
117
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 " vim -> broker: ['reply', id, success, result] " result is an exception string on failure
143 "
144 " Methods:
145 " open (path) " add/load a listed buffer without selecting it
146 " 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 " undo (buffer, expected_changedtick)
153 " redo (buffer, expected_changedtick)
154 " update (buffer, expected_changedtick) " save to current filename if modified
155 " set_cursor (buffer, line, col [, expected_changedtick])
156 " ex (command [, buffer, expected_changedtick]) " unrestricted; broker checks exec permission
157 "
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 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 " execute() captures messages without suppressing command errors.
329 " 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 return {'output': execute (a:command)}
342 endfunction
343
344 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 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 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 " 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 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 " 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 let new_lines = readfile (newfile, 'b')
457
458 if !empty (new_lines)
459 if new_lines[-1] !=# ''
460 throw 'rpc/patch_missing_eol'
461 endif
462 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 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 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 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 call s:rpc_enter_buffer (a:buf, info)
792
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 ==# 'update-metadata'
849 for session in values (s:sessions)
850 if session.session ==# a:msg[1]
851 call extend (session.state, a:msg[2])
852 endif
853 endfor
854
855 elseif cmd ==# 'ex'
856 execute a:msg[1]
857
858 elseif cmd ==# 'echo'
859 if s:popup
860 call popup_close (s:popup)
861 endif
862 echom 'mcp-vim: ' . a:msg[1]
863
864 elseif cmd ==# 'error'
865 if s:popup
866 call popup_close (s:popup)
867 endif
868 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] })
869
870 elseif cmd ==# 'goto'
871 for b in keys (s:sessions)
872 if s:sessions[b].session ==# a:msg[1]
873 execute 'buffer ' . b
874 call cursor (a:msg[2], a:msg[3])
875 break
876 endif
877 endfor
878
879 else
880 echom 'mcp-vim: unknown command ' . string (cmd)
881 endif
882 endfunction
883
884 function! s:save (file, force) abort
885 if type (s:channel) != v:t_channel
886 return
887 endif
888
889 let b = bufnr ('%')
890
891 if has_key (s:sessions, b) && a:file ==# s:sessions[b].path
892 call ch_sendexpr (s:channel, ['save', s:sessions[b].session, a:force])
893
894 let data = readblob (s:sessions[b].path)
895 call ch_sendexpr (s:channel, ['data', base64_encode (data)])
896 call ch_sendexpr (s:channel, ['eof', 1])
897
898 endif
899 endfunction
900
901 function! s:close (buf) abort
902 if has_key (s:sessions, a:buf)
903 call delete (s:sessions[a:buf].path)
904 if type (s:channel) == v:t_channel
905 call ch_sendexpr (s:channel, ['close', s:sessions[a:buf].session])
906 endif
907 call remove (s:sessions, a:buf)
908 endif
909 endfunction
910
911 augroup mcp_vim
912 autocmd!
913
914 autocmd BufWritePost * call s:save (expand ('<afile>:p'), v:cmdbang)
915 autocmd BufUnload * call s:close (str2nr (expand ('<abuf>')))
916 autocmd VimLeavePre * call s:quit ()
917 augroup END
918