Skip to content

Commit

Permalink
Support of ultisnips snippet (#3682)
Browse files Browse the repository at this point in the history
The goal is better support for ultinips python features, including:

- Dependencies of placeholder with python block, like:
    ``` snippet
    ${1:`!p snip.rv = t[2]`} ${2:`!p snip.rv = t[3]`} ${3:`!p snip.rv = t[4][0]`} ${4:bar}
    ```
- Variables like `snip.c`:
    ``` snippet
	#ifndef ${1:`!p
	if not snip.c:
		import random, string
		name = re.sub(r'[^A-Za-z0-9]+','_', snip.fn).upper()
		rand = ''.join(random.sample(string.ascii_letters+string.digits, 8))
		snip.rv = ('%s_%s' % (name,rand)).upper()
	else:
		snip.rv = snip.c`}
	#define $1
    ```
- Calculate code blocks on placeholder change, like:
    ``` snippet
    `!p
    box = make_box(len(t[1]))
    snip.rv = box[0]
    snip += box[1]
    `${1:${VISUAL:content}}`!p
    box = make_box(len(t[1]))
    snip.rv = box[2]
    snip += box[3]`
    $0
    ```

Other changes:

- Fixed the changed placeholder could be wrong.
- Add highlight support for current active placeholders.
- Improved text edit on change by reduce the range as much as possible.
- Nested placeholder support when insert nested snippet.
- New synchronize logic which support cancellation.
- Not jump to last placeholder when invoke `g:coc_snippet_prev` on first placeholder
- Use `<Cmd>` for key mapping to avoid mode change.
- Improve python code error by include python code.
- Improve snippet preview by resolve python code.

New VSCode snippet variables:

- RELATIVE_FILEPATH
- RANDOM
- RANDOM_HEX
- UUID
- BLOCK_COMMENT_START
- BLOCK_COMMENT_END
- LINE_COMMENT
- WORKSPACE_NAME
- WORKSPACE_FOLDER
  • Loading branch information
chemzqm authored Mar 10, 2022
1 parent 33d8a71 commit 8401279
Show file tree
Hide file tree
Showing 40 changed files with 3,096 additions and 1,435 deletions.
12 changes: 10 additions & 2 deletions autoload/coc/compat.vim
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function! coc#compat#buf_del_var(bufnr, name) abort
if exists('*nvim_buf_del_var')
silent! call nvim_buf_del_var(a:bufnr, a:name)
else
if bufnr == bufnr('%')
if a:bufnr == bufnr('%')
execute 'unlet! b:'.a:name
elseif exists('*win_execute')
let winid = coc#compat#buf_win_id(a:bufnr)
Expand Down Expand Up @@ -150,6 +150,14 @@ function! coc#compat#matchaddgroups(winid, groups) abort
endif
endfunction

function! coc#compat#del_var(name) abort
if exists('*nvim_del_var')
silent! call nvim_del_var(a:name)
else
execute 'unlet! '.a:name
endif
endfunction

" remove keymap for specific buffer
function! coc#compat#buf_del_keymap(bufnr, mode, lhs) abort
if !bufloaded(a:bufnr)
Expand All @@ -170,7 +178,7 @@ function! coc#compat#buf_del_keymap(bufnr, mode, lhs) abort
if exists('*win_execute')
let winid = coc#compat#buf_win_id(a:bufnr)
if winid != -1
call win_execute(winid, 'silent! '.a:mode.'unmap <buffer> '.a:lhs)
call win_execute(winid, a:mode.'unmap <buffer> '.a:lhs, 'silent!')
endif
endif
endfunction
Expand Down
2 changes: 1 addition & 1 deletion autoload/coc/highlight.vim
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ function! coc#highlight#ranges(bufnr, key, hlGroup, ranges, ...) abort
endif
" TODO don't know how to count UTF16 code point, should work most cases.
let colStart = lnum == start['line'] + 1 ? strlen(strcharpart(line, 0, start['character'])) : 0
let colEnd = lnum == end['line'] + 1 ? strlen(strcharpart(line, 0, end['character'])) : -1
let colEnd = lnum == end['line'] + 1 ? strlen(strcharpart(line, 0, end['character'])) : strlen(line)
if colStart == colEnd
continue
endif
Expand Down
7 changes: 4 additions & 3 deletions autoload/coc/snippet.vim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
scriptencoding utf-8
let s:is_vim = !has('nvim')
let s:map_next = 1
let s:cmd_mapping = has('nvim') || has('patch-8.2.1978')

function! coc#snippet#_select_mappings()
if !get(g:, 'coc_selectmode_mapping', 1)
Expand Down Expand Up @@ -38,7 +39,6 @@ function! coc#snippet#enable()
return
endif
let b:coc_snippet_active = 1
silent! unlet g:coc_selected_text
call coc#snippet#_select_mappings()
let nextkey = get(g:, 'coc_snippet_next', '<C-j>')
let prevkey = get(g:, 'coc_snippet_prev', '<C-k>')
Expand All @@ -48,9 +48,10 @@ function! coc#snippet#enable()
if s:map_next
execute 'inoremap <buffer><nowait><silent>'.nextkey." <C-R>=coc#rpc#request('snippetNext', [])<cr>"
endif
let pre = s:cmd_mapping ? '<Cmd>' : '<Esc>'
execute 'inoremap <buffer><nowait><silent>'.prevkey." <C-R>=coc#rpc#request('snippetPrev', [])<cr>"
execute 'snoremap <buffer><nowait><silent>'.prevkey." <Esc>:call coc#rpc#request('snippetPrev', [])<cr>"
execute 'snoremap <buffer><nowait><silent>'.nextkey." <Esc>:call coc#rpc#request('snippetNext', [])<cr>"
execute 'snoremap <buffer><nowait><silent>'.prevkey." ".pre.":call coc#rpc#request('snippetPrev', [])<cr>"
execute 'snoremap <buffer><nowait><silent>'.nextkey." ".pre.":call coc#rpc#request('snippetNext', [])<cr>"
endfunction

function! coc#snippet#disable()
Expand Down
3 changes: 3 additions & 0 deletions autoload/health/coc.vim
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ function! s:checkEnvironment() abort
silent pyx print("")
catch /.*/
call health#report_warn('pyx command not work, some extensions may fail to work, checkout ":h pythonx"')
if has('nvim')
call health#report_warn('Install pynvim by command: pip install pynvim --upgrade')
endif
endtry
endif
return valid
Expand Down
5 changes: 5 additions & 0 deletions data/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,11 @@
"default": "SNIP",
"description": "Text shown in statusline to indicate snippet session is activated."
},
"coc.preferences.snippetHighlight": {
"type": "boolean",
"description": "Use highlight group 'CocSnippetVisual' to highlight placeholders with same index of current one.",
"default": true
},
"coc.preferences.currentFunctionSymbolAutoUpdate": {
"type": "boolean",
"description": "Automatically update the value of b:coc_current_function on CursorHold event",
Expand Down
8 changes: 8 additions & 0 deletions doc/coc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,13 @@ Built-in configurations:~

Valid options: ["daily","weekly","never"]

"coc.preferences.snippetHighlight":~

Use highlight group 'CocSnippetVisual' to highlight placeholders with
same index of current one.

default: `true`

"coc.preferences.snippetStatusText":~

Text shown in 'statusline' to indicate snippet session is activate.
Expand Down Expand Up @@ -3036,6 +3043,7 @@ Others~
*CocMenuSel* for current menu item in menu dialog, works on neovim only since
vim doesn't support change highlight group of cursorline inside popup.
*CocSelectedRange* for highlight ranges of outgoing calls.
*CocSnippetVisual* for highlight snippet placeholders.

Semantic highlights~
*coc-semantic-highlights*
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
"vscode-languageserver": "7.0.0"
},
"dependencies": {
"@chemzqm/neovim": "^5.7.4",
"@chemzqm/neovim": "^5.7.5",
"ansi-styles": "^5.0.0",
"bytes": "^3.1.0",
"cli-table": "^0.3.4",
Expand Down
2 changes: 2 additions & 0 deletions plugin/coc.vim
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ function! s:Hi() abort
hi default link CocCursorRange Search
hi default link CocHighlightRead CocHighlightText
hi default link CocHighlightWrite CocHighlightText
" Snippet
hi default link CocSnippetVisual Visual
" Tree view highlights
hi default link CocTreeTitle Title
hi default link CocTreeDescription Comment
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/core/editors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('editors', () => {
resolve(e)
}, null, disposables)
})
await nvim.command('vs')
await nvim.command('sp')
let editor = await promise
let winid = await nvim.call('win_getid')
expect(editor.winid).toBe(winid)
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/core/terminals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ describe('create terminal', () => {
})
let res = await terminal.show(true)
expect(res).toBe(true)
expect(typeof terminal.bufnr).toBe('number')
let winid = await nvim.call('bufwinid', [terminal.bufnr])
let curr = await nvim.call('win_getid', [])
expect(winid != curr).toBe(true)
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/handler/outline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ describe('symbols outline', () => {
strictIndexing: false
})
await doc.synchronize()
await helper.wait(200)
buf = await getOutlineBuffer()
await helper.waitFor('eval', [`getbufline(${buf.id},1)[0]`], /No\sresults/)
let lines = await buf.lines
expect(lines).toEqual([
'No results',
Expand Down Expand Up @@ -355,10 +355,10 @@ describe('symbols outline', () => {
await createBuffer()
let bufnr = await nvim.call('bufnr', ['%'])
await symbols.showOutline(0)
await helper.waitFor('getline', [1], 'OUTLINE')
await helper.waitFor('getline', [3], /fun1/)
await nvim.command('exe 3')
await nvim.input('<tab>')
await helper.wait(50)
await helper.waitFloat()
await nvim.input('<cr>')
await helper.waitFor('mode', [], 'v')
let buf = await nvim.buffer
Expand Down
8 changes: 4 additions & 4 deletions src/__tests__/handler/semanticTokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,16 +401,16 @@ describe('semanticTokens', () => {

describe('rangeProvider', () => {
it('should invoke range provider first time when both kind exists', async () => {
let t = 0
let fn = jest.fn()
disposables.push(registerRangeProvider('rust', () => {
t++
fn()
return []
}))
let buf = await createRustBuffer()
let item = highlighter.getItem(buf.id)
await item.waitRefresh()
await helper.wait(30)
expect(t).toBe(1)
expect(fn).toBeCalled()
})

it('should do range highlight first time', async () => {
Expand Down Expand Up @@ -445,7 +445,7 @@ describe('semanticTokens', () => {
return []
}))
await nvim.command('normal! G')
await helper.wait(50)
await helper.wait(100)
expect(r).toBeDefined()
expect(r.end).toEqual({ line: 201, character: 0 })
})
Expand Down
33 changes: 0 additions & 33 deletions src/__tests__/handler/signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,39 +89,6 @@ describe('signatureHelp', () => {
let lines = await helper.getWinLines(win.id)
expect(lines.join('\n')).toMatch(/description/)
})

it('should consider coc_last_placeholder on select mode', async () => {
let pos: Position
disposables.push(languages.registerSignatureHelpProvider([{ scheme: 'file' }], {
provideSignatureHelp: (_doc, position) => {
pos = position
return {
signatures: [
SignatureInformation.create('foo(a, b)', 'my signature', ParameterInformation.create('a', 'description')),
],
activeParameter: 1,
activeSignature: null
}
}
}, []))
let doc = await helper.createDocument()
let line = await nvim.call('line', ['.'])
await nvim.setLine(' fn(abc, def)')
await nvim.command('normal! 0fave')
await nvim.input('<C-g>')
let placeholder = {
bufnr: doc.bufnr,
start: Position.create(line - 1, 5),
end: Position.create(line - 1, 8)
}
await nvim.setVar('coc_last_placeholder', placeholder)
let m = await nvim.mode
expect(m.mode).toBe('s')
await signature.triggerSignatureHelp()
let win = await helper.getFloat()
expect(win).toBeDefined()
expect(pos).toEqual(Position.create(0, 5))
})
})

describe('events', () => {
Expand Down
13 changes: 11 additions & 2 deletions src/__tests__/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ export class Helper extends EventEmitter {
this.setMaxListeners(99)
}

public setupNvim(): void {
const vimrc = path.resolve(__dirname, 'vimrc')
let proc = this.proc = cp.spawn('nvim', ['-u', vimrc, '-i', 'NONE', '--embed'], {
cwd: __dirname
})
let plugin = attach({ proc })
this.nvim = plugin.nvim
}

public setup(): Promise<void> {
const vimrc = path.resolve(__dirname, 'vimrc')
let proc = this.proc = cp.spawn('nvim', ['-u', vimrc, '-i', 'NONE', '--embed'], {
Expand Down Expand Up @@ -79,7 +88,7 @@ export class Helper extends EventEmitter {
}

public async shutdown(): Promise<void> {
this.plugin.dispose()
if (this.plugin) this.plugin.dispose()
this.nvim.removeAllListeners()
this.nvim = null
if (this.proc) {
Expand Down Expand Up @@ -278,7 +287,7 @@ export class Helper extends EventEmitter {
for (let i = 0; i < 40; i++) {
await this.wait(50)
let res = await this.nvim.call(method, args) as T
if (res == value) {
if (res == value || (value instanceof RegExp && value.test(res.toString()))) {
find = true
break
}
Expand Down
43 changes: 7 additions & 36 deletions src/__tests__/modules/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { Neovim } from '@chemzqm/neovim'
import { Disposable } from 'vscode-jsonrpc'
import { CompletionItem, CompletionList, InsertTextFormat, Position, Range, TextEdit } from 'vscode-languageserver-types'
import completion from '../../completion'
import events from '../../events'
import languages from '../../languages'
import { CompletionItemProvider } from '../../provider'
import snippetManager from '../../snippets/manager'
import sources from '../../sources'
import { CompleteOption, CompleteResult, ISource, SourceType } from '../../types'
import { disposeAll } from '../../util'
import workspace from '../../workspace'
import events from '../../events'
import helper from '../helper'

let nvim: Neovim
Expand Down Expand Up @@ -130,7 +130,6 @@ describe('completion resumeCompletion', () => {

it('should stop if no filtered items', async () => {
await nvim.setLine('foo ')
await helper.wait(50)
await nvim.input('Af')
await helper.waitPopup()
expect(completion.isActivated).toBe(true)
Expand Down Expand Up @@ -482,9 +481,7 @@ describe('completion TextChangedP', () => {
await nvim.input('i?')
await helper.waitPopup()
await nvim.eval('feedkeys("\\<C-n>", "in")')
await helper.wait(200)
let line = await nvim.line
expect(line).toBe('?foo')
await helper.waitFor('getline', ['.'], '?foo')
})

it('should fix cursor position with snippet on additionalTextEdits', async () => {
Expand All @@ -503,16 +500,7 @@ describe('completion TextChangedP', () => {
let res = await helper.getItems()
let idx = res.findIndex(o => o.menu == '[edit]')
await helper.selectCompleteItem(idx)
let line: string
for (let i = 0; i < 40; i++) {
await helper.wait(50)
line = await nvim.line
if (line == 'bar if()') break
}
expect(line).toBe('bar if()')
let [, lnum, col] = await nvim.call('getcurpos')
expect(lnum).toBe(1)
expect(col).toBe(8)
await helper.waitFor('col', ['.'], 8)
})

it('should fix cursor position with plain text snippet on additionalTextEdits', async () => {
Expand Down Expand Up @@ -553,10 +541,8 @@ describe('completion TextChangedP', () => {
await nvim.input('if')
await helper.waitPopup()
await helper.selectCompleteItem(0)
await helper.wait(200)
let line = await nvim.line
await helper.waitFor('getline', ['.'], 'bar func(do)')
let [, lnum, col] = await nvim.call('getcurpos')
expect(line).toBe('bar func(do)')
expect(lnum).toBe(1)
expect(col).toBe(12)
})
Expand All @@ -582,7 +568,7 @@ describe('completion TextChangedP', () => {
await helper.selectCompleteItem(idx)
await helper.waitFor('getline', ['.'], 'foo = foo0bar1')
await helper.wait(50)
expect(snippetManager.isActived(doc.bufnr)).toBe(true)
expect(snippetManager.session).toBeDefined()
let [, lnum, col] = await nvim.call('getcurpos')
expect(lnum).toBe(1)
expect(col).toBe(3)
Expand Down Expand Up @@ -839,9 +825,8 @@ describe('completion TextChangedI', () => {
helper.updateConfiguration('suggest.acceptSuggestionOnCommitCharacter', true)
helper.updateConfiguration('suggest.noselect', false)
let source: ISource = {
priority: 0,
enable: true,
name: 'slow',
name: 'commit',
sourceType: SourceType.Service,
triggerCharacters: ['.'],
doComplete: (opt: CompleteOption): Promise<CompleteResult> => {
Expand All @@ -856,20 +841,6 @@ describe('completion TextChangedI', () => {
await nvim.input('if')
await helper.waitPopup()
await nvim.input('.')
await helper.wait(100)
let line = await nvim.line
expect(line).toBe('foo.')
})

it('should cancel completion with same pretext', async () => {
await nvim.setLine('foo')
await nvim.input('of')
await helper.waitPopup()
await nvim.input('<space><bs>')
await helper.wait(100)
let line = await nvim.line
let visible = await nvim.call('pumvisible')
expect(line).toBe('f')
expect(visible).toBe(0)
await helper.waitFor('getline', ['.'], 'foo.')
})
})
Loading

0 comments on commit 8401279

Please sign in to comment.