diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b5ba17a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = off + +[*.{yaml,yml}] +indent_size = 2 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index ccdeccc..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: [hrsh7th] diff --git a/.github/workflows/linux_neovim.yml b/.github/workflows/linux_neovim.yml index 18231a8..3c5ca68 100644 --- a/.github/workflows/linux_neovim.yml +++ b/.github/workflows/linux_neovim.yml @@ -46,4 +46,3 @@ jobs: themis ./spec export VIRTUALEDIT=1 themis ./spec - diff --git a/.themisrc b/.themisrc index 66f6134..91e60db 100644 --- a/.themisrc +++ b/.themisrc @@ -3,25 +3,37 @@ call themis#option('exclude', '\.vim$') set shiftwidth=2 set expandtab + if get(environ(), 'VIRTUALEDIT', '0') == '1' set virtualedit=all else set virtualedit= endif +if get(environ(), 'EXCLUSIVE', '0') == '1' + set selection=exclusive +else + set selection=inclusive +endif let g:vsnip_test_mode = v:true let g:vsnip_snippet_dir = fnamemodify(expand(''), ':h') . '/misc' -let &runtimepath .= ',' . g:vsnip_snippet_dir . '/source_spec_vscode' +let g:vsnip_deactivate_on = g:vsnip#DeactivateOn.OutsideOfSnippet let g:vsnip_filetypes = {} let g:vsnip_filetypes.source_spec_enhanced = ['source_spec'] -imap vsnip#expandable() ? '(vsnip-expand)' : '' -smap vsnip#expandable() ? '(vsnip-expand)' : '' +let &runtimepath .= ',' . g:vsnip_snippet_dir . '/source_spec_vscode' + +inoremap (vsnip-C-j) +inoremap (vsnip-Tab) +inoremap (vsnip-S-Tab) + +imap vsnip#expandable() ? '(vsnip-expand)' : '(vsnip-C-j)' +smap vsnip#expandable() ? '(vsnip-expand)' : '(vsnip-C-j)' -imap vsnip#available(1) ? '(vsnip-expand-or-jump)' : '' -smap vsnip#jumpable(1) ? '(vsnip-jump-next)' : '' -imap vsnip#jumpable(-1) ? '(vsnip-jump-prev)' : '' -smap vsnip#jumpable(-1) ? '(vsnip-jump-prev)' : '' +imap vsnip#available(1) ? '(vsnip-expand-or-jump)' : '(vsnip-Tab)' +smap vsnip#jumpable(1) ? '(vsnip-jump-next)' : '(vsnip-Tab)' +imap vsnip#jumpable(-1) ? '(vsnip-jump-prev)' : '(vsnip-S-Tab)' +smap vsnip#jumpable(-1) ? '(vsnip-jump-prev)' : '(vsnip-S-Tab)' imap (vsnip-assert) =assert() nmap (vsnip-assert) assert() diff --git a/README.md b/README.md index 5e646f6..bcec69b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # vim-vsnip -VSCode(LSP)'s snippet feature in vim. +VSCode(LSP)'s snippet feature in vim/nvim. # Features @@ -14,16 +14,23 @@ VSCode(LSP)'s snippet feature in vim. - LSP-client - [vim-lsp](https://github.com/prabirshrestha/vim-lsp) - [vim-lsc](https://github.com/natebosch/vim-lsc) + - [yegappan-lsp](https://github.com/yegappan/lsp) - [LanguageClient-neovim](https://github.com/autozimu/LanguageClient-neovim) - [neovim built-in lsp](https://github.com/neovim/neovim) - - [vim-lamp](https://github.com/hrsh7th/vim-lamp) - completion-engine - - [deoplete.nvim](https://github.com/Shougo/deoplete.nvim) - [asyncomplete.vim](https://github.com/prabirshrestha/asyncomplete.vim) - [vim-mucomplete](https://github.com/lifepillar/vim-mucomplete) - - [completion-nvim](https://github.com/nvim-lua/completion-nvim) + - [vimcomplete](https://github.com/girishji/vimcomplete) + - [ddc.vim](https://github.com/Shougo/ddc.vim) + - [vim-easycompletion](https://github.com/jayli/vim-easycomplete) +- Support built-in completion + - A function can be registered for finding completions. + `set complete+=Fvsnip#completefunc` - Vim script interpolation - You can use Vim script interpolation as `${VIM:...Vim script expression...}`. +- SnipMate-like syntax support + - Snippet files in SnipMate format with the extension `.snippets` can be load. + - NOTE: Full compatibility is not guaranteed. It is intended to easily create user-defined snippets. # Concept @@ -116,7 +123,7 @@ The snippet format was described in [here](https://code.visualstudio.com/docs/ed # Recipe -### $TM_FILENAME_BASE +### $TM\_FILENAME\_BASE You can insert the filename via `fname\(vsnip-expand)`. @@ -129,7 +136,7 @@ You can insert the filename via `fname\(vsnip-expand)`. } ``` -### Log $TM_SELECTED_TEXT +### Log $TM\_SELECTED\_TEXT You can fill `$TM_SELECTED_TEXT` by `(vsnip-select-text)` or `(vsnip-cut-text)`. @@ -193,4 +200,3 @@ You can run `npm run test` after install [vim-themis](https://github.com/thinca/ 1. compute the `user-diff` ... `s:Session.flush_changes` 2. reflect the `user-diff` to snippet ast ... `s:Snippet.follow` 3. reflect the `sync-diff` to buffer content ... `s:Snippet.sync & s:Session.flush_changes` - diff --git a/autoload/vital/_vsnip/VS/LSP/TextEdit.vim b/autoload/vital/_vsnip/VS/LSP/TextEdit.vim index 7d6504f..d4a3f36 100644 --- a/autoload/vital/_vsnip/VS/LSP/TextEdit.vim +++ b/autoload/vital/_vsnip/VS/LSP/TextEdit.vim @@ -28,16 +28,21 @@ endfunction " apply " function! s:apply(path, text_edits) abort - let l:current_bufname = bufname('%') - let l:current_position = s:Position.cursor() + let l:current_bufnr = bufnr('%') + for l:bufnr in range(1, bufnr('$')) + if index([fnamemodify(a:path, ':p'),fnameescape(fnamemodify(a:path, ':p'))], bufname(l:bufnr)) != -1 + let l:target_bufnr = l:bufnr + break + endif + endfor + if !exists('l:target_bufnr') + let l:target_bufnr = s:Buffer.ensure(a:path) + endif - let l:target_bufnr = s:_switch(a:path) - call s:_substitute(l:target_bufnr, a:text_edits, l:current_position) - let l:current_bufnr = s:_switch(l:current_bufname) + let l:current_position = s:Position.cursor() + call s:Buffer.do(l:target_bufnr, { -> s:_substitute(l:target_bufnr, a:text_edits, l:current_position) }) - if l:current_bufnr == l:target_bufnr - call cursor(s:Position.lsp_to_vim('%', l:current_position)) - endif + call cursor(s:Position.lsp_to_vim('%', l:current_position)) endfunction " @@ -167,19 +172,3 @@ function! s:_fix_text_edits(bufnr, text_edits) abort return [l:fixeol, l:text_edits] endfunction -" -" _switch -" -function! s:_switch(path) abort - let l:curr = bufnr('%') - let l:next = bufnr(a:path) - if l:next >= 0 - if l:curr != l:next - execute printf('noautocmd keepalt keepjumps %sbuffer!', bufnr(a:path)) - endif - else - execute printf('noautocmd keepalt keepjumps edit! %s', fnameescape(a:path)) - endif - return bufnr('%') -endfunction - diff --git a/autoload/vital/_vsnip/VS/Vim/Buffer.vim b/autoload/vital/_vsnip/VS/Vim/Buffer.vim index b5c10f3..f1275a7 100644 --- a/autoload/vital/_vsnip/VS/Vim/Buffer.vim +++ b/autoload/vital/_vsnip/VS/Vim/Buffer.vim @@ -4,7 +4,7 @@ function! s:_SID() abort return matchstr(expand(''), '\zs\d\+\ze__SID$') endfunction -execute join(['function! vital#_vsnip#VS#Vim#Buffer#import() abort', printf("return map({'get_line_count': '', 'do': '', 'create': '', 'pseudo': '', 'ensure': '', 'load': ''}, \"vital#_vsnip#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") +execute join(['function! vital#_vsnip#VS#Vim#Buffer#import() abort', printf("return map({'add': '', 'do': '', 'create': '', 'get_line_count': '', 'pseudo': '', 'ensure': '', 'load': ''}, \"vital#_vsnip#function('%s_' . v:key)\")", s:_SID()), 'endfunction'], "\n") delfunction s:_SID " ___vital___ let s:Do = { -> {} } @@ -51,11 +51,25 @@ function! s:ensure(expr) abort if type(a:expr) == type(0) throw printf('VS.Vim.Buffer: `%s` is not valid expr.', a:expr) endif - badd `=a:expr` + call s:add(a:expr) endif return bufnr(a:expr) endfunction +" +" add +" +if exists('*bufadd') + function! s:add(name) abort + let l:bufnr = bufadd(a:name) + call setbufvar(l:bufnr, '&buflisted', 1) + endfunction +else + function! s:add(name) abort + badd `=a:name` + endfunction +endif + " " load " diff --git a/autoload/vital/vsnip.vim b/autoload/vital/vsnip.vim index 6730f4e..5b7b102 100644 --- a/autoload/vital/vsnip.vim +++ b/autoload/vital/vsnip.vim @@ -190,6 +190,8 @@ function! s:_format_throwpoint(throwpoint) abort return join(funcs, "\n") endfunction +" @vimlint(EVL102, 1, l:_) +" @vimlint(EVL102, 1, l:__) function! s:_get_func_info(name) abort let name = a:name if a:name =~# '^\d\+$' " is anonymous-function @@ -213,8 +215,10 @@ function! s:_get_func_info(name) abort \ 'attrs': filter(['dict', 'abort', 'range', 'closure'], 'signature =~# (").*" . v:val)'), \ } endfunction +" @vimlint(EVL102, 0, l:__) +" @vimlint(EVL102, 0, l:_) -" s:_get_module() returns module object wihch has all script local functions. +" s:_get_module() returns module object which has all script local functions. function! s:_get_module(name) abort dict let funcname = s:_import_func_name(self.plugin_name(), a:name) try diff --git a/autoload/vital/vsnip.vital b/autoload/vital/vsnip.vital index 8bc4646..f1d4801 100644 --- a/autoload/vital/vsnip.vital +++ b/autoload/vital/vsnip.vital @@ -1,5 +1,5 @@ vsnip -2755f0c8fbd3442bcb7f567832e4d1455b57f9a2 +4d4bb61bb1f9b3ac6cd679e3551de88ad3682cc3 VS.LSP.TextEdit VS.LSP.Diff diff --git a/autoload/vsnip.vim b/autoload/vsnip.vim index afdaed9..6ccf4b0 100644 --- a/autoload/vsnip.vim +++ b/autoload/vsnip.vim @@ -6,6 +6,10 @@ let s:Position = vital#vsnip#import('VS.LSP.Position') let s:session = v:null let s:selected_text = '' +let g:vsnip#DeactivateOn = {} +let g:vsnip#DeactivateOn.OutsideOfSnippet = 1 +let g:vsnip#DeactivateOn.OutsideOfCurrentTabstop = 2 + " " vsnip#selected_text. " @@ -162,6 +166,25 @@ function! vsnip#get_context() abort return {} endfunction +" +" vsnip#completefunc +" +function! vsnip#completefunc(findstart, base) abort + if !a:findstart + if a:base ==# '' + return [] + endif + return vsnip#get_complete_items(bufnr('%')) + endif + + let line = getline('.') + let start = col('.') - 2 + while start >= 0 && line[start] =~# '\k' + let start -= 1 + endwhile + return start + 1 +endfunction + " " vsnip#get_complete_items " diff --git a/autoload/vsnip/indent.vim b/autoload/vsnip/indent.vim index cf2162a..98c0bf2 100644 --- a/autoload/vsnip/indent.vim +++ b/autoload/vsnip/indent.vim @@ -25,7 +25,7 @@ function! vsnip#indent#adjust_snippet_body(line, text) abort endwhile endif let l:text = substitute(l:text, "\n\\zs", l:base_indent, 'g') " add base_indent for all lines - let l:text = substitute(l:text, "\n\\s*\\ze\\(\n\\|$\\)", "\n", 'g') " remove empty line's indent + let l:text = substitute(l:text, "\n\\s*\\ze\n", "\n", 'g') " remove empty line's indent return l:text endfunction @@ -58,4 +58,3 @@ function! vsnip#indent#trim_base_indent(text) abort endfor return substitute(l:text, "\\%(^\\|\n\\)\\zs\\V" . l:base_indent, '', 'g') endfunction - diff --git a/autoload/vsnip/parser/combinator.vim b/autoload/vsnip/parser/combinator.vim index 59bfd83..7d4b06c 100644 --- a/autoload/vsnip/parser/combinator.vim +++ b/autoload/vsnip/parser/combinator.vim @@ -220,4 +220,3 @@ function! s:getchar(text, pos) abort endif return '' endfunction - diff --git a/autoload/vsnip/range.vim b/autoload/vsnip/range.vim new file mode 100644 index 0000000..868ba8f --- /dev/null +++ b/autoload/vsnip/range.vim @@ -0,0 +1,9 @@ +" +" vsnip#range#cover +" +function! vsnip#range#cover(whole_range, target_range) abort + let l:cover = v:true + let l:cover = l:cover && (a:whole_range.start.line < a:target_range.start.line || a:whole_range.start.line == a:target_range.start.line && a:whole_range.start.character <= a:target_range.start.character) + let l:cover = l:cover && (a:target_range.end.line < a:whole_range.end.line || a:target_range.end.line == a:whole_range.end.line && a:target_range.end.character <= a:whole_range.end.character) + return l:cover +endfunction diff --git a/autoload/vsnip/session.vim b/autoload/vsnip/session.vim index 2ba939b..e716e08 100644 --- a/autoload/vsnip/session.vim +++ b/autoload/vsnip/session.vim @@ -47,10 +47,14 @@ endfunction " merge. " function! s:Session.merge(session) abort + call s:TextEdit.apply(self.bufnr, self.snippet.sync()) + call self.store(self.changenr) + call a:session.expand() call self.snippet.merge(self.tabstop, a:session.snippet) call self.snippet.insert(deepcopy(a:session.snippet.position), a:session.snippet.children) call s:TextEdit.apply(self.bufnr, self.snippet.sync()) + call self.store(changenr()) endfunction " @@ -127,20 +131,21 @@ endfunction " @NOTE: Must work even if virtualedit=all/onmore or not. " function! s:Session.select(jump_point) abort - let l:pos = s:Position.lsp_to_vim('%', a:jump_point.range.end) - call cursor([l:pos[0], l:pos[1] - 1]) " Use `a:jump_point.range.end as inclusive position + let l:start_pos = s:Position.lsp_to_vim('%', a:jump_point.range.start) + let l:end_pos = s:Position.lsp_to_vim('%', a:jump_point.range.end) - let l:select_length = strlen(a:jump_point.placeholder.text()) - 1 let l:cmd = '' - let l:cmd .= mode()[0] ==# 'i' ? "\l" : '' - let l:cmd .= printf('v%s', l:select_length > 0 ? l:select_length . 'h' : '') + let l:cmd .= "\set virtualedit=onemore\" + let l:cmd .= mode()[0] ==# 'i' ? "\" : '' + let l:cmd .= printf("\call cursor(%s, %s)\", l:start_pos[0], l:start_pos[1]) + let l:cmd .= 'v' + let l:cmd .= printf("\call cursor(%s, %s)\%s", l:end_pos[0], l:end_pos[1], &selection ==# 'exclusive' ? '' : 'h') if get(g:, 'vsnip_test_mode', v:false) - let l:cmd .= "\gvo\" " Update `last visual selection` for getting it in test. - execute printf('normal! %s', l:cmd) - else - let l:cmd .= "o\" - call feedkeys(l:cmd, 'n') + let l:cmd .= "\gv" endif + let l:cmd .= printf("\set virtualedit=%s\", &virtualedit) + let l:cmd .= "\" + call feedkeys(l:cmd, 'ni') endfunction " @@ -153,10 +158,12 @@ function! s:Session.move(jump_point) abort call cursor(l:pos) - if l:pos[1] > strlen(getline(l:pos[0])) - startinsert! - else - startinsert + if mode()[0] ==# 'n' + if l:pos[1] != getcurpos()[2] + call feedkeys('a', 'ni') + else + call feedkeys('i', 'ni') + endif endif endfunction @@ -169,14 +176,14 @@ function! s:Session.refresh() abort endfunction " -" on_insert_leave. +" on_insert_leave " function! s:Session.on_insert_leave() abort call self.flush_changes() endfunction " -" on_text_changed. +" on_text_changed " function! s:Session.on_text_changed() abort if self.bufnr != bufnr('%') @@ -188,7 +195,6 @@ function! s:Session.on_text_changed() abort " save state. if self.changenr != l:changenr call self.store(self.changenr) - let self.changenr = l:changenr if has_key(self.changenrs, l:changenr) let self.tabstop = self.changenrs[l:changenr].tabstop let self.snippet = self.changenrs[l:changenr].snippet @@ -249,5 +255,5 @@ function! s:Session.store(changenr) abort \ 'tabstop': self.tabstop, \ 'snippet': deepcopy(self.snippet) \ } + let self.changenr = a:changenr endfunction - diff --git a/autoload/vsnip/snippet.vim b/autoload/vsnip/snippet.vim index 5f38ffd..114a102 100644 --- a/autoload/vsnip/snippet.vim +++ b/autoload/vsnip/snippet.vim @@ -82,7 +82,7 @@ function! s:Snippet.init() abort call self.traverse(self, l:fn.traverse) " Append ${MAX_TABSTOP} for the end of snippet. - if !l:fn.has_final_tabstop + if !l:fn.has_final_tabstop && g:vsnip_append_final_tabstop let self.children += [vsnip#snippet#node#create_from_ast({ \ 'type': 'placeholder', \ 'id': 0, @@ -95,11 +95,7 @@ endfunction " follow. " function! s:Snippet.follow(current_tabstop, diff) abort - let l:range = self.range() - let l:in_range = v:true - let l:in_range = l:in_range && (l:range.start.line < a:diff.range.start.line || l:range.start.line == a:diff.range.start.line && l:range.start.character <= a:diff.range.start.character) - let l:in_range = l:in_range && (a:diff.range.end.line < l:range.end.line || a:diff.range.end.line == l:range.end.line && a:diff.range.end.character <= l:range.end.character) - if !l:in_range + if !self.is_followable(a:current_tabstop, a:diff) return v:false endif @@ -200,7 +196,7 @@ function! s:Snippet.sync() abort call add(self.targets, { \ 'range': a:context.range, \ 'node': a:context.node, - \ 'new_text': self.new_texts[a:context.node.id], + \ 'new_text': a:context.node.transform.text(self.new_texts[a:context.node.id]), \ }) endif endif @@ -246,6 +242,24 @@ function! s:Snippet.text() abort return join(map(copy(self.children), 'v:val.text()'), '') endfunction +" +" is_followable. +" +function! s:Snippet.is_followable(current_tabstop, diff) abort + if g:vsnip#DeactivateOn.OutsideOfSnippet == g:vsnip_deactivate_on + return vsnip#range#cover(self.range(), a:diff.range) + elseif g:vsnip#DeactivateOn.OutsideOfCurrentTabstop == g:vsnip_deactivate_on + let l:context = self.get_placeholder_context_by_tabstop(a:current_tabstop) + if empty(l:context) + return v:false + endif + return vsnip#range#cover({ + \ 'start': self.offset_to_position(l:context.range[0]), + \ 'end': self.offset_to_position(l:context.range[1]), + \ }, a:diff.range) + endif +endfunction + " " get_placeholder_nodes " @@ -262,6 +276,23 @@ function! s:Snippet.get_placeholder_nodes() abort return sort(l:fn.nodes, { a, b -> a.id - b.id }) endfunction +" +" get_placeholder_context_by_tabstop +" +function! s:Snippet.get_placeholder_context_by_tabstop(current_tabstop) abort + let l:fn = {} + let l:fn.current_tabstop = a:current_tabstop + let l:fn.context = v:null + function! l:fn.traverse(context) abort + if a:context.node.type ==# 'placeholder' && a:context.node.id == self.current_tabstop + let self.context = a:context + return v:true + endif + endfunction + call self.traverse(self, l:fn.traverse) + return l:fn.context +endfunction + " " get_next_jump_point. " diff --git a/autoload/vsnip/snippet/node.vim b/autoload/vsnip/snippet/node.vim index 57f5049..83f9c14 100644 --- a/autoload/vsnip/snippet/node.vim +++ b/autoload/vsnip/snippet/node.vim @@ -1,6 +1,7 @@ let s:Placeholder = vsnip#snippet#node#placeholder#import() let s:Variable = vsnip#snippet#node#variable#import() let s:Text = vsnip#snippet#node#text#import() +let s:Transform = vsnip#snippet#node#transform#import() " " vsnip#snippet#node#create_from_ast @@ -33,3 +34,10 @@ function! vsnip#snippet#node#create_text(text) abort \ 'escaped': a:text \ }) endfunction + +" +" vsnip#snippet#node#create_transform +" +function! vsnip#snippet#node#create_transform(transform) abort + return s:Transform.new(a:transform) +endfunction diff --git a/autoload/vsnip/snippet/node/placeholder.vim b/autoload/vsnip/snippet/node/placeholder.vim index 2cc1c0d..db05209 100644 --- a/autoload/vsnip/snippet/node/placeholder.vim +++ b/autoload/vsnip/snippet/node/placeholder.vim @@ -21,6 +21,7 @@ function! s:Placeholder.new(ast) abort \ 'follower': v:false, \ 'choice': get(a:ast, 'choice', []), \ 'children': vsnip#snippet#node#create_from_ast(get(a:ast, 'children', [])), + \ 'transform': vsnip#snippet#node#create_transform(get(a:ast, 'transform')), \ }) if l:node.is_final diff --git a/autoload/vsnip/snippet/node/transform.vim b/autoload/vsnip/snippet/node/transform.vim new file mode 100644 index 0000000..64c6280 --- /dev/null +++ b/autoload/vsnip/snippet/node/transform.vim @@ -0,0 +1,112 @@ +function! vsnip#snippet#node#transform#import() abort + return s:Transform +endfunction + +let s:Transform = {} + +" +" new. +" +function! s:Transform.new(ast) abort + let l:transform = empty(a:ast) ? {} : a:ast + + let l:node = extend(deepcopy(s:Transform), { + \ 'type': 'transform', + \ 'regex': get(l:transform, 'regex', v:null), + \ 'replacements': get(l:transform, 'format', []), + \ 'options': get(l:transform, 'option', []), + \ }) + + let l:node.is_noop = l:node.regex is v:null + + return l:node +endfunction + +" +" text. +" +function! s:Transform.text(input_text) abort + if empty(a:input_text) || self.is_noop + return a:input_text + endif + + if self.regex.pattern !=# '(.*)' + " TODO: fully support regex + return a:input_text + endif + + let l:text = '' + + for l:replacement in self.replacements + if l:replacement.type ==# 'format' + if l:replacement.modifier ==# '/capitalize' + let l:text .= s:capitalize(a:input_text) + elseif l:replacement.modifier ==# '/downcase' + let l:text .= s:downcase(a:input_text) + elseif l:replacement.modifier ==# '/upcase' + let l:text .= s:upcase(a:input_text) + elseif l:replacement.modifier ==# '/camelcase' + let l:text .= s:camelcase(a:input_text) + elseif l:replacement.modifier ==# '/pascalcase' + let l:text .= s:capitalize(s:camelcase(a:input_text)) + endif + elseif l:replacement.type ==# 'text' + let l:text .= l:replacement.escaped + endif + endfor + + return l:text +endfunction + +" +" to_string +" +function! s:Transform.to_string() abort + if self.is_noop + return + end + + return printf('%s(regex=%s, total_replacements=%s, options=%s)', + \ self.type, + \ get(self.regex, 'pattern', ''), + \ len(self.replacements), + \ join(self.options, ''), + \ ) +endfunction + +" +" upcase +" +function! s:upcase(word) abort + let word = toupper(a:word) + return word +endfunction + +" +" downcase +" +function! s:downcase(word) abort + let word = tolower(a:word) + return word +endfunction + +" +" capitalize +" +function! s:capitalize(word) abort + let word = s:upcase(strpart(a:word, 0, 1)) . strpart(a:word, 1) + return word +endfunction + +" +" camelcase +" @see https://github.com/tpope/vim-abolish/blob/3f0c8faa/plugin/abolish.vim#L111-L118 +" +function! s:camelcase(word) abort + let word = substitute(a:word, '-', '_', 'g') + if word !~# '_' && word =~# '\l' + return substitute(word,'^.','\l&','') + else + return substitute(word,'\C\(_\)\=\(.\)','\=submatch(1)==""?tolower(submatch(2)) : toupper(submatch(2))','g') + endif +endfunction diff --git a/autoload/vsnip/snippet/node/variable.vim b/autoload/vsnip/snippet/node/variable.vim index 9b2ba21..4442d75 100644 --- a/autoload/vsnip/snippet/node/variable.vim +++ b/autoload/vsnip/snippet/node/variable.vim @@ -23,6 +23,7 @@ function! s:Variable.new(ast) abort \ 'unknown': empty(l:resolver), \ 'resolver': l:resolver, \ 'children': vsnip#snippet#node#create_from_ast(get(a:ast, 'children', [])), + \ 'transform': vsnip#snippet#node#create_transform(get(a:ast, 'transform')), \ }) endfunction @@ -30,7 +31,7 @@ endfunction " text. " function! s:Variable.text() abort - return join(map(copy(self.children), 'v:val.text()'), '') + return self.transform.text(join(map(copy(self.children), 'v:val.text()'), '')) endfunction " @@ -38,7 +39,7 @@ endfunction " function! s:Variable.resolve(context) abort if !self.unknown - let l:resolved = self.resolver.func({ 'node': self }) + let l:resolved = self.transform.text(self.resolver.func({ 'node': self })) if l:resolved isnot v:null " Fix indent when one variable returns multiple lines let l:base_indent = vsnip#indent#get_base_indent(split(a:context.before_text, "\n", v:true)[-1]) @@ -59,4 +60,3 @@ function! s:Variable.to_string() abort \ self.text() \ ) endfunction - diff --git a/autoload/vsnip/snippet/parser.vim b/autoload/vsnip/snippet/parser.vim index 4808a39..44873ee 100644 --- a/autoload/vsnip/snippet/parser.vim +++ b/autoload/vsnip/snippet/parser.vim @@ -5,15 +5,15 @@ let s:Combinator = vsnip#parser#combinator#import() " @see https://github.com/Microsoft/language-server-protocol/blob/master/snippetSyntax.md " function! vsnip#snippet#parser#parse(text) abort - if strlen(a:text) == 0 - return [] - endif + if strlen(a:text) == 0 + return [] + endif - let l:parsed = s:parser.parse(a:text, 0) - if !l:parsed[0] - throw json_encode({ 'text': a:text, 'result': l:parsed }) - endif - return l:parsed[1] + let l:parsed = s:parser.parse(a:text, 0) + if !l:parsed[0] + throw json_encode({ 'text': a:text, 'result': l:parsed }) + endif + return l:parsed[1] endfunction let s:skip = s:Combinator.skip @@ -37,7 +37,7 @@ let s:slash = s:token('/') let s:comma = s:token(',') let s:pipe = s:token('|') let s:varname = s:pattern('[_[:alpha:]]\w*') -let s:int = s:map(s:pattern('\d\+'), { value -> str2nr(value[0]) }) +let s:int = s:map(s:pattern('\d\+'), { value -> str2nr(value) }) let s:text = { stop, escape -> s:map( \ s:skip(stop, escape), \ { value -> { @@ -82,6 +82,8 @@ let s:format3 = s:map( \ s:token('/upcase'), \ s:token('/downcase'), \ s:token('/capitalize'), +\ s:token('/camelcase'), +\ s:token('/pascalcase'), \ s:token('+if'), \ s:token('?if:else'), \ s:token('-else'), @@ -207,4 +209,3 @@ let s:choice = s:map(s:seq( " parser. " let s:parser = s:many(s:or(s:any, s:text(['$'], ['}']))) - diff --git a/autoload/vsnip/source.vim b/autoload/vsnip/source.vim index d7a833b..28df252 100644 --- a/autoload/vsnip/source.vim +++ b/autoload/vsnip/source.vim @@ -4,6 +4,7 @@ function! vsnip#source#refresh(path) abort call vsnip#source#user_snippet#refresh(a:path) call vsnip#source#vscode#refresh(a:path) + call vsnip#source#snipmate#refresh(a:path) endfunction " @@ -13,15 +14,35 @@ function! vsnip#source#find(bufnr) abort let l:sources = [] let l:sources += vsnip#source#user_snippet#find(a:bufnr) let l:sources += vsnip#source#vscode#find(a:bufnr) + let l:sources += vsnip#source#snipmate#find(a:bufnr) return l:sources endfunction " " vsnip#source#filetypes " -function! vsnip#source#filetypes(bufnr) abort - let l:filetype = getbufvar(a:bufnr, '&filetype', '') - return split(l:filetype, '\.') + get(g:vsnip_filetypes, l:filetype, []) + ['global'] +function! vsnip#source#filetypes( bufnr ) abort + if has( "nvim" ) + let l:filetypes = v:lua.require'vsnip.treesitter'.get_ft_at_cursor( a:bufnr ) + + " buffer has no filetype defined + if l:filetypes.filetype == "" + return [ "global" ] + + " buffer has filetype + else + return + \ get( g:vsnip_filetypes, l:filetypes.injected_filetype, + \ get( g:vsnip_filetypes, l:filetypes.filetype, + \ [ l:filetypes.filetype ] + \ ) ) + \ + [ "global" ] + endif + else + let l:filetype = getbufvar( a:bufnr, "&filetype", "" ) + + return split( l:filetype, '\.' ) + get( g:vsnip_filetypes, l:filetype, [] ) + [ "global" ] + endif endfunction " @@ -65,7 +86,7 @@ endfunction " format_snippet " function! s:format_snippet(label, snippet) abort - let [l:prefixes, l:prefixes_alias] = s:resolve_prefix(a:snippet.prefix) + let [l:prefixes, l:prefixes_alias] = vsnip#source#resolve_prefix(a:snippet.prefix) let l:description = get(a:snippet, 'description', '') return { @@ -87,7 +108,7 @@ endfunction " " resolve_prefix. " -function! s:resolve_prefix(prefix) abort +function! vsnip#source#resolve_prefix(prefix) abort let l:prefixes = [] let l:prefixes_alias = [] @@ -111,4 +132,3 @@ function! s:resolve_prefix(prefix) abort \ sort(l:prefixes_alias, { a, b -> strlen(b) - strlen(a) }) \ ] endfunction - diff --git a/autoload/vsnip/source/snipmate.vim b/autoload/vsnip/source/snipmate.vim new file mode 100644 index 0000000..c500d1a --- /dev/null +++ b/autoload/vsnip/source/snipmate.vim @@ -0,0 +1,88 @@ +let s:cache = {} + +function! vsnip#source#snipmate#refresh(path) abort + if has_key(s:cache, a:path) + unlet s:cache[a:path] + endif +endfunction + +function! vsnip#source#snipmate#find(bufnr) abort + let filetypes = vsnip#source#filetypes(a:bufnr) + return s:find(filetypes, a:bufnr) +endfunction + +function! s:find(filetypes, bufnr) abort + let sources = [] + for path in s:get_source_paths(a:filetypes, a:bufnr) + if !has_key(s:cache, path) + let s:cache[path] = s:create(path, a:bufnr) + endif + call add(sources, s:cache[path]) + endfor + return sources +endfunction + +function! s:get_source_paths(filetypes, bufnr) abort + let paths = [] + for dir in s:get_source_dirs(a:bufnr) + for filetype in a:filetypes + let path = resolve(expand(printf('%s/%s.snippets', dir, filetype))) + if has_key(s:cache, path) || filereadable(path) + call add(paths, path) + endif + endfor + endfor + return paths +endfunction + +function! s:get_source_dirs(bufnr) abort + let dirs = [] + let buf_dir = getbufvar(a:bufnr, 'vsnip_snippet_dir', '') + if buf_dir !=# '' + let dirs += [buf_dir] + endif + let dirs += getbufvar(a:bufnr, 'vsnip_snippet_dirs', []) + let dirs += [g:vsnip_snippet_dir] + let dirs += g:vsnip_snippet_dirs + return dirs +endfunction + +function! s:create(path, bufnr) abort + let file = readfile(a:path) + let file = type(file) == v:t_list ? file : [file] + call map(file, { _, f -> iconv(f, 'utf-8', &encoding) }) + let source = [] + let i = -1 + while i + 1 < len(file) + let [i, line] = [i + 1, file[i + 1]] + if line =~# '^\(#\|\s*$\)' + " Comment, or blank line before snippets + elseif line =~# '^extends\s\+\S' + let filetypes = map(split(line[7:], ','), 'trim(v:val)') + let source += flatten(s:find(filetypes, a:bufnr)) + elseif line =~# '^snippet\s\+\S' && i + 1 < len(file) + let matched = matchlist(line, '^snippet\s\+\(\S\+\)\s*\(.*\)') + let [prefix, description] = [matched[1], matched[2]] + let body = [] + let indent = matchstr(file[i + 1], '^\s\+') + while i + 1 < len(file) && file[i + 1] =~# '^\(' . indent . '\|\s*$\)' + let [i, line] = [i + 1, file[i + 1]] + call add(body, line[strlen(indent):]) + endwhile + let [prefixes, prefixes_alias] = vsnip#source#resolve_prefix(prefix) + call add(source, { + \ 'label': prefix, + \ 'prefix': prefixes, + \ 'prefix_alias': prefixes_alias, + \ 'body': body, + \ 'description': description + \ }) + else + echohl ErrorMsg + echomsg printf('[vsnip] Parsing error occurred on: %s#L%s', a:path, i + 1) + echohl None + break + endif + endwhile + return sort(source, { a, b -> strlen(b.prefix[0]) - strlen(a.prefix[0]) }) +endfunction diff --git a/autoload/vsnip/source/user_snippet.vim b/autoload/vsnip/source/user_snippet.vim index b7e959f..5d6cc64 100644 --- a/autoload/vsnip/source/user_snippet.vim +++ b/autoload/vsnip/source/user_snippet.vim @@ -66,4 +66,3 @@ endfun fun! vsnip#source#user_snippet#paths(...) abort return s:get_source_paths(a:0 ? a:1 : bufnr('')) endfun - diff --git a/autoload/vsnip/source/vscode.vim b/autoload/vsnip/source/vscode.vim index 87b38a2..18e2d53 100644 --- a/autoload/vsnip/source/vscode.vim +++ b/autoload/vsnip/source/vscode.vim @@ -28,7 +28,8 @@ endfunction " function! s:find(languages) abort " Load `package.json#contributes.snippets` if does not exists it's cache. - for l:rtp in split(&runtimepath, ',') + let l:rtp_list = exists('*nvim_list_runtime_paths') ? nvim_list_runtime_paths() : split(&runtimepath, ',') + for l:rtp in l:rtp_list if has_key(s:runtimepaths, l:rtp) continue endif @@ -100,4 +101,3 @@ function! s:get_language(filetype) abort \ 'cs': 'csharp', \ }, a:filetype, a:filetype) endfunction - diff --git a/autoload/vsnip/variable.vim b/autoload/vsnip/variable.vim index bb7d860..8cae2d4 100644 --- a/autoload/vsnip/variable.vim +++ b/autoload/vsnip/variable.vim @@ -73,6 +73,11 @@ function! s:TM_FILEPATH(context) abort endfunction call vsnip#variable#register('TM_FILEPATH', function('s:TM_FILEPATH')) +function! s:RELATIVE_FILEPATH(context) abort + return expand('%') +endfunction +call vsnip#variable#register('RELATIVE_FILEPATH', function('s:RELATIVE_FILEPATH')) + function! s:CLIPBOARD(context) abort let l:clipboard = getreg(v:register) if empty(l:clipboard) @@ -181,4 +186,3 @@ function! s:VSNIP_CAMELCASE_FILENAME(context) abort return substitute(l:basename, '\(\%(\<\l\+\)\%(_\)\@=\)\|_\(\l\)', '\u\1\2', 'g') endfunction call vsnip#variable#register('VSNIP_CAMELCASE_FILENAME', function('s:VSNIP_CAMELCASE_FILENAME')) - diff --git a/doc/vsnip.txt b/doc/vsnip.txt index b2ed41e..040a405 100644 --- a/doc/vsnip.txt +++ b/doc/vsnip.txt @@ -53,29 +53,18 @@ If you want to know supported plugins, you can see https://github.com/hrsh7th/vi ============================================================================== VARIABLE *vsnip-variable* -> - let g:vsnip_extra_mapping = v:true -< + let g:vsnip_extra_mapping = v:true~ Enable or disable extra mappings. - -> - let g:vsnip_snippet_dir = expand('~/.vsnip') -< + let g:vsnip_snippet_dir = expand('~/.vsnip')~ Specify user snippet directory. Also as buffer-local variable: `b:vsnip_snippet_dir` - -> - let g:vsnip_snippet_dirs = [] -< + let g:vsnip_snippet_dirs = []~ List of user snippet directories. Also as buffer-local variable: `b:vsnip_snippet_dirs` - -> - let g:vsnip_filetypes = {} -< + let g:vsnip_filetypes = {}~ Specify extended filetypes. For example, you can extend `javascript` filetype with `javascriptreact` filetype. > @@ -83,30 +72,42 @@ VARIABLE *vsnip-variable* let g:vsnip_filetypes.javascriptreact = ['javascript'] < + If you are using `treesitter` you can define snippets for injected + languages like this: + > - let g:vsnip_sync_delay = 0 + let g:vsnip_filetypes['vim/lua'] = ['lua', 'vim/lua'] + let g:vsnip_filetypes['vue'] = ['html'] + let g:vsnip_filetypes['vue/javascript'] = ['javascript', 'vue/javascript'] < + + let g:vsnip_deactivate_on = g:vsnip#DeactivateOn.OutsideOfSnippet~ + Specify when to deactivate the current snippet. + + `g:vsnip#DeactivateOn.OutsideOfSnippet`: + Deactivate on edit the outside of snippet. + `g:vsnip#DeactivateOn.OutsideOfCurrentTabstop`: + Deactivate on edit the outside of current tabstop. + + let g:vsnip_sync_delay = 0~ Specify delay time to sync same tabstop placeholder. -1: No sync 0: Always sync N: Debounce N milliseconds - -> - let g:vsnip_choice_delay = 500 -< + let g:vsnip_choice_delay = 500~ Specify delay time to show choice candidates. Sometimes choice completion menu is closed by auto-completion engine. You can use this variable to solve this conflict. - -> - let g:vsnip_namespace = '' -< + let g:vsnip_namespace = ''~ Specify all snippet prefix's prefix. It useful when you use auto-completion. + let g:vsnip_append_final_tabstop = v:true~ + Specify whether to add a final tabstop. + ============================================================================== @@ -116,7 +117,12 @@ FUNCTION *vsnip-function* Register your own custom variable resolver. + vsnip#completefunc() + Register for finding completions. +> + set complete+=Fvsnip#completefunc +< ============================================================================== MAPPING *vsnip-mapping* @@ -154,14 +160,15 @@ COMMAND *vsnip-command* VsnipOpen~ -> - :VsnipOpen - :VsnipOpenEdit - :VsnipOpenSplit - :VsnipOpenVsplit -< + + :VsnipOpen [-format {type}] + :VsnipOpenEdit [-format {type}] + :VsnipOpenSplit [-format {type}] + :VsnipOpenVsplit [-format {type}] + Open snippet source file under `g:vsnip_snippet_dir`. +{type} is either 'snipmate' or 'vscode'. If omitted, it is 'vscode'. VsnipYank~ @@ -191,6 +198,7 @@ The following variables can be used in the same way they are in VSCode: `TM_FILENAME_BASE` The filename of the current document without its extensions `TM_DIRECTORY` The directory of the current document `TM_FILEPATH` The full file path of the current document + `RELATIVE_FILEPATH` The relative (to the current working directory) file path of the current document `CLIPBOARD` The contents of your clipboard `WORKSPACE_NAME` The name of the opened workspace or folder @@ -250,6 +258,44 @@ ${VIM:...Vim script expression...}~ +============================================================================== +SNIPMATE SUPPORT *vsnip-snipmate-support* + +Files with the extension 'snippets' in directories `g:vsnip_snippet_dir` or +`g:vsnip_snippet_dirs` are recognized as snippets with SnipMate-like syntax. + +NOTE: This feature does not guarantee that SnipMate's snippet collection can +be read in its entirety. It is intended to provide an easy way for users to +write their own new snippet definitions. + +The following two examples are equivalent. +In SnipMate format. > + snippet fn vim's function + function! $1($2) abort + $0 + endfunction +< +In VSCode format. > + { + "fn": { + "prefix": "fn", + "body": [ + "function! $1($2) abort", + "\t$0", + "endfunction" + ], + "description": "vim's function" + } + } +< +You can also use the extends syntax. For example, the first line of +cpp.snippets should have this. > + extends c +< + + + + ============================================================================== LIMITATION *vsnip-limitation* diff --git a/ftplugin/snippets.vim b/ftplugin/snippets.vim new file mode 100644 index 0000000..3ecefa5 --- /dev/null +++ b/ftplugin/snippets.vim @@ -0,0 +1,39 @@ +" MIT License +" +" Copyright 2009-2010 Michael Sanders. All rights reserved. + +" Permission is hereby granted, free of charge, to any person obtaining a copy +" of this software and associated documentation files (the "Software"), to deal +" in the Software without restriction, including without limitation the rights +" to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +" copies of the Software, and to permit persons to whom the Software is +" furnished to do so, subject to the following conditions: + +" The above copyright notice and this permission notice shall be included in all +" copies or substantial portions of the Software. + +" The software is provided "as is", without warranty of any kind, express or +" implied, including but not limited to the warranties of merchantability, +" fitness for a particular purpose and noninfringement. In no event shall the +" authors or copyright holders be liable for any claim, damages or other +" liability, whether in an action of contract, tort or otherwise, arising from, +" out of or in connection with the software or the use or other dealings in the +" software." From https://github.com/garbas/vim-snipmate + + +" Vim filetype plugin for SnipMate snippets (.snippets and .snippet files) + +if exists('b:did_ftplugin') + finish +endif +let b:did_ftplugin = 1 + +let b:undo_ftplugin = 'setl et< sts< cms< fdm< fde<' + +" Use hard tabs +setlocal noexpandtab softtabstop=0 + +setlocal foldmethod=expr foldexpr=getline(v:lnum)!~'^\\t\\\\|^$'?'>1':1 + +setlocal commentstring=#\ %s +setlocal nospell diff --git a/indent/snippets.vim b/indent/snippets.vim new file mode 100644 index 0000000..7deb397 --- /dev/null +++ b/indent/snippets.vim @@ -0,0 +1,55 @@ +" MIT License +" +" Copyright 2009-2010 Michael Sanders. All rights reserved. + +" Permission is hereby granted, free of charge, to any person obtaining a copy +" of this software and associated documentation files (the "Software"), to deal +" in the Software without restriction, including without limitation the rights +" to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +" copies of the Software, and to permit persons to whom the Software is +" furnished to do so, subject to the following conditions: + +" The above copyright notice and this permission notice shall be included in all +" copies or substantial portions of the Software. + +" The software is provided "as is", without warranty of any kind, express or +" implied, including but not limited to the warranties of merchantability, +" fitness for a particular purpose and noninfringement. In no event shall the +" authors or copyright holders be liable for any claim, damages or other +" liability, whether in an action of contract, tort or otherwise, arising from, +" out of or in connection with the software or the use or other dealings in the +" software." From https://github.com/garbas/vim-snipmate + + +" Simple indent support for SnipMate snippets files + +if exists('b:did_indent') + finish +endif +let b:did_indent = 1 + +setlocal nosmartindent +setlocal indentkeys=!^F,o,O,=snippet,=extends +setlocal indentexpr=GetSnippetIndent() + +if exists('*GetSnippetIndent') + finish +endif + +function! GetSnippetIndent() + let line = getline(v:lnum) + let prev_lnum = v:lnum - 1 + let prev_line = prev_lnum != 0 ? getline(prev_lnum) : '' + + if line =~# '\v^(snippet|extends) ' + return 0 + elseif indent(v:lnum) > 0 + return indent(v:lnum) + elseif prev_line =~# '^snippet ' + return &sw + elseif indent(prev_lnum) > 0 + return indent(prev_lnum) + endif + + return 0 +endfunction diff --git a/lua/vsnip/treesitter.lua b/lua/vsnip/treesitter.lua new file mode 100644 index 0000000..311be5a --- /dev/null +++ b/lua/vsnip/treesitter.lua @@ -0,0 +1,65 @@ +local M = {} + +local ok_parsers, ts_parsers = pcall( require, "nvim-treesitter.parsers" ) +if not ok_parsers then + ts_parsers = nil +end + +local ok_utils, ts_utils = pcall( require, "nvim-treesitter.ts_utils" ) +if not ok_utils then + ts_utils = nil +end + +local function get_parser_filetype ( lang ) + if lang and ts_parsers.list[ lang ] then + return ts_parsers.list[ lang ].filetype or lang + else + return "" + end +end + +local function is_available () + return ok_parsers and ok_utils +end + +function M.get_ft_at_cursor ( bufnr ) + local filetypes = { + filetype = "", + injected_filetype = "", + } + + if is_available() then + local cur_node = ts_utils.get_node_at_cursor( vim.fn.bufwinid( bufnr ) ) + + if cur_node then + local parser = ts_parsers.get_parser( bufnr ) + local language_tree_at_cursor = parser:language_for_range( { cur_node:range() } ) + local language_at_cursor = language_tree_at_cursor:lang() + + local filetype = get_parser_filetype( language_at_cursor ) + + if filetype ~= "" then + filetypes.filetype = filetype + + local parent_language_tree = language_tree_at_cursor:parent() + + if parent_language_tree then + local parent_language = parent_language_tree:lang() + local parent_filetype = get_parser_filetype( parent_language ) + + if parent_filetype ~= "" then + filetypes.injected_filetype = parent_filetype .. "/" .. filetype + end + end + + return filetypes + end + end + end + + filetypes.filetype = vim.bo[ bufnr ].filetype or "" + + return filetypes +end + +return M diff --git a/misc/integration.json b/misc/integration.json index 75e454d..d2fdf0b 100644 --- a/misc/integration.json +++ b/misc/integration.json @@ -69,6 +69,28 @@ "$1snip${2:pet}" ] }, + "multi1": { + "description": "jump at middle of snippet", + "prefix": ["マルチ1"], + "body": [ + "あ$1い$2う" + ] + }, + "multi2": { + "description": "select 4 length middle of snippet text", + "prefix": ["マルチ2"], + "body": [ + "あ$1い${2:かkaか}う" + ] + }, + "deactivate1": { + "prefix": "deactivate", + "body": [ + "function! $1() abort", + "\t$0", + "endfunction" + ] + }, "realworld1": { "description": "Complex example", "prefix": ["realworld1"], @@ -155,5 +177,9 @@ "\t$TM_SELECTED_TEXT$5", "}$0" ] + }, + "issue249": { + "prefix": "issue249", + "body": ["${1:FOO}\n"] } } diff --git a/misc/snipmate.snippets b/misc/snipmate.snippets new file mode 100644 index 0000000..8433e63 --- /dev/null +++ b/misc/snipmate.snippets @@ -0,0 +1,6 @@ +snippet arrow-function + () => +snippet fn vim's function + function! $1($2) abort + $0 + endfunction diff --git a/package.json b/package.json index da10091..f83d652 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,27 @@ "name": "vim-vsnip", "version": "1.0.0", "description": "This aims to plugin like Visual Studio Code's Snippet feature.", + "homepage": "https://github.com/hrsh7th/vim-test-snips#readme", + "bugs": { + "url": "https://github.com/hrsh7th/vim-test-snips/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hrsh7th/vim-test-snips.git" + }, + "license": "MIT", + "author": "hrsh7th", "scripts": { "open": "nvim -u .vimrc", "test": "run-s test:*", - "test:vim-virtualedit-off": "THEMIS_VIM=vim VIRTUALEDIT=0 themis ./spec", - "test:vim-virtualedit-on": "THEMIS_VIM=vim VIRTUALEDIT=1 themis ./spec", - "test:nvim-virtualedit-off": "THEMIS_VIM=nvim VIRTUALEDIT=0 themis ./spec", - "test:nvim-virtualedit-on": "THEMIS_VIM=nvim VIRTUALEDIT=1 themis ./spec", + "test:01": "THEMIS_VIM=vim EXCLUSIVE=0 VIRTUALEDIT=0 themis ./spec", + "test:02": "THEMIS_VIM=vim EXCLUSIVE=0 VIRTUALEDIT=1 themis ./spec", + "test:03": "THEMIS_VIM=vim EXCLUSIVE=1 VIRTUALEDIT=0 themis ./spec", + "test:04": "THEMIS_VIM=vim EXCLUSIVE=1 VIRTUALEDIT=1 themis ./spec", + "test:05": "THEMIS_VIM=nvim EXCLUSIVE=0 VIRTUALEDIT=0 themis ./spec", + "test:06": "THEMIS_VIM=nvim EXCLUSIVE=0 VIRTUALEDIT=1 themis ./spec", + "test:07": "THEMIS_VIM=nvim EXCLUSIVE=1 VIRTUALEDIT=0 themis ./spec", + "test:08": "THEMIS_VIM=nvim EXCLUSIVE=1 VIRTUALEDIT=1 themis ./spec", "lint": "vint ." }, "husky": { @@ -16,18 +30,8 @@ "pre-commit": "npm run lint && npm run test" } }, - "repository": { - "type": "git", - "url": "git+https://github.com/hrsh7th/vim-test-snips.git" - }, - "author": "hrsh7th", - "license": "MIT", - "bugs": { - "url": "https://github.com/hrsh7th/vim-test-snips/issues" - }, - "homepage": "https://github.com/hrsh7th/vim-test-snips#readme", "devDependencies": { - "husky": "^3.0.5", + "husky": "^9.0.0", "npm-run-all": "^4.1.5", "watch": "^1.0.2" } diff --git a/plugin/vsnip.vim b/plugin/vsnip.vim index 5b1f5be..19e42e6 100644 --- a/plugin/vsnip.vim +++ b/plugin/vsnip.vim @@ -7,10 +7,12 @@ let g:loaded_vsnip = 1 " variable " let g:vsnip_extra_mapping = get(g:, 'vsnip_extra_mapping', v:true) +let g:vsnip_deactivate_on = get(g:, 'vsnip_deactivate_on', g:vsnip#DeactivateOn.OutsideOfCurrentTabstop) let g:vsnip_snippet_dir = get(g:, 'vsnip_snippet_dir', expand('~/.vsnip')) let g:vsnip_snippet_dirs = get(g:, 'vsnip_snippet_dirs', []) let g:vsnip_sync_delay = get(g:, 'vsnip_sync_delay', 0) let g:vsnip_choice_delay = get(g:, 'vsnip_choice_delay', 500) +let g:vsnip_append_final_tabstop = get(g:, 'vsnip_append_final_tabstop', v:true) let g:vsnip_namespace = get(g:, 'vsnip_namespace', '') let g:vsnip_filetypes = get(g:, 'vsnip_filetypes', {}) let g:vsnip_filetypes.typescriptreact = get(g:vsnip_filetypes, 'typescriptreact', ['typescript']) @@ -26,11 +28,11 @@ augroup END " " command " -command! -bang VsnipOpen call s:open_command(0, 'vsplit') -command! -bang VsnipOpenEdit call s:open_command(0, 'edit') -command! -bang VsnipOpenVsplit call s:open_command(0, 'vsplit') -command! -bang VsnipOpenSplit call s:open_command(0, 'split') -function! s:open_command(bang, cmd) +command! -nargs=* -bang VsnipOpen call s:open_command(0, 'vsplit', ) +command! -nargs=* -bang VsnipOpenEdit call s:open_command(0, 'edit', ) +command! -nargs=* -bang VsnipOpenVsplit call s:open_command(0, 'vsplit', ) +command! -nargs=* -bang VsnipOpenSplit call s:open_command(0, 'split', ) +function! s:open_command(bang, cmd, arg) let l:candidates = vsnip#source#filetypes(bufnr('%')) if a:bang let l:idx = 1 @@ -51,9 +53,12 @@ function! s:open_command(bang, cmd) endif endif - execute printf('%s %s', a:cmd, fnameescape(printf('%s/%s.json', + let l:ext = a:arg =~# '-format\s\+snipmate' ? 'snippets' : 'json' + + execute printf('%s %s', a:cmd, fnameescape(printf('%s/%s.%s', \ resolve(l:expanded_dir), - \ l:candidates[l:idx - 1] + \ l:candidates[l:idx - 1], + \ l:ext \ ))) endfunction @@ -72,7 +77,7 @@ endfunction " extra mapping " if g:vsnip_extra_mapping - snoremap ("\" . (getcurpos()[2] == col('$') - 1 ? 'a' : 'i')) + snoremap ("\" . (&virtualedit ==# '' && getcurpos()[2] >= col('$') - 1 ? 'a' : 'i')) endif " @@ -93,7 +98,7 @@ function! s:expand_or_jump() endfunction " This is needed to keep normal-mode during 0ms to prevent CompleteDone handling by LSP Client. - let l:maybe_complete_done = !empty(v:completed_item) && !empty(v:completed_item.user_data) + let l:maybe_complete_done = !empty(v:completed_item) && has_key(v:completed_item, 'user_data') && !empty(v:completed_item.user_data) if l:maybe_complete_done call timer_start(0, { -> l:ctx.callback() }) else @@ -113,7 +118,7 @@ function! s:expand() abort endfunction " This is needed to keep normal-mode during 0ms to prevent CompleteDone handling by LSP Client. - let l:maybe_complete_done = !empty(v:completed_item) && !empty(v:completed_item.user_data) + let l:maybe_complete_done = !empty(v:completed_item) && has_key(v:completed_item, 'user_data') && !empty(v:completed_item.user_data) if l:maybe_complete_done call timer_start(0, { -> l:ctx.callback() }) else @@ -188,6 +193,7 @@ augroup vsnip autocmd InsertLeave * call s:on_insert_leave() autocmd TextChanged,TextChangedI,TextChangedP * call s:on_text_changed() autocmd BufWritePost * call s:on_buf_write_post() + autocmd BufRead,BufNewFile *.snippets setlocal filetype=snippets augroup END " @@ -216,4 +222,3 @@ endfunction function! s:on_buf_write_post() abort call vsnip#source#refresh(resolve(fnamemodify(bufname('%'), ':p'))) endfunction - diff --git a/spec/autoload/vsnip.vimspec b/spec/autoload/vsnip.vimspec index 3d8bc2e..f2da6a4 100644 --- a/spec/autoload/vsnip.vimspec +++ b/spec/autoload/vsnip.vimspec @@ -145,5 +145,50 @@ Describe vsnip End + Describe #completefunc + + It should return start position + enew! + set filetype=basic_spec + call setline(1, ' if') + call cursor([1, 3]) + call s:expect(vsnip#completefunc(1, '')).to_equal(1) + End + + It should return complete items + enew! + set filetype=basic_spec + call s:expect(vsnip#completefunc(0, 'if')[-1]).to_equal({ + \ 'word': 'if', + \ 'abbr': 'if', + \ 'kind': 'Snippet', + \ 'menu': '[v] if', + \ 'dup': 1, + \ 'user_data': json_encode({ + \ 'vsnip': { + \ 'snippet': [ + \ "if ${1:condition}", + \ "\t$0", + \ "endif", + \ ] + \ } + \ }) + \ }, { + \ 'word': 'inline-fn', + \ 'abbr': 'inline-fn', + \ 'kind': 'Snippet', + \ 'menu': '[v] inline-fn', + \ 'dup': 1, + \ 'user_data': json_encode({ + \ 'vsnip': { + \ 'snippet': [ + \ "{ -> $1 }$0" + \ ] + \ } + \ }) + \ }) + End + End + End diff --git a/spec/autoload/vsnip/snippet.vimspec b/spec/autoload/vsnip/snippet.vimspec index 31126a3..fa5df1d 100644 --- a/spec/autoload/vsnip/snippet.vimspec +++ b/spec/autoload/vsnip/snippet.vimspec @@ -31,6 +31,11 @@ Describe vsnip#snippet let l:snippet = s:Snippet.new(s:start_position, 'console.log(${CURRENT_YEAR})') call s:expect(l:snippet.text()).to_equal('console.log(' . strftime('%Y') . ')') End + + It should support whole word transform + let l:snippet = s:Snippet.new(s:start_position, '${1:state}, set${1/(.*)/${1:/capitalize}/}') + call s:expect(l:snippet.text()).to_equal('state, setState') + End End Describe #sync @@ -259,6 +264,62 @@ Describe vsnip#snippet call s:expect(l:snippet.text()).to_equal('default') End + It should support whole word transform (upcase) on tabstop + let l:snippet = s:Snippet.new(s:start_position, '${1:varName}, ${1/(.*)/${1:/upcase}/}') + call s:expect(l:snippet.text()).to_equal('varName, VARNAME') + End + + It should support whole word transform (downcase) on variable + call vsnip#selected_text('varName') + let l:snippet = s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/downcase}/}') + call s:expect(l:snippet.text()).to_equal('varname') + End + + It should support whole word transform (capitalize) + call vsnip#selected_text('varName') + let l:snippet = s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/capitalize}/}') + call s:expect(l:snippet.text()).to_equal('VarName') + End + + It should support whole word transform (camelcase) + call vsnip#selected_text('var_name') + call s:expect( + \ s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/camelcase}/}').text() + \ ).to_equal('varName') + + call vsnip#selected_text('VAR_NAME') + call s:expect( + \ s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/camelcase}/}').text() + \ ).to_equal('varName') + + call vsnip#selected_text('VarName') + call s:expect( + \ s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/camelcase}/}').text() + \ ).to_equal('varName') + End + + It should support whole word transform (pascalcase) + call vsnip#selected_text('var_name') + call s:expect( + \ s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/pascalcase}/}').text() + \ ).to_equal('VarName') + + call vsnip#selected_text('VAR_NAME') + call s:expect( + \ s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/pascalcase}/}').text() + \ ).to_equal('VarName') + + call vsnip#selected_text('varName') + call s:expect( + \ s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/${1:/pascalcase}/}').text() + \ ).to_equal('VarName') + End + + It should support whole word transform with additional text + call vsnip#selected_text('varName') + let l:snippet = s:Snippet.new(s:start_position, '${TM_SELECTED_TEXT/(.*)/start-lowercase-${1:/downcase}/}-end') + call s:expect(l:snippet.text()).to_equal('start-lowercase-varname-end') + End End Describe #range diff --git a/spec/autoload/vsnip/source.vimspec b/spec/autoload/vsnip/source.vimspec index 79cc2bd..767ca3b 100644 --- a/spec/autoload/vsnip/source.vimspec +++ b/spec/autoload/vsnip/source.vimspec @@ -57,5 +57,41 @@ Describe vsnip#source End + Describe #snipmate + + It should load snippets + enew! + set filetype=snipmate + let l:snippets = vsnip#source#find(bufnr('%')) + call s:expect(l:snippets[0]).to_have_length(2) + End + + It should format snippets + enew! + set filetype=snipmate + let l:snippets = vsnip#source#find(bufnr('%')) + call s:expect(sort(l:snippets[0])).to_equal(sort([{ + \ 'label': 'arrow-function', + \ 'description': '', + \ 'prefix': ['arrow-function'], + \ 'prefix_alias': ['af'], + \ 'body': [ + \ "() => " + \ ] + \ }, { + \ 'label': 'fn', + \ 'description': "vim's function", + \ 'prefix': ['fn'], + \ 'prefix_alias': [], + \ 'body': [ + \ "function! $1($2) abort", + \ "\t$0", + \ "endfunction", + \ ] + \ }])) + End + + End + End diff --git a/spec/plugin/vsnip.vimspec b/spec/plugin/vsnip.vimspec index dd212ae..aaf4477 100644 --- a/spec/plugin/vsnip.vimspec +++ b/spec/plugin/vsnip.vimspec @@ -1,5 +1,19 @@ let s:expect = themis#helper('expect') +function! s:expect_selection(s, e) abort + if &selection ==# 'exclusive' && &virtualedit ==# '' + return + endif + + call s:expect(getpos("'<")[1 : 2]).to_equal([a:s[0], a:s[1]]) + if &selection ==# 'exclusive' + let l:text = getline('.') + let l:char = strcharpart(l:text, charidx(l:text, a:e[1] - 1), 1) + let a:e[1] = a:e[1] + strlen(l:char) + endif + call s:expect(getpos("'>")[1 : 2]).to_equal([a:e[0], a:e[1]]) +endfunction + Describe vsnip After each @@ -16,6 +30,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() + call s:expect(getcurpos()[1 : 2]).to_equal([1, 8]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\(vsnip-assert)", 'x') @@ -50,6 +65,7 @@ Describe vsnip It should not expand when prefix is word and it does not separate by word boundary enew! set filetype=integration + set iskeyword=@,48-57,_,192-255 call setline(1, '(aspec1)') call cursor([1, 7]) @@ -128,9 +144,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 1]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 1]) + call s:expect_selection([1, 1], [1, 1]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\\(vsnip-assert)", 'x') @@ -144,15 +158,13 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 3]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 3]) + call s:expect_selection([1, 3], [1, 3]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\\(vsnip-assert)", 'x') End - It should select 1 length last of snippet text + It should select 1 length last of snippet text enew! set filetype=integration call setline(1, 'spec7') @@ -160,9 +172,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 7]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 7]) + call s:expect_selection([1, 7], [1, 7]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\\(vsnip-assert)", 'x') @@ -176,9 +186,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 1]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 3]) + call s:expect_selection([1, 1], [1, 3]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\\(vsnip-assert)", 'x') @@ -192,9 +200,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 3]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 5]) + call s:expect_selection([1, 3], [1, 5]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\\(vsnip-assert)", 'x') @@ -208,9 +214,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 5]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 7]) + call s:expect_selection([1, 5], [1, 7]) call s:expect(getbufline('%', '^', '$')).to_equal(['snippet']) endfunction call feedkeys("a\\\(vsnip-assert)", 'x') @@ -218,6 +222,115 @@ Describe vsnip End + Context multibyte + + It should jump to middle of snippet + enew! + set filetype=integration + call setline(1, 'マルチ1') + call cursor([1, 10]) + + let g:vsnip_assert = {} + function g:vsnip_assert.step1() + call s:expect(getcurpos()[1 : 2]).to_equal([1, 7]) + call s:expect(getbufline('%', '^', '$')).to_equal(['あいう']) + endfunction + call feedkeys("a\\\(vsnip-assert)", 'x') + End + + It should select 4 length middle of snippet text + enew! + set filetype=integration + call setline(1, 'マルチ2') + call cursor([1, 10]) + + let g:vsnip_assert = {} + function g:vsnip_assert.step1() + call s:expect_selection([1, 7], [1, 12]) + call s:expect(getbufline('%', '^', '$')).to_equal(['あいかkaかう']) + endfunction + call feedkeys("a\\\(vsnip-assert)", 'x') + End + + End + + Context g:vsnip_deactivate_on + It should deactivate snippet on edit the outside of snippet + enew! + set filetype=integration + let g:vsnip_deactivate_on = g:vsnip#DeactivateOn.OutsideOfSnippet + + call setline(1, [ + \ 'outside', + \ 'deactivate', + \ 'outside', + \ ]) + call cursor([2, 11]) + + let g:vsnip_assert = {} + let l:sequence = '' + + function g:vsnip_assert.step1() + call s:expect(vsnip#get_session()).not.to_be_empty() + endfunction + let l:sequence .= "a\\(vsnip-assert)" + + function g:vsnip_assert.step2() + call s:expect(vsnip#get_session()).not.to_be_empty() + endfunction + let l:sequence .= "funcname\(vsnip-assert)" + + function g:vsnip_assert.step3() + call s:expect(vsnip#get_session()).not.to_be_empty() + endfunction + let l:sequence .= "\return 1\(vsnip-assert)" + + function g:vsnip_assert.step4() + call s:expect(vsnip#get_session()).to_be_empty() + endfunction + let l:sequence .= "\call cursor([1, 1])\modified\" + + call feedkeys(l:sequence, 'x') + + let g:vsnip_deactivate_on = g:vsnip#DeactivateOn.OutsideOfSnippet + End + + It should deactivate snippet on edit the outside of current tabstop + enew! + set filetype=integration + let g:vsnip_deactivate_on = g:vsnip#DeactivateOn.OutsideOfCurrentTabstop + + call setline(1, [ + \ 'outside', + \ 'deactivate', + \ 'outside', + \ ]) + call cursor([2, 11]) + + let g:vsnip_assert = {} + let l:sequence = '' + + function g:vsnip_assert.step1() + call s:expect(vsnip#get_session()).not.to_be_empty() + endfunction + let l:sequence .= "a\\(vsnip-assert)" + + function g:vsnip_assert.step2() + call s:expect(vsnip#get_session()).not.to_be_empty() + endfunction + let l:sequence .= "funcname\(vsnip-assert)" + + function g:vsnip_assert.step3() + call s:expect(vsnip#get_session()).to_be_empty() + endfunction + let l:sequence .= "\arguments\(vsnip-assert)" + + call feedkeys(l:sequence, 'x') + + let g:vsnip_deactivate_on = g:vsnip#DeactivateOn.OutsideOfSnippet + End + End + Context realworld It should work (complex example) @@ -436,15 +549,13 @@ Describe vsnip " can jump to $2 let l:sequence .= "\\(vsnip-assert)" function g:vsnip_assert.step5() - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 11]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 24]) + call s:expect_selection([1, 11], [1, 24]) endfunction " can jump to $3 let l:sequence .= "\\(vsnip-assert)" function g:vsnip_assert.step6() - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 12]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 23]) + call s:expect_selection([1, 12], [1, 23]) endfunction call feedkeys(l:sequence, 'x') @@ -477,9 +588,7 @@ Describe vsnip let g:vsnip_assert = {} function g:vsnip_assert.step1() call s:expect(getbufline('%', '^', '$')).to_equal(['for i=1,10', ' print(i)']) - call s:expect(mode(1)).to_equal('s') - call s:expect(getpos("'<")[1 : 2]).to_equal([1, 5]) - call s:expect(getpos("'>")[1 : 2]).to_equal([1, 5]) + call s:expect_selection([1, 5], [1, 5]) endfunction call feedkeys("a\\(vsnip-assert)", 'x') End @@ -572,6 +681,28 @@ Describe vsnip call feedkeys(l:sequence, 'x') End + It should work issue249 + enew! + set filetype=integration + call setline(1, ['issue249']) + call cursor([1, 8]) + + let g:vsnip_assert = {} + let l:sequence = '' + + " expand + let l:sequence .= "a\\(vsnip-assert)" + function g:vsnip_assert.step1() + call s:expect_selection([1, 1], [1, 3]) + call s:expect(getbufline('%', '^', '$')).to_equal([ + \ 'FOO', + \ '', + \ ]) + endfunction + + call feedkeys(l:sequence, 'x') + End + End End diff --git a/syntax/snippets.vim b/syntax/snippets.vim new file mode 100644 index 0000000..02d8c20 --- /dev/null +++ b/syntax/snippets.vim @@ -0,0 +1,45 @@ +" MIT License +" +" Copyright 2009-2010 Michael Sanders. All rights reserved. + +" Permission is hereby granted, free of charge, to any person obtaining a copy +" of this software and associated documentation files (the "Software"), to deal +" in the Software without restriction, including without limitation the rights +" to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +" copies of the Software, and to permit persons to whom the Software is +" furnished to do so, subject to the following conditions: + +" The above copyright notice and this permission notice shall be included in all +" copies or substantial portions of the Software. + +" The software is provided "as is", without warranty of any kind, express or +" implied, including but not limited to the warranties of merchantability, +" fitness for a particular purpose and noninfringement. In no event shall the +" authors or copyright holders be liable for any claim, damages or other +" liability, whether in an action of contract, tort or otherwise, arising from, +" out of or in connection with the software or the use or other dealings in the +" software." From https://github.com/garbas/vim-snipmate + + +" Syntax highlighting for .snippets files (used for snipMate.vim) +" Hopefully this should make snippets a bit nicer to write! +syn match snipComment '^#.*' +syn match placeHolder '\${\d\+\(:.\{-}\)\=}' contains=snipCommand +syn match tabStop '\$\d\+' +syn match snipEscape '\\\\\|\\`' +syn match snipCommand '\%(\\\@