diff options
-rw-r--r-- | .editorconfig | 3 | ||||
-rw-r--r-- | .github/workflows/ci.yml | 5 | ||||
-rw-r--r-- | .github/workflows/squash-typos.yml | 33 | ||||
-rw-r--r-- | CMakeLists.txt | 4 | ||||
-rw-r--r-- | CONTRIBUTING.md | 2 | ||||
-rw-r--r-- | runtime/doc/lsp.txt | 10 | ||||
-rw-r--r-- | runtime/doc/sign.txt | 5 | ||||
-rw-r--r-- | runtime/filetype.vim | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 54 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 29 | ||||
-rw-r--r-- | scripts/squash_typos.py | 263 | ||||
-rw-r--r-- | src/mpack/lmpack.c | 3 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 14 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 10 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 5 | ||||
-rw-r--r-- | src/nvim/file_search.c | 15 | ||||
-rw-r--r-- | src/nvim/option.c | 27 | ||||
-rw-r--r-- | src/nvim/option.h | 1 | ||||
-rw-r--r-- | src/nvim/popupmnu.c | 43 | ||||
-rw-r--r-- | src/nvim/screen.c | 9 | ||||
-rw-r--r-- | src/nvim/sign.c | 3 | ||||
-rw-r--r-- | src/nvim/testdir/test_filetype.vim | 1 | ||||
-rw-r--r-- | src/nvim/testdir/test_signs.vim | 25 | ||||
-rw-r--r-- | test/functional/api/vim_spec.lua | 34 | ||||
-rw-r--r-- | test/functional/legacy/options_spec.lua | 5 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 37 | ||||
-rw-r--r-- | test/functional/ui/popupmenu_spec.lua | 130 |
27 files changed, 401 insertions, 373 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 2ca2385d8f..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}) 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/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..8a705abd1c 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 | 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/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_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/option.c b/src/nvim/option.c index 2ceb1bd992..02e7aeb98b 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/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/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index dbe0cd8388..dba7226119 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'], 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/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/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/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) |