Skip to content

Commit 9bfa856

Browse files
committed
Copilot.vim 1.12.0
Now with support for partially accepting suggestions.
1 parent 309b3c8 commit 9bfa856

File tree

9 files changed

+435
-427
lines changed

9 files changed

+435
-427
lines changed

autoload/copilot.vim

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -459,11 +459,18 @@ function! copilot#Accept(...) abort
459459
let s = copilot#GetDisplayedSuggestion()
460460
if !empty(s.text)
461461
unlet! b:_copilot
462-
call copilot#Request('notifyAccepted', {'uuid': s.uuid})
462+
let text = ''
463+
if a:0 > 1
464+
let text = substitute(matchstr(s.text, "\n*" . '\%(' . a:2 .'\)'), "\n*$", '', '')
465+
endif
466+
if empty(text)
467+
let text = s.text
468+
endif
469+
call copilot#Request('notifyAccepted', {'uuid': s.uuid, 'acceptedLength': copilot#doc#UTF16Width(text)})
463470
call s:ClearPreview()
464-
let s:suggestion_text = s.text
471+
let s:suggestion_text = text
465472
return repeat("\<Left>\<Del>", s.outdentSize) . repeat("\<Del>", s.deleteSize) .
466-
\ "\<C-R>\<C-O>=copilot#TextQueuedForInsertion()\<CR>\<End>"
473+
\ "\<C-R>\<C-O>=copilot#TextQueuedForInsertion()\<CR>" . (a:0 > 1 ? '' : "\<End>")
467474
endif
468475
let default = get(g:, 'copilot_tab_fallback', pumvisible() ? "\<C-N>" : "\t")
469476
if !a:0
@@ -481,19 +488,27 @@ function! copilot#Accept(...) abort
481488
endif
482489
endfunction
483490

491+
function! copilot#AcceptWord(...) abort
492+
return copilot#Accept(a:0 ? a:1 : '', '\%(\k\@!.\)*\k*')
493+
endfunction
494+
495+
function! copilot#AcceptLine(...) abort
496+
return copilot#Accept(a:0 ? a:1 : "\r", "[^\n]\\+")
497+
endfunction
498+
484499
function! s:BrowserCallback(into, code) abort
485500
let a:into.code = a:code
486501
endfunction
487502

488503
function! copilot#Browser() abort
489504
if type(get(g:, 'copilot_browser')) == v:t_list
490505
return copy(g:copilot_browser)
506+
elseif type(get(g:, 'browser_command')) == v:t_list
507+
return copy(g:browser_command)
491508
elseif has('win32') && executable('rundll32')
492509
return ['rundll32', 'url.dll,FileProtocolHandler']
493510
elseif isdirectory('/private') && executable('/usr/bin/open')
494511
return ['/usr/bin/open']
495-
elseif executable('gio')
496-
return ['gio', 'open']
497512
elseif executable('xdg-open')
498513
return ['xdg-open']
499514
else

autoload/copilot/agent.vim

Lines changed: 39 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ let g:autoloaded_copilot_agent = 1
55

66
scriptencoding utf-8
77

8-
let s:plugin_version = '1.11.4'
8+
let s:plugin_version = '1.12.0'
99

1010
let s:error_exit = -1
1111

@@ -44,13 +44,9 @@ function! s:LogSend(request, line) abort
4444
return '--> ' . a:line
4545
endfunction
4646

47-
let s:chansend = function(exists('*chansend') ? 'chansend' : 'ch_sendraw')
4847
function! s:Send(agent, request) abort
49-
let request = extend({'jsonrpc': '2.0'}, a:request, 'keep')
50-
let body = json_encode(request)
51-
call s:chansend(a:agent.job, "Content-Length: " . len(body) . "\r\n\r\n" . body)
52-
call copilot#logger#Trace(function('s:LogSend', [request, body]))
53-
return request
48+
call ch_sendexpr(a:agent.job, a:request)
49+
return a:request
5450
endfunction
5551

5652
function! s:AgentNotify(method, params) dict abort
@@ -142,6 +138,12 @@ function! s:BufferText(bufnr) abort
142138
return join(getbufline(a:bufnr, 1, '$'), "\n") . "\n"
143139
endfunction
144140

141+
function! s:ShowMessageRequest(params) abort
142+
let choice = inputlist([a:params.message . "\n\nRequest Actions:"] +
143+
\ map(copy(get(a:params, 'actions', [])), { i, v -> (i + 1) . '. ' . v.title}))
144+
return choice > 0 ? get(a:params.actions, choice - 1, v:null) : v:null
145+
endfunction
146+
145147
function! s:AgentRequest(method, params, ...) dict abort
146148
let s:id += 1
147149
let request = {'method': a:method, 'params': deepcopy(a:params), 'id': s:id}
@@ -224,32 +226,28 @@ function! s:DispatchMessage(agent, handler, id, params, ...) abort
224226
endfunction
225227

226228
function! s:OnMessage(agent, body, ...) abort
227-
call copilot#logger#Trace({ -> '<-- ' . a:body})
228-
let response = json_decode(a:body)
229-
if type(response) != v:t_dict
230-
return
229+
if !has_key(a:body, 'method')
230+
return s:OnResponse(a:agent, a:body)
231+
endif
232+
let request = a:body
233+
let id = get(request, 'id', v:null)
234+
let params = get(request, 'params', v:null)
235+
if empty(id)
236+
if has_key(a:agent.notifications, request.method)
237+
call timer_start(0, { _ -> a:agent.notifications[request.method](params) })
238+
elseif request.method ==# 'LogMessage'
239+
call copilot#logger#Raw(get(params, 'level', 3), get(params, 'message', ''))
240+
endif
241+
elseif has_key(a:agent.methods, request.method)
242+
call timer_start(0, function('s:DispatchMessage', [a:agent, a:agent.methods[request.method], id, params]))
243+
else
244+
return s:Send(a:agent, {"id": id, "error": {"code": -32700, "message": "Method not found: " . request.method}})
231245
endif
232-
return s:OnResponse(a:agent, response)
233246
endfunction
234247

235248
function! s:OnResponse(agent, response, ...) abort
236249
let response = a:response
237-
let id = get(response, 'id', v:null)
238-
if has_key(response, 'method')
239-
let params = get(response, 'params', v:null)
240-
if empty(id)
241-
if has_key(a:agent.notifications, response.method)
242-
call timer_start(0, { _ -> a:agent.notifications[response.method](params) })
243-
elseif response.method ==# 'LogMessage'
244-
call copilot#logger#Raw(get(params, 'level', 3), get(params, 'message', ''))
245-
endif
246-
elseif has_key(a:agent.methods, response.method)
247-
call timer_start(0, function('s:DispatchMessage', [a:agent, a:agent.methods[response.method], id, params]))
248-
else
249-
return s:Send(a:agent, {"id": id, "error": {"code": -32700, "message": "Method not found: " . response.method}})
250-
endif
251-
return
252-
endif
250+
let id = get(a:response, 'id', v:null)
253251
if !has_key(a:agent.requests, id)
254252
return
255253
endif
@@ -275,44 +273,11 @@ function! s:OnResponse(agent, response, ...) abort
275273
endif
276274
endfunction
277275

278-
function! s:OnOut(agent, state, data) abort
279-
let a:state.buffer .= a:data
280-
while 1
281-
if a:state.mode ==# 'body'
282-
let content_length = a:state.headers['content-length']
283-
if strlen(a:state.buffer) >= content_length
284-
let headers = remove(a:state, 'headers')
285-
let a:state.mode = 'headers'
286-
let a:state.headers = {}
287-
let body = strpart(a:state.buffer, 0, content_length)
288-
let a:state.buffer = strpart(a:state.buffer, content_length)
289-
call timer_start(0, function('s:OnMessage', [a:agent, body]))
290-
else
291-
return
292-
endif
293-
elseif a:state.mode ==# 'headers' && a:state.buffer =~# "\n"
294-
let line = matchstr(a:state.buffer, "^.[^\n]*")
295-
let a:state.buffer = strpart(a:state.buffer, strlen(line) + 1)
296-
let match = matchlist(line, '^\([^:]\+\): \(.\{-\}\)\r$')
297-
if len(match)
298-
let a:state.headers[tolower(match[1])] = match[2]
299-
elseif line =~# "^\r\\=$"
300-
let a:state.mode = 'body'
301-
else
302-
call copilot#logger#Error("Invalid header: " . line)
303-
call a:agent.Close()
304-
endif
305-
else
306-
return
307-
endif
308-
endwhile
309-
endfunction
310-
311-
function! s:OnErr(agent, line) abort
276+
function! s:OnErr(agent, line, ...) abort
312277
call copilot#logger#Debug('<-! ' . a:line)
313278
endfunction
314279

315-
function! s:OnExit(agent, code) abort
280+
function! s:OnExit(agent, code, ...) abort
316281
let a:agent.exit_status = a:code
317282
if has_key(a:agent, 'job')
318283
call remove(a:agent, 'job')
@@ -384,11 +349,11 @@ function! s:LspNotify(method, params) dict abort
384349
return v:lua.require'_copilot'.rpc_notify(self.id, a:method, a:params)
385350
endfunction
386351

387-
function! copilot#agent#LspHandle(agent_id, response) abort
352+
function! copilot#agent#LspHandle(agent_id, request) abort
388353
if !has_key(s:instances, a:agent_id)
389354
return
390355
endif
391-
call s:OnResponse(s:instances[a:agent_id], a:response)
356+
call s:OnMessage(s:instances[a:agent_id], a:request)
392357
endfunction
393358

394359
function! s:GetNodeVersion(command) abort
@@ -550,10 +515,15 @@ function! copilot#agent#New(...) abort
550515
else
551516
let state = {'headers': {}, 'mode': 'headers', 'buffer': ''}
552517
let instance.open_buffers = {}
553-
let instance.job = copilot#job#Stream(command,
554-
\ function('s:OnOut', [instance, state]),
555-
\ function('s:OnErr', [instance]),
556-
\ function('s:OnExit', [instance]))
518+
let instance.methods = extend({'window/showMessageRequest': function('s:ShowMessageRequest')}, instance.methods)
519+
let instance.job = job_start(command, {
520+
\ 'cwd': copilot#job#Cwd(),
521+
\ 'in_mode': 'lsp',
522+
\ 'out_mode': 'lsp',
523+
\ 'out_cb': { j, d -> timer_start(0, function('s:OnMessage', [instance, d])) },
524+
\ 'err_cb': { j, d -> timer_start(0, function('s:OnErr', [instance, d])) },
525+
\ 'exit_cb': { j, d -> timer_start(0, function('s:OnExit', [instance, d])) },
526+
\ })
557527
let instance.id = exists('*jobpid') ? jobpid(instance.job) : job_info(instance.job).process
558528
let request = instance.Request('initialize', {'capabilities': {'workspace': {'workspaceFolders': v:true}}}, function('s:GetCapabilitiesResult'), function('s:GetCapabilitiesError'), instance)
559529
endif

autoload/copilot/job.vim

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,24 @@ function! s:NvimExitCallback(out_cb, err_cb, exit_cb, job, data, type) dict abor
7070
call a:exit_cb(a:data)
7171
endfunction
7272

73+
function! copilot#job#Cwd() abort
74+
let home = expand("~")
75+
if !isdirectory(home) && isdirectory($VIM)
76+
return $VIM
77+
endif
78+
return home
79+
endfunction
80+
7381
function! copilot#job#Stream(argv, out_cb, err_cb, ...) abort
7482
let exit_status = []
7583
let ExitCb = function(a:0 && !empty(a:1) ? a:1 : { e -> add(exit_status, e) }, a:000[2:-1])
7684
let OutCb = function(empty(a:out_cb) ? 'copilot#job#Nop' : a:out_cb, a:000[2:-1])
7785
let ErrCb = function(empty(a:err_cb) ? 'copilot#job#Nop' : a:err_cb, a:000[2:-1])
7886
let state = {'headers': {}, 'mode': 'headers', 'buffer': ''}
79-
let cwd = expand("~")
80-
if !isdirectory(cwd) && isdirectory($VIM)
81-
let cwd = $VIM
82-
endif
8387
if exists('*job_start')
8488
let result = {}
8589
let job = job_start(a:argv, {
86-
\ 'cwd': cwd,
90+
\ 'cwd': copilot#job#Cwd(),
8791
\ 'out_mode': 'raw',
8892
\ 'out_cb': { j, d -> OutCb(d) },
8993
\ 'err_cb': { j, d -> ErrCb(d) },
@@ -92,7 +96,7 @@ function! copilot#job#Stream(argv, out_cb, err_cb, ...) abort
9296
\ })
9397
else
9498
let jopts = {
95-
\ 'cwd': cwd,
99+
\ 'cwd': copilot#job#Cwd(),
96100
\ 'stderr': [''],
97101
\ 'on_stdout': { j, d, t -> OutCb(join(d, "\n")) },
98102
\ 'on_stderr': function('s:NvimCallback', [ErrCb]),

autoload/copilot/panel.vim

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,13 @@ function! copilot#panel#Accept(...) abort
8585
return 'echoerr "Buffer has changed since synthesizing solution"'
8686
endif
8787
let lines = split(solution.displayText, "\n", 1)
88+
let old_first = getline(solution.range.start.line + 1)
89+
let lines[0] = strpart(old_first, 0, copilot#doc#UTF16ToByteIdx(old_first, solution.range.start.character)) . lines[0]
90+
let old_last = getline(solution.range.end.line + 1)
91+
let lines[-1] .= strpart(old_last, copilot#doc#UTF16ToByteIdx(old_last, solution.range.start.character))
8892
call setbufline(state.bufnr, solution.range.start.line + 1, lines[0])
8993
call appendbufline(state.bufnr, solution.range.start.line + 1, lines[1:-1])
94+
call copilot#Request('notifyAccepted', {'uuid': solution.solutionId})
9095
bwipeout
9196
let win = bufwinnr(state.bufnr)
9297
if win > 0

dist/agent.js

Lines changed: 317 additions & 340 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/agent.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/copilot.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,33 @@ copilot#Accept(). Here's an example with CTRL-J:
104104
imap <silent><script><expr> <C-J> copilot#Accept("\<CR>")
105105
let g:copilot_no_tab_map = v:true
106106
<
107+
Lua version:
108+
>
109+
vim.keymap.set('i', '<C-J>', 'copilot#Accept("\<CR>")', {
110+
expr = true,
111+
replace_keycodes = false
112+
})
113+
vim.g.copilot_no_tab_map = true
114+
<
107115
The argument to copilot#Accept() is the fallback for when no suggestion is
108116
displayed. In this example, a regular carriage return is used. If no
109117
fallback is desired, use an argument of "" (an empty string).
110118

111119
Other Maps ~
112120

121+
Note that M- (a.k.a. meta or alt) maps are highly dependent on your terminal
122+
to function correctly and may be unsupported with your setup. As an
123+
alternative, you can create your own versions that invoke the <Plug> maps
124+
instead. Here's an example that maps CTRL-L to accept one word of the
125+
current suggestion:
126+
>
127+
imap <C-L> <Plug>(copilot-accept-word)
128+
<
129+
Lua version:
130+
>
131+
vim.keymap.set('i', '<C-L>', '<Plug>(copilot-accept-word)')
132+
<
133+
113134
*copilot-i_CTRL-]*
114135
<C-]> Dismiss the current suggestion.
115136
<Plug>(copilot-dismiss)
@@ -126,6 +147,15 @@ Other Maps ~
126147
<M-\> Explicitly request a suggestion, even if Copilot
127148
<Plug>(copilot-suggest) is disabled.
128149

150+
*copilot-i_ALT-Right*
151+
<M-Right> Accept the next word of the current suggestion.
152+
<Plug>(copilot-accept-word)
153+
154+
*copilot-i_ALT-CTRL-Right*
155+
156+
<M-C-Right> Accept the next line of the current suggestion.
157+
<Plug>(copilot-accept-line)
158+
129159
SYNTAX HIGHLIGHTING *copilot-highlighting*
130160

131161
Inline suggestions are highlighted using the CopilotSuggestion group,

lua/_copilot.lua

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ copilot.lsp_start_client = function(cmd, handler_names)
1313
end
1414
id = vim.lsp.start_client({
1515
cmd = cmd,
16-
cmd_cwd = vim.fn.expand('~'),
16+
cmd_cwd = vim.call('copilot#job#Cwd'),
1717
name = 'copilot',
1818
handlers = handlers,
1919
get_language_id = function(bufnr, filetype)
@@ -35,10 +35,9 @@ copilot.lsp_request = function(client_id, method, params)
3535
local client = vim.lsp.get_client_by_id(client_id)
3636
if not client then return end
3737
vim.lsp.buf_attach_client(0, client_id)
38-
local bufnr
3938
for _, doc in ipairs({params.doc, params.textDocument}) do
4039
if doc and type(doc.uri) == 'number' then
41-
bufnr = doc.uri
40+
local bufnr = doc.uri
4241
vim.lsp.buf_attach_client(bufnr, client_id)
4342
doc.uri = vim.uri_from_bufnr(bufnr)
4443
doc.version = vim.lsp.util.buf_versions[bufnr]
@@ -47,7 +46,7 @@ copilot.lsp_request = function(client_id, method, params)
4746
local _, id
4847
_, id = client.request(method, params, function(err, result)
4948
vim.call('copilot#agent#LspResponse', client_id, {id = id, error = err, result = result})
50-
end, bufnr)
49+
end)
5150
return id
5251
end
5352

plugin/copilot.vim

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ if !get(g:, 'copilot_no_maps')
7575
imap <Plug>(copilot-next) <Cmd>call copilot#Next()<CR>
7676
imap <Plug>(copilot-previous) <Cmd>call copilot#Previous()<CR>
7777
imap <Plug>(copilot-suggest) <Cmd>call copilot#Suggest()<CR>
78+
imap <script><silent><nowait><expr> <Plug>(copilot-accept-word) copilot#AcceptWord()
79+
imap <script><silent><nowait><expr> <Plug>(copilot-accept-line) copilot#AcceptLine()
7880
try
7981
if !has('nvim') && &encoding ==# 'utf-8'
8082
" avoid 8-bit meta collision with UTF-8 characters
@@ -90,6 +92,12 @@ if !get(g:, 'copilot_no_maps')
9092
if empty(mapcheck('<M-Bslash>', 'i'))
9193
imap <M-Bslash> <Plug>(copilot-suggest)
9294
endif
95+
if empty(mapcheck('<M-Right>', 'i'))
96+
imap <M-Right> <Plug>(copilot-accept-word)
97+
endif
98+
if empty(mapcheck('<M-Down>', 'i'))
99+
imap <M-Down> <Plug>(copilot-accept-line)
100+
endif
93101
finally
94102
if exists('s:restore_encoding')
95103
set encoding=utf-8

0 commit comments

Comments
 (0)