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