aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig3
-rw-r--r--.github/workflows/ci.yml5
-rw-r--r--.github/workflows/squash-typos.yml33
-rw-r--r--CMakeLists.txt4
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--runtime/doc/lsp.txt10
-rw-r--r--runtime/doc/sign.txt5
-rw-r--r--runtime/filetype.vim4
-rw-r--r--runtime/lua/vim/lsp.lua54
-rw-r--r--runtime/lua/vim/lsp/util.lua29
-rw-r--r--scripts/squash_typos.py263
-rw-r--r--src/mpack/lmpack.c3
-rw-r--r--src/nvim/api/vim.c14
-rw-r--r--src/nvim/eval/funcs.c10
-rw-r--r--src/nvim/ex_docmd.c5
-rw-r--r--src/nvim/file_search.c15
-rw-r--r--src/nvim/option.c27
-rw-r--r--src/nvim/option.h1
-rw-r--r--src/nvim/popupmnu.c43
-rw-r--r--src/nvim/screen.c9
-rw-r--r--src/nvim/sign.c3
-rw-r--r--src/nvim/testdir/test_filetype.vim1
-rw-r--r--src/nvim/testdir/test_signs.vim25
-rw-r--r--test/functional/api/vim_spec.lua34
-rw-r--r--test/functional/legacy/options_spec.lua5
-rw-r--r--test/functional/plugin/lsp_spec.lua37
-rw-r--r--test/functional/ui/popupmenu_spec.lua130
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)