diff options
43 files changed, 797 insertions, 472 deletions
diff --git a/.editorconfig b/.editorconfig index bb6a1423ef..22fee54b22 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,9 @@ end_of_line = lf insert_final_newline = true charset = utf-8 +[*.{c,h,in,lua}] +max_line_length = 100 + [{Makefile,**/Makefile,runtime/doc/*.txt}] indent_style = tab indent_size = 8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a81ee4238..d2eef13098 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,11 @@ on: - 'master' - 'release-[0-9]+.[0-9]+' +# Cancel any in-progress CI runs for a PR if it is updated +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + jobs: unixish: name: ${{ matrix.runner }} ${{ matrix.flavor }} (cc=${{ matrix.cc }}) diff --git a/.github/workflows/squash-typos.yml b/.github/workflows/squash-typos.yml deleted file mode 100644 index 6779589dc6..0000000000 --- a/.github/workflows/squash-typos.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Squash Typo Pull Requests - -on: - pull_request_target: - types: labeled -concurrency: - group: ${{ github.workflow }} -jobs: - build: - if: github.event.label.name == 'typo' - runs-on: ubuntu-latest - - permissions: - contents: write - pull-requests: write - - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: actions/setup-python@v2 - - - name: Setup git config - run: | - git config --global user.name 'marvim' - git config --global user.email 'marvim@users.noreply.github.com' - - - run: python scripts/squash_typos.py - env: - PR_NUMBER: ${{ github.event.number }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c2587ec0b..9f1829cf55 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -543,7 +543,9 @@ endif() message(STATUS "Using Lua interpreter: ${LUA_PRG}") -if(NOT WIN32) +option(COMPILE_LUA "Pre-compile Lua sources into bytecode (for sources that are included in the binary)" ON) + +if(COMPILE_LUA AND NOT WIN32) if(PREFER_LUA) foreach(CURRENT_LUAC_PRG luac5.1 luac) find_program(_CHECK_LUAC_PRG ${CURRENT_LUAC_PRG}) @@ -553,7 +555,10 @@ if(NOT WIN32) endif() endforeach() elseif(LUA_PRG MATCHES "luajit") - set(LUAC_PRG "${LUA_PRG} -b -s %s -" CACHE STRING "Format for compiling to Lua bytecode") + check_lua_module(${LUA_PRG} "jit.bcsave" LUAJIT_HAS_JIT_BCSAVE) + if(LUAJIT_HAS_JIT_BCSAVE) + set(LUAC_PRG "${LUA_PRG} -b -s %s -" CACHE STRING "Format for compiling to Lua bytecode") + endif() endif() endif() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index faf9181a2c..b02aeb1ed1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ If you want to help but don't know where to start, here are some low-risk/isolated tasks: - [Merge a Vim patch]. -- Try a [good first issue](../../labels/good-first-issue) or [complexity:low] issue. +- Try a [complexity:low] issue. - Fix bugs found by [Clang](#clang-scan-build), [PVS](#pvs-studio) or [Coverity](#coverity). - [Improve documentation][wiki-contribute-help] diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 3793a21f36..a3929aeab9 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -485,6 +485,16 @@ buf_attach_client({bufnr}, {client_id}) *vim.lsp.buf_attach_client()* {bufnr} (number) Buffer handle, or 0 for current {client_id} (number) Client id +buf_detach_client({bufnr}, {client_id}) *vim.lsp.buf_detach_client()* + Detaches client from the specified buffer. Note: While the + server is notified that the text document (buffer) was closed, + it is still able to send notifications should it ignore this + notification. + + Parameters: ~ + {bufnr} number Buffer handle, or 0 for current + {client_id} number Client id + buf_get_clients({bufnr}) *vim.lsp.buf_get_clients()* Gets a map of client_id:client pairs for the given buffer, where each value is a |vim.lsp.client| object. diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 630df16e79..97062e5986 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -708,6 +708,38 @@ vim.mpack.decode({str}) *vim.mpack.decode* Decodes (or "unpacks") the msgpack-encoded {str} to a Lua object. ------------------------------------------------------------------------------ +VIM.SPELL *lua-spell* + +vim.spell.check({str}) *vim.spell.check()* + Check {str} for spelling errors. Similar to the Vimscript function + |spellbadword()|. + + Note: The behaviour of this function is dependent on: 'spelllang', + 'spellfile', 'spellcapcheck' and 'spelloptions' which can all be local + to the buffer. Consider calling this with |nvim_buf_call()|. + + Example: > + vim.spell.check("the quik brown fox") + --> + { + {'quik', 'bad', 4} + } +< + + Parameters: ~ + {str} String to spell check. + + Return: ~ + List of tuples with three items: + - The badly spelled word. + - The type of the spelling error: + "bad" spelling mistake + "rare" rare word + "local" word only valid in another region + "caps" word should start with Capital + - The position in {str} where the word begins. + +------------------------------------------------------------------------------ VIM *lua-builtin* vim.api.{func}({...}) *vim.api* diff --git a/runtime/doc/sign.txt b/runtime/doc/sign.txt index 68165f3d3d..5cfa06c33c 100644 --- a/runtime/doc/sign.txt +++ b/runtime/doc/sign.txt @@ -122,8 +122,9 @@ See |sign_define()| for the equivalent Vim script function. in. Most useful is defining a background color. numhl={group} - Highlighting group used for 'number' column at the associated - line. Overrides |hl-LineNr|, |hl-CursorLineNr|. + Highlighting group used for the line number on the line where + the sign is placed. Overrides |hl-LineNr|, |hl-LineNrAbove|, + |hl-LineNrBelow|, and |hl-CursorLineNr|. text={text} *E239* Define the text that is displayed when there is no icon or the diff --git a/runtime/filetype.vim b/runtime/filetype.vim index 57aaeab766..2eeddbfb83 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -1087,7 +1087,9 @@ au BufNewFile,BufRead *.mmp setf mmp " Modsim III (or LambdaProlog) au BufNewFile,BufRead *.mod - \ if getline(1) =~ '\<module\>' | + \ if expand("<afile>") =~ '\<go.mod$' | + \ setf gomod | + \ elseif getline(1) =~ '\<module\>' | \ setf lprolog | \ else | \ setf modsim3 | @@ -1744,6 +1746,7 @@ au BufNewFile,BufRead *.ice setf slice " Microsoft Visual Studio Solution au BufNewFile,BufRead *.sln setf solution +au BufNewFile,BufRead *.slnf setf json " Spice au BufNewFile,BufRead *.sp,*.spice setf spice diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 2e530ec17a..00839ec181 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1112,9 +1112,9 @@ local text_document_did_change_handler do text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline) - -- Don't do anything if there are no clients attached. + -- Detach (nvim_buf_attach) via returning True to on_lines if no clients are attached if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then - return + return true end util.buf_versions[bufnr] = changedtick local compute_change_and_notify = changetracking.prepare(bufnr, firstline, lastline, new_lastline) @@ -1156,6 +1156,12 @@ function lsp.buf_attach_client(bufnr, client_id) client_id = {client_id, 'n'}; } bufnr = resolve_bufnr(bufnr) + if not vim.api.nvim_buf_is_loaded(bufnr) then + local _ = log.warn() and log.warn( + string.format("buf_attach_client called on unloaded buffer (id: %d): ", bufnr) + ) + return false + end local buffer_client_ids = all_buffer_active_clients[bufnr] -- This is our first time attaching to this buffer. if not buffer_client_ids then @@ -1214,6 +1220,50 @@ function lsp.buf_attach_client(bufnr, client_id) return true end +--- Detaches client from the specified buffer. +--- Note: While the server is notified that the text document (buffer) +--- was closed, it is still able to send notifications should it ignore this notification. +--- +---@param bufnr number Buffer handle, or 0 for current +---@param client_id number Client id +function lsp.buf_detach_client(bufnr, client_id) + validate { + bufnr = {bufnr, 'n', true}; + client_id = {client_id, 'n'}; + } + bufnr = resolve_bufnr(bufnr) + + local client = lsp.get_client_by_id(client_id) + if not client or not client.attached_buffers[bufnr] then + vim.notify( + string.format('Buffer (id: %d) is not attached to client (id: %d). Cannot detach.', client_id, bufnr) + ) + return + end + + changetracking.reset_buf(client, bufnr) + + if client.resolved_capabilities.text_document_open_close then + local uri = vim.uri_from_bufnr(bufnr) + local params = { textDocument = { uri = uri; } } + client.notify('textDocument/didClose', params) + end + + client.attached_buffers[bufnr] = nil + util.buf_versions[bufnr] = nil + + all_buffer_active_clients[bufnr][client_id] = nil + if #vim.tbl_keys(all_buffer_active_clients[bufnr]) == 0 then + all_buffer_active_clients[bufnr] = nil + end + + local namespace = vim.lsp.diagnostic.get_namespace(client_id) + vim.diagnostic.reset(namespace, bufnr) + + vim.notify(string.format('Detached buffer (id: %d) from client (id: %d)', bufnr, client_id)) + +end + --- Checks if a buffer is attached for a particular client. --- ---@param bufnr (number) Buffer handle, or 0 for current diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 68a030d50b..5921eb5bf0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -97,15 +97,17 @@ end ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 ---@return number `encoding` index of `index` in `line` function M._str_utfindex_enc(line, index, encoding) - if encoding ~= 'utf-8' then - local col32, col16 = vim.str_utfindex(line, index) - if encoding == 'utf-32' then - return col32 - else - return col16 - end + if not encoding then encoding = 'utf-16' end + if encoding == 'utf-8' then + if index then return index else return #line end + elseif encoding == 'utf-16' then + local _, col16 = vim.str_utfindex(line, index) + return col16 + elseif encoding == 'utf-32' then + local col32, _ = vim.str_utfindex(line, index) + return col32 else - return index + error("Invalid encoding: " .. vim.inspect(encoding)) end end @@ -117,10 +119,15 @@ end ---@param encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 ---@return number byte (utf-8) index of `encoding` index `index` in `line` function M._str_byteindex_enc(line, index, encoding) - if encoding ~= 'utf-8' then - return vim.str_byteindex(line, index, not encoding or encoding ~= 'utf-32') + if not encoding then encoding = 'utf-16' end + if encoding == 'utf-8' then + if index then return index else return #line end + elseif encoding == 'utf-16' then + return vim.str_byteindex(line, index, true) + elseif encoding == 'utf-32' then + return vim.str_byteindex(line, index) else - return index + error("Invalid encoding: " .. vim.inspect(encoding)) end end diff --git a/scripts/squash_typos.py b/scripts/squash_typos.py deleted file mode 100644 index b403a9b7c8..0000000000 --- a/scripts/squash_typos.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python -""" - -This script squashes a PR tagged with the "typo" label into a single, dedicated -"squash PR". - -""" - -import subprocess -import sys -import os - - -def get_authors_and_emails_from_pr(): - """ - - Return all contributing authors and their emails for the PR on current branch. - This includes co-authors, meaning that if two authors are credited for a - single commit, which is possible with GitHub, then both will get credited. - - """ - - # Get a list of all authors involved in the pull request (including co-authors). - authors = subprocess.check_output( - [ - "gh", - "pr", - "view", - os.environ["PR_NUMBER"], - "--json", - "commits", - "--jq", - ".[][].authors.[].name", - ], - text=True, - ).splitlines() - - # Get a list of emails of the aforementioned authors. - emails = subprocess.check_output( - [ - "gh", - "pr", - "view", - os.environ["PR_NUMBER"], - "--json", - "commits", - "--jq", - ".[][].authors.[].email", - ], - text=True, - ).splitlines() - - authors_and_emails = [(author, mail) for author, mail in zip(authors, emails)] - - return authors_and_emails - - -def rebase_onto_pr(): - """ - - Rebase current branch onto the PR. - - """ - - # Check out the pull request. - subprocess.call(["gh", "pr", "checkout", os.environ["PR_NUMBER"]]) - - rebase_onto_master() - - # Change back to the original branch. - subprocess.call(["git", "switch", "-"]) - - # Rebase onto the pull request, aka include the commits in the pull request - # in the current branch. Abort with error message if rebase fails. - - try: - subprocess.check_call(["git", "rebase", "-"]) - except subprocess.CalledProcessError: - subprocess.call(["git", "rebase", "--abort"]) - squash_url = subprocess.check_output( - ["gh", "pr", "view", "--json", "url", "--jq", ".url"], text=True - ).strip() - - subprocess.call( - [ - "gh", - "pr", - "comment", - os.environ["PR_NUMBER"], - "--body", - f"Your edit conflicts with an already scheduled fix \ - ({squash_url}). Please check that batch PR whether your fix is \ - already included; if not, then please wait until the batch PR \ - is merged and then rebase your PR on top of master.", - ] - ) - - sys.exit( - f"\n\nERROR: Your edit conflicts with an already scheduled fix \ -{squash_url} \n\n" - ) - - -def rebase_onto_master(): - """ - - Rebase current branch onto the master i.e. make sure current branch is up - to date. Abort on error. - - """ - - default_branch = f"{os.environ['GITHUB_BASE_REF']}" - subprocess.check_call(["git", "rebase", default_branch]) - - -def squash_all_commits(message_body_before): - """ - - Squash all commits on the PR into a single commit. Credit all authors by - name and email. - - """ - - default_branch = f"{os.environ['GITHUB_BASE_REF']}" - subprocess.call(["git", "reset", "--soft", default_branch]) - - authors_and_emails = get_authors_and_emails_from_pr() - commit_message_coauthors = ( - "\n" - + "\n".join([f"Co-authored-by: {i[0]} <{i[1]}>" for i in authors_and_emails]) - + "\n" - + message_body_before - ) - subprocess.call( - ["git", "commit", "-m", "chore: typo fixes", "-m", commit_message_coauthors] - ) - - -def force_push(branch): - """ - - Like the name implies, force push <branch>. - - """ - - gh_actor = os.environ["GITHUB_ACTOR"] - gh_token = os.environ["GITHUB_TOKEN"] - gh_repo = os.environ["GITHUB_REPOSITORY"] - subprocess.call( - [ - "git", - "push", - "--force", - f"https://{gh_actor}:{gh_token}@github.com/{gh_repo}", - branch, - ] - ) - - -def checkout_branch(branch): - """ - - Create and checkout <branch>. Check if branch exists on remote, if so then - sync local branch to remote. - - Return True if remote branch exists, else False. - - """ - - # FIXME I'm not sure why the local branch isn't tracking the remote branch - # automatically. This works but I'm pretty sure it can be done in a more - # "elegant" fashion - - show_ref_output = subprocess.check_output(["git", "show-ref"], text=True).strip() - - if branch in show_ref_output: - subprocess.call(["git", "checkout", "-b", branch, f"origin/{branch}"]) - return True - - subprocess.call(["git", "checkout", "-b", branch]) - return False - - -def get_all_pr_urls(pr_branch_exists): - """ - - Return a list of URLs for the pull requests with the typo fixes. If a - squash branch exists then extract the URLs from the body text. - - """ - - all_pr_urls = "" - if pr_branch_exists: - all_pr_urls += subprocess.check_output( - ["gh", "pr", "view", "--json", "body", "--jq", ".body"], text=True - ) - - all_pr_urls += subprocess.check_output( - ["gh", "pr", "view", os.environ["PR_NUMBER"], "--json", "url", "--jq", ".url"], - text=True, - ).strip() - - return all_pr_urls - - -def main(): - pr_branch = "marvim/squash-typos" - - pr_branch_exists = checkout_branch(pr_branch) - - rebase_onto_master() - force_push(pr_branch) - - message_body_before = "\n".join( - subprocess.check_output( - ["git", "log", "--format=%B", "-n1", pr_branch], text=True - ).splitlines()[2:] - ) - - rebase_onto_pr() - force_push(pr_branch) - - subprocess.call( - [ - "gh", - "pr", - "create", - "--fill", - "--head", - pr_branch, - "--title", - "chore: typo fixes (automated)", - ], - text=True, - ) - - squash_all_commits(message_body_before) - force_push(pr_branch) - - all_pr_urls = get_all_pr_urls(pr_branch_exists) - subprocess.call(["gh", "pr", "edit", "--add-label", "typo", "--body", all_pr_urls]) - - subprocess.call(["gh", "pr", "close", os.environ["PR_NUMBER"]]) - - squash_url = subprocess.check_output( - ["gh", "pr", "view", "--json", "url", "--jq", ".url"], text=True - ).strip() - subprocess.call( - [ - "gh", - "pr", - "comment", - os.environ["PR_NUMBER"], - "--body", - f"Thank you for your contribution! We collect all typo fixes \ - into a single pull request and merge it once it gets big enough: \ - {squash_url}", - ] - ) - - -if __name__ == "__main__": - main() diff --git a/src/mpack/lmpack.c b/src/mpack/lmpack.c index 87acf46592..126f2f3824 100644 --- a/src/mpack/lmpack.c +++ b/src/mpack/lmpack.c @@ -595,6 +595,7 @@ static void lmpack_unparse_enter(mpack_parser_t *parser, mpack_node_t *node) /* push the pair */ result = lua_next(L, -2); assert(result); /* should not be here if the map was fully processed */ + (void)result; /* ignore unused warning */ if (parent->key_visited) { /* release the current key */ lmpack_unref(L, packer->reg, (int)parent->data[1].i); @@ -1010,6 +1011,7 @@ static int lmpack_session_reply(lua_State *L) "invalid request id"); result = mpack_rpc_reply(session->session, &b, &bl, (mpack_uint32_t)id); assert(result == MPACK_OK); + (void)result; /* ignore unused warning */ lua_pushlstring(L, buf, sizeof(buf) - bl); return 1; } @@ -1027,6 +1029,7 @@ static int lmpack_session_notify(lua_State *L) session = lmpack_check_session(L, 1); result = mpack_rpc_notify(session->session, &b, &bl); assert(result == MPACK_OK); + (void)result; /* ignore unused warning */ lua_pushlstring(L, buf, sizeof(buf) - bl); return 1; } diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 4f7c320129..f81cdf9deb 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -696,7 +696,17 @@ Object nvim_get_option_value(String name, Dict(option) *opts, Error *err) rv = INTEGER_OBJ(numval); break; case 2: - rv = BOOLEAN_OBJ(!!numval); + switch (numval) { + case 0: + case 1: + rv = BOOLEAN_OBJ(numval); + break; + default: + // Boolean options that return something other than 0 or 1 should return nil. Currently this + // only applies to 'autoread' which uses -1 as a local value to indicate "unset" + rv = NIL; + break; + } break; default: api_set_error(err, kErrorTypeValidation, "unknown option '%s'", name.data); @@ -749,7 +759,7 @@ void nvim_set_option_value(String name, Object value, Dict(option) *opts, Error stringval = value.data.string.data; break; case kObjectTypeNil: - // Do nothing + scope |= OPT_CLEAR; break; default: api_set_error(err, kErrorTypeValidation, "invalid value for option"); diff --git a/src/nvim/autocmd.c b/src/nvim/autocmd.c index 3780cad1d6..463bd5e0e6 100644 --- a/src/nvim/autocmd.c +++ b/src/nvim/autocmd.c @@ -932,6 +932,12 @@ static int do_autocmd_event(event_T event, char_u *pat, bool once, int nested, c last_mode = get_mode(); } + // If the event is CursorMoved, update the last cursor position + // position to avoid immediately triggering the autocommand + if (event == EVENT_CURSORMOVED && !has_event(EVENT_CURSORMOVED)) { + curwin->w_last_cursormoved = curwin->w_cursor; + } + ap->cmds = NULL; *prev_ap = ap; last_autopat[(int)event] = ap; diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 2135d0bcd2..5b63ff5648 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -1685,7 +1685,7 @@ static void init_prompt(int cmdchar_todo) // Insert always starts after the prompt, allow editing text after it. if (Insstart_orig.lnum != curwin->w_cursor.lnum || Insstart_orig.col != (colnr_T)STRLEN(prompt)) { Insstart.lnum = curwin->w_cursor.lnum; - Insstart.col = STRLEN(prompt); + Insstart.col = (colnr_T)STRLEN(prompt); Insstart_orig = Insstart; Insstart_textlen = Insstart.col; Insstart_blank_vcol = MAXCOL; @@ -1696,7 +1696,7 @@ static void init_prompt(int cmdchar_todo) coladvance(MAXCOL); } if (curwin->w_cursor.col < (colnr_T)STRLEN(prompt)) { - curwin->w_cursor.col = STRLEN(prompt); + curwin->w_cursor.col = (colnr_T)STRLEN(prompt); } // Make sure the cursor is in a valid position. check_cursor(); diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index d43eeb4a15..4d6ed56164 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -1082,15 +1082,13 @@ static void f_chdir(typval_T *argvars, typval_T *rettv, FunPtr fptr) // Return the current directory cwd = xmalloc(MAXPATHL); - if (cwd != NULL) { - if (os_dirname(cwd, MAXPATHL) != FAIL) { + if (os_dirname(cwd, MAXPATHL) != FAIL) { #ifdef BACKSLASH_IN_FILENAME - slash_adjust(cwd); + slash_adjust(cwd); #endif - rettv->vval.v_string = vim_strsave(cwd); - } - xfree(cwd); + rettv->vval.v_string = vim_strsave(cwd); } + xfree(cwd); if (curwin->w_localdir != NULL) { scope = kCdScopeWindow; diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index c0cb17fa61..4965eb9c20 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -4584,6 +4584,9 @@ void ex_global(exarg_T *eap) // a match on this line? match = vim_regexec_multi(®match, curwin, curbuf, lnum, (colnr_T)0, NULL, NULL); + if (regmatch.regprog == NULL) { + break; // re-compiling regprog failed + } if ((type == 'g' && match) || (type == 'v' && !match)) { ml_setmarked(lnum); ndone++; diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 9f0f8d93a3..ee7946fe3e 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -7816,8 +7816,9 @@ bool changedir_func(char_u *new_dir, CdScope scope) } #endif - if (vim_chdir(new_dir) == 0) { - bool dir_differs = pdir == NULL || pathcmp((char *)pdir, (char *)new_dir, -1) != 0; + bool dir_differs = new_dir == NULL || pdir == NULL + || pathcmp((char *)pdir, (char *)new_dir, -1) != 0; + if (new_dir != NULL && (!dir_differs || vim_chdir(new_dir) == 0)) { post_chdir(scope, dir_differs); retval = true; } else { diff --git a/src/nvim/file_search.c b/src/nvim/file_search.c index d31021b3ef..1884dd49c5 100644 --- a/src/nvim/file_search.c +++ b/src/nvim/file_search.c @@ -1667,14 +1667,19 @@ int vim_chdirfile(char_u *fname, CdCause cause) NameBuff[0] = NUL; } - if (os_chdir(dir) == 0) { - if (cause != kCdCauseOther && pathcmp(dir, (char *)NameBuff, -1) != 0) { - do_autocmd_dirchanged(dir, kCdScopeWindow, cause); - } - } else { + if (pathcmp(dir, (char *)NameBuff, -1) == 0) { + // nothing to do + return OK; + } + + if (os_chdir(dir) != 0) { return FAIL; } + if (cause != kCdCauseOther) { + do_autocmd_dirchanged(dir, kCdScopeWindow, cause); + } + return OK; } diff --git a/src/nvim/lua/spell.c b/src/nvim/lua/spell.c new file mode 100644 index 0000000000..b84124bc19 --- /dev/null +++ b/src/nvim/lua/spell.c @@ -0,0 +1,99 @@ + +#include <lua.h> +#include <lauxlib.h> + +#include "nvim/spell.h" +#include "nvim/vim.h" +#include "nvim/lua/spell.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "lua/spell.c.generated.h" +#endif + +int nlua_spell_check(lua_State *lstate) +{ + if (lua_gettop(lstate) < 1) { + return luaL_error(lstate, "Expected 1 argument"); + } + + if (lua_type(lstate, 1) != LUA_TSTRING) { + luaL_argerror(lstate, 1, "expected string"); + } + + const char *str = lua_tolstring(lstate, 1, NULL); + + // spell.c requires that 'spell' is enabled, so we need to temporarily enable + // it before we can call spell functions. + const int wo_spell_save = curwin->w_p_spell; + + if (!curwin->w_p_spell) { + did_set_spelllang(curwin); + curwin->w_p_spell = true; + } + + // Check 'spelllang' + if (*curwin->w_s->b_p_spl == NUL) { + emsg(_(e_no_spell)); + curwin->w_p_spell = wo_spell_save; + return 0; + } + + hlf_T attr = HLF_COUNT; + size_t len = 0; + size_t pos = 0; + int capcol = -1; + int no_res = 0; + const char * result; + + lua_createtable(lstate, 0, 0); + + while (*str != NUL) { + attr = HLF_COUNT; + len = spell_check(curwin, (char_u *)str, &attr, &capcol, false); + assert(len <= INT_MAX); + + if (attr != HLF_COUNT) { + lua_createtable(lstate, 3, 0); + + lua_pushlstring(lstate, str, len); + lua_rawseti(lstate, -2, 1); + + result = attr == HLF_SPB ? "bad" : + attr == HLF_SPR ? "rare" : + attr == HLF_SPL ? "local" : + attr == HLF_SPC ? "caps" : + NULL; + + assert(result != NULL); + + lua_pushstring(lstate, result); + lua_rawseti(lstate, -2, 2); + + // +1 for 1-indexing + lua_pushinteger(lstate, (long)pos + 1); + lua_rawseti(lstate, -2, 3); + + lua_rawseti(lstate, -2, ++no_res); + } + + str += len; + pos += len; + capcol -= (int)len; + } + + // Restore 'spell' + curwin->w_p_spell = wo_spell_save; + return 1; +} + +static const luaL_Reg spell_functions[] = { + { "check", nlua_spell_check }, + { NULL , NULL } +}; + +int luaopen_spell(lua_State *L) +{ + lua_newtable(L); + luaL_register(L, NULL, spell_functions); + return 1; +} diff --git a/src/nvim/lua/spell.h b/src/nvim/lua/spell.h new file mode 100644 index 0000000000..8f798a5191 --- /dev/null +++ b/src/nvim/lua/spell.h @@ -0,0 +1,12 @@ +#ifndef NVIM_LUA_SPELL_H +#define NVIM_LUA_SPELL_H + +#include <lauxlib.h> +#include <lua.h> +#include <lualib.h> + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "lua/spell.h.generated.h" +#endif + +#endif // NVIM_LUA_SPELL_H diff --git a/src/nvim/lua/stdlib.c b/src/nvim/lua/stdlib.c index 0d6789317c..18a579ed0f 100644 --- a/src/nvim/lua/stdlib.c +++ b/src/nvim/lua/stdlib.c @@ -30,6 +30,7 @@ #include "nvim/lua/stdlib.h" #include "nvim/lua/treesitter.h" #include "nvim/lua/xdiff.h" +#include "nvim/lua/spell.h" #include "nvim/macros.h" #include "nvim/map.h" #include "nvim/memline.h" @@ -518,6 +519,10 @@ void nlua_state_add_stdlib(lua_State *const lstate) lua_pushcfunction(lstate, &nlua_xdl_diff); lua_setfield(lstate, -2, "diff"); + // vim.spell + luaopen_spell(lstate); + lua_setfield(lstate, -2, "spell"); + lua_cjson_new(lstate); lua_setfield(lstate, -2, "json"); } diff --git a/src/nvim/option.c b/src/nvim/option.c index fba3de1fcc..65adda3c01 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -2991,7 +2991,7 @@ ambw_end: } } else if (varp == &curwin->w_p_fdc || varp == &curwin->w_allbuf_opt.wo_fdc) { // 'foldcolumn' - if (check_opt_strings(*varp, p_fdc_values, false) != OK) { + if (**varp == NUL || check_opt_strings(*varp, p_fdc_values, false) != OK) { errmsg = e_invarg; } } else if (varp == &p_pt) { @@ -3370,6 +3370,9 @@ static int int_cmp(const void *a, const void *b) /// @return OK when the value is valid, FAIL otherwise int check_signcolumn(char_u *val) { + if (*val == NUL) { + return FAIL; + } // check for basic match if (check_opt_strings(val, p_scl_values, false) == OK) { return OK; @@ -5059,6 +5062,9 @@ int get_option_value_strict(char *name, int64_t *numval, char **stringval, int o /// @param[in] number New value for the number or boolean option. /// @param[in] string New value for string option. /// @param[in] opt_flags Flags: OPT_LOCAL, OPT_GLOBAL, or 0 (both). +/// If OPT_CLEAR is set, the value of the option +/// is cleared (the exact semantics of this depend +/// on the option). /// /// @return NULL on success, error message on error. char *set_option_value(const char *const name, const long number, const char *const string, @@ -5084,7 +5090,7 @@ char *set_option_value(const char *const name, const long number, const char *co } if (flags & P_STRING) { const char *s = string; - if (s == NULL) { + if (s == NULL || opt_flags & OPT_CLEAR) { s = ""; } return set_string_option(opt_idx, s, opt_flags); @@ -5106,10 +5112,23 @@ char *set_option_value(const char *const name, const long number, const char *co return NULL; // do nothing as we hit an error } } + long numval = number; + if (opt_flags & OPT_CLEAR) { + if ((int *)varp == &curbuf->b_p_ar) { + numval = -1; + } else if ((long *)varp == &curbuf->b_p_ul) { + numval = NO_LOCAL_UNDOLEVEL; + } else if ((long *)varp == &curwin->w_p_so || (long *)varp == &curwin->w_p_siso) { + numval = -1; + } else { + char *s = NULL; + (void)get_option_value(name, &numval, (char_u **)&s, OPT_GLOBAL); + } + } if (flags & P_NUM) { - return set_num_option(opt_idx, varp, number, NULL, 0, opt_flags); + return set_num_option(opt_idx, varp, numval, NULL, 0, opt_flags); } else { - return set_bool_option(opt_idx, varp, (int)number, opt_flags); + return set_bool_option(opt_idx, varp, (int)numval, opt_flags); } } } diff --git a/src/nvim/option.h b/src/nvim/option.h index 452494172f..f7dbaafeec 100644 --- a/src/nvim/option.h +++ b/src/nvim/option.h @@ -22,6 +22,7 @@ typedef enum { OPT_ONECOLUMN = 64, ///< list options one per line OPT_NO_REDRAW = 128, ///< ignore redraw flags on option OPT_SKIPRTP = 256, ///< "skiprtp" in 'sessionoptions' + OPT_CLEAR = 512, ///< Clear local value of an option. } OptionFlags; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index 606c03f838..da2ada791f 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -386,7 +386,7 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i void pum_redraw(void) { int row = 0; - int col; + int grid_col; int attr_norm = win_hl_attr(curwin, HLF_PNI); int attr_select = win_hl_attr(curwin, HLF_PSI); int attr_scroll = win_hl_attr(curwin, HLF_PSB); @@ -479,7 +479,7 @@ void pum_redraw(void) // Display each entry, use two spaces for a Tab. // Do this 3 times: For the main text, kind and extra info - col = col_off; + grid_col = col_off; totwidth = 0; for (round = 1; round <= 3; ++round) { @@ -537,24 +537,15 @@ void pum_redraw(void) } } grid_puts_len(&pum_grid, rt, (int)STRLEN(rt), row, - col - size + 1, attr); + grid_col - size + 1, attr); xfree(rt_start); xfree(st); - col -= width; + grid_col -= width; } else { - int size = (int)STRLEN(st); - int cells = (int)mb_string2cells(st); - - // only draw the text that fits - while (size > 0 && col + cells > pum_width + pum_col) { - size--; - size -= utf_head_off(st, st + size); - cells -= utf_ptr2cells(st + size); - } - - grid_puts_len(&pum_grid, st, size, row, col, attr); + // use grid_puts_len() to truncate the text + grid_puts(&pum_grid, st, row, grid_col, attr); xfree(st); - col += width; + grid_col += width; } if (*p != TAB) { @@ -563,12 +554,12 @@ void pum_redraw(void) // Display two spaces for a Tab. if (pum_rl) { - grid_puts_len(&pum_grid, (char_u *)" ", 2, row, col - 1, + grid_puts_len(&pum_grid, (char_u *)" ", 2, row, grid_col - 1, attr); - col -= 2; + grid_col -= 2; } else { - grid_puts_len(&pum_grid, (char_u *)" ", 2, row, col, attr); - col += 2; + grid_puts_len(&pum_grid, (char_u *)" ", 2, row, grid_col, attr); + grid_col += 2; } totwidth += 2; // start text at next char @@ -599,21 +590,21 @@ void pum_redraw(void) if (pum_rl) { grid_fill(&pum_grid, row, row + 1, col_off - pum_base_width - n + 1, - col + 1, ' ', ' ', attr); - col = col_off - pum_base_width - n + 1; + grid_col + 1, ' ', ' ', attr); + grid_col = col_off - pum_base_width - n + 1; } else { - grid_fill(&pum_grid, row, row + 1, col, + grid_fill(&pum_grid, row, row + 1, grid_col, col_off + pum_base_width + n, ' ', ' ', attr); - col = col_off + pum_base_width + n; + grid_col = col_off + pum_base_width + n; } totwidth = pum_base_width + n; } if (pum_rl) { - grid_fill(&pum_grid, row, row + 1, col_off - pum_width + 1, col + 1, + grid_fill(&pum_grid, row, row + 1, col_off - pum_width + 1, grid_col + 1, ' ', ' ', attr); } else { - grid_fill(&pum_grid, row, row + 1, col, col_off + pum_width, ' ', ' ', + grid_fill(&pum_grid, row, row + 1, grid_col, col_off + pum_width, ' ', ' ', attr); } diff --git a/src/nvim/regexp_nfa.c b/src/nvim/regexp_nfa.c index eac1b4596e..41c927eaa6 100644 --- a/src/nvim/regexp_nfa.c +++ b/src/nvim/regexp_nfa.c @@ -2565,20 +2565,20 @@ static void nfa_print_state2(FILE *debugf, nfa_state_T *state, garray_T *indent) ga_concat(indent, (char_u *)"| "); else ga_concat(indent, (char_u *)" "); - ga_append(indent, '\0'); + ga_append(indent, NUL); nfa_print_state2(debugf, state->out, indent); /* replace last part of indent for state->out1 */ indent->ga_len -= 3; ga_concat(indent, (char_u *)" "); - ga_append(indent, '\0'); + ga_append(indent, NUL); nfa_print_state2(debugf, state->out1, indent); /* shrink indent */ indent->ga_len -= 3; - ga_append(indent, '\0'); + ga_append(indent, NUL); } /* diff --git a/src/nvim/screen.c b/src/nvim/screen.c index a938a3b062..e62e3ca7bc 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -5933,6 +5933,8 @@ void grid_puts_len(ScreenGrid *grid, char_u *text, int textlen, int row, int col // Only 1 cell left, but character requires 2 cells: // display a '>' in the last column to avoid wrapping. */ c = '>'; + u8c = '>'; + u8cc[0] = 0; mbyte_cells = 1; } @@ -5963,6 +5965,13 @@ void grid_puts_len(ScreenGrid *grid, char_u *text, int textlen, int row, int col clear_next_cell = true; } + // When at the start of the text and overwriting the right half of a + // two-cell character in the same grid, truncate that into a '>'. + if (ptr == text && col > 0 && grid->chars[off][0] == 0) { + grid->chars[off - 1][0] = '>'; + grid->chars[off - 1][1] = 0; + } + schar_copy(grid->chars[off], buf); grid->attrs[off] = attr; if (mbyte_cells == 2) { diff --git a/src/nvim/sign.c b/src/nvim/sign.c index 32be714184..a308df07d1 100644 --- a/src/nvim/sign.c +++ b/src/nvim/sign.c @@ -1740,7 +1740,7 @@ char_u *get_sign_name(expand_T *xp, int idx) case EXP_SUBCMD: return (char_u *)cmds[idx]; case EXP_DEFINE: { - char *define_arg[] = { "icon=", "linehl=", "text=", "texthl=", "numhl=", + char *define_arg[] = { "culhl=", "icon=", "linehl=", "numhl=", "text=", "texthl=", NULL }; return (char_u *)define_arg[idx]; } @@ -1849,6 +1849,7 @@ void set_context_in_sign_cmd(expand_T *xp, char_u *arg) case SIGNCMD_DEFINE: if (STRNCMP(last, "texthl", 6) == 0 || STRNCMP(last, "linehl", 6) == 0 + || STRNCMP(last, "culhl", 5) == 0 || STRNCMP(last, "numhl", 5) == 0) { xp->xp_context = EXPAND_HIGHLIGHT; } else if (STRNCMP(last, "icon", 4) == 0) { diff --git a/src/nvim/testdir/setup.vim b/src/nvim/testdir/setup.vim index b3df8c63e6..fdae0697c3 100644 --- a/src/nvim/testdir/setup.vim +++ b/src/nvim/testdir/setup.vim @@ -13,7 +13,7 @@ set fillchars=vert:\|,fold:- set laststatus=1 set listchars=eol:$ set joinspaces -set nohidden smarttab noautoindent noautoread complete-=i noruler noshowcmd +set nohidden nosmarttab noautoindent noautoread complete-=i noruler noshowcmd set nrformats+=octal set shortmess-=F set sidescroll=0 diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 4e1a24af61..45285b69a1 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -2380,95 +2380,7 @@ func Test_autocmd_was_using_freed_memory() pclose endfunc -func Test_FileChangedShell_reload() - if !has('unix') - return - endif - augroup testreload - au FileChangedShell Xchanged let g:reason = v:fcs_reason | let v:fcs_choice = 'reload' - augroup END - new Xchanged - call setline(1, 'reload this') - write - " Need to wait until the timestamp would change by at least a second. - sleep 2 - silent !echo 'extra line' >>Xchanged - checktime - call assert_equal('changed', g:reason) - call assert_equal(2, line('$')) - call assert_equal('extra line', getline(2)) - - " Only triggers once - let g:reason = '' - checktime - call assert_equal('', g:reason) - - " When deleted buffer is not reloaded - silent !rm Xchanged - let g:reason = '' - checktime - call assert_equal('deleted', g:reason) - call assert_equal(2, line('$')) - call assert_equal('extra line', getline(2)) - - " When recreated buffer is reloaded - call setline(1, 'buffer is changed') - silent !echo 'new line' >>Xchanged - let g:reason = '' - checktime - call assert_equal('conflict', g:reason) - call assert_equal(1, line('$')) - call assert_equal('new line', getline(1)) - - " Only mode changed - silent !chmod +x Xchanged - let g:reason = '' - checktime - call assert_equal('mode', g:reason) - call assert_equal(1, line('$')) - call assert_equal('new line', getline(1)) - - " Only time changed - sleep 2 - silent !touch Xchanged - let g:reason = '' - checktime - call assert_equal('time', g:reason) - call assert_equal(1, line('$')) - call assert_equal('new line', getline(1)) - - if has('persistent_undo') - " With an undo file the reload can be undone and a change before the - " reload. - set undofile - call setline(2, 'before write') - write - call setline(2, 'after write') - sleep 2 - silent !echo 'different line' >>Xchanged - let g:reason = '' - checktime - call assert_equal('conflict', g:reason) - call assert_equal(3, line('$')) - call assert_equal('before write', getline(2)) - call assert_equal('different line', getline(3)) - " undo the reload - undo - call assert_equal(2, line('$')) - call assert_equal('after write', getline(2)) - " undo the change before reload - undo - call assert_equal(2, line('$')) - call assert_equal('before write', getline(2)) - - set noundofile - endif - - - au! testreload - bwipe! - call delete('Xchanged') -endfunc +" FileChangedShell tested in test_filechanged.vim func LogACmd() call add(g:logged, line('$')) diff --git a/src/nvim/testdir/test_blob.vim b/src/nvim/testdir/test_blob.vim index 20758b0c0a..af42b3857d 100644 --- a/src/nvim/testdir/test_blob.vim +++ b/src/nvim/testdir/test_blob.vim @@ -346,4 +346,12 @@ func Test_blob_sort() endif endfunc +" The following used to cause an out-of-bounds memory access +func Test_blob2string() + let v = '0z' .. repeat('01010101.', 444) + let v ..= '01' + exe 'let b = ' .. v + call assert_equal(v, string(b)) +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_filechanged.vim b/src/nvim/testdir/test_filechanged.vim new file mode 100644 index 0000000000..b95cd5faf8 --- /dev/null +++ b/src/nvim/testdir/test_filechanged.vim @@ -0,0 +1,149 @@ +" Tests for when a file was changed outside of Vim. + +func Test_FileChangedShell_reload() + if !has('unix') + return + endif + augroup testreload + au FileChangedShell Xchanged_r let g:reason = v:fcs_reason | let v:fcs_choice = 'reload' + augroup END + new Xchanged_r + call setline(1, 'reload this') + write + " Need to wait until the timestamp would change by at least a second. + sleep 2 + silent !echo 'extra line' >>Xchanged_r + checktime + call assert_equal('changed', g:reason) + call assert_equal(2, line('$')) + call assert_equal('extra line', getline(2)) + + " Only triggers once + let g:reason = '' + checktime + call assert_equal('', g:reason) + + " When deleted buffer is not reloaded + silent !rm Xchanged_r + let g:reason = '' + checktime + call assert_equal('deleted', g:reason) + call assert_equal(2, line('$')) + call assert_equal('extra line', getline(2)) + + " When recreated buffer is reloaded + call setline(1, 'buffer is changed') + silent !echo 'new line' >>Xchanged_r + let g:reason = '' + checktime + call assert_equal('conflict', g:reason) + call assert_equal(1, line('$')) + call assert_equal('new line', getline(1)) + + " Only mode changed + silent !chmod +x Xchanged_r + let g:reason = '' + checktime + call assert_equal('mode', g:reason) + call assert_equal(1, line('$')) + call assert_equal('new line', getline(1)) + + " Only time changed + sleep 2 + silent !touch Xchanged_r + let g:reason = '' + checktime + call assert_equal('time', g:reason) + call assert_equal(1, line('$')) + call assert_equal('new line', getline(1)) + + if has('persistent_undo') + " With an undo file the reload can be undone and a change before the + " reload. + set undofile + call setline(2, 'before write') + write + call setline(2, 'after write') + sleep 2 + silent !echo 'different line' >>Xchanged_r + let g:reason = '' + checktime + call assert_equal('conflict', g:reason) + call assert_equal(3, line('$')) + call assert_equal('before write', getline(2)) + call assert_equal('different line', getline(3)) + " undo the reload + undo + call assert_equal(2, line('$')) + call assert_equal('after write', getline(2)) + " undo the change before reload + undo + call assert_equal(2, line('$')) + call assert_equal('before write', getline(2)) + + set noundofile + endif + + au! testreload + bwipe! + call delete(undofile('Xchanged_r')) + call delete('Xchanged_r') +endfunc + +func Test_file_changed_dialog() + throw 'skipped: TODO: ' + if !has('unix') || has('gui_running') + return + endif + au! FileChangedShell + + new Xchanged_d + call setline(1, 'reload this') + write + " Need to wait until the timestamp would change by at least a second. + sleep 2 + silent !echo 'extra line' >>Xchanged_d + call feedkeys('L', 'L') + checktime + call assert_match('W11:', v:warningmsg) + call assert_equal(2, line('$')) + call assert_equal('reload this', getline(1)) + call assert_equal('extra line', getline(2)) + + " delete buffer, only shows an error, no prompt + silent !rm Xchanged_d + checktime + call assert_match('E211:', v:warningmsg) + call assert_equal(2, line('$')) + call assert_equal('extra line', getline(2)) + let v:warningmsg = 'empty' + + " change buffer, recreate the file and reload + call setline(1, 'buffer is changed') + silent !echo 'new line' >Xchanged_d + call feedkeys('L', 'L') + checktime + call assert_match('W12:', v:warningmsg) + call assert_equal(1, line('$')) + call assert_equal('new line', getline(1)) + + " Only mode changed, reload + silent !chmod +x Xchanged_d + call feedkeys('L', 'L') + checktime + call assert_match('W16:', v:warningmsg) + call assert_equal(1, line('$')) + call assert_equal('new line', getline(1)) + + " Only time changed, no prompt + sleep 2 + silent !touch Xchanged_d + let v:warningmsg = '' + checktime + call assert_equal('', v:warningmsg) + call assert_equal(1, line('$')) + call assert_equal('new line', getline(1)) + + bwipe! + call delete('Xchanged_d') +endfunc diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index dbe0cd8388..1ee23bb646 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -204,6 +204,7 @@ let s:filename_checks = { \ 'gnash': ['gnashrc', '.gnashrc', 'gnashpluginrc', '.gnashpluginrc'], \ 'gnuplot': ['file.gpi'], \ 'go': ['file.go'], + \ 'gomod': ['go.mod'], \ 'gp': ['file.gp', '.gprc'], \ 'gpg': ['/.gnupg/options', '/.gnupg/gpg.conf', '/usr/any/gnupg/options.skel', 'any/.gnupg/gpg.conf', 'any/.gnupg/options', 'any/usr/any/gnupg/options.skel'], \ 'grads': ['file.gs'], @@ -262,7 +263,7 @@ let s:filename_checks = { \ 'jgraph': ['file.jgr'], \ 'jovial': ['file.jov', 'file.j73', 'file.jovial'], \ 'jproperties': ['file.properties', 'file.properties_xx', 'file.properties_xx_xx', 'some.properties_xx_xx_file'], - \ 'json': ['file.json', 'file.jsonp', 'file.json-patch', 'file.webmanifest', 'Pipfile.lock', 'file.ipynb', '.babelrc', '.eslintrc', '.prettierrc', '.firebaserc'], + \ 'json': ['file.json', 'file.jsonp', 'file.json-patch', 'file.webmanifest', 'Pipfile.lock', 'file.ipynb', '.babelrc', '.eslintrc', '.prettierrc', '.firebaserc', 'file.slnf'], \ 'jsonc': ['file.jsonc'], \ 'jsp': ['file.jsp'], \ 'julia': ['file.jl'], diff --git a/src/nvim/testdir/test_messages.vim b/src/nvim/testdir/test_messages.vim index 2140fe21ea..e0286548d9 100644 --- a/src/nvim/testdir/test_messages.vim +++ b/src/nvim/testdir/test_messages.vim @@ -108,3 +108,11 @@ func Test_echospace() set ruler& showcmd& endfunc + +" this was missing a terminating NUL +func Test_echo_string_partial() + function CountSpaces() + endfunction + call assert_equal("function('CountSpaces', [{'ccccccccccc': ['ab', 'cd'], 'aaaaaaaaaaa': v:false, 'bbbbbbbbbbbb': ''}])", string(function('CountSpaces', [#{aaaaaaaaaaa: v:false, bbbbbbbbbbbb: '', ccccccccccc: ['ab', 'cd']}]))) +endfunc + diff --git a/src/nvim/testdir/test_signs.vim b/src/nvim/testdir/test_signs.vim index 799e6cb57b..ff9ba3d8ed 100644 --- a/src/nvim/testdir/test_signs.vim +++ b/src/nvim/testdir/test_signs.vim @@ -218,15 +218,13 @@ func Test_sign_completion() call assert_equal('"sign define jump list place undefine unplace', @:) call feedkeys(":sign define Sign \<C-A>\<C-B>\"\<CR>", 'tx') - call assert_equal('"sign define Sign icon= linehl= numhl= text= texthl=', @:) + call assert_equal('"sign define Sign culhl= icon= linehl= numhl= text= texthl=', @:) - call feedkeys(":sign define Sign linehl=Spell\<C-A>\<C-B>\"\<CR>", 'tx') - call assert_equal('"sign define Sign linehl=SpellBad SpellCap ' . - \ 'SpellLocal SpellRare', @:) - - call feedkeys(":sign define Sign texthl=Spell\<C-A>\<C-B>\"\<CR>", 'tx') - call assert_equal('"sign define Sign texthl=SpellBad SpellCap ' . - \ 'SpellLocal SpellRare', @:) + for hl in ['culhl', 'linehl', 'numhl', 'texthl'] + call feedkeys(":sign define Sign "..hl.."=Spell\<C-A>\<C-B>\"\<CR>", 'tx') + call assert_equal('"sign define Sign '..hl..'=SpellBad SpellCap ' . + \ 'SpellLocal SpellRare', @:) + endfor call writefile(repeat(["Sun is shining"], 30), "XsignOne") call writefile(repeat(["Sky is blue"], 30), "XsignTwo") @@ -417,20 +415,21 @@ func Test_sign_funcs() " Tests for sign_define() let attr = {'text' : '=>', 'linehl' : 'Search', 'texthl' : 'Error', - \ 'culhl': 'Visual'} + \ 'culhl': 'Visual', 'numhl': 'Number'} call assert_equal(0, "sign1"->sign_define(attr)) - call assert_equal([{'name' : 'sign1', 'texthl' : 'Error', - \ 'linehl' : 'Search', 'culhl': 'Visual', 'text' : '=>'}], + call assert_equal([{'name' : 'sign1', 'texthl' : 'Error', 'linehl': 'Search', + \ 'culhl': 'Visual', 'numhl': 'Number', 'text' : '=>'}], \ sign_getdefined()) " Define a new sign without attributes and then update it call sign_define("sign2") let attr = {'text' : '!!', 'linehl' : 'DiffAdd', 'texthl' : 'DiffChange', - \ 'culhl': 'DiffDelete', 'icon' : 'sign2.ico'} + \ 'culhl': 'DiffDelete', 'numhl': 'Number', 'icon' : 'sign2.ico'} call Sign_define_ignore_error("sign2", attr) call assert_equal([{'name' : 'sign2', 'texthl' : 'DiffChange', \ 'linehl' : 'DiffAdd', 'culhl' : 'DiffDelete', 'text' : '!!', - \ 'icon' : 'sign2.ico'}], "sign2"->sign_getdefined()) + \ 'numhl': 'Number', 'icon' : 'sign2.ico'}], + \ "sign2"->sign_getdefined()) " Test for a sign name with digits call assert_equal(0, sign_define(0002, {'linehl' : 'StatusLine'})) diff --git a/src/nvim/testdir/test_visual.vim b/src/nvim/testdir/test_visual.vim index dbabdcf427..8344598486 100644 --- a/src/nvim/testdir/test_visual.vim +++ b/src/nvim/testdir/test_visual.vim @@ -144,7 +144,6 @@ endfun " Test Virtual replace mode. func Test_virtual_replace() - throw 'skipped: TODO: ' if exists('&t_kD') let save_t_kD = &t_kD endif @@ -166,7 +165,6 @@ func Test_virtual_replace() \ ], getline(1, 6)) normal G mark a - inoremap <C-D> <Del> exe "normal o0\<C-D>\nabcdefghi\njk\tlmn\n opq\trst\n\<C-D>uvwxyz\n" exe "normal 'ajgR0\<C-D> 1\nA\nBCDEFGHIJ\n\tKL\nMNO\nPQR" . repeat("\<BS>", 29) call assert_equal([' 1', diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index d53208a915..112adfaf2d 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -974,6 +974,40 @@ describe('API', function() eq('hello', nvim('get_option_value', 'makeprg', {})) eq('', nvim('get_option_value', 'makeprg', {scope = 'local'})) end) + + it('clears the local value of an option with nil', function() + -- Set global value + nvim('set_option_value', 'shiftwidth', 42, {}) + eq(42, nvim('get_option_value', 'shiftwidth', {})) + + -- Set local value + nvim('set_option_value', 'shiftwidth', 8, {scope = 'local'}) + eq(8, nvim('get_option_value', 'shiftwidth', {})) + eq(8, nvim('get_option_value', 'shiftwidth', {scope = 'local'})) + eq(42, nvim('get_option_value', 'shiftwidth', {scope = 'global'})) + + -- Clear value without scope + nvim('set_option_value', 'shiftwidth', NIL, {}) + eq(42, nvim('get_option_value', 'shiftwidth', {})) + eq(42, nvim('get_option_value', 'shiftwidth', {scope = 'local'})) + + -- Clear value with explicit scope + nvim('set_option_value', 'shiftwidth', 8, {scope = 'local'}) + nvim('set_option_value', 'shiftwidth', NIL, {scope = 'local'}) + eq(42, nvim('get_option_value', 'shiftwidth', {})) + eq(42, nvim('get_option_value', 'shiftwidth', {scope = 'local'})) + + -- Now try with options with a special "local is unset" value (e.g. 'undolevels') + nvim('set_option_value', 'undolevels', 1000, {}) + eq(1000, nvim('get_option_value', 'undolevels', {scope = 'local'})) + nvim('set_option_value', 'undolevels', NIL, {scope = 'local'}) + eq(-123456, nvim('get_option_value', 'undolevels', {scope = 'local'})) + + nvim('set_option_value', 'autoread', true, {}) + eq(true, nvim('get_option_value', 'autoread', {scope = 'local'})) + nvim('set_option_value', 'autoread', NIL, {scope = 'local'}) + eq(NIL, nvim('get_option_value', 'autoread', {scope = 'local'})) + end) end) describe('nvim_{get,set}_current_buf, nvim_list_bufs', function() diff --git a/test/functional/autocmd/cursormoved_spec.lua b/test/functional/autocmd/cursormoved_spec.lua index d0f46e689b..9641d4b096 100644 --- a/test/functional/autocmd/cursormoved_spec.lua +++ b/test/functional/autocmd/cursormoved_spec.lua @@ -31,4 +31,12 @@ describe('CursorMoved', function() eq({'aaa'}, funcs.nvim_buf_get_lines(eval('g:buf'), 0, -1, true)) eq(0, eval('g:cursormoved')) end) + + it("is not triggered by cursor movement prior to first CursorMoved instantiation", function() + source([[ + let g:cursormoved = 0 + autocmd CursorMoved * let g:cursormoved += 1 + ]]) + eq(0, eval('g:cursormoved')) + end) end) diff --git a/test/functional/legacy/options_spec.lua b/test/functional/legacy/options_spec.lua index 023cdd4ae1..bd14f3bc53 100644 --- a/test/functional/legacy/options_spec.lua +++ b/test/functional/legacy/options_spec.lua @@ -83,4 +83,9 @@ describe('set', function() Press ENTER or type command to continue^ | ]]) end) + + it('foldcolumn and signcolumn to empty string is disallowed', function() + matches('E474: Invalid argument: fdc=', exc_exec('set fdc=')) + matches('E474: Invalid argument: scl=', exc_exec('set scl=')) + end) end) diff --git a/test/functional/lua/spell_spec.lua b/test/functional/lua/spell_spec.lua new file mode 100644 index 0000000000..7e831f16a7 --- /dev/null +++ b/test/functional/lua/spell_spec.lua @@ -0,0 +1,53 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local eq = helpers.eq +local pcall_err = helpers.pcall_err + +describe('vim.spell', function() + before_each(function() + clear() + end) + + describe('.check', function() + local check = function(x, exp) + return eq(exp, exec_lua("return vim.spell.check(...)", x)) + end + + it('can handle nil', function() + eq([[Error executing lua: [string "<nvim>"]:0: bad argument #1 to 'check' (expected string)]], + pcall_err(exec_lua, [[vim.spell.check(nil)]])) + end) + + it('can check spellings', function() + check('hello', {}) + + check( + 'helloi', + {{"helloi", "bad", 1}} + ) + + check( + 'hello therei', + {{"therei", "bad", 7}} + ) + + check( + 'hello. there', + {{"there", "caps", 8}} + ) + + check( + 'neovim cna chkc spellins. okay?', + { + {"neovim" , "bad" , 1}, + {"cna" , "bad" , 8}, + {"chkc" , "bad" , 12}, + {"spellins", "bad" , 17}, + {"okay" , "caps", 27} + } + ) + end) + + end) +end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index b12d4227d5..1af31c38f8 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -301,6 +301,43 @@ describe('LSP', function() } end) + it('should detach buffer in response to nvim_buf_detach', function() + local expected_handlers = { + {NIL, {}, {method="shutdown", client_id=1}}; + {NIL, {}, {method="finish", client_id=1}}; + } + local client + test_rpc_server { + test_name = "basic_finish"; + on_setup = function() + exec_lua [[ + BUFFER = vim.api.nvim_create_buf(false, true) + ]] + eq(true, exec_lua("return lsp.buf_attach_client(BUFFER, TEST_RPC_CLIENT_ID)")) + eq(true, exec_lua("return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)")) + exec_lua [[ + vim.api.nvim_command(BUFFER.."bwipeout") + ]] + end; + on_init = function(_client) + client = _client + client.notify('finish') + end; + on_exit = function(code, signal) + eq(0, code, "exit code", fake_lsp_logfile) + eq(0, signal, "exit signal", fake_lsp_logfile) + end; + on_handler = function(err, result, ctx) + eq(table.remove(expected_handlers), {err, result, ctx}, "expected handler") + if ctx.method == 'finish' then + exec_lua("return lsp.buf_detach_client(BUFFER, TEST_RPC_CLIENT_ID)") + eq(false, exec_lua("return lsp.buf_is_attached(BUFFER, TEST_RPC_CLIENT_ID)")) + client.stop() + end + end; + } + end) + it('client should return settings via workspace/configuration handler', function() local expected_handlers = { {NIL, {}, {method="shutdown", client_id=1}}; diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index aeba049557..4fc5c389e5 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -2213,4 +2213,134 @@ describe('builtin popupmenu', function() feed('<c-y>') assert_alive() end) + + it('truncates double-width character correctly when there is no scrollbar', function() + screen:try_resize(32,8) + command('set completeopt+=menuone,noselect') + feed('i' .. string.rep(' ', 13)) + funcs.complete(14, {'哦哦哦哦哦哦哦哦哦哦'}) + screen:expect([[ + ^ | + {1:~ }{n: 哦哦哦哦哦哦哦哦哦>}| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:-- INSERT --} | + ]]) + end) + + it('truncates double-width character correctly when there is scrollbar', function() + screen:try_resize(32,8) + command('set completeopt+=noselect') + command('set pumheight=4') + feed('i' .. string.rep(' ', 12)) + local items = {} + for _ = 1, 8 do + table.insert(items, {word = '哦哦哦哦哦哦哦哦哦哦', equal = 1, dup = 1}) + end + funcs.complete(13, items) + screen:expect([[ + ^ | + {1:~ }{n: 哦哦哦哦哦哦哦哦哦>}{c: }| + {1:~ }{n: 哦哦哦哦哦哦哦哦哦>}{c: }| + {1:~ }{n: 哦哦哦哦哦哦哦哦哦>}{s: }| + {1:~ }{n: 哦哦哦哦哦哦哦哦哦>}{s: }| + {1:~ }| + {1:~ }| + {2:-- INSERT --} | + ]]) + end) +end) + +describe('builtin popupmenu with ui/ext_multigrid', function() + local screen + before_each(function() + clear() + screen = Screen.new(32, 20) + screen:attach({ext_multigrid=true}) + screen:set_default_attr_ids({ + -- popup selected item / scrollbar track + ['s'] = {background = Screen.colors.WebGray}, + -- popup non-selected item + ['n'] = {background = Screen.colors.LightMagenta}, + -- popup scrollbar knob + ['c'] = {background = Screen.colors.Grey0}, + [1] = {bold = true, foreground = Screen.colors.Blue}, + [2] = {bold = true}, + [3] = {reverse = true}, + [4] = {bold = true, reverse = true}, + [5] = {bold = true, foreground = Screen.colors.SeaGreen}, + [6] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + }) + end) + + it('truncates double-width character correctly when there is no scrollbar', function() + screen:try_resize(32,8) + command('set completeopt+=menuone,noselect') + feed('i' .. string.rep(' ', 13)) + funcs.complete(14, {'哦哦哦哦哦哦哦哦哦哦'}) + screen:expect({grid=[[ + ## grid 1 + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [3:--------------------------------]| + ## grid 2 + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ## grid 3 + {2:-- INSERT --} | + ## grid 4 + {n: 哦哦哦哦哦哦哦哦哦>}| + ]], float_pos={[4] = {{id = -1}, 'NW', 2, 1, 12, false, 100}}}) + end) + + it('truncates double-width character correctly when there is scrollbar', function() + screen:try_resize(32,8) + command('set completeopt+=noselect') + command('set pumheight=4') + feed('i' .. string.rep(' ', 12)) + local items = {} + for _ = 1, 8 do + table.insert(items, {word = '哦哦哦哦哦哦哦哦哦哦', equal = 1, dup = 1}) + end + funcs.complete(13, items) + screen:expect({grid=[[ + ## grid 1 + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [2:--------------------------------]| + [3:--------------------------------]| + ## grid 2 + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ## grid 3 + {2:-- INSERT --} | + ## grid 4 + {n: 哦哦哦哦哦哦哦哦哦>}{c: }| + {n: 哦哦哦哦哦哦哦哦哦>}{c: }| + {n: 哦哦哦哦哦哦哦哦哦>}{s: }| + {n: 哦哦哦哦哦哦哦哦哦>}{s: }| + ]], float_pos={[4] = {{id = -1}, 'NW', 2, 1, 11, false, 100}}}) + end) end) |