diff options
57 files changed, 1819 insertions, 569 deletions
diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 5044b7dcab..f190981322 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -13,7 +13,9 @@ jobs: with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} + path: pr_nvim - uses: rhysd/action-setup-vim@v1 with: neovim: true - - run: nvim --clean -es +"lua require('scripts.lintcommit').main({trace=true})" + - run: wget https://raw.githubusercontent.com/neovim/neovim/master/scripts/lintcommit.lua + - run: nvim --clean -es +"cd pr_nvim" +"lua dofile('../lintcommit.lua').main({trace=true})" diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim index 5d8734a625..aacecc521e 100644 --- a/runtime/autoload/dist/ft.vim +++ b/runtime/autoload/dist/ft.vim @@ -726,7 +726,7 @@ func dist#ft#FTperl() endif let save_cursor = getpos('.') call cursor(1,1) - let has_use = search('^use\s\s*\k', 'c', 30) + let has_use = search('^use\s\s*\k', 'c', 30) > 0 call setpos('.', save_cursor) if has_use setf perl @@ -758,7 +758,8 @@ func dist#ft#FTtex() let save_cursor = getpos('.') call cursor(1,1) let firstNC = search('^\s*[^[:space:]%]', 'c', 1000) - if firstNC " Check the next thousand lines for a LaTeX or ConTeXt keyword. + if firstNC > 0 + " Check the next thousand lines for a LaTeX or ConTeXt keyword. let lpat = 'documentclass\>\|usepackage\>\|begin{\|newcommand\>\|renewcommand\>' let cpat = 'start\a\+\|setup\a\+\|usemodule\|enablemode\|enableregime\|setvariables\|useencoding\|usesymbols\|stelle\a\+\|verwende\a\+\|stel\a\+\|gebruik\a\+\|usa\a\+\|imposta\a\+\|regle\a\+\|utilisemodule\>' let kwline = search('^\s*\\\%(' . lpat . '\)\|^\s*\\\(' . cpat . '\)', diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 2da1f5e40d..7bae5bb94b 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -186,7 +186,7 @@ About the `functions` map: a type name, e.g. `nvim_buf_get_lines` is the `get_lines` method of a Buffer instance. |dev-api| - Global functions have the "method=false" flag and are prefixed with just - `nvim_`, e.g. `nvim_get_buffers`. + `nvim_`, e.g. `nvim_list_bufs`. *api-mapping* External programs (clients) can use the metadata to discover the API, using diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 5e50f9c1f8..ed75acf36e 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -526,7 +526,7 @@ DirChanged After the |current-directory| was changed. "auto" to trigger on 'autochdir'. Sets these |v:event| keys: cwd: current working directory - scope: "global", "tab", "window" + scope: "global", "tabpage", "window" changed_window: v:true if we fired the event switching window (or tab) <afile> is set to the new directory name. diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index c68e86462d..9bf37a3759 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -22,8 +22,10 @@ acos({expr}) Float arc cosine of {expr} add({object}, {item}) List/Blob append {item} to {object} and({expr}, {expr}) Number bitwise AND api_info() Dict api metadata -append({lnum}, {string}) Number append {string} below line {lnum} -append({lnum}, {list}) Number append lines {list} below line {lnum} +append({lnum}, {text}) Number append {text} below line {lnum} +appendbufline({expr}, {lnum}, {text}) + Number append {text} below line {lnum} + in buffer {expr} argc([{winid}]) Number number of files in the argument list argidx() Number current index in the argument list arglistid([{winnr} [, {tabnr}]]) Number argument list id @@ -52,7 +54,7 @@ assert_notmatch({pat}, {text} [, {msg}]) assert_report({msg}) Number report a test failure assert_true({actual} [, {msg}]) Number assert {actual} is true atan({expr}) Float arc tangent of {expr} -atan2({expr}, {expr}) Float arc tangent of {expr1} / {expr2} +atan2({expr1}, {expr2}) Float arc tangent of {expr1} / {expr2} browse({save}, {title}, {initdir}, {default}) String put up a file requester browsedir({title}, {initdir}) String put up a directory requester @@ -72,9 +74,9 @@ call({func}, {arglist} [, {dict}]) any call {func} with arguments {arglist} ceil({expr}) Float round {expr} up changenr() Number current change number -chanclose({id}[, {stream}]) Number Closes a channel or one of its streams +chanclose({id} [, {stream}]) Number Closes a channel or one of its streams chansend({id}, {data}) Number Writes {data} to channel -char2nr({expr}[, {utf8}]) Number ASCII/UTF-8 value of first char in {expr} +char2nr({expr} [, {utf8}]) Number ASCII/UTF-8 value of first char in {expr} charcol({expr}) Number column number of cursor or mark charidx({string}, {idx} [, {countcc}]) Number char index of byte {idx} in {string} @@ -91,8 +93,8 @@ confirm({msg} [, {choices} [, {default} [, {type}]]]) copy({expr}) any make a shallow copy of {expr} cos({expr}) Float cosine of {expr} cosh({expr}) Float hyperbolic cosine of {expr} -count({list}, {expr} [, {ic} [, {start}]]) - Number count how many {expr} are in {list} +count({comp}, {expr} [, {ic} [, {start}]]) + Number count how many {expr} are in {comp} cscope_connection([{num}, {dbpath} [, {prepend}]]) Number checks existence of cscope connection ctxget([{index}]) Dict return the |context| dict at {index} @@ -100,7 +102,7 @@ ctxpop() none pop and restore |context| from the |context-stack| ctxpush([{types}]) none push the current |context| to the |context-stack| -ctxset({context}[, {index}]) none set |context| at {index} +ctxset({context} [, {index}]) none set |context| at {index} ctxsize() Number return |context-stack| size cursor({lnum}, {col} [, {off}]) Number move cursor to {lnum}, {col}, {off} @@ -108,7 +110,7 @@ cursor({list}) Number move cursor to position in {list} debugbreak({pid}) Number interrupt process being debugged deepcopy({expr} [, {noref}]) any make a full copy of {expr} delete({fname} [, {flags}]) Number delete the file or directory {fname} -deletebufline({buf}, {first}[, {last}]) +deletebufline({buf}, {first} [, {last}]) Number delete lines from buffer {buf} dictwatcheradd({dict}, {pattern}, {callback}) Start watching a dictionary @@ -212,7 +214,7 @@ gettabvar({nr}, {varname} [, {def}]) gettabwinvar({tabnr}, {winnr}, {name} [, {def}]) any {name} in {winnr} in tab page {tabnr} gettagstack([{nr}]) Dict get the tag stack of window {nr} -getwininfo([{winid}]) List list of windows +getwininfo([{winid}]) List list of info about each window getwinpos([{timeout}]) List X and Y coord in pixels of the Vim window getwinposx() Number X coord in pixels of Vim window getwinposy() Number Y coord in pixels of Vim window @@ -262,9 +264,9 @@ items({dict}) List key-value pairs in {dict} jobpid({id}) Number Returns pid of a job. jobresize({id}, {width}, {height}) Number Resize pseudo terminal window of a job -jobstart({cmd}[, {opts}]) Number Spawns {cmd} as a job +jobstart({cmd} [, {opts}]) Number Spawns {cmd} as a job jobstop({id}) Number Stops a job -jobwait({ids}[, {timeout}]) Number Wait for a set of jobs +jobwait({ids} [, {timeout}]) Number Wait for a set of jobs join({list} [, {sep}]) String join {list} items into one String json_decode({expr}) any Convert {expr} from JSON json_encode({expr}) String Convert {expr} to JSON @@ -279,28 +281,32 @@ list2str({list} [, {utf8}]) String turn numbers in {list} into a String localtime() Number current time log({expr}) Float natural logarithm (base e) of {expr} log10({expr}) Float logarithm of Float {expr} to base 10 -luaeval({expr}[, {expr}]) any evaluate Lua expression +luaeval({expr} [, {expr}]) any evaluate |Lua| expression map({expr1}, {expr2}) List/Dict change each item in {expr1} to {expr} -maparg({name}[, {mode} [, {abbr} [, {dict}]]]) +maparg({name} [, {mode} [, {abbr} [, {dict}]]]) String or Dict rhs of mapping {name} in mode {mode} -mapcheck({name}[, {mode} [, {abbr}]]) +mapcheck({name} [, {mode} [, {abbr}]]) String check for mappings matching {name} -match({expr}, {pat}[, {start}[, {count}]]) +match({expr}, {pat} [, {start} [, {count}]]) Number position where {pat} matches in {expr} -matchadd({group}, {pattern}[, {priority}[, {id}]]) +matchadd({group}, {pattern} [, {priority} [, {id} [, {dict}]]]) Number highlight {pattern} with {group} -matchaddpos({group}, {list}[, {priority}[, {id}]]) +matchaddpos({group}, {pos} [, {priority} [, {id} [, {dict}]]]) Number highlight positions with {group} matcharg({nr}) List arguments of |:match| matchdelete({id} [, {win}]) Number delete match identified by {id} -matchend({expr}, {pat}[, {start}[, {count}]]) +matchend({expr}, {pat} [, {start} [, {count}]]) Number position where {pat} ends in {expr} -matchlist({expr}, {pat}[, {start}[, {count}]]) +matchfuzzy({list}, {str} [, {dict}]) + List fuzzy match {str} in {list} +matchfuzzypos({list}, {str} [, {dict}]) + List fuzzy match {str} in {list} +matchlist({expr}, {pat} [, {start} [, {count}]]) List match and submatches of {pat} in {expr} -matchstr({expr}, {pat}[, {start}[, {count}]]) +matchstr({expr}, {pat} [, {start} [, {count}]]) String {count}'th match of {pat} in {expr} -matchstrpos({expr}, {pat}[, {start}[, {count}]]) +matchstrpos({expr}, {pat} [, {start} [, {count}]]) List {count}'th match of {pat} in {expr} max({expr}) Number maximum value of items in {expr} menu_get({path} [, {modes}]) List description of |menus| matched by {path} @@ -311,10 +317,10 @@ mode([expr]) String current editing mode msgpackdump({list} [, {type}]) List/Blob dump objects to msgpack msgpackparse({data}) List parse msgpack to a list of objects nextnonblank({lnum}) Number line nr of non-blank line >= {lnum} -nr2char({expr}[, {utf8}]) String single char with ASCII/UTF-8 value {expr} +nr2char({expr} [, {utf8}]) String single char with ASCII/UTF-8 value {expr} nvim_...({args}...) any call nvim |api| functions or({expr}, {expr}) Number bitwise OR -pathshorten({expr}) String shorten directory names in a path +pathshorten({expr} [, {len}]) String shorten directory names in a path perleval({expr}) any evaluate |perl| expression pow({x}, {y}) Float {x} to the power of {y} prevnonblank({lnum}) Number line nr of non-blank line <= {lnum} @@ -361,9 +367,9 @@ resolve({filename}) String get filename a shortcut points to reverse({list}) List reverse {list} in-place round({expr}) Float round off {expr} rubyeval({expr}) any evaluate |Ruby| expression -rpcnotify({channel}, {event}[, {args}...]) +rpcnotify({channel}, {event} [, {args}...]) Sends an |RPC| notification to {channel} -rpcrequest({channel}, {method}[, {args}...]) +rpcrequest({channel}, {method} [, {args}...]) Sends an |RPC| request to {channel} screenattr({row}, {col}) Number attribute at screen position screenchar({row}, {col}) Number character at screen position @@ -386,8 +392,8 @@ searchpos({pattern} [, {flags} [, {stopline} [, {timeout}]]]) server2client({clientid}, {string}) Number send reply string serverlist() String get a list of available servers -setbufline( {expr}, {lnum}, {line}) - Number set line {lnum} to {line} in buffer +setbufline({expr}, {lnum}, {text}) + Number set line {lnum} to {text} in buffer {expr} setbufvar({buf}, {varname}, {val}) set {varname} in buffer {buf} to {val} setcharpos({expr}, {list}) Number set the {expr} position to {list} @@ -406,7 +412,7 @@ setpos({expr}, {list}) Number set the {expr} position to {list} setqflist({list} [, {action}]) Number modify quickfix list using {list} setqflist({list}, {action}, {what}) Number modify specific quickfix list props -setreg({n}, {v}[, {opt}]) Number set register to value and type +setreg({n}, {v} [, {opt}]) Number set register to value and type settabvar({nr}, {varname}, {val}) set {varname} in tab page {nr} to {val} settabwinvar({tabnr}, {winnr}, {varname}, {val}) set {varname} in window {winnr} in tab page {tabnr} to {val} @@ -491,9 +497,9 @@ system({cmd} [, {input}]) String output of shell command/filter {cmd} systemlist({cmd} [, {input}]) List output of shell command/filter {cmd} tabpagebuflist([{arg}]) List list of buffer numbers in tab page tabpagenr([{arg}]) Number number of current or last tab page -tabpagewinnr({tabarg}[, {arg}]) +tabpagewinnr({tabarg} [, {arg}]) Number number of current window in tab page -taglist({expr}[, {filename}]) List list of tags matching {expr} +taglist({expr} [, {filename}]) List list of tags matching {expr} tagfiles() List tags files used tan({expr}) Float tangent of {expr} tanh({expr}) Float hyperbolic tangent of {expr} @@ -520,7 +526,7 @@ uniq({list} [, {func} [, {dict}]]) values({dict}) List values in {dict} virtcol({expr}) Number screen column of cursor or mark visualmode([expr]) String last visual mode used -wait({timeout}, {condition}[, {interval}]) +wait({timeout}, {condition} [, {interval}]) Number Wait until {condition} is satisfied wildmenumode() Number whether 'wildmenu' mode is active win_execute({id}, {command} [, {silent}]) @@ -995,7 +1001,7 @@ changenr() *changenr()* redo it is the number of the redone change. After undo it is one less than the number of the undone change. -chanclose({id}[, {stream}]) *chanclose()* +chanclose({id} [, {stream}]) *chanclose()* Close a channel or a specific stream associated with it. For a job, {stream} can be one of "stdin", "stdout", "stderr" or "rpc" (closes stdin/stdout for a job started @@ -1439,7 +1445,7 @@ ctxpush([{types}]) *ctxpush()* which |context-types| to include in the pushed context. Otherwise, all context types are included. -ctxset({context}[, {index}]) *ctxset()* +ctxset({context} [, {index}]) *ctxset()* Sets the |context| at {index} from the top of the |context-stack| to that represented by {context}. {context} is a Dictionary with context data (|context-dict|). @@ -1483,7 +1489,7 @@ cursor({list}) Can also be used as a |method|: > GetCursorPos()->cursor() -deepcopy({expr}[, {noref}]) *deepcopy()* *E698* +deepcopy({expr} [, {noref}]) *deepcopy()* *E698* Make a copy of {expr}. For Numbers and Strings this isn't different from using {expr} directly. When {expr} is a |List| a full copy is created. This means @@ -1526,7 +1532,7 @@ delete({fname} [, {flags}]) *delete()* Can also be used as a |method|: > GetName()->delete() -deletebufline({buf}, {first}[, {last}]) *deletebufline()* +deletebufline({buf}, {first} [, {last}]) *deletebufline()* Delete lines {first} to {last} (inclusive) from buffer {buf}. If {last} is omitted then delete line {first} only. On success 0 is returned, on failure 1 is returned. @@ -1536,7 +1542,7 @@ deletebufline({buf}, {first}[, {last}]) *deletebufline()* For the use of {buf}, see |bufname()| above. - {first} and {last} are used like with |setline()|. Note that + {first} and {last} are used like with |getline()|. Note that when using |line()| this refers to the current buffer. Use "$" to refer to the last line in buffer {buf}. @@ -2861,7 +2867,7 @@ getcursorcharpos([{winid}]) Can also be used as a |method|: > GetWinid()->getcursorcharpos() -getcwd([{winnr}[, {tabnr}]]) *getcwd()* +getcwd([{winnr} [, {tabnr}]]) *getcwd()* With no arguments, returns the name of the effective |current-directory|. With {winnr} or {tabnr} the working directory of that scope is returned, and 'autochdir' is @@ -3018,7 +3024,7 @@ getline({lnum} [, {end}]) < To get lines from another buffer see |getbufline()| -getloclist({nr},[, {what}]) *getloclist()* +getloclist({nr} [, {what}]) *getloclist()* Returns a |List| with all the entries in the location list for window {nr}. {nr} can be the window number or the |window-ID|. When {nr} is zero the current window is used. @@ -3645,7 +3651,7 @@ has_key({dict}, {key}) *has_key()* Can also be used as a |method|: > mydict->has_key(key) -haslocaldir([{winnr}[, {tabnr}]]) *haslocaldir()* +haslocaldir([{winnr} [, {tabnr}]]) *haslocaldir()* The result is a Number, which is 1 when the window has set a local path via |:lcd| or when {winnr} is -1 and the tabpage has set a local path via |:tcd|, otherwise 0. @@ -4147,7 +4153,7 @@ jobresize({job}, {width}, {height}) *jobresize()* columns and {height} rows. Fails if the job was not started with `"pty":v:true`. -jobstart({cmd}[, {opts}]) *jobstart()* +jobstart({cmd} [, {opts}]) *jobstart()* Spawns {cmd} as a job. If {cmd} is a List it runs directly (no 'shell'). If {cmd} is a String it runs in the 'shell', like this: > @@ -4234,7 +4240,7 @@ jobstop({id}) *jobstop()* Returns 1 for valid job id, 0 for invalid id, including jobs have exited or stopped. -jobwait({jobs}[, {timeout}]) *jobwait()* +jobwait({jobs} [, {timeout}]) *jobwait()* Waits for jobs and their |on_exit| handlers to complete. {jobs} is a List of |job-id|s to wait for. @@ -4491,7 +4497,7 @@ log10({expr}) *log10()* Can also be used as a |method|: > Compute()->log10() -luaeval({expr}[, {expr}]) +luaeval({expr} [, {expr}]) Evaluate Lua expression {expr} and return its result converted to Vim data structures. See |lua-eval| for more details. @@ -4711,7 +4717,7 @@ match({expr}, {pat} [, {start} [, {count}]]) *match()* GetList()->match('word') < *matchadd()* *E798* *E799* *E801* *E957* -matchadd({group}, {pattern}[, {priority}[, {id} [, {dict}]]]) +matchadd({group}, {pattern} [, {priority} [, {id} [, {dict}]]]) Defines a pattern to be highlighted in the current window (a "match"). It will be highlighted with {group}. Returns an identification number (ID), which can be used to delete the @@ -4857,6 +4863,87 @@ matchend({expr}, {pat} [, {start} [, {count}]]) *matchend()* Can also be used as a |method|: > GetText()->matchend('word') +matchfuzzy({list}, {str} [, {dict}]) *matchfuzzy()* + If {list} is a list of strings, then returns a |List| with all + the strings in {list} that fuzzy match {str}. The strings in + the returned list are sorted based on the matching score. + + The optional {dict} argument always supports the following + items: + matchseq When this item is present and {str} contains + multiple words separated by white space, then + returns only matches that contain the words in + the given sequence. + + If {list} is a list of dictionaries, then the optional {dict} + argument supports the following additional items: + key key of the item which is fuzzy matched against + {str}. The value of this item should be a + string. + text_cb |Funcref| that will be called for every item + in {list} to get the text for fuzzy matching. + This should accept a dictionary item as the + argument and return the text for that item to + use for fuzzy matching. + + {str} is treated as a literal string and regular expression + matching is NOT supported. The maximum supported {str} length + is 256. + + When {str} has multiple words each separated by white space, + then the list of strings that have all the words is returned. + + If there are no matching strings or there is an error, then an + empty list is returned. If length of {str} is greater than + 256, then returns an empty list. + + Refer to |fuzzy-match| for more information about fuzzy + matching strings. + + Example: > + :echo matchfuzzy(["clay", "crow"], "cay") +< results in ["clay"]. > + :echo getbufinfo()->map({_, v -> v.name})->matchfuzzy("ndl") +< results in a list of buffer names fuzzy matching "ndl". > + :echo getbufinfo()->matchfuzzy("ndl", {'key' : 'name'}) +< results in a list of buffer information dicts with buffer + names fuzzy matching "ndl". > + :echo getbufinfo()->matchfuzzy("spl", + \ {'text_cb' : {v -> v.name}}) +< results in a list of buffer information dicts with buffer + names fuzzy matching "spl". > + :echo v:oldfiles->matchfuzzy("test") +< results in a list of file names fuzzy matching "test". > + :let l = readfile("buffer.c")->matchfuzzy("str") +< results in a list of lines in "buffer.c" fuzzy matching "str". > + :echo ['one two', 'two one']->matchfuzzy('two one') +< results in ['two one', 'one two']. > + :echo ['one two', 'two one']->matchfuzzy('two one', + \ {'matchseq': 1}) +< results in ['two one']. + +matchfuzzypos({list}, {str} [, {dict}]) *matchfuzzypos()* + Same as |matchfuzzy()|, but returns the list of matched + strings, the list of character positions where characters + in {str} matches and a list of matching scores. You can + use |byteidx()| to convert a character position to a byte + position. + + If {str} matches multiple times in a string, then only the + positions for the best match is returned. + + If there are no matching strings or there is an error, then a + list with three empty list items is returned. + + Example: > + :echo matchfuzzypos(['testing'], 'tsg') +< results in [['testing'], [[0, 2, 6]], [99]] > + :echo matchfuzzypos(['clay', 'lacy'], 'la') +< results in [['lacy', 'clay'], [[0, 1], [1, 2]], [153, 133]] > + :echo [{'text': 'hello', 'id' : 10}] + \ ->matchfuzzypos('ll', {'key' : 'text'}) +< results in [[{'id': 10, 'text': 'hello'}], [[2, 3]], [127]] + matchlist({expr}, {pat} [, {start} [, {count}]]) *matchlist()* Same as |match()|, but return a |List|. The first item in the list is the matched string, same as what matchstr() would @@ -5204,13 +5291,17 @@ or({expr}, {expr}) *or()* < Can also be used as a |method|: > :let bits = bits->or(0x80) -pathshorten({path}) *pathshorten()* +pathshorten({expr} [, {len}]) *pathshorten()* Shorten directory names in the path {path} and return the result. The tail, the file name, is kept as-is. The other - components in the path are reduced to single letters. Leading - '~' and '.' characters are kept. Example: > + components in the path are reduced to {len} letters in length. + If {len} is omitted or smaller than 1 then 1 is used (single + letters). Leading '~' and '.' characters are kept. Examples: > :echo pathshorten('~/.config/nvim/autoload/file1.vim') < ~/.c/n/a/file1.vim ~ +> + :echo pathshorten('~/.config/nvim/autoload/file2.vim', 2) +< ~/.co/nv/au/file2.vim ~ It doesn't matter if the path exists or not. Can also be used as a |method|: > @@ -5939,19 +6030,19 @@ round({expr}) *round()* Can also be used as a |method|: > Compute()->round() -rpcnotify({channel}, {event}[, {args}...]) *rpcnotify()* +rpcnotify({channel}, {event} [, {args}...]) *rpcnotify()* Sends {event} to {channel} via |RPC| and returns immediately. If {channel} is 0, the event is broadcast to all channels. Example: > :au VimLeave call rpcnotify(0, "leaving") -rpcrequest({channel}, {method}[, {args}...]) *rpcrequest()* +rpcrequest({channel}, {method} [, {args}...]) *rpcrequest()* Sends a request to {channel} to invoke {method} via |RPC| and blocks until a response is received. Example: > :let result = rpcrequest(rpc_chan, "func", 1, 2, 3) -rpcstart({prog}[, {argv}]) *rpcstart()* +rpcstart({prog} [, {argv}]) *rpcstart()* Deprecated. Replace > :let id = rpcstart('prog', ['arg1', 'arg2']) < with > @@ -7719,11 +7810,11 @@ substitute({string}, {pat}, {sub}, {flags}) *substitute()* swapinfo({fname}) *swapinfo()* The result is a dictionary, which holds information about the swapfile {fname}. The available fields are: - version VIM version + version Vim version user user name host host name fname original file name - pid PID of the VIM process that created the swap + pid PID of the Vim process that created the swap file mtime last modification time in seconds inode Optional: INODE number of the file @@ -8034,7 +8125,7 @@ tempname() *tempname()* *temp-file-name* For MS-Windows forward slashes are used when the 'shellslash' option is set or when 'shellcmdflag' starts with '-'. -termopen({cmd}[, {opts}]) *termopen()* +termopen({cmd} [, {opts}]) *termopen()* Spawns {cmd} in a new pseudo-terminal session connected to the current buffer. {cmd} is the same as the one passed to |jobstart()|. This function fails if the current buffer is @@ -8393,7 +8484,7 @@ visualmode([{expr}]) *visualmode()* a non-empty String, then the Visual mode will be cleared and the old value is returned. See |non-zero-arg|. -wait({timeout}, {condition}[, {interval}]) *wait()* +wait({timeout}, {condition} [, {interval}]) *wait()* Waits until {condition} evaluates to |TRUE|, where {condition} is a |Funcref| or |string| containing an expression. diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt index 641cd93386..f2f6ebb2c9 100644 --- a/runtime/doc/cmdline.txt +++ b/runtime/doc/cmdline.txt @@ -908,8 +908,7 @@ These modifiers can be given, in this order: directory. :. Reduce file name to be relative to current directory, if possible. File name is unmodified if it is not below the - current directory, but on MS-Windows the drive is removed if - it is the current drive. + current directory. For maximum shortness, use ":~:.". :h Head of the file name (the last component and any separators removed). Cannot be used with :e, :r or :t. diff --git a/runtime/doc/pattern.txt b/runtime/doc/pattern.txt index 634145da3e..42005b0d78 100644 --- a/runtime/doc/pattern.txt +++ b/runtime/doc/pattern.txt @@ -1421,5 +1421,38 @@ Finally, these constructs are unique to Perl: are suggested to use ":match" for manual matching and ":2match" for another plugin. +============================================================================== +11. Fuzzy matching *fuzzy-match* + +Fuzzy matching refers to matching strings using a non-exact search string. +Fuzzy matching will match a string, if all the characters in the search string +are present anywhere in the string in the same order. Case is ignored. In a +matched string, other characters can be present between two consecutive +characters in the search string. If the search string has multiple words, then +each word is matched separately. So the words in the search string can be +present in any order in a string. + +Fuzzy matching assigns a score for each matched string based on the following +criteria: + - The number of sequentially matching characters. + - The number of characters (distance) between two consecutive matching + characters. + - Matches at the beginning of a word + - Matches at a camel case character (e.g. Case in CamelCase) + - Matches after a path separator or a hyphen. + - The number of unmatched characters in a string. +The matching string with the highest score is returned first. + +For example, when you search for the "get pat" string using fuzzy matching, it +will match the strings "GetPattern", "PatternGet", "getPattern", "patGetter", +"getSomePattern", "MatchpatternGet" etc. + +The functions |matchfuzzy()| and |matchfuzzypos()| can be used to fuzzy search +a string in a List of strings. The matchfuzzy() function returns a List of +matching strings. The matchfuzzypos() functions returns the List of matches, +the matching positions and the fuzzy match scores. + +The "f" flag of `:vimgrep` enables fuzzy matching. + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt index bb4d807413..ed736ad4eb 100644 --- a/runtime/doc/quickfix.txt +++ b/runtime/doc/quickfix.txt @@ -989,7 +989,7 @@ commands can be combined to create a NewGrep command: > 5.1 using Vim's internal grep *:vim* *:vimgrep* *E682* *E683* -:vim[grep][!] /{pattern}/[g][j] {file} ... +:vim[grep][!] /{pattern}/[g][j][f] {file} ... Search for {pattern} in the files {file} ... and set the error list to the matches. Files matching 'wildignore' are ignored; files in 'suffixes' are @@ -1014,6 +1014,13 @@ commands can be combined to create a NewGrep command: > updated. With the [!] any changes in the current buffer are abandoned. + 'f' When the 'f' flag is specified, fuzzy string + matching is used to find matching lines. In this + case, {pattern} is treated as a literal string + instead of a regular expression. See + |fuzzy-match| for more information about fuzzy + matching strings. + |QuickFixCmdPre| and |QuickFixCmdPost| are triggered. A file that is opened for matching may use a buffer number, but it is reused if possible to avoid @@ -1042,20 +1049,20 @@ commands can be combined to create a NewGrep command: > :vimgrep Error *.c < *:lv* *:lvimgrep* -:lv[imgrep][!] /{pattern}/[g][j] {file} ... +:lv[imgrep][!] /{pattern}/[g][j][f] {file} ... :lv[imgrep][!] {pattern} {file} ... Same as ":vimgrep", except the location list for the current window is used instead of the quickfix list. *:vimgrepa* *:vimgrepadd* -:vimgrepa[dd][!] /{pattern}/[g][j] {file} ... +:vimgrepa[dd][!] /{pattern}/[g][j][f] {file} ... :vimgrepa[dd][!] {pattern} {file} ... Just like ":vimgrep", but instead of making a new list of errors the matches are appended to the current list. *:lvimgrepa* *:lvimgrepadd* -:lvimgrepa[dd][!] /{pattern}/[g][j] {file} ... +:lvimgrepa[dd][!] /{pattern}/[g][j][f] {file} ... :lvimgrepa[dd][!] {pattern} {file} ... Same as ":vimgrepadd", except the location list for the current window is used instead of the quickfix diff --git a/runtime/doc/repeat.txt b/runtime/doc/repeat.txt index c7481ad290..a022049766 100644 --- a/runtime/doc/repeat.txt +++ b/runtime/doc/repeat.txt @@ -465,8 +465,8 @@ flag when defining the function, it is not relevant when executing it. > :set cpo-=C < *line-continuation-comment* -To add a comment in between the lines start with '\" '. Notice the space -after the double quote. Example: > +To add a comment in between the lines start with '"\ '. Notice the space +after the backslash. Example: > let array = [ "\ first entry comment \ 'first', diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index bf29c94d51..bf024315f6 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -608,6 +608,8 @@ String manipulation: *string-functions* toupper() turn a string to uppercase match() position where a pattern matches in a string matchend() position where a pattern match ends in a string + matchfuzzy() fuzzy matches a string in a list of strings + matchfuzzypos() fuzzy matches a string in a list of strings matchstr() match of a pattern in a string matchstrpos() match and positions of a pattern in a string matchlist() like matchstr() and also return submatches diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 11849632c5..7892b82137 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -322,6 +322,8 @@ coerced to strings. See |id()| for more details, currently it uses |c_CTRL-R| pasting a non-special register into |cmdline| omits the last <CR>. +|CursorMoved| always triggers when moving between windows. + Lua interface (|lua.txt|): - `:lua print("a\0b")` will print `a^@b`, like with `:echomsg "a\nb"` . In Vim @@ -483,7 +485,6 @@ Commands: :tearoff Compile-time features: - EBCDIC Emacs tags support X11 integration (see |x11-selection|) diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 0f2de6ce5c..9d4b38f08a 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -78,7 +78,7 @@ end --- --- Example: --- <pre> ---- vim.ui.input({ prompt = 'Select value for shiftwidth: ' }, function(input) +--- vim.ui.input({ prompt = 'Enter value for shiftwidth: ' }, function(input) --- vim.o.shiftwidth = tonumber(input) --- end) --- </pre> diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 94572b57cd..dcc20194f0 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -493,6 +493,9 @@ add_executable(nvim ${NVIM_GENERATED_FOR_SOURCES} ${NVIM_GENERATED_FOR_HEADERS} ${EXTERNAL_SOURCES} ${EXTERNAL_HEADERS}) target_link_libraries(nvim ${NVIM_EXEC_LINK_LIBRARIES}) install_helper(TARGETS nvim) +if(MSVC) + install(FILES $<TARGET_PDB_FILE:nvim> DESTINATION ${CMAKE_INSTALL_BINDIR} OPTIONAL) +endif() set_property(TARGET nvim APPEND PROPERTY INCLUDE_DIRECTORIES ${LUA_PREFERRED_INCLUDE_DIRS}) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index dccad5a2d0..926c385892 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -10624,12 +10624,13 @@ int modify_fname(char_u *src, bool tilde_file, size_t *usedlen, char_u **fnamep, char_u *s, *p, *pbuf; char_u dirname[MAXPATHL]; int c; - int has_fullname = 0; + bool has_fullname = false; + bool has_homerelative = false; repeat: // ":p" - full path/file_name if (src[*usedlen] == ':' && src[*usedlen + 1] == 'p') { - has_fullname = 1; + has_fullname = true; valid |= VALID_PATH; *usedlen += 2; @@ -10698,8 +10699,8 @@ repeat: } pbuf = NULL; // Need full path first (use expand_env() to remove a "~/") - if (!has_fullname) { - if (c == '.' && **fnamep == '~') { + if (!has_fullname && !has_homerelative) { + if ((c == '.' || c == '~') && **fnamep == '~') { p = pbuf = expand_env_save(*fnamep); } else { p = pbuf = (char_u *)FullName_save((char *)*fnamep, FALSE); @@ -10708,18 +10709,33 @@ repeat: p = *fnamep; } - has_fullname = 0; + has_fullname = false; if (p != NULL) { if (c == '.') { os_dirname(dirname, MAXPATHL); - s = path_shorten_fname(p, dirname); - if (s != NULL) { - *fnamep = s; - if (pbuf != NULL) { - xfree(*bufp); // free any allocated file name - *bufp = pbuf; - pbuf = NULL; + if (has_homerelative) { + s = vim_strsave(dirname); + home_replace(NULL, s, dirname, MAXPATHL, true); + xfree(s); + } + size_t namelen = STRLEN(dirname); + + // Do not call shorten_fname() here since it removes the prefix + // even though the path does not have a prefix. + if (fnamencmp(p, dirname, namelen) == 0) { + p += namelen; + if (vim_ispathsep(*p)) { + while (*p && vim_ispathsep(*p)) { + p++; + } + *fnamep = p; + if (pbuf != NULL) { + // free any allocated file name + xfree(*bufp); + *bufp = pbuf; + pbuf = NULL; + } } } } else { @@ -10730,6 +10746,7 @@ repeat: *fnamep = s; xfree(*bufp); *bufp = s; + has_homerelative = true; } } xfree(pbuf); diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 5c85d764fb..6dedd0f745 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -249,6 +249,8 @@ return { matcharg={args=1, base=1}, matchdelete={args={1, 2}, base=1}, matchend={args={2, 4}, base=1}, + matchfuzzy={args={2, 3}, base=1}, + matchfuzzypos={args={2, 3}, base=1}, matchlist={args={2, 4}, base=1}, matchstr={args={2, 4}, base=1}, matchstrpos={args={2,4}, base=1}, @@ -262,7 +264,7 @@ return { nextnonblank={args=1, base=1}, nr2char={args={1, 2}, base=1}, ['or']={args=2, base=1}, - pathshorten={args=1, base=1}, + pathshorten={args={1, 2}, base=1}, pow={args=2, base=1}, prevnonblank={args=1, base=1}, printf={args=varargs(1), base=2}, diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index edf6ed3c12..111fae0928 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -98,7 +98,6 @@ PRAGMA_DIAG_POP #endif -static char *e_listarg = N_("E686: Argument of %s must be a List"); static char *e_listblobarg = N_("E899: Argument of %s must be a List or Blob"); static char *e_invalwindow = N_("E957: Invalid window number"); @@ -3189,7 +3188,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) if (argvars[0].v_type == VAR_UNKNOWN) { // getchar(): blocking wait. // TODO(bfredl): deduplicate shared logic with state_enter ? - if (!(char_avail() || using_script() || input_available())) { + if (!char_avail()) { (void)os_inchar(NULL, 0, -1, 0, main_loop.events); if (!multiqueue_empty(main_loop.events)) { state_handle_k_event(); @@ -6846,12 +6845,23 @@ static void f_or(typval_T *argvars, typval_T *rettv, FunPtr fptr) */ static void f_pathshorten(typval_T *argvars, typval_T *rettv, FunPtr fptr) { + int trim_len = 1; + + if (argvars[1].v_type != VAR_UNKNOWN) { + trim_len = (int)tv_get_number(&argvars[1]); + if (trim_len < 1) { + trim_len = 1; + } + } + rettv->v_type = VAR_STRING; - const char *const s = tv_get_string_chk(&argvars[0]); - if (!s) { - return; + const char_u *p = (char_u *)tv_get_string_chk(&argvars[0]); + if (p == NULL) { + rettv->vval.v_string = NULL; + } else { + rettv->vval.v_string = vim_strsave(p); + shorten_dir_len(rettv->vval.v_string, trim_len); } - rettv->vval.v_string = shorten_dir((char_u *)xstrdup(s)); } /* diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index e583ce49b2..6f8b032d41 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -3205,8 +3205,9 @@ const char *tv_get_string_buf_chk(const typval_T *const tv, char *const buf) case VAR_BLOB: case VAR_UNKNOWN: emsg(_(str_errors[tv->v_type])); - return false; + return NULL; } + abort(); return NULL; } diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 3b3d4e50cc..81fce3565a 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -6141,12 +6141,14 @@ char_u *skip_vimgrep_pat(char_u *p, char_u **s, int *flags) p++; // Find the flags - while (*p == 'g' || *p == 'j') { + while (*p == 'g' || *p == 'j' || *p == 'f') { if (flags != NULL) { if (*p == 'g') { *flags |= VGR_GLOBAL; - } else { + } else if (*p == 'j') { *flags |= VGR_NOJUMP; + } else { + *flags |= VGR_FUZZY; } } p++; diff --git a/src/nvim/getchar.h b/src/nvim/getchar.h index be10e150e5..f24a4e7c7c 100644 --- a/src/nvim/getchar.h +++ b/src/nvim/getchar.h @@ -55,7 +55,7 @@ struct map_arguments { char_u *orig_rhs; /// The original text of the {rhs}. size_t orig_rhs_len; - char *desc; /// map escription + char *desc; /// map description }; typedef struct map_arguments MapArguments; #define MAP_ARGUMENTS_INIT { false, false, false, false, false, false, false, \ diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 041b60d838..f6fbe98ff0 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -979,6 +979,7 @@ EXTERN char e_invalidreg[] INIT(= N_("E850: Invalid register name")); EXTERN char e_dirnotf[] INIT(= N_("E919: Directory not found in '%s': \"%s\"")); EXTERN char e_au_recursive[] INIT(= N_("E952: Autocommand caused recursive behavior")); EXTERN char e_autocmd_close[] INIT(= N_("E813: Cannot close autocmd window")); +EXTERN char e_listarg[] INIT(= N_("E686: Argument of %s must be a List")); EXTERN char e_unsupportedoption[] INIT(= N_("E519: Option not supported")); EXTERN char e_fnametoolong[] INIT(= N_("E856: Filename too long")); EXTERN char e_float_as_string[] INIT(= N_("E806: using Float as a String")); diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 731e7d8d36..f5993c3f55 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -87,7 +87,8 @@ function vim._load_package(name) return nil end -table.insert(package.loaders, 1, vim._load_package) +-- Insert vim._load_package after the preloader at position 2 +table.insert(package.loaders, 2, vim._load_package) -- These are for loading runtime modules lazily since they aren't available in -- the nvim binary as specified in executor.c diff --git a/src/nvim/os/env.c b/src/nvim/os/env.c index e9f44d2775..e9868d6b61 100644 --- a/src/nvim/os/env.c +++ b/src/nvim/os/env.c @@ -1111,10 +1111,9 @@ size_t home_replace(const buf_T *const buf, const char_u *src, char_u *const dst *dst_p++ = '~'; } - // If it's just the home directory, add "/". - if (!vim_ispathsep(src[0]) && --dstlen > 0) { - *dst_p++ = '/'; - } + // Do not add directory separator into dst, because dst is + // expected to just return the directory name without the + // directory separator '/'. break; } if (p == homedir_env_mod) { diff --git a/src/nvim/path.c b/src/nvim/path.c index 39e276e0a5..7f7f941e26 100644 --- a/src/nvim/path.c +++ b/src/nvim/path.c @@ -269,16 +269,17 @@ int vim_ispathlistsep(int c) #endif } -/* - * Shorten the path of a file from "~/foo/../.bar/fname" to "~/f/../.b/fname" - * It's done in-place. - */ -char_u *shorten_dir(char_u *str) +/// Shorten the path of a file from "~/foo/../.bar/fname" to "~/f/../.b/fname" +/// "trim_len" specifies how many characters to keep for each directory. +/// Must be 1 or more. +/// It's done in-place. +void shorten_dir_len(char_u *str, int trim_len) { char_u *tail = path_tail(str); char_u *d = str; bool skip = false; - for (char_u *s = str;; ++s) { + int dirchunk_len = 0; + for (char_u *s = str;; s++) { if (s >= tail) { // copy the whole tail *d++ = *s; if (*s == NUL) { @@ -287,10 +288,16 @@ char_u *shorten_dir(char_u *str) } else if (vim_ispathsep(*s)) { // copy '/' and next char *d++ = *s; skip = false; + dirchunk_len = 0; } else if (!skip) { *d++ = *s; // copy next char if (*s != '~' && *s != '.') { // and leading "~" and "." - skip = true; + dirchunk_len++; // only count word chars for the size + // keep copying chars until we have our preferred length (or + // until the above if/else branches move us along) + if (dirchunk_len >= trim_len) { + skip = true; + } } int l = utfc_ptr2len(s); while (--l > 0) { @@ -298,7 +305,13 @@ char_u *shorten_dir(char_u *str) } } } - return str; +} + +/// Shorten the path of a file from "~/foo/../.bar/fname" to "~/f/../.b/fname" +/// It's done in-place. +void shorten_dir(char_u *str) +{ + shorten_dir_len(str, 1); } /* @@ -1508,7 +1521,7 @@ void simplify_filename(char_u *filename) p = filename; #ifdef BACKSLASH_IN_FILENAME - if (p[1] == ':') { // skip "x:" + if (p[0] != NUL && p[1] == ':') { // skip "x:" p += 2; } #endif @@ -2389,9 +2402,11 @@ static int path_to_absolute(const char_u *fname, char_u *buf, size_t len, int fo int path_is_absolute(const char_u *fname) { #ifdef WIN32 + if (*fname == NUL) { + return false; + } // A name like "d:/foo" and "//server/share" is absolute - return ((isalpha(fname[0]) && fname[1] == ':' - && vim_ispathsep_nocolon(fname[2])) + return ((isalpha(fname[0]) && fname[1] == ':' && vim_ispathsep_nocolon(fname[2])) || (vim_ispathsep_nocolon(fname[0]) && fname[0] == fname[1])); #else // UNIX: This just checks if the file name starts with '/' or '~'. diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 0196e05455..c609daa55c 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -5194,49 +5194,93 @@ static bool vgr_qflist_valid(win_T *wp, qf_info_T *qi, unsigned qfid, char_u *ti /// Search for a pattern in all the lines in a buffer and add the matching lines /// to a quickfix list. -static bool vgr_match_buflines(qf_list_T *qfl, char_u *fname, buf_T *buf, regmmatch_T *regmatch, - long *tomatch, int duplicate_name, int flags) - FUNC_ATTR_NONNULL_ARG(1, 3, 4, 5) +static bool vgr_match_buflines(qf_list_T *qfl, char_u *fname, buf_T *buf, char_u *spat, + regmmatch_T *regmatch, long *tomatch, int duplicate_name, int flags) + FUNC_ATTR_NONNULL_ARG(1, 3, 4, 5, 6) { bool found_match = false; for (long lnum = 1; lnum <= buf->b_ml.ml_line_count && *tomatch > 0; lnum++) { colnr_T col = 0; - while (vim_regexec_multi(regmatch, curwin, buf, lnum, col, NULL, - NULL) > 0) { - // Pass the buffer number so that it gets used even for a - // dummy buffer, unless duplicate_name is set, then the - // buffer will be wiped out below. - if (qf_add_entry(qfl, - NULL, // dir - fname, - NULL, - duplicate_name ? 0 : buf->b_fnum, - ml_get_buf(buf, regmatch->startpos[0].lnum + lnum, - false), - regmatch->startpos[0].lnum + lnum, - regmatch->endpos[0].lnum + lnum, - regmatch->startpos[0].col + 1, - regmatch->endpos[0].col + 1, - false, // vis_col - NULL, // search pattern - 0, // nr - 0, // type - true) // valid - == QF_FAIL) { - got_int = true; - break; - } - found_match = true; - if (--*tomatch == 0) { - break; - } - if ((flags & VGR_GLOBAL) == 0 || regmatch->endpos[0].lnum > 0) { - break; + if (!(flags & VGR_FUZZY)) { + // Regular expression match + while (vim_regexec_multi(regmatch, curwin, buf, lnum, col, NULL, NULL) > 0) { + // Pass the buffer number so that it gets used even for a + // dummy buffer, unless duplicate_name is set, then the + // buffer will be wiped out below. + if (qf_add_entry(qfl, + NULL, // dir + fname, + NULL, + duplicate_name ? 0 : buf->b_fnum, + ml_get_buf(buf, regmatch->startpos[0].lnum + lnum, false), + regmatch->startpos[0].lnum + lnum, + regmatch->endpos[0].lnum + lnum, + regmatch->startpos[0].col + 1, + regmatch->endpos[0].col + 1, + false, // vis_col + NULL, // search pattern + 0, // nr + 0, // type + true) // valid + == QF_FAIL) { + got_int = true; + break; + } + found_match = true; + if (--*tomatch == 0) { + break; + } + if ((flags & VGR_GLOBAL) == 0 || regmatch->endpos[0].lnum > 0) { + break; + } + col = regmatch->endpos[0].col + (col == regmatch->endpos[0].col); + if (col > (colnr_T)STRLEN(ml_get_buf(buf, lnum, false))) { + break; + } } - col = regmatch->endpos[0].col + (col == regmatch->endpos[0].col); - if (col > (colnr_T)STRLEN(ml_get_buf(buf, lnum, false))) { - break; + } else { + const size_t pat_len = STRLEN(spat); + char_u *const str = ml_get_buf(buf, lnum, false); + int score; + uint32_t matches[MAX_FUZZY_MATCHES]; + const size_t sz = sizeof(matches) / sizeof(matches[0]); + + // Fuzzy string match + while (fuzzy_match(str + col, spat, false, &score, matches, (int)sz) > 0) { + // Pass the buffer number so that it gets used even for a + // dummy buffer, unless duplicate_name is set, then the + // buffer will be wiped out below. + if (qf_add_entry(qfl, + NULL, // dir + fname, + NULL, + duplicate_name ? 0 : buf->b_fnum, + str, + lnum, + 0, + (colnr_T)matches[0] + col + 1, + 0, + false, // vis_col + NULL, // search pattern + 0, // nr + 0, // type + true) // valid + == QF_FAIL) { + got_int = true; + break; + } + found_match = true; + if (--*tomatch == 0) { + break; + } + if ((flags & VGR_GLOBAL) == 0) { + break; + } + col = (colnr_T)matches[pat_len - 1] + col + 1; + if (col > (colnr_T)STRLEN(str)) { + break; + } } } line_breakcheck(); @@ -5418,8 +5462,7 @@ void ex_vimgrep(exarg_T *eap) } else { // Try for a match in all lines of the buffer. // For ":1vimgrep" look for first match only. - found_match = vgr_match_buflines(qf_get_curlist(qi), - fname, buf, ®match, &tomatch, + found_match = vgr_match_buflines(qf_get_curlist(qi), fname, buf, s, ®match, &tomatch, duplicate_name, flags); if (using_dummy) { diff --git a/src/nvim/quickfix.h b/src/nvim/quickfix.h index f5178e332a..0da43e436c 100644 --- a/src/nvim/quickfix.h +++ b/src/nvim/quickfix.h @@ -7,6 +7,7 @@ // flags for skip_vimgrep_pat() #define VGR_GLOBAL 1 #define VGR_NOJUMP 2 +#define VGR_FUZZY 4 #ifdef INCLUDE_GENERATED_DECLARATIONS # include "quickfix.h.generated.h" diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 7e7115b6af..6b2a2afa41 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -7350,7 +7350,7 @@ void draw_tabline(void) if (room > 0) { // Get buffer name in NameBuff[] get_trans_bufname(cwp->w_buffer); - (void)shorten_dir(NameBuff); + shorten_dir(NameBuff); len = vim_strsize(NameBuff); p = NameBuff; while (len > room) { diff --git a/src/nvim/search.c b/src/nvim/search.c index 93180f97fe..3a2435e07a 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -4764,6 +4764,535 @@ the_end: restore_last_search_pattern(); } +/// Fuzzy string matching +/// +/// Ported from the lib_fts library authored by Forrest Smith. +/// https://github.com/forrestthewoods/lib_fts/tree/master/code +/// +/// The following blog describes the fuzzy matching algorithm: +/// https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/ +/// +/// Each matching string is assigned a score. The following factors are checked: +/// - Matched letter +/// - Unmatched letter +/// - Consecutively matched letters +/// - Proximity to start +/// - Letter following a separator (space, underscore) +/// - Uppercase letter following lowercase (aka CamelCase) +/// +/// Matched letters are good. Unmatched letters are bad. Matching near the start +/// is good. Matching the first letter in the middle of a phrase is good. +/// Matching the uppercase letters in camel case entries is good. +/// +/// The score assigned for each factor is explained below. +/// File paths are different from file names. File extensions may be ignorable. +/// Single words care about consecutive matches but not separators or camel +/// case. +/// Score starts at 100 +/// Matched letter: +0 points +/// Unmatched letter: -1 point +/// Consecutive match bonus: +15 points +/// First letter bonus: +15 points +/// Separator bonus: +30 points +/// Camel case bonus: +30 points +/// Unmatched leading letter: -5 points (max: -15) +/// +/// There is some nuance to this. Scores don’t have an intrinsic meaning. The +/// score range isn’t 0 to 100. It’s roughly [50, 150]. Longer words have a +/// lower minimum score due to unmatched letter penalty. Longer search patterns +/// have a higher maximum score due to match bonuses. +/// +/// Separator and camel case bonus is worth a LOT. Consecutive matches are worth +/// quite a bit. +/// +/// There is a penalty if you DON’T match the first three letters. Which +/// effectively rewards matching near the start. However there’s no difference +/// in matching between the middle and end. +/// +/// There is not an explicit bonus for an exact match. Unmatched letters receive +/// a penalty. So shorter strings and closer matches are worth more. +typedef struct { + int idx; ///< used for stable sort + listitem_T *item; + int score; + list_T *lmatchpos; +} fuzzyItem_T; + +/// bonus for adjacent matches; this is higher than SEPARATOR_BONUS so that +/// matching a whole word is preferred. +#define SEQUENTIAL_BONUS 40 +/// bonus if match occurs after a path separator +#define PATH_SEPARATOR_BONUS 30 +/// bonus if match occurs after a word separator +#define WORD_SEPARATOR_BONUS 25 +/// bonus if match is uppercase and prev is lower +#define CAMEL_BONUS 30 +/// bonus if the first letter is matched +#define FIRST_LETTER_BONUS 15 +/// penalty applied for every letter in str before the first match +#define LEADING_LETTER_PENALTY -5 +/// maximum penalty for leading letters +#define MAX_LEADING_LETTER_PENALTY -15 +/// penalty for every letter that doesn't match +#define UNMATCHED_LETTER_PENALTY -1 +/// penalty for gap in matching positions (-2 * k) +#define GAP_PENALTY -2 +/// Score for a string that doesn't fuzzy match the pattern +#define SCORE_NONE -9999 + +#define FUZZY_MATCH_RECURSION_LIMIT 10 + +/// Compute a score for a fuzzy matched string. The matching character locations +/// are in 'matches'. +static int fuzzy_match_compute_score(const char_u *const str, const int strSz, + const uint32_t *const matches, const int numMatches) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + // Initialize score + int score = 100; + + // Apply leading letter penalty + int penalty = LEADING_LETTER_PENALTY * matches[0]; + if (penalty < MAX_LEADING_LETTER_PENALTY) { + penalty = MAX_LEADING_LETTER_PENALTY; + } + score += penalty; + + // Apply unmatched penalty + const int unmatched = strSz - numMatches; + score += UNMATCHED_LETTER_PENALTY * unmatched; + + // Apply ordering bonuses + for (int i = 0; i < numMatches; i++) { + const uint32_t currIdx = matches[i]; + + if (i > 0) { + const uint32_t prevIdx = matches[i - 1]; + + // Sequential + if (currIdx == prevIdx + 1) { + score += SEQUENTIAL_BONUS; + } else { + score += GAP_PENALTY * (currIdx - prevIdx); + } + } + + // Check for bonuses based on neighbor character value + if (currIdx > 0) { + // Camel case + const char_u *p = str; + int neighbor; + + for (uint32_t sidx = 0; sidx < currIdx; sidx++) { + neighbor = utf_ptr2char(p); + MB_PTR_ADV(p); + } + const int curr = utf_ptr2char(p); + + if (mb_islower(neighbor) && mb_isupper(curr)) { + score += CAMEL_BONUS; + } + + // Bonus if the match follows a separator character + if (neighbor == '/' || neighbor == '\\') { + score += PATH_SEPARATOR_BONUS; + } else if (neighbor == ' ' || neighbor == '_') { + score += WORD_SEPARATOR_BONUS; + } + } else { + // First letter + score += FIRST_LETTER_BONUS; + } + } + return score; +} + +/// Perform a recursive search for fuzzy matching 'fuzpat' in 'str'. +/// @return the number of matching characters. +static int fuzzy_match_recursive(const char_u *fuzpat, const char_u *str, uint32_t strIdx, + int *const outScore, const char_u *const strBegin, + const int strLen, const uint32_t *const srcMatches, + uint32_t *const matches, const int maxMatches, int nextMatch, + int *const recursionCount) + FUNC_ATTR_NONNULL_ARG(1, 2, 4, 5, 8, 11) FUNC_ATTR_WARN_UNUSED_RESULT +{ + // Recursion params + bool recursiveMatch = false; + uint32_t bestRecursiveMatches[MAX_FUZZY_MATCHES]; + int bestRecursiveScore = 0; + + // Count recursions + (*recursionCount)++; + if (*recursionCount >= FUZZY_MATCH_RECURSION_LIMIT) { + return 0; + } + + // Detect end of strings + if (*fuzpat == NUL || *str == NUL) { + return 0; + } + + // Loop through fuzpat and str looking for a match + bool first_match = true; + while (*fuzpat != NUL && *str != NUL) { + const int c1 = utf_ptr2char(fuzpat); + const int c2 = utf_ptr2char(str); + + // Found match + if (mb_tolower(c1) == mb_tolower(c2)) { + // Supplied matches buffer was too short + if (nextMatch >= maxMatches) { + return 0; + } + + // "Copy-on-Write" srcMatches into matches + if (first_match && srcMatches != NULL) { + memcpy(matches, srcMatches, nextMatch * sizeof(srcMatches[0])); + first_match = false; + } + + // Recursive call that "skips" this match + uint32_t recursiveMatches[MAX_FUZZY_MATCHES]; + int recursiveScore = 0; + const char_u *const next_char = str + utfc_ptr2len(str); + if (fuzzy_match_recursive(fuzpat, next_char, strIdx + 1, &recursiveScore, strBegin, strLen, + matches, recursiveMatches, + sizeof(recursiveMatches) / sizeof(recursiveMatches[0]), nextMatch, + recursionCount)) { + // Pick best recursive score + if (!recursiveMatch || recursiveScore > bestRecursiveScore) { + memcpy(bestRecursiveMatches, recursiveMatches, + MAX_FUZZY_MATCHES * sizeof(recursiveMatches[0])); + bestRecursiveScore = recursiveScore; + } + recursiveMatch = true; + } + + // Advance + matches[nextMatch++] = strIdx; + MB_PTR_ADV(fuzpat); + } + MB_PTR_ADV(str); + strIdx++; + } + + // Determine if full fuzpat was matched + const bool matched = *fuzpat == NUL; + + // Calculate score + if (matched) { + *outScore = fuzzy_match_compute_score(strBegin, strLen, matches, nextMatch); + } + + // Return best result + if (recursiveMatch && (!matched || bestRecursiveScore > *outScore)) { + // Recursive score is better than "this" + memcpy(matches, bestRecursiveMatches, maxMatches * sizeof(matches[0])); + *outScore = bestRecursiveScore; + return nextMatch; + } else if (matched) { + return nextMatch; // "this" score is better than recursive + } + + return 0; // no match +} + +/// fuzzy_match() +/// +/// Performs exhaustive search via recursion to find all possible matches and +/// match with highest score. +/// Scores values have no intrinsic meaning. Possible score range is not +/// normalized and varies with pattern. +/// Recursion is limited internally (default=10) to prevent degenerate cases +/// (pat_arg="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"). +/// Uses char_u for match indices. Therefore patterns are limited to +/// MAX_FUZZY_MATCHES characters. +/// +/// @return true if 'pat_arg' matches 'str'. Also returns the match score in +/// 'outScore' and the matching character positions in 'matches'. +bool fuzzy_match(char_u *const str, const char_u *const pat_arg, const bool matchseq, + int *const outScore, uint32_t *const matches, const int maxMatches) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT +{ + const int len = mb_charlen(str); + bool complete = false; + int numMatches = 0; + + *outScore = 0; + + char_u *const save_pat = vim_strsave(pat_arg); + char_u *pat = save_pat; + char_u *p = pat; + + // Try matching each word in 'pat_arg' in 'str' + while (true) { + if (matchseq) { + complete = true; + } else { + // Extract one word from the pattern (separated by space) + p = skipwhite(p); + if (*p == NUL) { + break; + } + pat = p; + while (*p != NUL && !ascii_iswhite(utf_ptr2char(p))) { + MB_PTR_ADV(p); + } + if (*p == NUL) { // processed all the words + complete = true; + } + *p = NUL; + } + + int score = 0; + int recursionCount = 0; + const int matchCount + = fuzzy_match_recursive(pat, str, 0, &score, str, len, NULL, matches + numMatches, + maxMatches - numMatches, 0, &recursionCount); + if (matchCount == 0) { + numMatches = 0; + break; + } + + // Accumulate the match score and the number of matches + *outScore += score; + numMatches += matchCount; + + if (complete) { + break; + } + + // try matching the next word + p++; + } + + xfree(save_pat); + return numMatches != 0; +} + +/// Sort the fuzzy matches in the descending order of the match score. +/// For items with same score, retain the order using the index (stable sort) +static int fuzzy_match_item_compare(const void *const s1, const void *const s2) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + const int v1 = ((const fuzzyItem_T *)s1)->score; + const int v2 = ((const fuzzyItem_T *)s2)->score; + const int idx1 = ((const fuzzyItem_T *)s1)->idx; + const int idx2 = ((const fuzzyItem_T *)s2)->idx; + + return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1; +} + +/// Fuzzy search the string 'str' in a list of 'items' and return the matching +/// strings in 'fmatchlist'. +/// If 'matchseq' is true, then for multi-word search strings, match all the +/// words in sequence. +/// If 'items' is a list of strings, then search for 'str' in the list. +/// If 'items' is a list of dicts, then either use 'key' to lookup the string +/// for each item or use 'item_cb' Funcref function to get the string. +/// If 'retmatchpos' is true, then return a list of positions where 'str' +/// matches for each item. +static void fuzzy_match_in_list(list_T *const items, char_u *const str, const bool matchseq, + const char_u *const key, Callback *const item_cb, + const bool retmatchpos, list_T *const fmatchlist) + FUNC_ATTR_NONNULL_ARG(2, 5, 7) +{ + const long len = tv_list_len(items); + if (len == 0) { + return; + } + + fuzzyItem_T *const ptrs = xcalloc(len, sizeof(fuzzyItem_T)); + long i = 0; + bool found_match = false; + uint32_t matches[MAX_FUZZY_MATCHES]; + + // For all the string items in items, get the fuzzy matching score + TV_LIST_ITER(items, li, { + ptrs[i].idx = i; + ptrs[i].item = li; + ptrs[i].score = SCORE_NONE; + char_u *itemstr = NULL; + typval_T rettv; + rettv.v_type = VAR_UNKNOWN; + const typval_T *const tv = TV_LIST_ITEM_TV(li); + if (tv->v_type == VAR_STRING) { // list of strings + itemstr = tv->vval.v_string; + } else if (tv->v_type == VAR_DICT && (key != NULL || item_cb->type != kCallbackNone)) { + // For a dict, either use the specified key to lookup the string or + // use the specified callback function to get the string. + if (key != NULL) { + itemstr = (char_u *)tv_dict_get_string(tv->vval.v_dict, (const char *)key, false); + } else { + typval_T argv[2]; + + // Invoke the supplied callback (if any) to get the dict item + tv->vval.v_dict->dv_refcount++; + argv[0].v_type = VAR_DICT; + argv[0].vval.v_dict = tv->vval.v_dict; + argv[1].v_type = VAR_UNKNOWN; + if (callback_call(item_cb, 1, argv, &rettv)) { + if (rettv.v_type == VAR_STRING) { + itemstr = rettv.vval.v_string; + } + } + tv_dict_unref(tv->vval.v_dict); + } + } + + int score; + if (itemstr != NULL && fuzzy_match(itemstr, str, matchseq, &score, matches, + sizeof(matches) / sizeof(matches[0]))) { + // Copy the list of matching positions in itemstr to a list, if + // 'retmatchpos' is set. + if (retmatchpos) { + ptrs[i].lmatchpos = tv_list_alloc(kListLenMayKnow); + int j = 0; + const char_u *p = str; + while (*p != NUL) { + if (!ascii_iswhite(utf_ptr2char(p))) { + tv_list_append_number(ptrs[i].lmatchpos, matches[j]); + j++; + } + MB_PTR_ADV(p); + } + } + ptrs[i].score = score; + found_match = true; + } + i++; + tv_clear(&rettv); + }); + + if (found_match) { + // Sort the list by the descending order of the match score + qsort(ptrs, len, sizeof(fuzzyItem_T), fuzzy_match_item_compare); + + // For matchfuzzy(), return a list of matched strings. + // ['str1', 'str2', 'str3'] + // For matchfuzzypos(), return a list with three items. + // The first item is a list of matched strings. The second item + // is a list of lists where each list item is a list of matched + // character positions. The third item is a list of matching scores. + // [['str1', 'str2', 'str3'], [[1, 3], [1, 3], [1, 3]]] + list_T *l; + if (retmatchpos) { + const listitem_T *const li = tv_list_find(fmatchlist, 0); + assert(li != NULL && TV_LIST_ITEM_TV(li)->vval.v_list != NULL); + l = TV_LIST_ITEM_TV(li)->vval.v_list; + } else { + l = fmatchlist; + } + + // Copy the matching strings with a valid score to the return list + for (i = 0; i < len; i++) { + if (ptrs[i].score == SCORE_NONE) { + break; + } + tv_list_append_tv(l, TV_LIST_ITEM_TV(ptrs[i].item)); + } + + // next copy the list of matching positions + if (retmatchpos) { + const listitem_T *li = tv_list_find(fmatchlist, -2); + assert(li != NULL && TV_LIST_ITEM_TV(li)->vval.v_list != NULL); + l = TV_LIST_ITEM_TV(li)->vval.v_list; + for (i = 0; i < len; i++) { + if (ptrs[i].score == SCORE_NONE) { + break; + } + tv_list_append_list(l, ptrs[i].lmatchpos); + } + + // copy the matching scores + li = tv_list_find(fmatchlist, -1); + assert(li != NULL && TV_LIST_ITEM_TV(li)->vval.v_list != NULL); + l = TV_LIST_ITEM_TV(li)->vval.v_list; + for (i = 0; i < len; i++) { + if (ptrs[i].score == SCORE_NONE) { + break; + } + tv_list_append_number(l, ptrs[i].score); + } + } + } + xfree(ptrs); +} + +/// Do fuzzy matching. Returns the list of matched strings in 'rettv'. +/// If 'retmatchpos' is true, also returns the matching character positions. +static void do_fuzzymatch(const typval_T *const argvars, typval_T *const rettv, + const bool retmatchpos) + FUNC_ATTR_NONNULL_ALL +{ + // validate and get the arguments + if (argvars[0].v_type != VAR_LIST || argvars[0].vval.v_list == NULL) { + semsg(_(e_listarg), retmatchpos ? "matchfuzzypos()" : "matchfuzzy()"); + return; + } + if (argvars[1].v_type != VAR_STRING || argvars[1].vval.v_string == NULL) { + semsg(_(e_invarg2), tv_get_string(&argvars[1])); + return; + } + + Callback cb = CALLBACK_NONE; + const char_u *key = NULL; + bool matchseq = false; + if (argvars[2].v_type != VAR_UNKNOWN) { + if (argvars[2].v_type != VAR_DICT || argvars[2].vval.v_dict == NULL) { + emsg(_(e_dictreq)); + return; + } + + // To search a dict, either a callback function or a key can be + // specified. + dict_T *const d = argvars[2].vval.v_dict; + const dictitem_T *const di = tv_dict_find(d, "key", -1); + if (di != NULL) { + if (di->di_tv.v_type != VAR_STRING || di->di_tv.vval.v_string == NULL + || *di->di_tv.vval.v_string == NUL) { + semsg(_(e_invarg2), tv_get_string(&di->di_tv)); + return; + } + key = (const char_u *)tv_get_string(&di->di_tv); + } else if (!tv_dict_get_callback(d, "text_cb", -1, &cb)) { + semsg(_(e_invargval), "text_cb"); + return; + } + if (tv_dict_find(d, "matchseq", -1) != NULL) { + matchseq = true; + } + } + + // get the fuzzy matches + tv_list_alloc_ret(rettv, retmatchpos ? 3 : kListLenUnknown); + if (retmatchpos) { + // For matchfuzzypos(), a list with three items are returned. First + // item is a list of matching strings, the second item is a list of + // lists with matching positions within each string and the third item + // is the list of scores of the matches. + tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown)); + tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown)); + tv_list_append_list(rettv->vval.v_list, tv_list_alloc(kListLenUnknown)); + } + + fuzzy_match_in_list(argvars[0].vval.v_list, (char_u *)tv_get_string(&argvars[1]), matchseq, key, + &cb, retmatchpos, rettv->vval.v_list); + callback_free(&cb); +} + +/// "matchfuzzy()" function +void f_matchfuzzy(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + do_fuzzymatch(argvars, rettv, false); +} + +/// "matchfuzzypos()" function +void f_matchfuzzypos(typval_T *argvars, typval_T *rettv, FunPtr fptr) +{ + do_fuzzymatch(argvars, rettv, true); +} + /// Find identifiers or defines in included files. /// If p_ic && (compl_cont_status & CONT_SOL) then ptr must be in lowercase. /// diff --git a/src/nvim/search.h b/src/nvim/search.h index 15b8d41f39..53059cc1ea 100644 --- a/src/nvim/search.h +++ b/src/nvim/search.h @@ -55,6 +55,9 @@ #define SEARCH_STAT_DEF_MAX_COUNT 99 #define SEARCH_STAT_BUF_LEN 12 +/// Maximum number of characters that can be fuzzy matched +#define MAX_FUZZY_MATCHES 256 + /// Structure containing offset definition for the last search pattern /// /// @note Only offset for the last search pattern is used, not for the last diff --git a/src/nvim/state.c b/src/nvim/state.c index 9e4c9b2bad..f9a3aaab7f 100644 --- a/src/nvim/state.c +++ b/src/nvim/state.c @@ -39,10 +39,16 @@ void state_enter(VimState *s) int key; getkey: - if (char_avail() || using_script() || input_available()) { - // Don't block for events if there's a character already available for - // processing. Characters can come from mappings, scripts and other - // sources, so this scenario is very common. + // Expand mappings first by calling vpeekc() directly. + // - If vpeekc() returns non-NUL, there is a character already available for processing, so + // don't block for events. vgetc() may still block, in case of an incomplete UTF-8 sequence. + // - If vpeekc() returns NUL, vgetc() will block, and there are three cases: + // - There is no input available. + // - All of available input maps to an empty string. + // - There is an incomplete mapping. + // A blocking wait for a character should only be done in the third case, which is the only + // case of the three where typebuf.tb_len > 0 after vpeekc() returns NUL. + if (vpeekc() != NUL || typebuf.tb_len > 0) { key = safe_vgetc(); } else if (!multiqueue_empty(main_loop.events)) { // Event was made available after the last multiqueue_process_events call @@ -55,9 +61,11 @@ getkey: // mapping engine. (void)os_inchar(NULL, 0, -1, 0, main_loop.events); // If an event was put into the queue, we send K_EVENT directly. - key = !multiqueue_empty(main_loop.events) - ? K_EVENT - : safe_vgetc(); + if (!multiqueue_empty(main_loop.events)) { + key = K_EVENT; + } else { + goto getkey; + } } if (key == K_EVENT) { diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 433410248b..a8d51ef598 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -4,7 +4,7 @@ source shared.vim source check.vim source term_util.vim -func! s:cleanup_buffers() abort +func s:cleanup_buffers() abort for bnr in range(1, bufnr('$')) if bufloaded(bnr) && bufnr('%') != bnr execute 'bd! ' . bnr diff --git a/src/nvim/testdir/test_breakindent.vim b/src/nvim/testdir/test_breakindent.vim index 8d592f21ea..b619f2adb6 100644 --- a/src/nvim/testdir/test_breakindent.vim +++ b/src/nvim/testdir/test_breakindent.vim @@ -20,7 +20,7 @@ func s:screen_lines2(lnums, lnume, width) abort return ScreenLines([a:lnums, a:lnume], a:width) endfunc -func! s:compare_lines(expect, actual) +func s:compare_lines(expect, actual) call assert_equal(join(a:expect, "\n"), join(a:actual, "\n")) endfunc diff --git a/src/nvim/testdir/test_charsearch_utf8.vim b/src/nvim/testdir/test_charsearch_utf8.vim index 09341a90b0..82a807ac5b 100644 --- a/src/nvim/testdir/test_charsearch_utf8.vim +++ b/src/nvim/testdir/test_charsearch_utf8.vim @@ -1,7 +1,7 @@ " Tests for related f{char} and t{char} using utf-8. " Test for t,f,F,T movement commands -function! Test_search_cmds() +func Test_search_cmds() new! call setline(1, "・最初から最後まで最強のVimは最高") 1 diff --git a/src/nvim/testdir/test_display.vim b/src/nvim/testdir/test_display.vim index c2a9683f7c..9f74d0a38a 100644 --- a/src/nvim/testdir/test_display.vim +++ b/src/nvim/testdir/test_display.vim @@ -41,7 +41,7 @@ func Test_display_foldcolumn() quit! endfunc -func! Test_display_foldtext_mbyte() +func Test_display_foldtext_mbyte() CheckFeature folding call NewWindow(10, 40) diff --git a/src/nvim/testdir/test_edit.vim b/src/nvim/testdir/test_edit.vim index 17393d6edb..a1f6a84a99 100644 --- a/src/nvim/testdir/test_edit.vim +++ b/src/nvim/testdir/test_edit.vim @@ -213,7 +213,7 @@ func Test_edit_07() bw! endfunc -func! Test_edit_08() +func Test_edit_08() throw 'skipped: moved to test/functional/legacy/edit_spec.lua' " reset insertmode from i_ctrl-r_= let g:bufnr = bufnr('%') @@ -417,7 +417,7 @@ func Test_edit_13() bwipe! endfunc -func! Test_edit_CR() +func Test_edit_CR() " Test for <CR> in insert mode " basically only in quickfix mode ist tested, the rest " has been taken care of by other tests @@ -450,7 +450,7 @@ func! Test_edit_CR() call delete('Xqflist.txt') endfunc -func! Test_edit_CTRL_() +func Test_edit_CTRL_() " disabled for Windows builds, why? if !has("rightleft") || has("win32") return @@ -734,7 +734,7 @@ func Test_edit_CTRL_O() bw! endfunc -func! Test_edit_CTRL_R() +func Test_edit_CTRL_R() " Insert Register new " call test_override("ALL", 1) diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index d4e5563865..4ef35b3a46 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -731,101 +731,126 @@ func Test_setfiletype_completion() call assert_equal('"setfiletype java javacc javascript javascriptreact', @:) endfunc -func Test_hook_file() +""""""""""""""""""""""""""""""""""""""""""""""""" +" Tests for specific extentions and filetypes. +" Keep sorted. +""""""""""""""""""""""""""""""""""""""""""""""""" + +func Test_bas_file() filetype on - call writefile(['[Trigger]', 'this is pacman config'], 'Xfile.hook') - split Xfile.hook - call assert_equal('dosini', &filetype) + call writefile(['looks like BASIC'], 'Xfile.bas') + split Xfile.bas + call assert_equal('basic', &filetype) bwipe! - call writefile(['not pacman'], 'Xfile.hook') - split Xfile.hook - call assert_notequal('dosini', &filetype) + " Test dist#ft#FTbas() + + let g:filetype_bas = 'freebasic' + split Xfile.bas + call assert_equal('freebasic', &filetype) bwipe! + unlet g:filetype_bas - call delete('Xfile.hook') - filetype off -endfunc + " FreeBASIC -func Test_tf_file() - filetype on + call writefile(["/' FreeBASIC multiline comment '/"], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) + bwipe! - call writefile([';;; TF MUD client is super duper cool'], 'Xfile.tf') - split Xfile.tf - call assert_equal('tf', &filetype) + call writefile(['#define TESTING'], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) bwipe! - call writefile(['provider "azurerm" {'], 'Xfile.tf') - split Xfile.tf - call assert_equal('terraform', &filetype) + call writefile(['option byval'], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) bwipe! - call delete('Xfile.tf') - filetype off -endfunc + call writefile(['extern "C"'], 'Xfile.bas') + split Xfile.bas + call assert_equal('freebasic', &filetype) + bwipe! + " QB64 -func Test_ts_file() - filetype on + call writefile(['$LET TESTING = 1'], 'Xfile.bas') + split Xfile.bas + call assert_equal('qb64', &filetype) + bwipe! - call writefile(['<?xml version="1.0" encoding="utf-8"?>'], 'Xfile.ts') - split Xfile.ts - call assert_equal('xml', &filetype) + call writefile(['OPTION _EXPLICIT'], 'Xfile.bas') + split Xfile.bas + call assert_equal('qb64', &filetype) bwipe! - call writefile(['// looks like Typescript'], 'Xfile.ts') - split Xfile.ts - call assert_equal('typescript', &filetype) + " Visual Basic + + call writefile(['Attribute VB_NAME = "Testing"'], 'Xfile.bas') + split Xfile.bas + call assert_equal('vb', &filetype) bwipe! - call delete('Xfile.hook') + call delete('Xfile.bas') filetype off endfunc -func Test_ttl_file() +func Test_dep3patch_file() filetype on - call writefile(['@base <http://example.org/> .'], 'Xfile.ttl') - split Xfile.ttl - call assert_equal('turtle', &filetype) - bwipe! + call assert_true(mkdir('debian/patches', 'p')) - call writefile(['looks like Tera Term Language'], 'Xfile.ttl') - split Xfile.ttl - call assert_equal('teraterm', &filetype) + " series files are not patches + call writefile(['Description: some awesome patch'], 'debian/patches/series') + split debian/patches/series + call assert_notequal('dep3patch', &filetype) bwipe! - call delete('Xfile.ttl') - filetype off -endfunc + " diff/patch files without the right headers should still show up as ft=diff + call writefile([], 'debian/patches/foo.diff') + split debian/patches/foo.diff + call assert_equal('diff', &filetype) + bwipe! -func Test_pp_file() - filetype on + " Files with the right headers are detected as dep3patch, even if they don't + " have a diff/patch extension + call writefile(['Subject: dep3patches'], 'debian/patches/bar') + split debian/patches/bar + call assert_equal('dep3patch', &filetype) + bwipe! - call writefile(['looks like puppet'], 'Xfile.pp') - split Xfile.pp - call assert_equal('puppet', &filetype) + " Files in sub-directories are detected + call assert_true(mkdir('debian/patches/s390x', 'p')) + call writefile(['Subject: dep3patches'], 'debian/patches/s390x/bar') + split debian/patches/s390x/bar + call assert_equal('dep3patch', &filetype) bwipe! - let g:filetype_pp = 'pascal' - split Xfile.pp - call assert_equal('pascal', &filetype) + " The detection stops when seeing the "header end" marker + call writefile(['---', 'Origin: the cloud'], 'debian/patches/baz') + split debian/patches/baz + call assert_notequal('dep3patch', &filetype) bwipe! - unlet g:filetype_pp - " Test dist#ft#FTpp() - call writefile(['{ pascal comment'], 'Xfile.pp') - split Xfile.pp - call assert_equal('pascal', &filetype) + call delete('debian', 'rf') +endfunc + +func Test_dsl_file() + filetype on + + call writefile([' <!doctype dsssl-spec ['], 'dslfile.dsl') + split dslfile.dsl + call assert_equal('dsl', &filetype) bwipe! - call writefile(['procedure pascal'], 'Xfile.pp') - split Xfile.pp - call assert_equal('pascal', &filetype) + call writefile(['workspace {'], 'dslfile.dsl') + split dslfile.dsl + call assert_equal('structurizr', &filetype) bwipe! - call delete('Xfile.pp') + call delete('dslfile.dsl') filetype off endfunc @@ -866,20 +891,183 @@ func Test_ex_file() filetype off endfunc -func Test_dsl_file() +func Test_foam_file() filetype on + call assert_true(mkdir('0', 'p')) + call assert_true(mkdir('0.orig', 'p')) - call writefile([' <!doctype dsssl-spec ['], 'dslfile.dsl') - split dslfile.dsl - call assert_equal('dsl', &filetype) + call writefile(['FoamFile {', ' object something;'], 'Xfile1Dict') + split Xfile1Dict + call assert_equal('foam', &filetype) bwipe! - call writefile(['workspace {'], 'dslfile.dsl') - split dslfile.dsl - call assert_equal('structurizr', &filetype) + call writefile(['FoamFile {', ' object something;'], 'Xfile1Dict.something') + split Xfile1Dict.something + call assert_equal('foam', &filetype) bwipe! - call delete('dslfile.dsl') + call writefile(['FoamFile {', ' object something;'], 'XfileProperties') + split XfileProperties + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], 'XfileProperties.something') + split XfileProperties.something + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], 'XfileProperties') + split XfileProperties + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], 'XfileProperties.something') + split XfileProperties.something + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], '0/Xfile') + split 0/Xfile + call assert_equal('foam', &filetype) + bwipe! + + call writefile(['FoamFile {', ' object something;'], '0.orig/Xfile') + split 0.orig/Xfile + call assert_equal('foam', &filetype) + bwipe! + + call delete('0', 'rf') + call delete('0.orig', 'rf') + call delete('Xfile1Dict') + call delete('Xfile1Dict.something') + call delete('XfileProperties') + call delete('XfileProperties.something') + filetype off +endfunc + +func Test_frm_file() + filetype on + + call writefile(['looks like FORM'], 'Xfile.frm') + split Xfile.frm + call assert_equal('form', &filetype) + bwipe! + + " Test dist#ft#FTfrm() + + let g:filetype_frm = 'form' + split Xfile.frm + call assert_equal('form', &filetype) + bwipe! + unlet g:filetype_frm + + " Visual Basic + + call writefile(['Begin VB.Form Form1'], 'Xfile.frm') + split Xfile.frm + call assert_equal('vb', &filetype) + bwipe! + + call delete('Xfile.frm') + filetype off +endfunc + +func Test_fs_file() + filetype on + + call writefile(['looks like F#'], 'Xfile.fs') + split Xfile.fs + call assert_equal('fsharp', &filetype) + bwipe! + + let g:filetype_fs = 'forth' + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + unlet g:filetype_fs + + " Test dist#ft#FTfs() + + " Forth (Gforth) + + call writefile(['( Forth inline comment )'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile(['.( Forth displayed inline comment )'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile(['\ Forth line comment'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + " empty line comment - no space required + call writefile(['\'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile(['\G Forth documentation comment '], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call writefile([': squared ( n -- n^2 )', 'dup * ;'], 'Xfile.fs') + split Xfile.fs + call assert_equal('forth', &filetype) + bwipe! + + call delete('Xfile.fs') + filetype off +endfunc + +func Test_git_file() + filetype on + + call assert_true(mkdir('Xrepo.git', 'p')) + + call writefile([], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('', &filetype) + bwipe! + + call writefile(['0000000000000000000000000000000000000000'], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('git', &filetype) + bwipe! + + call writefile(['0000000000000000000000000000000000000000000000000000000000000000'], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('git', &filetype) + bwipe! + + call writefile(['ref: refs/heads/master'], 'Xrepo.git/HEAD') + split Xrepo.git/HEAD + call assert_equal('git', &filetype) + bwipe! + + call delete('Xrepo.git', 'rf') + filetype off +endfunc + +func Test_hook_file() + filetype on + + call writefile(['[Trigger]', 'this is pacman config'], 'Xfile.hook') + split Xfile.hook + call assert_equal('dosini', &filetype) + bwipe! + + call writefile(['not pacman'], 'Xfile.hook') + split Xfile.hook + call assert_notequal('dosini', &filetype) + bwipe! + + call delete('Xfile.hook') filetype off endfunc @@ -983,111 +1171,6 @@ func Test_m_file() filetype off endfunc -func Test_xpm_file() - filetype on - - call writefile(['this is XPM2'], 'file.xpm') - split file.xpm - call assert_equal('xpm2', &filetype) - bwipe! - - call delete('file.xpm') - filetype off -endfunc - -func Test_fs_file() - filetype on - - call writefile(['looks like F#'], 'Xfile.fs') - split Xfile.fs - call assert_equal('fsharp', &filetype) - bwipe! - - let g:filetype_fs = 'forth' - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - unlet g:filetype_fs - - " Test dist#ft#FTfs() - - " Forth (Gforth) - - call writefile(['( Forth inline comment )'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - - call writefile(['.( Forth displayed inline comment )'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - - call writefile(['\ Forth line comment'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - - " empty line comment - no space required - call writefile(['\'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - - call writefile(['\G Forth documentation comment '], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - - call writefile([': squared ( n -- n^2 )', 'dup * ;'], 'Xfile.fs') - split Xfile.fs - call assert_equal('forth', &filetype) - bwipe! - - call delete('Xfile.fs') - filetype off -endfunc - -func Test_dep3patch_file() - filetype on - - call assert_true(mkdir('debian/patches', 'p')) - - " series files are not patches - call writefile(['Description: some awesome patch'], 'debian/patches/series') - split debian/patches/series - call assert_notequal('dep3patch', &filetype) - bwipe! - - " diff/patch files without the right headers should still show up as ft=diff - call writefile([], 'debian/patches/foo.diff') - split debian/patches/foo.diff - call assert_equal('diff', &filetype) - bwipe! - - " Files with the right headers are detected as dep3patch, even if they don't - " have a diff/patch extension - call writefile(['Subject: dep3patches'], 'debian/patches/bar') - split debian/patches/bar - call assert_equal('dep3patch', &filetype) - bwipe! - - " Files in sub-directories are detected - call assert_true(mkdir('debian/patches/s390x', 'p')) - call writefile(['Subject: dep3patches'], 'debian/patches/s390x/bar') - split debian/patches/s390x/bar - call assert_equal('dep3patch', &filetype) - bwipe! - - " The detection stops when seeing the "header end" marker - call writefile(['---', 'Origin: the cloud'], 'debian/patches/baz') - split debian/patches/baz - call assert_notequal('dep3patch', &filetype) - bwipe! - - call delete('debian', 'rf') -endfunc - func Test_patch_file() filetype on @@ -1110,171 +1193,132 @@ func Test_patch_file() filetype off endfunc -func Test_git_file() +func Test_perl_file() filetype on - call assert_true(mkdir('Xrepo.git', 'p')) - - call writefile([], 'Xrepo.git/HEAD') - split Xrepo.git/HEAD - call assert_equal('', &filetype) - bwipe! - - call writefile(['0000000000000000000000000000000000000000'], 'Xrepo.git/HEAD') - split Xrepo.git/HEAD - call assert_equal('git', &filetype) - bwipe! - - call writefile(['0000000000000000000000000000000000000000000000000000000000000000'], 'Xrepo.git/HEAD') - split Xrepo.git/HEAD - call assert_equal('git', &filetype) - bwipe! + " only tests one case, should do more + let lines =<< trim END - call writefile(['ref: refs/heads/master'], 'Xrepo.git/HEAD') - split Xrepo.git/HEAD - call assert_equal('git', &filetype) - bwipe! + use a + END + call writefile(lines, "Xfile.t") + split Xfile.t + call assert_equal('perl', &filetype) + bwipe - call delete('Xrepo.git', 'rf') + call delete('Xfile.t') filetype off endfunc -func Test_foam_file() +func Test_pp_file() filetype on - call assert_true(mkdir('0', 'p')) - call assert_true(mkdir('0.orig', 'p')) - call writefile(['FoamFile {', ' object something;'], 'Xfile1Dict') - split Xfile1Dict - call assert_equal('foam', &filetype) + call writefile(['looks like puppet'], 'Xfile.pp') + split Xfile.pp + call assert_equal('puppet', &filetype) bwipe! - call writefile(['FoamFile {', ' object something;'], 'Xfile1Dict.something') - split Xfile1Dict.something - call assert_equal('foam', &filetype) + let g:filetype_pp = 'pascal' + split Xfile.pp + call assert_equal('pascal', &filetype) bwipe! + unlet g:filetype_pp - call writefile(['FoamFile {', ' object something;'], 'XfileProperties') - split XfileProperties - call assert_equal('foam', &filetype) + " Test dist#ft#FTpp() + call writefile(['{ pascal comment'], 'Xfile.pp') + split Xfile.pp + call assert_equal('pascal', &filetype) bwipe! - call writefile(['FoamFile {', ' object something;'], 'XfileProperties.something') - split XfileProperties.something - call assert_equal('foam', &filetype) + call writefile(['procedure pascal'], 'Xfile.pp') + split Xfile.pp + call assert_equal('pascal', &filetype) bwipe! - call writefile(['FoamFile {', ' object something;'], 'XfileProperties') - split XfileProperties - call assert_equal('foam', &filetype) - bwipe! + call delete('Xfile.pp') + filetype off +endfunc - call writefile(['FoamFile {', ' object something;'], 'XfileProperties.something') - split XfileProperties.something - call assert_equal('foam', &filetype) - bwipe! +func Test_tex_file() + filetype on - call writefile(['FoamFile {', ' object something;'], '0/Xfile') - split 0/Xfile - call assert_equal('foam', &filetype) - bwipe! + " only tests one case, should do more + let lines =<< trim END + % This is a sentence. - call writefile(['FoamFile {', ' object something;'], '0.orig/Xfile') - split 0.orig/Xfile - call assert_equal('foam', &filetype) - bwipe! + This is a sentence. + END + call writefile(lines, "Xfile.tex") + split Xfile.tex + call assert_equal('plaintex', &filetype) + bwipe - call delete('0', 'rf') - call delete('0.orig', 'rf') + call delete('Xfile.tex') filetype off endfunc -func Test_bas_file() +func Test_tf_file() filetype on - call writefile(['looks like BASIC'], 'Xfile.bas') - split Xfile.bas - call assert_equal('basic', &filetype) + call writefile([';;; TF MUD client is super duper cool'], 'Xfile.tf') + split Xfile.tf + call assert_equal('tf', &filetype) bwipe! - " Test dist#ft#FTbas() - - let g:filetype_bas = 'freebasic' - split Xfile.bas - call assert_equal('freebasic', &filetype) + call writefile(['provider "azurerm" {'], 'Xfile.tf') + split Xfile.tf + call assert_equal('terraform', &filetype) bwipe! - unlet g:filetype_bas - " FreeBASIC - - call writefile(["/' FreeBASIC multiline comment '/"], 'Xfile.bas') - split Xfile.bas - call assert_equal('freebasic', &filetype) - bwipe! + call delete('Xfile.tf') + filetype off +endfunc - call writefile(['#define TESTING'], 'Xfile.bas') - split Xfile.bas - call assert_equal('freebasic', &filetype) - bwipe! +func Test_ts_file() + filetype on - call writefile(['option byval'], 'Xfile.bas') - split Xfile.bas - call assert_equal('freebasic', &filetype) + call writefile(['<?xml version="1.0" encoding="utf-8"?>'], 'Xfile.ts') + split Xfile.ts + call assert_equal('xml', &filetype) bwipe! - call writefile(['extern "C"'], 'Xfile.bas') - split Xfile.bas - call assert_equal('freebasic', &filetype) + call writefile(['// looks like Typescript'], 'Xfile.ts') + split Xfile.ts + call assert_equal('typescript', &filetype) bwipe! - " QB64 + call delete('Xfile.ts') + filetype off +endfunc - call writefile(['$LET TESTING = 1'], 'Xfile.bas') - split Xfile.bas - call assert_equal('qb64', &filetype) - bwipe! +func Test_ttl_file() + filetype on - call writefile(['OPTION _EXPLICIT'], 'Xfile.bas') - split Xfile.bas - call assert_equal('qb64', &filetype) + call writefile(['@base <http://example.org/> .'], 'Xfile.ttl') + split Xfile.ttl + call assert_equal('turtle', &filetype) bwipe! - " Visual Basic - - call writefile(['Attribute VB_NAME = "Testing"'], 'Xfile.bas') - split Xfile.bas - call assert_equal('vb', &filetype) + call writefile(['looks like Tera Term Language'], 'Xfile.ttl') + split Xfile.ttl + call assert_equal('teraterm', &filetype) bwipe! - call delete('Xfile.bas') + call delete('Xfile.ttl') filetype off endfunc -func Test_frm_file() +func Test_xpm_file() filetype on - call writefile(['looks like FORM'], 'Xfile.frm') - split Xfile.frm - call assert_equal('form', &filetype) - bwipe! - - " Test dist#ft#FTfrm() - - let g:filetype_frm = 'form' - split Xfile.frm - call assert_equal('form', &filetype) - bwipe! - unlet g:filetype_frm - - " Visual Basic - - call writefile(['Begin VB.Form Form1'], 'Xfile.frm') - split Xfile.frm - call assert_equal('vb', &filetype) + call writefile(['this is XPM2'], 'file.xpm') + split file.xpm + call assert_equal('xpm2', &filetype) bwipe! - call delete('Xfile.frm') + call delete('file.xpm') filetype off endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_fnamemodify.vim b/src/nvim/testdir/test_fnamemodify.vim index 411f7ebbb3..5ae2a5ee17 100644 --- a/src/nvim/testdir/test_fnamemodify.vim +++ b/src/nvim/testdir/test_fnamemodify.vim @@ -3,8 +3,10 @@ func Test_fnamemodify() let save_home = $HOME let save_shell = &shell + let save_shellslash = &shellslash let $HOME = fnamemodify('.', ':p:h:h') set shell=sh + set shellslash call assert_equal('/', fnamemodify('.', ':p')[-1:]) call assert_equal('r', fnamemodify('.', ':p:h')[-1:]) @@ -27,6 +29,21 @@ func Test_fnamemodify() call assert_equal('fb2.tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e:e')) call assert_equal('fb2.tar.gz', fnamemodify('abc.fb2.tar.gz', ':e:e:e:e')) call assert_equal('tar', fnamemodify('abc.fb2.tar.gz', ':e:e:r')) + call assert_equal(getcwd(), fnamemodify('', ':p:h')) + + let cwd = getcwd() + call chdir($HOME) + call assert_equal('foobar', fnamemodify('~/foobar', ':~:.')) + call chdir(cwd) + call mkdir($HOME . '/XXXXXXXX/a', 'p') + call mkdir($HOME . '/XXXXXXXX/b', 'p') + call chdir($HOME . '/XXXXXXXX/a/') + call assert_equal('foo', fnamemodify($HOME . '/XXXXXXXX/a/foo', ':p:~:.')) + call assert_equal('~/XXXXXXXX/b/foo', fnamemodify($HOME . '/XXXXXXXX/b/foo', ':p:~:.')) + call mkdir($HOME . '/XXXXXXXX/a.ext', 'p') + call assert_equal('~/XXXXXXXX/a.ext/foo', fnamemodify($HOME . '/XXXXXXXX/a.ext/foo', ':p:~:.')) + call chdir(cwd) + call delete($HOME . '/XXXXXXXX', 'rf') call assert_equal('''abc def''', fnamemodify('abc def', ':S')) call assert_equal('''abc" "def''', fnamemodify('abc" "def', ':S')) @@ -44,6 +61,7 @@ func Test_fnamemodify() let $HOME = save_home let &shell = save_shell + let &shellslash = save_shellslash endfunc func Test_fnamemodify_er() @@ -73,6 +91,7 @@ func Test_fnamemodify_er() call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e')) call assert_equal('b.c', fnamemodify('a.b.c.d.e', ':r:r:e:e:e:e')) + call assert_equal('', fnamemodify('', ':p:t')) call assert_equal('', fnamemodify(v:_null_string, v:_null_string)) endfunc diff --git a/src/nvim/testdir/test_functions.vim b/src/nvim/testdir/test_functions.vim index 6c2b0b97b6..438bed51c6 100644 --- a/src/nvim/testdir/test_functions.vim +++ b/src/nvim/testdir/test_functions.vim @@ -376,6 +376,25 @@ func Test_pathshorten() call assert_equal('~.f/bar', pathshorten('~.foo/bar')) call assert_equal('.~f/bar', pathshorten('.~foo/bar')) call assert_equal('~/f/bar', pathshorten('~/foo/bar')) + call assert_fails('call pathshorten([])', 'E730:') + + " test pathshorten with optional variable to set preferred size of shortening + call assert_equal('', pathshorten('', 2)) + call assert_equal('foo', pathshorten('foo', 2)) + call assert_equal('/foo', pathshorten('/foo', 2)) + call assert_equal('fo/', pathshorten('foo/', 2)) + call assert_equal('fo/bar', pathshorten('foo/bar', 2)) + call assert_equal('fo/ba/foobar', pathshorten('foo/bar/foobar', 2)) + call assert_equal('/fo/ba/foobar', pathshorten('/foo/bar/foobar', 2)) + call assert_equal('.fo/bar', pathshorten('.foo/bar', 2)) + call assert_equal('~fo/bar', pathshorten('~foo/bar', 2)) + call assert_equal('~.fo/bar', pathshorten('~.foo/bar', 2)) + call assert_equal('.~fo/bar', pathshorten('.~foo/bar', 2)) + call assert_equal('~/fo/bar', pathshorten('~/foo/bar', 2)) + call assert_fails('call pathshorten([],2)', 'E730:') + call assert_notequal('~/fo/bar', pathshorten('~/foo/bar', 3)) + call assert_equal('~/foo/bar', pathshorten('~/foo/bar', 3)) + call assert_equal('~/f/bar', pathshorten('~/foo/bar', 0)) endfunc func Test_strpart() diff --git a/src/nvim/testdir/test_ins_complete.vim b/src/nvim/testdir/test_ins_complete.vim index f066d842b4..186fa8871f 100644 --- a/src/nvim/testdir/test_ins_complete.vim +++ b/src/nvim/testdir/test_ins_complete.vim @@ -109,7 +109,7 @@ func s:CompleteDone_CompleteFuncNone( findstart, base ) return v:none endfunc -function! s:CompleteDone_CompleteFuncDict( findstart, base ) +func s:CompleteDone_CompleteFuncDict( findstart, base ) if a:findstart return 0 endif @@ -126,7 +126,7 @@ function! s:CompleteDone_CompleteFuncDict( findstart, base ) \ } \ ] \ } -endfunction +endfunc func s:CompleteDone_CheckCompletedItemNone() let s:called_completedone = 1 diff --git a/src/nvim/testdir/test_lambda.vim b/src/nvim/testdir/test_lambda.vim index 72ddbcf6dc..c1fe47d1c9 100644 --- a/src/nvim/testdir/test_lambda.vim +++ b/src/nvim/testdir/test_lambda.vim @@ -1,24 +1,24 @@ " Test for lambda and closure -function! Test_lambda_feature() +func Test_lambda_feature() call assert_equal(1, has('lambda')) -endfunction +endfunc -function! Test_lambda_with_filter() +func Test_lambda_with_filter() let s:x = 2 call assert_equal([2, 3], filter([1, 2, 3], {i, v -> v >= s:x})) -endfunction +endfunc -function! Test_lambda_with_map() +func Test_lambda_with_map() let s:x = 1 call assert_equal([2, 3, 4], map([1, 2, 3], {i, v -> v + s:x})) -endfunction +endfunc -function! Test_lambda_with_sort() +func Test_lambda_with_sort() call assert_equal([1, 2, 3, 4, 7], sort([3,7,2,1,4], {a, b -> a - b})) -endfunction +endfunc -function! Test_lambda_with_timer() +func Test_lambda_with_timer() if !has('timers') return endif @@ -54,10 +54,10 @@ function! Test_lambda_with_timer() call assert_true(s:n > m) endfunc -function! Test_lambda_with_partial() +func Test_lambda_with_partial() let l:Cb = function({... -> ['zero', a:1, a:2, a:3]}, ['one', 'two']) call assert_equal(['zero', 'one', 'two', 'three'], l:Cb('three')) -endfunction +endfunc function Test_lambda_fails() call assert_equal(3, {a, b -> a + b}(1, 2)) @@ -70,59 +70,59 @@ func Test_not_lambda() call assert_equal('foo', x['>']) endfunc -function! Test_lambda_capture_by_reference() +func Test_lambda_capture_by_reference() let v = 1 let l:F = {x -> x + v} let v = 2 call assert_equal(12, l:F(10)) -endfunction +endfunc -function! Test_lambda_side_effect() - function! s:update_and_return(arr) +func Test_lambda_side_effect() + func! s:update_and_return(arr) let a:arr[1] = 5 return a:arr - endfunction + endfunc - function! s:foo(arr) + func! s:foo(arr) return {-> s:update_and_return(a:arr)} - endfunction + endfunc let arr = [3,2,1] call assert_equal([3, 5, 1], s:foo(arr)()) -endfunction +endfunc -function! Test_lambda_refer_local_variable_from_other_scope() - function! s:foo(X) +func Test_lambda_refer_local_variable_from_other_scope() + func! s:foo(X) return a:X() " refer l:x in s:bar() - endfunction + endfunc - function! s:bar() + func! s:bar() let x = 123 return s:foo({-> x}) - endfunction + endfunc call assert_equal(123, s:bar()) -endfunction +endfunc -function! Test_lambda_do_not_share_local_variable() - function! s:define_funcs() +func Test_lambda_do_not_share_local_variable() + func! s:define_funcs() let l:One = {-> split(execute("let a = 'abc' | echo a"))[0]} let l:Two = {-> exists("a") ? a : "no"} return [l:One, l:Two] - endfunction + endfunc let l:F = s:define_funcs() call assert_equal('no', l:F[1]()) call assert_equal('abc', l:F[0]()) call assert_equal('no', l:F[1]()) -endfunction +endfunc -function! Test_lambda_closure_counter() - function! s:foo() +func Test_lambda_closure_counter() + func! s:foo() let x = 0 return {-> [execute("let x += 1"), x][-1]} - endfunction + endfunc let l:F = s:foo() call garbagecollect() @@ -130,52 +130,52 @@ function! Test_lambda_closure_counter() call assert_equal(2, l:F()) call assert_equal(3, l:F()) call assert_equal(4, l:F()) -endfunction +endfunc -function! Test_lambda_with_a_var() - function! s:foo() +func Test_lambda_with_a_var() + func! s:foo() let x = 2 return {... -> a:000 + [x]} - endfunction - function! s:bar() + endfunc + func! s:bar() return s:foo()(1) - endfunction + endfunc call assert_equal([1, 2], s:bar()) -endfunction +endfunc -function! Test_lambda_call_lambda_from_lambda() - function! s:foo(x) +func Test_lambda_call_lambda_from_lambda() + func! s:foo(x) let l:F1 = {-> {-> a:x}} return {-> l:F1()} - endfunction + endfunc let l:F = s:foo(1) call assert_equal(1, l:F()()) -endfunction +endfunc -function! Test_lambda_delfunc() - function! s:gen() +func Test_lambda_delfunc() + func! s:gen() let pl = l: let l:Foo = {-> get(pl, "Foo", get(pl, "Bar", {-> 0}))} let l:Bar = l:Foo delfunction l:Foo return l:Bar - endfunction + endfunc let l:F = s:gen() call assert_fails(':call l:F()', 'E933:') -endfunction +endfunc -function! Test_lambda_scope() - function! s:NewCounter() +func Test_lambda_scope() + func! s:NewCounter() let c = 0 return {-> [execute('let c += 1'), c][-1]} - endfunction + endfunc - function! s:NewCounter2() + func! s:NewCounter2() return {-> [execute('let c += 100'), c][-1]} - endfunction + endfunc let l:C = s:NewCounter() let l:D = s:NewCounter2() @@ -183,37 +183,37 @@ function! Test_lambda_scope() call assert_equal(1, l:C()) call assert_fails(':call l:D()', 'E121:') call assert_equal(2, l:C()) -endfunction +endfunc -function! Test_lambda_share_scope() - function! s:New() +func Test_lambda_share_scope() + func! s:New() let c = 0 let l:Inc0 = {-> [execute('let c += 1'), c][-1]} let l:Dec0 = {-> [execute('let c -= 1'), c][-1]} return [l:Inc0, l:Dec0] - endfunction + endfunc let [l:Inc, l:Dec] = s:New() call assert_equal(1, l:Inc()) call assert_equal(2, l:Inc()) call assert_equal(1, l:Dec()) -endfunction +endfunc -function! Test_lambda_circular_reference() - function! s:Foo() +func Test_lambda_circular_reference() + func! s:Foo() let d = {} let d.f = {-> d} return d.f - endfunction + endfunc call s:Foo() call garbagecollect() let i = 0 | while i < 10000 | call s:Foo() | let i+= 1 | endwhile call garbagecollect() -endfunction +endfunc -function! Test_lambda_combination() +func Test_lambda_combination() call assert_equal(2, {x -> {x -> x}}(1)(2)) call assert_equal(10, {y -> {x -> x(y)(10)}({y -> y})}({z -> z})) call assert_equal(5.0, {x -> {y -> x / y}}(10)(2.0)) @@ -226,17 +226,17 @@ function! Test_lambda_combination() let Z = {f -> {x -> f({y -> x(x)(y)})}({x -> f({y -> x(x)(y)})})} let Fact = {f -> {x -> x == 0 ? 1 : x * f(x - 1)}} call assert_equal(120, Z(Fact)(5)) -endfunction +endfunc -function! Test_closure_counter() - function! s:foo() +func Test_closure_counter() + func! s:foo() let x = 0 - function! s:bar() closure + func! s:bar() closure let x += 1 return x - endfunction + endfunc return function('s:bar') - endfunction + endfunc let l:F = s:foo() call garbagecollect() @@ -244,30 +244,30 @@ function! Test_closure_counter() call assert_equal(2, l:F()) call assert_equal(3, l:F()) call assert_equal(4, l:F()) -endfunction +endfunc -function! Test_closure_unlet() - function! s:foo() +func Test_closure_unlet() + func! s:foo() let x = 1 - function! s:bar() closure + func! s:bar() closure unlet x - endfunction + endfunc call s:bar() return l: - endfunction + endfunc call assert_false(has_key(s:foo(), 'x')) call garbagecollect() -endfunction +endfunc -function! LambdaFoo() +func LambdaFoo() let x = 0 - function! LambdaBar() closure + func! LambdaBar() closure let x += 1 return x - endfunction + endfunc return function('LambdaBar') -endfunction +endfunc func Test_closure_refcount() let g:Count = LambdaFoo() diff --git a/src/nvim/testdir/test_listdict.vim b/src/nvim/testdir/test_listdict.vim index f6c404d390..10c6164c7c 100644 --- a/src/nvim/testdir/test_listdict.vim +++ b/src/nvim/testdir/test_listdict.vim @@ -516,22 +516,22 @@ func Test_dict_lock_operator() endfunc " No remove() of write-protected scope-level variable -func! Tfunc(this_is_a_long_parameter_name) +func Tfunc1(this_is_a_long_parameter_name) call assert_fails("call remove(a:, 'this_is_a_long_parameter_name')", 'E742') -endfun +endfunc func Test_dict_scope_var_remove() - call Tfunc('testval') + call Tfunc1('testval') endfunc " No extend() of write-protected scope-level variable func Test_dict_scope_var_extend() call assert_fails("call extend(a:, {'this_is_a_long_parameter_name': 1234})", 'E742') endfunc -func! Tfunc(this_is_a_long_parameter_name) +func Tfunc2(this_is_a_long_parameter_name) call assert_fails("call extend(a:, {'this_is_a_long_parameter_name': 1234})", 'E742') endfunc func Test_dict_scope_var_extend_overwrite() - call Tfunc('testval') + call Tfunc2('testval') endfunc " No :unlet of variable in locked scope diff --git a/src/nvim/testdir/test_marks.vim b/src/nvim/testdir/test_marks.vim index 6b9904ec0a..00ee8f6d6a 100644 --- a/src/nvim/testdir/test_marks.vim +++ b/src/nvim/testdir/test_marks.vim @@ -1,6 +1,6 @@ " Test that a deleted mark is restored after delete-undo-redo-undo. -function! Test_Restore_DelMark() +func Test_Restore_DelMark() enew! call append(0, [" textline A", " textline B", " textline C"]) normal! 2gg @@ -11,10 +11,10 @@ function! Test_Restore_DelMark() call assert_equal(2, pos[1]) call assert_equal(1, pos[2]) enew! -endfunction +endfunc " Test that CTRL-A and CTRL-X updates last changed mark '[, ']. -function! Test_Incr_Marks() +func Test_Incr_Marks() enew! call append(0, ["123 123 123", "123 123 123", "123 123 123"]) normal! gg @@ -23,7 +23,7 @@ function! Test_Incr_Marks() call assert_equal("123 XXXXXXX", getline(2)) call assert_equal("XXX 123 123", getline(3)) enew! -endfunction +endfunc func Test_previous_jump_mark() new diff --git a/src/nvim/testdir/test_matchadd_conceal.vim b/src/nvim/testdir/test_matchadd_conceal.vim index 2cbaf5cb76..29a2c30b0d 100644 --- a/src/nvim/testdir/test_matchadd_conceal.vim +++ b/src/nvim/testdir/test_matchadd_conceal.vim @@ -7,7 +7,7 @@ source shared.vim source term_util.vim source view_util.vim -function! Test_simple_matchadd() +func Test_simple_matchadd() new 1put='# This is a Test' @@ -333,7 +333,7 @@ func Test_matchadd_and_syn_conceal() call assert_notequal(screenattr(1, 10) , screenattr(1, 11)) call assert_notequal(screenattr(1, 11) , screenattr(1, 12)) call assert_equal(screenattr(1, 11) , screenattr(1, 32)) -endfunction +endfunc func Test_cursor_column_in_concealed_line_after_window_scroll() CheckRunVimInTerminal diff --git a/src/nvim/testdir/test_matchadd_conceal_utf8.vim b/src/nvim/testdir/test_matchadd_conceal_utf8.vim index 7bfac13ad8..1d0c740734 100644 --- a/src/nvim/testdir/test_matchadd_conceal_utf8.vim +++ b/src/nvim/testdir/test_matchadd_conceal_utf8.vim @@ -3,19 +3,19 @@ if !has('conceal') finish endif -function! s:screenline(lnum) abort +func s:screenline(lnum) abort let line = [] for c in range(1, winwidth(0)) call add(line, nr2char(a:lnum->screenchar(c))) endfor return s:trim(join(line, '')) -endfunction +endfunc -function! s:trim(str) abort +func s:trim(str) abort return matchstr(a:str,'^\s*\zs.\{-}\ze\s*$') -endfunction +endfunc -function! Test_match_using_multibyte_conceal_char() +func Test_match_using_multibyte_conceal_char() new setlocal concealcursor=n conceallevel=1 diff --git a/src/nvim/testdir/test_matchfuzzy.vim b/src/nvim/testdir/test_matchfuzzy.vim new file mode 100644 index 0000000000..abcc9b40c1 --- /dev/null +++ b/src/nvim/testdir/test_matchfuzzy.vim @@ -0,0 +1,248 @@ +" Tests for fuzzy matching + +source shared.vim +source check.vim + +" Test for matchfuzzy() +func Test_matchfuzzy() + call assert_fails('call matchfuzzy(10, "abc")', 'E686:') + " Needs v8.2.1183; match the final error that's thrown for now + " call assert_fails('call matchfuzzy(["abc"], [])', 'E730:') + call assert_fails('call matchfuzzy(["abc"], [])', 'E475:') + call assert_fails("let x = matchfuzzy(v:_null_list, 'foo')", 'E686:') + call assert_fails('call matchfuzzy(["abc"], v:_null_string)', 'E475:') + call assert_equal([], matchfuzzy([], 'abc')) + call assert_equal([], matchfuzzy(['abc'], '')) + call assert_equal(['abc'], matchfuzzy(['abc', 10], 'ac')) + call assert_equal([], matchfuzzy([10, 20], 'ac')) + call assert_equal(['abc'], matchfuzzy(['abc'], 'abc')) + call assert_equal(['crayon', 'camera'], matchfuzzy(['camera', 'crayon'], 'cra')) + call assert_equal(['aabbaa', 'aaabbbaaa', 'aaaabbbbaaaa', 'aba'], matchfuzzy(['aba', 'aabbaa', 'aaabbbaaa', 'aaaabbbbaaaa'], 'aa')) + call assert_equal(['one'], matchfuzzy(['one', 'two'], 'one')) + call assert_equal(['oneTwo', 'onetwo'], matchfuzzy(['onetwo', 'oneTwo'], 'oneTwo')) + call assert_equal(['onetwo', 'one_two'], matchfuzzy(['onetwo', 'one_two'], 'oneTwo')) + call assert_equal(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], matchfuzzy(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'], 'aa')) + call assert_equal(256, matchfuzzy([repeat('a', 256)], repeat('a', 256))[0]->len()) + call assert_equal([], matchfuzzy([repeat('a', 300)], repeat('a', 257))) + " matches with same score should not be reordered + let l = ['abc1', 'abc2', 'abc3'] + call assert_equal(l, l->matchfuzzy('abc')) + + " Tests for match preferences + " preference for camel case match + call assert_equal(['oneTwo', 'onetwo'], ['onetwo', 'oneTwo']->matchfuzzy('onetwo')) + " preference for match after a separator (_ or space) + call assert_equal(['onetwo', 'one_two', 'one two'], ['onetwo', 'one_two', 'one two']->matchfuzzy('onetwo')) + " preference for leading letter match + call assert_equal(['onetwo', 'xonetwo'], ['xonetwo', 'onetwo']->matchfuzzy('onetwo')) + " preference for sequential match + call assert_equal(['onetwo', 'oanbectdweo'], ['oanbectdweo', 'onetwo']->matchfuzzy('onetwo')) + " non-matching leading letter(s) penalty + call assert_equal(['xonetwo', 'xxonetwo'], ['xxonetwo', 'xonetwo']->matchfuzzy('onetwo')) + " total non-matching letter(s) penalty + call assert_equal(['one', 'onex', 'onexx'], ['onexx', 'one', 'onex']->matchfuzzy('one')) + " prefer complete matches over separator matches + call assert_equal(['.vim/vimrc', '.vim/vimrc_colors', '.vim/v_i_m_r_c'], ['.vim/vimrc', '.vim/vimrc_colors', '.vim/v_i_m_r_c']->matchfuzzy('vimrc')) + " gap penalty + call assert_equal(['xxayybxxxx', 'xxayyybxxx', 'xxayyyybxx'], ['xxayyyybxx', 'xxayyybxxx', 'xxayybxxxx']->matchfuzzy('ab')) + " path separator vs word separator + call assert_equal(['color/setup.vim', 'color\\setup.vim', 'color setup.vim', 'color_setup.vim', 'colorsetup.vim'], matchfuzzy(['colorsetup.vim', 'color setup.vim', 'color/setup.vim', 'color_setup.vim', 'color\\setup.vim'], 'setup.vim')) + + " match multiple words (separated by space) + call assert_equal(['foo bar baz'], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzy('baz foo')) + call assert_equal([], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzy('one two')) + call assert_equal([], ['foo bar']->matchfuzzy(" \t ")) + + " test for matching a sequence of words + call assert_equal(['bar foo'], ['foo bar', 'bar foo', 'foobar', 'barfoo']->matchfuzzy('bar foo', {'matchseq' : 1})) + call assert_equal([#{text: 'two one'}], [#{text: 'one two'}, #{text: 'two one'}]->matchfuzzy('two one', #{key: 'text', matchseq: v:true})) + + %bw! + eval ['somebuf', 'anotherone', 'needle', 'yetanotherone']->map({_, v -> bufadd(v) + bufload(v)}) + let l = getbufinfo()->map({_, v -> v.name})->matchfuzzy('ndl') + call assert_equal(1, len(l)) + call assert_match('needle', l[0]) + + " Test for fuzzy matching dicts + let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}] + call assert_equal([{'id' : 6, 'val' : 'camera'}], matchfuzzy(l, 'cam', {'text_cb' : {v -> v.val}})) + call assert_equal([{'id' : 6, 'val' : 'camera'}], matchfuzzy(l, 'cam', {'key' : 'val'})) + call assert_equal([], matchfuzzy(l, 'day', {'text_cb' : {v -> v.val}})) + call assert_equal([], matchfuzzy(l, 'day', {'key' : 'val'})) + call assert_fails("let x = matchfuzzy(l, 'cam', 'random')", 'E715:') + call assert_equal([], matchfuzzy(l, 'day', {'text_cb' : {v -> []}})) + call assert_equal([], matchfuzzy(l, 'day', {'text_cb' : {v -> 1}})) + call assert_fails("let x = matchfuzzy(l, 'day', {'text_cb' : {a, b -> 1}})", 'E119:') + call assert_equal([], matchfuzzy(l, 'cam')) + " Nvim's callback implementation is different, so E6000 is expected instead, + " but we need v8.2.1183 to assert it + " call assert_fails("let x = matchfuzzy(l, 'cam', {'text_cb' : []})", 'E921:') + " call assert_fails("let x = matchfuzzy(l, 'cam', {'text_cb' : []})", 'E6000:') + call assert_fails("let x = matchfuzzy(l, 'cam', {'text_cb' : []})", 'E475:') + " call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : []})", 'E730:') + call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : []})", 'E475:') + call assert_fails("let x = matchfuzzy(l, 'cam', v:_null_dict)", 'E715:') + call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : v:_null_string})", 'E475:') + " Nvim doesn't have null functions + " call assert_fails("let x = matchfuzzy(l, 'foo', {'text_cb' : test_null_function()})", 'E475:') + " matches with same score should not be reordered + let l = [#{text: 'abc', id: 1}, #{text: 'abc', id: 2}, #{text: 'abc', id: 3}] + call assert_equal(l, l->matchfuzzy('abc', #{key: 'text'})) + + let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}] + call assert_fails("let x = matchfuzzy(l, 'foo', {'key' : 'name'})", 'E730:') + + " Test in latin1 encoding + let save_enc = &encoding + " Nvim supports utf-8 encoding only + " set encoding=latin1 + call assert_equal(['abc'], matchfuzzy(['abc'], 'abc')) + let &encoding = save_enc +endfunc + +" Test for the matchfuzzypos() function +func Test_matchfuzzypos() + call assert_equal([['curl', 'world'], [[2,3], [2,3]], [128, 127]], matchfuzzypos(['world', 'curl'], 'rl')) + call assert_equal([['curl', 'world'], [[2,3], [2,3]], [128, 127]], matchfuzzypos(['world', 'one', 'curl'], 'rl')) + call assert_equal([['hello', 'hello world hello world'], + \ [[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], [275, 257]], + \ matchfuzzypos(['hello world hello world', 'hello', 'world'], 'hello')) + call assert_equal([['aaaaaaa'], [[0, 1, 2]], [191]], matchfuzzypos(['aaaaaaa'], 'aaa')) + call assert_equal([['a b'], [[0, 3]], [219]], matchfuzzypos(['a b'], 'a b')) + call assert_equal([['a b'], [[0, 3]], [219]], matchfuzzypos(['a b'], 'a b')) + call assert_equal([['a b'], [[0]], [112]], matchfuzzypos(['a b'], ' a ')) + call assert_equal([[], [], []], matchfuzzypos(['a b'], ' ')) + call assert_equal([[], [], []], matchfuzzypos(['world', 'curl'], 'ab')) + let x = matchfuzzypos([repeat('a', 256)], repeat('a', 256)) + call assert_equal(range(256), x[1][0]) + call assert_equal([[], [], []], matchfuzzypos([repeat('a', 300)], repeat('a', 257))) + call assert_equal([[], [], []], matchfuzzypos([], 'abc')) + + " match in a long string + call assert_equal([[repeat('x', 300) .. 'abc'], [[300, 301, 302]], [-135]], + \ matchfuzzypos([repeat('x', 300) .. 'abc'], 'abc')) + + " preference for camel case match + call assert_equal([['xabcxxaBc'], [[6, 7, 8]], [189]], matchfuzzypos(['xabcxxaBc'], 'abc')) + " preference for match after a separator (_ or space) + call assert_equal([['xabx_ab'], [[5, 6]], [145]], matchfuzzypos(['xabx_ab'], 'ab')) + " preference for leading letter match + call assert_equal([['abcxabc'], [[0, 1]], [150]], matchfuzzypos(['abcxabc'], 'ab')) + " preference for sequential match + call assert_equal([['aobncedone'], [[7, 8, 9]], [158]], matchfuzzypos(['aobncedone'], 'one')) + " best recursive match + call assert_equal([['xoone'], [[2, 3, 4]], [168]], matchfuzzypos(['xoone'], 'one')) + + " match multiple words (separated by space) + call assert_equal([['foo bar baz'], [[8, 9, 10, 0, 1, 2]], [369]], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('baz foo')) + call assert_equal([[], [], []], ['foo bar baz', 'foo', 'foo bar', 'baz bar']->matchfuzzypos('one two')) + call assert_equal([[], [], []], ['foo bar']->matchfuzzypos(" \t ")) + call assert_equal([['grace'], [[1, 2, 3, 4, 2, 3, 4, 0, 1, 2, 3, 4]], [657]], ['grace']->matchfuzzypos('race ace grace')) + + let l = [{'id' : 5, 'val' : 'crayon'}, {'id' : 6, 'val' : 'camera'}] + call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [192]], + \ matchfuzzypos(l, 'cam', {'text_cb' : {v -> v.val}})) + call assert_equal([[{'id' : 6, 'val' : 'camera'}], [[0, 1, 2]], [192]], + \ matchfuzzypos(l, 'cam', {'key' : 'val'})) + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> v.val}})) + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'key' : 'val'})) + call assert_fails("let x = matchfuzzypos(l, 'cam', 'random')", 'E715:') + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> []}})) + call assert_equal([[], [], []], matchfuzzypos(l, 'day', {'text_cb' : {v -> 1}})) + call assert_fails("let x = matchfuzzypos(l, 'day', {'text_cb' : {a, b -> 1}})", 'E119:') + call assert_equal([[], [], []], matchfuzzypos(l, 'cam')) + " Nvim's callback implementation is different, so E6000 is expected instead, + " but we need v8.2.1183 to assert it + " call assert_fails("let x = matchfuzzypos(l, 'cam', {'text_cb' : []})", 'E921:') + " call assert_fails("let x = matchfuzzypos(l, 'cam', {'text_cb' : []})", 'E6000:') + call assert_fails("let x = matchfuzzypos(l, 'cam', {'text_cb' : []})", 'E475:') + " call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : []})", 'E730:') + call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : []})", 'E475:') + call assert_fails("let x = matchfuzzypos(l, 'cam', v:_null_dict)", 'E715:') + call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : v:_null_string})", 'E475:') + " Nvim doesn't have null functions + " call assert_fails("let x = matchfuzzypos(l, 'foo', {'text_cb' : test_null_function()})", 'E475:') + + let l = [{'id' : 5, 'name' : 'foo'}, {'id' : 6, 'name' : []}, {'id' : 7}] + call assert_fails("let x = matchfuzzypos(l, 'foo', {'key' : 'name'})", 'E730:') +endfunc + +" Test for matchfuzzy() with multibyte characters +func Test_matchfuzzy_mbyte() + CheckFeature multi_lang + call assert_equal(['ンヹㄇヺヴ'], matchfuzzy(['ンヹㄇヺヴ'], 'ヹヺ')) + " reverse the order of characters + call assert_equal([], matchfuzzy(['ンヹㄇヺヴ'], 'ヺヹ')) + call assert_equal(['αβΩxxx', 'xαxβxΩx'], + \ matchfuzzy(['αβΩxxx', 'xαxβxΩx'], 'αβΩ')) + call assert_equal(['ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ', 'πbπ'], + \ matchfuzzy(['πbπ', 'ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ'], 'ππ')) + + " match multiple words (separated by space) + call assert_equal(['세 마리의 작은 돼지'], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzy('돼지 마리의')) + call assert_equal([], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzy('파란 하늘')) + + " preference for camel case match + call assert_equal(['oneĄwo', 'oneąwo'], + \ ['oneąwo', 'oneĄwo']->matchfuzzy('oneąwo')) + " preference for complete match then match after separator (_ or space) + call assert_equal(['ⅠⅡabㄟㄠ'] + sort(['ⅠⅡa_bㄟㄠ', 'ⅠⅡa bㄟㄠ']), + \ ['ⅠⅡabㄟㄠ', 'ⅠⅡa bㄟㄠ', 'ⅠⅡa_bㄟㄠ']->matchfuzzy('ⅠⅡabㄟㄠ')) + " preference for match after a separator (_ or space) + call assert_equal(['ㄓㄔabㄟㄠ', 'ㄓㄔa_bㄟㄠ', 'ㄓㄔa bㄟㄠ'], + \ ['ㄓㄔa_bㄟㄠ', 'ㄓㄔa bㄟㄠ', 'ㄓㄔabㄟㄠ']->matchfuzzy('ㄓㄔabㄟㄠ')) + " preference for leading letter match + call assert_equal(['ŗŝţũŵż', 'xŗŝţũŵż'], + \ ['xŗŝţũŵż', 'ŗŝţũŵż']->matchfuzzy('ŗŝţũŵż')) + " preference for sequential match + call assert_equal(['ㄞㄡㄤfffifl', 'ㄞaㄡbㄤcffdfiefl'], + \ ['ㄞaㄡbㄤcffdfiefl', 'ㄞㄡㄤfffifl']->matchfuzzy('ㄞㄡㄤfffifl')) + " non-matching leading letter(s) penalty + call assert_equal(['xㄞㄡㄤfffifl', 'xxㄞㄡㄤfffifl'], + \ ['xxㄞㄡㄤfffifl', 'xㄞㄡㄤfffifl']->matchfuzzy('ㄞㄡㄤfffifl')) + " total non-matching letter(s) penalty + call assert_equal(['ŗŝţ', 'ŗŝţx', 'ŗŝţxx'], + \ ['ŗŝţxx', 'ŗŝţ', 'ŗŝţx']->matchfuzzy('ŗŝţ')) +endfunc + +" Test for matchfuzzypos() with multibyte characters +func Test_matchfuzzypos_mbyte() + CheckFeature multi_lang + call assert_equal([['こんにちは世界'], [[0, 1, 2, 3, 4]], [273]], + \ matchfuzzypos(['こんにちは世界'], 'こんにちは')) + call assert_equal([['ンヹㄇヺヴ'], [[1, 3]], [88]], matchfuzzypos(['ンヹㄇヺヴ'], 'ヹヺ')) + " reverse the order of characters + call assert_equal([[], [], []], matchfuzzypos(['ンヹㄇヺヴ'], 'ヺヹ')) + call assert_equal([['αβΩxxx', 'xαxβxΩx'], [[0, 1, 2], [1, 3, 5]], [222, 113]], + \ matchfuzzypos(['αβΩxxx', 'xαxβxΩx'], 'αβΩ')) + call assert_equal([['ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ', 'πbπ'], + \ [[0, 1], [0, 1], [0, 1], [0, 2]], [151, 148, 145, 110]], + \ matchfuzzypos(['πbπ', 'ππbbππ', 'πππbbbπππ', 'ππππbbbbππππ'], 'ππ')) + call assert_equal([['ααααααα'], [[0, 1, 2]], [191]], + \ matchfuzzypos(['ααααααα'], 'ααα')) + + call assert_equal([[], [], []], matchfuzzypos(['ンヹㄇ', 'ŗŝţ'], 'fffifl')) + let x = matchfuzzypos([repeat('Ψ', 256)], repeat('Ψ', 256)) + call assert_equal(range(256), x[1][0]) + call assert_equal([[], [], []], matchfuzzypos([repeat('✓', 300)], repeat('✓', 257))) + + " match multiple words (separated by space) + call assert_equal([['세 마리의 작은 돼지'], [[9, 10, 2, 3, 4]], [328]], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('돼지 마리의')) + call assert_equal([[], [], []], ['세 마리의 작은 돼지', '마리의', '마리의 작은', '작은 돼지']->matchfuzzypos('파란 하늘')) + + " match in a long string + call assert_equal([[repeat('ぶ', 300) .. 'ẼẼẼ'], [[300, 301, 302]], [-135]], + \ matchfuzzypos([repeat('ぶ', 300) .. 'ẼẼẼ'], 'ẼẼẼ')) + " preference for camel case match + call assert_equal([['xѳѵҁxxѳѴҁ'], [[6, 7, 8]], [189]], matchfuzzypos(['xѳѵҁxxѳѴҁ'], 'ѳѵҁ')) + " preference for match after a separator (_ or space) + call assert_equal([['xちだx_ちだ'], [[5, 6]], [145]], matchfuzzypos(['xちだx_ちだ'], 'ちだ')) + " preference for leading letter match + call assert_equal([['ѳѵҁxѳѵҁ'], [[0, 1]], [150]], matchfuzzypos(['ѳѵҁxѳѵҁ'], 'ѳѵ')) + " preference for sequential match + call assert_equal([['aンbヹcㄇdンヹㄇ'], [[7, 8, 9]], [158]], matchfuzzypos(['aンbヹcㄇdンヹㄇ'], 'ンヹㄇ')) + " best recursive match + call assert_equal([['xффйд'], [[2, 3, 4]], [168]], matchfuzzypos(['xффйд'], 'фйд')) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_messages.vim b/src/nvim/testdir/test_messages.vim index e0286548d9..9c84d77dd2 100644 --- a/src/nvim/testdir/test_messages.vim +++ b/src/nvim/testdir/test_messages.vim @@ -40,7 +40,7 @@ endfunc " indicator (e.g., "-- INSERT --") when ":stopinsert" is invoked. Message " output could then be disturbed when 'cmdheight' was greater than one. " This test ensures that the bugfix for this issue remains in place. -function! Test_stopinsert_does_not_break_message_output() +func Test_stopinsert_does_not_break_message_output() set cmdheight=2 redraw! @@ -55,7 +55,7 @@ function! Test_stopinsert_does_not_break_message_output() redraw! set cmdheight& -endfunction +endfunc func Test_message_completion() call feedkeys(":message \<C-A>\<C-B>\"\<CR>", 'tx') diff --git a/src/nvim/testdir/test_options.vim b/src/nvim/testdir/test_options.vim index f7bfa48943..a5adb5ff16 100644 --- a/src/nvim/testdir/test_options.vim +++ b/src/nvim/testdir/test_options.vim @@ -22,16 +22,16 @@ func Test_whichwrap() call assert_equal('h', &whichwrap) set whichwrap& -endfunction +endfunc -function! Test_isfname() +func Test_isfname() " This used to cause Vim to access uninitialized memory. set isfname= call assert_equal("~X", expand("~X")) set isfname& -endfunction +endfunc -function Test_wildchar() +func Test_wildchar() " Empty 'wildchar' used to access invalid memory. call assert_fails('set wildchar=', 'E521:') call assert_fails('set wildchar=abc', 'E521:') @@ -42,7 +42,7 @@ function Test_wildchar() let a=execute('set wildchar?') call assert_equal("\n wildchar=<Esc>", a) set wildchar& -endfunction +endfunc func Test_wildoptions() set wildoptions= @@ -90,7 +90,7 @@ func Test_options_command() close endfunc -function! Test_path_keep_commas() +func Test_path_keep_commas() " Test that changing 'path' keeps two commas. set path=foo,,bar set path-=bar @@ -98,7 +98,7 @@ function! Test_path_keep_commas() call assert_equal('foo,,bar', &path) set path& -endfunction +endfunc func Test_filetype_valid() set ft=valid_name diff --git a/src/nvim/testdir/test_quickfix.vim b/src/nvim/testdir/test_quickfix.vim index f137ed5346..00679e1958 100644 --- a/src/nvim/testdir/test_quickfix.vim +++ b/src/nvim/testdir/test_quickfix.vim @@ -32,7 +32,7 @@ func s:setup_commands(cchar) command! -count -nargs=* -bang Xnfile <mods><count>cnfile<bang> <args> command! -nargs=* -bang Xpfile <mods>cpfile<bang> <args> command! -nargs=* Xexpr <mods>cexpr <args> - command! -count -nargs=* Xvimgrep <mods> <count>vimgrep <args> + command! -count=999 -nargs=* Xvimgrep <mods> <count>vimgrep <args> command! -nargs=* Xvimgrepadd <mods> vimgrepadd <args> command! -nargs=* Xgrep <mods> grep <args> command! -nargs=* Xgrepadd <mods> grepadd <args> @@ -69,7 +69,7 @@ func s:setup_commands(cchar) command! -count -nargs=* -bang Xnfile <mods><count>lnfile<bang> <args> command! -nargs=* -bang Xpfile <mods>lpfile<bang> <args> command! -nargs=* Xexpr <mods>lexpr <args> - command! -count -nargs=* Xvimgrep <mods> <count>lvimgrep <args> + command! -count=999 -nargs=* Xvimgrep <mods> <count>lvimgrep <args> command! -nargs=* Xvimgrepadd <mods> lvimgrepadd <args> command! -nargs=* Xgrep <mods> lgrep <args> command! -nargs=* Xgrepadd <mods> lgrepadd <args> @@ -5028,6 +5028,52 @@ func Test_qfbuf_update() call Xqfbuf_update('l') endfunc +" Test for the :vimgrep 'f' flag (fuzzy match) +func Xvimgrep_fuzzy_match(cchar) + call s:setup_commands(a:cchar) + + Xvimgrep /three one/f Xfile* + let l = g:Xgetlist() + call assert_equal(2, len(l)) + call assert_equal(['Xfile1', 1, 9, 'one two three'], + \ [bufname(l[0].bufnr), l[0].lnum, l[0].col, l[0].text]) + call assert_equal(['Xfile2', 2, 1, 'three one two'], + \ [bufname(l[1].bufnr), l[1].lnum, l[1].col, l[1].text]) + + Xvimgrep /the/f Xfile* + let l = g:Xgetlist() + call assert_equal(3, len(l)) + call assert_equal(['Xfile1', 1, 9, 'one two three'], + \ [bufname(l[0].bufnr), l[0].lnum, l[0].col, l[0].text]) + call assert_equal(['Xfile2', 2, 1, 'three one two'], + \ [bufname(l[1].bufnr), l[1].lnum, l[1].col, l[1].text]) + call assert_equal(['Xfile2', 4, 4, 'aaathreeaaa'], + \ [bufname(l[2].bufnr), l[2].lnum, l[2].col, l[2].text]) + + Xvimgrep /aaa/fg Xfile* + let l = g:Xgetlist() + call assert_equal(4, len(l)) + call assert_equal(['Xfile1', 2, 1, 'aaaaaa'], + \ [bufname(l[0].bufnr), l[0].lnum, l[0].col, l[0].text]) + call assert_equal(['Xfile1', 2, 4, 'aaaaaa'], + \ [bufname(l[1].bufnr), l[1].lnum, l[1].col, l[1].text]) + call assert_equal(['Xfile2', 4, 1, 'aaathreeaaa'], + \ [bufname(l[2].bufnr), l[2].lnum, l[2].col, l[2].text]) + call assert_equal(['Xfile2', 4, 9, 'aaathreeaaa'], + \ [bufname(l[3].bufnr), l[3].lnum, l[3].col, l[3].text]) + + call assert_fails('Xvimgrep /xyz/fg Xfile*', 'E480:') +endfunc + +func Test_vimgrep_fuzzy_match() + call writefile(['one two three', 'aaaaaa'], 'Xfile1') + call writefile(['one', 'three one two', 'two', 'aaathreeaaa'], 'Xfile2') + call Xvimgrep_fuzzy_match('c') + call Xvimgrep_fuzzy_match('l') + call delete('Xfile1') + call delete('Xfile2') +endfunc + " Test for getting a specific item from a quickfix list func Xtest_getqflist_by_idx(cchar) call s:setup_commands(a:cchar) diff --git a/src/nvim/testdir/test_substitute.vim b/src/nvim/testdir/test_substitute.vim index 20b760ac15..ecd980472a 100644 --- a/src/nvim/testdir/test_substitute.vim +++ b/src/nvim/testdir/test_substitute.vim @@ -105,7 +105,7 @@ func Test_substitute_variants() call assert_equal(var.exp, getline('.'), msg) endfor endfor -endfunction +endfunc " Test the l, p, # flags. func Test_substitute_flags_lp() diff --git a/src/nvim/testdir/test_system.vim b/src/nvim/testdir/test_system.vim index 1858b48807..5b8079d7b6 100644 --- a/src/nvim/testdir/test_system.vim +++ b/src/nvim/testdir/test_system.vim @@ -46,9 +46,9 @@ func Test_System() bwipe! call assert_fails('call system("wc -l", 99999)', 'E86:') -endfunction +endfunc -function! Test_system_exmode() +func Test_system_exmode() if has('unix') " echo $? only works on Unix let cmd = ' -es --headless -u NONE -c "source Xscript" +q; echo "result=$?"' " Need to put this in a script, "catch" isn't found after an unknown diff --git a/src/nvim/testdir/test_utf8_comparisons.vim b/src/nvim/testdir/test_utf8_comparisons.vim index 62947c6e6e..f3c86b44fb 100644 --- a/src/nvim/testdir/test_utf8_comparisons.vim +++ b/src/nvim/testdir/test_utf8_comparisons.vim @@ -1,12 +1,12 @@ " Tests for case-insensitive UTF-8 comparisons (utf_strnicmp() in mbyte.c) " Also test "g~ap". -function! Ch(a, op, b, expected) +func Ch(a, op, b, expected) call assert_equal(eval(printf('"%s" %s "%s"', a:a, a:op, a:b)), a:expected, \ printf('"%s" %s "%s" should return %d', a:a, a:op, a:b, a:expected)) -endfunction +endfunc -function! Chk(a, b, result) +func Chk(a, b, result) if a:result == 0 call Ch(a:a, '==?', a:b, 1) call Ch(a:a, '!=?', a:b, 0) diff --git a/src/nvim/testdir/test_vartabs.vim b/src/nvim/testdir/test_vartabs.vim index d05008d2dd..017bb6675d 100644 --- a/src/nvim/testdir/test_vartabs.vim +++ b/src/nvim/testdir/test_vartabs.vim @@ -145,7 +145,7 @@ func Test_retab_invalid_arg() bwipe! endfunc -func! Test_vartabs_breakindent() +func Test_vartabs_breakindent() if !exists("+breakindent") return endif diff --git a/src/nvim/testdir/test_vimscript.vim b/src/nvim/testdir/test_vimscript.vim index c59cab5f36..75a965f16d 100644 --- a/src/nvim/testdir/test_vimscript.vim +++ b/src/nvim/testdir/test_vimscript.vim @@ -25,7 +25,7 @@ com! -nargs=1 Xout call Xout(<args>) " in the variable argument list. This function is useful if similar tests are " to be made for a ":return" from a function call or a ":finish" in a script " file. -function! MakeScript(funcname, ...) +func MakeScript(funcname, ...) let script = tempname() execute "redir! >" . script execute "function" a:funcname @@ -1156,6 +1156,82 @@ func Test_type() call assert_equal(v:t_list, type(v:_null_list)) call assert_equal(v:t_dict, type(v:_null_dict)) call assert_equal(v:t_blob, type(v:_null_blob)) + + call assert_equal(0, 0 + v:false) + call assert_equal(1, 0 + v:true) + " call assert_equal(0, 0 + v:none) + call assert_equal(0, 0 + v:null) + + call assert_equal('false', '' . v:false) + call assert_equal('true', '' . v:true) + " call assert_equal('none', '' . v:none) + call assert_equal('null', '' . v:null) + + call assert_true(v:false == 0) + call assert_false(v:false != 0) + call assert_true(v:true == 1) + call assert_false(v:true != 1) + call assert_false(v:true == v:false) + call assert_true(v:true != v:false) + + call assert_true(v:null == 0) + call assert_false(v:null != 0) + " call assert_true(v:none == 0) + " call assert_false(v:none != 0) + + call assert_true(v:false is v:false) + call assert_true(v:true is v:true) + " call assert_true(v:none is v:none) + call assert_true(v:null is v:null) + + call assert_false(v:false isnot v:false) + call assert_false(v:true isnot v:true) + " call assert_false(v:none isnot v:none) + call assert_false(v:null isnot v:null) + + call assert_false(v:false is 0) + call assert_false(v:true is 1) + call assert_false(v:true is v:false) + " call assert_false(v:none is 0) + call assert_false(v:null is 0) + " call assert_false(v:null is v:none) + + call assert_true(v:false isnot 0) + call assert_true(v:true isnot 1) + call assert_true(v:true isnot v:false) + " call assert_true(v:none isnot 0) + call assert_true(v:null isnot 0) + " call assert_true(v:null isnot v:none) + + call assert_equal(v:false, eval(string(v:false))) + call assert_equal(v:true, eval(string(v:true))) + " call assert_equal(v:none, eval(string(v:none))) + call assert_equal(v:null, eval(string(v:null))) + + call assert_equal(v:false, copy(v:false)) + call assert_equal(v:true, copy(v:true)) + " call assert_equal(v:none, copy(v:none)) + call assert_equal(v:null, copy(v:null)) + + call assert_equal([v:false], deepcopy([v:false])) + call assert_equal([v:true], deepcopy([v:true])) + " call assert_equal([v:none], deepcopy([v:none])) + call assert_equal([v:null], deepcopy([v:null])) + + call assert_true(empty(v:false)) + call assert_false(empty(v:true)) + call assert_true(empty(v:null)) + " call assert_true(empty(v:none)) + + func ChangeYourMind() + try + return v:true + finally + return 'something else' + endtry + endfunc + + call ChangeYourMind() endfunc "------------------------------------------------------------------------------- diff --git a/src/nvim/testdir/test_window_cmd.vim b/src/nvim/testdir/test_window_cmd.vim index db5c0b2a11..ef6dec580f 100644 --- a/src/nvim/testdir/test_window_cmd.vim +++ b/src/nvim/testdir/test_window_cmd.vim @@ -575,7 +575,7 @@ func Test_winrestcmd() only endfunc -function! Fun_RenewFile() +func Fun_RenewFile() " Need to wait a bit for the timestamp to be older. let old_ftime = getftime("tmp.txt") while getftime("tmp.txt") == old_ftime @@ -585,7 +585,7 @@ function! Fun_RenewFile() sp wincmd p edit! tmp.txt -endfunction +endfunc func Test_window_prevwin() " Can we make this work on MS-Windows? diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 6e48df3734..6b889cf97c 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -227,7 +227,7 @@ static void forward_modified_utf8(TermInput *input, TermKeyKey *key) && !(key->modifiers & TERMKEY_KEYMOD_SHIFT) && ASCII_ISUPPER(key->code.codepoint)) { assert(len <= 62); - // Make remove for the S- + // Make room for the S- memmove(buf + 3, buf + 1, len - 1); buf[1] = 'S'; buf[2] = '-'; diff --git a/test/functional/legacy/eval_spec.lua b/test/functional/legacy/eval_spec.lua index b5de5cd232..05d853622e 100644 --- a/test/functional/legacy/eval_spec.lua +++ b/test/functional/legacy/eval_spec.lua @@ -666,7 +666,7 @@ describe('eval', function() source([[ " Vim script used in test_eval.in. Needed for script-local function. - func! s:Testje() + func s:Testje() return "foo" endfunc diff --git a/test/functional/ui/input_spec.lua b/test/functional/ui/input_spec.lua index 78f4c6ddd3..7000505909 100644 --- a/test/functional/ui/input_spec.lua +++ b/test/functional/ui/input_spec.lua @@ -140,6 +140,25 @@ describe('input utf sequences that contain CSI (0x9B)', function() end) end) +describe('input split utf sequences', function() + it('ok', function() + local str = '►' + feed('i' .. str:sub(1, 1)) + helpers.sleep(10) + feed(str:sub(2, 3)) + expect('►') + end) + + it('can be mapped', function() + command('inoremap ► E296BA') + local str = '►' + feed('i' .. str:sub(1, 1)) + helpers.sleep(10) + feed(str:sub(2, 3)) + expect('E296BA') + end) +end) + describe('input non-printable chars', function() after_each(function() os.remove('Xtest-overwrite') diff --git a/test/functional/vimscript/timer_spec.lua b/test/functional/vimscript/timer_spec.lua index e45b64422f..25e46062b7 100644 --- a/test/functional/vimscript/timer_spec.lua +++ b/test/functional/vimscript/timer_spec.lua @@ -272,4 +272,12 @@ describe('timers', function() ]] eq("Vim(call):E48: Not allowed in sandbox", exc_exec("sandbox call timer_start(0, 'Scary')")) end) + + it('can be triggered after an empty string <expr> mapping', function() + local screen = Screen.new(40, 6) + screen:attach() + command([=[imap <expr> <F2> [timer_start(0, { _ -> execute("throw 'x'", "") }), ''][-1]]=]) + feed('i<F2>') + screen:expect({any='E605: Exception not caught: x'}) + end) end) |