453 changed files with 132617 additions and 74499 deletions
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
aw-watcher-vim |
||||
============== |
||||
|
||||
### Installation |
||||
|
||||
This plugin depends on curl, so make sure that it's installed and available in your PATH |
||||
|
||||
It is recommended to have a vim runtime manager to make it easier to install (such as Pathogen or Vundle) |
||||
|
||||
Then simply clone this repository to the bundle folder in your vim config folder (usually `~/.vim/bundle` or `~/.config/nvim/bundle` for neovim) |
||||
|
||||
### Usage |
||||
|
||||
Once installed in the bundle directory, it should load automatically if you have a vim runtime manager |
||||
|
||||
``` |
||||
:AWStart - start logging if not already logging |
||||
:AWStop - stop logging if logging |
||||
:AWStatus - verify that the watcher is running |
||||
``` |
||||
|
||||
If aw-watcher-vim loses connection it will give you an error message and stop logging. You then need to either run :AWStart or restart vim to start logging again |
||||
|
||||
### Configuration |
||||
|
||||
The following global variables are available: |
||||
|
||||
| Variable Name | Description | Default Value | |
||||
|--------------------|------------------------------------------------|---------------| |
||||
| `g:aw_apiurl_host` | Sets the _host_ of the Api Url | `127.0.0.1` | |
||||
| `g:aw_apiurl_port` | Sets the _port_ of the Api Url | `5600` | |
||||
| `g:aw_api_timeout` | Sets the _timeout_ seconds of the Api request | `2.0` | |
@ -0,0 +1,163 @@
@@ -0,0 +1,163 @@
|
||||
if exists("g:loaded_activitywatch") |
||||
finish |
||||
endif |
||||
let g:loaded_activitywatch = 1 |
||||
|
||||
" compatibility mode which set this script to run with default vim settings |
||||
let s:save_cpo = &cpo |
||||
set cpo&vim |
||||
|
||||
let s:nvim = has('nvim') |
||||
|
||||
let s:last_heartbeat = localtime() |
||||
let s:file = '' |
||||
let s:language = '' |
||||
let s:project = '' |
||||
|
||||
let s:connected = 0 |
||||
let s:apiurl_host = get(g:, 'aw_apiurl_host', '127.0.0.1') |
||||
let s:apiurl_port = get(g:, 'aw_apiurl_port', '5600') |
||||
let s:api_timeout = get(g:, 'aw_api_timeout', 2) |
||||
let s:base_apiurl = printf('http://%s:%s/api/0', s:apiurl_host, s:apiurl_port) |
||||
let s:hostname = get(g:, 'aw_hostname', hostname()) |
||||
let s:bucketname = printf('aw-watcher-vim_%s', s:hostname) |
||||
let s:bucket_apiurl = printf('%s/buckets/%s', s:base_apiurl, s:bucketname) |
||||
let s:heartbeat_apiurl = printf('%s/heartbeat?pulsetime=30', s:bucket_apiurl) |
||||
|
||||
" dict of all responses |
||||
" the key is the jobid and the value the HTTP status code |
||||
let s:http_response_code = {} |
||||
|
||||
function! HTTPPostJson(url, data) |
||||
let l:req = ['curl', '-s', a:url, |
||||
\ '-H', 'Content-Type: application/json', |
||||
\ '-X', 'POST', |
||||
\ '-d', json_encode(a:data), |
||||
\ '-o', '/dev/null', |
||||
\ '-m', s:api_timeout, |
||||
\ '-w', "%{http_code}"] |
||||
if s:nvim |
||||
let l:req_job = jobstart(l:req, |
||||
\ {"detach": 1, |
||||
\ "on_stdout": "HTTPPostOnStdoutNeovim", |
||||
\ "on_exit": "HTTPPostOnExitNeovim", |
||||
\ }) |
||||
else |
||||
let l:req_job = job_start(l:req, |
||||
\ {"out_cb": "HTTPPostOnStdoutVim", |
||||
\ "close_cb": "HTTPPostOnExitVim", |
||||
\ "in_mode": "raw", |
||||
\ }) |
||||
endif |
||||
endfunc |
||||
|
||||
function! HTTPPostOnExitNeovim(jobid, exitcode, eventtype) |
||||
let l:jobid_str = printf('%d', a:jobid) |
||||
let l:status_code = str2nr(s:http_response_code[l:jobid_str][0]) |
||||
call HTTPPostOnExit(l:jobid_str, l:status_code) |
||||
endfunc |
||||
|
||||
function! HTTPPostOnExitVim(jobmsg) |
||||
" cut out channelnum from string 'channel X running' |
||||
let l:jobid_str = substitute(a:jobmsg, '[ A-Za-z]*', '', "g") |
||||
let l:status_code = str2nr(s:http_response_code[l:jobid_str]) |
||||
call HTTPPostOnExit(l:jobid_str, l:status_code) |
||||
endfunc |
||||
|
||||
function! HTTPPostOnExit(jobid_str, status_code) |
||||
if a:status_code == 0 |
||||
" We cannot connect to aw-server |
||||
echoerr "aw-watcher-vim: Failed to connect to aw-server, logging will be disabled. You can retry to connect with ':AWStart'" |
||||
let s:connected = 0 |
||||
elseif a:status_code >= 100 && a:status_code < 300 || a:status_code == 304 |
||||
" We are connected! |
||||
let s:connected = 1 |
||||
else |
||||
" aw-server didn't like our request |
||||
echoerr printf("aw-watcher-vim: aw-server did not accept our request with status code %d. See aw-server logs for reason or stop aw-watcher-vim with :AWStop", a:status_code) |
||||
endif |
||||
" Cleanup response code |
||||
unlet s:http_response_code[a:jobid_str] |
||||
endfunc |
||||
|
||||
function! HTTPPostOnStdoutVim(jobmsg, data) |
||||
" cut out channelnum from string 'channel X running' |
||||
let l:jobid_str = substitute(a:jobmsg, '[ A-Za-z]*', '', "g") |
||||
let s:http_response_code[l:jobid_str] = a:data |
||||
"echo printf('aw-watcher-vim job %d stdout: %s', l:jobid_str, json_encode(a:data)) |
||||
endfunc |
||||
|
||||
function! HTTPPostOnStdoutNeovim(jobid, data, event) |
||||
if a:data != [''] |
||||
let l:jobid_str = printf('%d', a:jobid) |
||||
let s:http_response_code[l:jobid_str] = a:data |
||||
"echo printf('aw-watcher-vim job %d stdout: %s', a:jobid, json_encode(a:data)) |
||||
endif |
||||
endfunc |
||||
|
||||
function! s:CreateBucket() |
||||
let l:body = { |
||||
\ 'name': s:bucketname, |
||||
\ 'hostname': s:hostname, |
||||
\ 'client': 'aw-watcher-vim', |
||||
\ 'type': 'app.editor.activity' |
||||
\} |
||||
call HTTPPostJson(s:bucket_apiurl, l:body) |
||||
endfunc |
||||
|
||||
function! s:Heartbeat() |
||||
" Only send heartbeats if we can connect to aw-server |
||||
if s:connected < 1 |
||||
return |
||||
endif |
||||
let l:duration = 0 |
||||
let l:localtime = localtime() |
||||
let l:timestamp = strftime('%FT%H:%M:%S%z') |
||||
let l:file = expand('%p') |
||||
let l:language = &filetype |
||||
let l:project = getcwd() |
||||
" Only send heartbeat if data was changed or more than 1 second has passed |
||||
" since last heartbeat |
||||
if s:file != l:file || |
||||
\ s:language != l:language || |
||||
\ s:project != l:project || |
||||
\ l:localtime - s:last_heartbeat > 1 |
||||
|
||||
let l:req_body = { |
||||
\ 'duration': 0, |
||||
\ 'timestamp': l:timestamp, |
||||
\ 'data': { |
||||
\ 'file': l:file, |
||||
\ 'language': l:language, |
||||
\ 'project': l:project |
||||
\ } |
||||
\} |
||||
call HTTPPostJson(s:heartbeat_apiurl, l:req_body) |
||||
let s:file = l:file |
||||
let s:language = l:language |
||||
let s:project = l:project |
||||
let s:last_heartbeat = l:localtime |
||||
endif |
||||
endfunc |
||||
|
||||
function! AWStart() |
||||
call s:CreateBucket() |
||||
endfunc |
||||
|
||||
function! AWStop() |
||||
let s:connected = 0 |
||||
endfunc |
||||
|
||||
augroup ActivityWatch |
||||
autocmd VimEnter * call AWStart() |
||||
autocmd BufEnter,CursorMoved,CursorMovedI * call s:Heartbeat() |
||||
autocmd CmdlineEnter,CmdlineChanged * call s:Heartbeat() |
||||
augroup END |
||||
|
||||
command! AWHeartbeat call s:Heartbeat() |
||||
command! AWStart call AWStart() |
||||
command! AWStop call AWStop() |
||||
command! AWStatus echom printf('aw-watcher-vim running: %b', s:connected) |
||||
|
||||
" reset compatibility mode |
||||
let &cpo = s:save_cpo |
@ -0,0 +1,667 @@
@@ -0,0 +1,667 @@
|
||||
scriptencoding utf-8 |
||||
let s:is_vim = !has('nvim') |
||||
let s:root = expand('<sfile>:h:h:h') |
||||
let s:prompt_win_bufnr = 0 |
||||
let s:list_win_bufnr = 0 |
||||
let s:prompt_win_width = get(g:, 'coc_prompt_win_width', 32) |
||||
let s:frames = ['· ', '·· ', '···', ' ··', ' ·', ' '] |
||||
let s:sign_group = 'PopUpCocDialog' |
||||
let s:detail_bufnr = 0 |
||||
|
||||
" Float window aside pum |
||||
function! coc#dialog#create_pum_float(lines, config) abort |
||||
let winid = coc#float#get_float_by_kind('pumdetail') |
||||
if empty(a:lines) || !coc#pum#visible() |
||||
if winid |
||||
call coc#float#close(winid) |
||||
endif |
||||
return |
||||
endif |
||||
let pumbounding = coc#pum#info() |
||||
let border = get(a:config, 'border', []) |
||||
let pw = pumbounding['width'] + (pumbounding['border'] ? 0 : get(pumbounding, 'scrollbar', 0)) |
||||
let rp = &columns - pumbounding['col'] - pw |
||||
let showRight = pumbounding['col'] > rp ? 0 : 1 |
||||
let maxWidth = showRight ? coc#math#min(rp - 1, a:config['maxWidth']) : coc#math#min(pumbounding['col'] - 1, a:config['maxWidth']) |
||||
let bh = get(border, 0 ,0) + get(border, 2, 0) |
||||
let maxHeight = &lines - pumbounding['row'] - &cmdheight - 1 - bh |
||||
if maxWidth <= 2 || maxHeight < 1 |
||||
return v:null |
||||
endif |
||||
let width = 0 |
||||
for line in a:lines |
||||
let dw = max([1, strdisplaywidth(line)]) |
||||
let width = max([width, dw + 2]) |
||||
endfor |
||||
let width = float2nr(coc#math#min(maxWidth, width)) |
||||
let ch = coc#string#content_height(a:lines, width - 2) |
||||
let height = float2nr(coc#math#min(maxHeight, ch)) |
||||
let lines = map(a:lines, {_, s -> s =~# '^─' ? repeat('─', width - 2 + (s:is_vim && ch > height ? -1 : 0)) : s}) |
||||
let opts = { |
||||
\ 'lines': lines, |
||||
\ 'highlights': get(a:config, 'highlights', []), |
||||
\ 'relative': 'editor', |
||||
\ 'col': showRight ? pumbounding['col'] + pw : pumbounding['col'] - width, |
||||
\ 'row': pumbounding['row'], |
||||
\ 'height': height, |
||||
\ 'width': width - 2 + (s:is_vim && ch > height ? -1 : 0), |
||||
\ 'scrollinside': showRight ? 0 : 1, |
||||
\ 'codes': get(a:config, 'codes', []), |
||||
\ } |
||||
for key in ['border', 'highlight', 'borderhighlight', 'winblend', 'focusable', 'shadow', 'rounded'] |
||||
if has_key(a:config, key) |
||||
let opts[key] = a:config[key] |
||||
endif |
||||
endfor |
||||
call s:close_auto_hide_wins(winid) |
||||
let result = coc#float#create_float_win(winid, s:detail_bufnr, opts) |
||||
if empty(result) |
||||
return |
||||
endif |
||||
let s:detail_bufnr = result[1] |
||||
call setwinvar(result[0], 'kind', 'pumdetail') |
||||
if !s:is_vim |
||||
call coc#float#nvim_scrollbar(result[0]) |
||||
endif |
||||
endfunction |
||||
|
||||
" Float window below/above cursor |
||||
function! coc#dialog#create_cursor_float(winid, bufnr, lines, config) abort |
||||
if coc#prompt#activated() |
||||
return v:null |
||||
endif |
||||
let pumAlignTop = get(a:config, 'pumAlignTop', 0) |
||||
let modes = get(a:config, 'modes', ['n', 'i', 'ic', 's']) |
||||
let mode = mode() |
||||
let currbuf = bufnr('%') |
||||
let pos = [line('.'), col('.')] |
||||
if index(modes, mode) == -1 |
||||
return v:null |
||||
endif |
||||
if !s:is_vim && !has('nvim-0.5.0') && mode ==# 'i' |
||||
" helps to fix undo issue, don't know why. |
||||
call feedkeys("\<C-g>u", 'n') |
||||
endif |
||||
if mode ==# 's' && has('patch-8.2.4969') && !has('patch-8.2.4996') |
||||
echohl WarningMsg | echon 'Popup not created to avoid issue #10466 on vim >= 8.2.4969' | echohl None |
||||
return v:null |
||||
endif |
||||
let dimension = coc#dialog#get_config_cursor(a:lines, a:config) |
||||
if empty(dimension) |
||||
return v:null |
||||
endif |
||||
if coc#pum#visible() && ((pumAlignTop && dimension['row'] <0)|| (!pumAlignTop && dimension['row'] > 0)) |
||||
return v:null |
||||
endif |
||||
let width = dimension['width'] |
||||
let lines = map(a:lines, {_, s -> s =~# '^─' ? repeat('─', width) : s}) |
||||
let config = extend(extend({'lines': lines, 'relative': 'cursor'}, a:config), dimension) |
||||
call s:close_auto_hide_wins(a:winid) |
||||
let res = coc#float#create_float_win(a:winid, a:bufnr, config) |
||||
if empty(res) |
||||
return v:null |
||||
endif |
||||
let alignTop = dimension['row'] < 0 |
||||
let winid = res[0] |
||||
let bufnr = res[1] |
||||
redraw |
||||
if has('nvim') |
||||
call coc#float#nvim_scrollbar(winid) |
||||
endif |
||||
return [currbuf, pos, winid, bufnr, alignTop] |
||||
endfunction |
||||
|
||||
" Create float window for input |
||||
function! coc#dialog#create_prompt_win(title, default, opts) abort |
||||
call s:close_auto_hide_wins() |
||||
let bufnr = has('nvim') ? s:prompt_win_bufnr : 0 |
||||
if s:is_vim |
||||
execute 'hi link CocPopupTerminal '.get(a:opts, 'highlight', 'CocFloating') |
||||
let node = expand(get(g:, 'coc_node_path', 'node')) |
||||
let bufnr = term_start([node, s:root . '/bin/prompt.js', a:default], { |
||||
\ 'term_rows': 1, |
||||
\ 'term_highlight': 'CocPopupTerminal', |
||||
\ 'hidden': 1, |
||||
\ 'term_finish': 'close' |
||||
\ }) |
||||
call term_setapi(bufnr, 'Coc') |
||||
call setbufvar(bufnr, 'current', type(a:default) == v:t_string ? a:default : '') |
||||
endif |
||||
let config = s:get_prompt_dimension(a:title, a:default, a:opts) |
||||
let res = coc#float#create_float_win(0, bufnr, extend(config, { |
||||
\ 'style': 'minimal', |
||||
\ 'border': get(a:opts, 'border', [1,1,1,1]), |
||||
\ 'rounded': get(a:opts, 'rounded', 1), |
||||
\ 'prompt': 1, |
||||
\ 'title': a:title, |
||||
\ 'lines': s:is_vim ? v:null : [a:default], |
||||
\ 'highlight': get(a:opts, 'highlight', 'CocFloating'), |
||||
\ 'borderhighlight': [get(a:opts, 'borderhighlight', 'CocFloating')], |
||||
\ })) |
||||
if empty(res) |
||||
return |
||||
endif |
||||
let winid = res[0] |
||||
let bufnr = res[1] |
||||
if has('nvim') |
||||
let s:prompt_win_bufnr = res[1] |
||||
call sign_unplace(s:sign_group, { 'buffer': s:prompt_win_bufnr }) |
||||
call nvim_set_current_win(winid) |
||||
inoremap <buffer> <C-a> <Home> |
||||
inoremap <buffer><expr><C-e> pumvisible() ? "\<C-e>" : "\<End>" |
||||
exe 'imap <silent><nowait><buffer> <esc> <esc><esc>' |
||||
exe 'nnoremap <silent><buffer> <esc> :call coc#float#close('.winid.')<CR>' |
||||
exe 'inoremap <silent><expr><nowait><buffer> <cr> "\<C-r>=coc#dialog#prompt_insert(getline(''.''))\<cr>\<esc>"' |
||||
if get(a:opts, 'list', 0) |
||||
for key in ['<C-j>', '<C-k>', '<C-n>', '<C-p>', '<up>', '<down>', '<C-f>', '<C-b>', '<C-space>'] |
||||
" Can't use < in remap |
||||
let escaped = key ==# '<C-space>' ? "C-@" : strcharpart(key, 1, strchars(key) - 2) |
||||
exe 'inoremap <nowait><buffer> '.key.' <Cmd>call coc#rpc#notify("PromptKeyPress", ['.bufnr.', "'.escaped.'"])<CR>' |
||||
endfor |
||||
endif |
||||
call feedkeys('A', 'in') |
||||
endif |
||||
call coc#util#do_autocmd('CocOpenFloatPrompt') |
||||
if s:is_vim |
||||
let pos = popup_getpos(winid) |
||||
" width height row col |
||||
let dimension = [pos['width'], pos['height'], pos['line'] - 1, pos['col'] - 1] |
||||
else |
||||
let id = coc#float#get_related(winid, 'border') |
||||
if !has('nvim-0.6.0') |
||||
redraw |
||||
endif |
||||
let pos = nvim_win_get_position(id) |
||||
let dimension = [nvim_win_get_width(id), nvim_win_get_height(id), pos[0], pos[1]] |
||||
endif |
||||
return [bufnr, winid, dimension] |
||||
endfunction |
||||
|
||||
" Create list window under target window |
||||
function! coc#dialog#create_list(target, dimension, opts) abort |
||||
let maxHeight = get(a:opts, 'maxHeight', 10) |
||||
let height = max([1, len(get(a:opts, 'lines', []))]) |
||||
let height = min([maxHeight, height, &lines - &cmdheight - 1 - a:dimension['row'] + a:dimension['height']]) |
||||
let chars = get(a:opts, 'rounded', 1) ? ['╯', '╰'] : ['┘', '└'] |
||||
let config = extend(copy(a:opts), { |
||||
\ 'relative': 'editor', |
||||
\ 'row': a:dimension['row'] + a:dimension['height'], |
||||
\ 'col': a:dimension['col'], |
||||
\ 'width': a:dimension['width'] - 2, |
||||
\ 'height': height, |
||||
\ 'border': [1, 1, 1, 1], |
||||
\ 'scrollinside': 1, |
||||
\ 'borderchars': extend(['─', '│', '─', '│', '├', '┤'], chars) |
||||
\ }) |
||||
let bufnr = 0 |
||||
let result = coc#float#create_float_win(0, s:list_win_bufnr, config) |
||||
if empty(result) |
||||
return |
||||
endif |
||||
let winid = result[0] |
||||
call coc#float#add_related(winid, a:target) |
||||
call setwinvar(winid, 'auto_height', get(a:opts, 'autoHeight', 1)) |
||||
call setwinvar(winid, 'max_height', maxHeight) |
||||
call setwinvar(winid, 'target_winid', a:target) |
||||
call setwinvar(winid, 'kind', 'list') |
||||
call coc#dialog#check_scroll_vim(a:target) |
||||
return result |
||||
endfunction |
||||
|
||||
" Create menu picker for pick single item |
||||
function! coc#dialog#create_menu(lines, config) abort |
||||
call s:close_auto_hide_wins() |
||||
let highlight = get(a:config, 'highlight', 'CocFloating') |
||||
let borderhighlight = get(a:config, 'borderhighlight', [highlight]) |
||||
let relative = get(a:config, 'relative', 'cursor') |
||||
let lines = copy(a:lines) |
||||
let content = get(a:config, 'content', '') |
||||
let maxWidth = get(a:config, 'maxWidth', 80) |
||||
let highlights = get(a:config, 'highlights', []) |
||||
let contentCount = 0 |
||||
if !empty(content) |
||||
let contentLines = coc#string#reflow(split(content, '\r\?\n'), maxWidth) |
||||
let contentCount = len(contentLines) |
||||
let lines = extend(contentLines, lines) |
||||
if !empty(highlights) |
||||
for item in highlights |
||||
let item['lnum'] = item['lnum'] + contentCount |
||||
endfor |
||||
endif |
||||
endif |
||||
let opts = { |
||||
\ 'lines': lines, |
||||
\ 'highlight': highlight, |
||||
\ 'title': get(a:config, 'title', ''), |
||||
\ 'borderhighlight': borderhighlight, |
||||
\ 'maxWidth': maxWidth, |
||||
\ 'maxHeight': get(a:config, 'maxHeight', 80), |
||||
\ 'rounded': get(a:config, 'rounded', 0), |
||||
\ 'border': [1, 1, 1, 1], |
||||
\ 'highlights': highlights, |
||||
\ 'relative': relative, |
||||
\ } |
||||
if relative ==# 'editor' |
||||
let dimension = coc#dialog#get_config_editor(lines, opts) |
||||
else |
||||
let dimension = coc#dialog#get_config_cursor(lines, opts) |
||||
endif |
||||
call extend(opts, dimension) |
||||
let ids = coc#float#create_float_win(0, s:prompt_win_bufnr, opts) |
||||
if empty(ids) |
||||
return |
||||
endif |
||||
let s:prompt_win_bufnr = ids[1] |
||||
call coc#dialog#set_cursor(ids[0], ids[1], contentCount + 1) |
||||
redraw |
||||
if has('nvim') |
||||
call coc#float#nvim_scrollbar(ids[0]) |
||||
endif |
||||
return [ids[0], ids[1], contentCount] |
||||
endfunction |
||||
|
||||
" Create dialog at center of screen |
||||
function! coc#dialog#create_dialog(lines, config) abort |
||||
call s:close_auto_hide_wins() |
||||
" dialog always have borders |
||||
let title = get(a:config, 'title', '') |
||||
let buttons = get(a:config, 'buttons', []) |
||||
let highlight = get(a:config, 'highlight', 'CocFloating') |
||||
let borderhighlight = get(a:config, 'borderhighlight', [highlight]) |
||||
let opts = { |
||||
\ 'title': title, |
||||
\ 'rounded': get(a:config, 'rounded', 0), |
||||
\ 'relative': 'editor', |
||||
\ 'border': [1,1,1,1], |
||||
\ 'close': get(a:config, 'close', 1), |
||||
\ 'highlight': highlight, |
||||
\ 'highlights': get(a:config, 'highlights', []), |
||||
\ 'buttons': buttons, |
||||
\ 'borderhighlight': borderhighlight, |
||||
\ 'getchar': get(a:config, 'getchar', 0) |
||||
\ } |
||||
call extend(opts, coc#dialog#get_config_editor(a:lines, a:config)) |
||||
let bufnr = coc#float#create_buf(0, a:lines) |
||||
let res = coc#float#create_float_win(0, bufnr, opts) |
||||
if empty(res) |
||||
return |
||||
endif |
||||
if get(a:config, 'cursorline', 0) |
||||
call coc#dialog#place_sign(bufnr, 1) |
||||
endif |
||||
if has('nvim') |
||||
redraw |
||||
call coc#float#nvim_scrollbar(res[0]) |
||||
endif |
||||
return res |
||||
endfunction |
||||
|
||||
function! coc#dialog#prompt_confirm(title, cb) abort |
||||
call s:close_auto_hide_wins() |
||||
if s:is_vim && exists('*popup_dialog') |
||||
try |
||||
call popup_dialog(a:title. ' (y/n)?', { |
||||
\ 'highlight': 'Normal', |
||||
\ 'filter': 'popup_filter_yesno', |
||||
\ 'callback': {id, res -> a:cb(v:null, res)}, |
||||
\ 'borderchars': get(g:, 'coc_borderchars', ['─', '│', '─', '│', '╭', '╮', '╯', '╰']), |
||||
\ 'borderhighlight': ['MoreMsg'] |
||||
\ }) |
||||
catch /.*/ |
||||
call a:cb(v:exception) |
||||
endtry |
||||
return |
||||
endif |
||||
let text = ' '. a:title . ' (y/n)? ' |
||||
let maxWidth = coc#math#min(78, &columns - 2) |
||||
let width = coc#math#min(maxWidth, strdisplaywidth(text)) |
||||
let maxHeight = &lines - &cmdheight - 1 |
||||
let height = coc#math#min(maxHeight, float2nr(ceil(str2float(string(strdisplaywidth(text)))/width))) |
||||
let arr = coc#float#create_float_win(0, s:prompt_win_bufnr, { |
||||
\ 'col': &columns/2 - width/2 - 1, |
||||
\ 'row': maxHeight/2 - height/2 - 1, |
||||
\ 'width': width, |
||||
\ 'height': height, |
||||
\ 'border': [1,1,1,1], |
||||
\ 'focusable': v:false, |
||||
\ 'relative': 'editor', |
||||
\ 'highlight': 'Normal', |
||||
\ 'borderhighlight': 'MoreMsg', |
||||
\ 'style': 'minimal', |
||||
\ 'lines': [text], |
||||
\ }) |
||||
if empty(arr) |
||||
call a:cb('Window create failed!') |
||||
return |
||||
endif |
||||
let winid = arr[0] |
||||
let s:prompt_win_bufnr = arr[1] |
||||
call sign_unplace(s:sign_group, { 'buffer': s:prompt_win_bufnr }) |
||||
let res = 0 |
||||
redraw |
||||
" same result as vim |
||||
while 1 |
||||
let key = nr2char(getchar()) |
||||
if key == "\<C-c>" |
||||
let res = -1 |
||||
break |
||||
elseif key == "\<esc>" || key == 'n' || key == 'N' |
||||
let res = 0 |
||||
break |
||||
elseif key == 'y' || key == 'Y' |
||||
let res = 1 |
||||
break |
||||
endif |
||||
endw |
||||
call coc#float#close(winid) |
||||
call a:cb(v:null, res) |
||||
endfunction |
||||
|
||||
function! coc#dialog#get_config_editor(lines, config) abort |
||||
let title = get(a:config, 'title', '') |
||||
let maxheight = min([get(a:config, 'maxHeight', 78), &lines - &cmdheight - 6]) |
||||
let maxwidth = min([get(a:config, 'maxWidth', 78), &columns - 2]) |
||||
let buttons = get(a:config, 'buttons', []) |
||||
let minwidth = s:min_btns_width(buttons) |
||||
if maxheight <= 0 || maxwidth <= 0 || minwidth > maxwidth |
||||
throw 'Not enough spaces for float window' |
||||
endif |
||||
let ch = 0 |
||||
let width = min([strdisplaywidth(title) + 1, maxwidth]) |
||||
for line in a:lines |
||||
let dw = max([1, strdisplaywidth(line)]) |
||||
if dw < maxwidth && dw > width |
||||
let width = dw |
||||
elseif dw >= maxwidth |
||||
let width = maxwidth |
||||
endif |
||||
let ch += float2nr(ceil(str2float(string(dw))/maxwidth)) |
||||
endfor |
||||
let width = max([minwidth, width]) |
||||
let height = coc#math#min(ch ,maxheight) |
||||
return { |
||||
\ 'row': &lines/2 - (height + 4)/2, |
||||
\ 'col': &columns/2 - (width + 2)/2, |
||||
\ 'width': width, |
||||
\ 'height': height, |
||||
\ } |
||||
endfunction |
||||
|
||||
function! coc#dialog#prompt_insert(text) abort |
||||
call coc#rpc#notify('PromptInsert', [a:text, bufnr('%')]) |
||||
return '' |
||||
endfunction |
||||
|
||||
" Dimension of window with lines relative to cursor |
||||
" Width & height excludes border & padding |
||||
function! coc#dialog#get_config_cursor(lines, config) abort |
||||
let preferTop = get(a:config, 'preferTop', 0) |
||||
let title = get(a:config, 'title', '') |
||||
let border = get(a:config, 'border', []) |
||||
if empty(border) && len(title) |
||||
let border = [1, 1, 1, 1] |
||||
endif |
||||
let bh = get(border, 0, 0) + get(border, 2, 0) |
||||
let vh = &lines - &cmdheight - 1 |
||||
if vh <= 0 |
||||
return v:null |
||||
endif |
||||
let maxWidth = coc#math#min(get(a:config, 'maxWidth', &columns - 1), &columns - 1) |
||||
if maxWidth < 3 |
||||
return v:null |
||||
endif |
||||
let maxHeight = coc#math#min(get(a:config, 'maxHeight', vh), vh) |
||||
let width = coc#math#min(40, strdisplaywidth(title)) + 3 |
||||
for line in a:lines |
||||
let dw = max([1, strdisplaywidth(line)]) |
||||
let width = max([width, dw + 2]) |
||||
endfor |
||||
let width = coc#math#min(maxWidth, width) |
||||
let ch = coc#string#content_height(a:lines, width - 2) |
||||
let [lineIdx, colIdx] = coc#cursor#screen_pos() |
||||
" How much we should move left |
||||
let offsetX = coc#math#min(get(a:config, 'offsetX', 0), colIdx) |
||||
let showTop = 0 |
||||
let hb = vh - lineIdx -1 |
||||
if lineIdx > bh + 2 && (preferTop || (lineIdx > hb && hb < ch + bh)) |
||||
let showTop = 1 |
||||
endif |
||||
let height = coc#math#min(maxHeight, ch + bh, showTop ? lineIdx - 1 : hb) |
||||
if height <= bh |
||||
return v:null |
||||
endif |
||||
let col = - max([offsetX, colIdx - (&columns - 1 - width)]) |
||||
let row = showTop ? - height + bh : 1 |
||||
return { |
||||
\ 'row': row, |
||||
\ 'col': col, |
||||
\ 'width': width - 2, |
||||
\ 'height': height - bh |
||||
\ } |
||||
endfunction |
||||
|
||||
function! coc#dialog#change_border_hl(winid, hlgroup) abort |
||||
if !hlexists(a:hlgroup) |
||||
return |
||||
endif |
||||
if s:is_vim |
||||
if coc#float#valid(a:winid) |
||||
call popup_setoptions(a:winid, {'borderhighlight': repeat([a:hlgroup], 4)}) |
||||
redraw |
||||
endif |
||||
else |
||||
let winid = coc#float#get_related(a:winid, 'border') |
||||
if winid > 0 |
||||
call setwinvar(winid, '&winhl', 'Normal:'.a:hlgroup) |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#dialog#change_title(winid, title) abort |
||||
if s:is_vim |
||||
if coc#float#valid(a:winid) |
||||
call popup_setoptions(a:winid, {'title': a:title}) |
||||
redraw |
||||
endif |
||||
else |
||||
let winid = coc#float#get_related(a:winid, 'border') |
||||
if winid > 0 |
||||
let bufnr = winbufnr(winid) |
||||
let line = getbufline(bufnr, 1)[0] |
||||
let top = strcharpart(line, 0, 1) |
||||
\.repeat('─', strchars(line) - 2) |
||||
\.strcharpart(line, strchars(line) - 1, 1) |
||||
if !empty(a:title) |
||||
let top = coc#string#compose(top, 1, a:title.' ') |
||||
endif |
||||
call nvim_buf_set_lines(bufnr, 0, 1, v:false, [top]) |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#dialog#change_loading(winid, loading) abort |
||||
if coc#float#valid(a:winid) |
||||
let winid = coc#float#get_related(a:winid, 'loading') |
||||
if !a:loading && winid > 0 |
||||
call coc#float#close(winid) |
||||
endif |
||||
if a:loading && winid == 0 |
||||
let bufnr = s:create_loading_buf() |
||||
if s:is_vim |
||||
let pos = popup_getpos(a:winid) |
||||
let winid = popup_create(bufnr, { |
||||
\ 'line': pos['line'] + 1, |
||||
\ 'col': pos['col'] + pos['width'] - 4, |
||||
\ 'maxheight': 1, |
||||
\ 'maxwidth': 3, |
||||
\ 'zindex': 999, |
||||
\ 'highlight': get(popup_getoptions(a:winid), 'highlight', 'CocFloating') |
||||
\ }) |
||||
else |
||||
let pos = nvim_win_get_position(a:winid) |
||||
let width = nvim_win_get_width(a:winid) |
||||
let opts = { |
||||
\ 'relative': 'editor', |
||||
\ 'row': pos[0], |
||||
\ 'col': pos[1] + width - 3, |
||||
\ 'focusable': v:false, |
||||
\ 'width': 3, |
||||
\ 'height': 1, |
||||
\ 'style': 'minimal', |
||||
\ } |
||||
if has('nvim-0.5.1') |
||||
let opts['zindex'] = 900 |
||||
endif |
||||
let winid = nvim_open_win(bufnr, v:false, opts) |
||||
call setwinvar(winid, '&winhl', getwinvar(a:winid, '&winhl')) |
||||
endif |
||||
call setwinvar(winid, 'kind', 'loading') |
||||
call setbufvar(bufnr, 'target_winid', a:winid) |
||||
call setbufvar(bufnr, 'popup', winid) |
||||
call coc#float#add_related(winid, a:winid) |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
" Update list with new lines and highlights |
||||
function! coc#dialog#update_list(winid, bufnr, lines, highlights) abort |
||||
if coc#window#tabnr(a:winid) == tabpagenr() |
||||
if getwinvar(a:winid, 'auto_height', 0) |
||||
let row = coc#float#get_row(a:winid) |
||||
" core height |
||||
let height = max([1, len(copy(a:lines))]) |
||||
let height = min([getwinvar(a:winid, 'max_height', 10), height, &lines - &cmdheight - 1 - row]) |
||||
let curr = s:is_vim ? popup_getpos(a:winid)['core_height'] : nvim_win_get_height(a:winid) |
||||
let delta = height - curr |
||||
if delta != 0 |
||||
call coc#float#change_height(a:winid, delta) |
||||
endif |
||||
endif |
||||
call coc#compat#buf_set_lines(a:bufnr, 0, -1, a:lines) |
||||
call coc#highlight#add_highlights(a:winid, [], a:highlights) |
||||
if s:is_vim |
||||
let target = getwinvar(a:winid, 'target_winid', -1) |
||||
if target != -1 |
||||
call coc#dialog#check_scroll_vim(target) |
||||
endif |
||||
call win_execute(a:winid, 'exe 1') |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
" Fix width of prompt window same as list window on scrollbar change |
||||
function! coc#dialog#check_scroll_vim(winid) abort |
||||
if s:is_vim && coc#float#valid(a:winid) |
||||
let winid = coc#float#get_related(a:winid, 'list') |
||||
if winid |
||||
redraw |
||||
let pos = popup_getpos(winid) |
||||
let width = pos['width'] + (pos['scrollbar'] ? 1 : 0) |
||||
if width != popup_getpos(a:winid)['width'] |
||||
call popup_move(a:winid, { |
||||
\ 'minwidth': width - 2, |
||||
\ 'maxwidth': width - 2, |
||||
\ }) |
||||
endif |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#dialog#set_cursor(winid, bufnr, line) abort |
||||
if s:is_vim |
||||
call coc#compat#execute(a:winid, 'exe '.a:line, 'silent!') |
||||
call popup_setoptions(a:winid, {'cursorline' : 1}) |
||||
call popup_setoptions(a:winid, {'cursorline' : 0}) |
||||
else |
||||
call nvim_win_set_cursor(a:winid, [a:line, 0]) |
||||
endif |
||||
call coc#dialog#place_sign(a:bufnr, a:line) |
||||
endfunction |
||||
|
||||
function! coc#dialog#place_sign(bufnr, line) abort |
||||
call sign_unplace(s:sign_group, { 'buffer': a:bufnr }) |
||||
if a:line > 0 |
||||
call sign_place(6, s:sign_group, 'CocCurrentLine', a:bufnr, {'lnum': a:line}) |
||||
endif |
||||
endfunction |
||||
|
||||
" Could be center(with optional marginTop) or cursor |
||||
function! s:get_prompt_dimension(title, default, opts) abort |
||||
let relative = get(a:opts, 'position', 'cursor') ==# 'cursor' ? 'cursor' : 'editor' |
||||
let curr = win_screenpos(winnr())[1] + wincol() - 2 |
||||
let minWidth = get(a:opts, 'minWidth', s:prompt_win_width) |
||||
let width = min([max([strwidth(a:default) + 2, strwidth(a:title) + 2, minWidth]), &columns - 2]) |
||||
if get(a:opts, 'maxWidth', 0) |
||||
let width = min([width, a:opts['maxWidth']]) |
||||
endif |
||||
if relative ==# 'cursor' |
||||
let [lineIdx, colIdx] = coc#cursor#screen_pos() |
||||
if width == &columns - 2 |
||||
let col = 0 - curr |
||||
else |
||||
let col = curr + width <= &columns - 2 ? 0 : curr + width - &columns + 2 |
||||
endif |
||||
let config = { |
||||
\ 'row': lineIdx == 0 ? 1 : 0, |
||||
\ 'col': colIdx == 0 ? 0 : col - 1, |
||||
\ } |
||||
else |
||||
let marginTop = get(a:opts, 'marginTop', v:null) |
||||
if marginTop is v:null |
||||
let row = (&lines - &cmdheight - 2) / 2 |
||||
else |
||||
let row = marginTop < 2 ? 1 : min([marginTop, &columns - &cmdheight]) |
||||
endif |
||||
let config = { |
||||
\ 'col': float2nr((&columns - width) / 2), |
||||
\ 'row': row - s:is_vim, |
||||
\ } |
||||
endif |
||||
return extend(config, {'relative': relative, 'width': width, 'height': 1}) |
||||
endfunction |
||||
|
||||
function! s:min_btns_width(buttons) abort |
||||
if empty(a:buttons) |
||||
return 0 |
||||
endif |
||||
let minwidth = len(a:buttons)*3 - 1 |
||||
for txt in a:buttons |
||||
let minwidth = minwidth + strdisplaywidth(txt) |
||||
endfor |
||||
return minwidth |
||||
endfunction |
||||
|
||||
" Close windows that should auto hide |
||||
function! s:close_auto_hide_wins(...) abort |
||||
let winids = coc#float#get_float_win_list() |
||||
let except = get(a:, 1, 0) |
||||
for id in winids |
||||
if except && id == except |
||||
continue |
||||
endif |
||||
if coc#window#get_var(id, 'autohide', 0) |
||||
call coc#float#close(id) |
||||
endif |
||||
endfor |
||||
endfunction |
||||
|
||||
function! s:create_loading_buf() abort |
||||
let bufnr = coc#float#create_buf(0) |
||||
call s:change_loading_buf(bufnr, 0) |
||||
return bufnr |
||||
endfunction |
||||
|
||||
function! s:change_loading_buf(bufnr, idx) abort |
||||
if bufloaded(a:bufnr) |
||||
let target = getbufvar(a:bufnr, 'target_winid', v:null) |
||||
if !empty(target) && !coc#float#valid(target) |
||||
call coc#float#close(getbufvar(a:bufnr, 'popup')) |
||||
return |
||||
endif |
||||
let line = get(s:frames, a:idx, ' ') |
||||
call setbufline(a:bufnr, 1, line) |
||||
call coc#highlight#add_highlight(a:bufnr, -1, 'CocNotificationProgress', 0, 0, -1) |
||||
let idx = a:idx == len(s:frames) - 1 ? 0 : a:idx + 1 |
||||
call timer_start(100, { -> s:change_loading_buf(a:bufnr, idx)}) |
||||
endif |
||||
endfunction |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
scriptencoding utf-8 |
||||
|
||||
function! coc#dict#equal(one, two) abort |
||||
for key in keys(a:one) |
||||
if a:one[key] != a:two[key] |
||||
return 0 |
||||
endif |
||||
endfor |
||||
return 1 |
||||
endfunction |
||||
|
||||
" Return new dict with keys removed |
||||
function! coc#dict#omit(dict, keys) abort |
||||
let res = {} |
||||
for key in keys(a:dict) |
||||
if index(a:keys, key) == -1 |
||||
let res[key] = a:dict[key] |
||||
endif |
||||
endfor |
||||
return res |
||||
endfunction |
||||
|
||||
" Return new dict with keys only |
||||
function! coc#dict#pick(dict, keys) abort |
||||
let res = {} |
||||
for key in keys(a:dict) |
||||
if index(a:keys, key) != -1 |
||||
let res[key] = a:dict[key] |
||||
endif |
||||
endfor |
||||
return res |
||||
endfunction |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
|
||||
" support for float values |
||||
function! coc#math#min(first, ...) abort |
||||
let val = a:first |
||||
for i in range(0, len(a:000) - 1) |
||||
if a:000[i] < val |
||||
let val = a:000[i] |
||||
endif |
||||
endfor |
||||
return val |
||||
endfunction |
@ -0,0 +1,583 @@
@@ -0,0 +1,583 @@
|
||||
scriptencoding utf-8 |
||||
let s:is_vim = !has('nvim') |
||||
let s:pum_bufnr = 0 |
||||
let s:pum_winid = 0 |
||||
let s:pum_index = -1 |
||||
let s:pum_size = 0 |
||||
let s:inserted = 0 |
||||
let s:virtual_text = 0 |
||||
let s:virtual_text_ns = coc#highlight#create_namespace('pum-virtual') |
||||
let s:ignore = s:is_vim || has('nvim-0.5.0') ? "\<Ignore>" : "\<space>\<bs>" |
||||
let s:hide_pum = has('nvim-0.6.1') || has('patch-8.2.3389') |
||||
let s:virtual_text_support = has('nvim-0.5.0') || has('patch-9.0.0067') |
||||
" bufnr, &indentkeys |
||||
let s:saved_indenetkeys = [] |
||||
let s:prop_id = 0 |
||||
let s:reversed = 0 |
||||
let s:check_hl_group = 0 |
||||
|
||||
if s:is_vim && s:virtual_text_support |
||||
if empty(prop_type_get('CocPumVirtualText')) |
||||
call prop_type_add('CocPumVirtualText', {'highlight': 'CocPumVirtualText'}) |
||||
endif |
||||
endif |
||||
|
||||
function! coc#pum#visible() abort |
||||
if !s:pum_winid |
||||
return 0 |
||||
endif |
||||
return getwinvar(s:pum_winid, 'float', 0) == 1 |
||||
endfunction |
||||
|
||||
function! coc#pum#winid() abort |
||||
return s:pum_winid |
||||
endfunction |
||||
|
||||
function! coc#pum#close_detail() abort |
||||
let winid = coc#float#get_float_by_kind('pumdetail') |
||||
if winid |
||||
call coc#float#close(winid, 1) |
||||
if s:is_vim |
||||
call timer_start(0, { -> execute('redraw')}) |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#pum#close(...) abort |
||||
if coc#float#valid(s:pum_winid) |
||||
let kind = get(a:, 1, '') |
||||
if kind ==# 'cancel' |
||||
let input = getwinvar(s:pum_winid, 'input', '') |
||||
let s:pum_index = -1 |
||||
call s:insert_word(input) |
||||
call s:on_pum_change(0) |
||||
doautocmd <nomodeline> TextChangedI |
||||
elseif kind ==# 'confirm' |
||||
let words = getwinvar(s:pum_winid, 'words', []) |
||||
if s:pum_index >= 0 |
||||
let word = get(words, s:pum_index, '') |
||||
call s:insert_word(word) |
||||
call s:restore_indentkeys() |
||||
endif |
||||
doautocmd <nomodeline> TextChangedI |
||||
endif |
||||
call s:close_pum() |
||||
if !get(a:, 2, 0) |
||||
" vim possible have unexpected text inserted without timer. |
||||
call timer_start(1, { -> coc#rpc#notify('CompleteStop', [kind])}) |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#pum#select_confirm() abort |
||||
if s:pum_index < 0 |
||||
let s:pum_index = 0 |
||||
call s:on_pum_change(0) |
||||
endif |
||||
call coc#pum#close('confirm') |
||||
endfunction |
||||
|
||||
function! coc#pum#insert() abort |
||||
call timer_start(1, { -> s:insert_current()}) |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
" Add one more character from the matched complete item(or first one), |
||||
" the word should starts with input, the same as vim's CTRL-L behavior. |
||||
function! coc#pum#one_more() abort |
||||
call timer_start(1, { -> s:insert_one_more()}) |
||||
return '' |
||||
endfunction |
||||
|
||||
function! coc#pum#_close() abort |
||||
if coc#float#valid(s:pum_winid) |
||||
call s:close_pum() |
||||
if s:is_vim |
||||
call timer_start(0, { -> execute('redraw')}) |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:insert_one_more() abort |
||||
if coc#float#valid(s:pum_winid) |
||||
let parts = getwinvar(s:pum_winid, 'parts', []) |
||||
let input = strpart(getline('.'), strchars(parts[0]), col('.') - 1) |
||||
let words = getwinvar(s:pum_winid, 'words', []) |
||||
let word = get(words, s:pum_index == -1 ? 0 : s:pum_index, '') |
||||
if !empty(word) && strcharpart(word, 0, strchars(input)) ==# input |
||||
let ch = strcharpart(word, strchars(input), 1) |
||||
if !empty(ch) |
||||
call feedkeys(ch, "int") |
||||
endif |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:insert_current() abort |
||||
if coc#float#valid(s:pum_winid) |
||||
if s:pum_index >= 0 |
||||
let words = getwinvar(s:pum_winid, 'words', []) |
||||
let word = get(words, s:pum_index, '') |
||||
call s:insert_word(word) |
||||
call s:restore_indentkeys() |
||||
endif |
||||
doautocmd <nomodeline> TextChangedI |
||||
call s:close_pum() |
||||
call coc#rpc#notify('CompleteStop', ['']) |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:close_pum() abort |
||||
call s:clear_virtual_text() |
||||
call coc#float#close(s:pum_winid, 1) |
||||
let s:pum_winid = 0 |
||||
let s:pum_size = 0 |
||||
let winid = coc#float#get_float_by_kind('pumdetail') |
||||
if winid |
||||
call coc#float#close(winid, 1) |
||||
endif |
||||
call s:restore_indentkeys() |
||||
endfunction |
||||
|
||||
function! s:restore_indentkeys() abort |
||||
if get(s:saved_indenetkeys, 0, 0) == bufnr('%') |
||||
call setbufvar(s:saved_indenetkeys[0], '&indentkeys', get(s:saved_indenetkeys, 1, '')) |
||||
let s:saved_indenetkeys = [] |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#pum#next(insert) abort |
||||
call timer_start(1, { -> s:navigate(1, a:insert)}) |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
function! coc#pum#prev(insert) abort |
||||
call timer_start(1, { -> s:navigate(0, a:insert)}) |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
function! coc#pum#stop() abort |
||||
call timer_start(1, { -> coc#pum#close()}) |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
function! coc#pum#cancel() abort |
||||
call timer_start(1, { -> coc#pum#close('cancel')}) |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
function! coc#pum#confirm() abort |
||||
call timer_start(1, { -> coc#pum#close('confirm')}) |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
function! coc#pum#select(index, insert, confirm) abort |
||||
if !coc#float#valid(s:pum_winid) |
||||
return '' |
||||
endif |
||||
if a:index == -1 |
||||
call coc#pum#close('cancel') |
||||
return '' |
||||
endif |
||||
if a:index < 0 || a:index >= s:pum_size |
||||
throw 'index out of range ' . a:index |
||||
endif |
||||
call s:select_by_index(a:index, a:insert) |
||||
if a:confirm |
||||
call coc#pum#close('confirm') |
||||
endif |
||||
return '' |
||||
endfunction |
||||
|
||||
function! coc#pum#info() abort |
||||
let bufnr = winbufnr(s:pum_winid) |
||||
let words = getwinvar(s:pum_winid, 'words', []) |
||||
let word = s:pum_index < 0 ? '' : get(words, s:pum_index, '') |
||||
let pretext = strpart(getline('.'), 0, col('.') - 1) |
||||
if s:is_vim |
||||
let pos = popup_getpos(s:pum_winid) |
||||
let border = has_key(popup_getoptions(s:pum_winid), 'border') |
||||
let add = pos['scrollbar'] && border ? 1 : 0 |
||||
return { |
||||
\ 'word': word, |
||||
\ 'index': s:pum_index, |
||||
\ 'scrollbar': pos['scrollbar'], |
||||
\ 'row': pos['line'] - 1, |
||||
\ 'col': pos['col'] - 1, |
||||
\ 'width': pos['width'] + add, |
||||
\ 'height': pos['height'], |
||||
\ 'size': s:pum_size, |
||||
\ 'border': border, |
||||
\ 'inserted': s:inserted ? v:true : v:false, |
||||
\ 'reversed': s:reversed ? v:true : v:false, |
||||
\ } |
||||
else |
||||
let scrollbar = coc#float#get_related(s:pum_winid, 'scrollbar') |
||||
let winid = coc#float#get_related(s:pum_winid, 'border', s:pum_winid) |
||||
let pos = nvim_win_get_position(winid) |
||||
return { |
||||
\ 'word': word, |
||||
\ 'index': s:pum_index, |
||||
\ 'scrollbar': scrollbar && nvim_win_is_valid(scrollbar) ? 1 : 0, |
||||
\ 'row': pos[0], |
||||
\ 'col': pos[1], |
||||
\ 'width': nvim_win_get_width(winid), |
||||
\ 'height': nvim_win_get_height(winid), |
||||
\ 'size': s:pum_size, |
||||
\ 'border': winid != s:pum_winid, |
||||
\ 'inserted': s:inserted ? v:true : v:false, |
||||
\ 'reversed': s:reversed ? v:true : v:false, |
||||
\ } |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#pum#scroll(forward) abort |
||||
if coc#pum#visible() |
||||
let height = s:get_height(s:pum_winid) |
||||
if s:pum_size > height |
||||
call timer_start(10, { -> s:scroll_pum(a:forward, height, s:pum_size)}) |
||||
endif |
||||
endif |
||||
return s:ignore |
||||
endfunction |
||||
|
||||
function! s:get_height(winid) abort |
||||
if has('nvim') |
||||
return nvim_win_get_height(a:winid) |
||||
endif |
||||
return get(popup_getpos(a:winid), 'core_height', 0) |
||||
endfunction |
||||
|
||||
function! s:scroll_pum(forward, height, size) abort |
||||
let topline = s:get_topline(s:pum_winid) |
||||
if !a:forward && topline == 1 |
||||
if s:pum_index >= 0 |
||||
call s:select_line(s:pum_winid, 1) |
||||
call s:on_pum_change(1) |
||||
endif |
||||
return |
||||
endif |
||||
if a:forward && topline + a:height - 1 >= a:size |
||||
if s:pum_index >= 0 |
||||
call s:select_line(s:pum_winid, a:size) |
||||
call s:on_pum_change(1) |
||||
endif |
||||
return |
||||
endif |
||||
call coc#float#scroll_win(s:pum_winid, a:forward, a:height) |
||||
if s:pum_index >= 0 |
||||
let lnum = s:pum_index + 1 |
||||
let topline = s:get_topline(s:pum_winid) |
||||
if lnum >= topline && lnum <= topline + a:height - 1 |
||||
return |
||||
endif |
||||
call s:select_line(s:pum_winid, topline) |
||||
call s:on_pum_change(1) |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:get_topline(winid) abort |
||||
if has('nvim') |
||||
let info = getwininfo(a:winid)[0] |
||||
return info['topline'] |
||||
else |
||||
let pos = popup_getpos(a:winid) |
||||
return pos['firstline'] |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:navigate(next, insert) abort |
||||
if !coc#float#valid(s:pum_winid) |
||||
return |
||||
endif |
||||
let index = s:get_index(a:next) |
||||
call s:select_by_index(index, a:insert) |
||||
endfunction |
||||
|
||||
function! s:select_by_index(index, insert) abort |
||||
let lnum = a:index == -1 ? 0 : s:index_to_lnum(a:index) |
||||
call s:set_cursor(s:pum_winid, lnum) |
||||
if !s:is_vim |
||||
call coc#float#nvim_scrollbar(s:pum_winid) |
||||
endif |
||||
if a:insert |
||||
let s:inserted = 1 |
||||
if a:index < 0 |
||||
let input = getwinvar(s:pum_winid, 'input', '') |
||||
call s:insert_word(input) |
||||
call coc#pum#close_detail() |
||||
else |
||||
let words = getwinvar(s:pum_winid, 'words', []) |
||||
let word = get(words, a:index, '') |
||||
call s:insert_word(word) |
||||
endif |
||||
doautocmd <nomodeline> TextChangedP |
||||
endif |
||||
call s:on_pum_change(1) |
||||
endfunction |
||||
|
||||
function! s:get_index(next) abort |
||||
if a:next |
||||
let index = s:pum_index + 1 == s:pum_size ? -1 : s:pum_index + 1 |
||||
else |
||||
let index = s:pum_index == -1 ? s:pum_size - 1 : s:pum_index - 1 |
||||
endif |
||||
return index |
||||
endfunction |
||||
|
||||
function! s:insert_word(word) abort |
||||
let parts = getwinvar(s:pum_winid, 'parts', []) |
||||
if !empty(parts) && mode() ==# 'i' |
||||
let curr = getline('.') |
||||
let saved_completeopt = &completeopt |
||||
if saved_completeopt =~ 'menuone' |
||||
noa set completeopt=menu |
||||
endif |
||||
noa call complete(strlen(parts[0]) + 1, [{ 'empty': v:true, 'word': a:word }]) |
||||
" exit complete state |
||||
if s:hide_pum |
||||
call feedkeys("\<C-x>\<C-z>", 'in') |
||||
else |
||||
let g:coc_disable_space_report = 1 |
||||
call feedkeys("\<space>\<bs>", 'in') |
||||
endif |
||||
execute 'noa set completeopt='.saved_completeopt |
||||
endif |
||||
endfunction |
||||
|
||||
" create or update pum with lines, CompleteOption and config. |
||||
" return winid & dimension |
||||
function! coc#pum#create(lines, opt, config) abort |
||||
if mode() !=# 'i' || a:opt['line'] != line('.') |
||||
return |
||||
endif |
||||
let len = col('.') - a:opt['col'] - 1 |
||||
if len < 0 |
||||
return |
||||
endif |
||||
let input = len == 0 ? '' : strpart(getline('.'), a:opt['col'], len) |
||||
if input !=# a:opt['input'] |
||||
return |
||||
endif |
||||
let config = s:get_pum_dimension(a:lines, a:opt['col'], a:config) |
||||
if empty(config) |
||||
return |
||||
endif |
||||
let s:reversed = get(a:config, 'reverse', 0) && config['row'] < 0 |
||||
let s:virtual_text = s:virtual_text_support && a:opt['virtualText'] |
||||
let s:pum_size = len(a:lines) |
||||
let s:pum_index = a:opt['index'] |
||||
let lnum = s:index_to_lnum(s:pum_index) |
||||
call extend(config, { |
||||
\ 'lines': s:reversed ? reverse(copy(a:lines)) : a:lines, |
||||
\ 'relative': 'cursor', |
||||
\ 'nopad': 1, |
||||
\ 'cursorline': 1, |
||||
\ 'index': lnum - 1, |
||||
\ 'focusable': v:false |
||||
\ }) |
||||
call extend(config, coc#dict#pick(a:config, ['highlight', 'rounded', 'highlights', 'winblend', 'shadow', 'border', 'borderhighlight'])) |
||||
if s:reversed |
||||
for item in config['highlights'] |
||||
let item['lnum'] = s:pum_size - item['lnum'] - 1 |
||||
endfor |
||||
endif |
||||
if empty(get(config, 'winblend', 0)) && exists('&pumblend') |
||||
let config['winblend'] = &pumblend |
||||
endif |
||||
let result = coc#float#create_float_win(s:pum_winid, s:pum_bufnr, config) |
||||
if empty(result) |
||||
return |
||||
endif |
||||
let s:inserted = 0 |
||||
let s:pum_winid = result[0] |
||||
let s:pum_bufnr = result[1] |
||||
call setwinvar(s:pum_winid, 'above', config['row'] < 0) |
||||
let firstline = s:get_firstline(lnum, s:pum_size, config['height']) |
||||
if s:is_vim |
||||
call popup_setoptions(s:pum_winid, { 'firstline': firstline }) |
||||
else |
||||
call coc#compat#execute(s:pum_winid, 'call winrestview({"lnum":'.lnum.',"topline":'.firstline.'})') |
||||
endif |
||||
call coc#dialog#place_sign(s:pum_bufnr, s:pum_index == -1 ? 0 : lnum) |
||||
" content before col and content after cursor |
||||
let linetext = getline('.') |
||||
let parts = [strpart(linetext, 0, a:opt['col']), strpart(linetext, col('.') - 1)] |
||||
call setwinvar(s:pum_winid, 'input', input) |
||||
call setwinvar(s:pum_winid, 'parts', parts) |
||||
call setwinvar(s:pum_winid, 'words', a:opt['words']) |
||||
call setwinvar(s:pum_winid, 'kind', 'pum') |
||||
if !s:is_vim |
||||
if s:pum_size > config['height'] |
||||
redraw |
||||
call coc#float#nvim_scrollbar(s:pum_winid) |
||||
else |
||||
call coc#float#close_related(s:pum_winid, 'scrollbar') |
||||
endif |
||||
endif |
||||
call s:on_pum_change(0) |
||||
let bufnr = bufnr('%') |
||||
if !empty(&indentexpr) && get(s:saved_indenetkeys, 0, 0) != bufnr |
||||
let s:saved_indenetkeys = [bufnr, &indentkeys] |
||||
execute 'setl indentkeys=' |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:get_firstline(lnum, total, height) abort |
||||
if a:lnum <= a:height |
||||
return 1 |
||||
endif |
||||
return min([a:total - a:height + 1, a:lnum - (a:height*2/3)]) |
||||
endfunction |
||||
|
||||
function! s:on_pum_change(move) abort |
||||
if coc#float#valid(s:pum_winid) |
||||
if s:virtual_text |
||||
call s:insert_virtual_text() |
||||
endif |
||||
let ev = extend(coc#pum#info(), {'move': a:move ? v:true : v:false}) |
||||
call coc#rpc#notify('CocAutocmd', ['MenuPopupChanged', ev, win_screenpos(winnr())[0] + winline() - 2]) |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:index_to_lnum(index) abort |
||||
if s:reversed |
||||
if a:index <= 0 |
||||
return s:pum_size |
||||
endif |
||||
return s:pum_size - a:index |
||||
endif |
||||
return max([1, a:index + 1]) |
||||
endfunction |
||||
|
||||
function! s:get_pum_dimension(lines, col, config) abort |
||||
let linecount = len(a:lines) |
||||
let [lineIdx, colIdx] = coc#cursor#screen_pos() |
||||
let bh = empty(get(a:config, 'border', [])) ? 0 : 2 |
||||
let columns = &columns |
||||
let pumwidth = max([15, exists('&pumwidth') ? &pumwidth : 0]) |
||||
let width = min([columns, max([pumwidth, a:config['width']])]) |
||||
let vh = &lines - &cmdheight - 1 - !empty(&tabline) |
||||
if vh <= 0 |
||||
return v:null |
||||
endif |
||||
let pumheight = empty(&pumheight) ? vh : &pumheight |
||||
let showTop = getwinvar(s:pum_winid, 'above', v:null) |
||||
if type(showTop) != v:t_number |
||||
if vh - lineIdx - bh - 1 < min([pumheight, linecount]) && vh - lineIdx < min([10, vh/2]) |
||||
let showTop = 1 |
||||
else |
||||
let showTop = 0 |
||||
endif |
||||
endif |
||||
let height = showTop ? min([lineIdx - bh - !empty(&tabline), linecount, pumheight]) : min([vh - lineIdx - bh - 1, linecount, pumheight]) |
||||
if height <= 0 |
||||
return v:null |
||||
endif |
||||
let col = - (col('.') - a:col - 1) - 1 |
||||
let row = showTop ? - height : 1 |
||||
let delta = colIdx + col |
||||
if width > pumwidth && delta + width > columns |
||||
let width = max([columns - delta, pumwidth]) |
||||
endif |
||||
if delta < 0 |
||||
let col = col - delta |
||||
elseif delta + width > columns |
||||
let col = max([-colIdx, col - (delta + width - columns)]) |
||||
endif |
||||
return { |
||||
\ 'row': row, |
||||
\ 'col': col, |
||||
\ 'width': width, |
||||
\ 'height': height |
||||
\ } |
||||
endfunction |
||||
|
||||
" can't use coc#dialog#set_cursor on vim8, don't know why |
||||
function! s:set_cursor(winid, line) abort |
||||
if s:is_vim |
||||
let pos = popup_getpos(a:winid) |
||||
let core_height = pos['core_height'] |
||||
let lastline = pos['firstline'] + core_height - 1 |
||||
if a:line > lastline |
||||
call popup_setoptions(a:winid, { |
||||
\ 'firstline': pos['firstline'] + a:line - lastline, |
||||
\ }) |
||||
elseif a:line < pos['firstline'] |
||||
if s:reversed |
||||
call popup_setoptions(a:winid, { |
||||
\ 'firstline': a:line == 0 ? s:pum_size - core_height + 1 : a:line - core_height + 1, |
||||
\ }) |
||||
else |
||||
call popup_setoptions(a:winid, { |
||||
\ 'firstline': max([1, a:line]), |
||||
\ }) |
||||
endif |
||||
endif |
||||
endif |
||||
call s:select_line(a:winid, a:line) |
||||
endfunction |
||||
|
||||
function! s:select_line(winid, line) abort |
||||
let s:pum_index = s:reversed ? (a:line == 0 ? -1 : s:pum_size - a:line) : a:line - 1 |
||||
let lnum = s:reversed ? (a:line == 0 ? s:pum_size : a:line) : max([1, a:line]) |
||||
if s:is_vim |
||||
call coc#compat#execute(a:winid, 'exe '.lnum) |
||||
else |
||||
call nvim_win_set_cursor(a:winid, [lnum, 0]) |
||||
endif |
||||
call coc#dialog#place_sign(s:pum_bufnr, a:line == 0 ? 0 : lnum) |
||||
endfunction |
||||
|
||||
function! s:insert_virtual_text() abort |
||||
let bufnr = bufnr('%') |
||||
if !s:virtual_text || s:pum_index < 0 |
||||
call s:clear_virtual_text() |
||||
else |
||||
" Check if could create |
||||
let insert = '' |
||||
let line = line('.') - 1 |
||||
let words = getwinvar(s:pum_winid, 'words', []) |
||||
let word = get(words, s:pum_index, '') |
||||
let parts = getwinvar(s:pum_winid, 'parts', []) |
||||
let start = strlen(parts[0]) |
||||
let input = strpart(getline('.'), start, col('.') - 1 - start) |
||||
if strchars(word) > strchars(input) && strcharpart(word, 0, strchars(input)) ==# input |
||||
let insert = strcharpart(word, strchars(input)) |
||||
endif |
||||
if s:is_vim |
||||
if s:prop_id != 0 |
||||
call prop_remove({'id': s:prop_id}, line + 1, line + 1) |
||||
endif |
||||
if !empty(insert) |
||||
let s:prop_id = prop_add(line + 1, col('.'), { |
||||
\ 'text': insert, |
||||
\ 'type': 'CocPumVirtualText' |
||||
\ }) |
||||
endif |
||||
else |
||||
call nvim_buf_clear_namespace(bufnr, s:virtual_text_ns, line, line + 1) |
||||
if !empty(insert) |
||||
let opts = { |
||||
\ 'hl_mode': 'combine', |
||||
\ 'virt_text': [[insert, 'CocPumVirtualText']], |
||||
\ 'virt_text_pos': 'overlay', |
||||
\ 'virt_text_win_col': virtcol('.') - 1, |
||||
\ } |
||||
call nvim_buf_set_extmark(bufnr, s:virtual_text_ns, line, col('.') - 1, opts) |
||||
endif |
||||
endif |
||||
endif |
||||
endfunction |
||||
|
||||
function! s:clear_virtual_text() abort |
||||
if s:virtual_text_support |
||||
if s:is_vim |
||||
if s:prop_id != 0 |
||||
call prop_remove({'id': s:prop_id}) |
||||
endif |
||||
else |
||||
call nvim_buf_clear_namespace(bufnr('%'), s:virtual_text_ns, 0, -1) |
||||
endif |
||||
endif |
||||
endfunction |
@ -0,0 +1,473 @@
@@ -0,0 +1,473 @@
|
||||
let s:is_vim = !has('nvim') |
||||
let s:is_win = has('win32') || has('win64') |
||||
let s:is_mac = has('mac') |
||||
let s:sign_api = exists('*sign_getplaced') && exists('*sign_place') |
||||
let s:sign_groups = [] |
||||
let s:outline_preview_bufnr = 0 |
||||
|
||||
" Check <Tab> and <CR> |
||||
function! coc#ui#check_pum_keymappings(trigger) abort |
||||
if a:trigger !=# 'none' |
||||
for key in ['<cr>', '<tab>', '<c-y>', '<s-tab>'] |
||||
let arg = maparg(key, 'i', 0, 1) |
||||
if get(arg, 'expr', 0) |
||||
let rhs = get(arg, 'rhs', '') |
||||
if rhs =~# '\<pumvisible()' && rhs !~# '\<coc#pum#visible()' |
||||
let rhs = substitute(rhs, '\Cpumvisible()', 'coc#pum#visible()', 'g') |
||||
let rhs = substitute(rhs, '\c"\\<C-n>"', 'coc#pum#next(1)', '') |
||||
let rhs = substitute(rhs, '\c"\\<C-p>"', 'coc#pum#prev(1)', '') |
||||
let rhs = substitute(rhs, '\c"\\<C-y>"', 'coc#pum#confirm()', '') |
||||
execute 'inoremap <silent><nowait><expr> '.arg['lhs'].' '.rhs |
||||
endif |
||||
endif |
||||
endfor |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#ui#quickpick(title, items, cb) abort |
||||
if exists('*popup_menu') |
||||
function! s:QuickpickHandler(id, result) closure |
||||
call a:cb(v:null, a:result) |
||||
endfunction |
||||
function! s:QuickpickFilter(id, key) closure |
||||
for i in range(1, len(a:items)) |
||||
if a:key == string(i) |
||||
call popup_close(a:id, i) |
||||
return 1 |
||||
endif |
||||
endfor |
||||
" No shortcut, pass to generic filter |
||||
return popup_filter_menu(a:id, a:key) |
||||
endfunction |
||||
try |
||||
call popup_menu(a:items, { |
||||
\ 'title': a:title, |
||||
\ 'filter': function('s:QuickpickFilter'), |
||||
\ 'callback': function('s:QuickpickHandler'), |
||||
\ }) |
||||
redraw |
||||
catch /.*/ |
||||
call a:cb(v:exception) |
||||
endtry |
||||
else |
||||
let res = inputlist([a:title] + a:items) |
||||
call a:cb(v:null, res) |
||||
endif |
||||
endfunction |
||||
|
||||
" cmd, cwd |
||||
function! coc#ui#open_terminal(opts) abort |
||||
if s:is_vim && !exists('*term_start') |
||||
echohl WarningMsg | echon "Your vim doesn't have terminal support!" | echohl None |
||||
return |
||||
endif |
||||
if get(a:opts, 'position', 'bottom') ==# 'bottom' |
||||
let p = '5new' |
||||
else |
||||
let p = 'vnew' |
||||
endif |
||||
execute 'belowright '.p.' +setl\ buftype=nofile ' |
||||
setl buftype=nofile |
||||
setl winfixheight |
||||
setl norelativenumber |
||||
setl nonumber |
||||
setl bufhidden=wipe |
||||
if exists('#User#CocTerminalOpen') |
||||
exe 'doautocmd <nomodeline> User CocTerminalOpen' |
||||
endif |
||||
let cmd = get(a:opts, 'cmd', '') |
||||
let autoclose = get(a:opts, 'autoclose', 1) |
||||
if empty(cmd) |
||||
throw 'command required!' |
||||
endif |
||||
let cwd = get(a:opts, 'cwd', getcwd()) |
||||
let keepfocus = get(a:opts, 'keepfocus', 0) |
||||
let bufnr = bufnr('%') |
||||
let Callback = get(a:opts, 'Callback', v:null) |
||||
|
||||
function! s:OnExit(status) closure |
||||
let content = join(getbufline(bufnr, 1, '$'), "\n") |
||||
if a:status == 0 && autoclose == 1 |
||||
execute 'silent! bd! '.bufnr |
||||
endif |
||||
if !empty(Callback) |
||||
call call(Callback, [a:status, bufnr, content]) |
||||
endif |
||||
endfunction |
||||
|
||||
if has('nvim') |
||||
call termopen(cmd, { |
||||
\ 'cwd': cwd, |
||||
\ 'on_exit': {job, status -> s:OnExit(status)}, |
||||
\}) |
||||
else |
||||
if s:is_win |
||||
let cmd = 'cmd.exe /C "'.cmd.'"' |
||||
endif |
||||
call term_start(cmd, { |
||||
\ 'cwd': cwd, |
||||
\ 'exit_cb': {job, status -> s:OnExit(status)}, |
||||
\ 'curwin': 1, |
||||
\}) |
||||
endif |
||||
if keepfocus |
||||
wincmd p |
||||
endif |
||||
return bufnr |
||||
endfunction |
||||
|
||||
" run command in terminal |
||||
function! coc#ui#run_terminal(opts, cb) |
||||
let cmd = get(a:opts, 'cmd', '') |
||||
if empty(cmd) |
||||
return a:cb('command required for terminal') |
||||
endif |
||||
let opts = { |
||||
\ 'cmd': cmd, |
||||
\ 'cwd': get(a:opts, 'cwd', getcwd()), |
||||
\ 'keepfocus': get(a:opts, 'keepfocus', 0), |
||||
\ 'Callback': {status, bufnr, content -> a:cb(v:null, {'success': status == 0 ? v:true : v:false, 'bufnr': bufnr, 'content': content})} |
||||
\} |
||||
call coc#ui#open_terminal(opts) |
||||
endfunction |
||||
|
||||
function! coc#ui#echo_hover(msg) |
||||
echohl MoreMsg |
||||
echo a:msg |
||||
echohl None |
||||
let g:coc_last_hover_message = a:msg |
||||
endfunction |
||||
|
||||
function! coc#ui#echo_messages(hl, msgs) |
||||
if a:hl !~# 'Error' && (mode() !~# '\v^(i|n)$') |
||||
return |
||||
endif |
||||
let msgs = filter(copy(a:msgs), '!empty(v:val)') |
||||
if empty(msgs) |
||||
return |
||||
endif |
||||
execute 'echohl '.a:hl |
||||
echom a:msgs[0] |
||||
redraw |
||||
echo join(msgs, "\n") |
||||
echohl None |
||||
endfunction |
||||
|
||||
function! coc#ui#preview_info(lines, filetype, ...) abort |
||||
pclose |
||||
keepalt new +setlocal\ previewwindow|setlocal\ buftype=nofile|setlocal\ noswapfile|setlocal\ wrap [Document] |
||||
setl bufhidden=wipe |
||||
setl nobuflisted |
||||
setl nospell |
||||
exe 'setl filetype='.a:filetype |
||||
setl conceallevel=0 |
||||
setl nofoldenable |
||||
for command in a:000 |
||||
execute command |
||||
endfor |
||||
call append(0, a:lines) |
||||
exe "normal! z" . len(a:lines) . "\<cr>" |
||||
exe "normal! gg" |
||||
wincmd p |
||||
endfunction |
||||
|
||||
function! coc#ui#open_files(files) |
||||
let bufnrs = [] |
||||
" added on latest vim8 |
||||
if exists('*bufadd') && exists('*bufload') |
||||
for file in a:files |
||||
let file = fnamemodify(file, ':.') |
||||
if bufloaded(file) |
||||
call add(bufnrs, bufnr(file)) |
||||
else |
||||
let bufnr = bufadd(file) |
||||
call bufload(file) |
||||
call add(bufnrs, bufnr) |
||||
call setbufvar(bufnr, '&buflisted', 1) |
||||
endif |
||||
endfor |
||||
else |
||||
noa keepalt 1new +setl\ bufhidden=wipe |
||||
for file in a:files |
||||
let file = fnamemodify(file, ':.') |
||||
execute 'noa edit +setl\ bufhidden=hide '.fnameescape(file) |
||||
if &filetype ==# '' |
||||
filetype detect |
||||
endif |
||||
call add(bufnrs, bufnr('%')) |
||||
endfor |
||||
noa close |
||||
endif |
||||
doautocmd BufEnter |
||||
return bufnrs |
||||
endfunction |
||||
|
||||
function! coc#ui#echo_lines(lines) |
||||
echo join(a:lines, "\n") |
||||
endfunction |
||||
|
||||
function! coc#ui#echo_signatures(signatures) abort |
||||
if pumvisible() | return | endif |
||||
echo "" |
||||
for i in range(len(a:signatures)) |
||||
call s:echo_signature(a:signatures[i]) |
||||
if i != len(a:signatures) - 1 |
||||
echon "\n" |
||||
endif |
||||
endfor |
||||
endfunction |
||||
|
||||
function! s:echo_signature(parts) |
||||
for part in a:parts |
||||
let hl = get(part, 'type', 'Normal') |
||||
let text = get(part, 'text', '') |
||||
if !empty(text) |
||||
execute 'echohl '.hl |
||||
execute "echon '".substitute(text, "'", "''", 'g')."'" |
||||
echohl None |
||||
endif |
||||
endfor |
||||
endfunction |
||||
|
||||
function! coc#ui#iterm_open(dir) |
||||
return s:osascript( |
||||
\ 'if application "iTerm2" is not running', |
||||
\ 'error', |
||||
\ 'end if') && s:osascript( |
||||
\ 'tell application "iTerm2"', |
||||
\ 'tell current window', |
||||
\ 'create tab with default profile', |
||||
\ 'tell current session', |
||||
\ 'write text "cd ' . a:dir . '"', |
||||
\ 'write text "clear"', |
||||
\ 'activate', |
||||
\ 'end tell', |
||||
\ 'end tell', |
||||
\ 'end tell') |
||||
endfunction |
||||
|
||||
function! s:osascript(...) abort |
||||
let args = join(map(copy(a:000), '" -e ".shellescape(v:val)'), '') |
||||
call s:system('osascript'. args) |
||||
return !v:shell_error |
||||
endfunction |
||||
|
||||
function! s:system(cmd) |
||||
let output = system(a:cmd) |
||||
if v:shell_error && output !=# "" |
||||
echohl Error | echom output | echohl None |
||||
return |
||||
endif |
||||
return output |
||||
endfunction |
||||
|
||||
function! coc#ui#set_lines(bufnr, changedtick, original, replacement, start, end, changes, cursor, col) abort |
||||
if !bufloaded(a:bufnr) |
||||
return |
||||
endif |
||||
let delta = 0 |
||||
if !empty(a:col) |
||||
let delta = col('.') - a:col |
||||
endif |
||||
if getbufvar(a:bufnr, 'changedtick') > a:changedtick && bufnr('%') == a:bufnr |
||||
" try apply current line change |
||||
let lnum = line('.') |
||||
" change for current line |
||||
if a:end - a:start == 1 && a:end == lnum && len(a:replacement) == 1 |
||||
let idx = a:start - lnum + 1 |
||||
let previous = get(a:original, idx, 0) |
||||
if type(previous) == 1 |
||||
let content = getline('.') |
||||
if previous !=# content |
||||
let diff = coc#string#diff(content, previous, col('.')) |
||||
let changed = get(a:replacement, idx, 0) |
||||
if type(changed) == 1 && strcharpart(previous, 0, diff['end']) ==# strcharpart(changed, 0, diff['end']) |
||||
let applied = coc#string#apply(changed, diff) |
||||
let replacement = copy(a:replacement) |
||||
let replacement[idx] = applied |
||||
call coc#compat#buf_set_lines(a:bufnr, a:start, a:end, replacement) |
||||
return |
||||
endif |
||||
endif |
||||
endif |
||||
endif |
||||
endif |
||||
if exists('*nvim_buf_set_text') && !empty(a:changes) |
||||
for item in reverse(copy(a:changes)) |
||||
call nvim_buf_set_text(a:bufnr, item[1], item[2], item[3], item[4], item[0]) |
||||
endfor |
||||
else |
||||
call coc#compat#buf_set_lines(a:bufnr, a:start, a:end, a:replacement) |
||||
endif |
||||
if !empty(a:cursor) |
||||
call cursor(a:cursor[0], a:cursor[1] + delta) |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#ui#change_lines(bufnr, list) abort |
||||
if !bufloaded(a:bufnr) | return v:null | endif |
||||
undojoin |
||||
if exists('*setbufline') |
||||
for [lnum, line] in a:list |
||||
call setbufline(a:bufnr, lnum + 1, line) |
||||
endfor |
||||
elseif a:bufnr == bufnr('%') |
||||
for [lnum, line] in a:list |
||||
call setline(lnum + 1, line) |
||||
endfor |
||||
else |
||||
let bufnr = bufnr('%') |
||||
exe 'noa buffer '.a:bufnr |
||||
for [lnum, line] in a:list |
||||
call setline(lnum + 1, line) |
||||
endfor |
||||
exe 'noa buffer '.bufnr |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#ui#open_url(url) |
||||
if has('mac') && executable('open') |
||||
call system('open '.a:url) |
||||
return |
||||
endif |
||||
if executable('xdg-open') |
||||
call system('xdg-open '.a:url) |
||||
return |
||||
endif |
||||
call system('cmd /c start "" /b '. substitute(a:url, '&', '^&', 'g')) |
||||
if v:shell_error |
||||
echohl Error | echom 'Failed to open '.a:url | echohl None |
||||
return |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#ui#rename_file(oldPath, newPath, write) abort |
||||
let bufnr = bufnr(a:oldPath) |
||||
if bufnr == -1 |
||||
throw 'Unable to get bufnr of '.a:oldPath |
||||
endif |
||||
if a:oldPath =~? a:newPath && (s:is_mac || s:is_win) |
||||
return coc#ui#safe_rename(bufnr, a:oldPath, a:newPath, a:write) |
||||
endif |
||||
if bufloaded(a:newPath) |
||||
execute 'silent bdelete! '.bufnr(a:newPath) |
||||
endif |
||||
let current = bufnr == bufnr('%') |
||||
let bufname = fnamemodify(a:newPath, ":~:.") |
||||
let filepath = fnamemodify(bufname(bufnr), '%:p') |
||||
let winid = coc#compat#buf_win_id(bufnr) |
||||
let curr = -1 |
||||
if winid == -1 |
||||
let curr = win_getid() |
||||
let file = fnamemodify(bufname(bufnr), ':.') |
||||
execute 'keepalt tab drop '.fnameescape(bufname(bufnr)) |
||||
let winid = win_getid() |
||||
endif |
||||
call coc#compat#execute(winid, 'keepalt file '.fnameescape(bufname), 'silent') |
||||
call coc#compat#execute(winid, 'doautocmd BufEnter') |
||||
if a:write |
||||
call coc#compat#execute(winid, 'noa write!', 'silent') |
||||
call delete(filepath, '') |
||||
endif |
||||
if curr != -1 |
||||
call win_gotoid(curr) |
||||
endif |
||||
return bufnr |
||||
endfunction |
||||
|
||||
" System is case in sensitive and newPath have different case. |
||||
function! coc#ui#safe_rename(bufnr, oldPath, newPath, write) abort |
||||
let winid = win_getid() |
||||
let lines = getbufline(a:bufnr, 1, '$') |
||||
execute 'keepalt tab drop '.fnameescape(fnamemodify(a:oldPath, ':.')) |
||||
let view = winsaveview() |
||||
execute 'keepalt bwipeout! '.a:bufnr |
||||
if a:write |
||||
call delete(a:oldPath, '') |
||||
endif |
||||
execute 'keepalt edit '.fnameescape(fnamemodify(a:newPath, ':~:.')) |
||||
let bufnr = bufnr('%') |
||||
call coc#compat#buf_set_lines(bufnr, 0, -1, lines) |
||||
if a:write |
||||
execute 'noa write' |
||||
endif |
||||
call winrestview(view) |
||||
call win_gotoid(winid) |
||||
return bufnr |
||||
endfunction |
||||
|
||||
function! coc#ui#sign_unplace() abort |
||||
if exists('*sign_unplace') |
||||
for group in s:sign_groups |
||||
call sign_unplace(group) |
||||
endfor |
||||
endif |
||||
endfunction |
||||
|
||||
function! coc#ui#update_signs(bufnr, group, signs) abort |
||||
if !s:sign_api || !bufloaded(a:bufnr) |
||||
return |
||||
endif |
||||
call sign_unplace(a:group, {'buffer': a:bufnr}) |
||||
for def in a:signs |
||||
let opts = {'lnum': def['lnum']} |
||||
if has_key(def, 'priority') |
||||
let opts['priority'] = def['priority'] |
||||
endif |
||||
call sign_place(0, a:group, def['name'], a:bufnr, opts) |
||||
endfor |
||||
endfunction |
||||
|
||||
function! coc#ui#outline_preview(config) abort |
||||
let view_id = get(w:, 'cocViewId', '') |
||||
if view_id !=# 'OUTLINE' |
||||
return |
||||
endif |
||||
let wininfo = get(getwininfo(win_getid()), 0, v:null) |
||||
if empty(wininfo) |
||||
return |
||||
endif |
||||
let border = get(a:config, 'border', v:true) |
||||
let th = &lines - &cmdheight - 2 |
||||
let range = a:config['range'] |
||||
let height = min([range['end']['line'] - range['start']['line'] + 1, th - 4]) |
||||
let to_left = &columns - wininfo['wincol'] - wininfo['width'] < wininfo['wincol'] |
||||
let start_lnum = range['start']['line'] + 1 |
||||
let end_lnum = range['end']['line'] + 1 - start_lnum > &lines ? start_lnum + &lines : range['end']['line'] + 1 |
||||
let lines = getbufline(a:config['bufnr'], start_lnum, end_lnum) |
||||
let content_width = max(map(copy(lines), 'strdisplaywidth(v:val)')) |
||||
let width = min([content_width, a:config['maxWidth'], to_left ? wininfo['wincol'] - 3 : &columns - wininfo['wincol'] - wininfo['width']]) |
||||
let filetype = getbufvar(a:config['bufnr'], '&filetype') |
||||
let cursor_row = coc#cursor#screen_pos()[0] |
||||
let config = { |
||||
\ 'relative': 'editor', |
||||
\ 'row': cursor_row - 1 + height < th ? cursor_row - (border ? 1 : 0) : th - height - (border ? 1 : -1), |
||||
\ 'col': to_left ? wininfo['wincol'] - 4 - width : wininfo['wincol'] + wininfo['width'], |
||||
\ 'width': width, |
||||
\ 'height': height, |
||||
\ 'lines': lines, |
||||
\ 'border': border ? [1,1,1,1] : v:null, |
||||
\ 'rounded': get(a:config, 'rounded', 1) ? 1 : 0, |
||||
\ 'winblend': a:config['winblend'], |
||||
\ 'highlight': a:config['highlight'], |
||||
\ 'borderhighlight': a:config['borderhighlight'], |
||||
\ } |
||||
let winid = coc#float#get_float_by_kind('outline-preview') |
||||
let result = coc#float#create_float_win(winid, s:outline_preview_bufnr, config) |
||||
if empty(result) |
||||
return v:null |
||||
endif |
||||
call setwinvar(result[0], 'kind', 'outline-preview') |
||||
let s:outline_preview_bufnr = result[1] |
||||
if !empty(filetype) |
||||
call coc#compat#execute(result[0], 'setfiletype '.filetype) |
||||
endif |
||||
return result[1] |
||||
endfunction |
||||
|
||||
function! coc#ui#outline_close_preview() abort |
||||
let winid = coc#float#get_float_by_kind('outline-preview') |
||||
if winid |
||||
call coc#float#close(winid) |
||||
endif |
||||
endfunction |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
let s:is_vim = !has('nvim') |
||||
let s:virtual_text_support = has('nvim-0.5.0') || has('patch-9.0.0067') |
||||
let s:text_options = has('patch-9.0.0121') |
||||
|
||||
" opts.hl_mode default to 'combine'. |
||||
" opts.col not used on neovim. |
||||
" opts.virt_text_win_col neovim only. |
||||
" opts.text_align could be 'after' 'right' 'below', vim9 only. |
||||
" opts.text_wrap could be 'wrap' and 'truncate', vim9 only. |
||||
function! coc#vtext#add(bufnr, src_id, line, blocks, opts) abort |
||||
if !s:virtual_text_support |
||||
return |
||||
endif |
||||
if s:is_vim |
||||
for [text, hl] in a:blocks |
||||
let type = coc#api#create_type(a:src_id, hl, a:opts) |
||||
let column = get(a:opts, 'col', 0) |
||||
let opts = { 'text': text, 'type': type } |
||||
if s:text_options && column == 0 |
||||
let opts['text_align'] = get(a:opts, 'text_align', 'after') |
||||
let opts['text_wrap'] = get(a:opts, 'text_wrap', 'truncate') |
||||
endif |
||||
call prop_add(a:line + 1, column, opts) |
||||
endfor |
||||
else |
||||
let opts = { |
||||
\ 'virt_text': a:blocks, |
||||
\ 'hl_mode': get(a:opts, 'hl_mode', 'combine'), |
||||
\ } |
||||
if has('nvim-0.5.1') && has_key(a:opts, 'virt_text_win_col') |
||||
let opts['virt_text_win_col'] = a:opts['virt_text_win_col'] |
||||
let opts['virt_text_pos'] = 'overlay' |
||||
endif |
||||
call nvim_buf_set_extmark(a:bufnr, a:src_id, a:line, 0, opts) |
||||
endif |
||||
endfunction |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
local api = vim.api |
||||
|
||||
local M = {} |
||||
|
||||
-- Get single line extmarks |
||||
function M.getHighlights(bufnr, key, s, e) |
||||
if not api.nvim_buf_is_loaded(bufnr) then |
||||
return nil |
||||
end |
||||
s = s or 0 |
||||
e = e or -1 |
||||
local max = e == -1 and api.nvim_buf_line_count(bufnr) or e + 1 |
||||
local ns = api.nvim_create_namespace('coc-' .. key) |
||||
local markers = api.nvim_buf_get_extmarks(bufnr, ns, {s, 0}, {e, -1}, {details = true}) |
||||
local res = {} |
||||
for _, mark in ipairs(markers) do |
||||
local id = mark[1] |
||||
local line = mark[2] |
||||
local startCol = mark[3] |
||||
local details = mark[4] |
||||
local endCol = details.end_col |
||||
if line < max then |
||||
local delta = details.end_row - line |
||||
if delta <= 1 and (delta == 0 or endCol == 0) then |
||||
if startCol == endCol then |
||||
api.nvim_buf_del_extmark(bufnr, ns, id) |
||||
else |
||||
if delta == 1 then |
||||
local text = api.nvim_buf_get_lines(bufnr, line, line + 1, false)[1] or '' |
||||
endCol = #text |
||||
end |
||||
table.insert(res, {details.hl_group, line, startCol, endCol, id}) |
||||
end |
||||
end |
||||
end |
||||
end |
||||
return res |
||||
end |
||||
|
||||
local function addHighlights(bufnr, ns, highlights, priority) |
||||
for _, items in ipairs(highlights) do |
||||
local hlGroup = items[1] |
||||
local line = items[2] |
||||
local startCol = items[3] |
||||
local endCol = items[4] |
||||
local hlMode = items[5] and 'combine' or 'replace' |
||||
-- Error: col value outside range |
||||
pcall(api.nvim_buf_set_extmark, bufnr, ns, line, startCol, { |
||||
end_col = endCol, |
||||
hl_group = hlGroup, |
||||
hl_mode = hlMode, |
||||
right_gravity = true, |
||||
priority = type(priority) == 'number' and math.min(priority, 4096) or 4096 |
||||
}) |
||||
end |
||||
end |
||||
|
||||
local function addHighlightTimer(bufnr, ns, highlights, priority, maxCount) |
||||
local hls = {} |
||||
local next = {} |
||||
for i, v in ipairs(highlights) do |
||||
if i < maxCount then |
||||
table.insert(hls, v) |
||||
else |
||||
table.insert(next, v) |
||||
end |
||||
end |
||||
addHighlights(bufnr, ns, hls, priority) |
||||
if #next > 0 then |
||||
vim.defer_fn(function() |
||||
addHighlightTimer(bufnr, ns, next, priority, maxCount) |
||||
end, 30) |
||||
end |
||||
end |
||||
|
||||
function M.set(bufnr, ns, highlights, priority) |
||||
local maxCount = vim.g.coc_highlight_maximum_count |
||||
if #highlights > maxCount then |
||||
addHighlightTimer(bufnr, ns, highlights, priority, maxCount) |
||||
else |
||||
addHighlights(bufnr, ns, highlights, priority) |
||||
end |
||||
end |
||||
|
||||
return M |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
" vim source for emails |
||||
function! coc#source#email#init() abort |
||||
return { |
||||
\ 'priority': 9, |
||||
\ 'shortcut': 'Email', |
||||
\ 'triggerCharacters': ['@'] |
||||
\} |
||||
endfunction |
||||
|
||||
function! coc#source#email#should_complete(opt) abort |
||||
return 1 |
||||
endfunction |
||||
|
||||
function! coc#source#email#complete(opt, cb) abort |
||||
let items = ['foo@gmail.com', 'bar@yahoo.com'] |
||||
call a:cb(items) |
||||
endfunction |
@ -0,0 +1,177 @@
@@ -0,0 +1,177 @@
|
||||
import path from 'path' |
||||
import { DidChangeConfigurationNotification, DocumentSelector } from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
import { SyncConfigurationFeature } from '../../language-client/configuration' |
||||
import { LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from '../../language-client/index' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
function createClient(section: string | string[] | undefined, middleware: Middleware = {}, opts: Partial<LanguageClientOptions> = {}): LanguageClient { |
||||
const serverModule = path.join(__dirname, './server/configServer.js') |
||||
const serverOptions: ServerOptions = { |
||||
run: { module: serverModule, transport: TransportKind.ipc }, |
||||
debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } |
||||
} |
||||
|
||||
const documentSelector: DocumentSelector = [{ scheme: 'file' }] |
||||
const clientOptions: LanguageClientOptions = Object.assign({ |
||||
documentSelector, |
||||
synchronize: { |
||||
configurationSection: section |
||||
}, |
||||
initializationOptions: {}, |
||||
middleware |
||||
}, opts) |
||||
|
||||
const result = new LanguageClient('test', 'Test Language Server', serverOptions, clientOptions) |
||||
return result |
||||
} |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('pull configuration feature', () => { |
||||
let client: LanguageClient |
||||
beforeAll(async () => { |
||||
client = createClient(undefined) |
||||
await client.start() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should request all configuration', async () => { |
||||
let config: any |
||||
client.middleware.workspace = client.middleware.workspace ?? {} |
||||
client.middleware.workspace.configuration = (params, token, next) => { |
||||
config = next(params, token) |
||||
return config |
||||
} |
||||
await client.sendNotification('pull0') |
||||
await helper.waitValue(() => { |
||||
return config != null |
||||
}, true) |
||||
expect(config[0].http).toBeDefined() |
||||
}) |
||||
|
||||
it('should request configurations with sections', async () => { |
||||
let config: any |
||||
client.middleware.workspace = client.middleware.workspace ?? {} |
||||
client.middleware.workspace.configuration = (params, token, next) => { |
||||
config = next(params, token) |
||||
return config |
||||
} |
||||
await client.sendNotification('pull1') |
||||
await helper.waitValue(() => { |
||||
return config?.length |
||||
}, 3) |
||||
expect(config[1]).toBeNull() |
||||
expect(config[0].proxy).toBeDefined() |
||||
expect(config[2]).toBeNull() |
||||
}) |
||||
}) |
||||
|
||||
describe('publish configuration feature', () => { |
||||
it('should send configuration for languageserver', async () => { |
||||
let client: LanguageClient |
||||
client = createClient('languageserver.cpp.settings') |
||||
let changed |
||||
client.onNotification('configurationChange', params => { |
||||
changed = params |
||||
}) |
||||
await client.start() |
||||
await helper.waitValue(() => { |
||||
return changed != null |
||||
}, true) |
||||
expect(changed).toEqual({ settings: {} }) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should get configuration from workspace folder', async () => { |
||||
let folder = path.resolve(__dirname, '../sample') |
||||
workspace.workspaceFolderControl.addWorkspaceFolder(folder, false) |
||||
let configFilePath = path.join(folder, '.vim/coc-settings.json') |
||||
workspace.configurations.addFolderFile(configFilePath, false, folder) |
||||
let client = createClient('coc.preferences', {}, { |
||||
workspaceFolder: { name: 'sample', uri: URI.file(folder).toString() } |
||||
}) |
||||
let changed |
||||
client.onNotification('configurationChange', params => { |
||||
changed = params |
||||
}) |
||||
await client.start() |
||||
await helper.waitValue(() => { |
||||
return changed != null |
||||
}, true) |
||||
expect(changed.settings.coc.preferences.rootPath).toBe('./src') |
||||
workspace.workspaceFolderControl.removeWorkspaceFolder(folder) |
||||
let feature = client.getFeature(DidChangeConfigurationNotification.method) |
||||
feature.dispose() |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should send configuration for specific sections', async () => { |
||||
let client: LanguageClient |
||||
let called = false |
||||
client = createClient(['coc.preferences', 'npm', 'unknown'], { |
||||
workspace: { |
||||
didChangeConfiguration: (sections, next) => { |
||||
called = true |
||||
return next(sections) |
||||
} |
||||
} |
||||
}) |
||||
let changed |
||||
client.onNotification('configurationChange', params => { |
||||
changed = params |
||||
}) |
||||
await client.start() |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await helper.waitValue(() => { |
||||
return changed != null |
||||
}, true) |
||||
expect(changed.settings.coc).toBeDefined() |
||||
expect(changed.settings.npm).toBeDefined() |
||||
let { configurations } = workspace |
||||
configurations.updateMemoryConfig({ 'npm.binPath': 'cnpm' }) |
||||
await helper.waitValue(() => { |
||||
return changed.settings.npm?.binPath |
||||
}, 'cnpm') |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should catch reject error', async () => { |
||||
let client: LanguageClient |
||||
let called = false |
||||
client = createClient(['cpp'], { |
||||
workspace: { |
||||
didChangeConfiguration: () => { |
||||
return Promise.reject(new Error('custom error')) |
||||
} |
||||
} |
||||
}) |
||||
let changed |
||||
client.onNotification('configurationChange', params => { |
||||
changed = params |
||||
}) |
||||
await client.start() |
||||
await helper.wait(50) |
||||
expect(called).toBe(false) |
||||
void client.stop() |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should extractSettingsInformation', async () => { |
||||
let res = SyncConfigurationFeature.extractSettingsInformation(['http.proxy', 'http.proxyCA']) |
||||
expect(res.http).toBeDefined() |
||||
expect(res.http.proxy).toBeDefined() |
||||
}) |
||||
}) |
@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
|
||||
import { Duplex } from 'stream' |
||||
import { createProtocolConnection, ProgressType, DocumentSymbolParams, DocumentSymbolRequest, InitializeParams, InitializeRequest, InitializeResult, ProtocolConnection, StreamMessageReader, StreamMessageWriter } from 'vscode-languageserver-protocol/node' |
||||
import { SymbolInformation, SymbolKind } from 'vscode-languageserver-types' |
||||
import { NullLogger } from '../../language-client/client' |
||||
|
||||
class TestStream extends Duplex { |
||||
public _write(chunk: string, _encoding: string, done: () => void): void { |
||||
this.emit('data', chunk) |
||||
done() |
||||
} |
||||
|
||||
public _read(_size: number): void { |
||||
} |
||||
} |
||||
|
||||
let serverConnection: ProtocolConnection |
||||
let clientConnection: ProtocolConnection |
||||
let progressType: ProgressType<any> = new ProgressType() |
||||
|
||||
beforeEach(() => { |
||||
const up = new TestStream() |
||||
const down = new TestStream() |
||||
const logger = new NullLogger() |
||||
serverConnection = createProtocolConnection(new StreamMessageReader(up), new StreamMessageWriter(down), logger) |
||||
clientConnection = createProtocolConnection(new StreamMessageReader(down), new StreamMessageWriter(up), logger) |
||||
serverConnection.listen() |
||||
clientConnection.listen() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
serverConnection.dispose() |
||||
clientConnection.dispose() |
||||
}) |
||||
|
||||
describe('Connection Tests', () => { |
||||
it('should ensure proper param passing', async () => { |
||||
let paramsCorrect = false |
||||
serverConnection.onRequest(InitializeRequest.type, params => { |
||||
paramsCorrect = !Array.isArray(params) |
||||
let result: InitializeResult = { |
||||
capabilities: { |
||||
} |
||||
} |
||||
return result |
||||
}) |
||||
|
||||
const init: InitializeParams = { |
||||
rootUri: 'file:///home/dirkb', |
||||
processId: 1, |
||||
capabilities: {}, |
||||
workspaceFolders: null, |
||||
} |
||||
await clientConnection.sendRequest(InitializeRequest.type, init) |
||||
expect(paramsCorrect).toBe(true) |
||||
}) |
||||
|
||||
it('should provide token', async () => { |
||||
serverConnection.onRequest(DocumentSymbolRequest.type, params => { |
||||
expect(params.partialResultToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') |
||||
return [] |
||||
}) |
||||
|
||||
const params: DocumentSymbolParams = { |
||||
textDocument: { uri: 'file:///abc.txt' }, |
||||
partialResultToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' |
||||
} |
||||
await clientConnection.sendRequest(DocumentSymbolRequest.type, params) |
||||
}) |
||||
|
||||
it('should report result', async () => { |
||||
let result: SymbolInformation = { |
||||
name: 'abc', |
||||
kind: SymbolKind.Class, |
||||
location: { |
||||
uri: 'file:///abc.txt', |
||||
range: { start: { line: 0, character: 1 }, end: { line: 2, character: 3 } } |
||||
} |
||||
} |
||||
serverConnection.onRequest(DocumentSymbolRequest.type, async params => { |
||||
expect(params.partialResultToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') |
||||
await serverConnection.sendProgress(progressType, params.partialResultToken, [result]) |
||||
return [] |
||||
}) |
||||
|
||||
const params: DocumentSymbolParams = { |
||||
textDocument: { uri: 'file:///abc.txt' }, |
||||
partialResultToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' |
||||
} |
||||
let progressOK = false |
||||
clientConnection.onProgress(progressType, '3b1db4c9-e011-489e-a9d1-0653e64707c2', values => { |
||||
progressOK = (values !== undefined && values.length === 1) |
||||
}) |
||||
await clientConnection.sendRequest(DocumentSymbolRequest.type, params) |
||||
expect(progressOK).toBeTruthy() |
||||
}) |
||||
|
||||
it('should provide workDoneToken', async () => { |
||||
serverConnection.onRequest(DocumentSymbolRequest.type, params => { |
||||
expect(params.workDoneToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') |
||||
return [] |
||||
}) |
||||
|
||||
const params: DocumentSymbolParams = { |
||||
textDocument: { uri: 'file:///abc.txt' }, |
||||
workDoneToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' |
||||
} |
||||
await clientConnection.sendRequest(DocumentSymbolRequest.type, params) |
||||
}) |
||||
|
||||
it('should report work done progress', async () => { |
||||
serverConnection.onRequest(DocumentSymbolRequest.type, async params => { |
||||
expect(params.workDoneToken).toBe('3b1db4c9-e011-489e-a9d1-0653e64707c2') |
||||
await serverConnection.sendProgress(progressType, params.workDoneToken, { |
||||
kind: 'begin', |
||||
title: 'progress' |
||||
}) |
||||
await serverConnection.sendProgress(progressType, params.workDoneToken, { |
||||
kind: 'report', |
||||
message: 'message' |
||||
}) |
||||
await serverConnection.sendProgress(progressType, params.workDoneToken, { |
||||
kind: 'end', |
||||
message: 'message' |
||||
}) |
||||
return [] |
||||
}) |
||||
|
||||
const params: DocumentSymbolParams = { |
||||
textDocument: { uri: 'file:///abc.txt' }, |
||||
workDoneToken: '3b1db4c9-e011-489e-a9d1-0653e64707c2' |
||||
} |
||||
let result = '' |
||||
clientConnection.onProgress(progressType, '3b1db4c9-e011-489e-a9d1-0653e64707c2', value => { |
||||
result += value.kind |
||||
}) |
||||
await clientConnection.sendRequest(DocumentSymbolRequest.type, params) |
||||
expect(result).toBe('beginreportend') |
||||
}) |
||||
}) |
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
import { CompletionTriggerKind, Position, TextDocumentItem, TextDocumentSaveReason } from 'vscode-languageserver-protocol' |
||||
import { TextDocument } from 'vscode-languageserver-textdocument' |
||||
import { URI } from 'vscode-uri' |
||||
import * as cv from '../../language-client/utils/converter' |
||||
|
||||
describe('converter', () => { |
||||
|
||||
function createDocument(): TextDocument { |
||||
return TextDocument.create('file:///1', 'css', 1, '') |
||||
} |
||||
|
||||
it('should convertToTextDocumentItem', () => { |
||||
let doc = createDocument() |
||||
expect(cv.convertToTextDocumentItem(doc).uri).toBe(doc.uri) |
||||
expect(TextDocumentItem.is(cv.convertToTextDocumentItem(doc))).toBe(true) |
||||
}) |
||||
|
||||
it('should asCloseTextDocumentParams', () => { |
||||
let doc = createDocument() |
||||
expect(cv.asCloseTextDocumentParams(doc).textDocument.uri).toBe(doc.uri) |
||||
}) |
||||
|
||||
it('should asRelativePattern', async () => { |
||||
let rp = cv.asRelativePattern({ |
||||
baseUri: 'file:///a', |
||||
pattern: '**/*' |
||||
}) |
||||
expect(rp.baseUri.fsPath).toBe('/a') |
||||
rp = cv.asRelativePattern({ |
||||
baseUri: { uri: 'file:///a', name: 'name' }, |
||||
pattern: '**/*' |
||||
}) |
||||
expect(rp.baseUri.fsPath).toBe('/a') |
||||
}) |
||||
|
||||
it('should asChangeTextDocumentParams', () => { |
||||
let doc = createDocument() |
||||
expect(cv.asFullChangeTextDocumentParams(doc).textDocument.uri).toBe(doc.uri) |
||||
}) |
||||
|
||||
it('should asWillSaveTextDocumentParams', () => { |
||||
let res = cv.asWillSaveTextDocumentParams({ document: createDocument(), reason: TextDocumentSaveReason.Manual, waitUntil: () => {} }) |
||||
expect(res.textDocument).toBeDefined() |
||||
expect(res.reason).toBeDefined() |
||||
}) |
||||
|
||||
it('should asVersionedTextDocumentIdentifier', () => { |
||||
let res = cv.asVersionedTextDocumentIdentifier(createDocument()) |
||||
expect(res.uri).toBeDefined() |
||||
expect(res.version).toBeDefined() |
||||
}) |
||||
|
||||
it('should asSaveTextDocumentParams', () => { |
||||
let res = cv.asSaveTextDocumentParams(createDocument(), true) |
||||
expect(res.textDocument.uri).toBeDefined() |
||||
expect(res.text).toBeDefined() |
||||
res = cv.asSaveTextDocumentParams(createDocument(), false) |
||||
expect(res.text).toBeUndefined() |
||||
}) |
||||
|
||||
it('should asUri', () => { |
||||
let uri = URI.file('/tmp/a') |
||||
expect(cv.asUri(uri)).toBe(uri.toString()) |
||||
}) |
||||
|
||||
it('should asCompletionParams', () => { |
||||
let params = cv.asCompletionParams(createDocument(), Position.create(0, 0), { triggerKind: CompletionTriggerKind.Invoked }) |
||||
expect(params.textDocument).toBeDefined() |
||||
expect(params.position).toBeDefined() |
||||
expect(params.context).toBeDefined() |
||||
}) |
||||
|
||||
it('should asTextDocumentPositionParams', () => { |
||||
let params = cv.asTextDocumentPositionParams(createDocument(), Position.create(0, 0)) |
||||
expect(params.textDocument).toBeDefined() |
||||
expect(params.position).toBeDefined() |
||||
}) |
||||
|
||||
it('should asTextDocumentIdentifier', () => { |
||||
let doc = cv.asTextDocumentIdentifier(createDocument()) |
||||
expect(doc.uri).toBeDefined() |
||||
}) |
||||
|
||||
it('should asReferenceParams', () => { |
||||
let params = cv.asReferenceParams(createDocument(), Position.create(0, 0), { includeDeclaration: false }) |
||||
expect(params.textDocument.uri).toBeDefined() |
||||
expect(params.position).toBeDefined() |
||||
}) |
||||
|
||||
it('should asDocumentSymbolParams', () => { |
||||
let doc = cv.asDocumentSymbolParams(createDocument()) |
||||
expect(doc.textDocument.uri).toBeDefined() |
||||
}) |
||||
|
||||
it('should asCodeLensParams', () => { |
||||
let doc = cv.asCodeLensParams(createDocument()) |
||||
expect(doc.textDocument.uri).toBeDefined() |
||||
}) |
||||
}) |
@ -0,0 +1,334 @@
@@ -0,0 +1,334 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { v4 as uuid } from 'uuid' |
||||
import { CancellationToken, DocumentDiagnosticRequest, Position, TextEdit } from 'vscode-languageserver-protocol' |
||||
import { TextDocument } from 'vscode-languageserver-textdocument' |
||||
import { URI } from 'vscode-uri' |
||||
import * as lsclient from '../../language-client' |
||||
import { BackgroundScheduler, DocumentPullStateTracker, PullState } from '../../language-client/diagnostic' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
function getId(uri: string): number { |
||||
let ms = uri.match(/\d+$/) |
||||
return ms ? Number(ms[0]) : undefined |
||||
} |
||||
|
||||
function createDocument(id: number, version = 1): TextDocument { |
||||
let uri = `file:///${id}` |
||||
return TextDocument.create(uri, '', version, '') |
||||
} |
||||
|
||||
function createUri(id: number): URI { |
||||
return URI.file(id.toString()) |
||||
} |
||||
|
||||
describe('BackgroundScheduler', () => { |
||||
it('should schedule documents by add', async () => { |
||||
let uris: string[] = [] |
||||
let s = new BackgroundScheduler({ |
||||
pull(document) { |
||||
uris.push(document.uri) |
||||
} |
||||
}) |
||||
s.add(createDocument(1)) |
||||
s.add(createDocument(1)) |
||||
s.add(createDocument(2)) |
||||
s.add(createDocument(3)) |
||||
await helper.waitValue(() => { |
||||
return uris.length |
||||
}, 3) |
||||
let ids = uris.map(u => getId(u)) |
||||
expect(ids).toEqual([1, 2, 3]) |
||||
}) |
||||
|
||||
it('should schedule documents by remove', async () => { |
||||
let uris: string[] = [] |
||||
let s = new BackgroundScheduler({ |
||||
pull(document) { |
||||
uris.push(document.uri) |
||||
} |
||||
}) |
||||
s.add(createDocument(1)) |
||||
s.add(createDocument(2)) |
||||
s.remove(createDocument(2)) |
||||
s.add(createDocument(3)) |
||||
s.remove(createDocument(3)) |
||||
s.remove(createDocument(1)) |
||||
await helper.waitValue(() => { |
||||
return uris.length |
||||
}, 3) |
||||
let ids = uris.map(u => getId(u)) |
||||
expect(ids).toEqual([2, 3, 1]) |
||||
s.dispose() |
||||
}) |
||||
}) |
||||
|
||||
describe('DocumentPullStateTracker', () => { |
||||
it('should track document', async () => { |
||||
let tracker = new DocumentPullStateTracker() |
||||
let state = tracker.track(PullState.document, createDocument(1)) |
||||
let other = tracker.track(PullState.document, createDocument(1)) |
||||
expect(state).toBe(other) |
||||
tracker.track(PullState.workspace, createDocument(3)) |
||||
let id = 'dcf06d3b-79f6-4a5e-bc8d-d3334f7b4cad' |
||||
tracker.update(PullState.document, createDocument(1, 2), id) |
||||
tracker.update(PullState.document, createDocument(2, 2), 'f758ae47-c94e-406e-ba41-0f3bb2fe4fc7') |
||||
let curr = tracker.getResultId(PullState.document, createDocument(1, 2)) |
||||
expect(curr).toBe(id) |
||||
expect(tracker.getResultId(PullState.workspace, createDocument(1, 2))).toBeUndefined() |
||||
tracker.unTrack(PullState.document, createDocument(2, 2)) |
||||
expect(tracker.trackingDocuments()).toEqual(['file:///1']) |
||||
tracker.update(PullState.workspace, createDocument(3, 2), 'fcb905e2-8edb-4239-9150-198c8175ed4a') |
||||
tracker.update(PullState.workspace, createDocument(1, 2), 'fe96d175-c19f-4705-bff1-101bf83b2953') |
||||
expect(tracker.tracks(PullState.workspace, createDocument(3, 1))).toBe(true) |
||||
expect(tracker.tracks(PullState.document, createDocument(4, 1))).toBe(false) |
||||
let res = tracker.getAllResultIds() |
||||
expect(res.length).toBe(2) |
||||
}) |
||||
|
||||
it('should track URI', async () => { |
||||
let tracker = new DocumentPullStateTracker() |
||||
let state = tracker.track(PullState.document, createUri(1), undefined) |
||||
let other = tracker.track(PullState.document, createUri(1), undefined) |
||||
expect(state).toBe(other) |
||||
tracker.track(PullState.workspace, createUri(3), undefined) |
||||
let id = 'dcf06d3b-79f6-4a5e-bc8d-d3334f7b4cad' |
||||
tracker.update(PullState.document, createUri(1), undefined, id) |
||||
tracker.update(PullState.document, createUri(2), undefined, 'f758ae47-c94e-406e-ba41-0f3bb2fe4fc7') |
||||
let curr = tracker.getResultId(PullState.document, createUri(1)) |
||||
expect(curr).toBe(id) |
||||
tracker.unTrack(PullState.document, createUri(2)) |
||||
expect(tracker.trackingDocuments()).toEqual(['file:///1']) |
||||
tracker.update(PullState.workspace, createUri(3), undefined, undefined) |
||||
tracker.update(PullState.workspace, createUri(1), undefined, 'fe96d175-c19f-4705-bff1-101bf83b2953') |
||||
expect(tracker.tracks(PullState.workspace, createUri(3))).toBe(true) |
||||
expect(tracker.tracks(PullState.document, createUri(4))).toBe(false) |
||||
let res = tracker.getAllResultIds() |
||||
expect(res.length).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('DiagnosticFeature', () => { |
||||
let nvim: Neovim |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = workspace.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
async function createServer(interFileDependencies: boolean, workspaceDiagnostics = false, middleware: lsclient.Middleware = {}, fun?: (opt: lsclient.LanguageClientOptions) => void) { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
documentSelector: [{ language: '*' }], |
||||
middleware, |
||||
initializationOptions: { |
||||
interFileDependencies: interFileDependencies == true, |
||||
workspaceDiagnostics |
||||
} |
||||
} |
||||
if (fun) fun(clientOptions) |
||||
let serverModule = path.join(__dirname, './server/diagnosticServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.ipc |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
return client |
||||
} |
||||
|
||||
function getUri(s: number | string): string { |
||||
let fsPath = path.join(os.tmpdir(), s.toString()) |
||||
return URI.file(fsPath).toString() |
||||
} |
||||
|
||||
it('should work when change visible editor', async () => { |
||||
let doc = await workspace.loadFile(getUri(1), 'edit') |
||||
await workspace.loadFile(getUri(3), 'tabe') |
||||
let client = await createServer(true) |
||||
await helper.wait(30) |
||||
await workspace.loadFile(getUri(2), 'edit') |
||||
await helper.wait(30) |
||||
await workspace.loadFile(getUri(3), 'tabe') |
||||
await helper.wait(30) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
expect(feature).toBeDefined() |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
let res = provider.knows(PullState.document, doc.textDocument) |
||||
expect(res).toBe(false) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should filter by document selector', async () => { |
||||
let client = await createServer(true, false, {}, opt => { |
||||
opt.documentSelector = [{ language: 'vim' }] |
||||
}) |
||||
let doc = await workspace.loadFile(getUri(1), 'edit') |
||||
await helper.wait(10) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
let provider = feature.getProvider(TextDocument.create('file:///1', 'vim', 1, '')) |
||||
let res = provider.knows(PullState.document, doc.textDocument) |
||||
expect(res).toBe(false) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should filter by ignore', async () => { |
||||
let client = await createServer(true, false, {}, opt => { |
||||
opt.diagnosticPullOptions = { |
||||
ignored: ['**/*.ts'] |
||||
} |
||||
}) |
||||
let doc = await workspace.loadFile(getUri('a.ts'), 'edit') |
||||
await helper.wait(10) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
let res = provider.knows(PullState.document, doc.textDocument) |
||||
expect(res).toBe(false) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should not throw on request error', async () => { |
||||
let client = await createServer(true) |
||||
await workspace.loadFile(getUri('error'), 'edit') |
||||
await workspace.loadFile(getUri('cancel'), 'tabe') |
||||
await workspace.loadFile(getUri('retrigger'), 'tabe') |
||||
await helper.wait(10) |
||||
await nvim.command('normal! 2gt') |
||||
await workspace.loadFile(getUri('unchanged'), 'edit') |
||||
await helper.wait(20) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should pull diagnostic on change', async () => { |
||||
let doc = await workspace.loadFile(getUri('change'), 'edit') |
||||
let client = await createServer(true, false, {}, opt => { |
||||
opt.diagnosticPullOptions = { |
||||
onChange: true, |
||||
filter: doc => { |
||||
return doc.uri.endsWith('filtered') |
||||
} |
||||
} |
||||
}) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
await helper.waitValue(() => { |
||||
return provider.knows(PullState.document, doc.textDocument) |
||||
}, true) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) |
||||
await helper.wait(30) |
||||
let changeCount = await client.sendRequest('getChangeCount') |
||||
expect(changeCount).toBe(2) |
||||
await nvim.call('setline', [1, 'foo']) |
||||
let d = await workspace.loadFile(getUri('filtered'), 'tabe') |
||||
await d.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) |
||||
await helper.wait(30) |
||||
await nvim.command(`bd! ${doc.bufnr}`) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should pull diagnostic on save', async () => { |
||||
let doc = await workspace.loadFile(getUri(uuid() + 'filtered'), 'edit') |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) |
||||
doc = await workspace.loadFile(getUri(uuid() + 'save'), 'tabe') |
||||
let client = await createServer(true, false, {}, opt => { |
||||
opt.diagnosticPullOptions = { |
||||
onSave: true, |
||||
filter: doc => { |
||||
return doc.uri.endsWith('filtered') |
||||
} |
||||
} |
||||
}) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
await helper.waitValue(() => { |
||||
return provider.knows(PullState.document, doc.textDocument) |
||||
}, true) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) |
||||
await nvim.command('wa') |
||||
await helper.wait(10) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should use provideDiagnostics middleware', async () => { |
||||
let called = false |
||||
let callHandle = false |
||||
let client = await createServer(true, false, { |
||||
provideDiagnostics: (doc, id, token, next) => { |
||||
called = true |
||||
return next(doc, id, token) |
||||
}, |
||||
handleDiagnostics: (uri, diagnostics, next) => { |
||||
callHandle = true |
||||
return next(uri, diagnostics) |
||||
} |
||||
}) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
expect(feature).toBeDefined() |
||||
let textDocument = TextDocument.create(getUri('empty'), 'e', 1, '') |
||||
let provider = feature.getProvider(textDocument) |
||||
let res = await provider.diagnostics.provideDiagnostics(textDocument, '', CancellationToken.None) |
||||
expect(called).toBe(true) |
||||
expect(res).toEqual({ kind: 'full', items: [] }) |
||||
await helper.waitValue(() => { |
||||
return callHandle |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should use provideWorkspaceDiagnostics middleware', async () => { |
||||
let called = false |
||||
let client = await createServer(false, true, { |
||||
provideWorkspaceDiagnostics: (resultIds, token, resultReporter, next) => { |
||||
called = true |
||||
return next(resultIds, token, resultReporter) |
||||
} |
||||
}) |
||||
expect(called).toBe(true) |
||||
await helper.waitValue(async () => { |
||||
let count = await client.sendRequest('getWorkspceCount') |
||||
return count > 1 |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should receive partial result', async () => { |
||||
let client = await createServer(false, true, {}, opt => { |
||||
opt.diagnosticPullOptions = { |
||||
workspace: false |
||||
} |
||||
}) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
let textDocument = TextDocument.create(getUri('empty'), 'e', 1, '') |
||||
let provider = feature.getProvider(textDocument) |
||||
let n = 0 |
||||
await provider.diagnostics.provideWorkspaceDiagnostics([{ uri: 'uri', value: '1' }], CancellationToken.None, chunk => { |
||||
n++ |
||||
}) |
||||
expect(n).toBe(4) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should fire refresh event', async () => { |
||||
let client = await createServer(true, false, {}) |
||||
let feature = client.getFeature(DocumentDiagnosticRequest.method) |
||||
let textDocument = TextDocument.create(getUri('1'), 'e', 1, '') |
||||
let provider = feature.getProvider(textDocument) |
||||
let called = false |
||||
provider.onDidChangeDiagnosticsEmitter.event(() => { |
||||
called = true |
||||
}) |
||||
await client.sendNotification('fireRefresh') |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
}) |
@ -0,0 +1,393 @@
@@ -0,0 +1,393 @@
|
||||
import path from 'path' |
||||
import { CancellationToken, CodeActionRequest, CodeLensRequest, CompletionRequest, DidChangeWorkspaceFoldersNotification, DidCreateFilesNotification, DidDeleteFilesNotification, DidRenameFilesNotification, DocumentSymbolRequest, ExecuteCommandRequest, InlineValueRequest, Position, Range, RenameRequest, SemanticTokensRegistrationType, SymbolInformation, SymbolKind, WillDeleteFilesRequest, WillRenameFilesRequest, WorkspaceFolder, WorkspaceSymbolRequest } from 'vscode-languageserver-protocol' |
||||
import { TextDocument } from 'vscode-languageserver-textdocument' |
||||
import * as lsclient from '../../language-client' |
||||
import helper from '../helper' |
||||
import commands from '../../commands' |
||||
import { URI } from 'vscode-uri' |
||||
import workspace from '../../workspace' |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('DynamicFeature', () => { |
||||
let textDocument = TextDocument.create('file:///1', 'vim', 1, '\n') |
||||
let position = Position.create(1, 1) |
||||
let token = CancellationToken.None |
||||
|
||||
async function startServer(opts: any = {}, middleware: lsclient.Middleware = {}): Promise<lsclient.LanguageClient> { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
documentSelector: [{ language: '*' }], |
||||
initializationOptions: opts, |
||||
synchronize: { |
||||
configurationSection: 'languageserver.vim.settings' |
||||
}, |
||||
middleware |
||||
} |
||||
let serverModule = path.join(__dirname, './server/dynamicServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.ipc |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
return client |
||||
} |
||||
|
||||
describe('RenameFeature', () => { |
||||
it('should start server', async () => { |
||||
let client = await startServer({ prepareRename: false }) |
||||
let feature = client.getFeature(RenameRequest.method) |
||||
let provider = feature.getProvider(textDocument) |
||||
expect(provider.prepareRename).toBeUndefined() |
||||
feature.unregister('') |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should handle different result', async () => { |
||||
let client = await startServer({ prepareRename: true }, { |
||||
provideRenameEdits: (doc, pos, newName, token, next) => { |
||||
return next(doc, pos, newName, token) |
||||
}, |
||||
prepareRename: (doc, pos, token, next) => { |
||||
return next(doc, pos, token) |
||||
} |
||||
}) |
||||
let feature = client.getFeature(RenameRequest.method) |
||||
let provider = feature.getProvider(textDocument) |
||||
expect(provider.prepareRename).toBeDefined() |
||||
let res = await provider.prepareRename(textDocument, position, token) |
||||
expect(res).toBeNull() |
||||
|
||||
await client.sendRequest('setPrepareResponse', { defaultBehavior: true }) |
||||
res = await provider.prepareRename(textDocument, position, token) |
||||
expect(res).toBeNull() |
||||
await client.sendRequest('setPrepareResponse', { range: Range.create(0, 0, 0, 3), placeholder: 'placeholder' }) |
||||
res = await provider.prepareRename(textDocument, position, token) |
||||
expect((res as any).placeholder).toBe('placeholder') |
||||
await expect(async () => { |
||||
await client.sendRequest('setPrepareResponse', { defaultBehavior: false }) |
||||
res = await provider.prepareRename(textDocument, position, token) |
||||
}).rejects.toThrow(Error) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('WorkspaceSymbolFeature', () => { |
||||
it('should use middleware', async () => { |
||||
let client = await startServer({}, { |
||||
provideWorkspaceSymbols: (query, token, next) => { |
||||
return next(query, token) |
||||
}, |
||||
resolveWorkspaceSymbol: (item, token, next) => { |
||||
return next(item, token) |
||||
} |
||||
}) |
||||
let feature = client.getFeature(WorkspaceSymbolRequest.method) |
||||
await helper.waitValue(() => { |
||||
return feature.getProviders().length |
||||
}, 2) |
||||
let provider = feature.getProviders().find(o => typeof o.resolveWorkspaceSymbol === 'function') |
||||
expect(provider).toBeDefined() |
||||
let token = CancellationToken.None |
||||
let res = await provider.provideWorkspaceSymbols('', token) |
||||
expect(res.length).toBe(0) |
||||
let sym = SymbolInformation.create('name', SymbolKind.Array, Range.create(0, 1, 0, 1), 'file:///1') |
||||
let resolved = await provider.resolveWorkspaceSymbol(sym, token) |
||||
expect(resolved.name).toBe(sym.name) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('SemanticTokensFeature', () => { |
||||
it('should register semanticTokens', async () => { |
||||
let client = await startServer({}) |
||||
let feature = client.getFeature(SemanticTokensRegistrationType.method) |
||||
let provider: any |
||||
await helper.waitValue(() => { |
||||
provider = feature.getProvider(textDocument) |
||||
return provider != null |
||||
}, true) |
||||
expect(provider.range).toBeUndefined() |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should use middleware', async () => { |
||||
let client = await startServer({ rangeTokens: true, delta: true }, {}) |
||||
let feature = client.getFeature(SemanticTokensRegistrationType.method) |
||||
await helper.waitValue(() => { |
||||
return feature.getProvider(textDocument) != null |
||||
}, true) |
||||
let provider = feature.getProvider(textDocument) |
||||
expect(provider).toBeDefined() |
||||
expect(provider.range).toBeDefined() |
||||
let res = await provider.full.provideDocumentSemanticTokensEdits(textDocument, '2', CancellationToken.None) |
||||
expect(res.resultId).toBe('3') |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('CodeActionFeature', () => { |
||||
it('should use registered command', async () => { |
||||
let client = await startServer({}) |
||||
let feature = client.getFeature(CodeActionRequest.method) |
||||
await helper.waitValue(() => { |
||||
return feature.getProvider(textDocument) != null |
||||
}, true) |
||||
let provider = feature.getProvider(textDocument) |
||||
let actions = await provider.provideCodeActions(textDocument, Range.create(0, 1, 0, 1), { diagnostics: [] }, token) |
||||
expect(actions.length).toBe(1) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('PullConfigurationFeature', () => { |
||||
it('should pull configuration for configured languageserver', async () => { |
||||
helper.updateConfiguration('languageserver.vim.settings.foo', 'bar') |
||||
let client = await startServer({}) |
||||
await helper.wait(50) |
||||
await client.sendNotification('pullConfiguration') |
||||
await helper.wait(50) |
||||
let res = await client.sendRequest('getConfiguration') |
||||
expect(res).toEqual(['bar']) |
||||
helper.updateConfiguration('suggest.noselect', true) |
||||
await helper.wait(50) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('CodeLensFeature', () => { |
||||
it('should use codeLens middleware', async () => { |
||||
let fn = jest.fn() |
||||
let client = await startServer({}, { |
||||
provideCodeLenses: (doc, token, next) => { |
||||
fn() |
||||
return next(doc, token) |
||||
}, |
||||
resolveCodeLens: (codelens, token, next) => { |
||||
fn() |
||||
return next(codelens, token) |
||||
} |
||||
}) |
||||
let feature = client.getFeature(CodeLensRequest.method) |
||||
let provider = feature.getProvider(textDocument).provider |
||||
expect(provider).toBeDefined() |
||||
let res = await provider.provideCodeLenses(textDocument, token) |
||||
expect(res.length).toBe(2) |
||||
let resolved = await provider.resolveCodeLens(res[0], token) |
||||
expect(resolved.command).toBeDefined() |
||||
expect(fn).toBeCalledTimes(2) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('InlineValueFeature', () => { |
||||
it('should fire refresh', async () => { |
||||
let client = await startServer({}) |
||||
let feature = client.getFeature(InlineValueRequest.method) |
||||
expect(feature).toBeDefined() |
||||
await helper.waitValue(() => { |
||||
return feature.getProvider(textDocument) != null |
||||
}, true) |
||||
let provider = feature.getProvider(textDocument) |
||||
let called = false |
||||
provider.onDidChangeInlineValues.event(() => { |
||||
called = true |
||||
}) |
||||
await client.sendNotification('fireInlineValueRefresh') |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('ExecuteCommandFeature', () => { |
||||
it('should register command with middleware', async () => { |
||||
let called = false |
||||
let client = await startServer({}, { |
||||
executeCommand: (cmd, args, next) => { |
||||
called = true |
||||
return next(cmd, args) |
||||
} |
||||
}) |
||||
await helper.waitValue(() => { |
||||
return commands.has('test_command') |
||||
}, true) |
||||
let feature = client.getFeature(ExecuteCommandRequest.method) |
||||
expect(feature).toBeDefined() |
||||
expect(feature.getState().kind).toBe('workspace') |
||||
let res = await commands.executeCommand('test_command') |
||||
expect(res).toEqual({ success: true }) |
||||
expect(called).toBe(true) |
||||
await client.sendNotification('unregister') |
||||
await helper.wait(30) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should register command without middleware', async () => { |
||||
let client = await startServer({}, {}) |
||||
await helper.wait(50) |
||||
let res = await commands.executeCommand('test_command') |
||||
expect(res).toEqual({ success: true }) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('DocumentSymbolFeature', () => { |
||||
it('should provide documentSymbols without middleware', async () => { |
||||
let client = await startServer({}, {}) |
||||
let feature = client.getFeature(DocumentSymbolRequest.method) |
||||
expect(feature).toBeDefined() |
||||
expect(feature.getState()).toBeDefined() |
||||
let provider = feature.getProvider(textDocument) |
||||
let res = await provider.provideDocumentSymbols(textDocument, token) |
||||
expect(res).toEqual([]) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should provide documentSymbols with middleware', async () => { |
||||
let called = false |
||||
let client = await startServer({ label: true }, { |
||||
provideDocumentSymbols: (doc, token, next) => { |
||||
called = true |
||||
return next(doc, token) |
||||
} |
||||
}) |
||||
let feature = client.getFeature(DocumentSymbolRequest.method) |
||||
let provider = feature.getProvider(textDocument) |
||||
expect(provider.meta).toEqual({ label: 'test' }) |
||||
let res = await provider.provideDocumentSymbols(textDocument, token) |
||||
expect(res).toEqual([]) |
||||
expect(called).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('FileOperationFeature', () => { |
||||
it('should use middleware for FileOperationFeature', async () => { |
||||
let n = 0 |
||||
let client = await startServer({}, { |
||||
workspace: { |
||||
didCreateFiles: (ev, next) => { |
||||
n++ |
||||
return next(ev) |
||||
}, |
||||
didRenameFiles: (ev, next) => { |
||||
n++ |
||||
return next(ev) |
||||
}, |
||||
didDeleteFiles: (ev, next) => { |
||||
n++ |
||||
return next(ev) |
||||
}, |
||||
willRenameFiles: (ev, next) => { |
||||
n++ |
||||
return next(ev) |
||||
}, |
||||
willDeleteFiles: (ev, next) => { |
||||
n++ |
||||
return next(ev) |
||||
} |
||||
} |
||||
}) |
||||
let createFeature = client.getFeature(DidCreateFilesNotification.method) |
||||
await createFeature.send({ files: [URI.file('/a/b')] }) |
||||
let renameFeature = client.getFeature(DidRenameFilesNotification.method) |
||||
await renameFeature.send({ files: [{ oldUri: URI.file('/a/b'), newUri: URI.file('/c/d') }] }) |
||||
let deleteFeature = client.getFeature(DidDeleteFilesNotification.method) |
||||
await deleteFeature.send({ files: [URI.file('/x/y')] }) |
||||
let willRename = client.getFeature(WillRenameFilesRequest.method) |
||||
await willRename.send({ files: [{ oldUri: URI.file(__dirname), newUri: URI.file(path.join(__dirname, 'x')) }], waitUntil: () => {} }) |
||||
let willDelete = client.getFeature(WillDeleteFilesRequest.method) |
||||
await willDelete.send({ files: [URI.file('/x/y')], waitUntil: () => {} }) |
||||
await helper.waitValue(() => { |
||||
return n |
||||
}, 5) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('CompletionItemFeature', () => { |
||||
it('should register multiple completion sources', async () => { |
||||
let client = await startServer({}, {}) |
||||
let feature = client.getFeature(CompletionRequest.method) |
||||
await helper.waitValue(() => { |
||||
return feature.registrationLength |
||||
}, 2) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('WorkspaceFoldersFeature', () => { |
||||
it('should register listeners', async () => { |
||||
let client = await startServer({}, {}) |
||||
let feature = client.getFeature(DidChangeWorkspaceFoldersNotification.method) |
||||
expect(feature).toBeDefined() |
||||
let state = feature.getState() as any |
||||
expect(state.registrations).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should handle WorkspaceFoldersRequest', async () => { |
||||
let client = await startServer({ changeNotifications: true }, {}) |
||||
let folders = workspace.workspaceFolders |
||||
expect(folders.length).toBe(0) |
||||
await client.sendNotification('requestFolders') |
||||
await helper.wait(30) |
||||
let res = await client.sendRequest('getFolders') |
||||
expect(res).toBeNull() |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should use workspaceFolders middleware', async () => { |
||||
await workspace.loadFile(__filename) |
||||
let folders = workspace.workspaceFolders |
||||
expect(folders.length).toBe(1) |
||||
let called = false |
||||
let client = await startServer({ changeNotifications: true }, { |
||||
workspace: { |
||||
workspaceFolders: (token, next) => { |
||||
called = true |
||||
return next(token) |
||||
} |
||||
} |
||||
}) |
||||
await client.sendNotification('requestFolders') |
||||
await helper.waitValue(async () => { |
||||
let res = await client.sendRequest('getFolders') as WorkspaceFolder[] |
||||
return Array.isArray(res) && res.length == 1 |
||||
}, true) |
||||
expect(called).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should send folders event with middleware', async () => { |
||||
let called = false |
||||
let client = await startServer({ changeNotifications: true }, { |
||||
workspace: { |
||||
didChangeWorkspaceFolders: (ev, next) => { |
||||
called = true |
||||
return next(ev) |
||||
} |
||||
} |
||||
}) |
||||
let folders = workspace.workspaceFolders |
||||
expect(folders.length).toBe(0) |
||||
await workspace.loadFile(__filename) |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
}) |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,132 @@
@@ -0,0 +1,132 @@
|
||||
import path from 'path' |
||||
import { DidChangeWatchedFilesNotification, DocumentSelector, Emitter, Event, FileChangeType } from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
import { LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from '../../language-client/index' |
||||
import { FileSystemWatcher } from '../../types' |
||||
import helper from '../helper' |
||||
|
||||
function createClient(fileEvents: FileSystemWatcher | FileSystemWatcher[] | undefined, middleware: Middleware = {}): LanguageClient { |
||||
const serverModule = path.join(__dirname, './server/fileWatchServer.js') |
||||
const serverOptions: ServerOptions = { |
||||
run: { module: serverModule, transport: TransportKind.ipc }, |
||||
debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } |
||||
} |
||||
|
||||
const documentSelector: DocumentSelector = [{ scheme: 'file' }] |
||||
const clientOptions: LanguageClientOptions = { |
||||
documentSelector, |
||||
synchronize: { fileEvents }, |
||||
initializationOptions: {}, |
||||
middleware |
||||
}; |
||||
(clientOptions as ({ $testMode?: boolean })).$testMode = true |
||||
|
||||
const result = new LanguageClient('test', 'Test Language Server', serverOptions, clientOptions) |
||||
return result |
||||
} |
||||
|
||||
class CustomWatcher implements FileSystemWatcher { |
||||
public ignoreCreateEvents = false |
||||
public ignoreChangeEvents = false |
||||
public ignoreDeleteEvents = false |
||||
private readonly _onDidCreate = new Emitter<URI>() |
||||
public readonly onDidCreate: Event<URI> = this._onDidCreate.event |
||||
private readonly _onDidChange = new Emitter<URI>() |
||||
public readonly onDidChange: Event<URI> = this._onDidChange.event |
||||
private readonly _onDidDelete = new Emitter<URI>() |
||||
public readonly onDidDelete: Event<URI> = this._onDidDelete.event |
||||
constructor() { |
||||
} |
||||
|
||||
public fireCreate(uri: URI): void { |
||||
this._onDidCreate.fire(uri) |
||||
} |
||||
|
||||
public fireChange(uri: URI): void { |
||||
this._onDidChange.fire(uri) |
||||
} |
||||
|
||||
public fireDelete(uri: URI): void { |
||||
this._onDidDelete.fire(uri) |
||||
} |
||||
|
||||
public dispose() { |
||||
} |
||||
} |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('FileSystemWatcherFeature', () => { |
||||
it('should hook file events from client configuration', async () => { |
||||
let client: LanguageClient |
||||
let watcher = new CustomWatcher() |
||||
let called = false |
||||
let changes: FileChangeType[] = [] |
||||
client = createClient([watcher], { |
||||
workspace: { |
||||
didChangeWatchedFile: async (event, next): Promise<void> => { |
||||
called = true |
||||
changes.push(event.type) |
||||
return next(event) |
||||
} |
||||
} |
||||
}) |
||||
let received: any[] |
||||
client.onNotification('filesChange', params => { |
||||
received = params.changes |
||||
}) |
||||
await client.start() |
||||
expect(called).toBe(false) |
||||
let uri = URI.file(__filename) |
||||
watcher.fireCreate(uri) |
||||
expect(called).toBe(true) |
||||
watcher.fireChange(uri) |
||||
watcher.fireDelete(uri) |
||||
expect(changes).toEqual([1, 2, 3]) |
||||
await helper.wait(100) |
||||
await client.stop() |
||||
expect(received.length).toBe(1) |
||||
expect(received[0]).toEqual({ |
||||
uri: uri.toString(), |
||||
type: 3 |
||||
}) |
||||
}) |
||||
|
||||
it('should work with single watcher', async () => { |
||||
let client: LanguageClient |
||||
let watcher = new CustomWatcher() |
||||
client = createClient(watcher, {}) |
||||
let received: any[] |
||||
client.onNotification('filesChange', params => { |
||||
received = params.changes |
||||
}) |
||||
await client.start() |
||||
let uri = URI.file(__filename) |
||||
watcher.fireCreate(uri) |
||||
await helper.wait(100) |
||||
expect(received.length).toBe(1) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should support dynamic registration', async () => { |
||||
let client: LanguageClient |
||||
client = createClient(undefined) |
||||
await client.start() |
||||
await helper.wait(50) |
||||
let feature = client.getFeature(DidChangeWatchedFilesNotification.method) |
||||
await (feature as any)._notifyFileEvent() |
||||
let state = feature.getState() |
||||
expect((state as any).registrations).toBe(true) |
||||
await client.sendNotification('unwatch') |
||||
await helper.wait(50) |
||||
state = feature.getState() |
||||
expect((state as any).registrations).toBe(false) |
||||
await client.stop() |
||||
}) |
||||
}) |
@ -0,0 +1,654 @@
@@ -0,0 +1,654 @@
|
||||
import * as assert from 'assert' |
||||
import cp from 'child_process' |
||||
import path from 'path' |
||||
import { CancellationToken, CancellationTokenSource, DidCreateFilesNotification, LSPErrorCodes, MessageType, ResponseError, Trace, WorkDoneProgress } from 'vscode-languageserver-protocol' |
||||
import { IPCMessageReader, IPCMessageWriter } from 'vscode-languageserver-protocol/node' |
||||
import { Diagnostic, MarkupKind, Range } from 'vscode-languageserver-types' |
||||
import { URI } from 'vscode-uri' |
||||
import * as lsclient from '../../language-client' |
||||
import { CloseAction, ErrorAction, HandleDiagnosticsSignature } from '../../language-client' |
||||
import { InitializationFailedHandler } from '../../language-client/utils/errorHandler' |
||||
import { CancellationError } from '../../util/errors' |
||||
import window from '../../window' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('global functions', () => { |
||||
it('should get working directory', async () => { |
||||
let cwd = await lsclient.getServerWorkingDir() |
||||
expect(cwd).toBeDefined() |
||||
cwd = await lsclient.getServerWorkingDir({ cwd: 'not_exists' }) |
||||
expect(cwd).toBeUndefined() |
||||
}) |
||||
|
||||
it('should get main root', async () => { |
||||
expect(lsclient.mainGetRootPath()).toBeUndefined() |
||||
let uri = URI.file(__filename) |
||||
await workspace.openResource(uri.toString()) |
||||
expect(lsclient.mainGetRootPath()).toBeDefined() |
||||
}) |
||||
|
||||
it('should get runtime path', async () => { |
||||
expect(lsclient.getRuntimePath(__filename, undefined)).toBeDefined() |
||||
let uri = URI.file(__filename) |
||||
await workspace.openResource(uri.toString()) |
||||
expect(lsclient.getRuntimePath('package.json', undefined)).toBeDefined() |
||||
let name = path.basename(__filename) |
||||
expect(lsclient.getRuntimePath(name, __dirname)).toBeDefined() |
||||
}) |
||||
|
||||
it('should check debug mode', async () => { |
||||
expect(lsclient.startedInDebugMode(['--debug'])).toBe(true) |
||||
expect(lsclient.startedInDebugMode(undefined)).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('Client events', () => { |
||||
it('should start server', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = {} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.ipc |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should register events before server start', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = {} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.ipc |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
let fn = jest.fn() |
||||
let disposable = client.onRequest('customRequest', () => { |
||||
fn() |
||||
disposable.dispose() |
||||
return {} |
||||
}) |
||||
let dispose = client.onNotification('customNotification', () => { |
||||
fn() |
||||
dispose.dispose() |
||||
}) |
||||
let dis = client.onProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', p => { |
||||
expect(p).toEqual({ kind: 'end', message: 'end message' }) |
||||
fn() |
||||
dis.dispose() |
||||
}) |
||||
await client.start() |
||||
await client.sendNotification('send') |
||||
await helper.wait(60) |
||||
expect(fn).toBeCalledTimes(3) |
||||
// let client = await testEventServer({ initEvent: true })
|
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should register events after server start', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
synchronize: {}, |
||||
initializationOptions: { initEvent: true } |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
let fn = jest.fn() |
||||
let disposable = client.onRequest('customRequest', () => { |
||||
fn() |
||||
disposable.dispose() |
||||
return {} |
||||
}) |
||||
let dispose = client.onNotification('customNotification', () => { |
||||
fn() |
||||
dispose.dispose() |
||||
}) |
||||
let dis = client.onProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', p => { |
||||
expect(p).toEqual({ kind: 'end', message: 'end message' }) |
||||
fn() |
||||
dis.dispose() |
||||
}) |
||||
await client.sendNotification('send') |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalledTimes(3) |
||||
// let client = await testEventServer({ initEvent: true })
|
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should send progress', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
synchronize: {}, |
||||
initializationOptions: { initEvent: true } |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
let fn = jest.fn() |
||||
client.onNotification('progressResult', res => { |
||||
fn() |
||||
expect(res).toEqual({ kind: 'begin', title: 'begin progress' }) |
||||
}) |
||||
await client.sendProgress(WorkDoneProgress.type, '4b3a71d0-2b3f-46af-be2c-2827f548579f', { kind: 'begin', title: 'begin progress' }) |
||||
await client.start() |
||||
await helper.wait(50) |
||||
let p = client.stop() |
||||
await expect(async () => { |
||||
await client._start() |
||||
}).rejects.toThrow(Error) |
||||
await p |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should handle error', async () => { |
||||
let called = false |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
synchronize: {}, |
||||
errorHandler: { |
||||
error: () => { |
||||
return ErrorAction.Shutdown |
||||
}, |
||||
closed: () => { |
||||
called = true |
||||
return CloseAction.DoNotRestart |
||||
} |
||||
}, |
||||
initializationOptions: { initEvent: true } |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.sendRequest('doExit') |
||||
await client.start() |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should handle message events', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
synchronize: {}, |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
await client.sendNotification('logMessage') |
||||
await client.sendNotification('showMessage') |
||||
let types = [MessageType.Error, MessageType.Warning, MessageType.Info, MessageType.Log] |
||||
for (const t of types) { |
||||
await client.sendNotification('requestMessage', { type: t }) |
||||
await helper.wait(30) |
||||
if (t == MessageType.Error) { |
||||
await workspace.nvim.input('1') |
||||
} else { |
||||
await workspace.nvim.input('<cr>') |
||||
} |
||||
} |
||||
let uri = URI.file(__filename) |
||||
await client.sendNotification('showDocument', { external: true, uri: 'lsptest:///1' }) |
||||
await client.sendNotification('showDocument', { uri: 'lsptest:///1', takeFocus: false }) |
||||
await client.sendNotification('showDocument', { uri: uri.toString() }) |
||||
await client.sendNotification('showDocument', { uri: uri.toString(), selection: Range.create(0, 0, 1, 0) }) |
||||
await helper.wait(300) |
||||
expect(client.hasPendingResponse).toBe(false) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should invoke showDocument middleware', async () => { |
||||
let fn = jest.fn() |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
synchronize: {}, |
||||
middleware: { |
||||
window: { |
||||
showDocument: async (params, next) => { |
||||
fn() |
||||
let res = await next(params, CancellationToken.None) |
||||
return res as any |
||||
} |
||||
} |
||||
} |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
let uri = URI.file(__filename) |
||||
await client.start() |
||||
await client.sendNotification('showDocument', { uri: uri.toString() }) |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalled() |
||||
await client.restart() |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('Client integration', () => { |
||||
async function testLanguageServer(serverOptions: lsclient.ServerOptions, clientOpts?: lsclient.LanguageClientOptions): Promise<lsclient.LanguageClient> { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
documentSelector: ['css'], |
||||
initializationOptions: {} |
||||
} |
||||
if (clientOpts) Object.assign(clientOptions, clientOpts) |
||||
let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
expect(client.initializeResult).toBeDefined() |
||||
expect(client.started).toBe(true) |
||||
return client |
||||
} |
||||
|
||||
it('should initialize from function', async () => { |
||||
async function testServer(serverOptions: lsclient.ServerOptions) { |
||||
let clientOptions: lsclient.LanguageClientOptions = {} |
||||
let client = new lsclient.LanguageClient('HTML', serverOptions, clientOptions) |
||||
await client.start() |
||||
await client.dispose() |
||||
} |
||||
await testServer(() => { |
||||
let module = path.join(__dirname, './server/eventServer.js') |
||||
let sp = cp.fork(module, ['--node-ipc'], { cwd: process.cwd() }) |
||||
return Promise.resolve({ reader: new IPCMessageReader(sp), writer: new IPCMessageWriter(sp) }) |
||||
}) |
||||
await testServer(() => { |
||||
let module = path.join(__dirname, './server/eventServer.js') |
||||
let sp = cp.fork(module, ['--stdio'], { |
||||
cwd: process.cwd(), |
||||
execArgv: [], |
||||
silent: true, |
||||
}) |
||||
return Promise.resolve({ reader: sp.stdout, writer: sp.stdin }) |
||||
}) |
||||
await testServer(() => { |
||||
let module = path.join(__dirname, './server/eventServer.js') |
||||
let sp = cp.fork(module, ['--stdio'], { |
||||
cwd: process.cwd(), |
||||
execArgv: [], |
||||
silent: true, |
||||
}) |
||||
return Promise.resolve({ process: sp, detached: false }) |
||||
}) |
||||
await testServer(() => { |
||||
let module = path.join(__dirname, './server/eventServer.js') |
||||
let sp = cp.fork(module, ['--stdio'], { |
||||
cwd: process.cwd(), |
||||
execArgv: [], |
||||
silent: true, |
||||
}) |
||||
return Promise.resolve(sp) |
||||
}) |
||||
}) |
||||
|
||||
it('should initialize use IPC channel', async () => { |
||||
helper.updateConfiguration('css.trace.server.verbosity', 'verbose') |
||||
helper.updateConfiguration('css.trace.server.format', 'json') |
||||
let uri = URI.file(__filename) |
||||
await workspace.loadFile(uri.toString()) |
||||
let serverModule = path.join(__dirname, './server/testInitializeResult.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
run: { module: serverModule, transport: lsclient.TransportKind.ipc }, |
||||
debug: { module: serverModule, transport: lsclient.TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } |
||||
} |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
rootPatterns: ['.vim'], |
||||
requireRootPattern: true, |
||||
documentSelector: ['css'], |
||||
synchronize: {}, initializationOptions: {}, |
||||
middleware: { |
||||
handleDiagnostics: (uri, diagnostics, next) => { |
||||
assert.equal(uri, "uri:/test.ts") |
||||
assert.ok(Array.isArray(diagnostics)) |
||||
assert.equal(diagnostics.length, 0) |
||||
next(uri, diagnostics) |
||||
} |
||||
} |
||||
} |
||||
let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
let expected = { |
||||
capabilities: { |
||||
textDocumentSync: 1, |
||||
completionProvider: { resolveProvider: true, triggerCharacters: ['"', ':'] }, |
||||
hoverProvider: true, |
||||
renameProvider: { |
||||
prepareProvider: true |
||||
} |
||||
}, |
||||
customResults: { |
||||
hello: "world" |
||||
} |
||||
} |
||||
assert.deepEqual(client.initializeResult, expected) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should initialize use stdio', async () => { |
||||
helper.updateConfiguration('css.trace.server.verbosity', 'verbose') |
||||
helper.updateConfiguration('css.trace.server.format', 'text') |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio |
||||
} |
||||
let client = await testLanguageServer(serverOptions, { |
||||
workspaceFolder: { name: 'test', uri: URI.file(__dirname).toString() }, |
||||
outputChannel: window.createOutputChannel('test'), |
||||
markdown: {}, |
||||
disabledFeatures: ['pullDiagnostic'], |
||||
revealOutputChannelOn: lsclient.RevealOutputChannelOn.Info, |
||||
outputChannelName: 'custom', |
||||
connectionOptions: { |
||||
cancellationStrategy: {} as any, |
||||
maxRestartCount: 10, |
||||
}, |
||||
stdioEncoding: 'utf8', |
||||
errorHandler: { |
||||
error: (): lsclient.ErrorAction => { |
||||
return lsclient.ErrorAction.Continue |
||||
}, |
||||
closed: () => { |
||||
return lsclient.CloseAction.DoNotRestart |
||||
} |
||||
}, |
||||
progressOnInitialization: true, |
||||
disableMarkdown: true, |
||||
disableDiagnostics: true |
||||
}) |
||||
assert.deepStrictEqual(client.supportedMarkupKind, [MarkupKind.PlainText]) |
||||
assert.strictEqual(client.name, 'Test Language Server') |
||||
assert.strictEqual(client.diagnostics, undefined) |
||||
client.trace = Trace.Verbose |
||||
let d = client.start() |
||||
let s = new CancellationTokenSource() |
||||
s.cancel() |
||||
client.handleFailedRequest(DidCreateFilesNotification.type, s.token, undefined, '') |
||||
await expect(async () => { |
||||
let error = new ResponseError(LSPErrorCodes.RequestCancelled, 'request cancelled') |
||||
client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') |
||||
}).rejects.toThrow(CancellationError) |
||||
let error = new ResponseError(LSPErrorCodes.ContentModified, 'content changed') |
||||
client.handleFailedRequest(DidCreateFilesNotification.type, undefined, error, '') |
||||
await client.stop() |
||||
client.info('message', new Error('my error'), true) |
||||
client.warn('message', 'error', true) |
||||
client.warn('message', 0, true) |
||||
client.logFailedRequest() |
||||
assert.strictEqual(client.diagnostics, undefined) |
||||
d.dispose() |
||||
}) |
||||
|
||||
it('should initialize use pipe', async () => { |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.pipe |
||||
} |
||||
let client = await testLanguageServer(serverOptions, { |
||||
ignoredRootPaths: [workspace.root] |
||||
}) |
||||
expect(client.serviceState).toBeDefined() |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should initialize use socket', async () => { |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
options: { |
||||
env: { |
||||
NODE_SOCKET_TEST: 1 |
||||
} |
||||
}, |
||||
transport: { |
||||
kind: lsclient.TransportKind.socket, |
||||
port: 8088 |
||||
} |
||||
} |
||||
let client = await testLanguageServer(serverOptions) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should initialize as command', async () => { |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
command: 'node', |
||||
args: [serverModule, '--stdio'] |
||||
} |
||||
let client = await testLanguageServer(serverOptions) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should not throw as command', async () => { |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
command: 'not_exists', |
||||
args: [serverModule, '--stdio'] |
||||
} |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
documentSelector: ['css'], |
||||
initializationOptions: {} |
||||
} |
||||
await expect(async () => { |
||||
let client = new lsclient.LanguageClient('css', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
await client.stop() |
||||
}).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should logMessage', async () => { |
||||
let called = false |
||||
let outputChannel = { |
||||
name: 'empty', |
||||
content: '', |
||||
append: () => { |
||||
called = true |
||||
}, |
||||
appendLine: () => {}, |
||||
clear: () => {}, |
||||
show: () => {}, |
||||
hide: () => {}, |
||||
dispose: () => {} |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
command: 'node', |
||||
args: [serverModule, '--stdio'] |
||||
} |
||||
let client = await testLanguageServer(serverOptions, { outputChannel }) |
||||
client.logMessage('message') |
||||
client.logMessage(Buffer.from('message', 'utf8')) |
||||
expect(called).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should separate diagnostics', async () => { |
||||
async function startServer(disable?: boolean, handleDiagnostics?: (uri: string, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => void): Promise<lsclient.LanguageClient> { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
disableDiagnostics: disable, |
||||
separateDiagnostics: true, |
||||
initializationOptions: {}, |
||||
middleware: { |
||||
handleDiagnostics |
||||
} |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio, |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
return client |
||||
} |
||||
let client = await startServer() |
||||
await client.sendNotification('diagnostics') |
||||
await helper.waitValue(() => { |
||||
let collection = client.diagnostics |
||||
let res = collection.get('lsptest:/2') |
||||
return res.length |
||||
}, 2) |
||||
await client.stop() |
||||
client = await startServer(true) |
||||
await client.sendNotification('diagnostics') |
||||
await helper.wait(50) |
||||
let collection = client.diagnostics |
||||
expect(collection).toBeUndefined() |
||||
await client.stop() |
||||
let called = false |
||||
client = await startServer(false, (uri, diagnostics, next) => { |
||||
called = true |
||||
next(uri, diagnostics) |
||||
}) |
||||
await client.sendNotification('diagnostics') |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should check version on apply workspaceEdit', async () => { |
||||
let uri = URI.file(__filename) |
||||
await workspace.loadFile(uri.toString()) |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
documentSelector: [{ scheme: 'file' }], |
||||
initializationOptions: {}, |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio, |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
let res |
||||
client.onNotification('result', p => { |
||||
res = p |
||||
}) |
||||
await client.start() |
||||
await client.sendNotification('edits') |
||||
await helper.wait(50) |
||||
expect(res).toBeDefined() |
||||
expect(res).toEqual({ applied: false }) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should apply simple workspaceEdit', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
initializationOptions: {}, |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.stdio, |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
let res |
||||
client.onNotification('result', p => { |
||||
res = p |
||||
}) |
||||
await client.start() |
||||
await client.sendNotification('simpleEdit') |
||||
await helper.wait(30) |
||||
expect(res).toBeDefined() |
||||
expect(res).toEqual({ applied: true }) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should handle error on initialize', async () => { |
||||
let client: lsclient.LanguageClient |
||||
async function startServer(handler: InitializationFailedHandler | undefined, key = 'throwError'): Promise<lsclient.LanguageClient> { |
||||
let clientOptions: lsclient.LanguageClientOptions = { |
||||
initializationFailedHandler: handler, |
||||
initializationOptions: { |
||||
[key]: true |
||||
} |
||||
} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.ipc, |
||||
} |
||||
client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
return client |
||||
} |
||||
let n = 0 |
||||
let fn = async () => { |
||||
await startServer(() => { |
||||
n++ |
||||
return n == 1 |
||||
}) |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
await helper.waitValue(() => { |
||||
return n |
||||
}, 5) |
||||
fn = async () => { |
||||
await startServer(undefined, 'normalThrow') |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
fn = async () => { |
||||
await startServer(undefined, 'utf8') |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
fn = async () => { |
||||
await client.stop() |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
let spy = jest.spyOn(window, 'showErrorMessage').mockImplementation(() => { |
||||
return undefined |
||||
}) |
||||
fn = async () => { |
||||
await startServer(undefined) |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
spy.mockRestore() |
||||
await helper.wait(100) |
||||
}) |
||||
}) |
||||
|
||||
describe('SettingMonitor', () => { |
||||
it('should setup SettingMonitor', async () => { |
||||
let clientOptions: lsclient.LanguageClientOptions = {} |
||||
let serverModule = path.join(__dirname, './server/eventServer.js') |
||||
let serverOptions: lsclient.ServerOptions = { |
||||
module: serverModule, |
||||
transport: lsclient.TransportKind.ipc |
||||
} |
||||
let client = new lsclient.LanguageClient('html', 'Test Language Server', serverOptions, clientOptions) |
||||
await client.start() |
||||
let monitor = new lsclient.SettingMonitor(client, 'html.enabled') |
||||
let disposable = monitor.start() |
||||
helper.updateConfiguration('html.enabled', false) |
||||
await helper.wait(30) |
||||
expect(client.state).toBe(lsclient.State.Stopped) |
||||
helper.updateConfiguration('html.enabled', true) |
||||
await helper.wait(30) |
||||
expect(client.state).toBe(lsclient.State.Starting) |
||||
await client.onReady() |
||||
disposable.dispose() |
||||
}) |
||||
}) |
@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Emitter, Event, NotificationHandler, WorkDoneProgressBegin, WorkDoneProgressEnd, WorkDoneProgressReport } from 'vscode-languageserver-protocol' |
||||
import { ProgressContext, ProgressPart } from '../../language-client/progressPart' |
||||
import helper from '../helper' |
||||
|
||||
type ProgressType = WorkDoneProgressBegin | WorkDoneProgressReport | WorkDoneProgressEnd |
||||
|
||||
let nvim: Neovim |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('ProgressPart', () => { |
||||
function createClient(): ProgressContext & { fire: (ev: ProgressType) => void, token: string | undefined } { |
||||
let _onDidProgress = new Emitter<ProgressType>() |
||||
let onDidProgress: Event<ProgressType> = _onDidProgress.event |
||||
let notificationToken: string | undefined |
||||
return { |
||||
id: 'test', |
||||
get token() { |
||||
return notificationToken |
||||
}, |
||||
fire(ev) { |
||||
_onDidProgress.fire(ev) |
||||
}, |
||||
onProgress<ProgressType>(_, __, handler: NotificationHandler<ProgressType>) { |
||||
return onDidProgress(ev => { |
||||
handler(ev as any) |
||||
}) |
||||
}, |
||||
sendNotification(_, params) { |
||||
notificationToken = (params as any).token |
||||
} |
||||
} |
||||
} |
||||
|
||||
it('should not start if cancelled', async () => { |
||||
let client = createClient() |
||||
let p = new ProgressPart(client, '0c7faec8-e36c-4cde-9815-95635c37d696') |
||||
p.cancel() |
||||
expect(p.begin({ kind: 'begin', title: 'canceleld' })).toBe(false) |
||||
}) |
||||
|
||||
it('should report progress', async () => { |
||||
let client = createClient() |
||||
let p = new ProgressPart(client, '0c7faec8-e36c-4cde-9815-95635c37d696') |
||||
p.begin({ kind: 'begin', title: 'p', percentage: 1, cancellable: true }) |
||||
await helper.wait(30) |
||||
p.report({ kind: 'report', message: 'msg', percentage: 10 }) |
||||
await helper.wait(10) |
||||
p.report({ kind: 'report', message: 'msg', percentage: 50 }) |
||||
await helper.wait(10) |
||||
p.done('finised') |
||||
}) |
||||
|
||||
it('should close notification on cancel', async () => { |
||||
helper.updateConfiguration('notification.statusLineProgress', false) |
||||
let client = createClient() |
||||
let p = new ProgressPart(client, '0c7faec8-e36c-4cde-9815-95635c37d696') |
||||
let started = p.begin({ kind: 'begin', title: 'canceleld' }) |
||||
expect(started).toBe(true) |
||||
p.cancel() |
||||
p.cancel() |
||||
let winids = await nvim.call('coc#notify#win_list') as number[] |
||||
await helper.wait(30) |
||||
expect(winids.length).toBe(1) |
||||
let win = nvim.createWindow(winids[0]) |
||||
let closing = await win.getVar('closing') |
||||
expect(closing).toBe(1) |
||||
}) |
||||
|
||||
it('should send notification on cancel', async () => { |
||||
helper.updateConfiguration('notification.statusLineProgress', false) |
||||
let client = createClient() |
||||
let token = '0c7faec8-e36c-4cde-9815-95635c37d696' |
||||
let p = new ProgressPart(client, token) |
||||
let started = p.begin({ kind: 'begin', title: 'canceleld', cancellable: true }) |
||||
expect(started).toBe(true) |
||||
for (let i = 0; i < 10; i++) { |
||||
await helper.wait(30) |
||||
let winids = await nvim.call('coc#notify#win_list') as number[] |
||||
if (winids.length == 1) break |
||||
} |
||||
await helper.wait(30) |
||||
nvim.call('coc#float#close_all', [], true) |
||||
await helper.waitValue(() => { |
||||
return client.token |
||||
}, token) |
||||
}) |
||||
}) |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
'use strict' |
||||
const {createConnection, ConfigurationRequest, DidChangeConfigurationNotification} = require('vscode-languageserver') |
||||
const {URI} = require('vscode-uri') |
||||
|
||||
const connection = createConnection() |
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
connection.onInitialize((_params) => { |
||||
return {capabilities: {}} |
||||
}) |
||||
|
||||
connection.onNotification('pull0', () => { |
||||
connection.sendRequest(ConfigurationRequest.type, { |
||||
items: [{ |
||||
scopeUri: URI.file(__filename).toString() |
||||
}] |
||||
}) |
||||
}) |
||||
|
||||
connection.onNotification('pull1', () => { |
||||
connection.sendRequest(ConfigurationRequest.type, { |
||||
items: [{ |
||||
section: 'http' |
||||
}, { |
||||
section: 'editor.cpp.format' |
||||
}, { |
||||
section: 'unknown' |
||||
}] |
||||
}) |
||||
}) |
||||
|
||||
connection.onNotification(DidChangeConfigurationNotification.type, params => { |
||||
connection.sendNotification('configurationChange', params) |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
'use strict' |
||||
const {createConnection} = require('vscode-languageserver') |
||||
|
||||
const connection = createConnection() |
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
connection.onInitialize((_params) => { |
||||
return {capabilities: {}} |
||||
}) |
||||
connection.onShutdown(() => { |
||||
process.exit(100) |
||||
}) |
||||
connection.listen() |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
"use strict" |
||||
Object.defineProperty(exports, "__esModule", {value: true}) |
||||
const node_1 = require('vscode-languageserver') |
||||
var CrashNotification; |
||||
(function (CrashNotification) { |
||||
CrashNotification.type = new node_1.NotificationType0('test/crash') |
||||
})(CrashNotification || (CrashNotification = {})) |
||||
const connection = (0, node_1.createConnection)() |
||||
connection.onInitialize((_params) => { |
||||
return { |
||||
capabilities: {} |
||||
} |
||||
}) |
||||
connection.onNotification(CrashNotification.type, () => { |
||||
process.exit(100) |
||||
}) |
||||
connection.listen() |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
"use strict" |
||||
Object.defineProperty(exports, "__esModule", {value: true}) |
||||
const node_1 = require("vscode-languageserver") |
||||
const connection = (0, node_1.createConnection)() |
||||
connection.onInitialize((_params) => { |
||||
return { |
||||
capabilities: {} |
||||
} |
||||
}) |
||||
connection.onRequest('request', (param) => { |
||||
return param.value + 1 |
||||
}) |
||||
connection.onNotification('notification', () => { |
||||
}) |
||||
connection.onRequest('triggerRequest', async () => { |
||||
await connection.sendRequest('request') |
||||
}) |
||||
connection.onRequest('triggerNotification', async () => { |
||||
await connection.sendNotification('notification') |
||||
}) |
||||
connection.listen() |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
'use strict' |
||||
const {createConnection, ResponseError, LSPErrorCodes, DiagnosticRefreshRequest, DocumentDiagnosticReportKind, Diagnostic, Range, DiagnosticSeverity, TextDocuments, TextDocumentSyncKind} = require('vscode-languageserver') |
||||
const {TextDocument} = require('vscode-languageserver-textdocument') |
||||
let documents = new TextDocuments(TextDocument) |
||||
|
||||
const connection = createConnection() |
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
let options |
||||
documents.listen(connection) |
||||
connection.onInitialize((params) => { |
||||
options = params.initializationOptions || {} |
||||
const interFileDependencies = options.interFileDependencies !== false |
||||
const workspaceDiagnostics = options.workspaceDiagnostics === true |
||||
const identifier = options.identifier ?? '6d52eff6-96c7-4fd1-910f-f060bcffb23f' |
||||
return { |
||||
capabilities: { |
||||
textDocumentSync: TextDocumentSyncKind.Incremental, |
||||
diagnosticProvider: { |
||||
identifier, |
||||
interFileDependencies, |
||||
workspaceDiagnostics |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
|
||||
let count = 0 |
||||
let saveCount = 0 |
||||
connection.languages.diagnostics.on((params) => { |
||||
let uri = params.textDocument.uri |
||||
if (uri.endsWith('error')) return Promise.reject(new Error('server error')) |
||||
if (uri.endsWith('cancel')) return new ResponseError(LSPErrorCodes.ServerCancelled, 'cancel', {retriggerRequest: false}) |
||||
if (uri.endsWith('retrigger')) return new ResponseError(LSPErrorCodes.ServerCancelled, 'retrigger', {retriggerRequest: true}) |
||||
if (uri.endsWith('change')) count++ |
||||
if (uri.endsWith('save')) saveCount++ |
||||
if (uri.endsWith('empty')) return null |
||||
if (uri.endsWith('unchanged')) return { |
||||
kind: DocumentDiagnosticReportKind.Unchanged, |
||||
resultId: '1' |
||||
} |
||||
return { |
||||
kind: DocumentDiagnosticReportKind.Full, |
||||
items: [ |
||||
Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) |
||||
] |
||||
} |
||||
}) |
||||
|
||||
let workspaceCount = 0 |
||||
connection.languages.diagnostics.onWorkspace((params, _, __, reporter) => { |
||||
if (params.previousResultIds.length > 0) { |
||||
return new Promise((resolve, reject) => { |
||||
setTimeout(() => { |
||||
reporter.report({ |
||||
items: [{ |
||||
kind: DocumentDiagnosticReportKind.Full, |
||||
uri: 'uri1', |
||||
version: 1, |
||||
items: [ |
||||
Diagnostic.create(Range.create(1, 0, 1, 1), 'diagnostic', DiagnosticSeverity.Error) |
||||
] |
||||
}] |
||||
}) |
||||
}, 10) |
||||
setTimeout(() => { |
||||
reporter.report(null) |
||||
}, 15) |
||||
setTimeout(() => { |
||||
reporter.report({ |
||||
items: [{ |
||||
kind: DocumentDiagnosticReportKind.Full, |
||||
uri: 'uri2', |
||||
version: 1, |
||||
items: [ |
||||
Diagnostic.create(Range.create(2, 0, 2, 1), 'diagnostic', DiagnosticSeverity.Error) |
||||
] |
||||
}] |
||||
}) |
||||
}, 20) |
||||
setTimeout(() => { |
||||
resolve({items: []}) |
||||
}, 50) |
||||
}) |
||||
} |
||||
workspaceCount++ |
||||
if (workspaceCount == 2) { |
||||
return new ResponseError(LSPErrorCodes.ServerCancelled, 'changed') |
||||
} |
||||
return { |
||||
items: [{ |
||||
kind: DocumentDiagnosticReportKind.Full, |
||||
uri: 'uri', |
||||
version: 1, |
||||
items: [ |
||||
Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) |
||||
] |
||||
}] |
||||
} |
||||
}) |
||||
|
||||
connection.onNotification('fireRefresh', () => { |
||||
connection.sendRequest(DiagnosticRefreshRequest.type) |
||||
}) |
||||
|
||||
connection.onRequest('getChangeCount', () => { |
||||
return count |
||||
}) |
||||
|
||||
connection.onRequest('getSaveCount', () => { |
||||
return saveCount |
||||
}) |
||||
|
||||
connection.onRequest('getWorkspceCount', () => { |
||||
return workspaceCount |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,221 @@
@@ -0,0 +1,221 @@
|
||||
'use strict' |
||||
const {createConnection, ProtocolRequestType, Range, TextDocumentSyncKind, Command, RenameRequest, WorkspaceSymbolRequest, CodeAction, SemanticTokensRegistrationType, CodeActionRequest, ConfigurationRequest, DidChangeConfigurationNotification, InlineValueRefreshRequest, ExecuteCommandRequest, CompletionRequest, WorkspaceFoldersRequest} = require('vscode-languageserver') |
||||
|
||||
const connection = createConnection() |
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
|
||||
let options |
||||
let disposables = [] |
||||
let prepareResponse |
||||
let configuration |
||||
let folders |
||||
let foldersEvent |
||||
connection.onInitialize((params) => { |
||||
options = params.initializationOptions || {} |
||||
let changeNotifications = options.changeNotifications ?? 'b346648e-88e0-44e3-91e3-52fd6addb8c7' |
||||
return { |
||||
capabilities: { |
||||
inlineValueProvider: {}, |
||||
executeCommandProvider: { |
||||
}, |
||||
documentSymbolProvider: options.label ? {label: 'test'} : true, |
||||
textDocumentSync: TextDocumentSyncKind.Full, |
||||
renameProvider: options.prepareRename ? {prepareProvider: true} : true, |
||||
workspaceSymbolProvider: true, |
||||
codeLensProvider: { |
||||
resolveProvider: true |
||||
}, |
||||
workspace: { |
||||
workspaceFolders: { |
||||
changeNotifications |
||||
}, |
||||
fileOperations: { |
||||
// Static reg is folders + .txt files with operation kind in the path
|
||||
didCreate: { |
||||
filters: [ |
||||
{scheme: 'lsptest', pattern: {glob: '**/*', matches: 'file', options: {}}}, |
||||
{scheme: 'file', pattern: {glob: '**/*', matches: 'file', options: {ignoreCase: false}}} |
||||
] |
||||
}, |
||||
didRename: { |
||||
filters: [ |
||||
{scheme: 'file', pattern: {glob: '**/*', matches: 'folder'}}, |
||||
{scheme: 'file', pattern: {glob: '**/*', matches: 'file'}} |
||||
] |
||||
}, |
||||
didDelete: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/*'}}] |
||||
}, |
||||
willCreate: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/*'}}] |
||||
}, |
||||
willRename: { |
||||
filters: [ |
||||
{scheme: 'file', pattern: {glob: '**/*', matches: 'folder'}}, |
||||
{scheme: 'file', pattern: {glob: '**/*', matches: 'file'}} |
||||
] |
||||
}, |
||||
willDelete: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/*'}}] |
||||
}, |
||||
} |
||||
}, |
||||
} |
||||
} |
||||
}) |
||||
|
||||
connection.onInitialized(() => { |
||||
connection.client.register(RenameRequest.type, { |
||||
prepareProvider: options.prepareRename |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(WorkspaceSymbolRequest.type, { |
||||
resolveProvider: true |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
let full = false |
||||
if (options.delta) { |
||||
full = {delta: true} |
||||
} |
||||
connection.client.register(SemanticTokensRegistrationType.method, { |
||||
full, |
||||
range: options.rangeTokens, |
||||
legend: { |
||||
tokenTypes: [], |
||||
tokenModifiers: [] |
||||
}, |
||||
}) |
||||
connection.client.register(CodeActionRequest.method, { |
||||
resolveProvider: false |
||||
}) |
||||
connection.client.register(DidChangeConfigurationNotification.type, {section: undefined}) |
||||
connection.client.register(ExecuteCommandRequest.type, { |
||||
commands: ['test_command'] |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(CompletionRequest.type, { |
||||
documentSelector: [{language: 'vim'}] |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(CompletionRequest.type, { |
||||
triggerCharacters: ['/'], |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
}) |
||||
|
||||
let lastFileOperationRequest |
||||
connection.workspace.onDidCreateFiles(params => {lastFileOperationRequest = {type: 'create', params}}) |
||||
connection.workspace.onDidRenameFiles(params => {lastFileOperationRequest = {type: 'rename', params}}) |
||||
connection.workspace.onDidDeleteFiles(params => {lastFileOperationRequest = {type: 'delete', params}}) |
||||
connection.workspace.onWillRenameFiles(params => {lastFileOperationRequest = {type: 'willRename', params}}) |
||||
connection.workspace.onWillDeleteFiles(params => {lastFileOperationRequest = {type: 'willDelete', params}}) |
||||
|
||||
// connection.onDidChangeWorkspaceFolders(e => {
|
||||
// foldersEvent = params
|
||||
// })
|
||||
|
||||
connection.onCompletion(_params => { |
||||
return [ |
||||
{label: 'item', insertText: 'text'} |
||||
] |
||||
}) |
||||
|
||||
connection.onCompletionResolve(item => { |
||||
item.detail = 'detail' |
||||
return item |
||||
}) |
||||
|
||||
connection.onRequest( |
||||
new ProtocolRequestType('testing/lastFileOperationRequest'), |
||||
() => { |
||||
return lastFileOperationRequest |
||||
}, |
||||
) |
||||
|
||||
connection.onNotification('unregister', () => { |
||||
for (let d of disposables) { |
||||
d.dispose() |
||||
disposables = [] |
||||
} |
||||
}) |
||||
|
||||
connection.onDocumentSymbol(() => { |
||||
return [] |
||||
}) |
||||
|
||||
connection.onExecuteCommand(param => { |
||||
if (param.command = 'test_command') { |
||||
return {success: true} |
||||
} |
||||
}) |
||||
|
||||
connection.languages.semanticTokens.onDelta(() => { |
||||
return { |
||||
resultId: '3', |
||||
data: [] |
||||
} |
||||
}) |
||||
|
||||
connection.onRequest('setPrepareResponse', param => { |
||||
prepareResponse = param |
||||
}) |
||||
|
||||
connection.onNotification('pullConfiguration', () => { |
||||
configuration = connection.sendRequest(ConfigurationRequest.type, { |
||||
items: [{section: 'foo'}] |
||||
}) |
||||
}) |
||||
|
||||
connection.onRequest('getConfiguration', () => { |
||||
return configuration |
||||
}) |
||||
|
||||
connection.onRequest('getFolders', () => { |
||||
return folders |
||||
}) |
||||
|
||||
connection.onRequest('getFoldersEvent', () => { |
||||
return foldersEvent |
||||
}) |
||||
|
||||
connection.onNotification('fireInlineValueRefresh', () => { |
||||
connection.sendRequest(InlineValueRefreshRequest.type) |
||||
}) |
||||
|
||||
connection.onNotification('requestFolders', async () => { |
||||
folders = await connection.sendRequest(WorkspaceFoldersRequest.type) |
||||
}) |
||||
|
||||
connection.onPrepareRename(() => { |
||||
return prepareResponse |
||||
}) |
||||
|
||||
connection.onCodeAction(() => { |
||||
return [ |
||||
Command.create('title', 'editor.action.triggerSuggest') |
||||
] |
||||
}) |
||||
|
||||
connection.onWorkspaceSymbol(() => { |
||||
return [] |
||||
}) |
||||
|
||||
connection.onWorkspaceSymbolResolve(item => { |
||||
return item |
||||
}) |
||||
|
||||
connection.onCodeLens(params => { |
||||
return [{range: Range.create(0, 0, 0, 3)}, {range: Range.create(1, 0, 1, 3)}] |
||||
}) |
||||
|
||||
connection.onCodeLensResolve(codelens => { |
||||
return {range: codelens.range, command: {title: 'format', command: 'format'}} |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
"use strict" |
||||
const {createConnection, ResponseError} = require("vscode-languageserver") |
||||
const connection = createConnection() |
||||
connection.onInitialize((_params) => { |
||||
return { |
||||
capabilities: {} |
||||
} |
||||
}) |
||||
|
||||
connection.onSignatureHelp(_params => { |
||||
return new ResponseError(-32803, 'failed') |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
|
||||
'use strict' |
||||
const {createConnection, TextEdit, TextDocuments, Range, DiagnosticSeverity, Location, Diagnostic, DiagnosticRelatedInformation, PositionEncodingKind, WorkDoneProgress, ResponseError, LogMessageNotification, MessageType, ShowMessageNotification, ShowMessageRequest, ShowDocumentRequest, ApplyWorkspaceEditRequest, TextDocumentSyncKind, Position} = require('vscode-languageserver') |
||||
const {TextDocument} = require('vscode-languageserver-textdocument') |
||||
let documents = new TextDocuments(TextDocument) |
||||
|
||||
const connection = createConnection() |
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
let options |
||||
documents.listen(connection) |
||||
connection.onInitialize((params) => { |
||||
options = params.initializationOptions || {} |
||||
if (options.throwError) { |
||||
setTimeout(() => { |
||||
process.exit() |
||||
}, 10) |
||||
return new ResponseError(1, 'message', {retry: true}) |
||||
} |
||||
if (options.normalThrow) { |
||||
setTimeout(() => { |
||||
process.exit() |
||||
}, 10) |
||||
throw new Error('message') |
||||
} |
||||
if (options.utf8) { |
||||
return {capabilities: {positionEncoding: PositionEncodingKind.UTF8}} |
||||
} |
||||
return { |
||||
capabilities: { |
||||
textDocumentSync: TextDocumentSyncKind.Full |
||||
} |
||||
} |
||||
}) |
||||
|
||||
connection.onNotification('diagnostics', () => { |
||||
let diagnostics = [] |
||||
let related = [] |
||||
let uri = 'lsptest:///2' |
||||
related.push(DiagnosticRelatedInformation.create(Location.create(uri, Range.create(0, 0, 0, 1)), 'dup')) |
||||
related.push(DiagnosticRelatedInformation.create(Location.create(uri, Range.create(0, 0, 1, 0)), 'dup')) |
||||
diagnostics.push(Diagnostic.create(Range.create(0, 0, 1, 0), 'msg', DiagnosticSeverity.Error, undefined, undefined, related)) |
||||
connection.sendDiagnostics({uri: 'lsptest:///1', diagnostics}) |
||||
connection.sendDiagnostics({uri: 'lsptest:///3', version: 1, diagnostics}) |
||||
}) |
||||
|
||||
connection.onNotification('simpleEdit', async () => { |
||||
let res = await connection.sendRequest(ApplyWorkspaceEditRequest.type, {edit: {documentChanges: []}}) |
||||
connection.sendNotification('result', res) |
||||
}) |
||||
|
||||
connection.onNotification('edits', async () => { |
||||
let uris = documents.keys() |
||||
let res = await connection.sendRequest(ApplyWorkspaceEditRequest.type, { |
||||
edit: { |
||||
documentChanges: uris.map(uri => { |
||||
return { |
||||
textDocument: {uri, version: documents.get(uri).version + 1}, |
||||
edits: [TextEdit.insert(Position.create(0, 0), 'foo')] |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
connection.sendNotification('result', res) |
||||
}) |
||||
|
||||
connection.onNotification('send', () => { |
||||
connection.sendRequest('customRequest') |
||||
connection.sendNotification('customNotification') |
||||
connection.sendProgress(WorkDoneProgress.type, '4fb247f8-0ede-415d-a80a-6629b6a9eaf8', {kind: 'end', message: 'end message'}) |
||||
}) |
||||
|
||||
connection.onNotification('logMessage', () => { |
||||
connection.sendNotification(LogMessageNotification.type, {type: MessageType.Error, message: 'msg'}) |
||||
connection.sendNotification(LogMessageNotification.type, {type: MessageType.Info, message: 'msg'}) |
||||
connection.sendNotification(LogMessageNotification.type, {type: MessageType.Log, message: 'msg'}) |
||||
connection.sendNotification(LogMessageNotification.type, {type: MessageType.Warning, message: 'msg'}) |
||||
}) |
||||
|
||||
connection.onNotification('showMessage', () => { |
||||
connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Error, message: 'msg'}) |
||||
connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Info, message: 'msg'}) |
||||
connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Log, message: 'msg'}) |
||||
connection.sendNotification(ShowMessageNotification.type, {type: MessageType.Warning, message: 'msg'}) |
||||
}) |
||||
|
||||
connection.onNotification('requestMessage', async params => { |
||||
await connection.sendRequest(ShowMessageRequest.type, {type: params.type, message: 'msg', actions: [{title: 'open'}]}) |
||||
}) |
||||
|
||||
connection.onNotification('showDocument', async params => { |
||||
await connection.sendRequest(ShowDocumentRequest.type, params) |
||||
}) |
||||
|
||||
connection.onProgress(WorkDoneProgress.type, '4b3a71d0-2b3f-46af-be2c-2827f548579f', (params) => { |
||||
connection.sendNotification('progressResult', params) |
||||
}) |
||||
|
||||
connection.onRequest('doExit', () => { |
||||
setTimeout(() => { |
||||
process.exit(1) |
||||
}, 30) |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
'use strict' |
||||
const {createConnection, DidChangeWatchedFilesNotification} = require('vscode-languageserver') |
||||
const {URI} = require('vscode-uri') |
||||
|
||||
const connection = createConnection() |
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
connection.onInitialize((_params) => { |
||||
return {capabilities: {}} |
||||
}) |
||||
|
||||
let disposables = [] |
||||
connection.onInitialized(() => { |
||||
connection.client.register(DidChangeWatchedFilesNotification.type, { |
||||
watchers: [{ |
||||
globPattern: '**/jsconfig.json', |
||||
}, { |
||||
globPattern: '**/*.ts', |
||||
kind: 1 |
||||
}, { |
||||
globPattern: { |
||||
baseUri: URI.file(process.cwd()).toString(), |
||||
pattern: '**/*.vim' |
||||
}, |
||||
kind: 1 |
||||
}, { |
||||
globPattern: '**/*.js', |
||||
kind: 2 |
||||
}, { |
||||
globPattern: -1 |
||||
}] |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(DidChangeWatchedFilesNotification.type, { |
||||
watchers: null |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
}) |
||||
connection.onNotification(DidChangeWatchedFilesNotification.type, params => { |
||||
connection.sendNotification('filesChange', params) |
||||
}) |
||||
|
||||
connection.onNotification('unwatch', () => { |
||||
for (let d of disposables) { |
||||
d.dispose() |
||||
} |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
"use strict" |
||||
Object.defineProperty(exports, "__esModule", {value: true}) |
||||
const node_1 = require("vscode-languageserver") |
||||
const connection = (0, node_1.createConnection)() |
||||
connection.onInitialize((_params) => { |
||||
return { |
||||
capabilities: {} |
||||
} |
||||
}) |
||||
connection.onShutdown(() => { |
||||
}) |
||||
connection.listen() |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
"use strict" |
||||
Object.defineProperty(exports, "__esModule", {value: true}) |
||||
const node_1 = require("vscode-languageserver") |
||||
const connection = (0, node_1.createConnection)() |
||||
connection.onInitialize((_params) => { |
||||
return { |
||||
capabilities: { |
||||
executeCommandProvider: { |
||||
commands: ['foo.command'], |
||||
} |
||||
} |
||||
} |
||||
}) |
||||
connection.onShutdown(() => { |
||||
}) |
||||
connection.listen() |
@ -0,0 +1,102 @@
@@ -0,0 +1,102 @@
|
||||
const {ResponseError, LSPErrorCodes} = require('vscode-languageserver') |
||||
const ls = require('vscode-languageserver') |
||||
const {TextDocument} = require('vscode-languageserver-textdocument') |
||||
let connection = ls.createConnection() |
||||
let documents = new ls.TextDocuments(TextDocument) |
||||
|
||||
let lastOpenEvent |
||||
let lastCloseEvent |
||||
let lastChangeEvent |
||||
let lastWillSave |
||||
let lastDidSave |
||||
documents.onDidOpen(e => { |
||||
lastOpenEvent = {uri: e.document.uri, version: e.document.version} |
||||
}) |
||||
documents.onDidClose(e => { |
||||
lastCloseEvent = {uri: e.document.uri} |
||||
}) |
||||
documents.onDidChangeContent(e => { |
||||
lastChangeEvent = {uri: e.document.uri, text: e.document.getText()} |
||||
}) |
||||
documents.onWillSave(e => { |
||||
lastWillSave = {uri: e.document.uri} |
||||
}) |
||||
documents.onWillSaveWaitUntil(e => { |
||||
let uri = e.document.uri |
||||
if (uri.endsWith('error.vim')) throw new ResponseError(LSPErrorCodes.ContentModified, 'content changed') |
||||
if (!uri.endsWith('foo.vim')) return [] |
||||
return [ls.TextEdit.insert(ls.Position.create(0, 0), 'abc')] |
||||
}) |
||||
documents.onDidSave(e => { |
||||
lastDidSave = {uri: e.document.uri} |
||||
}) |
||||
documents.listen(connection) |
||||
|
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
|
||||
let opts |
||||
connection.onInitialize(params => { |
||||
opts = params.initializationOptions |
||||
let capabilities = { |
||||
textDocumentSync: { |
||||
openClose: true, |
||||
change: ls.TextDocumentSyncKind.Full, |
||||
willSave: true, |
||||
willSaveWaitUntil: true, |
||||
save: true |
||||
} |
||||
} |
||||
return {capabilities} |
||||
}) |
||||
|
||||
connection.onRequest('getLastOpen', () => { |
||||
return lastOpenEvent |
||||
}) |
||||
|
||||
connection.onRequest('getLastClose', () => { |
||||
return lastCloseEvent |
||||
}) |
||||
|
||||
connection.onRequest('getLastChange', () => { |
||||
return lastChangeEvent |
||||
}) |
||||
|
||||
connection.onRequest('getLastWillSave', () => { |
||||
return lastWillSave |
||||
}) |
||||
|
||||
connection.onRequest('getLastDidSave', () => { |
||||
return lastDidSave |
||||
}) |
||||
|
||||
let disposables = [] |
||||
connection.onNotification('registerDocumentSync', () => { |
||||
let opt = {documentSelector: [{language: 'vim'}]} |
||||
connection.client.register(ls.DidOpenTextDocumentNotification.type, opt).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(ls.DidCloseTextDocumentNotification.type, opt).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(ls.DidChangeTextDocumentNotification.type, Object.assign({ |
||||
syncKind: opts.none === true ? ls.TextDocumentSyncKind.None : ls.TextDocumentSyncKind.Incremental |
||||
}, opt)).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(ls.WillSaveTextDocumentNotification.type, opt).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(ls.WillSaveTextDocumentWaitUntilRequest.type, opt).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
}) |
||||
|
||||
connection.onNotification('unregisterDocumentSync', () => { |
||||
for (let dispose of disposables) { |
||||
dispose.dispose() |
||||
} |
||||
disposables = [] |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
const languageserver = require('vscode-languageserver') |
||||
let connection = languageserver.createConnection() |
||||
let documents = new languageserver.TextDocuments() |
||||
documents.listen(connection) |
||||
|
||||
connection.onInitialize(() => { |
||||
let capabilities = { |
||||
textDocumentSync: documents.syncKind |
||||
} |
||||
return { capabilities } |
||||
}) |
||||
|
||||
connection.onInitialized(() => { |
||||
connection.sendRequest('client/registerCapability', { |
||||
registrations: [{ |
||||
id: 'didChangeWatchedFiles', |
||||
method: 'workspace/didChangeWatchedFiles', |
||||
registerOptions: { |
||||
watchers: [{ globPattern: "**" }] |
||||
} |
||||
}] |
||||
}) |
||||
}) |
||||
|
||||
let received |
||||
|
||||
connection.onNotification('workspace/didChangeWatchedFiles', params => { |
||||
received = params |
||||
}) |
||||
|
||||
connection.onRequest('custom/received', async () => { |
||||
return received |
||||
}) |
||||
|
||||
connection.listen() |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
'use strict' |
||||
Object.defineProperty(exports, "__esModule", {value: true}) |
||||
const tslib_1 = require("tslib") |
||||
const assert = tslib_1.__importStar(require("assert")) |
||||
const vscode_languageserver_1 = require("vscode-languageserver") |
||||
let connection = vscode_languageserver_1.createConnection() |
||||
|
||||
let documents = new vscode_languageserver_1.TextDocuments() |
||||
documents.listen(connection) |
||||
connection.onInitialize((params) => { |
||||
assert.equal(params.capabilities.workspace.applyEdit, true) |
||||
assert.equal(params.capabilities.workspace.workspaceEdit.documentChanges, true) |
||||
assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, [vscode_languageserver_1.ResourceOperationKind.Create, vscode_languageserver_1.ResourceOperationKind.Rename, vscode_languageserver_1.ResourceOperationKind.Delete]) |
||||
assert.equal(params.capabilities.workspace.workspaceEdit.failureHandling, vscode_languageserver_1.FailureHandlingKind.Undo) |
||||
assert.equal(params.capabilities.textDocument.completion.completionItem.deprecatedSupport, true) |
||||
assert.equal(params.capabilities.textDocument.completion.completionItem.preselectSupport, true) |
||||
assert.equal(params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport, true) |
||||
assert.equal(params.capabilities.textDocument.rename.prepareSupport, true) |
||||
let valueSet = params.capabilities.textDocument.completion.completionItemKind.valueSet |
||||
assert.equal(valueSet[0], 1) |
||||
assert.equal(valueSet[valueSet.length - 1], vscode_languageserver_1.CompletionItemKind.TypeParameter) |
||||
let capabilities = { |
||||
textDocumentSync: 1, |
||||
completionProvider: {resolveProvider: true, triggerCharacters: ['"', ':']}, |
||||
hoverProvider: true, |
||||
renameProvider: { |
||||
prepareProvider: true |
||||
} |
||||
} |
||||
return {capabilities, customResults: {"hello": "world"}} |
||||
}) |
||||
connection.onInitialized(() => { |
||||
connection.sendDiagnostics({uri: "uri:/test.ts", diagnostics: []}) |
||||
}) |
||||
// Listen on the connection
|
||||
connection.listen() |
@ -0,0 +1,602 @@
@@ -0,0 +1,602 @@
|
||||
const assert = require('assert') |
||||
const {URI} = require('vscode-uri') |
||||
const { |
||||
createConnection, CompletionItemKind, ResourceOperationKind, FailureHandlingKind, |
||||
DiagnosticTag, CompletionItemTag, TextDocumentSyncKind, MarkupKind, SignatureInformation, ParameterInformation, |
||||
Location, Range, DocumentHighlight, DocumentHighlightKind, CodeAction, Command, TextEdit, Position, DocumentLink, |
||||
ColorInformation, Color, ColorPresentation, FoldingRange, SelectionRange, SymbolKind, ProtocolRequestType, WorkDoneProgress, |
||||
SignatureHelpRequest, SemanticTokensRefreshRequest, WorkDoneProgressCreateRequest, CodeLensRefreshRequest, InlayHintRefreshRequest, WorkspaceSymbolRequest, DidChangeConfigurationNotification} = require('vscode-languageserver') |
||||
|
||||
const { |
||||
DidCreateFilesNotification, |
||||
DidRenameFilesNotification, |
||||
DidDeleteFilesNotification, |
||||
WillCreateFilesRequest, WillRenameFilesRequest, WillDeleteFilesRequest, InlayHint, InlayHintLabelPart, InlayHintKind, DocumentDiagnosticReportKind, Diagnostic, DiagnosticSeverity, InlineValueText, InlineValueVariableLookup, InlineValueEvaluatableExpression |
||||
} = require('vscode-languageserver-protocol') |
||||
|
||||
let connection = createConnection() |
||||
|
||||
console.log = connection.console.log.bind(connection.console) |
||||
console.error = connection.console.error.bind(connection.console) |
||||
let disposables = [] |
||||
connection.onInitialize(params => { |
||||
assert.equal((params.capabilities.workspace).applyEdit, true) |
||||
assert.equal(params.capabilities.workspace.workspaceEdit.documentChanges, true) |
||||
assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, ['create', 'rename', 'delete']) |
||||
assert.equal(params.capabilities.workspace.workspaceEdit.failureHandling, FailureHandlingKind.Undo) |
||||
assert.equal(params.capabilities.workspace.workspaceEdit.normalizesLineEndings, true) |
||||
assert.equal(params.capabilities.workspace.workspaceEdit.changeAnnotationSupport.groupsOnLabel, false) |
||||
assert.equal(params.capabilities.workspace.symbol.resolveSupport.properties[0], 'location.range') |
||||
assert.equal(params.capabilities.textDocument.completion.completionItem.deprecatedSupport, true) |
||||
assert.equal(params.capabilities.textDocument.completion.completionItem.preselectSupport, true) |
||||
assert.equal(params.capabilities.textDocument.completion.completionItem.tagSupport.valueSet.length, 1) |
||||
assert.equal(params.capabilities.textDocument.completion.completionItem.tagSupport.valueSet[0], CompletionItemTag.Deprecated) |
||||
assert.equal(params.capabilities.textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport, true) |
||||
assert.equal(params.capabilities.textDocument.definition.linkSupport, true) |
||||
assert.equal(params.capabilities.textDocument.declaration.linkSupport, true) |
||||
assert.equal(params.capabilities.textDocument.implementation.linkSupport, true) |
||||
assert.equal(params.capabilities.textDocument.typeDefinition.linkSupport, true) |
||||
assert.equal(params.capabilities.textDocument.rename.prepareSupport, true) |
||||
assert.equal(params.capabilities.textDocument.publishDiagnostics.relatedInformation, true) |
||||
assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet.length, 2) |
||||
assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet[0], DiagnosticTag.Unnecessary) |
||||
assert.equal(params.capabilities.textDocument.publishDiagnostics.tagSupport.valueSet[1], DiagnosticTag.Deprecated) |
||||
assert.equal(params.capabilities.textDocument.documentLink.tooltipSupport, true) |
||||
assert.equal(params.capabilities.textDocument.inlineValue.dynamicRegistration, true) |
||||
assert.equal(params.capabilities.textDocument.inlayHint.dynamicRegistration, true) |
||||
assert.equal(params.capabilities.textDocument.inlayHint.resolveSupport.properties[0], 'tooltip') |
||||
|
||||
let valueSet = params.capabilities.textDocument.completion.completionItemKind.valueSet |
||||
assert.equal(valueSet[0], 1) |
||||
assert.equal(valueSet[valueSet.length - 1], CompletionItemKind.TypeParameter) |
||||
assert.deepEqual(params.capabilities.workspace.workspaceEdit.resourceOperations, [ResourceOperationKind.Create, ResourceOperationKind.Rename, ResourceOperationKind.Delete]) |
||||
assert.equal(params.capabilities.workspace.fileOperations.willCreate, true) |
||||
|
||||
let diagnosticClientCapabilities = params.capabilities.textDocument.diagnostic |
||||
assert.equal(diagnosticClientCapabilities.dynamicRegistration, true) |
||||
assert.equal(diagnosticClientCapabilities.relatedDocumentSupport, true) |
||||
|
||||
const capabilities = { |
||||
textDocumentSync: TextDocumentSyncKind.Full, |
||||
definitionProvider: true, |
||||
hoverProvider: true, |
||||
signatureHelpProvider: { |
||||
triggerCharacters: [','], |
||||
retriggerCharacters: [';'] |
||||
}, |
||||
completionProvider: {resolveProvider: true, triggerCharacters: ['"', ':']}, |
||||
referencesProvider: true, |
||||
documentHighlightProvider: true, |
||||
codeActionProvider: { |
||||
resolveProvider: true |
||||
}, |
||||
codeLensProvider: { |
||||
resolveProvider: true |
||||
}, |
||||
documentFormattingProvider: true, |
||||
documentRangeFormattingProvider: true, |
||||
documentOnTypeFormattingProvider: { |
||||
firstTriggerCharacter: ':' |
||||
}, |
||||
renameProvider: { |
||||
prepareProvider: true |
||||
}, |
||||
documentLinkProvider: { |
||||
resolveProvider: true |
||||
}, |
||||
colorProvider: true, |
||||
declarationProvider: true, |
||||
foldingRangeProvider: true, |
||||
implementationProvider: { |
||||
documentSelector: [{language: '*'}] |
||||
}, |
||||
selectionRangeProvider: true, |
||||
inlineValueProvider: {}, |
||||
inlayHintProvider: { |
||||
resolveProvider: true |
||||
}, |
||||
typeDefinitionProvider: { |
||||
id: '82671a9a-2a69-4e9f-a8d7-e1034eaa0d2e', |
||||
documentSelector: [{language: '*'}] |
||||
}, |
||||
callHierarchyProvider: true, |
||||
semanticTokensProvider: { |
||||
legend: { |
||||
tokenTypes: [], |
||||
tokenModifiers: [] |
||||
}, |
||||
range: true, |
||||
full: { |
||||
delta: true |
||||
} |
||||
}, |
||||
workspace: { |
||||
fileOperations: { |
||||
// Static reg is folders + .txt files with operation kind in the path
|
||||
didCreate: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/created-static/**{/,/*.txt}'}}] |
||||
}, |
||||
didRename: { |
||||
filters: [ |
||||
{scheme: 'file', pattern: {glob: '**/renamed-static/**/', matches: 'folder'}}, |
||||
{scheme: 'file', pattern: {glob: '**/renamed-static/**/*.txt', matches: 'file'}} |
||||
] |
||||
}, |
||||
didDelete: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/deleted-static/**{/,/*.txt}'}}] |
||||
}, |
||||
willCreate: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/created-static/**{/,/*.txt}'}}] |
||||
}, |
||||
willRename: { |
||||
filters: [ |
||||
{scheme: 'file', pattern: {glob: '**/renamed-static/**/', matches: 'folder'}}, |
||||
{scheme: 'file', pattern: {glob: '**/renamed-static/**/*.txt', matches: 'file'}} |
||||
] |
||||
}, |
||||
willDelete: { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/deleted-static/**{/,/*.txt}'}}] |
||||
}, |
||||
}, |
||||
}, |
||||
linkedEditingRangeProvider: true, |
||||
diagnosticProvider: { |
||||
identifier: 'da348dc5-c30a-4515-9d98-31ff3be38d14', |
||||
interFileDependencies: true, |
||||
workspaceDiagnostics: true |
||||
}, |
||||
typeHierarchyProvider: true, |
||||
workspaceSymbolProvider: { |
||||
resolveProvider: true |
||||
}, |
||||
notebookDocumentSync: { |
||||
notebookSelector: [{ |
||||
notebook: {notebookType: 'jupyter-notebook'}, |
||||
cells: [{language: 'python'}] |
||||
}] |
||||
} |
||||
} |
||||
return {capabilities, customResults: {hello: 'world'}} |
||||
}) |
||||
|
||||
connection.onInitialized(() => { |
||||
// Dynamic reg is folders + .js files with operation kind in the path
|
||||
connection.client.register(DidCreateFilesNotification.type, { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/created-dynamic/**{/,/*.js}'}}] |
||||
}) |
||||
connection.client.register(DidRenameFilesNotification.type, { |
||||
filters: [ |
||||
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/', matches: 'folder'}}, |
||||
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/*.js', matches: 'file'}} |
||||
] |
||||
}) |
||||
connection.client.register(DidDeleteFilesNotification.type, { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/deleted-dynamic/**{/,/*.js}'}}] |
||||
}) |
||||
connection.client.register(WillCreateFilesRequest.type, { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/created-dynamic/**{/,/*.js}'}}] |
||||
}) |
||||
connection.client.register(WillRenameFilesRequest.type, { |
||||
filters: [ |
||||
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/', matches: 'folder'}}, |
||||
{scheme: 'file', pattern: {glob: '**/renamed-dynamic/**/*.js', matches: 'file'}} |
||||
] |
||||
}) |
||||
connection.client.register(WillDeleteFilesRequest.type, { |
||||
filters: [{scheme: 'file', pattern: {glob: '**/deleted-dynamic/**{/,/*.js}'}}] |
||||
}) |
||||
connection.client.register(SignatureHelpRequest.type, { |
||||
triggerCharacters: [':'], |
||||
retriggerCharacters: [':'] |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(WorkspaceSymbolRequest.type, { |
||||
workDoneProgress: false, |
||||
resolveProvider: true |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(DidChangeConfigurationNotification.type, { |
||||
section: 'http' |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
connection.client.register(DidCreateFilesNotification.type, { |
||||
filters: [{ |
||||
pattern: { |
||||
glob: '**/renamed-dynamic/**/', |
||||
matches: 'folder', |
||||
options: { |
||||
ignoreCase: true |
||||
} |
||||
} |
||||
}] |
||||
}).then(d => { |
||||
disposables.push(d) |
||||
}) |
||||
}) |
||||
|
||||
connection.onNotification('unregister', () => { |
||||
for (let d of disposables) { |
||||
d.dispose() |
||||
disposables = [] |
||||
} |
||||
}) |
||||
|
||||
connection.onCodeLens(params => { |
||||
return [{range: Range.create(0, 0, 0, 3)}, {range: Range.create(1, 0, 1, 3)}] |
||||
}) |
||||
|
||||
connection.onNotification('fireCodeLensRefresh', () => { |
||||
connection.sendRequest(CodeLensRefreshRequest.type) |
||||
}) |
||||
|
||||
connection.onNotification('fireSemanticTokensRefresh', () => { |
||||
connection.sendRequest(SemanticTokensRefreshRequest.type) |
||||
}) |
||||
|
||||
connection.onNotification('fireInlayHintsRefresh', () => { |
||||
connection.sendRequest(InlayHintRefreshRequest.type) |
||||
}) |
||||
|
||||
connection.onCodeLensResolve(codelens => { |
||||
return {range: codelens.range, command: {title: 'format', command: 'editor.action.format'}} |
||||
}) |
||||
|
||||
connection.onDeclaration(params => { |
||||
assert.equal(params.position.line, 1) |
||||
assert.equal(params.position.character, 1) |
||||
return {uri: params.textDocument.uri, range: {start: {line: 1, character: 1}, end: {line: 1, character: 2}}} |
||||
}) |
||||
|
||||
connection.onDefinition(params => { |
||||
assert.equal(params.position.line, 1) |
||||
assert.equal(params.position.character, 1) |
||||
return {uri: params.textDocument.uri, range: {start: {line: 0, character: 0}, end: {line: 0, character: 1}}} |
||||
}) |
||||
|
||||
connection.onHover(_params => { |
||||
return { |
||||
contents: { |
||||
kind: MarkupKind.PlainText, |
||||
value: 'foo' |
||||
} |
||||
} |
||||
}) |
||||
|
||||
connection.onCompletion(_params => { |
||||
return [ |
||||
{label: 'item', insertText: 'text'} |
||||
] |
||||
}) |
||||
|
||||
connection.onCompletionResolve(item => { |
||||
item.detail = 'detail' |
||||
return item |
||||
}) |
||||
|
||||
connection.onSignatureHelp(_params => { |
||||
const result = { |
||||
signatures: [ |
||||
SignatureInformation.create('label', 'doc', ParameterInformation.create('label', 'doc')) |
||||
], |
||||
activeSignature: 1, |
||||
activeParameter: 1 |
||||
} |
||||
return result |
||||
}) |
||||
|
||||
connection.onReferences(params => { |
||||
return [ |
||||
Location.create(params.textDocument.uri, Range.create(0, 0, 0, 0)), |
||||
Location.create(params.textDocument.uri, Range.create(1, 1, 1, 1)) |
||||
] |
||||
}) |
||||
|
||||
connection.onDocumentHighlight(_params => { |
||||
return [ |
||||
DocumentHighlight.create(Range.create(2, 2, 2, 2), DocumentHighlightKind.Read) |
||||
] |
||||
}) |
||||
|
||||
connection.onCodeAction(params => { |
||||
if (params.textDocument.uri.endsWith('empty.bat')) return undefined |
||||
return [ |
||||
CodeAction.create('title', Command.create('title', 'test_command')) |
||||
] |
||||
}) |
||||
|
||||
connection.onExecuteCommand(params => { |
||||
if (params.command == 'test_command') { |
||||
return {success: true} |
||||
} |
||||
}) |
||||
|
||||
connection.onCodeActionResolve(codeAction => { |
||||
codeAction.title = 'resolved' |
||||
return codeAction |
||||
}) |
||||
|
||||
connection.onDocumentFormatting(_params => { |
||||
return [ |
||||
TextEdit.insert(Position.create(0, 0), 'insert') |
||||
] |
||||
}) |
||||
|
||||
connection.onDocumentRangeFormatting(_params => { |
||||
return [ |
||||
TextEdit.del(Range.create(1, 1, 1, 2)) |
||||
] |
||||
}) |
||||
|
||||
connection.onDocumentOnTypeFormatting(_params => { |
||||
return [ |
||||
TextEdit.replace(Range.create(2, 2, 2, 3), 'replace') |
||||
] |
||||
}) |
||||
|
||||
connection.onPrepareRename(_params => { |
||||
return Range.create(1, 1, 1, 2) |
||||
}) |
||||
|
||||
connection.onRenameRequest(_params => { |
||||
return {documentChanges: []} |
||||
}) |
||||
|
||||
connection.onDocumentLinks(_params => { |
||||
return [ |
||||
DocumentLink.create(Range.create(1, 1, 1, 2)) |
||||
] |
||||
}) |
||||
|
||||
connection.onDocumentLinkResolve(link => { |
||||
link.target = URI.file('/target.txt').toString() |
||||
return link |
||||
}) |
||||
|
||||
connection.onDocumentColor(_params => { |
||||
return [ |
||||
ColorInformation.create(Range.create(1, 1, 1, 2), Color.create(1, 1, 1, 1)) |
||||
] |
||||
}) |
||||
|
||||
connection.onColorPresentation(_params => { |
||||
return [ |
||||
ColorPresentation.create('label') |
||||
] |
||||
}) |
||||
|
||||
connection.onFoldingRanges(_params => { |
||||
return [ |
||||
FoldingRange.create(1, 2) |
||||
] |
||||
}) |
||||
|
||||
connection.onImplementation(params => { |
||||
assert.equal(params.position.line, 1) |
||||
assert.equal(params.position.character, 1) |
||||
return {uri: params.textDocument.uri, range: {start: {line: 2, character: 2}, end: {line: 3, character: 3}}} |
||||
}) |
||||
|
||||
connection.onSelectionRanges(_params => { |
||||
return [ |
||||
SelectionRange.create(Range.create(1, 2, 3, 4)) |
||||
] |
||||
}) |
||||
|
||||
let lastFileOperationRequest |
||||
connection.workspace.onDidCreateFiles(params => {lastFileOperationRequest = {type: 'create', params}}) |
||||
connection.workspace.onDidRenameFiles(params => {lastFileOperationRequest = {type: 'rename', params}}) |
||||
connection.workspace.onDidDeleteFiles(params => {lastFileOperationRequest = {type: 'delete', params}}) |
||||
|
||||
connection.onRequest( |
||||
new ProtocolRequestType('testing/lastFileOperationRequest'), |
||||
() => { |
||||
return lastFileOperationRequest |
||||
}, |
||||
) |
||||
|
||||
connection.workspace.onWillCreateFiles(params => { |
||||
const createdFilenames = params.files.map(f => `${f.uri}`).join('\n') |
||||
return { |
||||
documentChanges: [{ |
||||
textDocument: {uri: '/dummy-edit', version: null}, |
||||
edits: [ |
||||
TextEdit.insert(Position.create(0, 0), `WILL CREATE:\n${createdFilenames}`), |
||||
] |
||||
}], |
||||
} |
||||
}) |
||||
|
||||
connection.workspace.onWillRenameFiles(params => { |
||||
const renamedFilenames = params.files.map(f => `${f.oldUri} -> ${f.newUri}`).join('\n') |
||||
return { |
||||
documentChanges: [{ |
||||
textDocument: {uri: '/dummy-edit', version: null}, |
||||
edits: [ |
||||
TextEdit.insert(Position.create(0, 0), `WILL RENAME:\n${renamedFilenames}`), |
||||
] |
||||
}], |
||||
} |
||||
}) |
||||
|
||||
connection.workspace.onWillDeleteFiles(params => { |
||||
const deletedFilenames = params.files.map(f => `${f.uri}`).join('\n') |
||||
return { |
||||
documentChanges: [{ |
||||
textDocument: {uri: '/dummy-edit', version: null}, |
||||
edits: [ |
||||
TextEdit.insert(Position.create(0, 0), `WILL DELETE:\n${deletedFilenames}`), |
||||
] |
||||
}], |
||||
} |
||||
}) |
||||
|
||||
connection.onTypeDefinition(params => { |
||||
assert.equal(params.position.line, 1) |
||||
assert.equal(params.position.character, 1) |
||||
return {uri: params.textDocument.uri, range: {start: {line: 2, character: 2}, end: {line: 3, character: 3}}} |
||||
}) |
||||
|
||||
connection.languages.callHierarchy.onPrepare(params => { |
||||
return [ |
||||
{ |
||||
kind: SymbolKind.Function, |
||||
name: 'name', |
||||
range: Range.create(1, 1, 1, 1), |
||||
selectionRange: Range.create(2, 2, 2, 2), |
||||
uri: params.textDocument.uri |
||||
} |
||||
] |
||||
}) |
||||
|
||||
connection.languages.callHierarchy.onIncomingCalls(params => { |
||||
return [ |
||||
{ |
||||
from: params.item, |
||||
fromRanges: [Range.create(1, 1, 1, 1)] |
||||
} |
||||
] |
||||
}) |
||||
|
||||
connection.languages.callHierarchy.onOutgoingCalls(params => { |
||||
return [ |
||||
{ |
||||
to: params.item, |
||||
fromRanges: [Range.create(1, 1, 1, 1)] |
||||
} |
||||
] |
||||
}) |
||||
|
||||
connection.languages.semanticTokens.onRange(() => { |
||||
return { |
||||
resultId: '1', |
||||
data: [] |
||||
} |
||||
}) |
||||
|
||||
connection.languages.semanticTokens.on(() => { |
||||
return { |
||||
resultId: '2', |
||||
data: [] |
||||
} |
||||
}) |
||||
|
||||
connection.languages.semanticTokens.onDelta(() => { |
||||
return { |
||||
resultId: '3', |
||||
data: [] |
||||
} |
||||
}) |
||||
|
||||
connection.languages.diagnostics.on(() => { |
||||
return { |
||||
kind: DocumentDiagnosticReportKind.Full, |
||||
items: [ |
||||
Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) |
||||
] |
||||
} |
||||
}) |
||||
|
||||
connection.languages.diagnostics.onWorkspace(() => { |
||||
return { |
||||
items: [{ |
||||
kind: DocumentDiagnosticReportKind.Full, |
||||
uri: 'uri', |
||||
version: 1, |
||||
items: [ |
||||
Diagnostic.create(Range.create(1, 1, 1, 1), 'diagnostic', DiagnosticSeverity.Error) |
||||
] |
||||
}] |
||||
} |
||||
}) |
||||
|
||||
const typeHierarchySample = { |
||||
superTypes: [], |
||||
subTypes: [] |
||||
} |
||||
connection.languages.typeHierarchy.onPrepare(params => { |
||||
const currentItem = { |
||||
kind: SymbolKind.Class, |
||||
name: 'ClazzB', |
||||
range: Range.create(1, 1, 1, 1), |
||||
selectionRange: Range.create(2, 2, 2, 2), |
||||
uri: params.textDocument.uri |
||||
} |
||||
typeHierarchySample.superTypes = [{...currentItem, name: 'classA', uri: 'uri-for-A'}] |
||||
typeHierarchySample.subTypes = [{...currentItem, name: 'classC', uri: 'uri-for-C'}] |
||||
return [currentItem] |
||||
}) |
||||
|
||||
connection.languages.typeHierarchy.onSupertypes(_params => { |
||||
return typeHierarchySample.superTypes |
||||
}) |
||||
|
||||
connection.languages.typeHierarchy.onSubtypes(_params => { |
||||
return typeHierarchySample.subTypes |
||||
}) |
||||
|
||||
connection.languages.inlineValue.on(_params => { |
||||
return [ |
||||
InlineValueText.create(Range.create(1, 2, 3, 4), 'text'), |
||||
InlineValueVariableLookup.create(Range.create(1, 2, 3, 4), 'variableName', false), |
||||
InlineValueEvaluatableExpression.create(Range.create(1, 2, 3, 4), 'expression'), |
||||
] |
||||
}) |
||||
connection.languages.inlayHint.on(() => { |
||||
const one = InlayHint.create(Position.create(1, 1), [InlayHintLabelPart.create('type')], InlayHintKind.Type) |
||||
one.data = '1' |
||||
const two = InlayHint.create(Position.create(2, 2), [InlayHintLabelPart.create('parameter')], InlayHintKind.Parameter) |
||||
two.data = '2' |
||||
return [one, two] |
||||
}) |
||||
|
||||
connection.languages.inlayHint.resolve(hint => { |
||||
if (typeof hint.label === 'string') { |
||||
hint.label = 'tooltip' |
||||
} else { |
||||
hint.label[0].tooltip = 'tooltip' |
||||
} |
||||
return hint |
||||
}) |
||||
|
||||
connection.languages.onLinkedEditingRange(() => { |
||||
return { |
||||
ranges: [Range.create(1, 1, 1, 1)], |
||||
wordPattern: '\\w' |
||||
} |
||||
}) |
||||
|
||||
connection.onRequest( |
||||
new ProtocolRequestType('testing/sendSampleProgress'), |
||||
async (_, __) => { |
||||
const progressToken = 'TEST-PROGRESS-TOKEN' |
||||
await connection.sendRequest(WorkDoneProgressCreateRequest.type, {token: progressToken}) |
||||
void connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'begin', title: 'Test Progress'}) |
||||
void connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'report', percentage: 50, message: 'Halfway!'}) |
||||
void connection.sendProgress(WorkDoneProgress.type, progressToken, {kind: 'end', message: 'Completed!'}) |
||||
}, |
||||
) |
||||
|
||||
connection.onRequest( |
||||
new ProtocolRequestType('testing/beginOnlyProgress'), |
||||
async (_, __) => { |
||||
const progressToken = 'TEST-PROGRESS-BEGIN' |
||||
await connection.sendRequest(WorkDoneProgressCreateRequest.type, {token: progressToken}) |
||||
}, |
||||
) |
||||
|
||||
connection.onWorkspaceSymbol(() => { |
||||
return [ |
||||
{name: 'name', kind: SymbolKind.Array, location: {uri: 'file:///abc.txt'}} |
||||
] |
||||
}) |
||||
|
||||
connection.onWorkspaceSymbolResolve(symbol => { |
||||
symbol.location = Location.create(symbol.location.uri, Range.create(1, 2, 3, 4)) |
||||
return symbol |
||||
}) |
||||
|
||||
// Listen on the connection
|
||||
connection.listen() |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
"use strict" |
||||
Object.defineProperty(exports, "__esModule", {value: true}) |
||||
const node_1 = require("vscode-languageserver") |
||||
const connection = (0, node_1.createConnection)() |
||||
connection.onInitialize((_params) => { |
||||
return {capabilities: {}} |
||||
}) |
||||
connection.onShutdown(async () => { |
||||
return new Promise((resolve) => { |
||||
setTimeout(resolve, 200000) |
||||
}) |
||||
}) |
||||
connection.listen() |
@ -0,0 +1,324 @@
@@ -0,0 +1,324 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import fs from 'fs' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { v4 as uuidv4 } from 'uuid' |
||||
import { DidChangeTextDocumentNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, DocumentSelector, Position, Range, TextDocumentSaveReason, TextEdit, WillSaveTextDocumentNotification } from 'vscode-languageserver-protocol' |
||||
import { TextDocument } from 'vscode-languageserver-textdocument' |
||||
import { URI } from 'vscode-uri' |
||||
import { LanguageClient, LanguageClientOptions, Middleware, ServerOptions, TransportKind } from '../../language-client/index' |
||||
import { TextDocumentContentChange } from '../../types' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
function createClient(documentSelector: DocumentSelector | undefined | null, middleware: Middleware = {}, opts: any = {}): LanguageClient { |
||||
const serverModule = path.join(__dirname, './server/testDocuments.js') |
||||
const serverOptions: ServerOptions = { |
||||
run: { module: serverModule, transport: TransportKind.ipc }, |
||||
debug: { module: serverModule, transport: TransportKind.ipc, options: { execArgv: ['--nolazy', '--inspect=6014'] } } |
||||
} |
||||
if (documentSelector === undefined) documentSelector = [{ scheme: 'file' }] |
||||
const clientOptions: LanguageClientOptions = { |
||||
documentSelector, |
||||
synchronize: {}, |
||||
initializationOptions: opts, |
||||
middleware |
||||
}; |
||||
(clientOptions as ({ $testMode?: boolean })).$testMode = true |
||||
|
||||
const result = new LanguageClient('test', 'Test Language Server', serverOptions, clientOptions) |
||||
return result |
||||
} |
||||
|
||||
let nvim: Neovim |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = workspace.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('TextDocumentSynchronization', () => { |
||||
describe('DidOpenTextDocumentFeature', () => { |
||||
it('should register with empty documentSelector', async () => { |
||||
let client = createClient(undefined) |
||||
await client.start() |
||||
let feature = client.getFeature(DidOpenTextDocumentNotification.method) |
||||
feature.register({ id: uuidv4(), registerOptions: { documentSelector: null } }) |
||||
let res = await client.sendRequest('getLastOpen') |
||||
expect(res).toBe(null) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should send event on document create', async () => { |
||||
let client = createClient([{ language: 'vim' }]) |
||||
await client.start() |
||||
let uri = URI.file(path.join(os.tmpdir(), 't.vim')) |
||||
let doc = await workspace.loadFile(uri.toString()) |
||||
expect(doc.languageId).toBe('vim') |
||||
let res = await client.sendRequest('getLastOpen') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
expect(res.version).toBe(doc.version) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should work with middleware', async () => { |
||||
let called = false |
||||
let client = createClient([{ language: 'vim' }], { |
||||
didOpen: (doc, next) => { |
||||
called = true |
||||
return next(doc) |
||||
} |
||||
}) |
||||
await client.start() |
||||
let uri = URI.file(path.join(os.tmpdir(), 't.js')) |
||||
let doc = await workspace.loadFile(uri.toString()) |
||||
expect(doc.languageId).toBe('javascript') |
||||
let feature = client.getFeature(DidOpenTextDocumentNotification.method) |
||||
feature.register({ id: uuidv4(), registerOptions: { documentSelector: [{ language: 'javascript' }] } }) |
||||
let res = await client.sendRequest('getLastOpen') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
expect(called).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('DidCloseTextDocumentFeature', () => { |
||||
it('should send close event', async () => { |
||||
let uri = URI.file(path.join(os.tmpdir(), 't.vim')) |
||||
let doc = await workspace.loadFile(uri.toString()) |
||||
let client = createClient([{ language: 'vim' }]) |
||||
await client.start() |
||||
await workspace.nvim.command(`bd! ${doc.bufnr}`) |
||||
await helper.wait(30) |
||||
let res = await client.sendRequest('getLastClose') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should unregister document selector', async () => { |
||||
let called = false |
||||
let client = createClient([{ language: 'javascript' }], { |
||||
didClose: (e, next) => { |
||||
called = true |
||||
return next(e) |
||||
} |
||||
}) |
||||
await client.start() |
||||
let openFeature = client.getFeature(DidOpenTextDocumentNotification.method) |
||||
let id = uuidv4() |
||||
let options = { id, registerOptions: { documentSelector: [{ language: 'vim' }] } } |
||||
openFeature.register(options) |
||||
let feature = client.getFeature(DidCloseTextDocumentNotification.method) |
||||
feature.register(options) |
||||
let uri = URI.file(path.join(os.tmpdir(), 't.vim')) |
||||
let doc = await workspace.loadFile(uri.toString()) |
||||
await helper.wait(30) |
||||
feature.unregister(id) |
||||
feature.unregister('unknown') |
||||
await helper.wait(30) |
||||
let res = await client.sendRequest('getLastClose') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
expect(called).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('DidChangeTextDocumentFeature', () => { |
||||
it('should send full change event ', async () => { |
||||
let called = false |
||||
let client = createClient([{ language: 'vim' }], { |
||||
didChange: (e, next) => { |
||||
called = true |
||||
return next(e) |
||||
} |
||||
}) |
||||
await client.start() |
||||
let uri = URI.file(path.join(os.tmpdir(), 'x.vim')) |
||||
let doc = await workspace.loadFile(uri.toString()) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) |
||||
await client.forceDocumentSync() |
||||
let res = await client.sendRequest('getLastChange') as any |
||||
expect(res.text).toBe('bar\n') |
||||
expect(called).toBe(true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should send incremental change event', async () => { |
||||
let client = createClient([{ scheme: 'lsptest' }]) |
||||
await client.start() |
||||
await client.sendNotification('registerDocumentSync') |
||||
await helper.wait(30) |
||||
let feature = client.getFeature(DidChangeTextDocumentNotification.method) |
||||
let fn = jest.fn() |
||||
feature.onNotificationSent(() => { |
||||
fn() |
||||
}) |
||||
await nvim.command(`edit ${uuidv4()}.vim`) |
||||
let doc = await workspace.document |
||||
await nvim.call('setline', [1, 'foo']) |
||||
await doc.synchronize() |
||||
await client.forceDocumentSync() |
||||
await helper.wait(50) |
||||
await nvim.call('setline', [1, 'foo']) |
||||
await doc.synchronize() |
||||
expect(fn).toBeCalled() |
||||
let res = await client.sendRequest('getLastChange') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
expect(res.text).toBe('foo\n') |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
expect(provider).toBeDefined() |
||||
await provider.send({ contentChanges: [], textDocument: { uri: doc.uri, version: doc.version }, bufnr: doc.bufnr, original: '', originalLines: [] }) |
||||
await client.sendNotification('unregisterDocumentSync') |
||||
await helper.wait(10) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should not send change event when syncKind is none', async () => { |
||||
let client = createClient([{ scheme: 'lsptest' }], {}, { none: true }) |
||||
await client.start() |
||||
await client.sendNotification('registerDocumentSync') |
||||
await nvim.command('edit x.vim') |
||||
let doc = await workspace.document |
||||
|
||||
let feature = client.getFeature(DidChangeTextDocumentNotification.method) |
||||
await helper.waitValue(() => { |
||||
return feature.getProvider(doc.textDocument) != null |
||||
}, true) |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
let changes: TextDocumentContentChange[] = [{ |
||||
range: Range.create(0, 0, 0, 0), |
||||
text: 'foo' |
||||
}] |
||||
await provider.send({ contentChanges: changes, textDocument: { uri: doc.uri, version: doc.version }, bufnr: doc.bufnr } as any) |
||||
let res = await client.sendRequest('getLastChange') as any |
||||
expect(res.text).toBe('\n') |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('WillSaveFeature', () => { |
||||
it('should will save event', async () => { |
||||
let called = false |
||||
let client = createClient([{ language: 'vim' }], { |
||||
willSave: (e, next) => { |
||||
called = true |
||||
return next(e) |
||||
} |
||||
}) |
||||
await client.start() |
||||
let fsPath = path.join(os.tmpdir(), `${uuidv4()}.vim`) |
||||
let uri = URI.file(fsPath) |
||||
await workspace.openResource(uri.toString()) |
||||
let doc = await workspace.document |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) |
||||
let feature = client.getFeature(WillSaveTextDocumentNotification.method) |
||||
let provider = feature.getProvider(doc.textDocument) |
||||
expect(provider).toBeDefined() |
||||
await provider.send({ document: doc.textDocument, reason: TextDocumentSaveReason.Manual, waitUntil: () => {} }) |
||||
let res = await client.sendRequest('getLastWillSave') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
await client.stop() |
||||
expect(called).toBe(true) |
||||
if (fs.existsSync(fsPath)) { |
||||
fs.unlinkSync(fsPath) |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
describe('WillSaveWaitUntilFeature', () => { |
||||
it('should send will save until request', async () => { |
||||
let client = createClient([{ scheme: 'lsptest' }]) |
||||
await client.start() |
||||
await client.sendNotification('registerDocumentSync') |
||||
await helper.wait(30) |
||||
let fsPath = path.join(os.tmpdir(), `${uuidv4()}-foo.vim`) |
||||
let uri = URI.file(fsPath) |
||||
await workspace.openResource(uri.toString()) |
||||
let doc = await workspace.document |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'x')]) |
||||
nvim.command('w', true) |
||||
await helper.waitValue(() => { |
||||
return doc.getDocumentContent() |
||||
}, 'abcx\n') |
||||
await client.sendNotification('unregisterDocumentSync') |
||||
await client.stop() |
||||
if (fs.existsSync(fsPath)) { |
||||
fs.unlinkSync(fsPath) |
||||
} |
||||
}) |
||||
|
||||
it('should not throw on response error', async () => { |
||||
let called = false |
||||
let client = createClient([], { |
||||
willSaveWaitUntil: (event, next) => { |
||||
called = true |
||||
return next(event) |
||||
} |
||||
}) |
||||
await client.start() |
||||
await client.sendNotification('registerDocumentSync') |
||||
let fsPath = path.join(os.tmpdir(), `${uuidv4()}-error.vim`) |
||||
let uri = URI.file(fsPath) |
||||
await helper.waitValue(() => { |
||||
let feature = client.getFeature(DidOpenTextDocumentNotification.method) |
||||
let provider = feature.getProvider(TextDocument.create(uri.toString(), 'vim', 1, '')) |
||||
return provider != null |
||||
}, true) |
||||
await workspace.openResource(uri.toString()) |
||||
let doc = await workspace.document |
||||
await doc.synchronize() |
||||
nvim.command('w', true) |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
|
||||
it('should unregister event handler', async () => { |
||||
let client = createClient(null) |
||||
await client.start() |
||||
await client.sendNotification('registerDocumentSync') |
||||
await helper.waitValue(() => { |
||||
let feature = client.getFeature(DidOpenTextDocumentNotification.method) |
||||
let provider = feature.getProvider(TextDocument.create('file:///f.vim', 'vim', 1, '')) |
||||
return provider != null |
||||
}, true) |
||||
await client.sendNotification('unregisterDocumentSync') |
||||
await helper.waitValue(() => { |
||||
let feature = client.getFeature(DidOpenTextDocumentNotification.method) |
||||
let provider = feature.getProvider(TextDocument.create('file:///f.vim', 'vim', 1, '')) |
||||
return provider == null |
||||
}, true) |
||||
await client.stop() |
||||
}) |
||||
}) |
||||
|
||||
describe('DidSaveTextDocumentFeature', () => { |
||||
it('should send did save notification', async () => { |
||||
let called = false |
||||
let client = createClient([{ language: 'vim' }], { |
||||
didSave: (e, next) => { |
||||
called = true |
||||
return next(e) |
||||
} |
||||
}) |
||||
await client.start() |
||||
let fsPath = path.join(os.tmpdir(), `${uuidv4()}.vim`) |
||||
let uri = URI.file(fsPath) |
||||
await workspace.openResource(uri.toString()) |
||||
let doc = await workspace.document |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'bar')]) |
||||
nvim.command('w', true) |
||||
await helper.waitValue(() => { |
||||
return called |
||||
}, true) |
||||
let res = await client.sendRequest('getLastWillSave') as any |
||||
expect(res.uri).toBe(doc.uri) |
||||
await client.stop() |
||||
fs.unlinkSync(fsPath) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,103 @@
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable */ |
||||
import assert from 'assert' |
||||
import { Delayer } from '../../language-client/utils/async' |
||||
import { ConsoleLogger, NullLogger } from '../../language-client/utils/logger' |
||||
import { wait } from '../../util/index' |
||||
import { CloseAction, DefaultErrorHandler, ErrorAction } from '../../language-client/utils/errorHandler' |
||||
import helper from '../helper' |
||||
|
||||
test('Logger', async () => { |
||||
const logger = new ConsoleLogger() |
||||
logger.error('error') |
||||
logger.warn('warn') |
||||
logger.info('info') |
||||
logger.log('log') |
||||
const nullLogger = new NullLogger() |
||||
nullLogger.error('error') |
||||
nullLogger.warn('warn') |
||||
nullLogger.info('info') |
||||
nullLogger.log('log') |
||||
}) |
||||
|
||||
test('DefaultErrorHandler', async () => { |
||||
const handler = new DefaultErrorHandler('test', 2) |
||||
expect(handler.error(new Error('test'), { jsonrpc: '' }, 1)).toBe(ErrorAction.Continue) |
||||
expect(handler.error(new Error('test'), { jsonrpc: '' }, 5)).toBe(ErrorAction.Shutdown) |
||||
handler.closed() |
||||
handler.milliseconds = 1 |
||||
await helper.wait(10) |
||||
let res = handler.closed() |
||||
expect(res).toBe(CloseAction.Restart) |
||||
handler.milliseconds = 10 * 1000 |
||||
res = handler.closed() |
||||
expect(res).toBe(CloseAction.DoNotRestart) |
||||
}) |
||||
|
||||
test('Delayer', () => { |
||||
let count = 0 |
||||
let factory = () => { |
||||
return Promise.resolve(++count) |
||||
} |
||||
|
||||
let delayer = new Delayer(0) |
||||
let promises: Thenable<any>[] = [] |
||||
|
||||
assert(!delayer.isTriggered()) |
||||
|
||||
promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })) |
||||
assert(delayer.isTriggered()) |
||||
|
||||
promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })) |
||||
assert(delayer.isTriggered()) |
||||
|
||||
promises.push(delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) })) |
||||
assert(delayer.isTriggered()) |
||||
|
||||
return Promise.all(promises).then(() => { |
||||
assert(!delayer.isTriggered()) |
||||
}).finally(() => { |
||||
delayer.dispose() |
||||
}) |
||||
}) |
||||
|
||||
test('Delayer - forceDelivery', async () => { |
||||
let count = 0 |
||||
let factory = () => { |
||||
return Promise.resolve(++count) |
||||
} |
||||
|
||||
let delayer = new Delayer(150) |
||||
delayer.forceDelivery() |
||||
delayer.trigger(factory).then((result) => { assert.equal(result, 1); assert(!delayer.isTriggered()) }) |
||||
await wait(10) |
||||
delayer.forceDelivery() |
||||
expect(count).toBe(1) |
||||
void delayer.trigger(factory) |
||||
delayer.trigger(factory, -1) |
||||
await wait(10) |
||||
delayer.cancel() |
||||
expect(count).toBe(1) |
||||
}) |
||||
|
||||
test('Delayer - last task should be the one getting called', function() { |
||||
let factoryFactory = (n: number) => () => { |
||||
return Promise.resolve(n) |
||||
} |
||||
|
||||
let delayer = new Delayer(0) |
||||
let promises: Thenable<any>[] = [] |
||||
|
||||
assert(!delayer.isTriggered()) |
||||
|
||||
promises.push(delayer.trigger(factoryFactory(1)).then((n) => { assert.equal(n, 3) })) |
||||
promises.push(delayer.trigger(factoryFactory(2)).then((n) => { assert.equal(n, 3) })) |
||||
promises.push(delayer.trigger(factoryFactory(3)).then((n) => { assert.equal(n, 3) })) |
||||
|
||||
const p = Promise.all(promises).then(() => { |
||||
assert(!delayer.isTriggered()) |
||||
}) |
||||
|
||||
assert(delayer.isTriggered()) |
||||
|
||||
return p |
||||
}) |
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
'use strict' |
||||
|
||||
import * as assert from 'assert' |
||||
import { WorkspaceFoldersFeature } from '../../language-client/workspaceFolders' |
||||
import { BaseLanguageClient, MessageTransports } from '../../language-client/client' |
||||
import { Disposable, DidChangeWorkspaceFoldersParams } from 'vscode-languageserver-protocol' |
||||
import * as proto from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
|
||||
class TestLanguageClient extends BaseLanguageClient { |
||||
protected createMessageTransports(): Promise<MessageTransports> { |
||||
throw new Error('Method not implemented.') |
||||
} |
||||
public onRequest(): Disposable { |
||||
return { |
||||
dispose: () => {} |
||||
} |
||||
} |
||||
} |
||||
|
||||
type MaybeFolders = proto.WorkspaceFolder[] | undefined |
||||
|
||||
class TestWorkspaceFoldersFeature extends WorkspaceFoldersFeature { |
||||
public sendInitialEvent(currentWorkspaceFolders: MaybeFolders): void { |
||||
super.sendInitialEvent(currentWorkspaceFolders) |
||||
} |
||||
|
||||
public initializeWithFolders(currentWorkspaceFolders: MaybeFolders) { |
||||
super.initializeWithFolders(currentWorkspaceFolders) |
||||
} |
||||
} |
||||
|
||||
function testEvent(initial: MaybeFolders, then: MaybeFolders, added: proto.WorkspaceFolder[], removed: proto.WorkspaceFolder[]) { |
||||
const client = new TestLanguageClient('foo', 'bar', {}) |
||||
|
||||
let arg: any |
||||
let spy = jest.spyOn(client, 'sendNotification').mockImplementation((_p1, p2) => { |
||||
arg = p2 |
||||
return Promise.resolve() |
||||
}) |
||||
|
||||
const feature = new TestWorkspaceFoldersFeature(client) |
||||
|
||||
feature.initializeWithFolders(initial) |
||||
feature.sendInitialEvent(then) |
||||
|
||||
expect(spy).toHaveBeenCalled() |
||||
expect(spy).toHaveBeenCalledTimes(1) |
||||
const notification: DidChangeWorkspaceFoldersParams = arg |
||||
assert.deepEqual(notification.event.added, added) |
||||
assert.deepEqual(notification.event.removed, removed) |
||||
} |
||||
|
||||
function testNoEvent(initial: MaybeFolders, then: MaybeFolders) { |
||||
const client = new TestLanguageClient('foo', 'bar', {}) |
||||
|
||||
let spy = jest.spyOn(client, 'sendNotification').mockImplementation(() => { |
||||
return Promise.resolve() |
||||
}) |
||||
|
||||
const feature = new TestWorkspaceFoldersFeature(client) |
||||
|
||||
feature.initializeWithFolders(initial) |
||||
feature.sendInitialEvent(then) |
||||
expect(spy).toHaveBeenCalledTimes(0) |
||||
} |
||||
|
||||
describe('Workspace Folder Feature Tests', () => { |
||||
const removedFolder = { uri: URI.parse('file://xox/removed').toString(), name: 'removedName', index: 0 } |
||||
const addedFolder = { uri: URI.parse('file://foo/added').toString(), name: 'addedName', index: 0 } |
||||
const addedProto = { uri: 'file://foo/added', name: 'addedName' } |
||||
const removedProto = { uri: 'file://xox/removed', name: 'removedName' } |
||||
|
||||
test('remove/add', async () => { |
||||
assert.ok(!MessageTransports.is({})) |
||||
testEvent([removedFolder], [addedFolder], [addedProto], [removedProto]) |
||||
}) |
||||
|
||||
test('remove', async () => { |
||||
testEvent([removedFolder], [], [], [removedProto]) |
||||
}) |
||||
|
||||
test('remove2', async () => { |
||||
testEvent([removedFolder], undefined, [], [removedProto]) |
||||
}) |
||||
|
||||
test('add', async () => { |
||||
testEvent([], [addedFolder], [addedProto], []) |
||||
}) |
||||
|
||||
test('add2', async () => { |
||||
testEvent(undefined, [addedFolder], [addedProto], []) |
||||
}) |
||||
|
||||
test('noChange1', async () => { |
||||
testNoEvent([addedFolder, removedFolder], [addedFolder, removedFolder]) |
||||
}) |
||||
|
||||
test('noChange2', async () => { |
||||
testNoEvent([], []) |
||||
}) |
||||
|
||||
test('noChange3', async () => { |
||||
testNoEvent(undefined, undefined) |
||||
}) |
||||
}) |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
{ |
||||
"suggest.timeout": 5000, |
||||
"tslint.enable": false |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,179 @@
@@ -0,0 +1,179 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import Floating from '../../completion/floating' |
||||
import { fixFollow } from '../../completion/pum' |
||||
import sources from '../../sources' |
||||
import { CompleteResult, FloatConfig, ISource, SourceType } from '../../types' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let source: ISource |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
source = { |
||||
name: 'float', |
||||
priority: 10, |
||||
enable: true, |
||||
sourceType: SourceType.Native, |
||||
doComplete: (): Promise<CompleteResult> => Promise.resolve({ |
||||
items: [{ |
||||
word: 'foo', |
||||
info: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' |
||||
}, { |
||||
word: 'foot', |
||||
info: 'foot' |
||||
}, { |
||||
word: 'football', |
||||
}] |
||||
}) |
||||
} |
||||
sources.addSource(source) |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
sources.removeSource(source) |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('completion float', () => { |
||||
it('should fix word by check follow', async () => { |
||||
expect(fixFollow('foo', '', '')).toBe('foo') |
||||
expect(fixFollow('foobar', '', 'oobar')).toBe('f') |
||||
expect(fixFollow('foobar', 'f', 'oobar')).toBe('f') |
||||
expect(fixFollow('foobar', 'foo', 'oobar')).toBe('foobar') |
||||
}) |
||||
|
||||
it('should cancel float window', async () => { |
||||
await helper.edit() |
||||
await nvim.input('if') |
||||
await helper.visible('foo', 'float') |
||||
let items = await helper.getItems() |
||||
expect(items[0].word).toBe('foo') |
||||
expect(items[0].info.length > 0).toBeTruthy() |
||||
await helper.confirmCompletion(0) |
||||
let hasFloat = await nvim.call('coc#float#has_float') |
||||
expect(hasFloat).toBe(0) |
||||
}) |
||||
|
||||
it('should adjust float window position', async () => { |
||||
await helper.edit() |
||||
await nvim.setLine(' '.repeat(70)) |
||||
await nvim.input('Af') |
||||
await helper.visible('foo', 'float') |
||||
let floatWin = await helper.getFloat('pumdetail') |
||||
let config = await floatWin.getConfig() |
||||
expect(config.col + config.width).toBeLessThan(180) |
||||
}) |
||||
|
||||
it('should redraw float window on item change', async () => { |
||||
await helper.edit() |
||||
await nvim.setLine(' '.repeat(70)) |
||||
await nvim.input('Af') |
||||
await helper.visible('foo', 'float') |
||||
await nvim.call('coc#pum#select', [1, 1, 0]) |
||||
let floatWin = await helper.getFloat('pumdetail') |
||||
let buf = await floatWin.buffer |
||||
let lines = await buf.lines |
||||
expect(lines.length).toBeGreaterThan(0) |
||||
expect(lines[0]).toMatch('foot') |
||||
}) |
||||
|
||||
it('should hide float window when item info is empty', async () => { |
||||
await helper.edit() |
||||
await nvim.setLine(' '.repeat(70)) |
||||
await nvim.input('Af') |
||||
await helper.visible('foo', 'float') |
||||
await nvim.call('coc#pum#select', [2, 1, 0]) |
||||
let floatWin = await helper.getFloat('pumdetail') |
||||
expect(floatWin).toBeUndefined() |
||||
}) |
||||
|
||||
it('should hide float window after completion', async () => { |
||||
await helper.edit() |
||||
await nvim.setLine(' '.repeat(70)) |
||||
await nvim.input('Af') |
||||
await helper.visible('foo', 'float') |
||||
await nvim.input('<C-n>') |
||||
await helper.wait(30) |
||||
await nvim.input('<C-y>') |
||||
await helper.wait(30) |
||||
let floatWin = await helper.getFloat('pumdetail') |
||||
expect(floatWin).toBeUndefined() |
||||
}) |
||||
}) |
||||
|
||||
describe('float config', () => { |
||||
beforeEach(async () => { |
||||
await nvim.input('of') |
||||
await helper.waitPopup() |
||||
}) |
||||
|
||||
async function createFloat(config: Partial<FloatConfig>, docs = [{ filetype: 'txt', content: 'doc' }]): Promise<Floating> { |
||||
let floating = new Floating(nvim, { |
||||
floatConfig: { |
||||
border: true, |
||||
...config |
||||
} |
||||
}) |
||||
floating.show(docs) |
||||
return floating |
||||
} |
||||
|
||||
async function getFloat(): Promise<number> { |
||||
let win = await helper.getFloat('pumdetail') |
||||
return win ? win.id : -1 |
||||
} |
||||
|
||||
async function getRelated(winid: number, kind: string): Promise<number> { |
||||
if (!winid || winid == -1) return -1 |
||||
let win = nvim.createWindow(winid) |
||||
let related = await win.getVar('related') as number[] |
||||
if (!related || !related.length) return -1 |
||||
for (let id of related) { |
||||
let w = nvim.createWindow(id) |
||||
let v = await w.getVar('kind') |
||||
if (v == kind) { |
||||
return id |
||||
} |
||||
} |
||||
return -1 |
||||
} |
||||
|
||||
it('should not shown with empty lines', async () => { |
||||
await createFloat({}, [{ filetype: 'txt', content: '' }]) |
||||
let floatWin = await helper.getFloat('pumdetail') |
||||
expect(floatWin).toBeUndefined() |
||||
}) |
||||
|
||||
it('should show window with border', async () => { |
||||
await createFloat({ border: true, rounded: true, focusable: true }) |
||||
let winid = await getFloat() |
||||
expect(winid).toBeGreaterThan(0) |
||||
let id = await getRelated(winid, 'border') |
||||
expect(id).toBeGreaterThan(0) |
||||
}) |
||||
|
||||
it('should change window highlights', async () => { |
||||
await createFloat({ border: true, highlight: 'WarningMsg', borderhighlight: 'MoreMsg' }) |
||||
let winid = await getFloat() |
||||
expect(winid).toBeGreaterThan(0) |
||||
let win = nvim.createWindow(winid) |
||||
let res = await win.getOption('winhl') as string |
||||
expect(res).toMatch('WarningMsg') |
||||
let id = await getRelated(winid, 'border') |
||||
expect(id).toBeGreaterThan(0) |
||||
win = nvim.createWindow(id) |
||||
res = await win.getOption('winhl') as string |
||||
expect(res).toMatch('MoreMsg') |
||||
}) |
||||
|
||||
it('should add shadow and winblend', async () => { |
||||
await createFloat({ shadow: true, winblend: 30 }) |
||||
let winid = await getFloat() |
||||
expect(winid).toBeGreaterThan(0) |
||||
}) |
||||
}) |
@ -0,0 +1,617 @@
@@ -0,0 +1,617 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
import { CompletionItem, CompletionList, InsertReplaceEdit, InsertTextFormat, InsertTextMode, Position, Range, TextEdit } from 'vscode-languageserver-types' |
||||
import completion from '../../completion' |
||||
import languages from '../../languages' |
||||
import { CompletionItemProvider } from '../../provider' |
||||
import snippetManager from '../../snippets/manager' |
||||
import { getRange, emptLabelDetails, getStartColumn, ItemDefaults } from '../../sources/source-language' |
||||
import { disposeAll } from '../../util' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('LanguageSource util', () => { |
||||
describe('emptLabelDetails', () => { |
||||
it('should check emptLabelDetails', async () => { |
||||
expect(emptLabelDetails(null)).toBe(true) |
||||
expect(emptLabelDetails({})).toBe(true) |
||||
expect(emptLabelDetails({ detail: '' })).toBe(true) |
||||
expect(emptLabelDetails({ detail: 'detail' })).toBe(false) |
||||
expect(emptLabelDetails({ description: 'detail' })).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('getStartColumn()', () => { |
||||
it('should get start col', async () => { |
||||
expect(getStartColumn('', [{ label: 'foo' }])).toBe(undefined) |
||||
expect(getStartColumn('', [{ label: 'foo' }], { editRange: Range.create(0, 0, 0, 3) })).toBe(0) |
||||
expect(getStartColumn('', [ |
||||
{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 0), 'a') }, |
||||
{ label: 'bar' }])).toBe(undefined) |
||||
expect(getStartColumn('foo', [ |
||||
{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 0), 'a') }, |
||||
{ label: 'bar', textEdit: TextEdit.insert(Position.create(0, 1), 'b') }])).toBe(undefined) |
||||
expect(getStartColumn('foo', [ |
||||
{ label: 'foo', textEdit: TextEdit.insert(Position.create(0, 2), 'a') }, |
||||
{ label: 'bar', textEdit: TextEdit.insert(Position.create(0, 2), 'b') }])).toBe(2) |
||||
}) |
||||
}) |
||||
|
||||
describe('getRange()', () => { |
||||
it('should use range from textEdit', async () => { |
||||
let item = { label: 'foo', textEdit: TextEdit.replace(Range.create(0, 1, 0, 3), 'foo') } |
||||
let res = getRange(item, { editRange: Range.create(0, 0, 0, 0) }) |
||||
expect(res).toEqual(Range.create(0, 1, 0, 3)) |
||||
}) |
||||
|
||||
it('should use range from itemDefaults', async () => { |
||||
let item = { label: 'foo' } |
||||
expect(getRange(item, { editRange: Range.create(0, 0, 0, 1) })).toEqual(Range.create(0, 0, 0, 1)) |
||||
expect(getRange(item, { editRange: InsertReplaceEdit.create('', Range.create(0, 0, 0, 0), Range.create(0, 0, 0, 1)) })).toEqual(Range.create(0, 0, 0, 1)) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('language source', () => { |
||||
describe('resolveCompletionItem()', () => { |
||||
async function getDetailContent(): Promise<string | undefined> { |
||||
let winid = await nvim.call('coc#float#get_float_by_kind', ['pumdetail']) |
||||
if (!winid) return |
||||
let bufnr = await nvim.call('winbufnr', [winid]) |
||||
let lines = await (nvim.createBuffer(bufnr)).lines |
||||
return lines.join('\n') |
||||
} |
||||
|
||||
it('should add detail to preview when no resolve exists', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'foo', |
||||
detail: 'detail of foo' |
||||
}, { |
||||
label: 'bar', |
||||
detail: 'bar()' |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) |
||||
let mode = await nvim.mode |
||||
if (mode.mode !== 'i') { |
||||
await nvim.input('i') |
||||
} |
||||
nvim.call('coc#start', { source: 'foo' }, true) |
||||
await helper.waitPopup() |
||||
await helper.waitValue(async () => { |
||||
let content = await getDetailContent() |
||||
return content && /foo/.test(content) |
||||
}, true) |
||||
await nvim.input('<C-n>') |
||||
await helper.waitValue(async () => { |
||||
let content = await getDetailContent() |
||||
return content && /bar/.test(content) |
||||
}, true) |
||||
}) |
||||
|
||||
it('should add documentation to preview when no resolve exists', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'foo', |
||||
labelDetails: {}, |
||||
documentation: 'detail of foo' |
||||
}, { |
||||
label: 'bar', |
||||
documentation: { |
||||
kind: 'plaintext', |
||||
value: 'bar' |
||||
} |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) |
||||
await nvim.input('i') |
||||
await nvim.call('coc#start', { source: 'foo' }) |
||||
await helper.waitPopup() |
||||
await helper.wait(10) |
||||
let content = await getDetailContent() |
||||
expect(content).toMatch('foo') |
||||
await nvim.input('<C-n>') |
||||
await helper.wait(30) |
||||
content = await getDetailContent() |
||||
expect(content).toMatch('bar') |
||||
}) |
||||
|
||||
it('should resolve again when request cancelled', async () => { |
||||
let count = 0 |
||||
let cancelled = false |
||||
let resolved = false |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'this' |
||||
}, { |
||||
label: 'other', |
||||
}, { |
||||
label: 'third', |
||||
}], |
||||
resolveCompletionItem: (item, token) => { |
||||
if (item.label === 'this') { |
||||
count++ |
||||
if (count == 1) { |
||||
return new Promise(resolve => { |
||||
token.onCancellationRequested(() => { |
||||
cancelled = true |
||||
clearTimeout(timer) |
||||
resolve(undefined) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
resolve(item) |
||||
}, 1000) |
||||
}) |
||||
} else { |
||||
resolved = true |
||||
item.documentation = 'doc of this' |
||||
} |
||||
} |
||||
return item |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) |
||||
await nvim.input('i') |
||||
await nvim.call('coc#start', { source: 'foo' }) |
||||
await helper.waitPopup() |
||||
await nvim.input('<C-n>') |
||||
await helper.waitValue(() => { |
||||
return cancelled |
||||
}, true) |
||||
await nvim.input('<C-p>') |
||||
await helper.waitValue(() => { |
||||
return resolved |
||||
}, true) |
||||
}) |
||||
|
||||
it('should resolve once for same CompletionItem', async () => { |
||||
let count = 0 |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'this', |
||||
documentation: 'detail of this' |
||||
}], |
||||
resolveCompletionItem: item => { |
||||
if (item.label === 'this') { |
||||
count++ |
||||
} |
||||
return item |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('foo', 'f', null, provider)) |
||||
await nvim.input('i') |
||||
await nvim.call('coc#start', { source: 'foo' }) |
||||
await helper.waitPopup() |
||||
await nvim.input('h') |
||||
await helper.wait(20) |
||||
await nvim.input('i') |
||||
await helper.wait(20) |
||||
expect(count).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('labelDetails', () => { |
||||
it('should show labelDetails to documentation window', async () => { |
||||
helper.updateConfiguration('suggest.labelMaxLength', 10) |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'foo', |
||||
labelDetails: { |
||||
detail: 'foo'.repeat(5) |
||||
} |
||||
}, { |
||||
label: 'bar', |
||||
labelDetails: { |
||||
description: 'bar'.repeat(5) |
||||
} |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('i') |
||||
await nvim.call('coc#start', { source: 'edits' }) |
||||
let winid: number |
||||
await helper.waitValue(async () => { |
||||
winid = await nvim.call('coc#float#get_float_by_kind', ['pumdetail']) |
||||
return winid > 0 |
||||
}, true) |
||||
let lines = await helper.getLines(winid) |
||||
expect(lines[0]).toMatch('foo') |
||||
await nvim.call('coc#pum#next', [1]) |
||||
await helper.waitValue(async () => { |
||||
lines = await helper.getLines(winid) |
||||
return lines.join(' ').includes('bar') |
||||
}, true) |
||||
}) |
||||
}) |
||||
|
||||
describe('additionalTextEdits', () => { |
||||
it('should fix cursor position with plain text on additionalTextEdits', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'foo', |
||||
filterText: 'foo', |
||||
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'a\nbar')] |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('if') |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'barfoo') |
||||
}) |
||||
|
||||
it('should fix cursor position with snippet on additionalTextEdits', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'if', |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
textEdit: { range: Range.create(0, 0, 0, 1), newText: 'if($1)' }, |
||||
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')], |
||||
preselect: true |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('ii') |
||||
await helper.waitPopup() |
||||
let res = await helper.getItems() |
||||
let idx = res.findIndex(o => o.source == 'edits') |
||||
await helper.confirmCompletion(idx) |
||||
await helper.waitFor('col', ['.'], 8) |
||||
}) |
||||
|
||||
it('should fix cursor position with plain text snippet on additionalTextEdits', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'if', |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
textEdit: { range: Range.create(0, 0, 0, 2), newText: 'do$0' }, |
||||
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')], |
||||
preselect: true |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('iif') |
||||
await helper.waitPopup() |
||||
let items = await helper.getItems() |
||||
let idx = items.findIndex(o => o.word == 'do' && o.source == 'edits') |
||||
await helper.confirmCompletion(idx) |
||||
await helper.waitFor('getline', ['.'], 'bar do') |
||||
await helper.waitFor('col', ['.'], 7) |
||||
}) |
||||
|
||||
it('should fix cursor position with nested snippet on additionalTextEdits', async () => { |
||||
let res = await snippetManager.insertSnippet('func($1)$0') |
||||
expect(res).toBe(true) |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'if', |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
insertText: 'do$0', |
||||
additionalTextEdits: [TextEdit.insert(Position.create(0, 0), 'bar ')], |
||||
preselect: true |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('if') |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'bar func(do)') |
||||
let [, lnum, col] = await nvim.call('getcurpos') |
||||
expect(lnum).toBe(1) |
||||
expect(col).toBe(12) |
||||
}) |
||||
|
||||
it('should fix cursor position and keep placeholder with snippet on additionalTextEdits', async () => { |
||||
let text = 'foo0bar1' |
||||
await nvim.setLine(text) |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'var', |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
textEdit: { range: Range.create(0, text.length + 1, 0, text.length + 1), newText: '${1:foo} = foo0bar1' }, |
||||
additionalTextEdits: [TextEdit.del(Range.create(0, 0, 0, text.length + 1))], |
||||
preselect: true |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.'])) |
||||
await nvim.input('A.') |
||||
await helper.waitPopup() |
||||
let res = await helper.getItems() |
||||
let idx = res.findIndex(o => o.source == 'edits') |
||||
await helper.confirmCompletion(idx) |
||||
await helper.waitFor('getline', ['.'], 'foo = foo0bar1') |
||||
await helper.wait(50) |
||||
expect(snippetManager.session).toBeDefined() |
||||
let [, lnum, col] = await nvim.call('getcurpos') |
||||
expect(lnum).toBe(1) |
||||
expect(col).toBe(3) |
||||
}) |
||||
|
||||
it('should cancel current snippet session when additionalTextEdits inside snippet', async () => { |
||||
await nvim.input('i') |
||||
await snippetManager.insertSnippet('foo($1, $2)$0', true) |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'bar', |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
textEdit: { range: Range.create(0, 4, 0, 5), newText: 'bar($1)' }, |
||||
additionalTextEdits: [TextEdit.del(Range.create(0, 0, 0, 3))] |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.'])) |
||||
await nvim.input('b') |
||||
await helper.waitPopup() |
||||
let res = await helper.getItems() |
||||
let idx = res.findIndex(o => o.source == 'edits') |
||||
await helper.confirmCompletion(idx) |
||||
await helper.waitFor('getline', ['.'], '(bar(), )') |
||||
let col = await nvim.call('col', ['.']) |
||||
expect(col).toBe(6) |
||||
}) |
||||
}) |
||||
|
||||
describe('filterText', () => { |
||||
it('should fix input for snippet item', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'foo', |
||||
filterText: 'foo', |
||||
insertText: '${1:foo}($2)', |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('snippets-test', 'st', null, provider)) |
||||
await nvim.input('if') |
||||
await helper.waitPopup() |
||||
await nvim.call('coc#pum#select', [0, 1, 0]) |
||||
await helper.waitFor('getline', ['.'], 'foo') |
||||
}) |
||||
|
||||
it('should fix filterText of complete item', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'name', |
||||
sortText: '11', |
||||
textEdit: { |
||||
range: Range.create(0, 1, 0, 2), |
||||
newText: '?.name' |
||||
} |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('name', 'N', null, provider, ['.'])) |
||||
await nvim.setLine('t') |
||||
await nvim.input('A.') |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
let line = await nvim.line |
||||
expect(line).toBe('t?.name') |
||||
}) |
||||
}) |
||||
|
||||
describe('inComplete result', () => { |
||||
it('should filter in complete request', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (doc, pos, token, context): Promise<CompletionList> => { |
||||
let option = (context as any).option |
||||
if (context.triggerCharacter == '.') { |
||||
return { |
||||
isIncomplete: true, |
||||
items: [ |
||||
{ |
||||
label: 'foo' |
||||
}, { |
||||
label: 'bar' |
||||
} |
||||
] |
||||
} |
||||
} |
||||
if (option.input == 'f') { |
||||
if (token.isCancellationRequested) return |
||||
return { |
||||
isIncomplete: true, |
||||
items: [ |
||||
{ |
||||
label: 'foo' |
||||
} |
||||
] |
||||
} |
||||
} |
||||
if (option.input == 'fo') { |
||||
if (token.isCancellationRequested) return |
||||
return { |
||||
isIncomplete: false, |
||||
items: [ |
||||
{ |
||||
label: 'foo' |
||||
} |
||||
] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['.'])) |
||||
await nvim.input('i.') |
||||
await helper.waitPopup() |
||||
await nvim.input('fo') |
||||
await helper.waitValue(async () => { |
||||
let items = await helper.getItems() |
||||
return items.length |
||||
}, 1) |
||||
}) |
||||
}) |
||||
|
||||
describe('itemDefaults', () => { |
||||
async function start(item: CompletionItem, itemDefaults: ItemDefaults): Promise<void> { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionList> => { |
||||
return { items: [item], itemDefaults, isIncomplete: false } |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('test', 't', null, provider)) |
||||
await nvim.input('i') |
||||
nvim.call('coc#start', [{ source: 'test' }], true) |
||||
await helper.waitPopup() |
||||
} |
||||
|
||||
it('should use commitCharacters from itemDefaults', async () => { |
||||
helper.updateConfiguration('suggest.acceptSuggestionOnCommitCharacter', true) |
||||
await start({ label: 'foo' }, { commitCharacters: ['.'] }) |
||||
await nvim.input('.') |
||||
await helper.waitFor('getline', ['.'], 'foo.') |
||||
}) |
||||
|
||||
it('should use range of editRange from itemDefaults', async () => { |
||||
await nvim.call('setline', ['.', 'bar']) |
||||
await start({ label: 'foo' }, { |
||||
editRange: Range.create(0, 0, 0, 3) |
||||
}) |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'foo') |
||||
}) |
||||
|
||||
it('should use replace range of editRange from itemDefaults', async () => { |
||||
await nvim.call('setline', ['.', 'bar']) |
||||
await start({ label: 'foo' }, { |
||||
editRange: { |
||||
insert: Range.create(0, 0, 0, 0), |
||||
replace: Range.create(0, 0, 0, 3), |
||||
} |
||||
}) |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'foo') |
||||
}) |
||||
|
||||
it('should use insertTextFormat from itemDefaults', async () => { |
||||
await start({ label: 'foo', insertText: 'foo($1)$0' }, { |
||||
insertTextFormat: InsertTextFormat.Snippet, |
||||
insertTextMode: InsertTextMode.asIs, |
||||
data: {} |
||||
}) |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'foo()') |
||||
}) |
||||
}) |
||||
|
||||
describe('textEdit', () => { |
||||
it('should fix bad range', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: 'foo', |
||||
filterText: 'foo', |
||||
textEdit: { range: Range.create(0, 0, 0, 0), newText: 'foo' }, |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('i') |
||||
nvim.call('coc#start', [{ source: 'edits' }], true) |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'foo') |
||||
}) |
||||
|
||||
it('should applyEdits for empty word', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => [{ |
||||
label: '', |
||||
filterText: '!', |
||||
textEdit: { range: Range.create(0, 0, 0, 1), newText: 'foo' }, |
||||
data: { word: '' } |
||||
}] |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider, ['!'])) |
||||
await nvim.input('i!') |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'foo') |
||||
}) |
||||
|
||||
it('should provide word when textEdit after startcol', async () => { |
||||
// some LS would send textEdit after first character,
|
||||
// need fix the word from newText
|
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (_, position): Promise<CompletionItem[]> => { |
||||
if (position.line != 0) return null |
||||
return [{ |
||||
label: 'bar', |
||||
textEdit: { |
||||
range: Range.create(0, 1, 0, 1), |
||||
newText: 'bar' |
||||
} |
||||
}, { |
||||
label: 'bad', |
||||
textEdit: { |
||||
replace: Range.create(0, 1, 0, 1), |
||||
insert: Range.create(0, 1, 0, 1), |
||||
newText: 'bad' |
||||
} |
||||
}] |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('edits', 'edit', null, provider)) |
||||
await nvim.input('ib') |
||||
await helper.waitPopup() |
||||
let items = completion.activeItems |
||||
expect(items[0].word).toBe('bar') |
||||
}) |
||||
|
||||
it('should adjust completion position by textEdit start position', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (_document, _position, _token, context): Promise<CompletionItem[]> => { |
||||
if (!context.triggerCharacter) return |
||||
return [{ |
||||
label: 'foo', |
||||
textEdit: { |
||||
range: Range.create(0, 0, 0, 1), |
||||
newText: '?foo' |
||||
} |
||||
}] |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('fix', 'f', null, provider, ['?'])) |
||||
await nvim.input('i?') |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
let line = await nvim.line |
||||
expect(line).toBe('?foo') |
||||
}) |
||||
|
||||
it('should fix range of removed text range', async () => { |
||||
let provider: CompletionItemProvider = { |
||||
provideCompletionItems: async (): Promise<CompletionItem[]> => { |
||||
return [{ |
||||
label: 'React', |
||||
textEdit: { |
||||
range: Range.create(0, 0, 0, 8), |
||||
newText: 'import React$1 from "react"' |
||||
}, |
||||
insertTextFormat: InsertTextFormat.Snippet |
||||
}] |
||||
} |
||||
} |
||||
disposables.push(languages.registerCompletionItemProvider('fix', 'f', null, provider, ['?'])) |
||||
await nvim.call('setline', ['.', 'import r;']) |
||||
await nvim.call('cursor', [1, 8]) |
||||
await nvim.input('a') |
||||
await nvim.call('coc#start', { source: 'fix' }) |
||||
await helper.waitPopup() |
||||
await helper.confirmCompletion(0) |
||||
await helper.waitFor('getline', ['.'], 'import React from "react";') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Position, Range, TextEdit } from 'vscode-languageserver-types' |
||||
import sources from '../../sources/index' |
||||
import { matchLine } from '../../sources/keywords' |
||||
import workspace from '../../workspace' |
||||
import helper, { createTmpFile } from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('utils', () => { |
||||
it('should matchLine', async () => { |
||||
let doc = await workspace.document |
||||
let text = 'a'.repeat(2048) |
||||
expect(matchLine(text, doc.chars)).toEqual(['a'.repeat(1024)]) |
||||
expect(matchLine('a b c', doc.chars)).toEqual([]) |
||||
expect(matchLine('foo bar', doc.chars)).toEqual(['foo', 'bar']) |
||||
expect(matchLine('?foo bar', doc.chars)).toEqual(['foo', 'bar']) |
||||
expect(matchLine('?foo $', doc.chars)).toEqual(['foo']) |
||||
expect(matchLine('?foo foo foo', doc.chars)).toEqual(['foo']) |
||||
}) |
||||
}) |
||||
|
||||
describe('KeywordsBuffer', () => { |
||||
it('should parse keywords', async () => { |
||||
let filepath = await createTmpFile(' ab') |
||||
let doc = await helper.createDocument(filepath) |
||||
let b = sources.getKeywordsBuffer(doc.bufnr) |
||||
let words = b.getWords() |
||||
expect(words).toEqual(['ab']) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) |
||||
words = b.getWords() |
||||
expect(words).toEqual(['foo', 'bar', 'ab']) |
||||
await doc.applyEdits([TextEdit.replace(Range.create(0, 0, 1, 3), 'def ')]) |
||||
words = b.getWords() |
||||
expect(words).toEqual(['def', 'ab']) |
||||
}) |
||||
|
||||
it('should fuzzy for matchKeywords', async () => { |
||||
let filepath = await createTmpFile(`_foo\nbar\n`) |
||||
let doc = await helper.createDocument(filepath) |
||||
let b = sources.getKeywordsBuffer(doc.bufnr) |
||||
const getResults = (iterable: Iterable<string>) => { |
||||
let res: string[] = [] |
||||
for (let word of iterable) { |
||||
res.push(word) |
||||
} |
||||
return res |
||||
} |
||||
let iterable = b.matchWords(0, 'br', true) |
||||
expect(getResults(iterable)).toEqual(['bar']) |
||||
iterable = b.matchWords(0, 'f', true) |
||||
expect(getResults(iterable)).toEqual(['_foo']) |
||||
iterable = b.matchWords(0, '_', true) |
||||
expect(getResults(iterable)).toEqual(['_foo']) |
||||
}) |
||||
|
||||
it('should match by unicode', async () => { |
||||
let filepath = await createTmpFile(`aéà\nàçé\n`) |
||||
let doc = await helper.createDocument(filepath) |
||||
let b = sources.getKeywordsBuffer(doc.bufnr) |
||||
const getResults = (iterable: Iterable<string>) => { |
||||
let res: string[] = [] |
||||
for (let word of iterable) { |
||||
res.push(word) |
||||
} |
||||
return res |
||||
} |
||||
let iterable = b.matchWords(0, 'ae', true) |
||||
expect(getResults(iterable)).toEqual([ |
||||
'aéà', 'àçé' |
||||
]) |
||||
}) |
||||
}) |
||||
|
||||
describe('native sources', () => { |
||||
it('should works for around source', async () => { |
||||
let doc = await workspace.document |
||||
await nvim.setLine('foo ') |
||||
await doc.synchronize() |
||||
let { mode } = await nvim.mode |
||||
expect(mode).toBe('n') |
||||
await nvim.input('Af') |
||||
await helper.waitPopup() |
||||
let res = await helper.visible('foo', 'around') |
||||
expect(res).toBe(true) |
||||
await nvim.input('<esc>') |
||||
}) |
||||
|
||||
it('should works for buffer source', async () => { |
||||
await helper.createDocument() |
||||
await nvim.command('set hidden') |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('other') |
||||
await nvim.command('bp') |
||||
await doc.synchronize() |
||||
let { mode } = await nvim.mode |
||||
expect(mode).toBe('n') |
||||
await nvim.input('io') |
||||
let res = await helper.visible('other', 'buffer') |
||||
expect(res).toBe(true) |
||||
}) |
||||
|
||||
it('should works with file source', async () => { |
||||
await helper.edit() |
||||
await nvim.input('i/') |
||||
await helper.waitPopup() |
||||
}) |
||||
}) |
@ -0,0 +1,317 @@
@@ -0,0 +1,317 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CompletionItemKind, Disposable, Position, Range } from 'vscode-languageserver-protocol' |
||||
import { caseScore, matchScore, matchScoreWithPositions } from '../../completion/match' |
||||
import { checkIgnoreRegexps, indentChanged, createKindMap, getInput, getKindText, getResumeInput, getValidWord, highlightOffert, shouldIndent, shouldStop } from '../../completion/util' |
||||
import { WordDistance } from '../../completion/wordDistance' |
||||
import languages from '../../languages' |
||||
import { CompleteOption } from '../../types' |
||||
import { disposeAll } from '../../util' |
||||
import { getCharCodes } from '../../util/fuzzy' |
||||
import workspace from '../../workspace' |
||||
import events from '../../events' |
||||
import helper, { createTmpFile } from '../helper' |
||||
let disposables: Disposable[] = [] |
||||
|
||||
let nvim: Neovim |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
disposeAll(disposables) |
||||
}) |
||||
|
||||
describe('caseScore()', () => { |
||||
it('should get caseScore', () => { |
||||
expect(typeof caseScore(10, 10, 2)).toBe('number') |
||||
}) |
||||
}) |
||||
|
||||
describe('indentChanged()', () => { |
||||
it('should check indentChanged', async () => { |
||||
expect(indentChanged(undefined, [1, 1, ''], '')).toBe(false) |
||||
expect(indentChanged({ word: 'foo' }, [1, 4, 'foo'], ' foo')).toBe(true) |
||||
expect(indentChanged({ word: 'foo' }, [1, 4, 'bar'], ' foo')).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('highlightOffert()', () => { |
||||
it('should get highlight offset', () => { |
||||
let n = highlightOffert(3, { abbr: 'abc', word: '', filterText: 'def' }) |
||||
expect(n).toBe(-1) |
||||
expect(highlightOffert(3, { abbr: 'abc', word: '', filterText: 'abc' })).toBe(3) |
||||
expect(highlightOffert(3, { abbr: 'xy abc', word: '', filterText: 'abc' })).toBe(6) |
||||
}) |
||||
}) |
||||
|
||||
describe('getKindText()', () => { |
||||
it('should getKindText', async () => { |
||||
expect(getKindText('t', new Map(), '')).toBe('t') |
||||
let m = new Map() |
||||
m.set(CompletionItemKind.Class, 'C') |
||||
expect(getKindText(CompletionItemKind.Class, m, 'D')).toBe('C') |
||||
expect(getKindText(CompletionItemKind.Class, new Map(), 'D')).toBe('D') |
||||
}) |
||||
}) |
||||
|
||||
describe('createKindMap()', () => { |
||||
it('should createKindMap', async () => { |
||||
let map = createKindMap({ constructor: 'C' }) |
||||
expect(map.get(CompletionItemKind.Constructor)).toBe('C') |
||||
map = createKindMap({ constructor: undefined }) |
||||
expect(map.get(CompletionItemKind.Constructor)).toBe('') |
||||
}) |
||||
}) |
||||
|
||||
describe('getValidWord()', () => { |
||||
it('should getValidWord', async () => { |
||||
expect(getValidWord('label', [])).toBe('label') |
||||
}) |
||||
}) |
||||
|
||||
describe('checkIgnoreRegexps()', () => { |
||||
it('should checkIgnoreRegexps', async () => { |
||||
expect(checkIgnoreRegexps([], '')).toBe(false) |
||||
expect(checkIgnoreRegexps(['^^*^^'], 'input')).toBe(false) |
||||
expect(checkIgnoreRegexps(['^inp', '^ind'], 'input')).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('getResumeInput()', () => { |
||||
it('should getResumeInput', async () => { |
||||
let opt = { line: 'foo', colnr: 4, col: 1 } |
||||
expect(getResumeInput(opt, 'f')).toBeNull() |
||||
expect(getResumeInput(opt, 'bar')).toBeNull() |
||||
expect(getResumeInput(opt, 'foo f')).toBeNull() |
||||
}) |
||||
}) |
||||
|
||||
describe('shouldStop()', () => { |
||||
function createOption(bufnr: number, linenr: number, line: string, colnr: number): Pick<CompleteOption, 'bufnr' | 'linenr' | 'line' | 'colnr'> { |
||||
return { bufnr, linenr, line, colnr } |
||||
} |
||||
|
||||
it('should check stop', async () => { |
||||
let opt = createOption(1, 1, 'a', 2) |
||||
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: '' }, opt)).toBe(true) |
||||
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: ' ' }, opt)).toBe(true) |
||||
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'fo' }, opt)).toBe(true) |
||||
expect(shouldStop(2, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'foob' }, opt)).toBe(true) |
||||
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 2, changedtick: 1, pre: 'foob' }, opt)).toBe(true) |
||||
expect(shouldStop(1, 'foo', { line: '', col: 2, lnum: 1, changedtick: 1, pre: 'barb' }, opt)).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('shouldIndent()', () => { |
||||
it('should check indent', async () => { |
||||
let res = shouldIndent('0{,0},0),0],!^F,o,O,e,=endif,=enddef,=endfu,=endfor', 'endfor') |
||||
expect(res).toBe(true) |
||||
res = shouldIndent('', 'endfor') |
||||
expect(res).toBe(false) |
||||
res = shouldIndent('0{,0},0),0],!^F,o,O,e,=endif,=enddef,=endfu,=endfor', 'foo bar') |
||||
expect(res).toBe(false) |
||||
res = shouldIndent('=~endif,=enddef,=endfu,=endfor', 'Endif') |
||||
expect(res).toBe(true) |
||||
res = shouldIndent(' ', '') |
||||
expect(res).toBe(false) |
||||
res = shouldIndent('*=endif', 'endif') |
||||
expect(res).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('getInput()', () => { |
||||
it('should consider none word character as input', async () => { |
||||
let doc = await helper.createDocument('t.vim') |
||||
let res = getInput(doc, 'a#b#', false) |
||||
expect(res).toBe('a#b#') |
||||
res = getInput(doc, '你b#', true) |
||||
expect(res).toBe('b#') |
||||
}) |
||||
}) |
||||
|
||||
describe('matchScore', () => { |
||||
function score(word: string, input: string): number { |
||||
return matchScore(word, getCharCodes(input)) |
||||
} |
||||
|
||||
it('should match score for last letter', () => { |
||||
expect(score('#!3', '3')).toBe(1) |
||||
expect(score('bar', 'f')).toBe(0) |
||||
}) |
||||
|
||||
it('should return 0 when not matched', async () => { |
||||
expect(score('and', '你')).toBe(0) |
||||
expect(score('你and', '你的')).toBe(0) |
||||
expect(score('fooBar', 'Bt')).toBe(0) |
||||
expect(score('thisbar', 'tihc')).toBe(0) |
||||
}) |
||||
|
||||
it('should match first letter', () => { |
||||
expect(score('abc', '')).toBe(0) |
||||
expect(score('abc', 'a')).toBe(5) |
||||
expect(score('Abc', 'a')).toBe(2.5) |
||||
expect(score('__abc', 'a')).toBe(2) |
||||
expect(score('$Abc', 'a')).toBe(1) |
||||
expect(score('$Abc', 'A')).toBe(2) |
||||
expect(score('$Abc', '$A')).toBe(6) |
||||
expect(score('$Abc', '$a')).toBe(5.5) |
||||
expect(score('foo_bar', 'b')).toBe(2) |
||||
expect(score('foo_Bar', 'b')).toBe(1) |
||||
expect(score('_foo_Bar', 'b')).toBe(0.5) |
||||
expect(score('_foo_Bar', 'f')).toBe(2) |
||||
expect(score('bar', 'a')).toBe(1) |
||||
expect(score('fooBar', 'B')).toBe(2) |
||||
expect(score('fooBar', 'b')).toBe(1) |
||||
expect(score('fobtoBar', 'bt')).toBe(2) |
||||
}) |
||||
|
||||
it('should match follow letters', () => { |
||||
expect(score('abc', 'ab')).toBe(6) |
||||
expect(score('adB', 'ab')).toBe(5.75) |
||||
expect(score('adb', 'ab')).toBe(5.1) |
||||
expect(score('adCB', 'ab')).toBe(5.05) |
||||
expect(score('a_b_c', 'ab')).toBe(6) |
||||
expect(score('FooBar', 'fb')).toBe(3.25) |
||||
expect(score('FBar', 'fb')).toBe(3) |
||||
expect(score('FooBar', 'FB')).toBe(6) |
||||
expect(score('FBar', 'FB')).toBe(6) |
||||
expect(score('a__b', 'a__b')).toBe(8) |
||||
expect(score('aBc', 'ab')).toBe(5.5) |
||||
expect(score('a_B_c', 'ab')).toBe(5.75) |
||||
expect(score('abc', 'abc')).toBe(7) |
||||
expect(score('abc', 'aC')).toBe(0) |
||||
expect(score('abc', 'ac')).toBe(5.1) |
||||
expect(score('abC', 'ac')).toBe(5.75) |
||||
expect(score('abC', 'aC')).toBe(6) |
||||
}) |
||||
|
||||
it('should only allow search once', () => { |
||||
expect(score('foobar', 'fbr')).toBe(5.2) |
||||
expect(score('foobaRow', 'fbr')).toBe(5.85) |
||||
expect(score('foobaRow', 'fbR')).toBe(6.1) |
||||
expect(score('foobar', 'fa')).toBe(5.1) |
||||
}) |
||||
|
||||
it('should have higher score for strict match', () => { |
||||
expect(score('language-client-protocol', 'lct')).toBe(6.1) |
||||
expect(score('language-client-types', 'lct')).toBe(7) |
||||
}) |
||||
|
||||
it('should find highest score', () => { |
||||
expect(score('ArrayRotateTail', 'art')).toBe(3.6) |
||||
}) |
||||
}) |
||||
|
||||
describe('matchScoreWithPositions', () => { |
||||
function assertMatch(word: string, input: string, res: [number, ReadonlyArray<number>] | undefined): void { |
||||
let result = matchScoreWithPositions(word, getCharCodes(input)) |
||||
if (!res) { |
||||
expect(result).toBeUndefined() |
||||
} else { |
||||
expect(result).toEqual(res) |
||||
} |
||||
} |
||||
|
||||
it('should return undefined when not match found', async () => { |
||||
assertMatch('a', 'abc', undefined) |
||||
assertMatch('a', '', undefined) |
||||
assertMatch('ab', 'ac', undefined) |
||||
}) |
||||
|
||||
it('should find matches by position fix', async () => { |
||||
assertMatch('this', 'tih', [5.6, [0, 1, 2]]) |
||||
assertMatch('globalThis', 'tihs', [2.6, [6, 7, 8, 9]]) |
||||
}) |
||||
|
||||
it('should find matched positions', async () => { |
||||
assertMatch('this', 'th', [6, [0, 1]]) |
||||
assertMatch('foo_bar', 'fb', [6, [0, 4]]) |
||||
assertMatch('assertMatch', 'am', [5.75, [0, 6]]) |
||||
}) |
||||
}) |
||||
|
||||
describe('wordDistance', () => { |
||||
it('should empty when not enabled', async () => { |
||||
let w = await WordDistance.create(false, {} as any) |
||||
expect(w.distance(Position.create(0, 0), {} as any)).toBe(0) |
||||
}) |
||||
|
||||
it('should empty when selectRanges is empty', async () => { |
||||
let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption |
||||
let w = await WordDistance.create(true, opt) |
||||
expect(w).toBe(WordDistance.None) |
||||
}) |
||||
|
||||
it('should empty when timeout', async () => { |
||||
disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { |
||||
provideSelectionRanges: _doc => { |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 1) |
||||
}] |
||||
} |
||||
})) |
||||
let spy = jest.spyOn(workspace, 'computeWordRanges').mockImplementation(() => { |
||||
return new Promise(resolve => { |
||||
setTimeout(() => { |
||||
resolve(null) |
||||
}, 50) |
||||
}) |
||||
}) |
||||
let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption |
||||
let w = await WordDistance.create(true, opt) |
||||
spy.mockRestore() |
||||
expect(w).toBe(WordDistance.None) |
||||
}) |
||||
|
||||
it('should get distance', async () => { |
||||
disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { |
||||
provideSelectionRanges: _doc => { |
||||
return [{ |
||||
range: Range.create(0, 0, 1, 0), |
||||
parent: { |
||||
range: Range.create(0, 0, 3, 0) |
||||
} |
||||
}] |
||||
} |
||||
})) |
||||
let filepath = await createTmpFile('foo bar\ndef', disposables) |
||||
await helper.edit(filepath) |
||||
let opt = await nvim.call('coc#util#get_complete_option') as CompleteOption |
||||
let w = await WordDistance.create(true, opt) |
||||
expect(w.distance(Position.create(1, 0), {} as any)).toBeGreaterThan(0) |
||||
expect(w.distance(Position.create(0, 0), { word: '', kind: CompletionItemKind.Keyword })).toBeGreaterThan(0) |
||||
expect(w.distance(Position.create(0, 0), { word: 'not_exists' })).toBeGreaterThan(0) |
||||
expect(w.distance(Position.create(0, 0), { word: 'bar' })).toBe(0) |
||||
expect(w.distance(Position.create(0, 0), { word: 'def' })).toBeGreaterThan(0) |
||||
await nvim.call('cursor', [1, 2]) |
||||
await events.fire('CursorMoved', [opt.bufnr, [1, 2]]) |
||||
expect(w.distance(Position.create(0, 0), { word: 'bar' })).toBe(0) |
||||
}) |
||||
|
||||
it('should get same range', async () => { |
||||
disposables.push(languages.registerSelectionRangeProvider([{ language: '*' }], { |
||||
provideSelectionRanges: _doc => { |
||||
return [{ |
||||
range: Range.create(0, 0, 1, 0), |
||||
parent: { |
||||
range: Range.create(0, 0, 3, 0) |
||||
} |
||||
}] |
||||
} |
||||
})) |
||||
let spy = jest.spyOn(workspace, 'computeWordRanges').mockImplementation(() => { |
||||
return Promise.resolve({ foo: [Range.create(0, 0, 0, 0)] }) |
||||
}) |
||||
let opt = await nvim.call('coc#util#get_complete_option') as any |
||||
opt.word = '' |
||||
let w = await WordDistance.create(true, opt) |
||||
spy.mockRestore() |
||||
let res = w.distance(Position.create(0, 0), { word: 'foo' }) |
||||
expect(res).toBe(0) |
||||
}) |
||||
}) |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,424 @@
@@ -0,0 +1,424 @@
|
||||
import fs from 'fs' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { v1 as uuid } from 'uuid' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
import Configurations from '../../configuration' |
||||
import { ConfigurationModel } from '../../configuration/model' |
||||
import ConfigurationProxy from '../../configuration/shape' |
||||
import { ConfigurationTarget, ConfigurationUpdateTarget } from '../../types' |
||||
import { CONFIG_FILE_NAME, disposeAll, wait } from '../../util' |
||||
import { remove } from '../../util/fs' |
||||
import helper from '../helper' |
||||
|
||||
const workspaceConfigFile = path.resolve(__dirname, `../sample/.vim/${CONFIG_FILE_NAME}`) |
||||
|
||||
function U(fsPath: string): string { |
||||
return URI.file(fsPath).toString() |
||||
} |
||||
|
||||
function createConfigurations(): Configurations { |
||||
let userConfigFile = path.join(__dirname, './settings.json') |
||||
return new Configurations(userConfigFile) |
||||
} |
||||
|
||||
const disposables: Disposable[] = [] |
||||
|
||||
afterEach(() => { |
||||
disposeAll(disposables) |
||||
}) |
||||
|
||||
function generateTmpDir(): string { |
||||
return path.join(os.tmpdir(), uuid()) |
||||
} |
||||
|
||||
describe('Configurations', () => { |
||||
describe('ConfigurationProxy', () => { |
||||
it('should create file and parent folder when necessary', async () => { |
||||
let folder = generateTmpDir() |
||||
let uri = URI.file(path.join(folder, 'a/b/settings.json')) |
||||
let proxy = new ConfigurationProxy({}, false) |
||||
await proxy.modifyConfiguration(uri.fsPath, 'foo', true) |
||||
let content = fs.readFileSync(uri.fsPath, 'utf8') |
||||
expect(JSON.parse(content)).toEqual({ foo: true }) |
||||
await proxy.modifyConfiguration(uri.fsPath, 'foo', false) |
||||
content = fs.readFileSync(uri.fsPath, 'utf8') |
||||
expect(JSON.parse(content)).toEqual({ foo: false }) |
||||
await remove(folder) |
||||
}) |
||||
|
||||
it('should get folder from resolver', async () => { |
||||
let proxy = new ConfigurationProxy({ |
||||
getWorkspaceFolder: (uri: string) => { |
||||
let fsPath = URI.parse(uri).fsPath |
||||
if (fsPath.startsWith(os.tmpdir())) { |
||||
return { uri: URI.file(os.tmpdir()).toString(), name: 'tmp' } |
||||
} |
||||
if (fsPath.startsWith(os.homedir())) { |
||||
return { uri: URI.file(os.homedir()).toString(), name: 'home' } |
||||
} |
||||
return undefined |
||||
}, |
||||
root: __dirname |
||||
}) |
||||
let uri = proxy.getWorkspaceFolder(URI.file(path.join(os.tmpdir(), 'foo')).toString()) |
||||
expect(uri.fsPath.startsWith(os.tmpdir())).toBe(true) |
||||
uri = proxy.getWorkspaceFolder(URI.file('abc').toString()) |
||||
expect(uri).toBeUndefined() |
||||
proxy = new ConfigurationProxy({}) |
||||
uri = proxy.getWorkspaceFolder(URI.file(path.join(os.tmpdir(), 'foo')).toString()) |
||||
expect(uri).toBeUndefined() |
||||
}) |
||||
}) |
||||
|
||||
describe('watchFile', () => { |
||||
it('should watch user config file', async () => { |
||||
let userConfigFile = path.join(os.tmpdir(), `settings-${uuid()}.json`) |
||||
fs.writeFileSync(userConfigFile, '{"foo.bar": true}', { encoding: 'utf8' }) |
||||
let conf = new Configurations(userConfigFile, undefined, false) |
||||
disposables.push(conf) |
||||
await wait(50) |
||||
fs.writeFileSync(userConfigFile, '{"foo.bar": false}', { encoding: 'utf8' }) |
||||
await helper.waitValue(() => { |
||||
let c = conf.getConfiguration('foo') |
||||
return c.get('bar') |
||||
}, false) |
||||
if (fs.existsSync(userConfigFile)) fs.unlinkSync(userConfigFile) |
||||
}) |
||||
|
||||
it('should watch folder config file', async () => { |
||||
let dir = generateTmpDir() |
||||
let configFile = path.join(dir, '.vim/coc-settings.json') |
||||
fs.mkdirSync(path.dirname(configFile), { recursive: true }) |
||||
fs.writeFileSync(configFile, '{"foo.bar": true}', { encoding: 'utf8' }) |
||||
let conf = new Configurations('', { |
||||
get root() { |
||||
return dir |
||||
}, |
||||
modifyConfiguration: async () => {}, |
||||
getWorkspaceFolder: () => { |
||||
return URI.file(dir) |
||||
} |
||||
}, false) |
||||
disposables.push(conf) |
||||
let uri = U(dir) |
||||
let resolved = conf.locateFolderConfigution(uri) |
||||
expect(resolved).toBeDefined() |
||||
await wait(20) |
||||
fs.writeFileSync(configFile, '{"foo.bar": false}', { encoding: 'utf8' }) |
||||
await helper.waitValue(() => { |
||||
let c = conf.getConfiguration('foo') |
||||
return c.get('bar') |
||||
}, false) |
||||
}) |
||||
}) |
||||
|
||||
describe('loadDefaultConfigurations', () => { |
||||
it('should not throw', async () => { |
||||
let fn = fs.readFileSync |
||||
let spy = jest.spyOn(fs, 'readFileSync').mockImplementation((path, opt) => { |
||||
if (typeof path === 'string' && path.endsWith('/data/schema.json')) { |
||||
return '{"properties":{"x":{"default":1},"x.y":{"default":{}}}}' |
||||
} |
||||
return fn(path, opt) |
||||
}) |
||||
let called = false |
||||
let s = jest.spyOn(console, 'error').mockImplementation(() => { |
||||
called = true |
||||
}) |
||||
new Configurations(undefined, undefined, true, os.homedir()) |
||||
s.mockRestore() |
||||
spy.mockRestore() |
||||
expect(called).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('addFolderFile()', () => { |
||||
it('should not add invalid folder from cwd', async () => { |
||||
let userConfigFile = path.join(__dirname, '.vim/coc-settings.json') |
||||
let conf = new Configurations(userConfigFile, undefined, true, os.homedir()) |
||||
let res = conf.folderToConfigfile(os.homedir()) |
||||
expect(res).toBeUndefined() |
||||
res = conf.folderToConfigfile(__dirname) |
||||
expect(res).toBeUndefined() |
||||
}) |
||||
|
||||
it('should add folder as workspace configuration', () => { |
||||
let configurations = createConfigurations() |
||||
disposables.push(configurations) |
||||
let fired = false |
||||
configurations.onDidChange(() => { |
||||
fired = true |
||||
}) |
||||
configurations.addFolderFile(workspaceConfigFile) |
||||
let resource = URI.file(path.resolve(workspaceConfigFile, '../../tmp')) |
||||
let c = configurations.getConfiguration('coc.preferences', resource) |
||||
let res = c.inspect('rootPath') |
||||
expect(res.key).toBe('coc.preferences.rootPath') |
||||
expect(res.workspaceFolderValue).toBe('./src') |
||||
expect(c.get('rootPath')).toBe('./src') |
||||
}) |
||||
|
||||
it('should not add invalid folders', async () => { |
||||
let configurations = createConfigurations() |
||||
expect(configurations.addFolderFile('ab')).toBe(false) |
||||
}) |
||||
|
||||
it('should resolve folder configuration when possible', async () => { |
||||
let configurations = createConfigurations() |
||||
expect(configurations.locateFolderConfigution('test:///foo')).toBe(false) |
||||
let fsPath = path.join(__dirname, `../sample/abc`) |
||||
expect(configurations.locateFolderConfigution(URI.file(fsPath).toString())).toBe(true) |
||||
fsPath = path.join(__dirname, `../sample/foo`) |
||||
expect(configurations.locateFolderConfigution(URI.file(fsPath).toString())).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('getConfiguration()', () => { |
||||
it('should load default configurations', () => { |
||||
let conf = new Configurations(undefined, { |
||||
modifyConfiguration: async () => {} |
||||
}) |
||||
disposables.push(conf) |
||||
expect(conf.configuration.defaults.contents.coc).toBeDefined() |
||||
let c = conf.getConfiguration('languageserver') |
||||
expect(c).toEqual({}) |
||||
expect(c.has('not_exists')).toBe(false) |
||||
}) |
||||
|
||||
it('should load configuration without folder configuration', async () => { |
||||
let conf = new Configurations(undefined, { |
||||
root: path.join(path.dirname(__dirname), 'sample'), |
||||
modifyConfiguration: async () => {} |
||||
}) |
||||
disposables.push(conf) |
||||
conf.addFolderFile(workspaceConfigFile) |
||||
let c = conf.getConfiguration('coc.preferences') |
||||
expect(c.rootPath).toBeDefined() |
||||
c = conf.getConfiguration('coc.preferences', null) |
||||
expect(c.rootPath).toBeUndefined() |
||||
}) |
||||
|
||||
it('should inspect configuration', async () => { |
||||
let conf = new Configurations() |
||||
let c = conf.getConfiguration('suggest') |
||||
let res = c.inspect('not_exists') |
||||
expect(res.defaultValue).toBeUndefined() |
||||
expect(res.globalValue).toBeUndefined() |
||||
expect(res.workspaceValue).toBeUndefined() |
||||
c = conf.getConfiguration() |
||||
res = c.inspect('not_exists') |
||||
expect(res.key).toBe('not_exists') |
||||
}) |
||||
|
||||
it('should update memory config #1', () => { |
||||
let conf = new Configurations() |
||||
let fn = jest.fn() |
||||
conf.onDidChange(e => { |
||||
expect(e.affectsConfiguration('x')).toBe(true) |
||||
fn() |
||||
}) |
||||
conf.updateMemoryConfig({ x: 1 }) |
||||
let config = conf.configuration.memory |
||||
expect(config.contents).toEqual({ x: 1 }) |
||||
expect(fn).toBeCalled() |
||||
expect(conf.configuration.workspace).toBeDefined() |
||||
}) |
||||
|
||||
it('should update memory config #2', () => { |
||||
let conf = new Configurations() |
||||
conf.updateMemoryConfig({ x: 1 }) |
||||
conf.updateMemoryConfig({ x: undefined }) |
||||
let config = conf.configuration.user |
||||
expect(config.contents).toEqual({}) |
||||
}) |
||||
|
||||
it('should update memory config #3', () => { |
||||
let conf = new Configurations() |
||||
conf.updateMemoryConfig({ 'suggest.floatConfig': { border: true } }) |
||||
conf.updateMemoryConfig({ 'x.y': { foo: 1 } }) |
||||
let val = conf.getConfiguration() |
||||
let res = val.get('suggest') as any |
||||
expect(res.floatConfig).toEqual({ border: true }) |
||||
res = val.get('x.y') as any |
||||
expect(res).toEqual({ foo: 1 }) |
||||
}) |
||||
|
||||
it('should handle errors', () => { |
||||
let tmpFile = path.join(os.tmpdir(), uuid()) |
||||
fs.writeFileSync(tmpFile, '{"x":', 'utf8') |
||||
let conf = new Configurations(tmpFile) |
||||
disposables.push(conf) |
||||
let errors = conf.errorItems |
||||
expect(errors.length > 1).toBe(true) |
||||
}) |
||||
|
||||
it('should get nested property', () => { |
||||
let config = createConfigurations() |
||||
disposables.push(config) |
||||
let conf = config.getConfiguration('servers.c') |
||||
let res = conf.get<string>('trace.server', '') |
||||
expect(res).toBe('verbose') |
||||
}) |
||||
|
||||
it('should get user and workspace configuration', () => { |
||||
let userConfigFile = path.join(__dirname, './settings.json') |
||||
let configurations = new Configurations(userConfigFile) |
||||
disposables.push(configurations) |
||||
let data = configurations.configuration.toData() |
||||
expect(data.user).toBeDefined() |
||||
expect(data.workspace).toBeDefined() |
||||
expect(data.defaults).toBeDefined() |
||||
let value = configurations.configuration.getValue(undefined, {}) |
||||
expect(value.foo).toBeDefined() |
||||
expect(value.foo.bar).toBe(1) |
||||
}) |
||||
|
||||
it('should extends defaults', () => { |
||||
let configurations = createConfigurations() |
||||
disposables.push(configurations) |
||||
configurations.extendsDefaults({ 'a.b': 1 }) |
||||
configurations.extendsDefaults({ 'a.b': 2 }) |
||||
let o = configurations.configuration.defaults.contents |
||||
expect(o.a.b).toBe(2) |
||||
configurations.configuration.defaults.freeze() |
||||
configurations.extendsDefaults({ 'a.b': 3 }) |
||||
o = configurations.configuration.defaults.contents |
||||
expect(o.a.b).toBe(3) |
||||
}) |
||||
|
||||
it('should not extends builtin keys', async () => { |
||||
let configurations = new Configurations(undefined, { |
||||
modifyConfiguration: async () => {} |
||||
}) |
||||
disposables.push(configurations) |
||||
configurations.extendsDefaults({ 'npm.binPath': 'cnpm' }, 'test') |
||||
let o = configurations.configuration.defaults.contents |
||||
expect(o.npm.binPath).toBe('npm') |
||||
}) |
||||
|
||||
it('should update configuration', async () => { |
||||
let configurations = createConfigurations() |
||||
disposables.push(configurations) |
||||
configurations.addFolderFile(workspaceConfigFile) |
||||
let resource = URI.file(path.resolve(workspaceConfigFile, '../..')) |
||||
let fn = jest.fn() |
||||
configurations.onDidChange(e => { |
||||
expect(e.affectsConfiguration('foo')).toBe(true) |
||||
expect(e.affectsConfiguration('foo.bar')).toBe(true) |
||||
expect(e.affectsConfiguration('foo.bar', 'file://tmp/foo.js')).toBe(false) |
||||
fn() |
||||
}) |
||||
let config = configurations.getConfiguration('foo', resource) |
||||
let o = config.get<number>('bar') |
||||
expect(o).toBe(1) |
||||
await config.update('bar', 6) |
||||
config = configurations.getConfiguration('foo', resource) |
||||
expect(config.get<number>('bar')).toBe(6) |
||||
expect(fn).toBeCalledTimes(1) |
||||
}) |
||||
|
||||
it('should remove configuration', async () => { |
||||
let configurations = createConfigurations() |
||||
disposables.push(configurations) |
||||
configurations.addFolderFile(workspaceConfigFile) |
||||
let resource = URI.file(path.resolve(workspaceConfigFile, '../..')) |
||||
let fn = jest.fn() |
||||
configurations.onDidChange(e => { |
||||
expect(e.affectsConfiguration('foo')).toBe(true) |
||||
expect(e.affectsConfiguration('foo.bar')).toBe(true) |
||||
fn() |
||||
}) |
||||
let config = configurations.getConfiguration('foo', resource) |
||||
let o = config.get<number>('bar') |
||||
expect(o).toBe(1) |
||||
await config.update('bar', null, true) |
||||
config = configurations.getConfiguration('foo', resource) |
||||
expect(config.get<any>('bar')).toBeUndefined() |
||||
expect(fn).toBeCalledTimes(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('changeConfiguration', () => { |
||||
it('should change workspace configuration', async () => { |
||||
let con = createConfigurations() |
||||
let m = new ConfigurationModel({ x: { a: 1 } }, ['x.a']) |
||||
con.changeConfiguration(ConfigurationTarget.Workspace, m, undefined) |
||||
let res = con.getConfiguration('x') |
||||
expect(res.a).toBe(1) |
||||
}) |
||||
|
||||
it('should change default configuration', async () => { |
||||
let m = new ConfigurationModel({ x: { a: 1 } }, ['x.a']) |
||||
let con = createConfigurations() |
||||
con.changeConfiguration(ConfigurationTarget.Default, m, undefined) |
||||
let res = con.getConfiguration('x') |
||||
expect(res.a).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('update()', () => { |
||||
it('should update workspace configuration', async () => { |
||||
let target = ConfigurationUpdateTarget.Workspace |
||||
let con = createConfigurations() |
||||
let res = con.getConfiguration() |
||||
await res.update('x', 3, target) |
||||
let val = con.getConfiguration().get('x') |
||||
expect(val).toBe(3) |
||||
}) |
||||
|
||||
it('should show error when workspace folder not resolved', async () => { |
||||
let called = false |
||||
let s = jest.spyOn(console, 'error').mockImplementation(() => { |
||||
called = true |
||||
}) |
||||
let con = new Configurations(undefined, { |
||||
modifyConfiguration: async () => {}, |
||||
getWorkspaceFolder: () => { |
||||
return undefined |
||||
} |
||||
}) |
||||
let conf = con.getConfiguration(undefined, 'file:///1') |
||||
await conf.update('x', 3, ConfigurationUpdateTarget.WorkspaceFolder) |
||||
s.mockRestore() |
||||
expect(called).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('getWorkspaceConfigUri()', () => { |
||||
it('should not get config uri for undefined resource', async () => { |
||||
let conf = createConfigurations() |
||||
let res = conf.resolveWorkspaceFolderForResource() |
||||
expect(res).toBeUndefined() |
||||
}) |
||||
|
||||
it('should not get config folder same as home', async () => { |
||||
let conf = new Configurations(undefined, { |
||||
modifyConfiguration: async () => {}, |
||||
getWorkspaceFolder: () => { |
||||
return URI.file(os.homedir()) |
||||
} |
||||
}) |
||||
let uri = U(__filename) |
||||
let res = conf.resolveWorkspaceFolderForResource(uri) |
||||
expect(res).toBeUndefined() |
||||
}) |
||||
|
||||
it('should create config file for workspace folder', async () => { |
||||
let folder = path.join(os.tmpdir(), `test-workspace-folder-${uuid()}`) |
||||
let conf = new Configurations(undefined, { |
||||
modifyConfiguration: async () => {}, |
||||
getWorkspaceFolder: () => { |
||||
return URI.file(folder) |
||||
} |
||||
}) |
||||
let res = conf.resolveWorkspaceFolderForResource('file:///1') |
||||
expect(res).toBe(folder) |
||||
let configFile = path.join(folder, '.vim/coc-settings.json') |
||||
expect(fs.existsSync(configFile)).toBe(true) |
||||
res = conf.resolveWorkspaceFolderForResource('file:///1') |
||||
expect(res).toBe(folder) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
{ |
||||
"foo.bar": 1, |
||||
"bar.foo": 2, |
||||
"schema": { |
||||
"https://example.com": "*.yaml" |
||||
}, |
||||
"servers": { |
||||
"c": { |
||||
"trace.server": "verbose" |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,218 @@
@@ -0,0 +1,218 @@
|
||||
import * as assert from 'assert' |
||||
import { ParseError } from 'jsonc-parser' |
||||
import { getConfigurationValue, overrideIdentifiersFromKey, scopeToOverrides, mergeConfigProperties, convertTarget, removeFromValueTree, mergeChanges, toJSONObject, addToValueTree } from '../../configuration/util' |
||||
import { ConfigurationTarget, ConfigurationUpdateTarget } from '../../types' |
||||
|
||||
describe('Configuration utils', () => { |
||||
it('should convertTarget', async () => { |
||||
expect(convertTarget(ConfigurationUpdateTarget.Global)).toBe(ConfigurationTarget.User) |
||||
expect(convertTarget(ConfigurationUpdateTarget.Workspace)).toBe(ConfigurationTarget.Workspace) |
||||
expect(convertTarget(ConfigurationUpdateTarget.WorkspaceFolder)).toBe(ConfigurationTarget.WorkspaceFolder) |
||||
}) |
||||
|
||||
it('should scopeToOverrides', async () => { |
||||
expect(scopeToOverrides(null)).toBeUndefined() |
||||
}) |
||||
|
||||
it('should get overrideIdentifiersFromKey', async () => { |
||||
let res = overrideIdentifiersFromKey('[ ]') |
||||
expect(res).toEqual([]) |
||||
}) |
||||
|
||||
it('should merge preperties', async () => { |
||||
let res = mergeConfigProperties({ |
||||
foo: 'bar', |
||||
"x.y.a": "x", |
||||
"x.y.b": "y", |
||||
"x.t": "z" |
||||
}) |
||||
expect(res).toEqual({ |
||||
foo: 'bar', x: { y: { a: 'x', b: 'y' }, t: 'z' } |
||||
}) |
||||
}) |
||||
|
||||
it('should addToValueTree conflict #1', async () => { |
||||
let fn = jest.fn() |
||||
let obj = { x: 66 } |
||||
addToValueTree(obj, 'x.y', '3', () => { |
||||
fn() |
||||
}) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should addToValueTree conflict #2', async () => { |
||||
let fn = jest.fn() |
||||
addToValueTree(undefined, 'x', '3', () => { |
||||
fn() |
||||
}) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should addToValueTree conflict #3', async () => { |
||||
let obj = { x: true } |
||||
let fn = jest.fn() |
||||
addToValueTree(obj, 'x.y', ['foo'], () => { |
||||
fn() |
||||
}) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a non existing key', () => { |
||||
let target = { a: { b: 2 } } |
||||
removeFromValueTree(target, 'c') |
||||
assert.deepStrictEqual(target, { a: { b: 2 } }) |
||||
removeFromValueTree(target, 'c.d.e') |
||||
assert.deepStrictEqual(target, { a: { b: 2 } }) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key from an object that has only sub sections of the key', () => { |
||||
let target = { a: { b: 2 } } |
||||
|
||||
removeFromValueTree(target, 'a.b.c') |
||||
|
||||
assert.deepStrictEqual(target, { a: { b: 2 } }) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a single segmented key', () => { |
||||
let target = { a: 1 } |
||||
|
||||
removeFromValueTree(target, 'a') |
||||
|
||||
assert.deepStrictEqual(target, {}) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a single segmented key when its value is undefined', () => { |
||||
let target = { a: undefined } |
||||
|
||||
removeFromValueTree(target, 'a') |
||||
|
||||
assert.deepStrictEqual(target, {}) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key when its value is undefined', () => { |
||||
let target = { a: { b: 1 } } |
||||
|
||||
removeFromValueTree(target, 'a.b') |
||||
|
||||
assert.deepStrictEqual(target, {}) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key when its value is array', () => { |
||||
let target = { a: { b: [1] } } |
||||
|
||||
removeFromValueTree(target, 'a.b') |
||||
|
||||
assert.deepStrictEqual(target, {}) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key first segment value is array', () => { |
||||
let target = { a: [1] } |
||||
|
||||
removeFromValueTree(target, 'a.0') |
||||
|
||||
assert.deepStrictEqual(target, { a: [1] }) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove when key is the first segment', () => { |
||||
let target = { a: { b: 1 } } |
||||
|
||||
removeFromValueTree(target, 'a') |
||||
|
||||
assert.deepStrictEqual(target, {}) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key when the first node has more values', () => { |
||||
let target = { a: { b: { c: 1 }, d: 1 } } |
||||
|
||||
removeFromValueTree(target, 'a.b.c') |
||||
|
||||
assert.deepStrictEqual(target, { a: { d: 1 } }) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key when in between node has more values', () => { |
||||
let target = { a: { b: { c: { d: 1 }, d: 1 } } } |
||||
|
||||
removeFromValueTree(target, 'a.b.c.d') |
||||
|
||||
assert.deepStrictEqual(target, { a: { b: { d: 1 } } }) |
||||
}) |
||||
|
||||
it('removeFromValueTree: remove a multi segmented key when the last but one node has more values', () => { |
||||
let target = { a: { b: { c: 1, d: 1 } } } |
||||
|
||||
removeFromValueTree(target, 'a.b.c') |
||||
|
||||
assert.deepStrictEqual(target, { a: { b: { d: 1 } } }) |
||||
}) |
||||
|
||||
it('should convert errors', () => { |
||||
let errors: ParseError[] = [] |
||||
for (let i = 0; i < 17; i++) { |
||||
errors.push({ |
||||
error: i, |
||||
offset: 0, |
||||
length: 10 |
||||
}) |
||||
} |
||||
// let res = convertErrors('file:///1', 'abc', errors)
|
||||
// expect(res.length).toBe(17)
|
||||
}) |
||||
|
||||
it('should get configuration value', () => { |
||||
let root = { |
||||
foo: { |
||||
bar: 1, |
||||
from: { |
||||
to: 2 |
||||
} |
||||
}, |
||||
bar: [1, 2] |
||||
} |
||||
let res = getConfigurationValue(root, 'foo.from.to', 1) |
||||
expect(res).toBe(2) |
||||
res = getConfigurationValue(root, 'foo.from', 1) |
||||
expect(res).toEqual({ to: 2 }) |
||||
}) |
||||
|
||||
it('should get json object', async () => { |
||||
let obj = [{ x: 1 }, { y: 2 }] |
||||
expect(toJSONObject(obj)).toEqual(obj) |
||||
}) |
||||
}) |
||||
|
||||
describe('mergeChanges', () => { |
||||
test('merge only keys', () => { |
||||
const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] }) |
||||
assert.deepStrictEqual(actual, { keys: ['a', 'b', 'c', 'd'], overrides: [] }) |
||||
}) |
||||
|
||||
test('merge only keys with duplicates', () => { |
||||
const actual = mergeChanges({ keys: ['a', 'b'], overrides: [] }, { keys: ['c', 'd'], overrides: [] }, { keys: ['a', 'd', 'e'], overrides: [] }) |
||||
assert.deepStrictEqual(actual, { keys: ['a', 'b', 'c', 'd', 'e'], overrides: [] }) |
||||
}) |
||||
|
||||
test('merge only overrides', () => { |
||||
const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']]] }, { keys: [], overrides: [['b', ['3', '4']]] }) |
||||
assert.deepStrictEqual(actual, { keys: [], overrides: [['a', ['1', '2']], ['b', ['3', '4']]] }) |
||||
}) |
||||
|
||||
test('merge only overrides with duplicates', () => { |
||||
const actual = mergeChanges({ keys: [], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: [], overrides: [['b', ['3', '4']]] }, { keys: [], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] }) |
||||
assert.deepStrictEqual(actual, { keys: [], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] }) |
||||
}) |
||||
|
||||
test('merge', () => { |
||||
const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }, { keys: ['b'], overrides: [['b', ['3', '4']]] }, { keys: ['c', 'a'], overrides: [['c', ['1', '4']], ['a', ['2', '3']]] }) |
||||
assert.deepStrictEqual(actual, { keys: ['b', 'c', 'a'], overrides: [['a', ['1', '2', '3']], ['b', ['5', '4', '3']], ['c', ['1', '4']]] }) |
||||
}) |
||||
|
||||
test('merge single change', () => { |
||||
const actual = mergeChanges({ keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }) |
||||
assert.deepStrictEqual(actual, { keys: ['b', 'b'], overrides: [['a', ['1', '2']], ['b', ['5', '4']]] }) |
||||
}) |
||||
|
||||
test('merge no changes', () => { |
||||
const actual = mergeChanges() |
||||
assert.deepStrictEqual(actual, { keys: [], overrides: [] }) |
||||
}) |
||||
}) |
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('setupDynamicAutocmd()', () => { |
||||
it('should setup autocmd on vim', async () => { |
||||
await nvim.setLine('foo') |
||||
let fn = nvim.hasFunction |
||||
nvim.hasFunction = () => { |
||||
return false |
||||
} |
||||
let called = false |
||||
workspace.registerAutocmd({ |
||||
event: 'CursorMoved', |
||||
request: true, |
||||
callback: () => { |
||||
called = true |
||||
} |
||||
}) |
||||
await helper.wait(50) |
||||
await nvim.command('normal! $') |
||||
await helper.wait(100) |
||||
nvim.hasFunction = fn |
||||
expect(called).toBe(true) |
||||
nvim.command(`augroup coc_dynamic_autocmd| autocmd!|augroup end`, true) |
||||
}) |
||||
|
||||
it('should setup user autocmd', async () => { |
||||
let called = false |
||||
workspace.registerAutocmd({ |
||||
event: 'User CocJumpPlaceholder', |
||||
request: true, |
||||
callback: () => { |
||||
called = true |
||||
} |
||||
}) |
||||
workspace.autocmds.setupDynamicAutocmd(true) |
||||
await helper.wait(50) |
||||
await nvim.command('doautocmd <nomodeline> User CocJumpPlaceholder') |
||||
await helper.wait(100) |
||||
expect(called).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('doAutocmd()', () => { |
||||
it('should not throw when command id does not exist', async () => { |
||||
await workspace.autocmds.doAutocmd(999, []) |
||||
}) |
||||
|
||||
it('should dispose', async () => { |
||||
workspace.autocmds.dispose() |
||||
}) |
||||
}) |
@ -0,0 +1,143 @@
@@ -0,0 +1,143 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import fs from 'fs' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { v4 as uuid } from 'uuid' |
||||
import { LocationLink, Position, Range, TextEdit } from 'vscode-languageserver-types' |
||||
import { URI } from 'vscode-uri' |
||||
import Documents from '../../core/documents' |
||||
import events from '../../events' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let documents: Documents |
||||
let nvim: Neovim |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
documents = workspace.documentsManager |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('documents', () => { |
||||
it('should get document', async () => { |
||||
await helper.createDocument('bar') |
||||
let doc = await helper.createDocument('foo') |
||||
let res = documents.getDocument(doc.uri) |
||||
expect(res.uri).toBe(doc.uri) |
||||
}) |
||||
|
||||
it('should consider lisp option for iskeyword', async () => { |
||||
await nvim.command(`e +setl\\ lisp t`) |
||||
let doc = await workspace.document |
||||
expect(doc.isWord('-')).toBe(true) |
||||
}) |
||||
|
||||
it('should get languageId', async () => { |
||||
await helper.createDocument('t.vim') |
||||
expect(documents.getLanguageId('/a/b')).toBe('') |
||||
expect(documents.getLanguageId('/a/b.vim')).toBe('vim') |
||||
expect(documents.getLanguageId('/a/b.c')).toBe('') |
||||
}) |
||||
|
||||
it('should get lines', async () => { |
||||
let doc = await helper.createDocument('tmp') |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) |
||||
let lines = await documents.getLines(doc.uri) |
||||
expect(lines).toEqual(['foo', 'bar']) |
||||
lines = await documents.getLines('lsptest:///1') |
||||
expect(lines).toEqual([]) |
||||
lines = await documents.getLines('file:///not_exists_file') |
||||
expect(lines).toEqual([]) |
||||
let uri = URI.file(__filename).toString() |
||||
lines = await documents.getLines(uri) |
||||
expect(lines.length).toBeGreaterThan(0) |
||||
}) |
||||
|
||||
it('should read empty string from none file', async () => { |
||||
let res = await documents.readFile('test:///1') |
||||
expect(res).toBe('') |
||||
}) |
||||
|
||||
it('should get empty line from none file', async () => { |
||||
let res = await documents.getLine('test:///1', 1) |
||||
expect(res).toBe('') |
||||
let uri = URI.file(path.join(__dirname, 'not_exists_file')).toString() |
||||
res = await documents.getLine(uri, 1) |
||||
expect(res).toBe('') |
||||
}) |
||||
|
||||
it('should get QuickfixItem from location link', async () => { |
||||
let doc = await helper.createDocument('quickfix') |
||||
let loc = LocationLink.create(doc.uri, Range.create(0, 0, 3, 0), Range.create(0, 0, 0, 3)) |
||||
let res = await documents.getQuickfixItem(loc, 'text', 'E', 'module') |
||||
expect(res.targetRange).toBeDefined() |
||||
expect(res.type).toBe('E') |
||||
expect(res.module).toBe('module') |
||||
expect(res.bufnr).toBe(doc.bufnr) |
||||
}) |
||||
|
||||
it('should create document', async () => { |
||||
await helper.createDocument() |
||||
let bufnrs = await nvim.call('coc#ui#open_files', [[__filename]]) as number[] |
||||
let bufnr = bufnrs[0] |
||||
let doc = workspace.getDocument(bufnr) |
||||
expect(doc).toBeUndefined() |
||||
doc = await documents.createDocument(bufnr) |
||||
expect(doc).toBeDefined() |
||||
}) |
||||
|
||||
it('should check buffer rename on save', async () => { |
||||
let doc = await workspace.document |
||||
let bufnr = doc.bufnr |
||||
let name = `${uuid()}.vim` |
||||
let tmpfile = path.join(os.tmpdir(), name) |
||||
await nvim.command(`write ${tmpfile}`) |
||||
doc = workspace.getDocument(bufnr) |
||||
expect(doc).toBeDefined() |
||||
expect(doc.filetype).toBe('vim') |
||||
expect(doc.bufname).toMatch(name) |
||||
fs.unlinkSync(tmpfile) |
||||
}) |
||||
|
||||
it('should get current document', async () => { |
||||
let p1 = workspace.document |
||||
let p2 = workspace.document |
||||
let arr = await Promise.all([p1, p2]) |
||||
expect(arr[0]).toBe(arr[1]) |
||||
}) |
||||
|
||||
it('should get bufnrs', async () => { |
||||
await workspace.document |
||||
let bufnrs = documents.bufnrs |
||||
expect(bufnrs.length).toBe(1) |
||||
}) |
||||
|
||||
it('should get uri', async () => { |
||||
let doc = await workspace.document |
||||
expect(documents.uri).toBe(doc.uri) |
||||
}) |
||||
|
||||
it('should attach events on vim', async () => { |
||||
await documents.attach(nvim, workspace.env) |
||||
let env = Object.assign(workspace.env, { isVim: true }) |
||||
documents.detach() |
||||
await documents.attach(nvim, env) |
||||
documents.detach() |
||||
await events.fire('CursorMoved', [1, [1, 1]]) |
||||
}) |
||||
|
||||
it('should compute word ranges', async () => { |
||||
expect(await workspace.computeWordRanges('file:///1', Range.create(0, 0, 1, 0))).toBeNull() |
||||
let doc = await workspace.document |
||||
expect(await workspace.computeWordRanges(doc.uri, Range.create(0, 0, 1, 0))).toBeDefined() |
||||
}) |
||||
}) |
@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import Editors, { TextEditor } from '../../core/editors' |
||||
import workspace from '../../workspace' |
||||
import window from '../../window' |
||||
import events from '../../events' |
||||
import helper from '../helper' |
||||
import { disposeAll } from '../../util' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
|
||||
let editors: Editors |
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
editors = workspace.editors |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
disposeAll(disposables) |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('editors', () => { |
||||
|
||||
function assertEditor(editor: TextEditor, tabpagenr: number, winid: number) { |
||||
expect(editor).toBeDefined() |
||||
expect(editor.tabpagenr).toBe(tabpagenr) |
||||
expect(editor.winid).toBe(winid) |
||||
} |
||||
|
||||
it('should have active editor', async () => { |
||||
let winid = await nvim.call('win_getid') |
||||
let editor = window.activeTextEditor |
||||
assertEditor(editor, 1, winid) |
||||
let editors = window.visibleTextEditors |
||||
expect(editors.length).toBe(1) |
||||
}) |
||||
|
||||
it('should change active editor on split', async () => { |
||||
let promise = new Promise<TextEditor>(resolve => { |
||||
editors.onDidChangeActiveTextEditor(e => { |
||||
resolve(e) |
||||
}, null, disposables) |
||||
}) |
||||
await nvim.command('vnew') |
||||
let editor = await promise |
||||
let winid = await nvim.call('win_getid') |
||||
expect(editor.winid).toBe(winid) |
||||
}) |
||||
|
||||
it('should change active editor on tabe', async () => { |
||||
let promise = new Promise<TextEditor>(resolve => { |
||||
editors.onDidChangeActiveTextEditor(e => { |
||||
if (e.document.uri.includes('foo')) { |
||||
resolve(e) |
||||
} |
||||
}, null, disposables) |
||||
}) |
||||
await nvim.command('tabe a | tabe b | tabe foo') |
||||
let editor = await promise |
||||
let winid = await nvim.call('win_getid') |
||||
expect(editor.winid).toBe(winid) |
||||
}) |
||||
|
||||
it('should change active editor on edit', async () => { |
||||
await nvim.call('win_getid') |
||||
let fn = jest.fn() |
||||
window.onDidChangeVisibleTextEditors(() => { |
||||
fn() |
||||
}, null, disposables) |
||||
let promise = new Promise<TextEditor>(resolve => { |
||||
editors.onDidChangeActiveTextEditor(e => { |
||||
resolve(e) |
||||
}) |
||||
}) |
||||
await nvim.command('edit foo') |
||||
let editor = await promise |
||||
expect(editor.document.uri).toMatch('foo') |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should change active editor on window switch', async () => { |
||||
let winid = await nvim.call('win_getid') |
||||
await nvim.command('vs foo') |
||||
await nvim.command('wincmd p') |
||||
let curr = editors.activeTextEditor |
||||
expect(curr.winid).toBe(winid) |
||||
expect(editors.visibleTextEditors.length).toBe(2) |
||||
}) |
||||
|
||||
it('should not create editor for float window', async () => { |
||||
let fn = jest.fn() |
||||
await nvim.call('win_getid') |
||||
editors.onDidChangeActiveTextEditor(e => { |
||||
fn() |
||||
}) |
||||
let res = await nvim.call('coc#float#create_float_win', [0, 0, { |
||||
relative: 'editor', |
||||
row: 1, |
||||
col: 1, |
||||
width: 10, |
||||
height: 1, |
||||
lines: ['foo'] |
||||
}]) |
||||
await nvim.call('win_gotoid', [res[0]]) |
||||
await events.fire('CursorHold', [res[1]]) |
||||
await nvim.command('wincmd p') |
||||
expect(fn).toBeCalledTimes(0) |
||||
expect(editors.visibleTextEditors.length).toBe(1) |
||||
}) |
||||
|
||||
it('should cleanup on CursorHold', async () => { |
||||
let winid = await nvim.call('win_getid') |
||||
let promise = new Promise<TextEditor>(resolve => { |
||||
editors.onDidChangeActiveTextEditor(e => { |
||||
if (e.document.uri.includes('foo')) { |
||||
resolve(e) |
||||
} |
||||
}, null, disposables) |
||||
}) |
||||
await nvim.command('tabe foo') |
||||
await promise |
||||
await nvim.call('win_execute', [winid, 'noa close']) |
||||
let bufnr = await nvim.eval("bufnr('%')") |
||||
await events.fire('CursorHold', [bufnr]) |
||||
expect(editors.visibleTextEditors.length).toBe(1) |
||||
}) |
||||
|
||||
it('should cleanup on create', async () => { |
||||
let winid = await nvim.call('win_getid') |
||||
let promise = new Promise<TextEditor>(resolve => { |
||||
editors.onDidChangeActiveTextEditor(e => { |
||||
if (e.document.uri.includes('foo')) { |
||||
resolve(e) |
||||
} |
||||
}, null, disposables) |
||||
}) |
||||
await nvim.command('tabe foo') |
||||
await promise |
||||
await nvim.call('win_execute', [winid, 'noa close']) |
||||
await nvim.command('edit bar') |
||||
expect(editors.visibleTextEditors.length).toBe(2) |
||||
}) |
||||
|
||||
it('should have current tabnr after tab changed', async () => { |
||||
await nvim.command('tabe') |
||||
await helper.waitValue(() => { |
||||
return editors.visibleTextEditors.length |
||||
}, 2) |
||||
let editor = editors.visibleTextEditors.find(o => o.tabpagenr == 2) |
||||
await nvim.command('normal! 1gt') |
||||
await nvim.command('tabe') |
||||
await helper.waitValue(() => { |
||||
return editors.visibleTextEditors.length |
||||
}, 3) |
||||
expect(editor.tabpagenr).toBe(3) |
||||
await nvim.command('tabc') |
||||
await helper.waitValue(() => { |
||||
return editors.visibleTextEditors.length |
||||
}, 2) |
||||
expect(editor.tabpagenr).toBe(2) |
||||
}) |
||||
}) |
@ -0,0 +1,431 @@
@@ -0,0 +1,431 @@
|
||||
import bser from 'bser' |
||||
import fs from 'fs' |
||||
import net from 'net' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { v4 as uuid } from 'uuid' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
import Configurations from '../../configuration/index' |
||||
import { FileSystemWatcher, FileSystemWatcherManager } from '../../core/fileSystemWatcher' |
||||
import Watchman, { FileChangeItem, isValidWatchRoot } from '../../core/watchman' |
||||
import WorkspaceFolderController from '../../core/workspaceFolder' |
||||
import RelativePattern from '../../model/relativePattern' |
||||
import { GlobPattern } from '../../types' |
||||
import { disposeAll } from '../../util' |
||||
import helper from '../helper' |
||||
|
||||
let server: net.Server |
||||
let client: net.Socket |
||||
const cwd = process.cwd() |
||||
const sockPath = path.join(os.tmpdir(), `watchman-fake-${uuid()}`) |
||||
process.env.WATCHMAN_SOCK = sockPath |
||||
|
||||
let workspaceFolder: WorkspaceFolderController |
||||
let watcherManager: FileSystemWatcherManager |
||||
let configurations: Configurations |
||||
let disposables: Disposable[] = [] |
||||
|
||||
function wait(ms: number): Promise<any> { |
||||
return new Promise(resolve => { |
||||
setTimeout(() => { |
||||
resolve(undefined) |
||||
}, ms) |
||||
}) |
||||
} |
||||
|
||||
function createFileChange(file: string, isNew = true, exists = true): FileChangeItem { |
||||
return { |
||||
size: 1, |
||||
name: file, |
||||
exists, |
||||
new: isNew, |
||||
type: 'f', |
||||
mtime_ms: Date.now() |
||||
} |
||||
} |
||||
|
||||
function sendResponse(data: any): void { |
||||
client.write(bser.dumpToBuffer(data)) |
||||
} |
||||
|
||||
function sendSubscription(uid: string, root: string, files: FileChangeItem[]): void { |
||||
client.write(bser.dumpToBuffer({ |
||||
subscription: uid, |
||||
root, |
||||
files |
||||
})) |
||||
} |
||||
|
||||
let capabilities: any |
||||
let watchResponse: any |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
}) |
||||
|
||||
beforeAll(done => { |
||||
let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json') |
||||
configurations = new Configurations(userConfigFile, undefined) |
||||
workspaceFolder = new WorkspaceFolderController(configurations) |
||||
watcherManager = new FileSystemWatcherManager(workspaceFolder, '') |
||||
watcherManager.attach(helper.createNullChannel()) |
||||
// create a mock sever for watchman
|
||||
server = net.createServer(c => { |
||||
client = c |
||||
c.on('data', data => { |
||||
let obj = bser.loadFromBuffer(data) |
||||
if (obj[0] == 'watch-project') { |
||||
sendResponse(watchResponse || { watch: obj[1], warning: 'warning' }) |
||||
} else if (obj[0] == 'unsubscribe') { |
||||
sendResponse({ path: obj[1] }) |
||||
} else if (obj[0] == 'clock') { |
||||
sendResponse({ clock: 'clock' }) |
||||
} else if (obj[0] == 'version') { |
||||
let { optional, required } = obj[1] |
||||
let res = {} |
||||
for (let key of optional) { |
||||
res[key] = true |
||||
} |
||||
for (let key of required) { |
||||
res[key] = true |
||||
} |
||||
sendResponse({ capabilities: capabilities || res }) |
||||
} else if (obj[0] == 'subscribe') { |
||||
sendResponse({ subscribe: obj[2] }) |
||||
} else { |
||||
sendResponse({}) |
||||
} |
||||
}) |
||||
}) |
||||
server.on('error', err => { |
||||
throw err |
||||
}) |
||||
server.listen(sockPath, () => { |
||||
done() |
||||
}) |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
capabilities = undefined |
||||
watchResponse = undefined |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
watcherManager.dispose() |
||||
server.removeAllListeners() |
||||
server.close() |
||||
if (fs.existsSync(sockPath)) { |
||||
fs.unlinkSync(sockPath) |
||||
} |
||||
}) |
||||
|
||||
describe('watchman', () => { |
||||
it('should throw error when not watching', async () => { |
||||
let client = new Watchman(null) |
||||
disposables.push(client) |
||||
let fn = async () => { |
||||
await client.subscribe('**/*', () => {}) |
||||
} |
||||
await expect(fn()).rejects.toThrow(/not watching/) |
||||
}) |
||||
|
||||
it('should checkCapability', async () => { |
||||
let client = new Watchman(null) |
||||
let res = await client.checkCapability() |
||||
expect(res).toBe(true) |
||||
capabilities = { relative_root: false } |
||||
res = await client.checkCapability() |
||||
expect(res).toBe(false) |
||||
client.dispose() |
||||
}) |
||||
|
||||
it('should watchProject', async () => { |
||||
let client = new Watchman(null) |
||||
disposables.push(client) |
||||
let res = await client.watchProject(__dirname) |
||||
expect(res).toBe(true) |
||||
client.dispose() |
||||
}) |
||||
|
||||
it('should unsubscribe', async () => { |
||||
let client = new Watchman(null) |
||||
disposables.push(client) |
||||
await client.watchProject(cwd) |
||||
let fn = jest.fn() |
||||
let disposable = await client.subscribe(`${cwd}/*`, fn) |
||||
disposable.dispose() |
||||
client.dispose() |
||||
}) |
||||
}) |
||||
|
||||
describe('Watchman#subscribe', () => { |
||||
|
||||
it('should subscribe file change', async () => { |
||||
let client = new Watchman(null, helper.createNullChannel()) |
||||
disposables.push(client) |
||||
await client.watchProject(cwd) |
||||
let fn = jest.fn() |
||||
let disposable = await client.subscribe(`${cwd}/*`, fn) |
||||
let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)] |
||||
sendSubscription(disposable.subscribe, cwd, changes) |
||||
await wait(30) |
||||
expect(fn).toBeCalled() |
||||
let call = fn.mock.calls[0][0] |
||||
disposable.dispose() |
||||
expect(call.root).toBe(cwd) |
||||
client.dispose() |
||||
}) |
||||
|
||||
it('should subscribe with relative_path', async () => { |
||||
let client = new Watchman(null, helper.createNullChannel()) |
||||
watchResponse = { watch: cwd, relative_path: 'foo' } |
||||
await client.watchProject(cwd) |
||||
let fn = jest.fn() |
||||
let disposable = await client.subscribe(`${cwd}/*`, fn) |
||||
let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)] |
||||
sendSubscription(disposable.subscribe, cwd, changes) |
||||
await wait(30) |
||||
expect(fn).toBeCalled() |
||||
let call = fn.mock.calls[0][0] |
||||
disposable.dispose() |
||||
expect(call.root).toBe(path.join(cwd, 'foo')) |
||||
client.dispose() |
||||
}) |
||||
|
||||
it('should not subscribe invalid response', async () => { |
||||
let c = new Watchman(null, helper.createNullChannel()) |
||||
disposables.push(c) |
||||
watchResponse = { watch: cwd, relative_path: 'foo' } |
||||
await c.watchProject(cwd) |
||||
let fn = jest.fn() |
||||
let disposable = await c.subscribe(`${cwd}/*`, fn) |
||||
let changes: FileChangeItem[] = [createFileChange(`${cwd}/a`)] |
||||
sendSubscription('uuid', cwd, changes) |
||||
await wait(10) |
||||
sendSubscription(disposable.subscribe, cwd, []) |
||||
await wait(10) |
||||
client.write(bser.dumpToBuffer({ |
||||
subscription: disposable.subscribe, |
||||
root: cwd |
||||
})) |
||||
await wait(10) |
||||
expect(fn).toBeCalledTimes(0) |
||||
}) |
||||
}) |
||||
|
||||
describe('Watchman#createClient', () => { |
||||
it('should not create client when capabilities not match', async () => { |
||||
capabilities = { relative_root: false } |
||||
await expect(async () => { |
||||
await Watchman.createClient(null, cwd) |
||||
}).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should not create when watch failed', async () => { |
||||
watchResponse = {} |
||||
await expect(async () => { |
||||
await Watchman.createClient(null, cwd) |
||||
}).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should create client', async () => { |
||||
let client = await Watchman.createClient(null, cwd) |
||||
disposables.push(client) |
||||
expect(client).toBeDefined() |
||||
}) |
||||
|
||||
it('should not create client for root', async () => { |
||||
await expect(async () => { |
||||
await Watchman.createClient(null, '/') |
||||
}).rejects.toThrow(Error) |
||||
}) |
||||
}) |
||||
|
||||
describe('isValidWatchRoot()', () => { |
||||
it('should check valid root', async () => { |
||||
expect(isValidWatchRoot('/')).toBe(false) |
||||
expect(isValidWatchRoot(os.homedir())).toBe(false) |
||||
expect(isValidWatchRoot('/tmp/a/b/c')).toBe(false) |
||||
expect(isValidWatchRoot(os.tmpdir())).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('fileSystemWatcher', () => { |
||||
|
||||
function createWatcher(pattern: GlobPattern, ignoreCreateEvents = false, ignoreChangeEvents = false, ignoreDeleteEvents = false): FileSystemWatcher { |
||||
let watcher = watcherManager.createFileSystemWatcher( |
||||
pattern, |
||||
ignoreCreateEvents, |
||||
ignoreChangeEvents, |
||||
ignoreDeleteEvents |
||||
) |
||||
disposables.push(watcher) |
||||
return watcher |
||||
} |
||||
|
||||
beforeAll(async () => { |
||||
workspaceFolder.addWorkspaceFolder(cwd, true) |
||||
await watcherManager.waitClient(cwd) |
||||
}) |
||||
|
||||
it('should use relative pattern #1', async () => { |
||||
let folder = workspaceFolder.workspaceFolders[0] |
||||
expect(folder).toBeDefined() |
||||
let pattern = new RelativePattern(folder, '**/*') |
||||
let watcher = createWatcher(pattern, false, true, true) |
||||
let fn = jest.fn() |
||||
watcher.onDidCreate(fn) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`)] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should use relative pattern #2', async () => { |
||||
let called = false |
||||
let pattern = new RelativePattern(__dirname, '**/*') |
||||
let watcher = createWatcher(pattern, false, true, true) |
||||
watcher.onDidCreate(() => { |
||||
called = true |
||||
}) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`)] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(called).toBe(false) |
||||
}) |
||||
|
||||
it('should use relative pattern #3', async () => { |
||||
let called = false |
||||
let root = path.join(os.tmpdir(), 'not_exists') |
||||
let pattern = new RelativePattern(root, '**/*') |
||||
let watcher = createWatcher(pattern, false, true, true) |
||||
watcher.onDidCreate(() => { |
||||
called = true |
||||
}) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`)] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(called).toBe(false) |
||||
}) |
||||
|
||||
it('should watch for file create', async () => { |
||||
let watcher = createWatcher('**/*', false, true, true) |
||||
let fn = jest.fn() |
||||
watcher.onDidCreate(fn) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`)] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should watch for file delete', async () => { |
||||
let watcher = createWatcher('**/*', true, true, false) |
||||
let fn = jest.fn() |
||||
watcher.onDidDelete(fn) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`, false, false)] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should watch for file change', async () => { |
||||
let watcher = createWatcher('**/*', false, false, false) |
||||
let fn = jest.fn() |
||||
watcher.onDidChange(fn) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`, false, true)] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should watch for file rename', async () => { |
||||
let watcher = createWatcher('**/*', false, false, false) |
||||
let fn = jest.fn() |
||||
watcher.onDidRename(fn) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [ |
||||
createFileChange(`a`, false, false), |
||||
createFileChange(`b`, true, true), |
||||
] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should not watch for events', async () => { |
||||
let watcher = createWatcher('**/*', true, true, true) |
||||
let called = false |
||||
let onChange = () => { called = true } |
||||
watcher.onDidCreate(onChange) |
||||
watcher.onDidChange(onChange) |
||||
watcher.onDidDelete(onChange) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [ |
||||
createFileChange(`a`, false, false), |
||||
createFileChange(`b`, true, true), |
||||
createFileChange(`c`, false, true), |
||||
] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.wait(50) |
||||
expect(called).toBe(false) |
||||
}) |
||||
|
||||
it('should watch for folder rename', async () => { |
||||
let watcher = createWatcher('**/*') |
||||
let newFiles: string[] = [] |
||||
let count = 0 |
||||
watcher.onDidRename(e => { |
||||
count++ |
||||
newFiles.push(e.newUri.fsPath) |
||||
}) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [ |
||||
createFileChange(`a/1`, false, false), |
||||
createFileChange(`a/2`, false, false), |
||||
createFileChange(`b/1`, true, true), |
||||
createFileChange(`b/2`, true, true), |
||||
] |
||||
sendSubscription(watcher.subscribe, cwd, changes) |
||||
await helper.waitValue(() => { |
||||
return count |
||||
}, 2) |
||||
}) |
||||
|
||||
it('should watch for new folder', async () => { |
||||
let watcher = createWatcher('**/*') |
||||
expect(watcher).toBeDefined() |
||||
workspaceFolder.renameWorkspaceFolder(cwd, __dirname) |
||||
await helper.wait(50) |
||||
let uri: URI |
||||
watcher.onDidCreate(e => { |
||||
uri = e |
||||
}) |
||||
await helper.wait(50) |
||||
let changes: FileChangeItem[] = [createFileChange(`a`)] |
||||
sendSubscription(watcher.subscribe, __dirname, changes) |
||||
await helper.wait(50) |
||||
expect(uri.fsPath).toEqual(path.join(__dirname, 'a')) |
||||
}) |
||||
}) |
||||
|
||||
describe('create FileSystemWatcherManager', () => { |
||||
it('should attach to existing workspace folder', async () => { |
||||
let workspaceFolder = new WorkspaceFolderController(configurations) |
||||
workspaceFolder.addWorkspaceFolder(cwd, false) |
||||
let watcherManager = new FileSystemWatcherManager(workspaceFolder, '') |
||||
watcherManager.attach(helper.createNullChannel()) |
||||
await helper.wait(100) |
||||
await watcherManager.createClient(os.tmpdir()) |
||||
await watcherManager.createClient(cwd) |
||||
await watcherManager.waitClient(cwd) |
||||
watcherManager.dispose() |
||||
}) |
||||
}) |
@ -0,0 +1,840 @@
@@ -0,0 +1,840 @@
|
||||
import { Buffer, Neovim } from '@chemzqm/neovim' |
||||
import fs from 'fs' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { v4 as uuid } from 'uuid' |
||||
import { CancellationTokenSource, Disposable } from 'vscode-languageserver-protocol' |
||||
import { CreateFile, DeleteFile, Position, Range, RenameFile, TextDocumentEdit, TextEdit, VersionedTextDocumentIdentifier, WorkspaceEdit } from 'vscode-languageserver-types' |
||||
import { URI } from 'vscode-uri' |
||||
import { RecoverFunc, getOriginalLine } from '../../model/editInspect' |
||||
import RelativePattern from '../../model/relativePattern' |
||||
import { disposeAll } from '../../util' |
||||
import { readFile } from '../../util/fs' |
||||
import window from '../../window' |
||||
import workspace from '../../workspace' |
||||
import events from '../../events' |
||||
import helper, { createTmpFile } from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
disposeAll(disposables) |
||||
disposables = [] |
||||
}) |
||||
|
||||
describe('RelativePattern', () => { |
||||
function testThrow(fn: () => void) { |
||||
let err |
||||
try { |
||||
fn() |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
} |
||||
|
||||
it('should throw for invalid arguments', async () => { |
||||
testThrow(() => { |
||||
new RelativePattern('', undefined) |
||||
}) |
||||
testThrow(() => { |
||||
new RelativePattern({ uri: undefined } as any, '') |
||||
}) |
||||
}) |
||||
|
||||
it('should create relativePattern', async () => { |
||||
for (let base of [__filename, URI.file(__filename), { uri: URI.file(__dirname).toString(), name: 'test' }]) { |
||||
let p = new RelativePattern(base, '**/*') |
||||
expect(URI.isUri(p.baseUri)).toBe(true) |
||||
expect(p.toJSON()).toBeDefined() |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
describe('findFiles()', () => { |
||||
beforeEach(() => { |
||||
workspace.workspaceFolderControl.setWorkspaceFolders([__dirname]) |
||||
}) |
||||
|
||||
it('should use glob pattern', async () => { |
||||
let res = await workspace.findFiles('**/*.ts') |
||||
expect(res.length).toBeGreaterThan(0) |
||||
}) |
||||
|
||||
it('should use relativePattern', async () => { |
||||
let relativePattern = new RelativePattern(URI.file(__dirname), '**/*.ts') |
||||
let res = await workspace.findFiles(relativePattern) |
||||
expect(res.length).toBeGreaterThan(0) |
||||
}) |
||||
|
||||
it('should respect exclude as glob pattern', async () => { |
||||
let arr = await workspace.findFiles('**/*.ts', 'files*') |
||||
let res = arr.find(o => path.relative(__dirname, o.fsPath).startsWith('files')) |
||||
expect(res).toBeUndefined() |
||||
}) |
||||
|
||||
it('should respect exclude as relativePattern', async () => { |
||||
let relativePattern = new RelativePattern(URI.file(__dirname), 'files*') |
||||
let arr = await workspace.findFiles('**/*.ts', relativePattern) |
||||
let res = arr.find(o => path.relative(__dirname, o.fsPath).startsWith('files')) |
||||
expect(res).toBeUndefined() |
||||
}) |
||||
|
||||
it('should respect maxResults', async () => { |
||||
let arr = await workspace.findFiles('**/*.ts', undefined, 1) |
||||
expect(arr.length).toBe(1) |
||||
}) |
||||
|
||||
it('should respect token', async () => { |
||||
let source = new CancellationTokenSource() |
||||
source.cancel() |
||||
let arr = await workspace.findFiles('**/*.ts', undefined, 1, source.token) |
||||
expect(arr.length).toBe(0) |
||||
}) |
||||
|
||||
it('should cancel findFiles', async () => { |
||||
let source = new CancellationTokenSource() |
||||
let p = workspace.findFiles('**/*.ts', undefined, 1, source.token) |
||||
source.cancel() |
||||
let arr = await p |
||||
expect(arr.length).toBe(0) |
||||
}) |
||||
}) |
||||
|
||||
describe('applyEdits()', () => { |
||||
it('should not throw when unable to undo & redo', async () => { |
||||
await workspace.files.undoWorkspaceEdit() |
||||
await workspace.files.redoWorkspaceEdit() |
||||
}) |
||||
|
||||
it('should apply TextEdit of documentChanges', async () => { |
||||
let doc = await helper.createDocument() |
||||
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version) |
||||
let edit = TextEdit.insert(Position.create(0, 0), 'bar') |
||||
let change = TextDocumentEdit.create(versioned, [edit]) |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [change] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('bar') |
||||
}) |
||||
|
||||
it('should apply edit with out change buffers', async () => { |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('bar') |
||||
await doc.synchronize() |
||||
let version = doc.version |
||||
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, doc.version) |
||||
let edit = TextEdit.replace(Range.create(0, 0, 0, 3), 'bar') |
||||
let change = TextDocumentEdit.create(versioned, [edit]) |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [change] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
expect(doc.version).toBe(version) |
||||
}) |
||||
|
||||
it('should not apply TextEdit if version miss match', async () => { |
||||
let doc = await helper.createDocument() |
||||
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, 10) |
||||
let edit = TextEdit.insert(Position.create(0, 0), 'bar') |
||||
let change = TextDocumentEdit.create(versioned, [edit]) |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [change] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should apply edits with changes to buffer', async () => { |
||||
let doc = await helper.createDocument() |
||||
let changes = { |
||||
[doc.uri]: [TextEdit.insert(Position.create(0, 0), 'bar')] |
||||
} |
||||
let workspaceEdit: WorkspaceEdit = { changes } |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('bar') |
||||
}) |
||||
|
||||
it('should apply edits with changes to file not in buffer list', async () => { |
||||
let filepath = await createTmpFile('bar') |
||||
let uri = URI.file(filepath).toString() |
||||
let changes = { |
||||
[uri]: [TextEdit.insert(Position.create(0, 0), 'foo')] |
||||
} |
||||
let res = await workspace.applyEdit({ changes }) |
||||
expect(res).toBe(true) |
||||
let doc = workspace.getDocument(uri) |
||||
let content = doc.getDocumentContent() |
||||
expect(content).toMatch(/^foobar/) |
||||
await nvim.command('silent! %bwipeout!') |
||||
}) |
||||
|
||||
it('should apply edits when file does not exist', async () => { |
||||
let filepath = path.join(__dirname, 'not_exists') |
||||
disposables.push({ |
||||
dispose: () => { |
||||
if (fs.existsSync(filepath)) { |
||||
fs.unlinkSync(filepath) |
||||
} |
||||
} |
||||
}) |
||||
let uri = URI.file(filepath).toString() |
||||
let changes = { |
||||
[uri]: [TextEdit.insert(Position.create(0, 0), 'foo')] |
||||
} |
||||
let res = await workspace.applyEdit({ changes }) |
||||
expect(res).toBe(true) |
||||
}) |
||||
|
||||
it('should adjust cursor position after applyEdits', async () => { |
||||
let doc = await helper.createDocument() |
||||
let pos = await window.getCursorPosition() |
||||
expect(pos).toEqual({ line: 0, character: 0 }) |
||||
let edit = TextEdit.insert(Position.create(0, 0), 'foo\n') |
||||
let versioned = VersionedTextDocumentIdentifier.create(doc.uri, null) |
||||
let documentChanges = [TextDocumentEdit.create(versioned, [edit])] |
||||
let res = await workspace.applyEdit({ documentChanges }) |
||||
expect(res).toBe(true) |
||||
pos = await window.getCursorPosition() |
||||
expect(pos).toEqual({ line: 1, character: 0 }) |
||||
}) |
||||
|
||||
it('should support null version of documentChanges', async () => { |
||||
let file = path.join(__dirname, 'foo') |
||||
await workspace.createFile(file, { ignoreIfExists: true, overwrite: true }) |
||||
let uri = URI.file(file).toString() |
||||
let versioned = VersionedTextDocumentIdentifier.create(uri, null) |
||||
let edit = TextEdit.insert(Position.create(0, 0), 'bar') |
||||
let change = TextDocumentEdit.create(versioned, [edit]) |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [change] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
await nvim.command('wa') |
||||
let content = await readFile(file, 'utf8') |
||||
expect(content).toMatch(/^bar/) |
||||
await workspace.deleteFile(file, { ignoreIfNotExists: true }) |
||||
}) |
||||
|
||||
it('should support CreateFile edit', async () => { |
||||
let file = path.join(__dirname, 'foo') |
||||
let uri = URI.file(file).toString() |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [CreateFile.create(uri, { overwrite: true })] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
await workspace.deleteFile(file, { ignoreIfNotExists: true }) |
||||
}) |
||||
|
||||
it('should support DeleteFile edit', async () => { |
||||
let file = path.join(__dirname, 'foo') |
||||
await workspace.createFile(file, { ignoreIfExists: true, overwrite: true }) |
||||
let uri = URI.file(file).toString() |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [DeleteFile.create(uri)] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
}) |
||||
|
||||
it('should check uri for CreateFile edit', async () => { |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [CreateFile.create('term://.', { overwrite: true })] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should support RenameFile edit', async () => { |
||||
let file = path.join(__dirname, 'foo') |
||||
await workspace.createFile(file, { ignoreIfExists: true, overwrite: true }) |
||||
let newFile = path.join(__dirname, 'bar') |
||||
let uri = URI.file(file).toString() |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [RenameFile.create(uri, URI.file(newFile).toString())] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
await workspace.deleteFile(newFile, { ignoreIfNotExists: true }) |
||||
}) |
||||
|
||||
it('should support changes with edit and rename', async () => { |
||||
let fsPath = await createTmpFile('test') |
||||
let doc = await helper.createDocument(fsPath) |
||||
let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`) |
||||
let newUri = URI.file(newFile).toString() |
||||
let edit: WorkspaceEdit = { |
||||
documentChanges: [ |
||||
{ |
||||
textDocument: { |
||||
version: null, |
||||
uri: doc.uri, |
||||
}, |
||||
edits: [ |
||||
{ |
||||
range: { |
||||
start: { |
||||
line: 0, |
||||
character: 0 |
||||
}, |
||||
end: { |
||||
line: 0, |
||||
character: 4 |
||||
} |
||||
}, |
||||
newText: 'bar' |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
oldUri: doc.uri, |
||||
newUri, |
||||
kind: 'rename' |
||||
} |
||||
] |
||||
} |
||||
let res = await workspace.applyEdit(edit) |
||||
expect(res).toBe(true) |
||||
let curr = await workspace.document |
||||
expect(curr.uri).toBe(newUri) |
||||
expect(curr.getline(0)).toBe('bar') |
||||
let line = await nvim.line |
||||
expect(line).toBe('bar') |
||||
}) |
||||
|
||||
it('should support edit new file with CreateFile', async () => { |
||||
let file = path.join(os.tmpdir(), 'foo') |
||||
let uri = URI.file(file).toString() |
||||
let workspaceEdit: WorkspaceEdit = { |
||||
documentChanges: [ |
||||
CreateFile.create(uri, { overwrite: true }), |
||||
TextDocumentEdit.create({ uri, version: 0 }, [ |
||||
TextEdit.insert(Position.create(0, 0), 'foo bar') |
||||
]) |
||||
] |
||||
} |
||||
let res = await workspace.applyEdit(workspaceEdit) |
||||
expect(res).toBe(true) |
||||
let doc = workspace.getDocument(uri) |
||||
expect(doc).toBeDefined() |
||||
let line = doc.getline(0) |
||||
expect(line).toBe('foo bar') |
||||
await workspace.deleteFile(file, { ignoreIfNotExists: true }) |
||||
}) |
||||
|
||||
it('should undo and redo workspace edit', async () => { |
||||
const folder = path.join(os.tmpdir(), uuid()) |
||||
const pathone = path.join(folder, 'a') |
||||
const pathtwo = path.join(folder, 'b') |
||||
await workspace.files.createFile(pathone, { overwrite: true }) |
||||
await workspace.files.createFile(pathtwo, { overwrite: true }) |
||||
let uris = [URI.file(pathone).toString(), URI.file(pathtwo).toString()] |
||||
const assertContent = (one: string, two: string) => { |
||||
let doc = workspace.getDocument(uris[0]) |
||||
expect(doc.getDocumentContent()).toBe(one) |
||||
doc = workspace.getDocument(uris[1]) |
||||
expect(doc.getDocumentContent()).toBe(two) |
||||
} |
||||
let edits: TextDocumentEdit[] = [] |
||||
edits.push(TextDocumentEdit.create({ uri: uris[0], version: null }, [ |
||||
TextEdit.insert(Position.create(0, 0), 'foo') |
||||
])) |
||||
edits.push(TextDocumentEdit.create({ uri: uris[1], version: null }, [ |
||||
TextEdit.insert(Position.create(0, 0), 'bar') |
||||
])) |
||||
await workspace.applyEdit({ documentChanges: edits }) |
||||
assertContent('foo\n', 'bar\n') |
||||
await workspace.files.undoWorkspaceEdit() |
||||
assertContent('\n', '\n') |
||||
await workspace.files.redoWorkspaceEdit() |
||||
assertContent('foo\n', 'bar\n') |
||||
}) |
||||
|
||||
it('should should support annotations', async () => { |
||||
async function assertEdit(confirm: boolean): Promise<void> { |
||||
let doc = await helper.createDocument(uuid()) |
||||
let edit: WorkspaceEdit = { |
||||
documentChanges: [ |
||||
{ |
||||
textDocument: { version: doc.version, uri: doc.uri }, |
||||
edits: [ |
||||
{ |
||||
range: Range.create(0, 0, 0, 0), |
||||
newText: 'bar', |
||||
annotationId: '85bc78e2-5ef0-4949-b10c-13f476faf430' |
||||
} |
||||
] |
||||
}, |
||||
], |
||||
changeAnnotations: { |
||||
'85bc78e2-5ef0-4949-b10c-13f476faf430': { |
||||
needsConfirmation: true, |
||||
label: 'Text changes', |
||||
description: 'description' |
||||
} |
||||
} |
||||
} |
||||
let p = workspace.files.applyEdit(edit) |
||||
await helper.waitPrompt() |
||||
if (confirm) { |
||||
await nvim.input('<cr>') |
||||
} else { |
||||
await nvim.input('<esc>') |
||||
} |
||||
await p |
||||
let content = doc.getDocumentContent() |
||||
if (confirm) { |
||||
expect(content).toBe('bar\n') |
||||
} else { |
||||
expect(content).toBe('\n') |
||||
} |
||||
} |
||||
await assertEdit(true) |
||||
await assertEdit(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('getOriginalLine', () => { |
||||
it('should get original line', async () => { |
||||
let item = { index: 0, filepath: '' } |
||||
expect(getOriginalLine(item, undefined)).toBeUndefined() |
||||
expect(getOriginalLine({ index: 0, filepath: '', lnum: 1 }, undefined)).toBe(1) |
||||
let doc = await helper.createDocument() |
||||
let change = { |
||||
textDocument: { version: doc.version, uri: doc.uri }, |
||||
edits: [ |
||||
{ |
||||
range: Range.create(0, 0, 0, 0), |
||||
newText: 'bar', |
||||
} |
||||
] |
||||
} |
||||
expect(getOriginalLine({ index: 0, filepath: '', lnum: 1 }, change)).toBe(1) |
||||
}) |
||||
|
||||
describe('inspectEdit', () => { |
||||
async function inspect(edit: WorkspaceEdit): Promise<Buffer> { |
||||
await workspace.applyEdit(edit) |
||||
await workspace.files.inspectEdit() |
||||
let buf = await nvim.buffer |
||||
return buf |
||||
} |
||||
|
||||
it('should show wanring when edit not exists', async () => { |
||||
(workspace.files as any).editState = undefined |
||||
await workspace.files.inspectEdit() |
||||
}) |
||||
|
||||
it('should render with changes', async () => { |
||||
let fsPath = await createTmpFile('foo\n1\n2\nbar') |
||||
let doc = await helper.createDocument(fsPath) |
||||
let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`) |
||||
let newUri = URI.file(newFile).toString() |
||||
let createFile = path.join(os.tmpdir(), `coc-${process.pid}/create-${uuid()}`) |
||||
let deleteFile = await createTmpFile('delete') |
||||
disposables.push(Disposable.create(() => { |
||||
if (fs.existsSync(newFile)) fs.unlinkSync(newFile) |
||||
if (fs.existsSync(createFile)) fs.unlinkSync(createFile) |
||||
if (fs.existsSync(deleteFile)) fs.unlinkSync(deleteFile) |
||||
})) |
||||
let edit: WorkspaceEdit = { |
||||
documentChanges: [ |
||||
{ |
||||
textDocument: { version: null, uri: doc.uri, }, |
||||
edits: [ |
||||
TextEdit.del(Range.create(0, 0, 1, 0)), |
||||
TextEdit.replace(Range.create(3, 0, 3, 3), 'xyz'), |
||||
] |
||||
}, |
||||
{ |
||||
kind: 'rename', |
||||
oldUri: doc.uri, |
||||
newUri |
||||
}, { |
||||
kind: 'create', |
||||
uri: URI.file(createFile).toString() |
||||
}, { |
||||
kind: 'delete', |
||||
uri: URI.file(deleteFile).toString() |
||||
} |
||||
] |
||||
} |
||||
let buf = await inspect(edit) |
||||
let lines = await buf.lines |
||||
let content = lines.join('\n') |
||||
expect(content).toMatch('Change') |
||||
expect(content).toMatch('Rename') |
||||
expect(content).toMatch('Create') |
||||
expect(content).toMatch('Delete') |
||||
await nvim.command('exe 5') |
||||
await nvim.input('<CR>') |
||||
await helper.waitFor('expand', ['%:p'], newFile) |
||||
let line = await nvim.call('line', ['.']) |
||||
expect(line).toBe(3) |
||||
}) |
||||
|
||||
it('should render annotation label', async () => { |
||||
let doc = await helper.createDocument(uuid()) |
||||
let edit: WorkspaceEdit = { |
||||
documentChanges: [ |
||||
{ |
||||
textDocument: { version: doc.version, uri: doc.uri }, |
||||
edits: [ |
||||
{ |
||||
range: Range.create(0, 0, 0, 0), |
||||
newText: 'bar', |
||||
annotationId: 'dd866f37-a24c-4503-9c35-c139fb28e25b' |
||||
} |
||||
] |
||||
}, { |
||||
textDocument: { version: 1, uri: doc.uri }, |
||||
edits: [ |
||||
{ |
||||
range: Range.create(0, 0, 0, 0), |
||||
newText: 'bar', |
||||
annotationId: '9468b9bf-97b6-4b37-b21f-aba8df3ce658' |
||||
} |
||||
] |
||||
}], |
||||
changeAnnotations: { |
||||
'dd866f37-a24c-4503-9c35-c139fb28e25b': { |
||||
needsConfirmation: false, |
||||
label: 'Text changes' |
||||
} |
||||
} |
||||
} |
||||
let buf = await inspect(edit) |
||||
await events.fire('BufUnload', [buf.id + 1]) |
||||
let winid = await nvim.call('win_getid') |
||||
let lines = await buf.lines |
||||
expect(lines[0]).toBe('Text changes') |
||||
await nvim.command('exe 1') |
||||
await nvim.input('<CR>') |
||||
let bufnr = await nvim.call('bufnr', ['%']) |
||||
expect(bufnr).toBe(buf.id) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('<CR>') |
||||
let fsPath = URI.parse(doc.uri).fsPath |
||||
await helper.waitFor('expand', ['%:p'], fsPath) |
||||
await nvim.call('win_gotoid', [winid]) |
||||
await nvim.input('<esc>') |
||||
await helper.wait(10) |
||||
}) |
||||
}) |
||||
|
||||
describe('createFile()', () => { |
||||
it('should create and revert parent folder', async () => { |
||||
const folder = path.join(os.tmpdir(), uuid()) |
||||
const filepath = path.join(folder, 'bar') |
||||
disposables.push(Disposable.create(() => { |
||||
fs.rmSync(folder, { recursive: true, force: true }) |
||||
})) |
||||
let fns: RecoverFunc[] = [] |
||||
expect(fs.existsSync(folder)).toBe(false) |
||||
await workspace.files.createFile(filepath, {}, fns) |
||||
expect(fs.existsSync(filepath)).toBe(true) |
||||
for (let i = fns.length - 1; i >= 0; i--) { |
||||
await fns[i]() |
||||
} |
||||
expect(fs.existsSync(folder)).toBe(false) |
||||
}) |
||||
|
||||
it('should throw when file already exists', async () => { |
||||
let filepath = await createTmpFile('foo', disposables) |
||||
let fn = async () => { |
||||
await workspace.createFile(filepath, {}) |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should not create file if file exists with ignoreIfExists', async () => { |
||||
let file = await createTmpFile('foo') |
||||
await workspace.createFile(file, { ignoreIfExists: true }) |
||||
let content = fs.readFileSync(file, 'utf8') |
||||
expect(content).toBe('foo') |
||||
}) |
||||
|
||||
it('should create file if does not exist', async () => { |
||||
await helper.edit() |
||||
let filepath = path.join(__dirname, 'foo') |
||||
await workspace.createFile(filepath, { ignoreIfExists: true }) |
||||
let exists = fs.existsSync(filepath) |
||||
expect(exists).toBe(true) |
||||
fs.unlinkSync(filepath) |
||||
}) |
||||
|
||||
it('should revert file create', async () => { |
||||
let filepath = path.join(os.tmpdir(), uuid()) |
||||
disposables.push(Disposable.create(() => { |
||||
if (fs.existsSync(filepath)) fs.unlinkSync(filepath) |
||||
})) |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.createFile(filepath, { overwrite: true }, fns) |
||||
expect(fs.existsSync(filepath)).toBe(true) |
||||
let bufnr = await nvim.call('bufnr', [filepath]) |
||||
expect(bufnr).toBeGreaterThan(0) |
||||
let doc = workspace.getDocument(bufnr) |
||||
expect(doc).toBeDefined() |
||||
for (let fn of fns) { |
||||
await fn() |
||||
} |
||||
expect(fs.existsSync(filepath)).toBe(false) |
||||
let loaded = await nvim.call('bufloaded', [filepath]) |
||||
expect(loaded).toBe(0) |
||||
}) |
||||
}) |
||||
|
||||
describe('renameFile', () => { |
||||
it('should throw when oldPath not exists', async () => { |
||||
let filepath = path.join(__dirname, 'not_exists_file') |
||||
let newPath = path.join(__dirname, 'bar') |
||||
let fn = async () => { |
||||
await workspace.renameFile(filepath, newPath) |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should rename file on disk', async () => { |
||||
let filepath = await createTmpFile('test') |
||||
let newPath = path.join(path.dirname(filepath), 'new_file') |
||||
disposables.push(Disposable.create(() => { |
||||
if (fs.existsSync(newPath)) fs.unlinkSync(newPath) |
||||
if (fs.existsSync(filepath)) fs.unlinkSync(filepath) |
||||
})) |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.renameFile(filepath, newPath, { overwrite: true }, fns) |
||||
expect(fs.existsSync(newPath)).toBe(true) |
||||
for (let fn of fns) { |
||||
await fn() |
||||
} |
||||
expect(fs.existsSync(newPath)).toBe(false) |
||||
expect(fs.existsSync(filepath)).toBe(true) |
||||
}) |
||||
|
||||
it('should rename if file does not exist', async () => { |
||||
let filepath = path.join(__dirname, 'foo') |
||||
let newPath = path.join(__dirname, 'bar') |
||||
await workspace.createFile(filepath) |
||||
await workspace.renameFile(filepath, newPath) |
||||
expect(fs.existsSync(newPath)).toBe(true) |
||||
expect(fs.existsSync(filepath)).toBe(false) |
||||
fs.unlinkSync(newPath) |
||||
}) |
||||
|
||||
it('should rename current buffer with same bufnr', async () => { |
||||
let file = await createTmpFile('test') |
||||
let doc = await helper.createDocument(file) |
||||
await nvim.setLine('bar') |
||||
await doc.patchChange() |
||||
let newFile = path.join(os.tmpdir(), `coc-${process.pid}/new-${uuid()}`) |
||||
disposables.push(Disposable.create(() => { |
||||
if (fs.existsSync(newFile)) fs.unlinkSync(newFile) |
||||
})) |
||||
await workspace.renameFile(file, newFile) |
||||
let bufnr = await nvim.call('bufnr', ['%']) |
||||
expect(bufnr).toBe(doc.bufnr) |
||||
let line = await nvim.line |
||||
expect(line).toBe('bar') |
||||
let exists = fs.existsSync(newFile) |
||||
expect(exists).toBe(true) |
||||
}) |
||||
|
||||
it('should overwrite if file exists', async () => { |
||||
let filepath = path.join(os.tmpdir(), uuid()) |
||||
let newPath = path.join(os.tmpdir(), uuid()) |
||||
await workspace.createFile(filepath) |
||||
await workspace.createFile(newPath) |
||||
await workspace.renameFile(filepath, newPath, { overwrite: true }) |
||||
expect(fs.existsSync(newPath)).toBe(true) |
||||
expect(fs.existsSync(filepath)).toBe(false) |
||||
fs.unlinkSync(newPath) |
||||
}) |
||||
|
||||
it('should rename buffer in directory and revert', async () => { |
||||
let folder = path.join(os.tmpdir(), uuid()) |
||||
let newFolder = path.join(os.tmpdir(), uuid()) |
||||
fs.mkdirSync(folder) |
||||
disposables.push(Disposable.create(() => { |
||||
fs.rmSync(folder, { recursive: true, force: true }) |
||||
fs.rmSync(newFolder, { recursive: true, force: true }) |
||||
})) |
||||
let filepath = path.join(folder, 'new_file') |
||||
await workspace.createFile(filepath) |
||||
let bufnr = await nvim.call('bufnr', [filepath]) |
||||
expect(bufnr).toBeGreaterThan(0) |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.renameFile(folder, newFolder, { overwrite: true }, fns) |
||||
bufnr = await nvim.call('bufnr', [path.join(newFolder, 'new_file')]) |
||||
expect(bufnr).toBeGreaterThan(0) |
||||
for (let i = fns.length - 1; i >= 0; i--) { |
||||
await fns[i]() |
||||
} |
||||
bufnr = await nvim.call('bufnr', [filepath]) |
||||
expect(bufnr).toBeGreaterThan(0) |
||||
}) |
||||
}) |
||||
|
||||
describe('loadResource()', () => { |
||||
it('should load file as hidden buffer', async () => { |
||||
helper.updateConfiguration('workspace.openResourceCommand', '') |
||||
let filepath = await createTmpFile('foo') |
||||
let uri = URI.file(filepath).toString() |
||||
let doc = await workspace.files.loadResource(uri) |
||||
let bufnrs = await nvim.call('coc#window#bufnrs') as number[] |
||||
expect(bufnrs.indexOf(doc.bufnr)).toBe(-1) |
||||
}) |
||||
}) |
||||
|
||||
describe('deleteFile()', () => { |
||||
it('should throw when file not exists', async () => { |
||||
let filepath = path.join(__dirname, 'not_exists') |
||||
let fn = async () => { |
||||
await workspace.deleteFile(filepath) |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should ignore when ignoreIfNotExists set', async () => { |
||||
let filepath = path.join(__dirname, 'not_exists') |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.deleteFile(filepath, { ignoreIfNotExists: true }, fns) |
||||
expect(fns.length).toBe(0) |
||||
}) |
||||
|
||||
it('should unload loaded buffer', async () => { |
||||
let filepath = await createTmpFile('file to delete') |
||||
disposables.push(Disposable.create(() => { |
||||
if (fs.existsSync(filepath)) fs.unlinkSync(filepath) |
||||
})) |
||||
await workspace.files.loadResource(URI.file(filepath).toString()) |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.deleteFile(filepath, {}, fns) |
||||
let loaded = await nvim.call('bufloaded', [filepath]) |
||||
expect(loaded).toBe(0) |
||||
for (let i = fns.length - 1; i >= 0; i--) { |
||||
await fns[i]() |
||||
} |
||||
expect(fs.existsSync(filepath)).toBe(true) |
||||
loaded = await nvim.call('bufloaded', [filepath]) |
||||
expect(loaded).toBe(1) |
||||
}) |
||||
|
||||
it('should delete and recover folder', async () => { |
||||
let folder = path.join(os.tmpdir(), uuid()) |
||||
disposables.push(Disposable.create(() => { |
||||
if (fs.existsSync(folder)) fs.rmdirSync(folder) |
||||
})) |
||||
fs.mkdirSync(folder) |
||||
expect(fs.existsSync(folder)).toBe(true) |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.deleteFile(folder, {}, fns) |
||||
expect(fs.existsSync(folder)).toBe(false) |
||||
for (let i = fns.length - 1; i >= 0; i--) { |
||||
await fns[i]() |
||||
} |
||||
expect(fs.existsSync(folder)).toBe(true) |
||||
await workspace.files.deleteFile(folder, {}) |
||||
}) |
||||
|
||||
it('should delete and recover folder recursive', async () => { |
||||
let folder = path.join(os.tmpdir(), uuid()) |
||||
disposables.push(Disposable.create(() => { |
||||
fs.rmSync(folder, { recursive: true, force: true }) |
||||
})) |
||||
fs.mkdirSync(folder) |
||||
fs.writeFileSync(path.join(folder, 'new_file'), '', 'utf8') |
||||
let fns: RecoverFunc[] = [] |
||||
await workspace.files.deleteFile(folder, { recursive: true }, fns) |
||||
expect(fs.existsSync(folder)).toBe(false) |
||||
for (let i = fns.length - 1; i >= 0; i--) { |
||||
await fns[i]() |
||||
} |
||||
expect(fs.existsSync(folder)).toBe(true) |
||||
expect(fs.existsSync(path.join(folder, 'new_file'))).toBe(true) |
||||
await workspace.files.deleteFile(folder, { recursive: true }) |
||||
}) |
||||
|
||||
it('should delete file if exists', async () => { |
||||
let filepath = path.join(__dirname, 'foo') |
||||
await workspace.createFile(filepath) |
||||
expect(fs.existsSync(filepath)).toBe(true) |
||||
await workspace.deleteFile(filepath) |
||||
expect(fs.existsSync(filepath)).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('loadFile()', () => { |
||||
it('should single loadFile', async () => { |
||||
let doc = await helper.createDocument() |
||||
let newFile = URI.file(path.join(__dirname, 'abc')).toString() |
||||
let document = await workspace.loadFile(newFile) |
||||
let bufnr = await nvim.call('bufnr', '%') |
||||
expect(document.uri.endsWith('abc')).toBe(true) |
||||
expect(bufnr).toBe(doc.bufnr) |
||||
}) |
||||
}) |
||||
|
||||
describe('loadFiles', () => { |
||||
it('should loadFiles', async () => { |
||||
let files = ['a', 'b', 'c'].map(key => URI.file(path.join(__dirname, key)).toString()) |
||||
let docs = await workspace.loadFiles(files) |
||||
let uris = docs.map(o => o.uri) |
||||
expect(uris).toEqual(files) |
||||
}) |
||||
|
||||
it('should load empty files array', async () => { |
||||
await workspace.loadFiles([]) |
||||
}) |
||||
}) |
||||
|
||||
describe('openTextDocument()', () => { |
||||
it('should open document already exists', async () => { |
||||
let doc = await helper.createDocument('a') |
||||
await nvim.command('enew') |
||||
await workspace.openTextDocument(URI.parse(doc.uri)) |
||||
let curr = await workspace.document |
||||
expect(curr.uri).toBe(doc.uri) |
||||
}) |
||||
|
||||
it('should throw when file does not exist', async () => { |
||||
let err |
||||
try { |
||||
await workspace.openTextDocument('/a/b/c') |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should open untitled document', async () => { |
||||
let doc = await workspace.openTextDocument(URI.parse(`untitled:///a/b.js`)) |
||||
expect(doc.uri).toBe('file:///a/b.js') |
||||
}) |
||||
|
||||
it('should load file that exists', async () => { |
||||
let doc = await workspace.openTextDocument(URI.file(__filename)) |
||||
expect(URI.parse(doc.uri).fsPath).toBe(__filename) |
||||
let curr = await workspace.document |
||||
expect(curr.uri).toBe(doc.uri) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,120 @@
@@ -0,0 +1,120 @@
|
||||
import os from 'os' |
||||
import fs from 'fs' |
||||
import path from 'path' |
||||
import Configurations from '../../configuration/index' |
||||
import * as funcs from '../../core/funcs' |
||||
import Resolver from '../../model/resolver' |
||||
import which from 'which' |
||||
import { v4 as uuid } from 'uuid' |
||||
let configurations: Configurations |
||||
|
||||
beforeAll(async () => { |
||||
let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json') |
||||
configurations = new Configurations(userConfigFile, undefined) |
||||
}) |
||||
|
||||
describe('Resolver()', () => { |
||||
it('should resolve null', async () => { |
||||
let r = new Resolver() |
||||
let spy = jest.spyOn(which, 'sync').mockImplementation(() => { |
||||
throw new Error('not found') |
||||
}) |
||||
let res = await r.resolveModule('mode') |
||||
expect(res).toBe(null) |
||||
spy.mockRestore() |
||||
}) |
||||
|
||||
it('should resolve npm module', async () => { |
||||
let r = new Resolver() |
||||
let folder = path.join(os.tmpdir(), uuid()) |
||||
Object.assign(r, { |
||||
_npmFolder: folder, |
||||
_yarnFolder: __dirname, |
||||
}) |
||||
fs.mkdirSync(path.join(folder, 'name'), { recursive: true }) |
||||
fs.writeFileSync(path.join(folder, 'name', 'package.json'), '', 'utf8') |
||||
let res = await r.resolveModule('name') |
||||
expect(res).toBe(path.join(folder, 'name')) |
||||
}) |
||||
}) |
||||
|
||||
describe('has()', () => { |
||||
it('should throw for invalid argument', async () => { |
||||
let env = { |
||||
isVim: true, |
||||
version: '8023956' |
||||
} |
||||
let err |
||||
try { |
||||
expect(funcs.has(env, '0.5.0')).toBe(true) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should detect version on vim8', async () => { |
||||
let env = { |
||||
isVim: true, |
||||
version: '8023956' |
||||
} |
||||
expect(funcs.has(env, 'patch-7.4.248')).toBe(true) |
||||
expect(funcs.has(env, 'patch-8.5.1')).toBe(false) |
||||
}) |
||||
|
||||
it('should delete version on neovim', async () => { |
||||
let env = { |
||||
isVim: false, |
||||
version: '0.6.1' |
||||
} |
||||
expect(funcs.has(env, 'nvim-0.5.0')).toBe(true) |
||||
expect(funcs.has(env, 'nvim-0.7.0')).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('createNameSpace()', () => { |
||||
it('should create namespace', async () => { |
||||
let nr = funcs.createNameSpace('ns') |
||||
expect(nr).toBeDefined() |
||||
expect(nr).toBe(funcs.createNameSpace('ns')) |
||||
}) |
||||
}) |
||||
|
||||
describe('getWatchmanPath()', () => { |
||||
it('should get watchman path', async () => { |
||||
let res = funcs.getWatchmanPath(configurations) |
||||
expect(typeof res === 'string' || res == null).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('findUp()', () => { |
||||
it('should return null when can not find ', async () => { |
||||
let nvim: any = { |
||||
call: () => { |
||||
return __filename |
||||
} |
||||
} |
||||
let res = await funcs.findUp(nvim, os.homedir(), ['file_not_exists']) |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should return null when unable find cwd in cwd', async () => { |
||||
let nvim: any = { |
||||
call: () => { |
||||
return '' |
||||
} |
||||
} |
||||
let res = await funcs.findUp(nvim, os.homedir(), ['file_not_exists']) |
||||
expect(res).toBeNull() |
||||
}) |
||||
}) |
||||
|
||||
describe('score()', () => { |
||||
it('should return score', async () => { |
||||
expect(funcs.score(undefined, 'untitled:///1', '')).toBe(0) |
||||
expect(funcs.score({ scheme: '*' }, 'untitled:///1', '')).toBe(3) |
||||
expect(funcs.score('vim', 'untitled:///1', 'vim')).toBe(10) |
||||
expect(funcs.score('*', 'untitled:///1', '')).toBe(5) |
||||
expect(funcs.score('', 'untitled:///1', 'vim')).toBe(0) |
||||
}) |
||||
}) |
@ -0,0 +1,79 @@
@@ -0,0 +1,79 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import workspace from '../../workspace' |
||||
import Keymaps from '../../core/keymaps' |
||||
import helper from '../helper' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
import { disposeAll } from '../../util' |
||||
|
||||
let nvim: Neovim |
||||
let keymaps: Keymaps |
||||
let disposables: Disposable[] = [] |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
keymaps = workspace.keymaps |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('doKeymap()', () => { |
||||
it('should not throw when key not mapped', async () => { |
||||
await keymaps.doKeymap('<C-a>', '', '{C-a}') |
||||
}) |
||||
}) |
||||
|
||||
describe('registerKeymap()', () => { |
||||
it('should throw for invalid key', async () => { |
||||
let err |
||||
try { |
||||
keymaps.registerKeymap(['i'], '', jest.fn()) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should throw for duplicated key', async () => { |
||||
keymaps.registerKeymap(['i'], 'tmp', jest.fn()) |
||||
let err |
||||
try { |
||||
keymaps.registerKeymap(['i'], 'tmp', jest.fn()) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should register insert key mapping', async () => { |
||||
let fn = jest.fn() |
||||
disposables.push(keymaps.registerKeymap(['i'], 'test', fn)) |
||||
await helper.wait(10) |
||||
let res = await nvim.call('execute', ['verbose imap <Plug>(coc-test)']) |
||||
expect(res).toMatch('coc#_insert_key') |
||||
}) |
||||
}) |
||||
|
||||
describe('registerExprKeymap()', () => { |
||||
it('should visual key mapping', async () => { |
||||
await nvim.setLine('foo') |
||||
let called = false |
||||
let fn = () => { |
||||
called = true |
||||
return '' |
||||
} |
||||
disposables.push(keymaps.registerExprKeymap('x', 'x', fn, true)) |
||||
await helper.wait(50) |
||||
await nvim.command('normal! viw') |
||||
await nvim.input('x<esc>') |
||||
await helper.wait(50) |
||||
expect(called).toBe(true) |
||||
}) |
||||
}) |
@ -0,0 +1,148 @@
@@ -0,0 +1,148 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { Location, Range } from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
await nvim.command(`source ${path.join(process.cwd(), 'autoload/coc/ui.vim')}`) |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
function createLocations(): Location[] { |
||||
let uri = URI.file(__filename).toString() |
||||
return [Location.create(uri, Range.create(0, 0, 1, 0)), Location.create(uri, Range.create(2, 0, 3, 0))] |
||||
} |
||||
|
||||
describe('showLocations()', () => { |
||||
it('should show location list by default', async () => { |
||||
let locations = createLocations() |
||||
await workspace.showLocations(locations) |
||||
await helper.waitFor('bufname', ['%'], 'list:///location') |
||||
}) |
||||
|
||||
it('should fire autocmd when location list disabled', async () => { |
||||
Object.assign(workspace.env, { |
||||
locationlist: false |
||||
}) |
||||
await nvim.exec(` |
||||
function OnLocationsChange() |
||||
let g:called = 1 |
||||
endfunction |
||||
autocmd User CocLocationsChange :call OnLocationsChange()`)
|
||||
let locations = createLocations() |
||||
await workspace.showLocations(locations) |
||||
await helper.waitFor('eval', [`get(g:,'called',0)`], 1) |
||||
}) |
||||
|
||||
it('should show quickfix when quickfix enabled', async () => { |
||||
helper.updateConfiguration('coc.preferences.useQuickfixForLocations', true) |
||||
let locations = createLocations() |
||||
await workspace.showLocations(locations) |
||||
await helper.waitFor('eval', [`&buftype`], 'quickfix') |
||||
}) |
||||
|
||||
it('should use customized quickfix open command', async () => { |
||||
await nvim.setVar('coc_quickfix_open_command', 'copen 1') |
||||
helper.updateConfiguration('coc.preferences.useQuickfixForLocations', true) |
||||
let locations = createLocations() |
||||
await workspace.showLocations(locations) |
||||
await helper.waitFor('eval', [`&buftype`], 'quickfix') |
||||
let win = await nvim.window |
||||
let height = await win.height |
||||
expect(height).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('jumpTo()', () => { |
||||
it('should jumpTo position', async () => { |
||||
let uri = URI.file('/tmp/foo').toString() |
||||
await workspace.jumpTo(uri, { line: 1, character: 1 }) |
||||
await nvim.command('setl buftype=nofile') |
||||
let buf = await nvim.buffer |
||||
let name = await buf.name |
||||
expect(name).toMatch('/foo') |
||||
await buf.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
await workspace.jumpTo(uri, { line: 1, character: 1 }) |
||||
let pos = await nvim.call('getcurpos') |
||||
expect(pos.slice(1, 3)).toEqual([2, 2]) |
||||
}) |
||||
|
||||
it('should jumpTo uri without normalize', async () => { |
||||
let uri = 'zipfile:///tmp/clojure-1.9.0.jar::clojure/core.clj' |
||||
await workspace.jumpTo(uri) |
||||
let buf = await nvim.buffer |
||||
let name = await buf.name |
||||
expect(name).toBe(uri) |
||||
}) |
||||
|
||||
it('should jump without position', async () => { |
||||
let uri = URI.file('/tmp/foo').toString() |
||||
await workspace.jumpTo(uri) |
||||
let buf = await nvim.buffer |
||||
let name = await buf.name |
||||
expect(name).toMatch('/foo') |
||||
}) |
||||
|
||||
it('should jumpTo custom uri scheme', async () => { |
||||
let uri = 'jdt://foo' |
||||
await workspace.jumpTo(uri, { line: 1, character: 1 }) |
||||
let buf = await nvim.buffer |
||||
let name = await buf.name |
||||
expect(name).toBe(uri) |
||||
}) |
||||
|
||||
}) |
||||
|
||||
describe('openResource()', () => { |
||||
it('should open resource', async () => { |
||||
let uri = URI.file(path.join(os.tmpdir(), 'bar')).toString() |
||||
await workspace.openResource(uri) |
||||
let buf = await nvim.buffer |
||||
let name = await buf.name |
||||
expect(name).toMatch('bar') |
||||
}) |
||||
|
||||
it('should open none file uri', async () => { |
||||
workspace.registerTextDocumentContentProvider('jd', { |
||||
provideTextDocumentContent: () => 'jd' |
||||
}) |
||||
let uri = 'jd://abc' |
||||
await workspace.openResource(uri) |
||||
let buf = await nvim.buffer |
||||
let name = await buf.name |
||||
expect(name).toBe('jd://abc') |
||||
}) |
||||
|
||||
it('should open opened buffer', async () => { |
||||
let buf = await helper.edit() |
||||
let doc = workspace.getDocument(buf.id) |
||||
await workspace.openResource(doc.uri) |
||||
await helper.wait(30) |
||||
let bufnr = await nvim.call('bufnr', '%') |
||||
expect(bufnr).toBe(buf.id) |
||||
}) |
||||
|
||||
it('should open url', async () => { |
||||
await helper.mockFunction('coc#ui#open_url', 0) |
||||
let buf = await helper.edit() |
||||
let uri = 'http://example.com' |
||||
await workspace.openResource(uri) |
||||
await helper.wait(30) |
||||
let bufnr = await nvim.call('bufnr', '%') |
||||
expect(bufnr).toBe(buf.id) |
||||
}) |
||||
}) |
@ -0,0 +1,145 @@
@@ -0,0 +1,145 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import which from 'which' |
||||
import Terminals from '../../core/terminals' |
||||
import window from '../../window' |
||||
import TerminalModel from '../../model/terminal' |
||||
import helper from '../helper' |
||||
import { v4 as uuid } from 'uuid' |
||||
|
||||
let nvim: Neovim |
||||
let terminals: Terminals |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
terminals = new Terminals() |
||||
}) |
||||
|
||||
afterEach(() => { |
||||
terminals.reset() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('create terminal', () => { |
||||
it('should use cleaned env', async () => { |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash'), |
||||
strictEnv: true |
||||
}) |
||||
await helper.wait(50) |
||||
terminal.sendText(`echo $NODE_ENV`, true) |
||||
await helper.wait(50) |
||||
let buf = nvim.createBuffer(terminal.bufnr) |
||||
let lines = await buf.lines |
||||
expect(lines.includes('test')).toBe(false) |
||||
}) |
||||
|
||||
it('should use custom shell command', async () => { |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash') |
||||
}) |
||||
let bufnr = terminal.bufnr |
||||
let bufname = await nvim.call('bufname', [bufnr]) as string |
||||
expect(bufname.includes('bash')).toBe(true) |
||||
}) |
||||
|
||||
it('should use custom cwd', async () => { |
||||
let basename = path.basename(os.tmpdir()) |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
cwd: os.tmpdir() |
||||
}) |
||||
let bufnr = terminal.bufnr |
||||
let bufname = await nvim.call('bufname', [bufnr]) as string |
||||
expect(bufname.includes(basename)).toBe(true) |
||||
}) |
||||
|
||||
it('should have exit code', async () => { |
||||
let exitStatus |
||||
terminals.onDidCloseTerminal(terminal => { |
||||
exitStatus = terminal.exitStatus |
||||
}) |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash'), |
||||
strictEnv: true |
||||
}) |
||||
terminal.sendText('exit', true) |
||||
await helper.waitFor('bufloaded', [terminal.bufnr], 0) |
||||
await helper.waitValue(() => { |
||||
return exitStatus != null |
||||
}, true) |
||||
expect(exitStatus.code).toBeDefined() |
||||
}) |
||||
|
||||
it('should return false on show when buffer unloaded', async () => { |
||||
let model = new TerminalModel('bash', [], nvim) |
||||
await model.start() |
||||
expect(model.bufnr).toBeDefined() |
||||
await nvim.command(`bd! ${model.bufnr}`) |
||||
let res = await model.show() |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should not throw when show & hide disposed terminal', async () => { |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash') |
||||
}) |
||||
terminal.dispose() |
||||
await terminal.show() |
||||
await terminal.hide() |
||||
}) |
||||
|
||||
it('should show terminal on current window', async () => { |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash') |
||||
}) |
||||
let winid = await nvim.call('bufwinid', [terminal.bufnr]) |
||||
expect(winid).toBeGreaterThan(0) |
||||
await nvim.call('win_gotoid', [winid]) |
||||
await terminal.show() |
||||
}) |
||||
|
||||
it('should show terminal that shown', async () => { |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash') |
||||
}) |
||||
let res = await terminal.show(true) |
||||
expect(res).toBe(true) |
||||
expect(terminal.bufnr).toBeDefined() |
||||
let winid = await nvim.call('bufwinid', [terminal.bufnr]) |
||||
let curr = await nvim.call('win_getid', []) |
||||
expect(winid != curr).toBe(true) |
||||
}) |
||||
|
||||
it('should show hidden terminal', async () => { |
||||
let terminal = await terminals.createTerminal(nvim, { |
||||
name: `test-${uuid()}`, |
||||
shellPath: which.sync('bash') |
||||
}) |
||||
await terminal.hide() |
||||
await terminal.show() |
||||
}) |
||||
|
||||
it('should create terminal', async () => { |
||||
let terminal = await window.createTerminal({ |
||||
name: `test-${uuid()}`, |
||||
}) |
||||
expect(terminal).toBeDefined() |
||||
expect(terminal.processId).toBeDefined() |
||||
expect(terminal.name).toBeDefined() |
||||
terminal.dispose() |
||||
await helper.wait(30) |
||||
expect(terminal.exitStatus).toEqual({ code: undefined }) |
||||
}) |
||||
}) |
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Position, Range } from 'vscode-languageserver-types' |
||||
import * as ui from '../../core/ui' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('getCursorPosition()', () => { |
||||
it('should get cursor position', async () => { |
||||
await nvim.call('cursor', [1, 1]) |
||||
let res = await ui.getCursorPosition(nvim) |
||||
expect(res).toEqual({ |
||||
line: 0, |
||||
character: 0 |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('moveTo()', () => { |
||||
it('should moveTo position', async () => { |
||||
await nvim.setLine('foo') |
||||
await ui.moveTo(nvim, Position.create(0, 1), true) |
||||
let res = await ui.getCursorPosition(nvim) |
||||
expect(res).toEqual({ line: 0, character: 1 }) |
||||
}) |
||||
}) |
||||
|
||||
describe('getCursorScreenPosition()', () => { |
||||
it('should get cursor screen position', async () => { |
||||
let res = await ui.getCursorScreenPosition(nvim) |
||||
expect(res).toBeDefined() |
||||
expect(typeof res.row).toBe('number') |
||||
expect(typeof res.col).toBe('number') |
||||
}) |
||||
}) |
||||
|
||||
describe('createFloatFactory()', () => { |
||||
it('should create FloatFactory', async () => { |
||||
let f = ui.createFloatFactory(nvim, { border: true, autoHide: false }, { close: true }) |
||||
await f.show([{ content: 'shown', filetype: 'txt' }]) |
||||
let activated = await f.activated() |
||||
expect(activated).toBe(true) |
||||
expect(f.window != null).toBe(true) |
||||
let win = await helper.getFloat() |
||||
expect(win).toBeDefined() |
||||
let id = await nvim.call('coc#float#get_related', [win.id, 'border', 0]) as number |
||||
expect(id).toBeGreaterThan(0) |
||||
id = await nvim.call('coc#float#get_related', [win.id, 'close', 0]) as number |
||||
expect(id).toBeGreaterThan(0) |
||||
await f.show([{ content: 'shown', filetype: 'txt' }], { offsetX: 10 }) |
||||
let curr = await helper.getFloat() |
||||
expect(curr.id).toBe(win.id) |
||||
}) |
||||
}) |
||||
|
||||
describe('showMessage()', () => { |
||||
it('should showMessage on vim', async () => { |
||||
ui.showMessage(nvim, 'my message', 'MoreMsg', true) |
||||
await helper.wait(100) |
||||
let cmdline = await helper.getCmdline() |
||||
expect(cmdline).toMatch(/my message/) |
||||
}) |
||||
}) |
||||
|
||||
describe('getSelection()', () => { |
||||
it('should return null when no selection exists', async () => { |
||||
let res = await ui.getSelection(nvim, 'v') |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should return range for line selection', async () => { |
||||
await nvim.setLine('foo') |
||||
await nvim.input('V') |
||||
await nvim.input('<esc>') |
||||
let res = await ui.getSelection(nvim, 'V') |
||||
expect(res).toEqual({ start: { line: 0, character: 0 }, end: { line: 1, character: 0 } }) |
||||
}) |
||||
|
||||
it('should return range of current line', async () => { |
||||
await nvim.command('normal! gg') |
||||
let res = await ui.getSelection(nvim, 'currline') |
||||
expect(res).toEqual(Range.create(0, 0, 1, 0)) |
||||
}) |
||||
}) |
||||
|
||||
describe('selectRange()', () => { |
||||
it('should select range #1', async () => { |
||||
await nvim.call('setline', [1, ['foo', 'b']]) |
||||
await nvim.command('set selection=inclusive') |
||||
await nvim.command('set virtualedit=onemore') |
||||
await ui.selectRange(nvim, Range.create(0, 0, 1, 1), true) |
||||
await nvim.input('<esc>') |
||||
let res = await ui.getSelection(nvim, 'v') |
||||
expect(res).toEqual(Range.create(0, 0, 1, 1)) |
||||
}) |
||||
|
||||
it('should select range #2', async () => { |
||||
await nvim.call('setline', [1, ['foo', 'b']]) |
||||
await ui.selectRange(nvim, Range.create(0, 0, 1, 0), true) |
||||
await nvim.input('<esc>') |
||||
let res = await ui.getSelection(nvim, 'v') |
||||
expect(res).toEqual(Range.create(0, 0, 0, 3)) |
||||
}) |
||||
}) |
@ -0,0 +1,315 @@
@@ -0,0 +1,315 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import fs from 'fs' |
||||
import os from 'os' |
||||
import path from 'path' |
||||
import { Disposable, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode-languageserver-protocol' |
||||
import { URI } from 'vscode-uri' |
||||
import Configurations from '../../configuration/index' |
||||
import WorkspaceFolderController from '../../core/workspaceFolder' |
||||
import { PatternType } from '../../types' |
||||
import { disposeAll } from '../../util' |
||||
import { CancellationError } from '../../util/errors' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let workspaceFolder: WorkspaceFolderController |
||||
let configurations: Configurations |
||||
let disposables: Disposable[] = [] |
||||
let nvim: Neovim |
||||
|
||||
function updateConfiguration(key: string, value: any, defaults: any): void { |
||||
configurations.updateMemoryConfig({ [key]: value }) |
||||
disposables.push({ |
||||
dispose: () => { |
||||
configurations.updateMemoryConfig({ [key]: defaults }) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
let userConfigFile = path.join(process.env.COC_VIMCONFIG, 'coc-settings.json') |
||||
configurations = new Configurations(userConfigFile, undefined) |
||||
workspaceFolder = new WorkspaceFolderController(configurations) |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
disposeAll(disposables) |
||||
workspaceFolder.reset() |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('WorkspaceFolderController', () => { |
||||
describe('asRelativePath()', () => { |
||||
function assertAsRelativePath(input: string, expected: string, includeWorkspace?: boolean) { |
||||
const actual = workspaceFolder.getRelativePath(input, includeWorkspace) |
||||
expect(actual).toBe(expected) |
||||
} |
||||
|
||||
it('should get relative path', async () => { |
||||
workspaceFolder.addWorkspaceFolder(`/Coding/Applications/NewsWoWBot`, false) |
||||
assertAsRelativePath('/Coding/Applications/NewsWoWBot/bernd/das/brot', 'bernd/das/brot') |
||||
assertAsRelativePath('/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart', |
||||
'/Apps/DartPubCache/hosted/pub.dartlang.org/convert-2.0.1/lib/src/hex.dart') |
||||
assertAsRelativePath('', '') |
||||
assertAsRelativePath('/foo/bar', '/foo/bar') |
||||
assertAsRelativePath('in/out', 'in/out') |
||||
}) |
||||
|
||||
it('should asRelativePath, same paths, #11402', async () => { |
||||
const root = '/home/aeschli/workspaces/samples/docker' |
||||
const input = '/home/aeschli/workspaces/samples/docker' |
||||
workspaceFolder.addWorkspaceFolder(root, false) |
||||
assertAsRelativePath(input, input) |
||||
const input2 = '/home/aeschli/workspaces/samples/docker/a.file' |
||||
assertAsRelativePath(input2, 'a.file') |
||||
}) |
||||
|
||||
it('should asRelativePath, not workspaceFolder', async () => { |
||||
expect(workspace.getRelativePath('')).toBe('') |
||||
assertAsRelativePath('/foo/bar', '/foo/bar') |
||||
}) |
||||
|
||||
it('should asRelativePath, multiple folders', () => { |
||||
workspaceFolder.addWorkspaceFolder(`/Coding/One`, false) |
||||
workspaceFolder.addWorkspaceFolder(`/Coding/Two`, false) |
||||
assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt') |
||||
assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt') |
||||
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt') |
||||
}) |
||||
|
||||
it('should slightly inconsistent behaviour of asRelativePath and getWorkspaceFolder, #31553', async () => { |
||||
workspaceFolder.addWorkspaceFolder(`/Coding/One`, false) |
||||
workspaceFolder.addWorkspaceFolder(`/Coding/Two`, false) |
||||
|
||||
assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt') |
||||
assertAsRelativePath('/Coding/One/file.txt', 'One/file.txt', true) |
||||
assertAsRelativePath('/Coding/One/file.txt', 'file.txt', false) |
||||
assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt') |
||||
assertAsRelativePath('/Coding/Two/files/out.txt', 'Two/files/out.txt', true) |
||||
assertAsRelativePath('/Coding/Two/files/out.txt', 'files/out.txt', false) |
||||
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt') |
||||
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', true) |
||||
assertAsRelativePath('/Coding/Two2/files/out.txt', '/Coding/Two2/files/out.txt', false) |
||||
}) |
||||
}) |
||||
|
||||
describe('setWorkspaceFolders()', () => { |
||||
it('should set valid folders', async () => { |
||||
workspaceFolder.setWorkspaceFolders([os.tmpdir(), '/a/not_exists']) |
||||
let folders = workspaceFolder.workspaceFolders |
||||
expect(folders.length).toBe(2) |
||||
}) |
||||
}) |
||||
|
||||
describe('getWorkspaceFolder()', () => { |
||||
it('should get workspaceFolder by uri', async () => { |
||||
let res = workspaceFolder.getWorkspaceFolder(URI.parse('untitled://1')) |
||||
expect(res).toBeUndefined() |
||||
res = workspaceFolder.getWorkspaceFolder(URI.file('/a/b')) |
||||
expect(res).toBeUndefined() |
||||
let filepath = path.join(process.cwd(), 'a/b') |
||||
workspaceFolder.setWorkspaceFolders([process.cwd()]) |
||||
res = workspaceFolder.getWorkspaceFolder(URI.file(filepath)) |
||||
expect(URI.parse(res.uri).fsPath).toBe(process.cwd()) |
||||
}) |
||||
}) |
||||
|
||||
describe('getRootPatterns()', () => { |
||||
it('should get patterns from b:coc_root_patterns', async () => { |
||||
await nvim.command('edit t.vim | let b:coc_root_patterns=["foo"]') |
||||
await nvim.command('setf vim') |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.getRootPatterns(doc, PatternType.Buffer) |
||||
expect(res).toEqual(['foo']) |
||||
}) |
||||
|
||||
it('should get patterns from languageserver', async () => { |
||||
updateConfiguration('languageserver', { |
||||
test: { |
||||
filetypes: ['vim'], |
||||
rootPatterns: ['bar'] |
||||
} |
||||
}, {}) |
||||
workspaceFolder.addRootPattern('vim', ['foo']) |
||||
await nvim.command('edit t.vim') |
||||
await nvim.command('setf vim') |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.getRootPatterns(doc, PatternType.LanguageServer) |
||||
expect(res).toEqual(['bar', 'foo']) |
||||
}) |
||||
|
||||
it('should get patterns from user configuration', async () => { |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.getRootPatterns(doc, PatternType.Global) |
||||
expect(res.includes('.git')).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('resolveRoot()', () => { |
||||
const cwd = process.cwd() |
||||
const expand = (input: string) => { |
||||
return workspace.expand(input) |
||||
} |
||||
|
||||
it('should resolve to cwd for file in cwd', async () => { |
||||
updateConfiguration('coc.preferences.rootPatterns', [], ['.git', '.hg', '.projections.json']) |
||||
let file = path.join(os.tmpdir(), 'foo') |
||||
await nvim.command(`edit ${file}`) |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand) |
||||
expect(res).toBe(os.tmpdir()) |
||||
}) |
||||
|
||||
it('should not fallback to cwd as workspace folder', async () => { |
||||
updateConfiguration('coc.preferences.rootPatterns', [], ['.git', '.hg', '.projections.json']) |
||||
updateConfiguration('workspace.workspaceFolderFallbackCwd', false, true) |
||||
let file = path.join(os.tmpdir(), 'foo') |
||||
await nvim.command(`edit ${file}`) |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, os.tmpdir(), false, expand) |
||||
expect(res).toBe(null) |
||||
}) |
||||
|
||||
it('should return null for untitled buffer', async () => { |
||||
await nvim.command('enew') |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, cwd, false, expand) |
||||
expect(res).toBe(null) |
||||
}) |
||||
|
||||
it('should respect ignored filetypes', async () => { |
||||
updateConfiguration('workspace.ignoredFiletypes', ['vim'], []) |
||||
await nvim.command('edit t.vim') |
||||
await nvim.command('setf vim') |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, cwd, false, expand) |
||||
expect(res).toBe(null) |
||||
}) |
||||
|
||||
it('should respect workspaceFolderCheckCwd', async () => { |
||||
let called = 0 |
||||
disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(() => { |
||||
called++ |
||||
})) |
||||
workspaceFolder.addRootPattern('vim', ['.vim']) |
||||
await nvim.command('edit a/.vim/t.vim') |
||||
await nvim.command('setf vim') |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, cwd, true, expand) |
||||
expect(res).toBe(process.cwd()) |
||||
await nvim.command('edit a/foo') |
||||
doc = await workspace.document |
||||
res = workspaceFolder.resolveRoot(doc, cwd, true, expand) |
||||
expect(res).toBe(process.cwd()) |
||||
expect(called).toBe(1) |
||||
}) |
||||
|
||||
it('should respect ignored folders', async () => { |
||||
updateConfiguration('workspace.ignoredFolders', ['$HOME/foo', '$HOME'], []) |
||||
let file = path.join(os.homedir(), '.vim/bar') |
||||
workspaceFolder.addRootPattern('vim', ['.vim']) |
||||
await nvim.command(`edit ${file}`) |
||||
await nvim.command('setf vim') |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, path.join(os.homedir(), 'foo'), true, expand) |
||||
expect(res).toBe(null) |
||||
}) |
||||
|
||||
describe('bottomUpFileTypes', () => { |
||||
it('should respect specific filetype', async () => { |
||||
updateConfiguration('coc.preferences.rootPatterns', ['.vim'], ['.git', '.hg', '.projections.json']) |
||||
updateConfiguration('workspace.bottomUpFiletypes', ['vim'], []) |
||||
let root = path.join(os.tmpdir(), 'a') |
||||
let dir = path.join(root, '.vim') |
||||
if (!fs.existsSync(dir)) { |
||||
fs.mkdirSync(dir, { recursive: true }) |
||||
} |
||||
let file = path.join(dir, 'foo.vim') |
||||
await nvim.command(`edit ${file}`) |
||||
let doc = await workspace.document |
||||
expect(doc.filetype).toBe('vim') |
||||
let res = workspaceFolder.resolveRoot(doc, file, true, expand) |
||||
expect(res).toBe(root) |
||||
}) |
||||
|
||||
it('should respect wildcard', async () => { |
||||
updateConfiguration('coc.preferences.rootPatterns', ['.vim'], ['.git', '.hg', '.projections.json']) |
||||
updateConfiguration('workspace.bottomUpFiletypes', ['*'], []) |
||||
let root = path.join(os.tmpdir(), 'a') |
||||
let dir = path.join(root, '.vim') |
||||
if (!fs.existsSync(dir)) { |
||||
fs.mkdirSync(dir, { recursive: true }) |
||||
await helper.wait(30) |
||||
} |
||||
let file = path.join(dir, 'foo') |
||||
await nvim.command(`edit ${file}`) |
||||
let doc = await workspace.document |
||||
let res = workspaceFolder.resolveRoot(doc, file, true, expand) |
||||
expect(res).toBe(root) |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('renameWorkspaceFolder()', () => { |
||||
it('should rename workspaceFolder', async () => { |
||||
let e: WorkspaceFoldersChangeEvent |
||||
disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(ev => { |
||||
e = ev |
||||
})) |
||||
let cwd = process.cwd() |
||||
workspaceFolder.addWorkspaceFolder(cwd, false) |
||||
workspaceFolder.addWorkspaceFolder(cwd, false) |
||||
workspaceFolder.renameWorkspaceFolder(cwd, path.join(cwd, '.vim')) |
||||
expect(e.removed.length).toBe(1) |
||||
expect(e.added.length).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('removeWorkspaceFolder()', () => { |
||||
it('should remote workspaceFolder', async () => { |
||||
let e: WorkspaceFoldersChangeEvent |
||||
disposables.push(workspaceFolder.onDidChangeWorkspaceFolders(ev => { |
||||
e = ev |
||||
})) |
||||
let cwd = process.cwd() |
||||
workspaceFolder.addWorkspaceFolder(cwd, false) |
||||
workspaceFolder.removeWorkspaceFolder(cwd) |
||||
workspaceFolder.removeWorkspaceFolder('/a/b') |
||||
expect(e.removed.length).toBe(1) |
||||
expect(e.added.length).toBe(0) |
||||
}) |
||||
}) |
||||
|
||||
describe('checkPatterns()', () => { |
||||
it('should check if pattern exists', async () => { |
||||
expect(await workspaceFolder.checkPatterns([], ['p'])).toBe(false) |
||||
let folder: WorkspaceFolder = { name: '', uri: URI.file(process.cwd()).toString() } |
||||
let res = await workspaceFolder.checkPatterns([folder], ['package.json', '**/not_exists']) |
||||
expect(res).toBe(true) |
||||
res = await workspaceFolder.checkPatterns([folder], ['**/not_exists']) |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should not throw on timeout', async () => { |
||||
let spy = jest.spyOn(workspaceFolder, 'checkFolder').mockImplementation((_dir, _patterns, token) => { |
||||
return new Promise((resolve, reject) => { |
||||
let timer = setTimeout(resolve, 200) |
||||
token.onCancellationRequested(() => { |
||||
clearTimeout(timer) |
||||
reject(new CancellationError()) |
||||
}) |
||||
}) |
||||
}) |
||||
let folder: WorkspaceFolder = { name: '', uri: URI.file(process.cwd()).toString() } |
||||
let res = await workspaceFolder.checkPatterns([folder], ['**/schema.json']) |
||||
spy.mockRestore() |
||||
expect(res).toBe(false) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,401 @@
@@ -0,0 +1,401 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable, CallHierarchyItem, SymbolKind, Range, SymbolTag, CancellationToken, Position } from 'vscode-languageserver-protocol' |
||||
import CallHierarchyHandler from '../../handler/callHierarchy' |
||||
import languages from '../../languages' |
||||
import workspace from '../../workspace' |
||||
import { disposeAll } from '../../util' |
||||
import { URI } from 'vscode-uri' |
||||
import helper, { createTmpFile } from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let callHierarchy: CallHierarchyHandler |
||||
let disposables: Disposable[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
callHierarchy = helper.plugin.getHandler().callHierarchy |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
function createCallItem(name: string, kind: SymbolKind, uri: string, range: Range): CallHierarchyItem { |
||||
return { |
||||
name, |
||||
kind, |
||||
uri, |
||||
range, |
||||
selectionRange: range |
||||
} |
||||
} |
||||
|
||||
describe('CallHierarchy', () => { |
||||
it('should throw when provider does not exist', async () => { |
||||
let err |
||||
try { |
||||
await callHierarchy.getIncoming() |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should return null when provider not exist', async () => { |
||||
let token = CancellationToken.None |
||||
let doc = await workspace.document |
||||
let res: any |
||||
res = await languages.prepareCallHierarchy(doc.textDocument, Position.create(0, 0), token) |
||||
expect(res).toBeNull() |
||||
let item = createCallItem('name', SymbolKind.Class, doc.uri, Range.create(0, 0, 1, 0)) |
||||
res = await languages.provideOutgoingCalls(doc.textDocument, item, token) |
||||
expect(res).toBeNull() |
||||
res = await languages.provideIncomingCalls(doc.textDocument, item, token) |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should throw when prepare failed', async () => { |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return undefined |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
return [] |
||||
} |
||||
})) |
||||
let fn = async () => { |
||||
await callHierarchy.getOutgoing() |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should get incoming & outgoing callHierarchy items', async () => { |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, 'test:///foo', Range.create(0, 0, 0, 5)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [{ |
||||
from: createCallItem('bar', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), |
||||
fromRanges: [Range.create(0, 0, 0, 5)] |
||||
}] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
return [{ |
||||
to: createCallItem('bar', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), |
||||
fromRanges: [Range.create(1, 0, 1, 5)] |
||||
}] |
||||
} |
||||
})) |
||||
let res = await callHierarchy.getIncoming() |
||||
expect(res.length).toBe(1) |
||||
expect(res[0].from.name).toBe('bar') |
||||
let outgoing = await callHierarchy.getOutgoing() |
||||
expect(outgoing.length).toBe(1) |
||||
res = await callHierarchy.getIncoming(outgoing[0].to) |
||||
expect(res.length).toBe(1) |
||||
}) |
||||
|
||||
it('should show warning when provider does not exist', async () => { |
||||
await callHierarchy.showCallHierarchyTree('incoming') |
||||
let line = await helper.getCmdline() |
||||
expect(line).toMatch('not found') |
||||
}) |
||||
|
||||
it('should show message when no result returned.', async () => { |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return null |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
return [] |
||||
} |
||||
})) |
||||
await callHierarchy.showCallHierarchyTree('incoming') |
||||
let line = await helper.getCmdline() |
||||
expect(line).toMatch('Unable') |
||||
}) |
||||
|
||||
it('should render description and support default action', async () => { |
||||
helper.updateConfiguration('callHierarchy.enableTooltip', false) |
||||
let doc = await workspace.document |
||||
let bufnr = doc.bufnr |
||||
await doc.buffer.setLines(['foo'], { start: 0, end: -1, strictIndexing: false }) |
||||
let fsPath = await createTmpFile('foo\nbar\ncontent\n') |
||||
let uri = URI.file(fsPath).toString() |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(1, 0, 1, 3)) |
||||
item.detail = 'Detail' |
||||
item.tags = [SymbolTag.Deprecated] |
||||
return [{ |
||||
from: item, |
||||
fromRanges: [Range.create(2, 0, 2, 5)] |
||||
}] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
return [] |
||||
} |
||||
})) |
||||
await callHierarchy.showCallHierarchyTree('incoming') |
||||
let buf = await nvim.buffer |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'INCOMING CALLS', |
||||
'- c foo', |
||||
' + c bar Detail' |
||||
]) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('t') |
||||
await helper.waitFor('getline', ['.'], ' - c bar Detail') |
||||
await nvim.input('<cr>') |
||||
await helper.waitFor('expand', ['%:p'], fsPath) |
||||
let res = await nvim.call('coc#cursor#position') |
||||
expect(res).toEqual([1, 0]) |
||||
let matches = await nvim.call('getmatches') as any[] |
||||
expect(matches.length).toBe(2) |
||||
await nvim.command(`b ${bufnr}`) |
||||
await helper.wait(50) |
||||
matches = await nvim.call('getmatches') |
||||
expect(matches.length).toBe(0) |
||||
await nvim.command(`wincmd o`) |
||||
}) |
||||
|
||||
it('should invoke open in new tab action', async () => { |
||||
let doc = await workspace.document |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let fsPath = await createTmpFile('foo\nbar\ncontent\n') |
||||
let uri = URI.file(fsPath).toString() |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) |
||||
item.detail = 'Detail' |
||||
return [{ |
||||
to: item, |
||||
fromRanges: [Range.create(1, 0, 1, 3)] |
||||
}] |
||||
} |
||||
})) |
||||
let win = await nvim.window |
||||
await callHierarchy.showCallHierarchyTree('outgoing') |
||||
let buf = await nvim.buffer |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c foo', |
||||
' + c bar Detail' |
||||
]) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('<tab>') |
||||
await helper.waitPrompt() |
||||
await nvim.input('<cr>') |
||||
await helper.waitFor('tabpagenr', [], 2) |
||||
doc = await workspace.document |
||||
expect(doc.uri).toBe(uri) |
||||
await helper.waitValue(async () => { |
||||
let res = await nvim.call('getmatches', [win.id]) |
||||
return res.length |
||||
}, 1) |
||||
}) |
||||
|
||||
it('should invoke show incoming calls action', async () => { |
||||
let doc = await workspace.document |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let fsPath = await createTmpFile('foo\nbar\ncontent\n') |
||||
let uri = URI.file(fsPath).toString() |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [{ |
||||
from: createCallItem('test', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), |
||||
fromRanges: [Range.create(0, 0, 0, 5)] |
||||
}] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) |
||||
item.detail = 'Detail' |
||||
return [{ |
||||
to: item, |
||||
fromRanges: [Range.create(1, 0, 1, 3)] |
||||
}] |
||||
} |
||||
})) |
||||
await callHierarchy.showCallHierarchyTree('outgoing') |
||||
let buf = await nvim.buffer |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c foo', |
||||
' + c bar Detail' |
||||
]) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('<tab>') |
||||
await helper.waitPrompt() |
||||
await nvim.input('3') |
||||
await helper.wait(200) |
||||
lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'INCOMING CALLS', |
||||
'- c bar Detail', |
||||
' + c test' |
||||
]) |
||||
}) |
||||
|
||||
it('should invoke show outgoing calls action', async () => { |
||||
let doc = await workspace.document |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let fsPath = await createTmpFile('foo\nbar\ncontent\n') |
||||
let uri = URI.file(fsPath).toString() |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [{ |
||||
from: createCallItem('test', SymbolKind.Class, 'test:///bar', Range.create(1, 0, 1, 5)), |
||||
fromRanges: [Range.create(0, 0, 0, 5)] |
||||
}] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) |
||||
item.detail = 'Detail' |
||||
return [{ |
||||
to: item, |
||||
fromRanges: [Range.create(1, 0, 1, 3)] |
||||
}] |
||||
} |
||||
})) |
||||
await callHierarchy.showCallHierarchyTree('incoming') |
||||
let buf = await nvim.buffer |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'INCOMING CALLS', |
||||
'- c foo', |
||||
' + c test' |
||||
]) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('<tab>') |
||||
await helper.waitPrompt() |
||||
await nvim.input('4') |
||||
await helper.wait(200) |
||||
lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c test', |
||||
' + c bar Detail' |
||||
]) |
||||
}) |
||||
|
||||
it('should invoke dismiss action #1', async () => { |
||||
let doc = await workspace.document |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let fsPath = await createTmpFile('foo\nbar\ncontent\n') |
||||
let uri = URI.file(fsPath).toString() |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) |
||||
item.detail = 'Detail' |
||||
return [{ |
||||
to: item, |
||||
fromRanges: [Range.create(1, 0, 1, 3)] |
||||
}] |
||||
} |
||||
})) |
||||
await callHierarchy.showCallHierarchyTree('outgoing') |
||||
let buf = await nvim.buffer |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c foo', |
||||
' + c bar Detail' |
||||
]) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('<tab>') |
||||
await helper.waitPrompt() |
||||
await nvim.input('2') |
||||
await helper.wait(200) |
||||
lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c foo' |
||||
]) |
||||
await nvim.command('exe 2') |
||||
await nvim.input('<tab>') |
||||
await helper.waitPrompt() |
||||
await nvim.input('2') |
||||
await helper.wait(30) |
||||
}) |
||||
|
||||
it('should invoke dismiss action #2', async () => { |
||||
let doc = await workspace.document |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let fsPath = await createTmpFile('foo\nbar\ncontent\n') |
||||
let uri = URI.file(fsPath).toString() |
||||
disposables.push(languages.registerCallHierarchyProvider([{ language: '*' }], { |
||||
prepareCallHierarchy() { |
||||
return createCallItem('foo', SymbolKind.Class, doc.uri, Range.create(0, 0, 0, 3)) |
||||
}, |
||||
provideCallHierarchyIncomingCalls() { |
||||
return [] |
||||
}, |
||||
provideCallHierarchyOutgoingCalls() { |
||||
let item = createCallItem('bar', SymbolKind.Class, uri, Range.create(0, 0, 0, 1)) |
||||
item.detail = 'Detail' |
||||
return [{ |
||||
to: item, |
||||
fromRanges: [Range.create(1, 0, 1, 3)] |
||||
}] |
||||
} |
||||
})) |
||||
await callHierarchy.showCallHierarchyTree('outgoing') |
||||
let buf = await nvim.buffer |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c foo', |
||||
' + c bar Detail' |
||||
]) |
||||
await nvim.command('exe 3') |
||||
await nvim.input('t') |
||||
await helper.waitFor('line', ['$'], 4) |
||||
await nvim.command('exe 4') |
||||
await nvim.input('<tab>') |
||||
await helper.waitPrompt() |
||||
await nvim.input('2') |
||||
await helper.waitFor('line', ['$'], 3) |
||||
lines = await buf.lines |
||||
expect(lines).toEqual([ |
||||
'OUTGOING CALLS', |
||||
'- c foo', |
||||
' - c bar Detail' |
||||
]) |
||||
}) |
||||
}) |
@ -0,0 +1,449 @@
@@ -0,0 +1,449 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationToken, CodeAction, Command, CodeActionContext, CodeActionKind, TextEdit, Disposable, Range, Position } from 'vscode-languageserver-protocol' |
||||
import { TextDocument } from 'vscode-languageserver-textdocument' |
||||
import commands from '../../commands' |
||||
import ActionsHandler from '../../handler/codeActions' |
||||
import languages from '../../languages' |
||||
import { ProviderResult } from '../../provider' |
||||
import { disposeAll } from '../../util' |
||||
import { rangeInRange } from '../../util/position' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
let codeActions: ActionsHandler |
||||
let currActions: CodeAction[] |
||||
let resolvedAction: CodeAction |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
codeActions = helper.plugin.getHandler().codeActions |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: ( |
||||
_document: TextDocument, |
||||
_range: Range, |
||||
_context: CodeActionContext, |
||||
_token: CancellationToken |
||||
) => currActions, |
||||
resolveCodeAction: ( |
||||
_action: CodeAction, |
||||
_token: CancellationToken |
||||
): ProviderResult<CodeAction> => resolvedAction |
||||
}, undefined)) |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('handler codeActions', () => { |
||||
describe('organizeImport', () => { |
||||
it('should throw error when organize import action not found', async () => { |
||||
currActions = [] |
||||
await helper.createDocument() |
||||
let err |
||||
try { |
||||
await codeActions.organizeImport() |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should perform organize import action', async () => { |
||||
let doc = await helper.createDocument() |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar')) |
||||
edits.push(TextEdit.replace(Range.create(1, 0, 1, 3), 'foo')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('organize import', edit, CodeActionKind.SourceOrganizeImports) |
||||
currActions = [action, CodeAction.create('another action')] |
||||
await codeActions.organizeImport() |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar', 'foo']) |
||||
}) |
||||
|
||||
it('should register editor.action.organizeImport command', async () => { |
||||
let doc = await helper.createDocument() |
||||
await doc.buffer.setLines(['foo', 'bar'], { start: 0, end: -1, strictIndexing: false }) |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.replace(Range.create(0, 0, 0, 3), 'bar')) |
||||
edits.push(TextEdit.replace(Range.create(1, 0, 1, 3), 'foo')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('organize import', edit, CodeActionKind.SourceOrganizeImports) |
||||
currActions = [action, CodeAction.create('another action')] |
||||
await commands.executeCommand('editor.action.organizeImport') |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar', 'foo']) |
||||
}) |
||||
}) |
||||
|
||||
describe('codeActionRange', () => { |
||||
it('should show warning when no action available', async () => { |
||||
await helper.createDocument() |
||||
currActions = [] |
||||
await codeActions.codeActionRange(1, 2, CodeActionKind.QuickFix) |
||||
let line = await helper.getCmdline() |
||||
expect(line).toMatch(/No quickfix code action/) |
||||
}) |
||||
|
||||
it('should apply chosen action', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) |
||||
currActions = [action] |
||||
let p = codeActions.codeActionRange(1, 2, CodeActionKind.QuickFix) |
||||
await helper.waitPrompt() |
||||
await nvim.input('<CR>') |
||||
await p |
||||
let buf = nvim.createBuffer(doc.bufnr) |
||||
let lines = await buf.lines |
||||
expect(lines[0]).toBe('bar') |
||||
}) |
||||
}) |
||||
|
||||
describe('getCodeActions', () => { |
||||
it('should get empty actions', async () => { |
||||
currActions = [] |
||||
let doc = await helper.createDocument() |
||||
let res = await codeActions.getCodeActions(doc) |
||||
expect(res.length).toBe(0) |
||||
}) |
||||
|
||||
it('should not filter disabled actions', async () => { |
||||
currActions = [] |
||||
let action = CodeAction.create('foo', CodeActionKind.QuickFix) |
||||
action.disabled = { reason: 'disabled' } |
||||
currActions.push(action) |
||||
action = CodeAction.create('bar', CodeActionKind.QuickFix) |
||||
action.disabled = { reason: 'disabled' } |
||||
currActions.push(action) |
||||
let doc = await helper.createDocument() |
||||
let res = await codeActions.getCodeActions(doc) |
||||
expect(res.length).toBe(2) |
||||
}) |
||||
|
||||
it('should get all actions', async () => { |
||||
let doc = await helper.createDocument() |
||||
await doc.buffer.setLines(['', '', ''], { start: 0, end: -1, strictIndexing: false }) |
||||
let action = CodeAction.create('curr action', CodeActionKind.Empty) |
||||
currActions = [action] |
||||
let range: Range |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: ( |
||||
_document: TextDocument, |
||||
r: Range, |
||||
_context: CodeActionContext, _token: CancellationToken |
||||
) => { |
||||
range = r |
||||
return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')] |
||||
}, |
||||
}, undefined)) |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: () => { |
||||
return [CodeAction.create('a')] |
||||
}, |
||||
}, undefined)) |
||||
let res = await codeActions.getCodeActions(doc) |
||||
expect(range).toEqual(Range.create(0, 0, 3, 0)) |
||||
expect(res.length).toBe(4) |
||||
}) |
||||
|
||||
it('should filter actions by range', async () => { |
||||
let doc = await helper.createDocument() |
||||
await doc.buffer.setLines(['', '', ''], { start: 0, end: -1, strictIndexing: false }) |
||||
currActions = [] |
||||
let range: Range |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: ( |
||||
_document: TextDocument, |
||||
r: Range, |
||||
_context: CodeActionContext, _token: CancellationToken |
||||
) => { |
||||
range = r |
||||
if (rangeInRange(r, Range.create(0, 0, 1, 0))) return [CodeAction.create('a')] |
||||
return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')] |
||||
}, |
||||
}, undefined)) |
||||
let res = await codeActions.getCodeActions(doc, Range.create(0, 0, 0, 0)) |
||||
expect(range).toEqual(Range.create(0, 0, 0, 0)) |
||||
expect(res.length).toBe(1) |
||||
}) |
||||
|
||||
it('should filter actions by kind prefix', async () => { |
||||
let doc = await helper.createDocument() |
||||
let action = CodeAction.create('my action', CodeActionKind.SourceFixAll) |
||||
currActions = [action] |
||||
let res = await codeActions.getCodeActions(doc, undefined, [CodeActionKind.Source]) |
||||
expect(res.length).toBe(1) |
||||
expect(res[0].kind).toBe(CodeActionKind.SourceFixAll) |
||||
}) |
||||
}) |
||||
|
||||
describe('getCurrentCodeActions', () => { |
||||
let range: Range |
||||
beforeEach(() => { |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: ( |
||||
_document: TextDocument, |
||||
r: Range, |
||||
_context: CodeActionContext, _token: CancellationToken |
||||
) => { |
||||
range = r |
||||
return [CodeAction.create('a'), CodeAction.create('b'), CodeAction.create('c')] |
||||
}, |
||||
}, undefined)) |
||||
}) |
||||
|
||||
it('should get codeActions by line', async () => { |
||||
currActions = [] |
||||
await helper.createDocument() |
||||
let res = await codeActions.getCurrentCodeActions('line') |
||||
expect(range).toEqual(Range.create(0, 0, 1, 0)) |
||||
expect(res.length).toBe(3) |
||||
}) |
||||
|
||||
it('should get codeActions by cursor', async () => { |
||||
currActions = [] |
||||
await helper.createDocument() |
||||
let res = await codeActions.getCurrentCodeActions('cursor') |
||||
expect(range).toEqual(Range.create(0, 0, 0, 0)) |
||||
expect(res.length).toBe(3) |
||||
}) |
||||
|
||||
it('should get codeActions by visual mode', async () => { |
||||
currActions = [] |
||||
await helper.createDocument() |
||||
await nvim.setLine('foo') |
||||
await nvim.command('normal! 0v$') |
||||
await nvim.input('<esc>') |
||||
let res = await codeActions.getCurrentCodeActions('v') |
||||
expect(range).toEqual(Range.create(0, 0, 0, 3)) |
||||
expect(res.length).toBe(3) |
||||
}) |
||||
}) |
||||
|
||||
describe('doCodeAction', () => { |
||||
it('should not throw when no action exists', async () => { |
||||
currActions = [] |
||||
await helper.createDocument() |
||||
let err |
||||
try { |
||||
await codeActions.doCodeAction(undefined) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeUndefined() |
||||
}) |
||||
|
||||
it('should apply single code action when only is title', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) |
||||
currActions = [action] |
||||
await codeActions.doCodeAction(undefined, 'code fix') |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar']) |
||||
}) |
||||
|
||||
it('should apply single code action when only is codeAction array', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) |
||||
currActions = [action] |
||||
await codeActions.doCodeAction(undefined, [CodeActionKind.QuickFix]) |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar']) |
||||
}) |
||||
|
||||
it('should show disabled code action', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let refactorAction = CodeAction.create('code refactor', edit, CodeActionKind.Refactor) |
||||
refactorAction.disabled = { reason: 'invalid position' } |
||||
let fixAction = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) |
||||
currActions = [refactorAction, fixAction] |
||||
let p = codeActions.doCodeAction(undefined) |
||||
let winid = await helper.waitFloat() |
||||
let win = nvim.createWindow(winid) |
||||
let buf = await win.buffer |
||||
let lines = await buf.lines |
||||
expect(lines.length).toBe(2) |
||||
expect(lines[1]).toMatch(/code refactor/) |
||||
await nvim.input('2') |
||||
await helper.wait(50) |
||||
await nvim.input('j') |
||||
await nvim.input('<cr>') |
||||
await helper.wait(50) |
||||
let valid = await win.valid |
||||
expect(valid).toBe(true) |
||||
let cmdline = await helper.getCmdline() |
||||
expect(cmdline).toMatch(/invalid position/) |
||||
await nvim.input('<esc>') |
||||
}) |
||||
|
||||
it('should action dialog to choose action', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) |
||||
currActions = [action, CodeAction.create('foo')] |
||||
let promise = codeActions.doCodeAction(null) |
||||
await helper.wait(50) |
||||
let ids = await nvim.call('coc#float#get_float_win_list') as number[] |
||||
expect(ids.length).toBeGreaterThan(0) |
||||
await nvim.input('<CR>') |
||||
await promise |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar']) |
||||
}) |
||||
|
||||
it('should choose code actions by range', async () => { |
||||
let range: Range |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: ( |
||||
_document: TextDocument, |
||||
r: Range, |
||||
_context: CodeActionContext, _token: CancellationToken |
||||
) => { |
||||
range = r |
||||
return [CodeAction.create('my title'), CodeAction.create('b'), CodeAction.create('c')] |
||||
}, |
||||
}, undefined)) |
||||
await helper.createDocument() |
||||
await nvim.setLine('abc') |
||||
await nvim.command('normal! 0v$') |
||||
await nvim.input('<esc>') |
||||
await codeActions.doCodeAction('v', 'my title') |
||||
expect(range).toEqual({ start: { line: 0, character: 0 }, end: { line: 0, character: 3 } }) |
||||
}) |
||||
|
||||
it('should filter by provider kinds', async () => { |
||||
currActions = [] |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: () => { |
||||
return [CodeAction.create('my title'), CodeAction.create('b'), CodeAction.create('c')] |
||||
}, |
||||
}, undefined, [CodeActionKind.QuickFix])) |
||||
let doc = await workspace.document |
||||
let res = await languages.getCodeActions(doc.textDocument, Range.create(0, 0, 1, 1), { only: [CodeActionKind.Refactor], diagnostics: [] }, CancellationToken.None) |
||||
expect(res).toEqual([]) |
||||
}) |
||||
|
||||
it('should filter by codeAction kind', async () => { |
||||
currActions = [] |
||||
disposables.push(languages.registerCodeActionProvider([{ language: '*' }], { |
||||
provideCodeActions: () => { |
||||
return [ |
||||
CodeAction.create('my title', CodeActionKind.QuickFix), |
||||
CodeAction.create('b'), |
||||
Command.create('command', 'command') |
||||
] |
||||
}, |
||||
resolveCodeAction: () => { |
||||
return null |
||||
} |
||||
}, undefined)) |
||||
let doc = await workspace.document |
||||
let res = await languages.getCodeActions(doc.textDocument, Range.create(0, 0, 1, 1), { only: [CodeActionKind.QuickFix], diagnostics: [] }, CancellationToken.None) |
||||
expect(res.length).toBe(2) |
||||
let resolved = await languages.resolveCodeAction(res[0], CancellationToken.None) |
||||
expect(resolved).toBeDefined() |
||||
}) |
||||
}) |
||||
|
||||
describe('doQuickfix', () => { |
||||
it('should show message when quickfix action does not exist', async () => { |
||||
currActions = [] |
||||
await helper.createDocument() |
||||
await codeActions.doQuickfix() |
||||
let msg = await helper.getCmdline() |
||||
expect(msg).toMatch('No quickfix') |
||||
}) |
||||
|
||||
it('should do preferred quickfix action', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', edit, CodeActionKind.QuickFix) |
||||
action.isPreferred = true |
||||
currActions = [CodeAction.create('foo', CodeActionKind.QuickFix), action, CodeAction.create('bar')] |
||||
await codeActions.doQuickfix() |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar']) |
||||
}) |
||||
}) |
||||
|
||||
describe('applyCodeAction', () => { |
||||
it('should resolve codeAction', async () => { |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', CodeActionKind.QuickFix) |
||||
action.isPreferred = true |
||||
currActions = [action] |
||||
resolvedAction = Object.assign({ edit }, action) |
||||
let arr = await codeActions.getCurrentCodeActions('line', [CodeActionKind.QuickFix]) |
||||
await codeActions.applyCodeAction(arr[0]) |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar']) |
||||
}) |
||||
|
||||
it('should throw for disabled action', async () => { |
||||
let action: any = CodeAction.create('my action', CodeActionKind.Empty) |
||||
action.disabled = { reason: 'disabled', providerId: 'x' } |
||||
let err |
||||
try { |
||||
await codeActions.applyCodeAction(action) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should invoke registered command after apply edit', async () => { |
||||
let called |
||||
disposables.push(commands.registerCommand('test.execute', async (s: string) => { |
||||
called = s |
||||
await nvim.command(s) |
||||
})) |
||||
let doc = await helper.createDocument() |
||||
let edits: TextEdit[] = [] |
||||
edits.push(TextEdit.insert(Position.create(0, 0), 'bar')) |
||||
let edit = { changes: { [doc.uri]: edits } } |
||||
let action = CodeAction.create('code fix', CodeActionKind.QuickFix) |
||||
action.isPreferred = true |
||||
currActions = [action] |
||||
resolvedAction = Object.assign({ |
||||
edit, |
||||
command: Command.create('run vim command', 'test.execute', 'normal! $') |
||||
}, action) |
||||
let arr = await codeActions.getCurrentCodeActions('line', [CodeActionKind.QuickFix]) |
||||
await codeActions.applyCodeAction(arr[0]) |
||||
let lines = await doc.buffer.lines |
||||
expect(lines).toEqual(['bar']) |
||||
expect(called).toBe('normal! $') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,324 @@
@@ -0,0 +1,324 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationToken, CodeLens, Command, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' |
||||
import commands from '../../commands' |
||||
import events from '../../events' |
||||
import CodeLensBuffer, { getCommands } from '../../handler/codelens/buffer' |
||||
import CodeLensHandler from '../../handler/codelens/index' |
||||
import languages from '../../languages' |
||||
import { disposeAll } from '../../util' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let codeLens: CodeLensHandler |
||||
let disposables: Disposable[] = [] |
||||
let srcId: number |
||||
|
||||
jest.setTimeout(10000) |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
srcId = await nvim.createNamespace('coc-codelens') |
||||
codeLens = helper.plugin.getHandler().codeLens |
||||
}) |
||||
|
||||
beforeEach(() => { |
||||
helper.updateConfiguration('codeLens.enable', true) |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
disposeAll(disposables) |
||||
}) |
||||
|
||||
async function createBufferWithCodeLens(): Promise<CodeLensBuffer> { |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: () => { |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 1) |
||||
}] |
||||
}, |
||||
resolveCodeLens: codeLens => { |
||||
codeLens.command = Command.create('save', '__save', 1, 2, 3) |
||||
return codeLens |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('e.js') |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await doc.synchronize() |
||||
await codeLens.checkProvider() |
||||
return codeLens.buffers.getItem(doc.bufnr) |
||||
} |
||||
|
||||
describe('codeLenes featrue', () => { |
||||
it('should return codeLenes when not resolve exists', async () => { |
||||
let codeLens = CodeLens.create(Range.create(0, 0, 1, 1)) |
||||
let resolved = await languages.resolveCodeLens(codeLens, CancellationToken.None) |
||||
expect(resolved).toBeDefined() |
||||
}) |
||||
|
||||
it('should do codeLenes request and resolve codeLenes', async () => { |
||||
let buf = await createBufferWithCodeLens() |
||||
let doc = await workspace.document |
||||
let codelens = buf.currentCodeLens |
||||
expect(codelens).toBeDefined() |
||||
expect(codelens[0].command).toBeDefined() |
||||
let markers = await helper.getMarkers(doc.bufnr, srcId) |
||||
expect(markers.length).toBe(1) |
||||
}) |
||||
|
||||
it('should refresh on empty changes', async () => { |
||||
await createBufferWithCodeLens() |
||||
let doc = await workspace.document |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await doc.synchronize() |
||||
let markers = await helper.getMarkers(doc.bufnr, srcId) |
||||
expect(markers.length).toBe(1) |
||||
}) |
||||
|
||||
it('should work with empty codeLens', async () => { |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: () => { |
||||
return [] |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('t.js') |
||||
let buf = codeLens.buffers.getItem(doc.bufnr) |
||||
let codelens = buf.currentCodeLens |
||||
expect(codelens).toBeUndefined() |
||||
}) |
||||
|
||||
it('should change codeLenes position', async () => { |
||||
let fn = jest.fn() |
||||
helper.updateConfiguration('codeLens.position', 'eol') |
||||
disposables.push(commands.registerCommand('__save', (...args) => { |
||||
fn(...args) |
||||
})) |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: () => { |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 1) |
||||
}] |
||||
}, |
||||
resolveCodeLens: codeLens => { |
||||
codeLens.command = Command.create('save', '__save', 1, 2, 3) |
||||
return codeLens |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('example.js') |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await codeLens.checkProvider() |
||||
let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true }) |
||||
expect(res.length).toBeGreaterThan(0) |
||||
let arr = res[0][3]['virt_text'] |
||||
expect(arr[0][0]).toBe('save') |
||||
}) |
||||
|
||||
it('should refresh codeLens on CursorHold', async () => { |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: document => { |
||||
let n = document.lineCount |
||||
let arr: any[] = [] |
||||
for (let i = 0; i <= n - 2; i++) { |
||||
arr.push({ |
||||
range: Range.create(i, 0, i, 1), |
||||
command: Command.create('save', '__save', i) |
||||
}) |
||||
} |
||||
return arr |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('example.js') |
||||
await helper.wait(100) |
||||
let markers = await helper.getMarkers(doc.bufnr, srcId) |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await doc.synchronize() |
||||
await events.fire('CursorHold', [doc.bufnr]) |
||||
await helper.wait(200) |
||||
markers = await helper.getMarkers(doc.bufnr, srcId) |
||||
expect(markers.length).toBe(3) |
||||
}) |
||||
|
||||
it('should cancel codeLenes request on document change', async () => { |
||||
let cancelled = false |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: (_, token) => { |
||||
return new Promise(resolve => { |
||||
token.onCancellationRequested(() => { |
||||
cancelled = true |
||||
clearTimeout(timer) |
||||
resolve(null) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
resolve([{ |
||||
range: Range.create(0, 0, 0, 1) |
||||
}, { |
||||
range: Range.create(1, 0, 1, 1) |
||||
}]) |
||||
}, 2000) |
||||
disposables.push({ |
||||
dispose: () => { |
||||
clearTimeout(timer) |
||||
} |
||||
}) |
||||
}) |
||||
}, |
||||
resolveCodeLens: codeLens => { |
||||
codeLens.command = Command.create('save', '__save') |
||||
return codeLens |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('codelens.js') |
||||
await helper.wait(50) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'a\nb\nc')]) |
||||
expect(cancelled).toBe(true) |
||||
}) |
||||
|
||||
it('should resolve on CursorMoved', async () => { |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: () => { |
||||
return [{ |
||||
range: Range.create(90, 0, 90, 1) |
||||
}, { |
||||
range: Range.create(91, 0, 91, 1) |
||||
}] |
||||
}, |
||||
resolveCodeLens: async codeLens => { |
||||
codeLens.command = Command.create('save', '__save') |
||||
return codeLens |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('example.js') |
||||
let arr = new Array(100) |
||||
arr.fill('') |
||||
await nvim.call('setline', [1, arr]) |
||||
await doc.synchronize() |
||||
await codeLens.checkProvider() |
||||
await nvim.command('normal! gg') |
||||
await nvim.command('normal! G') |
||||
await helper.wait(100) |
||||
let buf = codeLens.buffers.getItem(doc.bufnr) |
||||
let codelens = buf.currentCodeLens |
||||
expect(codelens).toBeDefined() |
||||
expect(codelens[0].command).toBeDefined() |
||||
expect(codelens[1].command).toBeDefined() |
||||
}) |
||||
|
||||
it('should invoke codeLenes action', async () => { |
||||
let fn = jest.fn() |
||||
disposables.push(commands.registerCommand('__save', (...args) => { |
||||
fn(...args) |
||||
})) |
||||
await createBufferWithCodeLens() |
||||
await helper.doAction('codeLensAction') |
||||
expect(fn).toBeCalledWith(1, 2, 3) |
||||
await nvim.command('normal! G') |
||||
await helper.doAction('codeLensAction') |
||||
}) |
||||
|
||||
it('should use picker for multiple codeLenses', async () => { |
||||
let fn = jest.fn() |
||||
disposables.push(commands.registerCommand('__save', (...args) => { |
||||
fn(...args) |
||||
})) |
||||
disposables.push(commands.registerCommand('__delete', (...args) => { |
||||
fn(...args) |
||||
})) |
||||
disposables.push(languages.registerCodeLensProvider([{ language: 'javascript' }], { |
||||
provideCodeLenses: () => { |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 1), |
||||
command: Command.create('save', '__save', 1, 2, 3) |
||||
}, { |
||||
range: Range.create(0, 1, 0, 2), |
||||
command: Command.create('save', '__delete', 4, 5, 6) |
||||
}] |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('example.js') |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await doc.synchronize() |
||||
await codeLens.checkProvider() |
||||
let p = helper.doAction('codeLensAction') |
||||
await helper.waitPrompt() |
||||
await nvim.input('<cr>') |
||||
await p |
||||
expect(fn).toBeCalledWith(1, 2, 3) |
||||
}) |
||||
|
||||
it('should refresh for failed codeLens request', async () => { |
||||
let called = 0 |
||||
let fn = jest.fn() |
||||
disposables.push(commands.registerCommand('__save', (...args) => { |
||||
fn(...args) |
||||
})) |
||||
disposables.push(commands.registerCommand('__foo', (...args) => { |
||||
fn(...args) |
||||
})) |
||||
disposables.push(languages.registerCodeLensProvider([{ language: '*' }], { |
||||
provideCodeLenses: () => { |
||||
called++ |
||||
if (called == 1) { |
||||
return null |
||||
} |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 1), |
||||
command: Command.create('foo', '__foo') |
||||
}] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerCodeLensProvider([{ language: '*' }], { |
||||
provideCodeLenses: () => { |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 1), |
||||
command: Command.create('save', '__save') |
||||
}] |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('example.js') |
||||
await helper.wait(50) |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await codeLens.checkProvider() |
||||
let markers = await helper.getMarkers(doc.buffer.id, srcId) |
||||
expect(markers.length).toBeGreaterThan(0) |
||||
let codeLensBuffer = codeLens.buffers.getItem(doc.buffer.id) |
||||
await codeLensBuffer.forceFetch() |
||||
let curr = codeLensBuffer.currentCodeLens |
||||
expect(curr.length).toBeGreaterThan(1) |
||||
}) |
||||
|
||||
it('should use custom separator & position', async () => { |
||||
helper.updateConfiguration('codeLens.separator', '|') |
||||
helper.updateConfiguration('codeLens.position', 'eol') |
||||
let doc = await helper.createDocument('example.js') |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await doc.synchronize() |
||||
disposables.push(languages.registerCodeLensProvider([{ language: '*' }], { |
||||
provideCodeLenses: () => { |
||||
return [{ |
||||
range: Range.create(0, 0, 1, 0), |
||||
command: Command.create('save', '__save') |
||||
}, { |
||||
range: Range.create(0, 0, 1, 0), |
||||
command: Command.create('save', '__save') |
||||
}] |
||||
} |
||||
})) |
||||
await codeLens.checkProvider() |
||||
let res = await doc.buffer.getExtMarks(srcId, 0, -1, { details: true }) |
||||
expect(res.length).toBe(1) |
||||
}) |
||||
|
||||
it('should get commands from codeLenses', async () => { |
||||
expect(getCommands(1, undefined)).toEqual([]) |
||||
let codeLenses = [CodeLens.create(Range.create(0, 0, 0, 0))] |
||||
expect(getCommands(0, codeLenses)).toEqual([]) |
||||
codeLenses = [CodeLens.create(Range.create(0, 0, 1, 0)), CodeLens.create(Range.create(2, 0, 3, 0))] |
||||
codeLenses[0].command = Command.create('save', '__save') |
||||
expect(getCommands(0, codeLenses).length).toEqual(1) |
||||
}) |
||||
}) |
@ -0,0 +1,295 @@
@@ -0,0 +1,295 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationToken, Color, ColorInformation, ColorPresentation, Disposable, Position, Range } from 'vscode-languageserver-protocol' |
||||
import { TextDocument } from 'vscode-languageserver-textdocument' |
||||
import commands from '../../commands' |
||||
import { toHexString } from '../../util/color' |
||||
import Colors from '../../handler/colors/index' |
||||
import languages from '../../languages' |
||||
import { ProviderResult } from '../../provider' |
||||
import { disposeAll } from '../../util' |
||||
import path from 'path' |
||||
import helper from '../helper' |
||||
import workspace from '../../workspace' |
||||
|
||||
let nvim: Neovim |
||||
let state = 'normal' |
||||
let colors: Colors |
||||
let disposables: Disposable[] = [] |
||||
let colorPresentations: ColorPresentation[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
await nvim.command(`source ${path.join(process.cwd(), 'autoload/coc/color.vim')}`) |
||||
colors = helper.plugin.getHandler().colors |
||||
languages.registerDocumentColorProvider([{ language: '*' }], { |
||||
provideColorPresentations: ( |
||||
_color: Color, |
||||
_context: { document: TextDocument; range: Range }, |
||||
_token: CancellationToken |
||||
): ColorPresentation[] => colorPresentations, |
||||
provideDocumentColors: ( |
||||
document: TextDocument, |
||||
_token: CancellationToken |
||||
): ProviderResult<ColorInformation[]> => { |
||||
if (state == 'empty') return [] |
||||
if (state == 'error') return Promise.reject(new Error('no color')) |
||||
let matches = Array.from((document.getText() as any).matchAll(/#\w{6}/g)) as any |
||||
return matches.map(o => { |
||||
let start = document.positionAt(o.index) |
||||
let end = document.positionAt(o.index + o[0].length) |
||||
return { |
||||
range: Range.create(start, end), |
||||
color: getColor(255, 255, 255) |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
beforeEach(() => { |
||||
helper.updateConfiguration('colors.filetypes', ['*']) |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
state = 'normal' |
||||
colorPresentations = [] |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
function getColor(r: number, g: number, b: number): Color { |
||||
return { red: r / 255, green: g / 255, blue: b / 255, alpha: 1 } |
||||
} |
||||
|
||||
describe('Colors', () => { |
||||
describe('utils', () => { |
||||
it('should get hex string', () => { |
||||
let color = getColor(255, 255, 255) |
||||
let hex = toHexString(color) |
||||
expect(hex).toBe('ffffff') |
||||
}) |
||||
}) |
||||
|
||||
describe('configuration', () => { |
||||
it('should toggle enable state on configuration change', async () => { |
||||
let doc = await helper.createDocument() |
||||
helper.updateConfiguration('colors.filetypes', []) |
||||
let enabled = colors.isEnabled(doc.bufnr) |
||||
helper.updateConfiguration('colors.filetypes', ['*']) |
||||
expect(enabled).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('commands', () => { |
||||
it('should register editor.action.pickColor command', async () => { |
||||
await helper.mockFunction('coc#color#pick_color', [0, 0, 0]) |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
doc.forceSync() |
||||
await colors.doHighlight(doc.bufnr) |
||||
await commands.executeCommand('editor.action.pickColor') |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('#000000') |
||||
}) |
||||
|
||||
it('should register editor.action.colorPresentation command', async () => { |
||||
colorPresentations = [ColorPresentation.create('red'), ColorPresentation.create('#ff0000')] |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
await doc.synchronize() |
||||
await colors.doHighlight(doc.bufnr) |
||||
let p = commands.executeCommand('editor.action.colorPresentation') |
||||
await helper.waitPrompt() |
||||
await nvim.input('1') |
||||
await p |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('red') |
||||
}) |
||||
}) |
||||
|
||||
describe('doHighlight', () => { |
||||
it('should merge colors of providers', async () => { |
||||
disposables.push(languages.registerDocumentColorProvider([{ language: '*' }], { |
||||
provideColorPresentations: (): ColorPresentation[] => colorPresentations, |
||||
provideDocumentColors: ( |
||||
): ProviderResult<ColorInformation[]> => { |
||||
return [{ |
||||
range: Range.create(0, 0, 1, 0), |
||||
color: getColor(0, 0, 0) |
||||
}, { |
||||
range: Range.create(0, 0, 0, 7), |
||||
color: getColor(1, 1, 1) |
||||
}] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerDocumentColorProvider([{ language: '*' }], { |
||||
provideColorPresentations: (): ColorPresentation[] => colorPresentations, |
||||
provideDocumentColors: ( |
||||
): ProviderResult<ColorInformation[]> => { |
||||
return null |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
await nvim.setLine('#ffffff #ff0000') |
||||
await doc.synchronize() |
||||
let colors = await languages.provideDocumentColors(doc.textDocument, CancellationToken.None) |
||||
expect(colors.length).toBe(3) |
||||
let color = ColorInformation.create(Range.create(0, 0, 1, 0), getColor(0, 0, 0)) |
||||
let presentation = await languages.provideColorPresentations(color, doc.textDocument, CancellationToken.None) |
||||
expect(presentation).toBeNull() |
||||
}) |
||||
|
||||
it('should clearHighlight on empty result', async () => { |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
state = 'empty' |
||||
await colors.doHighlight(doc.bufnr) |
||||
let res = colors.hasColor(doc.bufnr) |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should not throw on error result', async () => { |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
state = 'error' |
||||
let err |
||||
try { |
||||
await colors.doHighlight(doc.bufnr) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeUndefined() |
||||
}) |
||||
|
||||
it('should highlight after document changed', async () => { |
||||
let doc = await helper.createDocument() |
||||
await colors.doHighlight(doc.bufnr) |
||||
expect(colors.hasColor(doc.bufnr)).toBe(false) |
||||
expect(colors.hasColorAtPosition(doc.bufnr, Position.create(0, 1))).toBe(false) |
||||
await nvim.setLine('#ffffff #ff0000') |
||||
await doc.synchronize() |
||||
await helper.waitValue(() => { |
||||
return colors.hasColorAtPosition(doc.bufnr, Position.create(0, 1)) |
||||
}, true) |
||||
expect(colors.hasColor(doc.bufnr)).toBe(true) |
||||
}) |
||||
|
||||
it('should clearHighlight on clearHighlight', async () => { |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff #ff0000') |
||||
await doc.synchronize() |
||||
await colors.doHighlight(doc.bufnr) |
||||
expect(colors.hasColor(doc.bufnr)).toBe(true) |
||||
colors.clearHighlight(doc.bufnr) |
||||
expect(colors.hasColor(doc.bufnr)).toBe(false) |
||||
}) |
||||
|
||||
it('should highlight colors', async () => { |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
await colors.doHighlight(doc.bufnr) |
||||
let exists = await nvim.call('hlexists', 'BGffffff') |
||||
expect(exists).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('hasColor()', () => { |
||||
it('should return false when bufnr does not exist', async () => { |
||||
let res = colors.hasColor(99) |
||||
colors.clearHighlight(99) |
||||
expect(res).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('getColorInformation()', () => { |
||||
it('should return null when highlighter does not exist', async () => { |
||||
let res = await colors.getColorInformation(99) |
||||
expect(res).toBe(null) |
||||
}) |
||||
|
||||
it('should return null when color not found', async () => { |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff foo ') |
||||
doc.forceSync() |
||||
await colors.doHighlight(doc.bufnr) |
||||
await nvim.call('cursor', [1, 12]) |
||||
let res = await colors.getColorInformation(doc.bufnr) |
||||
expect(res).toBe(null) |
||||
}) |
||||
}) |
||||
|
||||
describe('hasColorAtPosition()', () => { |
||||
it('should return false when bufnr does not exist', async () => { |
||||
let res = colors.hasColorAtPosition(99, Position.create(0, 0)) |
||||
expect(res).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('pickPresentation()', () => { |
||||
it('should show warning when color does not exist', async () => { |
||||
await helper.createDocument() |
||||
await colors.pickPresentation() |
||||
let msg = await helper.getCmdline() |
||||
expect(msg).toMatch('Color not found') |
||||
}) |
||||
|
||||
it('should not throw when presentations do not exist', async () => { |
||||
colorPresentations = [] |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
doc.forceSync() |
||||
await colors.doHighlight(99) |
||||
await colors.doHighlight(doc.bufnr) |
||||
await helper.doAction('colorPresentation') |
||||
}) |
||||
|
||||
it('should pick presentations', async () => { |
||||
colorPresentations = [ColorPresentation.create('red'), ColorPresentation.create('#ff0000')] |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
doc.forceSync() |
||||
await colors.doHighlight(doc.bufnr) |
||||
let p = helper.doAction('colorPresentation') |
||||
await helper.waitPrompt() |
||||
await nvim.input('1') |
||||
await p |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('red') |
||||
}) |
||||
}) |
||||
|
||||
describe('pickColor()', () => { |
||||
it('should show warning when color does not exist', async () => { |
||||
await helper.createDocument() |
||||
await colors.pickColor() |
||||
let msg = await helper.getCmdline() |
||||
expect(msg).toMatch('not found') |
||||
}) |
||||
|
||||
it('should pickColor', async () => { |
||||
await helper.mockFunction('coc#color#pick_color', [0, 0, 0]) |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
doc.forceSync() |
||||
await colors.doHighlight(doc.bufnr) |
||||
await helper.doAction('pickColor') |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('#000000') |
||||
}) |
||||
|
||||
it('should not throw when pick color return 0', async () => { |
||||
await helper.mockFunction('coc#color#pick_color', 0) |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine('#ffffff') |
||||
doc.forceSync() |
||||
await colors.doHighlight(doc.bufnr) |
||||
await helper.doAction('pickColor') |
||||
let line = await nvim.getLine() |
||||
expect(line).toBe('#ffffff') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
import CommandsHandler from '../../handler/commands' |
||||
import commandManager from '../../commands' |
||||
import { disposeAll } from '../../util' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let commands: CommandsHandler |
||||
let disposables: Disposable[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
commands = (helper.plugin as any).handler.commands |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
await helper.createDocument() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('Commands', () => { |
||||
describe('addVimCommand', () => { |
||||
it('should register global vim commands', async () => { |
||||
await commandManager.executeCommand('vim.config') |
||||
await helper.wait(50) |
||||
let bufname = await nvim.call('bufname', ['%']) |
||||
expect(bufname).toMatch('coc-settings.json') |
||||
let list = commands.getCommandList() |
||||
expect(list.includes('vim.config')).toBe(true) |
||||
}) |
||||
|
||||
it('should add vim command with title', async () => { |
||||
commands.addVimCommand({ id: 'list', cmd: 'CocList', title: 'list of coc.nvim' }) |
||||
let res = commandManager.titles.get('vim.list') |
||||
expect(res).toBe('list of coc.nvim') |
||||
commandManager.unregister('vim.list') |
||||
}) |
||||
}) |
||||
|
||||
describe('getCommands', () => { |
||||
it('should get command items', async () => { |
||||
let res = commands.getCommands() |
||||
let idx = res.findIndex(o => o.id == 'workspace.showOutput') |
||||
expect(idx != -1).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('repeat', () => { |
||||
it('should repeat command', async () => { |
||||
// let buf = await nvim.buffer
|
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await nvim.call('cursor', [1, 1]) |
||||
commands.addVimCommand({ id: 'remove', cmd: 'normal! dd' }) |
||||
await commands.runCommand('vim.remove') |
||||
await helper.wait(50) |
||||
let res = await nvim.call('getline', [1, '$']) |
||||
expect(res).toEqual(['b', 'c']) |
||||
await commands.repeat() |
||||
await helper.wait(50) |
||||
res = await nvim.call('getline', [1, '$']) |
||||
expect(res).toEqual(['c']) |
||||
}) |
||||
}) |
||||
|
||||
describe('runCommand', () => { |
||||
it('should open command list without id', async () => { |
||||
await commands.runCommand() |
||||
await helper.wait(100) |
||||
let bufname = await nvim.call('bufname', ['%']) |
||||
expect(bufname).toBe('list:///commands') |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,113 @@
@@ -0,0 +1,113 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationToken, CancellationTokenSource, Disposable, FoldingRange } from 'vscode-languageserver-protocol' |
||||
import FoldHandler from '../../handler/fold' |
||||
import languages from '../../languages' |
||||
import { disposeAll } from '../../util' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let folds: FoldHandler |
||||
let disposables: Disposable[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
folds = helper.plugin.getHandler().fold |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
await helper.createDocument() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('Folds', () => { |
||||
it('should return empty array when provider does not exist', async () => { |
||||
let doc = await workspace.document |
||||
let token = (new CancellationTokenSource()).token |
||||
expect(await languages.provideFoldingRanges(doc.textDocument, {}, token)).toEqual([]) |
||||
}) |
||||
|
||||
it('should return false when no fold ranges found', async () => { |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges(_doc) { |
||||
return [] |
||||
} |
||||
})) |
||||
let res = await folds.fold() |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should fold all fold ranges', async () => { |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges(_doc) { |
||||
return [FoldingRange.create(1, 3), FoldingRange.create(4, 6, 0, 0, 'comment')] |
||||
} |
||||
})) |
||||
await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']]) |
||||
let res = await folds.fold() |
||||
expect(res).toBe(true) |
||||
let closed = await nvim.call('foldclosed', [2]) |
||||
expect(closed).toBe(2) |
||||
closed = await nvim.call('foldclosed', [5]) |
||||
expect(closed).toBe(5) |
||||
}) |
||||
|
||||
it('should merge folds from all providers', async () => { |
||||
let doc = await workspace.document |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges() { |
||||
return [FoldingRange.create(2, 3), FoldingRange.create(4, 6)] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges() { |
||||
return [FoldingRange.create(1, 2), FoldingRange.create(5, 6), FoldingRange.create(7, 8)] |
||||
} |
||||
})) |
||||
await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']]) |
||||
await doc.synchronize() |
||||
let foldingRanges = await languages.provideFoldingRanges(doc.textDocument, {}, CancellationToken.None) |
||||
expect(foldingRanges.length).toBe(4) |
||||
}) |
||||
|
||||
it('should ignore range start at the same line', async () => { |
||||
let doc = await workspace.document |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges() { |
||||
return [FoldingRange.create(2, 3), FoldingRange.create(4, 6)] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges() { |
||||
return [FoldingRange.create(4, 5)] |
||||
} |
||||
})) |
||||
await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']]) |
||||
await doc.synchronize() |
||||
let foldingRanges = await languages.provideFoldingRanges(doc.textDocument, {}, CancellationToken.None) |
||||
expect(foldingRanges.length).toBe(2) |
||||
}) |
||||
|
||||
it('should fold comment ranges', async () => { |
||||
disposables.push(languages.registerFoldingRangeProvider([{ language: '*' }], { |
||||
provideFoldingRanges(_doc) { |
||||
return [FoldingRange.create(1, 3), FoldingRange.create(4, 6, 0, 0, 'comment')] |
||||
} |
||||
})) |
||||
await nvim.call('setline', [1, ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']]) |
||||
let res = await folds.fold('comment') |
||||
expect(res).toBe(true) |
||||
let closed = await nvim.call('foldclosed', [2]) |
||||
expect(closed).toBe(-1) |
||||
closed = await nvim.call('foldclosed', [5]) |
||||
expect(closed).toBe(5) |
||||
}) |
||||
}) |
@ -0,0 +1,285 @@
@@ -0,0 +1,285 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationToken, CancellationTokenSource, Disposable, Position, Range, TextEdit } from 'vscode-languageserver-protocol' |
||||
import Format from '../../handler/format' |
||||
import languages from '../../languages' |
||||
import { disposeAll } from '../../util' |
||||
import window from '../../window' |
||||
import workspace from '../../workspace' |
||||
import helper, { createTmpFile } from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
let format: Format |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
format = helper.plugin.getHandler().format |
||||
}) |
||||
|
||||
beforeEach(() => { |
||||
helper.updateConfiguration('coc.preferences.formatOnType', true) |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
disposeAll(disposables) |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
describe('format handler', () => { |
||||
describe('documentFormat', () => { |
||||
it('should return null when format provider not exists', async () => { |
||||
let doc = await workspace.document |
||||
let res = await languages.provideDocumentFormattingEdits(doc.textDocument, { insertSpaces: false, tabSize: 2 }, CancellationToken.None) |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should throw when provider not found', async () => { |
||||
let doc = await workspace.document |
||||
let err |
||||
try { |
||||
await format.documentFormat(doc) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
|
||||
it('should return false when get empty edits ', async () => { |
||||
disposables.push(languages.registerDocumentFormatProvider(['*'], { |
||||
provideDocumentFormattingEdits: () => { |
||||
return [] |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument() |
||||
let res = await format.documentFormat(doc) |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should use provider that have higher score', async () => { |
||||
disposables.push(languages.registerDocumentFormatProvider([{ language: 'vim' }], { |
||||
provideDocumentFormattingEdits: () => { |
||||
return [TextEdit.insert(Position.create(0, 0), ' ')] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerDocumentFormatProvider(['*'], { |
||||
provideDocumentFormattingEdits: () => { |
||||
return [] |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument('t.vim') |
||||
let res = await languages.provideDocumentFormattingEdits(doc.textDocument, { tabSize: 2, insertSpaces: false }, CancellationToken.None) |
||||
expect(res.length).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('formatOnSave', () => { |
||||
it('should not throw when provider not found', async () => { |
||||
helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['javascript']) |
||||
let filepath = await createTmpFile('') |
||||
await helper.edit(filepath) |
||||
await nvim.command('setf javascript') |
||||
await nvim.setLine('foo') |
||||
await nvim.command('silent w') |
||||
}) |
||||
|
||||
it('should invoke format on save', async () => { |
||||
helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['text']) |
||||
disposables.push(languages.registerDocumentFormatProvider(['text'], { |
||||
provideDocumentFormattingEdits: document => { |
||||
let lines = document.getText().replace(/\n$/, '').split(/\n/) |
||||
let edits: TextEdit[] = [] |
||||
for (let i = 0; i < lines.length; i++) { |
||||
let text = lines[i] |
||||
if (!text.startsWith(' ')) { |
||||
edits.push(TextEdit.insert(Position.create(i, 0), ' ')) |
||||
} |
||||
} |
||||
return edits |
||||
} |
||||
})) |
||||
let filepath = await createTmpFile('a\nb\nc\n') |
||||
let buf = await helper.edit(filepath) |
||||
await nvim.command('setf text') |
||||
await nvim.command('w') |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([' a', ' b', ' c']) |
||||
}) |
||||
|
||||
it('should cancel when timeout', async () => { |
||||
helper.updateConfiguration('coc.preferences.formatOnSaveFiletypes', ['*']) |
||||
let timer |
||||
disposables.push(languages.registerDocumentFormatProvider(['*'], { |
||||
provideDocumentFormattingEdits: () => { |
||||
return new Promise(resolve => { |
||||
timer = setTimeout(() => { |
||||
resolve(undefined) |
||||
}, 2000) |
||||
}) |
||||
} |
||||
})) |
||||
let filepath = await createTmpFile('a\nb\nc\n') |
||||
await helper.edit(filepath) |
||||
let n = Date.now() |
||||
await nvim.command('w') |
||||
expect(Date.now() - n).toBeLessThan(1000) |
||||
clearTimeout(timer) |
||||
}) |
||||
}) |
||||
|
||||
describe('rangeFormat', () => { |
||||
it('should return null when provider does not exist', async () => { |
||||
let doc = (await workspace.document).textDocument |
||||
let range = Range.create(0, 0, 1, 0) |
||||
let options = await workspace.getFormatOptions() |
||||
let token = (new CancellationTokenSource()).token |
||||
expect(await languages.provideDocumentRangeFormattingEdits(doc, range, options, token)).toBe(null) |
||||
expect(languages.hasProvider('onTypeEdit', doc)).toBe(false) |
||||
let edits = await languages.provideDocumentFormattingEdits(doc, options, token) |
||||
expect(edits).toBe(null) |
||||
}) |
||||
|
||||
it('should invoke range format', async () => { |
||||
disposables.push(languages.registerDocumentRangeFormatProvider(['text'], { |
||||
provideDocumentRangeFormattingEdits: (_document, range) => { |
||||
let lines: number[] = [] |
||||
for (let i = range.start.line; i <= range.end.line; i++) { |
||||
lines.push(i) |
||||
} |
||||
return lines.map(i => { |
||||
return TextEdit.insert(Position.create(i, 0), ' ') |
||||
}) |
||||
} |
||||
}, 1)) |
||||
let doc = await helper.createDocument() |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await nvim.command('setf text') |
||||
await nvim.command('normal! ggvG') |
||||
await nvim.input('<esc>') |
||||
expect(languages.hasFormatProvider(doc.textDocument)).toBe(true) |
||||
expect(languages.hasProvider('format', doc.textDocument)).toBe(true) |
||||
await helper.doAction('formatSelected', 'v') |
||||
let buf = nvim.createBuffer(doc.bufnr) |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([' a', ' b', ' c']) |
||||
let options = await workspace.getFormatOptions(doc.uri) |
||||
let token = (new CancellationTokenSource()).token |
||||
let edits = await languages.provideDocumentFormattingEdits(doc.textDocument, options, token) |
||||
expect(edits.length).toBeGreaterThan(0) |
||||
}) |
||||
|
||||
it('should format range by formatexpr option', async () => { |
||||
let range: Range |
||||
disposables.push(languages.registerDocumentRangeFormatProvider(['text'], { |
||||
provideDocumentRangeFormattingEdits: (_document, r) => { |
||||
range = r |
||||
return [] |
||||
} |
||||
})) |
||||
await helper.createDocument() |
||||
await nvim.call('setline', [1, ['a', 'b', 'c']]) |
||||
await nvim.command('setf text') |
||||
await nvim.command(`setl formatexpr=CocAction('formatSelected')`) |
||||
await nvim.command('normal! ggvGgq') |
||||
expect(range).toEqual({ |
||||
start: { line: 0, character: 0 }, end: { line: 3, character: 0 } |
||||
}) |
||||
}) |
||||
}) |
||||
|
||||
describe('formatOnType', () => { |
||||
it('should invoke format', async () => { |
||||
disposables.push(languages.registerDocumentFormatProvider(['text'], { |
||||
provideDocumentFormattingEdits: () => { |
||||
return [TextEdit.insert(Position.create(0, 0), ' ')] |
||||
} |
||||
})) |
||||
await helper.createDocument() |
||||
await nvim.setLine('foo') |
||||
await nvim.command('setf text') |
||||
await helper.doAction('format') |
||||
let line = await nvim.line |
||||
expect(line).toEqual(' foo') |
||||
}) |
||||
|
||||
it('should does format on type', async () => { |
||||
let doc = await workspace.document |
||||
disposables.push(languages.registerOnTypeFormattingEditProvider(['text'], { |
||||
provideOnTypeFormattingEdits: () => { |
||||
return [TextEdit.insert(Position.create(0, 0), ' ')] |
||||
} |
||||
}, ['|'])) |
||||
let res = languages.canFormatOnType('a', doc.textDocument) |
||||
expect(res).toBe(false) |
||||
await helper.edit() |
||||
await nvim.command('setf text') |
||||
await nvim.input('i|') |
||||
await helper.waitFor('getline', ['.'], ' |') |
||||
let cursor = await window.getCursorPosition() |
||||
expect(cursor).toEqual({ line: 0, character: 3 }) |
||||
}) |
||||
|
||||
it('should return null when provider not found', async () => { |
||||
let doc = await workspace.document |
||||
let res = await languages.provideDocumentOnTypeEdits('|', doc.textDocument, Position.create(0, 0), CancellationToken.None) |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should adjust cursor after format on type', async () => { |
||||
disposables.push(languages.registerOnTypeFormattingEditProvider(['text'], { |
||||
provideOnTypeFormattingEdits: () => { |
||||
return [ |
||||
TextEdit.insert(Position.create(0, 0), ' '), |
||||
TextEdit.insert(Position.create(0, 2), 'end') |
||||
] |
||||
} |
||||
}, ['|'])) |
||||
disposables.push(languages.registerOnTypeFormattingEditProvider([{ language: '*' }], { |
||||
provideOnTypeFormattingEdits: () => { |
||||
return [] |
||||
} |
||||
})) |
||||
await helper.edit() |
||||
await nvim.command('setf text') |
||||
await nvim.setLine('"') |
||||
await nvim.input('i|') |
||||
await helper.waitFor('getline', ['.'], ' |"end') |
||||
let cursor = await window.getCursorPosition() |
||||
expect(cursor).toEqual({ line: 0, character: 3 }) |
||||
}) |
||||
}) |
||||
|
||||
describe('bracketEnterImprove', () => { |
||||
afterEach(() => { |
||||
nvim.command('iunmap <CR>', true) |
||||
}) |
||||
|
||||
it('should format vim file on enter', async () => { |
||||
let buf = await helper.edit('foo.vim') |
||||
await nvim.command(`inoremap <silent><expr> <cr> pumvisible() ? coc#_select_confirm() : "\\<C-g>u\\<CR>\\<c-r>=coc#on_enter()\\<CR>"`) |
||||
await nvim.setLine('let foo={}') |
||||
await nvim.command(`normal! gg$`) |
||||
await nvim.input('i') |
||||
await nvim.eval(`feedkeys("\\<CR>", 'im')`) |
||||
await helper.waitFor('getline', [1], 'let foo={') |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual(['let foo={', ' \\ ', ' \\ }']) |
||||
}) |
||||
|
||||
it('should add new line between bracket', async () => { |
||||
let buf = await helper.edit() |
||||
await nvim.command(`inoremap <silent><expr> <cr> pumvisible() ? coc#_select_confirm() : "\\<C-g>u\\<CR>\\<c-r>=coc#on_enter()\\<CR>"`) |
||||
await nvim.setLine(' {}') |
||||
await nvim.command(`normal! gg$`) |
||||
await nvim.input('i') |
||||
await nvim.eval(`feedkeys("\\<CR>", 'im')`) |
||||
await helper.waitFor('getline', [2], ' ') |
||||
let lines = await buf.lines |
||||
expect(lines).toEqual([' {', ' ', ' }']) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,166 @@
@@ -0,0 +1,166 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable, DocumentHighlightKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' |
||||
import Highlights from '../../handler/highlights' |
||||
import languages from '../../languages' |
||||
import workspace from '../../workspace' |
||||
import { disposeAll } from '../../util' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
let highlights: Highlights |
||||
|
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
highlights = helper.plugin.getHandler().documentHighlighter |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
await helper.reset() |
||||
disposeAll(disposables) |
||||
disposables = [] |
||||
}) |
||||
|
||||
function registerProvider(): void { |
||||
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { |
||||
provideDocumentHighlights: async document => { |
||||
let word = await nvim.eval('expand("<cword>")') |
||||
// let word = document.get
|
||||
let matches = Array.from((document.getText() as any).matchAll(/\w+/g)) as any[] |
||||
let filtered = matches.filter(o => o[0] == word) |
||||
return filtered.map((o, i) => { |
||||
let start = document.positionAt(o.index) |
||||
let end = document.positionAt(o.index + o[0].length) |
||||
return { |
||||
range: Range.create(start, end), |
||||
kind: i % 2 == 0 ? DocumentHighlightKind.Read : DocumentHighlightKind.Write |
||||
} |
||||
}) |
||||
} |
||||
})) |
||||
} |
||||
|
||||
describe('document highlights', () => { |
||||
|
||||
function registerTimerProvider(fn: Function, timeout: number): void { |
||||
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { |
||||
provideDocumentHighlights: (_document, _position, token) => { |
||||
return new Promise(resolve => { |
||||
token.onCancellationRequested(() => { |
||||
clearTimeout(timer) |
||||
fn() |
||||
resolve([]) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
resolve([{ range: Range.create(0, 0, 0, 3) }]) |
||||
}, timeout) |
||||
}) |
||||
} |
||||
})) |
||||
} |
||||
|
||||
it('should not throw when provide throws', async () => { |
||||
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { |
||||
provideDocumentHighlights: () => { |
||||
return null |
||||
} |
||||
})) |
||||
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { |
||||
provideDocumentHighlights: () => { |
||||
throw new Error('fake error') |
||||
} |
||||
})) |
||||
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { |
||||
provideDocumentHighlights: () => { |
||||
return [{ |
||||
range: Range.create(0, 0, 0, 3), |
||||
kind: DocumentHighlightKind.Read |
||||
}] |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) |
||||
let res = await highlights.getHighlights(doc, Position.create(0, 0)) |
||||
expect(res).toBeDefined() |
||||
}) |
||||
|
||||
it('should return null when highlights provide not exist', async () => { |
||||
let doc = await workspace.document |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo')]) |
||||
let res = await highlights.getHighlights(doc, Position.create(0, 0)) |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should cancel request on CursorMoved', async () => { |
||||
let fn = jest.fn() |
||||
registerTimerProvider(fn, 3000) |
||||
await helper.edit() |
||||
await nvim.setLine('foo') |
||||
let p = highlights.highlight() |
||||
await helper.wait(50) |
||||
await nvim.call('cursor', [1, 2]) |
||||
await p |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should cancel on timeout', async () => { |
||||
helper.updateConfiguration('documentHighlight.timeout', 10) |
||||
let fn = jest.fn() |
||||
registerTimerProvider(fn, 3000) |
||||
await helper.edit() |
||||
await nvim.setLine('foo') |
||||
await highlights.highlight() |
||||
expect(fn).toBeCalled() |
||||
}) |
||||
|
||||
it('should add highlights to symbols', async () => { |
||||
registerProvider() |
||||
await helper.createDocument() |
||||
await nvim.setLine('foo bar foo') |
||||
await helper.doAction('highlight') |
||||
let winid = await nvim.call('win_getid') as number |
||||
expect(highlights.hasHighlights(winid)).toBe(true) |
||||
}) |
||||
|
||||
it('should return highlight ranges', async () => { |
||||
registerProvider() |
||||
await helper.createDocument() |
||||
await nvim.setLine('foo bar foo') |
||||
let res = await helper.doAction('symbolRanges') |
||||
expect(res.length).toBe(2) |
||||
}) |
||||
|
||||
it('should return null when cursor not in word range', async () => { |
||||
disposables.push(languages.registerDocumentHighlightProvider([{ language: '*' }], { |
||||
provideDocumentHighlights: () => { |
||||
return [{ range: Range.create(0, 0, 0, 3) }] |
||||
} |
||||
})) |
||||
let doc = await helper.createDocument() |
||||
await nvim.setLine(' oo') |
||||
await nvim.call('cursor', [1, 2]) |
||||
let res = await highlights.getHighlights(doc, Position.create(0, 0)) |
||||
expect(res).toBeNull() |
||||
}) |
||||
|
||||
it('should not throw when document is command line', async () => { |
||||
await nvim.call('feedkeys', ['q:', 'in']) |
||||
let doc = await workspace.document |
||||
expect(doc.isCommandLine).toBe(true) |
||||
await highlights.highlight() |
||||
await nvim.input('<C-c>') |
||||
}) |
||||
|
||||
it('should not throw when provider not found', async () => { |
||||
disposeAll(disposables) |
||||
await helper.createDocument() |
||||
await nvim.setLine(' oo') |
||||
await nvim.call('cursor', [1, 2]) |
||||
await highlights.highlight() |
||||
}) |
||||
}) |
@ -0,0 +1,238 @@
@@ -0,0 +1,238 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable, MarkedString, Hover, Range, TextEdit, Position, CancellationToken, MarkupKind } from 'vscode-languageserver-protocol' |
||||
import HoverHandler, { addDefinitions, addDocument, isDocumentation, readLines } from '../../handler/hover' |
||||
import { URI } from 'vscode-uri' |
||||
import languages from '../../languages' |
||||
import { disposeAll } from '../../util' |
||||
import helper, { createTmpFile } from '../helper' |
||||
import workspace from '../../workspace' |
||||
import { Documentation } from '../../types' |
||||
|
||||
let nvim: Neovim |
||||
let hover: HoverHandler |
||||
let disposables: Disposable[] = [] |
||||
let hoverResult: Hover |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
hover = helper.plugin.getHandler().hover |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
await helper.createDocument() |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return hoverResult |
||||
} |
||||
})) |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
async function getDocumentText(): Promise<string> { |
||||
let lines = await nvim.call('getbufline', ['coc://document', 1, '$']) as string[] |
||||
return lines.join('\n') |
||||
} |
||||
|
||||
describe('Hover', () => { |
||||
describe('utils', () => { |
||||
it('should addDocument', async () => { |
||||
let docs: Documentation[] = [] |
||||
addDocument(docs, '', '') |
||||
expect(docs.length).toBe(0) |
||||
}) |
||||
|
||||
it('should check documentation', async () => { |
||||
expect(isDocumentation({})).toBe(false) |
||||
expect(isDocumentation({ filetype: '', content: '' })).toBe(true) |
||||
}) |
||||
|
||||
it('should readLines', async () => { |
||||
let res = await readLines('file:///not_exists', 0, 1) |
||||
expect(res).toEqual([]) |
||||
}) |
||||
|
||||
it('should addDefinitions', async () => { |
||||
let hovers = [] |
||||
let range = Range.create(0, 0, 0, 0) |
||||
await addDefinitions(hovers, [undefined, {} as any, { targetUri: 'file:///not_exists', targetRange: range, targetSelectionRange: range }], '') |
||||
expect(hovers.length).toBe(0) |
||||
let file = await createTmpFile(' foo\n bar\n', disposables) |
||||
range = Range.create(0, 0, 300, 0) |
||||
await addDefinitions(hovers, [{ targetUri: URI.file(file).toString(), targetRange: range, targetSelectionRange: range }], '') |
||||
expect(hovers.length).toBe(1) |
||||
}) |
||||
}) |
||||
|
||||
describe('onHover', () => { |
||||
it('should return false when hover not found', async () => { |
||||
hoverResult = null |
||||
let res = await hover.onHover('preview') |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should show MarkupContent hover', async () => { |
||||
helper.updateConfiguration('hover.target', 'preview') |
||||
hoverResult = { contents: { kind: 'plaintext', value: 'my hover' } } |
||||
await hover.onHover() |
||||
let res = await getDocumentText() |
||||
expect(res).toMatch('my hover') |
||||
}) |
||||
|
||||
it('should merge hover results', async () => { |
||||
hoverResult = { contents: { kind: 'plaintext', value: 'my hover' } } |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return null |
||||
} |
||||
})) |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return { contents: { kind: 'plaintext', value: 'my hover' } } |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
let hovers = await languages.getHover(doc.textDocument, Position.create(0, 0), CancellationToken.None) |
||||
expect(hovers.length).toBe(1) |
||||
}) |
||||
|
||||
it('should show MarkedString hover', async () => { |
||||
hoverResult = { contents: 'string hover' } |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return { contents: { language: 'typescript', value: 'language hover' } } |
||||
} |
||||
})) |
||||
await hover.onHover('preview') |
||||
let res = await getDocumentText() |
||||
expect(res).toMatch('string hover') |
||||
expect(res).toMatch('language hover') |
||||
}) |
||||
|
||||
it('should show MarkedString hover array', async () => { |
||||
hoverResult = { contents: ['foo', { language: 'typescript', value: 'bar' }] } |
||||
await hover.onHover('preview') |
||||
let res = await getDocumentText() |
||||
expect(res).toMatch('foo') |
||||
expect(res).toMatch('bar') |
||||
}) |
||||
|
||||
it('should highlight hover range', async () => { |
||||
await nvim.setLine('var') |
||||
await nvim.command('normal! 0') |
||||
hoverResult = { contents: ['foo'], range: Range.create(0, 0, 0, 3) } |
||||
await hover.onHover('preview') |
||||
let res = await nvim.call('getmatches') as any[] |
||||
expect(res.length).toBe(1) |
||||
expect(res[0].group).toBe('CocHoverRange') |
||||
await helper.wait(600) |
||||
res = await nvim.call('getmatches') |
||||
expect(res.length).toBe(0) |
||||
}) |
||||
}) |
||||
|
||||
describe('previewHover', () => { |
||||
it('should echo hover message', async () => { |
||||
hoverResult = { contents: ['foo'] } |
||||
let res = await hover.onHover('echo') |
||||
expect(res).toBe(true) |
||||
let msg = await helper.getCmdline() |
||||
expect(msg).toMatch('foo') |
||||
}) |
||||
|
||||
it('should show hover in float window', async () => { |
||||
hoverResult = { contents: { kind: 'markdown', value: '```typescript\nconst foo:number\n```' } } |
||||
await hover.onHover('float') |
||||
let win = await helper.getFloat() |
||||
expect(win).toBeDefined() |
||||
let lines = await nvim.eval(`getbufline(winbufnr(${win.id}),1,'$')`) |
||||
expect(lines).toEqual(['const foo:number']) |
||||
}) |
||||
}) |
||||
|
||||
describe('getHover', () => { |
||||
it('should get hover from MarkedString array', async () => { |
||||
hoverResult = { contents: ['foo', { language: 'typescript', value: 'bar' }] } |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return { contents: { language: 'typescript', value: 'MarkupContent hover' } } |
||||
} |
||||
})) |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return { contents: MarkedString.fromPlainText('MarkedString hover') } |
||||
} |
||||
})) |
||||
let res = await hover.getHover() |
||||
expect(res.includes('foo')).toBe(true) |
||||
expect(res.includes('bar')).toBe(true) |
||||
expect(res.includes('MarkupContent hover')).toBe(true) |
||||
expect(res.includes('MarkedString hover')).toBe(true) |
||||
}) |
||||
|
||||
it('should filter empty hover message', async () => { |
||||
hoverResult = { contents: [''] } |
||||
disposables.push(languages.registerHoverProvider([{ language: '*' }], { |
||||
provideHover: (_doc, _pos, _token) => { |
||||
return { contents: { kind: MarkupKind.PlainText, value: 'value' } } |
||||
} |
||||
})) |
||||
let res = await hover.getHover() |
||||
expect(res).toEqual(['value']) |
||||
}) |
||||
}) |
||||
|
||||
describe('definitionHover', () => { |
||||
it('should load definition from buffer', async () => { |
||||
hoverResult = { contents: 'string hover' } |
||||
let doc = await helper.createDocument() |
||||
await nvim.call('cursor', [1, 1]) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) |
||||
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { |
||||
provideDefinition() { |
||||
return [{ |
||||
targetUri: doc.uri, |
||||
targetRange: Range.create(0, 0, 1, 3), |
||||
targetSelectionRange: Range.create(0, 0, 0, 3), |
||||
}] |
||||
} |
||||
})) |
||||
await hover.definitionHover('preview') |
||||
let res = await getDocumentText() |
||||
expect(res).toBe('string hover\n\nfoo\nbar') |
||||
}) |
||||
|
||||
it('should load definition link from file', async () => { |
||||
let fsPath = await createTmpFile('foo\nbar\n') |
||||
hoverResult = { contents: 'string hover', range: Range.create(0, 0, 0, 3) } |
||||
let doc = await helper.createDocument() |
||||
await nvim.call('cursor', [1, 1]) |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'foo\nbar')]) |
||||
disposables.push(languages.registerDefinitionProvider([{ language: '*' }], { |
||||
provideDefinition() { |
||||
return [{ |
||||
targetUri: URI.file(fsPath).toString(), |
||||
targetRange: Range.create(0, 0, 1, 3), |
||||
targetSelectionRange: Range.create(0, 0, 0, 3), |
||||
}] |
||||
} |
||||
})) |
||||
await hover.definitionHover('preview') |
||||
let res = await getDocumentText() |
||||
expect(res).toBe('string hover\n\nfoo\nbar') |
||||
}) |
||||
|
||||
it('should return false when hover not found', async () => { |
||||
hoverResult = undefined |
||||
let res = await hover.definitionHover('float') |
||||
expect(res).toBe(false) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable } from 'vscode-languageserver-protocol' |
||||
import Handler from '../../handler/index' |
||||
import { disposeAll } from '../../util' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let handler: Handler |
||||
let disposables: Disposable[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
handler = (helper.plugin as any).handler |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
await helper.createDocument() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('Handler', () => { |
||||
describe('hasProvider', () => { |
||||
it('should check provider for document', async () => { |
||||
let res = await handler.hasProvider('definition') |
||||
expect(res).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('checkProvier', () => { |
||||
it('should throw error when provider not found', async () => { |
||||
let doc = await helper.createDocument() |
||||
let err |
||||
try { |
||||
handler.checkProvier('definition', doc.textDocument) |
||||
} catch (e) { |
||||
err = e |
||||
} |
||||
expect(err).toBeDefined() |
||||
}) |
||||
}) |
||||
|
||||
describe('withRequestToken', () => { |
||||
it('should cancel previous request when called again', async () => { |
||||
let cancelled = false |
||||
let p = handler.withRequestToken('test', token => { |
||||
return new Promise(s => { |
||||
token.onCancellationRequested(() => { |
||||
cancelled = true |
||||
clearTimeout(timer) |
||||
s(undefined) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
s(undefined) |
||||
}, 3000) |
||||
}) |
||||
}, false) |
||||
setTimeout(async () => { |
||||
await handler.withRequestToken('test', () => { |
||||
return Promise.resolve(undefined) |
||||
}, false) |
||||
}, 50) |
||||
await p |
||||
expect(cancelled).toBe(true) |
||||
}) |
||||
|
||||
it('should cancel request on insert start', async () => { |
||||
let cancelled = false |
||||
let p = handler.withRequestToken('test', token => { |
||||
return new Promise(s => { |
||||
token.onCancellationRequested(() => { |
||||
cancelled = true |
||||
clearTimeout(timer) |
||||
s(undefined) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
s(undefined) |
||||
}, 3000) |
||||
}) |
||||
}, false) |
||||
await nvim.input('i') |
||||
await p |
||||
expect(cancelled).toBe(true) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,410 @@
@@ -0,0 +1,410 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationTokenSource, Disposable, InlayHint, InlayHintKind, Position, Range, TextEdit } from 'vscode-languageserver-protocol' |
||||
import commands from '../../commands' |
||||
import InlayHintHandler from '../../handler/inlayHint/index' |
||||
import languages from '../../languages' |
||||
import { InlayHintWithProvider, isInlayHint, isValidInlayHint, sameHint } from '../../provider/inlayHintManager' |
||||
import { disposeAll } from '../../util' |
||||
import { CancellationError } from '../../util/errors' |
||||
import workspace from '../../workspace' |
||||
import helper, { createTmpFile } from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let handler: InlayHintHandler |
||||
let disposables: Disposable[] = [] |
||||
let ns: number |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
handler = helper.plugin.getHandler().inlayHintHandler |
||||
ns = await nvim.createNamespace('coc-inlayHint') |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
async function registerProvider(content: string): Promise<Disposable> { |
||||
let doc = await workspace.document |
||||
let disposable = languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: (document, range) => { |
||||
let content = document.getText(range) |
||||
let lines = content.split(/\r?\n/) |
||||
let hints: InlayHint[] = [] |
||||
for (let i = 0; i < lines.length; i++) { |
||||
let line = lines[i] |
||||
if (!line.length) continue |
||||
let parts = line.split(/\s+/) |
||||
let kind: InlayHintKind = i == 0 ? InlayHintKind.Type : InlayHintKind.Parameter |
||||
hints.push(...parts.map(s => InlayHint.create(Position.create(range.start.line + i, line.length), s, kind))) |
||||
} |
||||
return hints |
||||
} |
||||
}) |
||||
await doc.buffer.setLines(content.split(/\n/), { start: 0, end: -1 }) |
||||
await doc.synchronize() |
||||
return disposable |
||||
} |
||||
|
||||
async function waitRefresh(bufnr: number) { |
||||
let buf = handler.getItem(bufnr) |
||||
return new Promise<void>((resolve, reject) => { |
||||
let timer = setTimeout(() => { |
||||
reject(new Error('not refresh after 1s')) |
||||
}, 1000) |
||||
buf.onDidRefresh(() => { |
||||
clearTimeout(timer) |
||||
resolve() |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
describe('InlayHint', () => { |
||||
describe('utils', () => { |
||||
it('should check same hint', () => { |
||||
let hint = InlayHint.create(Position.create(0, 0), 'foo') |
||||
expect(sameHint(hint, InlayHint.create(Position.create(0, 0), 'bar'))).toBe(false) |
||||
expect(sameHint(hint, InlayHint.create(Position.create(0, 0), [{ value: 'foo' }]))).toBe(true) |
||||
}) |
||||
|
||||
it('should check valid hint', () => { |
||||
let hint = InlayHint.create(Position.create(0, 0), 'foo') |
||||
expect(isValidInlayHint(hint, Range.create(0, 0, 1, 0))).toBe(true) |
||||
expect(isValidInlayHint(InlayHint.create(Position.create(0, 0), ''), Range.create(0, 0, 1, 0))).toBe(false) |
||||
expect(isValidInlayHint(InlayHint.create(Position.create(3, 0), 'foo'), Range.create(0, 0, 1, 0))).toBe(false) |
||||
expect(isValidInlayHint({ label: 'f' } as any, Range.create(0, 0, 1, 0))).toBe(false) |
||||
}) |
||||
|
||||
it('should check inlayHint instance', async () => { |
||||
expect(isInlayHint(null)).toBe(false) |
||||
let position = Position.create(0, 0) |
||||
expect(isInlayHint({ position, label: null })).toBe(false) |
||||
expect(isInlayHint({ position, label: [{ value: '' }] })).toBe(true) |
||||
}) |
||||
}) |
||||
|
||||
describe('provideInlayHints', () => { |
||||
it('should throw when failed', async () => { |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: () => { |
||||
return Promise.reject(new Error('Test failure')) |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
let fn = async () => { |
||||
let tokenSource = new CancellationTokenSource() |
||||
await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token) |
||||
} |
||||
await expect(fn()).rejects.toThrow(Error) |
||||
}) |
||||
|
||||
it('should merge provide results', async () => { |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: () => { |
||||
return [InlayHint.create(Position.create(0, 0), 'foo')] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: () => { |
||||
return [ |
||||
InlayHint.create(Position.create(0, 0), 'foo'), |
||||
InlayHint.create(Position.create(1, 0), 'bar'), |
||||
InlayHint.create(Position.create(5, 0), 'bad')] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: () => { |
||||
return null |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
let tokenSource = new CancellationTokenSource() |
||||
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 3, 0), tokenSource.token) |
||||
expect(res.length).toBe(2) |
||||
}) |
||||
|
||||
it('should resolve inlay hint', async () => { |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: () => { |
||||
return [InlayHint.create(Position.create(0, 0), 'foo')] |
||||
}, |
||||
resolveInlayHint: hint => { |
||||
hint.tooltip = 'tooltip' |
||||
return hint |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
let tokenSource = new CancellationTokenSource() |
||||
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token) |
||||
let resolved = await languages.resolveInlayHint(res[0], tokenSource.token) |
||||
expect(resolved.tooltip).toBe('tooltip') |
||||
resolved = await languages.resolveInlayHint(resolved, tokenSource.token) |
||||
expect(resolved.tooltip).toBe('tooltip') |
||||
}) |
||||
|
||||
it('should not resolve when cancelled', async () => { |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: '*' }], { |
||||
provideInlayHints: () => { |
||||
return [InlayHint.create(Position.create(0, 0), 'foo')] |
||||
}, |
||||
resolveInlayHint: (hint, token) => { |
||||
return new Promise(resolve => { |
||||
token.onCancellationRequested(() => { |
||||
clearTimeout(timer) |
||||
resolve(null) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
resolve(Object.assign({}, hint, { tooltip: 'tooltip' })) |
||||
}, 200) |
||||
}) |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
let tokenSource = new CancellationTokenSource() |
||||
let res = await languages.provideInlayHints(doc.textDocument, Range.create(0, 0, 1, 0), tokenSource.token) |
||||
let p = languages.resolveInlayHint(res[0], tokenSource.token) |
||||
tokenSource.cancel() |
||||
let resolved = await p |
||||
expect(resolved.tooltip).toBeUndefined() |
||||
}) |
||||
}) |
||||
|
||||
describe('env & options', () => { |
||||
it('should not create when virtualText not supported', async () => { |
||||
Object.assign(workspace.env, { |
||||
virtualText: false |
||||
}) |
||||
disposables.push(Disposable.create(() => { |
||||
Object.assign(workspace.env, { |
||||
virtualText: true |
||||
}) |
||||
})) |
||||
let doc = await helper.createDocument() |
||||
let item = handler.getItem(doc.bufnr) |
||||
expect(item).toBeUndefined() |
||||
}) |
||||
|
||||
it('should not enabled when disabled by configuration', async () => { |
||||
helper.updateConfiguration('inlayHint.filetypes', []) |
||||
let doc = await workspace.document |
||||
let item = handler.getItem(doc.bufnr) |
||||
item.clearVirtualText() |
||||
expect(item.enabled).toBe(false) |
||||
helper.updateConfiguration('inlayHint.filetypes', ['dos']) |
||||
doc = await helper.createDocument() |
||||
item = handler.getItem(doc.bufnr) |
||||
expect(item.enabled).toBe(false) |
||||
}) |
||||
}) |
||||
|
||||
describe('configuration', () => { |
||||
it('should refresh on insert mode', async () => { |
||||
helper.updateConfiguration('inlayHint.refreshOnInsertMode', true) |
||||
let doc = await helper.createDocument() |
||||
let disposable = await registerProvider('foo\nbar') |
||||
disposables.push(disposable) |
||||
await nvim.input('i') |
||||
await doc.applyEdits([TextEdit.insert(Position.create(0, 0), 'baz\n')]) |
||||
await waitRefresh(doc.bufnr) |
||||
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) |
||||
let obj = markers[0][3].virt_text |
||||
expect(obj).toEqual([['baz', 'CocInlayHintType']]) |
||||
expect(markers[1][3].virt_text).toEqual([['foo', 'CocInlayHintParameter']]) |
||||
}) |
||||
|
||||
it('should disable parameter inlayHint', async () => { |
||||
helper.updateConfiguration('inlayHint.enableParameter', false) |
||||
let doc = await helper.createDocument() |
||||
let disposable = await registerProvider('foo\nbar') |
||||
disposables.push(disposable) |
||||
await waitRefresh(doc.bufnr) |
||||
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBe(1) |
||||
}) |
||||
|
||||
it('should use custom subseparator', async () => { |
||||
helper.updateConfiguration('inlayHint.subSeparator', '|') |
||||
let doc = await helper.createDocument() |
||||
let disposable = await registerProvider('foo bar') |
||||
disposables.push(disposable) |
||||
await waitRefresh(doc.bufnr) |
||||
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) |
||||
let virt_text = markers[0][3].virt_text |
||||
expect(virt_text[1]).toEqual(['|', 'CocInlayHintType']) |
||||
}) |
||||
}) |
||||
|
||||
describe('toggle inlayHint', () => { |
||||
it('should not throw when buffer not exists', async () => { |
||||
handler.toggle(9) |
||||
await commands.executeCommand('document.toggleInlayHint', 9) |
||||
}) |
||||
|
||||
it('should show message when inlayHint not supported', async () => { |
||||
let doc = await workspace.document |
||||
handler.toggle(doc.bufnr) |
||||
let cmdline = await helper.getCmdline() |
||||
expect(cmdline).toMatch(/not\sfound/) |
||||
}) |
||||
|
||||
it('should show message when not enabled', async () => { |
||||
helper.updateConfiguration('inlayHint.filetypes', []) |
||||
let doc = await helper.createDocument() |
||||
let disposable = await registerProvider('') |
||||
disposables.push(disposable) |
||||
handler.toggle(doc.bufnr) |
||||
let cmdline = await helper.getCmdline() |
||||
expect(cmdline).toMatch(/not\senabled/) |
||||
}) |
||||
|
||||
it('should toggle inlayHints', async () => { |
||||
let doc = await helper.createDocument() |
||||
let disposable = await registerProvider('foo\nbar') |
||||
disposables.push(disposable) |
||||
handler.toggle(doc.bufnr) |
||||
handler.toggle(doc.bufnr) |
||||
await helper.waitValue(async () => { |
||||
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) |
||||
return markers.length |
||||
}, 2) |
||||
}) |
||||
}) |
||||
|
||||
describe('render()', () => { |
||||
it('should refresh on vim mode', async () => { |
||||
let doc = await workspace.document |
||||
await nvim.setLine('foo bar') |
||||
let item = handler.getItem(doc.bufnr) |
||||
let r = Range.create(0, 0, 1, 0) |
||||
item.setVirtualText(r, [], true) |
||||
let hint: InlayHintWithProvider = { |
||||
label: 'string', |
||||
position: Position.create(0, 0), |
||||
providerId: '' |
||||
} |
||||
let paddingHint: InlayHintWithProvider = { |
||||
label: 'string', |
||||
position: Position.create(0, 3), |
||||
providerId: '', |
||||
paddingLeft: true, |
||||
paddingRight: true |
||||
} |
||||
item.setVirtualText(r, [hint, paddingHint], true) |
||||
await helper.waitValue(async () => { |
||||
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) |
||||
return markers.length |
||||
}, 2) |
||||
}) |
||||
|
||||
it('should not refresh when languageId not match', async () => { |
||||
let doc = await workspace.document |
||||
disposables.push(languages.registerInlayHintsProvider([{ language: 'javascript' }], { |
||||
provideInlayHints: () => { |
||||
let hint = InlayHint.create(Position.create(0, 0), 'foo') |
||||
return [hint] |
||||
} |
||||
})) |
||||
await nvim.setLine('foo') |
||||
await doc.synchronize() |
||||
await helper.wait(30) |
||||
let markers = await doc.buffer.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBe(0) |
||||
}) |
||||
|
||||
it('should refresh on text change', async () => { |
||||
let buf = await nvim.buffer |
||||
let disposable = await registerProvider('foo') |
||||
disposables.push(disposable) |
||||
await waitRefresh(buf.id) |
||||
await buf.setLines(['a', 'b', 'c'], { start: 0, end: -1 }) |
||||
await waitRefresh(buf.id) |
||||
let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBe(3) |
||||
let item = handler.getItem(buf.id) |
||||
await item.renderRange() |
||||
expect(item.current.length).toBe(3) |
||||
}) |
||||
|
||||
it('should refresh on insert leave', async () => { |
||||
let doc = await helper.createDocument() |
||||
let buf = doc.buffer |
||||
let disposable = await registerProvider('foo') |
||||
disposables.push(disposable) |
||||
await nvim.input('i') |
||||
await helper.wait(10) |
||||
await buf.setLines(['a', 'b', 'c'], { start: 0, end: -1 }) |
||||
await helper.wait(30) |
||||
let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBe(0) |
||||
await nvim.input('<esc>') |
||||
await waitRefresh(doc.bufnr) |
||||
markers = await buf.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBe(3) |
||||
}) |
||||
|
||||
it('should refresh on provider dispose', async () => { |
||||
let buf = await nvim.buffer |
||||
let disposable = await registerProvider('foo bar') |
||||
await waitRefresh(buf.id) |
||||
disposable.dispose() |
||||
let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBe(0) |
||||
let item = handler.getItem(buf.id) |
||||
expect(item.current.length).toBe(0) |
||||
await item.renderRange() |
||||
expect(item.current.length).toBe(0) |
||||
}) |
||||
|
||||
it('should refresh on scroll', async () => { |
||||
let arr = new Array(200) |
||||
let content = arr.fill('foo').join('\n') |
||||
let buf = await nvim.buffer |
||||
let disposable = await registerProvider(content) |
||||
disposables.push(disposable) |
||||
await waitRefresh(buf.id) |
||||
let markers = await buf.getExtMarks(ns, 0, -1, { details: true }) |
||||
let len = markers.length |
||||
await nvim.command('normal! G') |
||||
await waitRefresh(buf.id) |
||||
await nvim.input('<C-y>') |
||||
await waitRefresh(buf.id) |
||||
markers = await buf.getExtMarks(ns, 0, -1, { details: true }) |
||||
expect(markers.length).toBeGreaterThan(len) |
||||
}) |
||||
|
||||
it('should cancel previous render', async () => { |
||||
let buf = await nvim.buffer |
||||
let disposable = await registerProvider('foo') |
||||
disposables.push(disposable) |
||||
await waitRefresh(buf.id) |
||||
let item = handler.getItem(buf.id) |
||||
await item.renderRange() |
||||
await item.renderRange() |
||||
expect(item.current.length).toBe(1) |
||||
}) |
||||
|
||||
it('should resend request on CancellationError', async () => { |
||||
let called = 0 |
||||
let disposable = languages.registerInlayHintsProvider([{ language: 'vim' }], { |
||||
provideInlayHints: () => { |
||||
if (called == 0) { |
||||
called++ |
||||
throw new CancellationError() |
||||
} |
||||
return [] |
||||
} |
||||
}) |
||||
disposables.push(disposable) |
||||
let filepath = await createTmpFile('a\n\b\nc\n', disposables) |
||||
let doc = await helper.createDocument(filepath) |
||||
await nvim.command('setfiletype vim') |
||||
await waitRefresh(doc.buffer.id) |
||||
expect(called).toBe(1) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { CancellationToken, Disposable, InlineValueText, Range } from 'vscode-languageserver-protocol' |
||||
import languages from '../../languages' |
||||
import { disposeAll } from '../../util' |
||||
import workspace from '../../workspace' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let disposables: Disposable[] = [] |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
// hover = helper.plugin.getHandler().hover
|
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
await helper.createDocument() |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
describe('InlineValue', () => { |
||||
describe('InlineValueManager', () => { |
||||
it('should return false when provider not exists', async () => { |
||||
let doc = await workspace.document |
||||
let res = languages.hasProvider('inlineValue', doc.textDocument) |
||||
expect(res).toBe(false) |
||||
}) |
||||
|
||||
it('should return merged results', async () => { |
||||
disposables.push(languages.registerInlineValuesProvider([{ language: '*' }], { |
||||
provideInlineValues: () => { |
||||
return null |
||||
} |
||||
})) |
||||
disposables.push(languages.registerInlineValuesProvider([{ language: '*' }], { |
||||
provideInlineValues: () => { |
||||
return [ |
||||
InlineValueText.create(Range.create(0, 0, 0, 1), 'foo'), |
||||
InlineValueText.create(Range.create(0, 3, 0, 5), 'bar'), |
||||
] |
||||
} |
||||
})) |
||||
disposables.push(languages.registerInlineValuesProvider([{ language: '*' }], { |
||||
provideInlineValues: () => { |
||||
return [ |
||||
InlineValueText.create(Range.create(0, 0, 0, 1), 'foo'), |
||||
] |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
let res = await languages.provideInlineValues(doc.textDocument, Range.create(0, 0, 3, 0), { frameId: 3, stoppedLocation: Range.create(0, 0, 0, 3) }, CancellationToken.None) |
||||
expect(res.length).toBe(2) |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,169 @@
@@ -0,0 +1,169 @@
|
||||
import { Neovim } from '@chemzqm/neovim' |
||||
import { Disposable, Range, Position } from 'vscode-languageserver-protocol' |
||||
import LinkedEditingHandler from '../../handler/linkedEditing' |
||||
import languages from '../../languages' |
||||
import workspace from '../../workspace' |
||||
import { disposeAll } from '../../util' |
||||
import helper from '../helper' |
||||
|
||||
let nvim: Neovim |
||||
let handler: LinkedEditingHandler |
||||
let disposables: Disposable[] = [] |
||||
let wordPattern: string | undefined |
||||
beforeAll(async () => { |
||||
await helper.setup() |
||||
nvim = helper.nvim |
||||
handler = helper.plugin.getHandler().linkedEditingHandler |
||||
}) |
||||
|
||||
afterAll(async () => { |
||||
await helper.shutdown() |
||||
}) |
||||
|
||||
beforeEach(async () => { |
||||
helper.updateConfiguration('coc.preferences.enableLinkedEditing', true) |
||||
}) |
||||
|
||||
afterEach(async () => { |
||||
disposeAll(disposables) |
||||
await helper.reset() |
||||
}) |
||||
|
||||
async function registerProvider(content: string, position: Position): Promise<void> { |
||||
let doc = await workspace.document |
||||
disposables.push(languages.registerLinkedEditingRangeProvider([{ language: '*' }], { |
||||
provideLinkedEditingRanges: (doc, pos) => { |
||||
let document = workspace.getDocument(doc.uri) |
||||
let range = document.getWordRangeAtPosition(pos) |
||||
if (!range) return null |
||||
let text = doc.getText(range) |
||||
let ranges: Range[] = document.getSymbolRanges(text) |
||||
return { ranges, wordPattern } |
||||
} |
||||
})) |
||||
await nvim.setLine(content) |
||||
await doc.synchronize() |
||||
await handler.enable(doc, position) |
||||
} |
||||
|
||||
async function matches(): Promise<number> { |
||||
let res = await nvim.call('getmatches') as any[] |
||||
res = res.filter(o => o.group === 'CocLinkedEditing') |
||||
return res.length |
||||
} |
||||
|
||||
describe('LinkedEditing', () => { |
||||
it('should active and cancel on cursor moved', async () => { |
||||
await registerProvider('foo foo a ', Position.create(0, 0)) |
||||
expect(await matches()).toBe(2) |
||||
await nvim.command(`normal! $`) |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 0) |
||||
}) |
||||
|
||||
it('should active when moved to another word', async () => { |
||||
await registerProvider('foo foo bar bar bar', Position.create(0, 0)) |
||||
await nvim.call('cursor', [1, 9]) |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 3) |
||||
}) |
||||
|
||||
it('should active on text change', async () => { |
||||
let doc = await workspace.document |
||||
await registerProvider('foo foo a ', Position.create(0, 0)) |
||||
await nvim.call('cursor', [1, 1]) |
||||
await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 0, ['i']]) |
||||
await doc.synchronize() |
||||
let line = await nvim.line |
||||
expect(line).toBe('ifoo ifoo a ') |
||||
await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 0, 0, 1, []]) |
||||
await doc.synchronize() |
||||
line = await nvim.line |
||||
expect(line).toBe('foo foo a ') |
||||
}) |
||||
|
||||
it('should cancel when change out of range', async () => { |
||||
let doc = await workspace.document |
||||
await registerProvider('foo foo bar', Position.create(0, 0)) |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 2) |
||||
await nvim.call('nvim_buf_set_text', [doc.bufnr, 0, 9, 0, 10, ['']]) |
||||
await doc.synchronize() |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 0) |
||||
}) |
||||
|
||||
it('should cancel on editor change', async () => { |
||||
await registerProvider('foo foo a ', Position.create(0, 0)) |
||||
await nvim.command(`enew`) |
||||
await helper.wait(50) |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 0) |
||||
}) |
||||
|
||||
it('should cancel when insert none word character', async () => { |
||||
await registerProvider('foo foo a ', Position.create(0, 0)) |
||||
await nvim.call('cursor', [1, 4]) |
||||
await nvim.input('i') |
||||
await nvim.input('a') |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 2) |
||||
await nvim.input('i') |
||||
await nvim.input('@') |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 0) |
||||
}) |
||||
|
||||
it('should cancel when insert not match wordPattern', async () => { |
||||
wordPattern = '[A-Z]' |
||||
await registerProvider('foo foo a ', Position.create(0, 0)) |
||||
await nvim.call('cursor', [1, 4]) |
||||
await nvim.input('i') |
||||
await nvim.input('A') |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 2) |
||||
await nvim.input('i') |
||||
await nvim.input('3') |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 0) |
||||
}) |
||||
|
||||
it('should cancel request on cursor moved', async () => { |
||||
disposables.push(languages.registerLinkedEditingRangeProvider([{ language: '*' }], { |
||||
provideLinkedEditingRanges: (doc, pos, token) => { |
||||
return new Promise(resolve => { |
||||
token.onCancellationRequested(() => { |
||||
clearTimeout(timer) |
||||
resolve(null) |
||||
}) |
||||
let timer = setTimeout(() => { |
||||
let document = workspace.getDocument(doc.uri) |
||||
let range = document.getWordRangeAtPosition(pos) |
||||
if (!range) return resolve(null) |
||||
let text = doc.getText(range) |
||||
let ranges: Range[] = document.getSymbolRanges(text) |
||||
resolve({ ranges, wordPattern }) |
||||
}, 1000) |
||||
}) |
||||
} |
||||
})) |
||||
let doc = await workspace.document |
||||
await nvim.setLine('foo foo ') |
||||
await doc.synchronize() |
||||
await nvim.call('cursor', [1, 2]) |
||||
await helper.wait(10) |
||||
await nvim.call('cursor', [1, 9]) |
||||
await helper.waitValue(() => { |
||||
return matches() |
||||
}, 0) |
||||
}) |
||||
}) |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue