diff options
98 files changed, 3660 insertions, 3474 deletions
diff --git a/MAINTAIN.md b/MAINTAIN.md index 4920074e36..f7dd8a1ef7 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -160,7 +160,6 @@ These dependencies are "vendored" (inlined), we must update the sources manually * Needs to be updated when LPeg is updated. * `src/bit.c`: only for PUC lua: port of `require'bit'` from luajit https://bitop.luajit.org/ * `runtime/lua/coxpcall.lua`: coxpcall (only needed for PUC lua, builtin to luajit) -* `src/termkey`: [libtermkey](https://github.com/neovim/libtermkey) Other dependencies -------------------------- diff --git a/cmake.deps/deps.txt b/cmake.deps/deps.txt index 304e3972ff..127cc3c0d9 100644 --- a/cmake.deps/deps.txt +++ b/cmake.deps/deps.txt @@ -1,5 +1,5 @@ -LIBUV_URL https://github.com/libuv/libuv/archive/v1.48.0.tar.gz -LIBUV_SHA256 8c253adb0f800926a6cbd1c6576abae0bc8eb86a4f891049b72f9e5b7dc58f33 +LIBUV_URL https://github.com/libuv/libuv/archive/0a00e80c3686b93eccb9a801954e86bd7d7fe6ab.tar.gz +LIBUV_SHA256 8d240ad56f779ebca94a249b2a2c71725d89182e732cf53c1f6d85098cc9bcb3 LUAJIT_URL https://github.com/LuaJIT/LuaJIT/archive/f725e44cda8f359869bf8f92ce71787ddca45618.tar.gz LUAJIT_SHA256 2b5514bd6a6573cb6111b43d013e952cbaf46762d14ebe26c872ddb80b5a84e0 diff --git a/runtime/autoload/hcl.vim b/runtime/autoload/hcl.vim new file mode 100644 index 0000000000..2215fc8f27 --- /dev/null +++ b/runtime/autoload/hcl.vim @@ -0,0 +1,40 @@ +" Language: HCL +" Maintainer: Gregory Anders +" Last Change: 2024-09-03 +" Based on: https://github.com/hashivim/vim-terraform + +function! hcl#indentexpr(lnum) + " Beginning of the file should have no indent + if a:lnum == 0 + return 0 + endif + + " Usual case is to continue at the same indent as the previous non-blank line. + let prevlnum = prevnonblank(a:lnum-1) + let thisindent = indent(prevlnum) + + " If that previous line is a non-comment ending in [ { (, increase the + " indent level. + let prevline = getline(prevlnum) + if prevline !~# '^\s*\(#\|//\)' && prevline =~# '[\[{\(]\s*$' + let thisindent += &shiftwidth + endif + + " If the current line ends a block, decrease the indent level. + let thisline = getline(a:lnum) + if thisline =~# '^\s*[\)}\]]' + let thisindent -= &shiftwidth + endif + + " If the previous line starts a block comment /*, increase by one + if prevline =~# '/\*' + let thisindent += 1 + endif + + " If the previous line ends a block comment */, decrease by one + if prevline =~# '\*/' + let thisindent -= 1 + endif + + return thisindent +endfunction diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index 7e65ab6108..69e251e657 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -444,6 +444,10 @@ when the right mouse button is pressed, if 'mousemodel' is set to popup or popup_setpos. The default "PopUp" menu is: >vim + anoremenu PopUp.Go\ to\ definition <Cmd>lua vim.lsp.buf.definition()<CR> + amenu PopUp.Open\ in\ web\ browser gx + anoremenu PopUp.Inspect <Cmd>Inspect<CR> + anoremenu PopUp.-1- <Nop> vnoremenu PopUp.Cut "+x vnoremenu PopUp.Copy "+y anoremenu PopUp.Paste "+gP @@ -452,8 +456,7 @@ The default "PopUp" menu is: >vim nnoremenu PopUp.Select\ All ggVG vnoremenu PopUp.Select\ All gg0oG$ inoremenu PopUp.Select\ All <C-Home><C-O>VG - anoremenu PopUp.Inspect <Cmd>Inspect<CR> - anoremenu PopUp.-1- <Nop> + anoremenu PopUp.-2- <Nop> anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR> < diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 0a7c53a482..0beee1507a 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2230,6 +2230,12 @@ vim.tbl_count({t}) *vim.tbl_count()* vim.tbl_deep_extend({behavior}, {...}) *vim.tbl_deep_extend()* Merges recursively two or more tables. + Only values that are empty tables or tables that are not |lua-list|s + (indexed by consecutive integers starting from 1) are merged recursively. + This is useful for merging nested tables like default and user + configurations where lists should be treated as literals (i.e., are + overwritten instead of merged). + Parameters: ~ • {behavior} (`'error'|'keep'|'force'`) Decides what to do if a key is found in more than one map: diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 507559cac7..bc1b9487b7 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -114,6 +114,10 @@ DEFAULTS • |grr| in Normal mode maps to |vim.lsp.buf.references()| • |gra| in Normal and Visual mode maps to |vim.lsp.buf.code_action()| • CTRL-S in Insert mode maps to |vim.lsp.buf.signature_help()| + • Mouse |popup-menu| includes an "Open in web browser" item when you right-click + on a URL. + • Mouse |popup-menu| includes a "Go to definition" item when LSP is active + in the buffer. • Snippet: • `<Tab>` in Insert and Select mode maps to `vim.snippet.jump({ direction = 1 })` @@ -210,9 +214,6 @@ These existing features changed their behavior. more emoji characters than before, including those encoded with multiple emoji codepoints combined with ZWJ (zero width joiner) codepoints. -• |vim.tbl_deep_extend()| no longer ignores any values for which |vim.isarray()| - returns `true`. - ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 4945a1b46d..3693f46790 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1129,9 +1129,9 @@ A jump table for the options with a short description can be found at |Q_op|. list:{n} Adds an additional indent for lines that match a numbered or bulleted list (using the 'formatlistpat' setting). - list:-1 Uses the length of a match with 'formatlistpat' - for indentation. (default: 0) + list:-1 Uses the width of a match with 'formatlistpat' for + indentation. column:{n} Indent at column {n}. Will overrule the other sub-options. Note: an additional indent may be added for the 'showbreak' setting. diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index d7f955a654..b2e8476fd7 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -106,23 +106,28 @@ standard actions ("Cut", "Copy", "Paste", …). Mouse is NOT enabled in |command-mode| or the |more-prompt|, so you can temporarily disable it just by typing ":". -If you don't like this you can disable the mouse in your |config| using any of -the following: +Or you can disable the popup-menu using any of the following: - Disable mouse completely by unsetting the 'mouse' option: >vim set mouse= -- Pressing <RightMouse> extends selection instead of showing popup-menu: >vim +- Change the 'mousemodel', so <RightMouse> extends selection instead of + showing the popup-menu: >vim set mousemodel=extend -- Pressing <A-LeftMouse> releases mouse until the cursor moves: >vim +- Map <A-LeftMouse> so that it temporarily disables mouse until the cursor + moves: >vim nnoremap <A-LeftMouse> <Cmd> \ set mouse=<Bar> \ echo 'mouse OFF until next cursor-move'<Bar> \ autocmd CursorMoved * ++once set mouse&<Bar> \ echo 'mouse ON'<CR> < -To remove the "How-to disable mouse" menu item and the separator above it: >vim +To remove the default popup-menu without disabling mouse: >vim + aunmenu PopUp + autocmd! nvim_popupmenu + +To remove only the "How-to disable mouse" menu item (and its separator): >vim aunmenu PopUp.How-to\ disable\ mouse - aunmenu PopUp.-1- -< + aunmenu PopUp.-2- + DEFAULT MAPPINGS *default-mappings* Nvim creates the following default mappings at |startup|. You can disable any diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt index 840d8b51af..35e5506efa 100644 --- a/runtime/doc/windows.txt +++ b/runtime/doc/windows.txt @@ -53,11 +53,17 @@ active yes yes 'a' hidden no yes 'h' inactive no no ' ' -Note: All CTRL-W commands can also be executed with |:wincmd|, for those -places where a Normal mode command can't be used or is inconvenient. + *buffer-reuse* +Each buffer has a unique number and the number will not change within a Vim +session. The |bufnr()| and |bufname()| functions can be used to convert +between a buffer name and the buffer number. There is one exception: if a new +empty buffer is created and it is not modified, the buffer will be re-used +when loading another file into that buffer. This also means the buffer number +will not change. The main Vim window can hold several split windows. There are also tab pages |tab-page|, each of which can hold multiple windows. + *window-ID* *winid* *windowid* Each window has a unique identifier called the window ID. This identifier will not change within a Vim session. The |win_getid()| and |win_id2tabwin()| @@ -69,9 +75,6 @@ across tabs. For most functions that take a window ID or a window number, the window number only applies to the current tab, while the window ID can refer to a window in any tab. -Each buffer has a unique number and the number will not change within a Vim -session. The |bufnr()| and |bufname()| functions can be used to convert -between a buffer name and the buffer number. ============================================================================== 2. Starting Vim *windows-starting* @@ -468,6 +471,10 @@ These commands can also be executed with ":wincmd": :exe nr .. "wincmd w" < This goes to window "nr". +Note: All CTRL-W commands can also be executed with |:wincmd|, for those +places where a Normal mode command can't be used or is inconvenient (e.g. +in a browser-based terminal). + ============================================================================== 5. Moving windows around *window-moving* diff --git a/runtime/ftplugin/dot.lua b/runtime/ftplugin/dot.lua new file mode 100644 index 0000000000..89ab42ef54 --- /dev/null +++ b/runtime/ftplugin/dot.lua @@ -0,0 +1,3 @@ +vim.bo.commentstring = '// %s' + +vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') .. '\n setl commentstring<' diff --git a/runtime/ftplugin/faust.lua b/runtime/ftplugin/faust.lua new file mode 100644 index 0000000000..89ab42ef54 --- /dev/null +++ b/runtime/ftplugin/faust.lua @@ -0,0 +1,3 @@ +vim.bo.commentstring = '// %s' + +vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') .. '\n setl commentstring<' diff --git a/runtime/ftplugin/hcl.vim b/runtime/ftplugin/hcl.vim new file mode 100644 index 0000000000..c47a0378d0 --- /dev/null +++ b/runtime/ftplugin/hcl.vim @@ -0,0 +1,10 @@ +" Vim filetype plugin +" Language: HCL +" Maintainer: Gregory Anders +" Last Change: 2024-09-03 + +if exists('b:did_ftplugin') + finish +endif + +runtime! ftplugin/terraform.vim diff --git a/runtime/ftplugin/stata.lua b/runtime/ftplugin/stata.lua new file mode 100644 index 0000000000..89ab42ef54 --- /dev/null +++ b/runtime/ftplugin/stata.lua @@ -0,0 +1,3 @@ +vim.bo.commentstring = '// %s' + +vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') .. '\n setl commentstring<' diff --git a/runtime/ftplugin/supercollider.lua b/runtime/ftplugin/supercollider.lua new file mode 100644 index 0000000000..89ab42ef54 --- /dev/null +++ b/runtime/ftplugin/supercollider.lua @@ -0,0 +1,3 @@ +vim.bo.commentstring = '// %s' + +vim.b.undo_ftplugin = (vim.b.undo_ftplugin or '') .. '\n setl commentstring<' diff --git a/runtime/ftplugin/swift.lua b/runtime/ftplugin/swift.lua new file mode 100644 index 0000000000..0f90644f11 --- /dev/null +++ b/runtime/ftplugin/swift.lua @@ -0,0 +1,3 @@ +vim.bo.commentstring = '// %s' + +vim.b.undo_ftplugin = vim.b.undo_ftplugin .. ' | setl commentstring<' diff --git a/runtime/indent/hcl.vim b/runtime/indent/hcl.vim new file mode 100644 index 0000000000..b13d2c5649 --- /dev/null +++ b/runtime/indent/hcl.vim @@ -0,0 +1,16 @@ +" Vim indent file +" Language: HCL +" Maintainer: Gregory Anders +" Upstream: https://github.com/hashivim/vim-terraform +" Last Change: 2024-09-03 + +if exists('b:did_indent') + finish +endif +let b:did_indent = 1 + +setlocal autoindent shiftwidth=2 tabstop=2 softtabstop=2 expandtab +setlocal indentexpr=hcl#indentexpr(v:lnum) +setlocal indentkeys+=<:>,0=},0=) + +let b:undo_indent = 'setlocal ai< sw< ts< sts< et< inde< indk<' diff --git a/runtime/indent/terraform.vim b/runtime/indent/terraform.vim new file mode 100644 index 0000000000..d62813d6a2 --- /dev/null +++ b/runtime/indent/terraform.vim @@ -0,0 +1,11 @@ +" Vim indent file +" Language: Terraform +" Maintainer: Gregory Anders +" Upstream: https://github.com/hashivim/vim-terraform +" Last Change: 2024-09-03 + +if exists('b:did_indent') + finish +endif + +runtime! indent/hcl.vim diff --git a/runtime/lua/tohtml.lua b/runtime/lua/tohtml.lua index 6a5bd6de9d..ed42b28725 100644 --- a/runtime/lua/tohtml.lua +++ b/runtime/lua/tohtml.lua @@ -1293,9 +1293,25 @@ local function opt_to_global_state(opt, title) local fonts = {} if opt.font then fonts = type(opt.font) == 'string' and { opt.font } or opt.font --[[@as (string[])]] + for i, v in pairs(fonts) do + fonts[i] = ('"%s"'):format(v) + end elseif vim.o.guifont:match('^[^:]+') then - table.insert(fonts, vim.o.guifont:match('^[^:]+')) + -- Example: + -- Input: "Font,Escape\,comma, Ignore space after comma" + -- Output: { "Font","Escape,comma","Ignore space after comma" } + local prev = '' + for name in vim.gsplit(vim.o.guifont:match('^[^:]+'), ',', { trimempty = true }) do + if vim.endswith(name, '\\') then + prev = prev .. vim.trim(name:sub(1, -2) .. ',') + elseif vim.trim(name) ~= '' then + table.insert(fonts, ('"%s%s"'):format(prev, vim.trim(name))) + prev = '' + end + end end + -- Generic family names (monospace here) must not be quoted + -- because the browser recognizes them as font families. table.insert(fonts, 'monospace') --- @type vim.tohtml.state.global local state = { diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 38cfdbdd32..911cc13e74 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -213,20 +213,48 @@ end --- Default menus do --- Right click popup menu - -- TODO VimScript, no l10n - vim.cmd([[ - vnoremenu PopUp.Cut "+x - vnoremenu PopUp.Copy "+y - anoremenu PopUp.Paste "+gP - vnoremenu PopUp.Paste "+P - vnoremenu PopUp.Delete "_x - nnoremenu PopUp.Select\ All ggVG - vnoremenu PopUp.Select\ All gg0oG$ - inoremenu PopUp.Select\ All <C-Home><C-O>VG - anoremenu PopUp.Inspect <Cmd>Inspect<CR> - anoremenu PopUp.-1- <Nop> - anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR> - ]]) + local function def_menu(ctx) + vim.cmd([[ + anoremenu PopUp.Go\ to\ definition <Cmd>lua vim.lsp.buf.definition()<CR> + amenu PopUp.Open\ in\ web\ browser gx + anoremenu PopUp.Inspect <Cmd>Inspect<CR> + anoremenu PopUp.-1- <Nop> + vnoremenu PopUp.Cut "+x + vnoremenu PopUp.Copy "+y + anoremenu PopUp.Paste "+gP + vnoremenu PopUp.Paste "+P + vnoremenu PopUp.Delete "_x + nnoremenu PopUp.Select\ All ggVG + vnoremenu PopUp.Select\ All gg0oG$ + inoremenu PopUp.Select\ All <C-Home><C-O>VG + anoremenu PopUp.-2- <Nop> + anoremenu PopUp.How-to\ disable\ mouse <Cmd>help disable-mouse<CR> + + amenu disable PopUp.Go\ to\ definition + amenu disable PopUp.Open\ in\ web\ browser + ]]) + + if ctx == 'url' then + vim.cmd([[amenu enable PopUp.Open\ in\ web\ browser]]) + elseif ctx == 'lsp' then + vim.cmd([[anoremenu enable PopUp.Go\ to\ definition]]) + end + end + def_menu() + + local nvim_popupmenu_augroup = vim.api.nvim_create_augroup('nvim_popupmenu', {}) + vim.api.nvim_create_autocmd('MenuPopup', { + pattern = '*', + group = nvim_popupmenu_augroup, + desc = 'Mouse popup menu', + -- nested = true, + callback = function() + local urls = require('vim.ui')._get_urls() + local url = vim.startswith(urls[1], 'http') + local ctx = url and 'url' or (vim.lsp.get_clients({ bufnr = 0 })[1] and 'lsp' or nil) + def_menu(ctx) + end, + }) end --- Default autocommands. See |default-autocmds| diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 05c9b89d77..787e661402 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -558,9 +558,9 @@ vim.wo.bri = vim.wo.breakindent --- list:{n} Adds an additional indent for lines that match a --- numbered or bulleted list (using the --- 'formatlistpat' setting). ---- list:-1 Uses the length of a match with 'formatlistpat' ---- for indentation. --- (default: 0) +--- list:-1 Uses the width of a match with 'formatlistpat' for +--- indentation. --- column:{n} Indent at column {n}. Will overrule the other --- sub-options. Note: an additional indent may be --- added for the 'showbreak' setting. diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index 1e224d1bef..4483b083de 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -77,12 +77,7 @@ function M.on_inlayhint(err, result, ctx, _) local col = position.character if col > 0 then local line = lines[position.line + 1] or '' - local ok, convert_result - ok, convert_result = pcall(util._str_byteindex_enc, line, col, client.offset_encoding) - if ok then - return convert_result - end - return math.min(#line, col) + return util._str_byteindex_enc(line, col, client.offset_encoding) end return col end diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 2ae86851d1..8182457dd0 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -140,12 +140,7 @@ local function tokens_to_ranges(data, bufnr, client, request) local function _get_byte_pos(col) if col > 0 then local buf_line = lines[line + 1] or '' - local ok, result - ok, result = pcall(util._str_byteindex_enc, buf_line, col, client.offset_encoding) - if ok then - return result - end - return math.min(#buf_line, col) + return util._str_byteindex_enc(buf_line, col, client.offset_encoding) end return col end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index cbbe3a66b7..09b97ac861 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -119,6 +119,7 @@ end ---@param encoding string|nil utf-8|utf-16|utf-32|nil defaults to utf-16 ---@return integer `encoding` index of `index` in `line` function M._str_utfindex_enc(line, index, encoding) + local len32, len16 = vim.str_utfindex(line) if not encoding then encoding = 'utf-16' end @@ -129,9 +130,15 @@ function M._str_utfindex_enc(line, index, encoding) return #line end elseif encoding == 'utf-16' then + if not index or index > len16 then + return len16 + end local _, col16 = vim.str_utfindex(line, index) return col16 elseif encoding == 'utf-32' then + if not index or index > len32 then + return len32 + end local col32, _ = vim.str_utfindex(line, index) return col32 else @@ -147,6 +154,12 @@ end ---@param encoding string utf-8|utf-16|utf-32| defaults to utf-16 ---@return integer byte (utf-8) index of `encoding` index `index` in `line` function M._str_byteindex_enc(line, index, encoding) + local len = vim.fn.strlen(line) + if index > len then + -- LSP spec: if character > line length, default to the line length. + -- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position + return len + end if not encoding then encoding = 'utf-16' end @@ -165,9 +178,6 @@ function M._str_byteindex_enc(line, index, encoding) end end -local _str_utfindex_enc = M._str_utfindex_enc -local _str_byteindex_enc = M._str_byteindex_enc - --- Replaces text in a range with new text. --- --- CAUTION: Changes in-place! @@ -334,12 +344,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- character if col > 0 then local line = get_line(bufnr, position.line) or '' - local ok, result - ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) - if ok then - return result - end - return math.min(#line, col) + return M._str_byteindex_enc(line, col, offset_encoding or 'utf-16') end return col end @@ -436,14 +441,15 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) e.end_col = last_line_len has_eol_text_edit = true else - -- If the replacement is over the end of a line (i.e. e.end_col is out of bounds and the + -- If the replacement is over the end of a line (i.e. e.end_col is equal to the line length and the -- replacement text ends with a newline We can likely assume that the replacement is assumed -- to be meant to replace the newline with another newline and we need to make sure this -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r' -- in the file some servers (clangd on windows) will include that character in the line -- while nvim_buf_set_text doesn't count it as part of the line. if - e.end_col > last_line_len + e.end_col >= last_line_len + and text_edit.range['end'].character > e.end_col and #text_edit.newText > 0 and string.sub(text_edit.newText, -1) == '\n' then @@ -1795,8 +1801,10 @@ function M.locations_to_items(locations, offset_encoding) local row = pos.line local end_row = end_pos.line local line = lines[row] or '' + local end_line = lines[end_row] or '' local col = M._str_byteindex_enc(line, pos.character, offset_encoding) - local end_col = M._str_byteindex_enc(lines[end_row] or '', end_pos.character, offset_encoding) + local end_col = M._str_byteindex_enc(end_line, end_pos.character, offset_encoding) + table.insert(items, { filename = filename, lnum = row + 1, @@ -1925,7 +1933,7 @@ local function make_position_param(window, offset_encoding) return { line = 0, character = 0 } end - col = _str_utfindex_enc(line, col, offset_encoding) + col = M._str_utfindex_enc(line, col, offset_encoding) return { line = row, character = col } end @@ -2107,11 +2115,7 @@ function M.character_offset(buf, row, col, offset_encoding) ) offset_encoding = vim.lsp.get_clients({ bufnr = buf })[1].offset_encoding end - -- If the col is past the EOL, use the line length. - if col > #line then - return _str_utfindex_enc(line, nil, offset_encoding) - end - return _str_utfindex_enc(line, col, offset_encoding) + return M._str_utfindex_enc(line, col, offset_encoding) end --- Helper function to return nested values in language server settings diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 2f10380bad..4d06cdd77d 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -354,6 +354,12 @@ function vim.tbl_isempty(t) return next(t) == nil end +--- We only merge empty tables or tables that are not list-like (indexed by consecutive integers +--- starting from 1) +local function can_merge(v) + return type(v) == 'table' and (vim.tbl_isempty(v) or not vim.islist(v)) +end + --- Recursive worker for tbl_extend --- @param behavior 'error'|'keep'|'force' --- @param deep_extend boolean @@ -368,7 +374,7 @@ local function tbl_extend_rec(behavior, deep_extend, ...) local tbl = select(i, ...) --[[@as table<any,any>]] if tbl then for k, v in pairs(tbl) do - if deep_extend and type(v) == 'table' and type(ret[k]) == 'table' then + if deep_extend and can_merge(v) and can_merge(ret[k]) then ret[k] = tbl_extend_rec(behavior, true, ret[k], v) elseif behavior ~= 'force' and ret[k] ~= nil then if behavior == 'error' then @@ -421,6 +427,11 @@ end --- Merges recursively two or more tables. --- +--- Only values that are empty tables or tables that are not |lua-list|s (indexed by consecutive +--- integers starting from 1) are merged recursively. This is useful for merging nested tables +--- like default and user configurations where lists should be treated as literals (i.e., are +--- overwritten instead of merged). +--- ---@see |vim.tbl_extend()| --- ---@generic T1: table diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 2d3b828ea1..3617e1d702 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -152,14 +152,14 @@ function M.open(path) else return nil, 'vim.ui.open: rundll32 not found' end - elseif vim.fn.executable('wslview') == 1 then - cmd = { 'wslview', path } - elseif vim.fn.executable('explorer.exe') == 1 then - cmd = { 'explorer.exe', path } elseif vim.fn.executable('xdg-open') == 1 then cmd = { 'xdg-open', path } opts.stdout = false opts.stderr = false + elseif vim.fn.executable('wslview') == 1 then + cmd = { 'wslview', path } + elseif vim.fn.executable('explorer.exe') == 1 then + cmd = { 'explorer.exe', path } else return nil, 'vim.ui.open: no handler found (tried: wslview, explorer.exe, xdg-open)' end diff --git a/runtime/syntax/hcl.vim b/runtime/syntax/hcl.vim new file mode 100644 index 0000000000..5e9349ab38 --- /dev/null +++ b/runtime/syntax/hcl.vim @@ -0,0 +1,66 @@ +" Vim syntax file +" Language: HCL +" Maintainer: Gregory Anders +" Upstream: https://github.com/hashivim/vim-terraform +" Last Change: 2024-09-03 + +if exists('b:current_syntax') + finish +endif + +syn iskeyword a-z,A-Z,48-57,_,- + +syn case match + +" A block is introduced by a type, some number of labels - which are either +" strings or identifiers - and an opening curly brace. Match the type. +syn match hclBlockType /^\s*\zs\K\k*\ze\s\+\(\("\K\k*"\|\K\k*\)\s\+\)*{/ + +" An attribute name is an identifier followed by an equals sign. +syn match hclAttributeAssignment /\(\K\k*\.\)*\K\k*\s\+=\s/ contains=hclAttributeName +syn match hclAttributeName /\<\K\k*\>/ contained + +syn keyword hclValueBool true false + +syn keyword hclTodo contained TODO FIXME XXX BUG +syn region hclComment start="/\*" end="\*/" contains=hclTodo,@Spell +syn region hclComment start="#" end="$" contains=hclTodo,@Spell +syn region hclComment start="//" end="$" contains=hclTodo,@Spell + +""" misc. +syn match hclValueDec "\<[0-9]\+\([kKmMgG]b\?\)\?\>" +syn match hclValueHexaDec "\<0x[0-9a-f]\+\([kKmMgG]b\?\)\?\>" +syn match hclBraces "[\[\]]" + +""" skip \" and \\ in strings. +syn region hclValueString start=/"/ skip=/\\\\\|\\"/ end=/"/ contains=hclStringInterp +syn region hclStringInterp matchgroup=hclBraces start=/\(^\|[^$]\)\$\zs{/ end=/}/ contained contains=ALLBUT,hclAttributeName +syn region hclHereDocText start=/<<-\?\z([a-z0-9A-Z]\+\)/ end=/^\s*\z1/ contains=hclStringInterp + +"" Functions. +syn match hclFunction "[a-z0-9]\+(\@=" + +""" HCL2 +syn keyword hclRepeat for in +syn keyword hclConditional if +syn keyword hclValueNull null + +" enable block folding +syn region hclBlockBody matchgroup=hclBraces start="{" end="}" fold transparent + +hi def link hclComment Comment +hi def link hclTodo Todo +hi def link hclBraces Delimiter +hi def link hclAttributeName Identifier +hi def link hclBlockType Type +hi def link hclValueBool Boolean +hi def link hclValueDec Number +hi def link hclValueHexaDec Number +hi def link hclValueString String +hi def link hclHereDocText String +hi def link hclFunction Function +hi def link hclRepeat Repeat +hi def link hclConditional Conditional +hi def link hclValueNull Constant + +let b:current_syntax = 'hcl' diff --git a/runtime/syntax/terraform.vim b/runtime/syntax/terraform.vim new file mode 100644 index 0000000000..559dc79568 --- /dev/null +++ b/runtime/syntax/terraform.vim @@ -0,0 +1,17 @@ +" Vim syntax file +" Language: Terraform +" Maintainer: Gregory Anders +" Upstream: https://github.com/hashivim/vim-terraform +" Last Change: 2024-09-03 + +if exists('b:current_syntax') + finish +endif + +runtime! syntax/hcl.vim + +syn keyword terraType string bool number object tuple list map set any + +hi def link terraType Type + +let b:current_syntax = 'terraform' diff --git a/runtime/syntax/tmux.vim b/runtime/syntax/tmux.vim index 80636f2c58..4b8454dd51 100644 --- a/runtime/syntax/tmux.vim +++ b/runtime/syntax/tmux.vim @@ -1,5 +1,5 @@ " Language: tmux(1) configuration file -" Version: 3.4 (git-171004df) +" Version: 3.4 (git-3d8ead8a) " URL: https://github.com/ericpruitt/tmux.vim/ " Maintainer: Eric Pruitt <eric.pruitt@gmail.com> " License: 2-Clause BSD (http://opensource.org/licenses/BSD-2-Clause) @@ -37,7 +37,7 @@ syn match tmuxInvalidVariableExpansion /\${[^}]*$/ display syn match tmuxInvalidVariableExpansion /\${[^A-Za-z_][^}]*}/ display syn match tmuxInvalidVariableExpansion /\$[^A-Za-z_{ \t]/ display " Contains invalid character. -syn match tmuxInvalidVariableExpansion /\${[^}]*[^A-Za-z0-9_][^}]*}/ display +syn match tmuxInvalidVariableExpansion /\${[^}]*[^A-Za-z0-9_}][^}]*}/ display syn region tmuxComment start=/#/ skip=/\\\@<!\\$/ end=/$/ contains=tmuxTodo,@Spell diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 1b229c1d87..ab08f01e33 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -54,8 +54,6 @@ if(ENABLE_WASMTIME) target_compile_definitions(nvim_bin PRIVATE HAVE_WASMTIME) endif() -target_compile_definitions(main_lib INTERFACE HAVE_UNIBILIUM) - # The unit test lib requires LuaJIT; it will be skipped if LuaJIT is missing. option(PREFER_LUA "Prefer Lua over LuaJIT in the nvim executable." OFF) if(PREFER_LUA) @@ -153,7 +151,7 @@ if(UNIX) endif() if(CMAKE_SYSTEM_NAME MATCHES "Windows") - target_compile_definitions(main_lib INTERFACE _WIN32_WINNT=0x0602 MSWIN) + target_compile_definitions(main_lib INTERFACE _WIN32_WINNT=0x0602 MSWIN WIN32_LEAN_AND_MEAN) target_link_libraries(main_lib INTERFACE netapi32) elseif(CMAKE_SYSTEM_NAME MATCHES "Darwin") target_link_libraries(nvim_bin PRIVATE "-framework CoreServices") @@ -366,8 +364,8 @@ file(MAKE_DIRECTORY ${TOUCHES_DIR} ${GENERATED_DIR} ${GENERATED_INCLUDES_DIR}) file(GLOB NVIM_SOURCES CONFIGURE_DEPENDS *.c) file(GLOB NVIM_HEADERS CONFIGURE_DEPENDS *.h) -file(GLOB EXTERNAL_SOURCES CONFIGURE_DEPENDS ../xdiff/*.c ../mpack/*.c ../cjson/*.c ../klib/*.c ../termkey/*.c ../vterm/*.c) -file(GLOB EXTERNAL_HEADERS CONFIGURE_DEPENDS ../xdiff/*.h ../mpack/*.h ../cjson/*.h ../klib/*.h ../termkey/*.h ../vterm/*.h) +file(GLOB EXTERNAL_SOURCES CONFIGURE_DEPENDS ../xdiff/*.c ../mpack/*.c ../cjson/*.c ../klib/*.c ../vterm/*.c) +file(GLOB EXTERNAL_HEADERS CONFIGURE_DEPENDS ../xdiff/*.h ../mpack/*.h ../cjson/*.h ../klib/*.h ../vterm/*.h) file(GLOB NLUA0_SOURCES CONFIGURE_DEPENDS ../mpack/*.c) @@ -378,6 +376,15 @@ if(PREFER_LUA) target_compile_definitions(main_lib INTERFACE NVIM_VENDOR_BIT) endif() +# Inlined external projects, we don't maintain it. #9306 +if(MSVC) + set_source_files_properties( + ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-wd4090;-wd4244;-wd4267") +else() + set_source_files_properties( + ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-Wno-conversion;-Wno-missing-noreturn;-Wno-missing-format-attribute;-Wno-double-promotion;-Wno-strict-prototypes;-Wno-misleading-indentation;-Wno-sign-compare;-Wno-implicit-fallthrough;-Wno-missing-prototypes;-Wno-missing-field-initializers") +endif() + list(APPEND NLUA0_SOURCES ${PROJECT_SOURCE_DIR}/src/nlua0.c) foreach(subdir @@ -386,6 +393,7 @@ foreach(subdir api/private msgpack_rpc tui + tui/termkey event eval lua @@ -407,49 +415,36 @@ endforeach() list(SORT NVIM_SOURCES) list(SORT NVIM_HEADERS) -list(APPEND LINT_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS}) - foreach(sfile ${NVIM_SOURCES}) get_filename_component(f ${sfile} NAME) if(WIN32 AND ${f} MATCHES "^(pty_process_unix.c)$") - list(APPEND to_remove ${sfile}) + list(REMOVE_ITEM NVIM_SOURCES ${sfile}) endif() if(NOT WIN32 AND ${f} MATCHES "^(pty_process_win.c)$") - list(APPEND to_remove ${sfile}) + list(REMOVE_ITEM NVIM_SOURCES ${sfile}) endif() if(NOT WIN32 AND ${f} MATCHES "^(pty_conpty_win.c)$") - list(APPEND to_remove ${sfile}) + list(REMOVE_ITEM NVIM_SOURCES ${sfile}) endif() if(NOT WIN32 AND ${f} MATCHES "^(os_win_console.c)$") - list(APPEND to_remove ${sfile}) + list(REMOVE_ITEM NVIM_SOURCES ${sfile}) endif() endforeach() -list(REMOVE_ITEM NVIM_SOURCES ${to_remove}) - foreach(hfile ${NVIM_HEADERS}) get_filename_component(f ${hfile} NAME) if(WIN32 AND ${f} MATCHES "^(unix_defs.h)$") - list(APPEND to_remove_h ${hfile}) + list(REMOVE_ITEM NVIM_HEADERS ${hfile}) endif() if(WIN32 AND ${f} MATCHES "^(pty_process_unix.h)$") - list(APPEND to_remove_h ${hfile}) + list(REMOVE_ITEM NVIM_HEADERS ${hfile}) endif() if(NOT WIN32 AND ${f} MATCHES "^(win_defs.h)$") - list(APPEND to_remove_h ${hfile}) + list(REMOVE_ITEM NVIM_HEADERS ${hfile}) endif() endforeach() -list(REMOVE_ITEM NVIM_HEADERS ${to_remove_h}) - -# xdiff, mpack, lua-cjson, termkey: inlined external project, we don't maintain it. #9306 -if(MSVC) - set_source_files_properties( - ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-wd4090;-wd4244;-wd4267") -else() - set_source_files_properties( - ${EXTERNAL_SOURCES} PROPERTIES COMPILE_OPTIONS "-Wno-conversion;-Wno-missing-noreturn;-Wno-missing-format-attribute;-Wno-double-promotion;-Wno-strict-prototypes;-Wno-misleading-indentation;-Wno-sign-compare;-Wno-implicit-fallthrough;-Wno-missing-prototypes;-Wno-missing-field-initializers") -endif() +list(APPEND LINT_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS}) # Log level (NVIM_LOG_DEBUG in log.h) if(CI_BUILD) @@ -849,7 +844,7 @@ endif() add_glob_target( TARGET lintc-clang-tidy COMMAND ${CLANG_TIDY_PRG} - FILES ${NVIM_SOURCES} ${NVIM_HEADERS} + FILES ${LINT_NVIM_SOURCES} FLAGS --quiet EXCLUDE ${EXCLUDE_CLANG_TIDY}) @@ -862,7 +857,7 @@ endif() add_glob_target( TARGET clang-analyzer COMMAND ${CLANG_TIDY_PRG} - FILES ${NVIM_SOURCES} ${NVIM_HEADERS} + FILES ${LINT_NVIM_SOURCES} FLAGS --quiet --checks=' -*, @@ -905,13 +900,13 @@ add_glob_target( TARGET lintc-uncrustify COMMAND ${UNCRUSTIFY_PRG} FLAGS -c ${UNCRUSTIFY_CONFIG} -q --check - FILES ${LINT_NVIM_SOURCES}) + FILES ${NVIM_SOURCES} ${NVIM_HEADERS}) add_glob_target( TARGET formatc COMMAND ${UNCRUSTIFY_PRG} FLAGS -c ${UNCRUSTIFY_CONFIG} --replace --no-backup - FILES ${LINT_NVIM_SOURCES}) + FILES ${NVIM_SOURCES} ${NVIM_HEADERS}) add_dependencies(lintc-uncrustify uncrustify_update_config) add_dependencies(formatc uncrustify_update_config) diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 7a4aefc620..630c534a7f 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -28,6 +28,7 @@ #include "nvim/cursor.h" #include "nvim/decoration.h" #include "nvim/drawscreen.h" +#include "nvim/edit.h" #include "nvim/errors.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" diff --git a/src/nvim/change.c b/src/nvim/change.c index 47a9f0ce92..51a13b80e7 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -756,10 +756,8 @@ void ins_char_bytes(char *buf, size_t charlen) // put back when BS is used. The bytes of a multi-byte character are // done the other way around, so that the first byte is popped off // first (it tells the byte length of the character). - replace_push(NUL); - for (size_t i = 0; i < oldlen; i++) { - i += (size_t)replace_push_mb(oldp + col + i) - 1; - } + replace_push_nul(); + replace_push(oldp + col, oldlen); } char *newp = xmalloc(linelen + newlen - oldlen); @@ -1137,12 +1135,10 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) // on the line onto the replace stack. We'll push any other characters // that might be replaced at the start of the next line (due to // autoindent etc) a bit later. - replace_push(NUL); // Call twice because BS over NL expects it - replace_push(NUL); + replace_push_nul(); // Call twice because BS over NL expects it + replace_push_nul(); p = saved_line + curwin->w_cursor.col; - while (*p != NUL) { - p += replace_push_mb(p); - } + replace_push(p, strlen(p)); saved_line[curwin->w_cursor.col] = NUL; } @@ -1691,13 +1687,13 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) // stack, preceded by a NUL, so they can be put back when a BS is // entered. if (REPLACE_NORMAL(State)) { - replace_push(NUL); // end of extra blanks + replace_push_nul(); // end of extra blanks } if (curbuf->b_p_ai || (flags & OPENLINE_DELSPACES)) { while ((*p_extra == ' ' || *p_extra == '\t') && !utf_iscomposing_first(utf_ptr2char(p_extra + 1))) { if (REPLACE_NORMAL(State)) { - replace_push(*p_extra); + replace_push(p_extra, 1); // always ascii, len = 1 } p_extra++; less_cols_off++; @@ -1794,7 +1790,7 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) // must be a NUL on the replace stack, for when it is deleted with BS if (REPLACE_NORMAL(State)) { for (colnr_T n = 0; n < curwin->w_cursor.col; n++) { - replace_push(NUL); + replace_push_nul(); } } newcol += curwin->w_cursor.col; @@ -1808,7 +1804,7 @@ bool open_line(int dir, int flags, int second_line_indent, bool *did_do_comment) // must be a NUL on the replace stack, for when it is deleted with BS. if (REPLACE_NORMAL(State)) { while (lead_len-- > 0) { - replace_push(NUL); + replace_push_nul(); } } diff --git a/src/nvim/edit.c b/src/nvim/edit.c index 64c54b0f37..13623eaa91 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -136,6 +136,8 @@ static TriState dont_sync_undo = kFalse; // CTRL-G U prevents syncing undo static linenr_T o_lnum = 0; +static kvec_t(char) replace_stack = KV_INITIAL_VALUE; + static void insert_enter(InsertState *s) { s->did_backspace = true; @@ -1618,9 +1620,8 @@ void undisplay_dollar(void) /// type == INDENT_SET set indent to "amount" /// /// @param round if true, round the indent to 'shiftwidth' (only with _INC and _Dec). -/// @param replaced replaced character, put on replace stack /// @param call_changed_bytes call changed_bytes() -void change_indent(int type, int amount, int round, int replaced, bool call_changed_bytes) +void change_indent(int type, int amount, int round, bool call_changed_bytes) { int insstart_less; // reduction for Insstart.col colnr_T orig_col = 0; // init for GCC @@ -1767,12 +1768,8 @@ void change_indent(int type, int amount, int round, int replaced, bool call_chan replace_join(0); // remove a NUL from the replace stack start_col--; } - while (start_col < (int)curwin->w_cursor.col || replaced) { - replace_push(NUL); - if (replaced) { - replace_push(replaced); - replaced = NUL; - } + while (start_col < (int)curwin->w_cursor.col) { + replace_push_nul(); start_col++; } } @@ -2325,7 +2322,7 @@ int stop_arrow(void) static void stop_insert(pos_T *end_insert_pos, int esc, int nomove) { stop_redo_ins(); - replace_flush(); // abandon replace stack + kv_destroy(replace_stack); // abandon replace stack (reinitializes) // Save the inserted text for later redo with ^@ and CTRL-A. // Don't do it when "restart_edit" was set and nothing was inserted, @@ -2802,57 +2799,51 @@ static bool echeck_abbr(int c) // that the NL replaced. The extra one stores the characters after the cursor // that were deleted (always white space). -static uint8_t *replace_stack = NULL; -static ssize_t replace_stack_nr = 0; // next entry in replace stack -static ssize_t replace_stack_len = 0; // max. number of entries - /// Push character that is replaced onto the replace stack. /// /// replace_offset is normally 0, in which case replace_push will add a new /// character at the end of the stack. If replace_offset is not 0, that many /// characters will be left on the stack above the newly inserted character. /// -/// @param c character that is replaced (NUL is none) -void replace_push(int c) +/// @param str character that is replaced (NUL is none) +/// @param len length of character in bytes +void replace_push(char *str, size_t len) { - if (replace_stack_nr < replace_offset) { // nothing to do + // TODO(bfredl): replace_offset is suss af, if we don't need it, this + // function is just kv_concat() :p + if (kv_size(replace_stack) < (size_t)replace_offset) { // nothing to do return; } - if (replace_stack_len <= replace_stack_nr) { - replace_stack_len += 50; - replace_stack = xrealloc(replace_stack, (size_t)replace_stack_len); - } - uint8_t *p = replace_stack + replace_stack_nr - replace_offset; + kv_ensure_space(replace_stack, len); + + char *p = replace_stack.items + kv_size(replace_stack) - replace_offset; if (replace_offset) { - memmove(p + 1, p, (size_t)replace_offset); + memmove(p + len, p, (size_t)replace_offset); } - *p = (uint8_t)c; - replace_stack_nr++; + memcpy(p, str, len); + kv_size(replace_stack) += len; } -/// Push a character onto the replace stack. Handles a multi-byte character in -/// reverse byte order, so that the first byte is popped off first. -/// -/// @return the number of bytes done (includes composing characters). -int replace_push_mb(char *p) +/// push NUL as separator between entries in the stack +void replace_push_nul(void) { - int l = utfc_ptr2len(p); - - // TODO(bfredl): stop doing this insantity and instead use utf_head_off() when popping. - // or just keep a secondary array with char byte lenghts - for (int j = l - 1; j >= 0; j--) { - replace_push(p[j]); - } - return l; + replace_push("", 1); } -/// Pop one item from the replace stack. +/// Check top of replace stack, pop it if it was NUL /// -/// @return -1 if stack is empty, replaced character or NUL otherwise -static int replace_pop(void) +/// when a non-NUL byte is found, use mb_replace_pop_ins() to +/// pop one complete multibyte character. +/// +/// @return -1 if stack is empty, last byte of char or NUL otherwise +static int replace_pop_if_nul(void) { - return (replace_stack_nr == 0) ? -1 : (int)replace_stack[--replace_stack_nr]; + int ch = (kv_size(replace_stack)) ? (uint8_t)kv_A(replace_stack, kv_size(replace_stack) - 1) : -1; + if (ch == NUL) { + kv_size(replace_stack)--; + } + return ch; } /// Join the top two items on the replace stack. This removes to "off"'th NUL @@ -2861,11 +2852,11 @@ static int replace_pop(void) /// @param off offset for which NUL to remove static void replace_join(int off) { - for (ssize_t i = replace_stack_nr; --i >= 0;) { - if (replace_stack[i] == NUL && off-- <= 0) { - replace_stack_nr--; - memmove(replace_stack + i, replace_stack + i + 1, - (size_t)(replace_stack_nr - i)); + for (ssize_t i = (ssize_t)kv_size(replace_stack); --i >= 0;) { + if (kv_A(replace_stack, i) == NUL && off-- <= 0) { + kv_size(replace_stack)--; + memmove(&kv_A(replace_stack, i), &kv_A(replace_stack, i + 1), + (kv_size(replace_stack) - (size_t)i)); return; } } @@ -2875,72 +2866,25 @@ static void replace_join(int off) /// before the cursor. Can only be used in MODE_REPLACE or MODE_VREPLACE state. static void replace_pop_ins(void) { - int cc; int oldState = State; State = MODE_NORMAL; // don't want MODE_REPLACE here - while ((cc = replace_pop()) > 0) { - mb_replace_pop_ins(cc); + while ((replace_pop_if_nul()) > 0) { + mb_replace_pop_ins(); dec_cursor(); } State = oldState; } -// Insert bytes popped from the replace stack. "cc" is the first byte. If it -// indicates a multi-byte char, pop the other bytes too. -static void mb_replace_pop_ins(int cc) -{ - int n; - uint8_t buf[MB_MAXBYTES + 1]; - - if ((n = MB_BYTE2LEN(cc)) > 1) { - buf[0] = (uint8_t)cc; - for (int i = 1; i < n; i++) { - buf[i] = (uint8_t)replace_pop(); - } - ins_bytes_len((char *)buf, (size_t)n); - } else { - ins_char(cc); - } - - // Handle composing chars. - while (true) { - int c = replace_pop(); - if (c == -1) { // stack empty - break; - } - if ((n = MB_BYTE2LEN(c)) == 1) { - // Not a multi-byte char, put it back. - replace_push(c); - break; - } - - buf[0] = (uint8_t)c; - assert(n > 1); - for (int i = 1; i < n; i++) { - buf[i] = (uint8_t)replace_pop(); - } - // TODO(bfredl): by fixing replace_push_mb, upgrade to use - // the new composing algorithm - if (utf_iscomposing_legacy(utf_ptr2char((char *)buf))) { - ins_bytes_len((char *)buf, (size_t)n); - } else { - // Not a composing char, put it back. - for (int i = n - 1; i >= 0; i--) { - replace_push(buf[i]); - } - break; - } - } -} - -// make the replace stack empty -// (called when exiting replace mode) -static void replace_flush(void) +/// Insert multibyte char popped from the replace stack. +/// +/// caller must already have checked the top of the stack is not NUL!! +static void mb_replace_pop_ins(void) { - XFREE_CLEAR(replace_stack); - replace_stack_len = 0; - replace_stack_nr = 0; + int len = utf_head_off(&kv_A(replace_stack, 0), + &kv_A(replace_stack, kv_size(replace_stack) - 1)) + 1; + kv_size(replace_stack) -= (size_t)len; + ins_bytes_len(&kv_A(replace_stack, kv_size(replace_stack)), (size_t)len); } // Handle doing a BS for one character. @@ -2955,7 +2899,7 @@ static void replace_do_bs(int limit_col) colnr_T start_vcol; const int l_State = State; - int cc = replace_pop(); + int cc = replace_pop_if_nul(); if (cc > 0) { int orig_len = 0; int orig_vcols = 0; @@ -2969,7 +2913,6 @@ static void replace_do_bs(int limit_col) if (l_State & VREPLACE_FLAG) { orig_len = get_cursor_pos_len(); } - replace_push(cc); replace_pop_ins(); if (l_State & VREPLACE_FLAG) { @@ -3628,9 +3571,9 @@ static void ins_shift(int c, int lastc) if (lastc == '^') { old_indent = get_indent(); // remember curr. indent } - change_indent(INDENT_SET, 0, true, 0, true); + change_indent(INDENT_SET, 0, true, true); } else { - change_indent(c == Ctrl_D ? INDENT_DEC : INDENT_INC, 0, true, 0, true); + change_indent(c == Ctrl_D ? INDENT_DEC : INDENT_INC, 0, true, true); } if (did_ai && *skipwhite(get_cursor_line_ptr()) != NUL) { @@ -3749,7 +3692,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) // cc >= 0: NL was replaced, put original characters back cc = -1; if (State & REPLACE_FLAG) { - cc = replace_pop(); // returns -1 if NL was inserted + cc = replace_pop_if_nul(); // returns -1 if NL was inserted } // In replace mode, in the line we started replacing, we only move the // cursor. @@ -3795,9 +3738,9 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) // restore characters (blanks) deleted after cursor while (cc > 0) { colnr_T save_col = curwin->w_cursor.col; - mb_replace_pop_ins(cc); + mb_replace_pop_ins(); curwin->w_cursor.col = save_col; - cc = replace_pop(); + cc = replace_pop_if_nul(); } // restore the characters that NL replaced replace_pop_ins(); @@ -3906,7 +3849,7 @@ static bool ins_bs(int c, int mode, int *inserted_space_p) } else { ins_str(" "); if ((State & REPLACE_FLAG)) { - replace_push(NUL); + replace_push_nul(); } } } @@ -4316,7 +4259,7 @@ static bool ins_tab(void) } else { ins_str(" "); if (State & REPLACE_FLAG) { // no char replaced - replace_push(NUL); + replace_push_nul(); } } } @@ -4483,7 +4426,7 @@ bool ins_eol(int c) // character under the cursor. Only push a NUL on the replace stack, // nothing to put back when the NL is deleted. if ((State & REPLACE_FLAG) && !(State & VREPLACE_FLAG)) { - replace_push(NUL); + replace_push_nul(); } // In MODE_VREPLACE state, a NL replaces the rest of the line, and starts @@ -4684,7 +4627,7 @@ static void ins_try_si(int c) i = get_indent(); curwin->w_cursor = old_pos; if (State & VREPLACE_FLAG) { - change_indent(INDENT_SET, i, false, NUL, true); + change_indent(INDENT_SET, i, false, true); } else { set_indent(i, SIN_CHANGED); } diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index de1d784577..aed6fdae14 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -7641,7 +7641,7 @@ static void get_xdg_var_list(const XDGVarType xdg, typval_T *rettv) return; } const void *iter = NULL; - const char *appname = get_appname(); + const char *appname = get_appname(false); do { size_t dir_len; const char *dir; diff --git a/src/nvim/event/loop.h b/src/nvim/event/loop.h index 6ecc7cb781..da4852b836 100644 --- a/src/nvim/event/loop.h +++ b/src/nvim/event/loop.h @@ -16,15 +16,14 @@ struct loop { uv_loop_t uv; MultiQueue *events; MultiQueue *thread_events; - // Immediate events: - // "Processed after exiting uv_run() (to avoid recursion), but before - // returning from loop_poll_events()." 502aee690c98 - // Practical consequence (for main_loop): these events are processed by - // state_enter()..os_inchar() - // whereas "regular" events (main_loop.events) are processed by - // state_enter()..VimState.execute() - // But state_enter()..os_inchar() can be "too early" if you want the event - // to trigger UI updates and other user-activity-related side-effects. + // Immediate events. + // - "Processed after exiting `uv_run()` (to avoid recursion), but before returning from + // `loop_poll_events()`." 502aee690c98 + // - Practical consequence (for `main_loop`): + // - these are processed by `state_enter()..input_get()` whereas "regular" events + // (`main_loop.events`) are processed by `state_enter()..VimState.execute()` + // - `state_enter()..input_get()` can be "too early" if you want the event to trigger UI + // updates and other user-activity-related side-effects. MultiQueue *fast_events; // used by process/job-control subsystem diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index 80bc31a1d2..293aaac036 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -2500,15 +2500,13 @@ int parse_command_modifiers(exarg_T *eap, const char **errormsg, cmdmod_T *cmod, // ignore comment and empty lines if (*eap->cmd == '"') { // a comment ends at a NL - if (eap->nextcmd == NULL) { - eap->nextcmd = vim_strchr(eap->cmd, '\n'); - if (eap->nextcmd != NULL) { - eap->nextcmd++; - } + eap->nextcmd = vim_strchr(eap->cmd, '\n'); + if (eap->nextcmd != NULL) { + eap->nextcmd++; } return FAIL; } - if (eap->nextcmd == NULL && *eap->cmd == '\n') { + if (*eap->cmd == '\n') { eap->nextcmd = eap->cmd + 1; return FAIL; } diff --git a/src/nvim/extmark.c b/src/nvim/extmark.c index 4ff298cde5..c20c7dea23 100644 --- a/src/nvim/extmark.c +++ b/src/nvim/extmark.c @@ -70,24 +70,19 @@ void extmark_set(buf_T *buf, uint32_t ns_id, uint32_t *idp, int row, colnr_T col extmark_del_id(buf, ns_id, id); } else { assert(marktree_itr_valid(itr)); - bool invalid = mt_invalid(old_mark); if (old_mark.pos.row == row && old_mark.pos.col == col) { // not paired: we can revise in place - if (!invalid && mt_decor_any(old_mark)) { - // TODO(bfredl): conflict of concerns: buf_decor_remove() must process - // the buffer as if MT_FLAG_DECOR_SIGNTEXT is already removed, however - // marktree must precisely adjust the set of flags from the old set to the new - uint16_t save_flags = mt_itr_rawkey(itr).flags; - mt_itr_rawkey(itr).flags &= (uint16_t) ~MT_FLAG_DECOR_SIGNTEXT; + if (!mt_invalid(old_mark) && mt_decor_any(old_mark)) { + mt_itr_rawkey(itr).flags &= (uint16_t) ~MT_FLAG_EXTERNAL_MASK; buf_decor_remove(buf, row, row, col, mt_decor(old_mark), true); - mt_itr_rawkey(itr).flags = save_flags; } - marktree_revise_flags(buf->b_marktree, itr, flags); + mt_itr_rawkey(itr).flags |= flags; mt_itr_rawkey(itr).decor_data = decor.data; + marktree_revise_meta(buf->b_marktree, itr, old_mark); goto revised; } marktree_del_itr(buf->b_marktree, itr, false); - if (!invalid) { + if (!mt_invalid(old_mark)) { buf_decor_remove(buf, old_mark.pos.row, old_mark.pos.row, old_mark.pos.col, mt_decor(old_mark), true); } @@ -131,6 +126,7 @@ static void extmark_setraw(buf_T *buf, uint64_t mark, int row, colnr_T col, bool int row2 = 0; if (invalid) { mt_itr_rawkey(itr).flags &= (uint16_t) ~MT_FLAG_INVALID; + marktree_revise_meta(buf->b_marktree, itr, key); } else if (move && key.flags & MT_FLAG_DECOR_SIGNTEXT && buf->b_signcols.autom) { MTPos end = marktree_get_altpos(buf->b_marktree, key, NULL); row1 = MIN(end.row, MIN(key.pos.row, row)); @@ -394,6 +390,7 @@ void extmark_splice_delete(buf_T *buf, int l_row, colnr_T l_col, int u_row, coln } else { invalidated = true; mt_itr_rawkey(itr).flags |= MT_FLAG_INVALID; + marktree_revise_meta(buf->b_marktree, itr, mark); buf_decor_remove(buf, mark.pos.row, endpos.row, mark.pos.col, mt_decor(mark), false); } } diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index f6a25a27a6..31a1c0e293 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -3264,18 +3264,12 @@ static void vim_mktempdir(void) char tmp[TEMP_FILE_PATH_MAXLEN]; char path[TEMP_FILE_PATH_MAXLEN]; char user[40] = { 0 }; - char appname[40] = { 0 }; os_get_username(user, sizeof(user)); // Usernames may contain slashes! #19240 memchrsub(user, '/', '_', sizeof(user)); memchrsub(user, '\\', '_', sizeof(user)); - // Appname may be a relative path, replace slashes to make it name-like. - xstrlcpy(appname, get_appname(), sizeof(appname)); - memchrsub(appname, '/', '%', sizeof(appname)); - memchrsub(appname, '\\', '%', sizeof(appname)); - // Make sure the umask doesn't remove the executable bit. // "repl" has been reported to use "0177". mode_t umask_save = umask(0077); @@ -3283,14 +3277,15 @@ static void vim_mktempdir(void) // Expand environment variables, leave room for "/tmp/nvim.<user>/XXXXXX/999999999". expand_env((char *)temp_dirs[i], tmp, TEMP_FILE_PATH_MAXLEN - 64); if (!os_isdir(tmp)) { + if (strequal("$TMPDIR", temp_dirs[i])) { + WLOG("$TMPDIR tempdir not a directory (or does not exist): %s", tmp); + } continue; } // "/tmp/" exists, now try to create "/tmp/nvim.<user>/". add_pathsep(tmp); - - xstrlcat(tmp, appname, sizeof(tmp)); - xstrlcat(tmp, ".", sizeof(tmp)); + xstrlcat(tmp, "nvim.", sizeof(tmp)); xstrlcat(tmp, user, sizeof(tmp)); os_mkdir(tmp, 0700); // Always create, to avoid a race. bool owned = os_file_owned(tmp); diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 7b388e6061..ba0b629896 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -1858,7 +1858,7 @@ static void getchar_common(typval_T *argvars, typval_T *rettv) if (!char_avail()) { // Flush screen updates before blocking. ui_flush(); - os_inchar(NULL, 0, -1, typebuf.tb_change_cnt, main_loop.events); + input_get(NULL, 0, -1, typebuf.tb_change_cnt, main_loop.events); if (!multiqueue_empty(main_loop.events)) { state_handle_k_event(); continue; @@ -2981,7 +2981,7 @@ int inchar(uint8_t *buf, int maxlen, long wait_time) uint8_t dum[DUM_LEN + 1]; while (true) { - len = os_inchar(dum, DUM_LEN, 0, 0, NULL); + len = input_get(dum, DUM_LEN, 0, 0, NULL); if (len == 0 || (len == 1 && dum[0] == Ctrl_C)) { break; } @@ -2997,7 +2997,7 @@ int inchar(uint8_t *buf, int maxlen, long wait_time) // Fill up to a third of the buffer, because each character may be // tripled below. - len = os_inchar(buf, maxlen / 3, (int)wait_time, tb_change_cnt, NULL); + len = input_get(buf, maxlen / 3, (int)wait_time, tb_change_cnt, NULL); } // If the typebuf was changed further down, it is like nothing was added by diff --git a/src/nvim/indent.c b/src/nvim/indent.c index 2f994036ad..b7e3842aad 100644 --- a/src/nvim/indent.c +++ b/src/nvim/indent.c @@ -891,7 +891,17 @@ int get_breakindent_win(win_T *wp, char *line) if (wp->w_briopt_list > 0) { prev_list += wp->w_briopt_list; } else { - prev_indent = (int)(*regmatch.endp - *regmatch.startp); + char *ptr = *regmatch.startp; + char *end_ptr = *regmatch.endp; + int indent = 0; + // Compute the width of the matched text. + // Use win_chartabsize() so that TAB size is correct, + // while wrapping is ignored. + while (ptr < end_ptr) { + indent += win_chartabsize(wp, ptr, indent); + MB_PTR_ADV(ptr); + } + prev_indent = indent; } } vim_regfree(regmatch.regprog); @@ -1407,7 +1417,7 @@ void fixthisline(IndentGetter get_the_indent) return; } - change_indent(INDENT_SET, amount, false, 0, true); + change_indent(INDENT_SET, amount, false, true); if (linewhite(curwin->w_cursor.lnum)) { did_ai = true; // delete the indent if the line stays empty } diff --git a/src/nvim/input.c b/src/nvim/input.c index e14bfe7539..ef400710fe 100644 --- a/src/nvim/input.c +++ b/src/nvim/input.c @@ -37,7 +37,7 @@ /// @param[in] str Prompt: question to ask user. Is always followed by /// " (y/n)?". /// @param[in] direct Determines what function to use to get user input. If -/// true then os_inchar() will be used, otherwise vgetc(). +/// true then input_get() will be used, otherwise vgetc(). /// I.e. when direct is true then characters are obtained /// directly from the user without buffers involved. /// @@ -111,7 +111,7 @@ int get_keystroke(MultiQueue *events) // First time: blocking wait. Second time: wait up to 100ms for a // terminal code to complete. - n = os_inchar(buf + len, maxlen, len == 0 ? -1 : 100, 0, events); + n = input_get(buf + len, maxlen, len == 0 ? -1 : 100, 0, events); if (n > 0) { // Replace zero and K_SPECIAL by a special key code. n = fix_input_buffer(buf + len, n); diff --git a/src/nvim/main.c b/src/nvim/main.c index a45ee81c81..b28667346b 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -266,7 +266,7 @@ int main(int argc, char **argv) if (argc > 1 && STRICMP(argv[1], "-ll") == 0) { if (argc == 2) { - print_mainerr(err_arg_missing, argv[1]); + print_mainerr(err_arg_missing, argv[1], NULL); exit(1); } nlua_run_script(argv, argc, 3); @@ -357,10 +357,8 @@ int main(int argc, char **argv) assert(!ui_client_channel_id && !use_builtin_ui); // Nvim server... - int listen_rv = server_init(params.listen_addr); - if (listen_rv != 0) { - mainerr("Failed to --listen", listen_rv < 0 - ? os_strerror(listen_rv) : (listen_rv == 1 ? "empty address" : NULL)); + if (!server_init(params.listen_addr)) { + mainerr(IObuff, NULL, NULL); } TIME_MSG("expanding arguments"); @@ -1053,7 +1051,7 @@ static void command_line_scan(mparm_T *parmp) // "+" or "+{number}" or "+/{pat}" or "+{command}" argument. if (argv[0][0] == '+' && !had_minmin) { if (parmp->n_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } argv_idx = -1; // skip to next argument if (argv[0][1] == NUL) { @@ -1074,7 +1072,7 @@ static void command_line_scan(mparm_T *parmp) parmp->no_swap_file = true; } else { if (parmp->edit_type > EDIT_STDIN) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->had_stdin_file = true; parmp->edit_type = EDIT_STDIN; @@ -1137,7 +1135,7 @@ static void command_line_scan(mparm_T *parmp) nlua_disable_preload = true; } else { if (argv[0][argv_idx]) { - mainerr(err_opt_unknown, argv[0]); + mainerr(err_opt_unknown, argv[0], NULL); } had_minmin = true; } @@ -1211,7 +1209,7 @@ static void command_line_scan(mparm_T *parmp) break; case 'q': // "-q" QuickFix mode if (parmp->edit_type != EDIT_NONE) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->edit_type = EDIT_QF; if (argv[0][argv_idx]) { // "-q{errorfile}" @@ -1240,7 +1238,7 @@ static void command_line_scan(mparm_T *parmp) break; case 't': // "-t {tag}" or "-t{tag}" jump to tag if (parmp->edit_type != EDIT_NONE) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->edit_type = EDIT_TAG; if (argv[0][argv_idx]) { // "-t{tag}" @@ -1274,7 +1272,7 @@ static void command_line_scan(mparm_T *parmp) case 'c': // "-c{command}" or "-c {command}" exec command if (argv[0][argv_idx] != NUL) { if (parmp->n_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } parmp->commands[parmp->n_commands++] = argv[0] + argv_idx; argv_idx = -1; @@ -1291,19 +1289,19 @@ static void command_line_scan(mparm_T *parmp) break; default: - mainerr(err_opt_unknown, argv[0]); + mainerr(err_opt_unknown, argv[0], NULL); } // Handle option arguments with argument. if (want_argument) { // Check for garbage immediately after the option letter. if (argv[0][argv_idx] != NUL) { - mainerr(err_opt_garbage, argv[0]); + mainerr(err_opt_garbage, argv[0], NULL); } argc--; if (argc < 1 && c != 'S') { // -S has an optional argument - mainerr(err_arg_missing, argv[0]); + mainerr(err_arg_missing, argv[0], NULL); } argv++; argv_idx = -1; @@ -1312,7 +1310,7 @@ static void command_line_scan(mparm_T *parmp) case 'c': // "-c {command}" execute command case 'S': // "-S {file}" execute Vim script if (parmp->n_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } if (c == 'S') { char *a; @@ -1343,7 +1341,7 @@ static void command_line_scan(mparm_T *parmp) if (strequal(argv[-1], "--cmd")) { // "--cmd {command}" execute command if (parmp->n_pre_commands >= MAX_ARG_CMDS) { - mainerr(err_extra_cmd, NULL); + mainerr(err_extra_cmd, NULL, NULL); } parmp->pre_commands[parmp->n_pre_commands++] = argv[0]; } else if (strequal(argv[-1], "--listen")) { @@ -1425,7 +1423,7 @@ scripterror: // Check for only one type of editing. if (parmp->edit_type > EDIT_STDIN) { - mainerr(err_too_many_args, argv[0]); + mainerr(err_too_many_args, argv[0], NULL); } parmp->edit_type = EDIT_FILE; @@ -1472,7 +1470,7 @@ scripterror: } if (embedded_mode && (silent_mode || parmp->luaf)) { - mainerr(_("--embed conflicts with -es/-Es/-l"), NULL); + mainerr(_("--embed conflicts with -es/-Es/-l"), NULL, NULL); } // If there is a "+123" or "-c" command, set v:swapcommand to the first one. @@ -2135,28 +2133,30 @@ static int execute_env(char *env) return OK; } -/// Prints the following then exits: -/// - An error message `errstr` -/// - A string `str` if not null +/// Prints a message of the form "{msg1}: {msg2}: {msg3}", then exits with code 1. /// -/// @param errstr string containing an error message -/// @param str string to append to the primary error message, or NULL -static void mainerr(const char *errstr, const char *str) +/// @param msg1 error message +/// @param msg2 extra message, or NULL +/// @param msg3 extra message, or NULL +static void mainerr(const char *msg1, const char *msg2, const char *msg3) FUNC_ATTR_NORETURN { - print_mainerr(errstr, str); + print_mainerr(msg1, msg2, msg3); os_exit(1); } -static void print_mainerr(const char *errstr, const char *str) +static void print_mainerr(const char *msg1, const char *msg2, const char *msg3) { char *prgname = path_tail(argv0); signal_stop(); // kill us with CTRL-C here, if you like - fprintf(stderr, "%s: %s", prgname, _(errstr)); - if (str != NULL) { - fprintf(stderr, ": \"%s\"", str); + fprintf(stderr, "%s: %s", prgname, _(msg1)); + if (msg2 != NULL) { + fprintf(stderr, ": \"%s\"", msg2); + } + if (msg3 != NULL) { + fprintf(stderr, ": \"%s\"", msg3); } fprintf(stderr, _("\nMore info with \"")); fprintf(stderr, "%s -h\"\n", prgname); diff --git a/src/nvim/marktree.c b/src/nvim/marktree.c index 9e3005b6a3..555fef5bbd 100644 --- a/src/nvim/marktree.c +++ b/src/nvim/marktree.c @@ -446,7 +446,7 @@ static MTNode *marktree_alloc_node(MarkTree *b, bool internal) // really meta_inc[kMTMetaCount] static void meta_describe_key_inc(uint32_t *meta_inc, MTKey *k) { - if (!mt_end(*k)) { + if (!mt_end(*k) && !mt_invalid(*k)) { meta_inc[kMTMetaInline] += (k->flags & MT_FLAG_DECOR_VIRT_TEXT_INLINE) ? 1 : 0; meta_inc[kMTMetaLines] += (k->flags & MT_FLAG_DECOR_VIRT_LINES) ? 1 : 0; meta_inc[kMTMetaSignHL] += (k->flags & MT_FLAG_DECOR_SIGNHL) ? 1 : 0; @@ -774,14 +774,10 @@ uint64_t marktree_del_itr(MarkTree *b, MarkTreeIter *itr, bool rev) return other; } -void marktree_revise_flags(MarkTree *b, MarkTreeIter *itr, uint16_t new_flags) +void marktree_revise_meta(MarkTree *b, MarkTreeIter *itr, MTKey old_key) { - uint32_t meta_old[4]; - meta_describe_key(meta_old, rawkey(itr)); - rawkey(itr).flags &= (uint16_t) ~MT_FLAG_EXTERNAL_MASK; - rawkey(itr).flags |= new_flags; - - uint32_t meta_new[4]; + uint32_t meta_old[4], meta_new[4]; + meta_describe_key(meta_old, old_key); meta_describe_key(meta_new, rawkey(itr)); if (!memcmp(meta_old, meta_new, sizeof(meta_old))) { diff --git a/src/nvim/mbyte.c b/src/nvim/mbyte.c index 6fd51e773d..e1701bb931 100644 --- a/src/nvim/mbyte.c +++ b/src/nvim/mbyte.c @@ -523,12 +523,14 @@ int utf_ptr2cells(const char *p_in) } /// Convert a UTF-8 byte sequence to a character number. -/// Doesn't handle ascii! only multibyte and illegal sequences. +/// Doesn't handle ascii! only multibyte and illegal sequences. ASCII (including NUL) +/// are treated like illegal sequences. /// /// @param[in] p String to convert. /// @param[in] len Length of the character in bytes, 0 or 1 if illegal. /// -/// @return Unicode codepoint. A negative value when the sequence is illegal. +/// @return Unicode codepoint. A negative value when the sequence is illegal (or +/// ASCII, including NUL). int32_t utf_ptr2CharInfo_impl(uint8_t const *p, uintptr_t const len) FUNC_ATTR_PURE FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { @@ -1780,15 +1782,15 @@ int utf_head_off(const char *base_in, const char *p_in) start--; } - uint8_t cur_len = utf8len_tab[*start]; - int32_t cur_code = utf_ptr2CharInfo_impl(start, (uintptr_t)cur_len); - if (cur_code < 0) { + const uint8_t last_len = utf8len_tab[*start]; + int32_t cur_code = utf_ptr2CharInfo_impl(start, (uintptr_t)last_len); + if (cur_code < 0 || p - start >= last_len) { return 0; // p must be part of an illegal sequence } - const uint8_t * const safe_end = start + cur_len; + const uint8_t * const safe_end = start + last_len; int cur_bc = utf8proc_get_property(cur_code)->boundclass; - if (always_break(cur_bc)) { + if (always_break(cur_bc) || start == base) { return (int)(p - start); } @@ -1796,18 +1798,23 @@ int utf_head_off(const char *base_in, const char *p_in) const uint8_t *cur_pos = start; const uint8_t *const p_start = start; - if (start == base) { - return (int)(p - start); - } + while (true) { + if (start[-1] == NUL) { + break; + } + + start--; + if (*start < 0x80) { // stop on ascii, we are done + break; + } - start--; - while (*start >= 0x80) { // stop on ascii, we are done while (start > base && (*start & 0xc0) == 0x80 && (cur_pos - start) < 6) { start--; } - int32_t prev_code = utf_ptr2CharInfo_impl(start, (uintptr_t)utf8len_tab[*start]); - if (prev_code < 0) { + int prev_len = utf8len_tab[*start]; + int32_t prev_code = utf_ptr2CharInfo_impl(start, (uintptr_t)prev_len); + if (prev_code < 0 || prev_len < cur_pos - start) { start = cur_pos; // start at valid sequence after invalid bytes break; } @@ -1822,12 +1829,10 @@ int utf_head_off(const char *base_in, const char *p_in) cur_pos = start; cur_bc = prev_bc; cur_code = prev_code; - - start--; } // hot path: we are already on the first codepoint of a sequence - if (start == p_start) { + if (start == p_start && last_len > p - start) { return (int)(p - start); } @@ -2920,17 +2925,17 @@ void f_setcellwidths(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) emsg(_(e_listreq)); return; } + const list_T *const l = argvars[0].vval.v_list; - if (tv_list_len(l) == 0) { + cw_interval_T *table = NULL; + const size_t table_size = (size_t)tv_list_len(l); + if (table_size == 0) { // Clearing the table. - xfree(cw_table); - cw_table = NULL; - cw_table_size = 0; - return; + goto update; } // Note: use list_T instead of listitem_T so that TV_LIST_ITEM_NEXT can be used properly below. - const list_T **ptrs = xmalloc(sizeof(const list_T *) * (size_t)tv_list_len(l)); + const list_T **ptrs = xmalloc(sizeof(const list_T *) * table_size); // Check that all entries are a list with three numbers, the range is // valid and the cell width is valid. @@ -2982,12 +2987,12 @@ void f_setcellwidths(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) }); // Sort the list on the first number. - qsort((void *)ptrs, (size_t)tv_list_len(l), sizeof(const list_T *), tv_nr_compare); + qsort((void *)ptrs, table_size, sizeof(const list_T *), tv_nr_compare); - cw_interval_T *table = xmalloc(sizeof(cw_interval_T) * (size_t)tv_list_len(l)); + table = xmalloc(sizeof(cw_interval_T) * table_size); // Store the items in the new table. - for (item = 0; item < tv_list_len(l); item++) { + for (item = 0; (size_t)item < table_size; item++) { const list_T *const li_l = ptrs[item]; const listitem_T *lili = tv_list_first(li_l); const varnumber_T n1 = TV_LIST_ITEM_TV(lili)->vval.v_number; @@ -3006,10 +3011,12 @@ void f_setcellwidths(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) xfree((void *)ptrs); +update: + ; cw_interval_T *const cw_table_save = cw_table; const size_t cw_table_size_save = cw_table_size; cw_table = table; - cw_table_size = (size_t)tv_list_len(l); + cw_table_size = table_size; // Check that the new value does not conflict with 'listchars' or // 'fillchars'. diff --git a/src/nvim/msgpack_rpc/channel.h b/src/nvim/msgpack_rpc/channel.h index ff73a2fc53..abf1ce7bf6 100644 --- a/src/nvim/msgpack_rpc/channel.h +++ b/src/nvim/msgpack_rpc/channel.h @@ -12,7 +12,7 @@ /// HACK: os/input.c drains this queue immediately before blocking for input. /// Events on this queue are async-safe, but they need the resolved state -/// of os_inchar(), so they are processed "just-in-time". +/// of input_get(), so they are processed "just-in-time". EXTERN MultiQueue *ch_before_blocking_events INIT( = NULL); #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/msgpack_rpc/server.c b/src/nvim/msgpack_rpc/server.c index ae34829181..7f26fd35cd 100644 --- a/src/nvim/msgpack_rpc/server.c +++ b/src/nvim/msgpack_rpc/server.c @@ -11,12 +11,14 @@ #include "nvim/event/socket.h" #include "nvim/garray.h" #include "nvim/garray_defs.h" +#include "nvim/globals.h" #include "nvim/log.h" #include "nvim/main.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/server.h" #include "nvim/os/os.h" #include "nvim/os/stdpaths_defs.h" +#include "nvim/types_defs.h" #define MAX_CONNECTIONS 32 #define ENV_LISTEN "NVIM_LISTEN_ADDRESS" // deprecated @@ -27,36 +29,30 @@ static garray_T watchers = GA_EMPTY_INIT_VALUE; # include "msgpack_rpc/server.c.generated.h" #endif -/// Initializes the module +/// Initializes resources, handles `--listen`, starts the primary server at v:servername. /// -/// @returns 0: success, 1: validation error, 2: already listening, -errno: failed to bind/listen. -int server_init(const char *listen_addr) +/// @returns true on success, false on fatal error (message stored in IObuff) +bool server_init(const char *listen_addr) { + bool ok = true; bool must_free = false; + TriState user_arg = kTrue; // User-provided --listen arg. ga_init(&watchers, sizeof(SocketWatcher *), 1); // $NVIM_LISTEN_ADDRESS (deprecated) if ((!listen_addr || listen_addr[0] == '\0') && os_env_exists(ENV_LISTEN)) { + user_arg = kFalse; // User-provided env var. listen_addr = os_getenv(ENV_LISTEN); } if (!listen_addr || listen_addr[0] == '\0') { + user_arg = kNone; // Autogenerated server address. listen_addr = server_address_new(NULL); must_free = true; } - if (!listen_addr) { - abort(); // Cannot happen. - } - int rv = server_start(listen_addr); - if (os_env_exists(ENV_LISTEN)) { - // Unset $NVIM_LISTEN_ADDRESS, it's a liability hereafter. It is "input only", it must not be - // leaked to child jobs or :terminal. - os_unsetenv(ENV_LISTEN); - } - // TODO(justinmk): this is for logging_spec. Can remove this after nvim_log #7062 is merged. if (os_env_exists("__NVIM_TEST_LOG")) { ELOG("test log message"); @@ -66,7 +62,28 @@ int server_init(const char *listen_addr) xfree((char *)listen_addr); } - return rv; + if (rv == 0 || user_arg == kNone) { + // The autogenerated servername can fail if the user has a broken $XDG_RUNTIME_DIR. #30282 + // But that is not fatal (startup will continue, logged in $NVIM_LOGFILE, empty v:servername). + goto end; + } + + (void)snprintf(IObuff, IOSIZE, + user_arg == + kTrue ? "Failed to --listen: %s: \"%s\"" + : "Failed $NVIM_LISTEN_ADDRESS: %s: \"%s\"", + rv < 0 ? os_strerror(rv) : (rv == 1 ? "empty address" : "?"), + listen_addr); + ok = false; + +end: + if (os_env_exists(ENV_LISTEN)) { + // Unset $NVIM_LISTEN_ADDRESS, it's a liability hereafter. It is "input only", it must not be + // leaked to child jobs or :terminal. + os_unsetenv(ENV_LISTEN); + } + + return ok; } /// Teardown a single server @@ -97,17 +114,19 @@ void server_teardown(void) /// - Windows: "\\.\pipe\<name>.<pid>.<counter>" /// - Other: "/tmp/nvim.user/xxx/<name>.<pid>.<counter>" char *server_address_new(const char *name) + FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_NONNULL_RET { static uint32_t count = 0; char fmt[ADDRESS_MAX_SIZE]; - const char *appname = get_appname(); #ifdef MSWIN + (void)get_appname(true); int r = snprintf(fmt, sizeof(fmt), "\\\\.\\pipe\\%s.%" PRIu64 ".%" PRIu32, - name ? name : appname, os_get_pid(), count++); + name ? name : NameBuff, os_get_pid(), count++); #else char *dir = stdpaths_get_xdg_var(kXDGRuntimeDir); + (void)get_appname(true); int r = snprintf(fmt, sizeof(fmt), "%s/%s.%" PRIu64 ".%" PRIu32, - dir, name ? name : appname, os_get_pid(), count++); + dir, name ? name : NameBuff, os_get_pid(), count++); xfree(dir); #endif if ((size_t)r >= sizeof(fmt)) { diff --git a/src/nvim/normal.c b/src/nvim/normal.c index f3bdea9a85..b9ce891b49 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -6597,11 +6597,11 @@ static void nv_open(cmdarg_T *cap) static void nv_event(cmdarg_T *cap) { // Garbage collection should have been executed before blocking for events in - // the `os_inchar` in `state_enter`, but we also disable it here in case the - // `os_inchar` branch was not executed (!multiqueue_empty(loop.events), which + // the `input_get` in `state_enter`, but we also disable it here in case the + // `input_get` branch was not executed (!multiqueue_empty(loop.events), which // could have `may_garbage_collect` set to true in `normal_check`). // - // That is because here we may run code that calls `os_inchar` + // That is because here we may run code that calls `input_get` // later(`f_confirm` or `get_keystroke` for example), but in these cases it is // not safe to perform garbage collection because there could be unreferenced // lists or dicts being used. diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 4b3f69a378..8faf0a6b47 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -306,7 +306,7 @@ void shift_line(bool left, bool round, int amount, int call_changed_bytes) // Set new indent if (State & VREPLACE_FLAG) { - change_indent(INDENT_SET, count, false, NUL, call_changed_bytes); + change_indent(INDENT_SET, count, false, call_changed_bytes); } else { set_indent(count, call_changed_bytes ? SIN_CHANGED : 0); } diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 1c17b0fc9f..71b0d26b49 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -761,9 +761,9 @@ return { list:{n} Adds an additional indent for lines that match a numbered or bulleted list (using the 'formatlistpat' setting). - list:-1 Uses the length of a match with 'formatlistpat' - for indentation. (default: 0) + list:-1 Uses the width of a match with 'formatlistpat' for + indentation. column:{n} Indent at column {n}. Will overrule the other sub-options. Note: an additional indent may be added for the 'showbreak' setting. diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index 8affc58591..a240c0e5b6 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -33,12 +33,6 @@ #define READ_BUFFER_SIZE 0xfff #define INPUT_BUFFER_SIZE ((READ_BUFFER_SIZE * 4) + MAX_KEY_CODE_LEN) -typedef enum { - kInputNone, - kInputAvail, - kInputEof, -} InbufPollResult; - static RStream read_stream = { .s.closed = true }; // Input before UI starts. static char input_buffer[INPUT_BUFFER_SIZE]; static char *input_read_pos = input_buffer; @@ -84,57 +78,76 @@ static void cursorhold_event(void **argv) static void create_cursorhold_event(bool events_enabled) { // If events are enabled and the queue has any items, this function should not - // have been called (inbuf_poll would return kInputAvail). + // have been called (`inbuf_poll` would return `kTrue`). // TODO(tarruda): Cursorhold should be implemented as a timer set during the // `state_check` callback for the states where it can be triggered. assert(!events_enabled || multiqueue_empty(main_loop.events)); multiqueue_put(main_loop.events, cursorhold_event, NULL); } -static void restart_cursorhold_wait(int tb_change_cnt) +static void reset_cursorhold_wait(int tb_change_cnt) { cursorhold_time = 0; cursorhold_tb_change_cnt = tb_change_cnt; } -/// Low level input function +/// Reads OS input into `buf`, and consumes pending events while waiting (if `ms != 0`). +/// +/// - Consumes available input received from the OS. +/// - Consumes pending events. +/// - Manages CursorHold events. +/// - Handles EOF conditions. /// -/// Wait until either the input buffer is non-empty or, if `events` is not NULL -/// until `events` is non-empty. -int os_inchar(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *events) +/// Originally based on the Vim `mch_inchar` function. +/// +/// @param buf Buffer to store read input. +/// @param maxlen Maximum bytes to read into `buf`, or 0 to skip reading. +/// @param ms Timeout in milliseconds. -1 for indefinite wait, 0 for no wait. +/// @param tb_change_cnt Used to detect when typeahead changes. +/// @param events (optional) Events to process. +/// @return Bytes read into buf, or 0 if no input was read +int input_get(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *events) { // This check is needed so that feeding typeahead from RPC can prevent CursorHold. if (tb_change_cnt != cursorhold_tb_change_cnt) { - restart_cursorhold_wait(tb_change_cnt); + reset_cursorhold_wait(tb_change_cnt); } - if (maxlen && input_available()) { - restart_cursorhold_wait(tb_change_cnt); - size_t to_read = MIN((size_t)maxlen, input_available()); - memcpy(buf, input_read_pos, to_read); - input_read_pos += to_read; - return (int)to_read; - } +#define TRY_READ() \ + do { \ + if (maxlen && input_available()) { \ + reset_cursorhold_wait(tb_change_cnt); \ + assert(maxlen >= 0); \ + size_t to_read = MIN((size_t)maxlen, input_available()); \ + memcpy(buf, input_read_pos, to_read); \ + input_read_pos += to_read; \ + /* This is safe because INPUT_BUFFER_SIZE fits in an int. */ \ + assert(to_read <= INT_MAX); \ + return (int)to_read; \ + } \ + } while (0) + + TRY_READ(); // No risk of a UI flood, so disable CTRL-C "interrupt" behavior if it's mapped. if ((mapped_ctrl_c | curbuf->b_mapped_ctrl_c) & get_real_state()) { ctrl_c_interrupts = false; } - InbufPollResult result; + TriState result; ///< inbuf_poll result. if (ms >= 0) { - if ((result = inbuf_poll(ms, events)) == kInputNone) { + if ((result = inbuf_poll(ms, events)) == kFalse) { return 0; } } else { uint64_t wait_start = os_hrtime(); cursorhold_time = MIN(cursorhold_time, (int)p_ut); - if ((result = inbuf_poll((int)p_ut - cursorhold_time, events)) == kInputNone) { + if ((result = inbuf_poll((int)p_ut - cursorhold_time, events)) == kFalse) { if (read_stream.s.closed && silent_mode) { // Drained eventloop & initial input; exit silent/batch-mode (-es/-Es). read_error_exit(); } - restart_cursorhold_wait(tb_change_cnt); + reset_cursorhold_wait(tb_change_cnt); if (trigger_cursorhold() && !typebuf_changed(tb_change_cnt)) { create_cursorhold_event(events == main_loop.events); } else { @@ -153,32 +166,26 @@ int os_inchar(uint8_t *buf, int maxlen, int ms, int tb_change_cnt, MultiQueue *e return 0; } - if (maxlen && input_available()) { - restart_cursorhold_wait(tb_change_cnt); - // Safe to convert `to_read` to int, it will never overflow since - // INPUT_BUFFER_SIZE fits in an int - size_t to_read = MIN((size_t)maxlen, input_available()); - memcpy(buf, input_read_pos, to_read); - input_read_pos += to_read; - return (int)to_read; - } + TRY_READ(); // If there are events, return the keys directly if (maxlen && pending_events(events)) { return push_event_key(buf, maxlen); } - if (result == kInputEof) { + if (result == kNone) { read_error_exit(); } return 0; + +#undef TRY_READ } // Check if a character is available for reading bool os_char_avail(void) { - return inbuf_poll(0, NULL) == kInputAvail; + return inbuf_poll(0, NULL) == kTrue; } /// Poll for fast events. `got_int` will be set to `true` if CTRL-C was typed. @@ -463,15 +470,22 @@ bool input_blocking(void) return blocking; } -// This is a replacement for the old `WaitForChar` function in os_unix.c -static InbufPollResult inbuf_poll(int ms, MultiQueue *events) +/// Checks for (but does not read) available input, and consumes `main_loop.events` while waiting. +/// +/// @param ms Timeout in milliseconds. -1 for indefinite wait, 0 for no wait. +/// @param events (optional) Queue to check for pending events. +/// @return TriState: +/// - kTrue: Input/events available +/// - kFalse: No input/events +/// - kNone: EOF reached on the input stream +static TriState inbuf_poll(int ms, MultiQueue *events) { if (os_input_ready(events)) { - return kInputAvail; + return kTrue; } if (do_profiling == PROF_YES && ms) { - prof_inchar_enter(); + prof_input_start(); } if ((ms == -1 || ms > 0) && events != main_loop.events && !input_eof) { @@ -479,20 +493,18 @@ static InbufPollResult inbuf_poll(int ms, MultiQueue *events) blocking = true; multiqueue_process_events(ch_before_blocking_events); } - DLOG("blocking... events_enabled=%d events_pending=%d", events != NULL, - events && !multiqueue_empty(events)); - LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, ms, - os_input_ready(events) || input_eof); + DLOG("blocking... events=%d pending=%d", !!events, pending_events(events)); + LOOP_PROCESS_EVENTS_UNTIL(&main_loop, NULL, ms, os_input_ready(events) || input_eof); blocking = false; if (do_profiling == PROF_YES && ms) { - prof_inchar_exit(); + prof_input_end(); } if (os_input_ready(events)) { - return kInputAvail; + return kTrue; } - return input_eof ? kInputEof : kInputNone; + return input_eof ? kNone : kFalse; } static size_t input_read_cb(RStream *stream, const char *buf, size_t c, void *data, bool at_eof) @@ -533,8 +545,8 @@ static void process_ctrl_c(void) } } -// Helper function used to push bytes from the 'event' key sequence partially -// between calls to os_inchar when maxlen < 3 +/// Pushes bytes from the "event" key sequence (KE_EVENT) partially between calls to input_get when +/// `maxlen < 3`. static int push_event_key(uint8_t *buf, int maxlen) { static const uint8_t key[3] = { K_SPECIAL, KS_EXTRA, KE_EVENT }; diff --git a/src/nvim/os/stdpaths.c b/src/nvim/os/stdpaths.c index e4435bcaa8..e9a74d197f 100644 --- a/src/nvim/os/stdpaths.c +++ b/src/nvim/os/stdpaths.c @@ -63,22 +63,32 @@ static const char *const xdg_defaults[] = { #endif }; -/// Get the value of $NVIM_APPNAME or "nvim" if not set. +/// Gets the value of $NVIM_APPNAME, or "nvim" if not set. +/// +/// @param namelike Write "name-like" value (no path separators) in `NameBuff`. /// /// @return $NVIM_APPNAME value -const char *get_appname(void) +const char *get_appname(bool namelike) { const char *env_val = os_getenv("NVIM_APPNAME"); if (env_val == NULL || *env_val == NUL) { env_val = "nvim"; } + + if (namelike) { + // Appname may be a relative path, replace slashes to make it name-like. + xstrlcpy(NameBuff, env_val, sizeof(NameBuff)); + memchrsub(NameBuff, '/', '-', sizeof(NameBuff)); + memchrsub(NameBuff, '\\', '-', sizeof(NameBuff)); + } + return env_val; } /// Ensure that APPNAME is valid. Must be a name or relative path. bool appname_is_valid(void) { - const char *appname = get_appname(); + const char *appname = get_appname(false); if (path_is_absolute(appname) // TODO(justinmk): on Windows, path_is_absolute says "/" is NOT absolute. Should it? || strequal(appname, "/") @@ -193,7 +203,7 @@ char *get_xdg_home(const XDGVarType idx) FUNC_ATTR_WARN_UNUSED_RESULT { char *dir = stdpaths_get_xdg_var(idx); - const char *appname = get_appname(); + const char *appname = get_appname(false); size_t appname_len = strlen(appname); assert(appname_len < (IOSIZE - sizeof("-data"))); diff --git a/src/nvim/os/win_defs.h b/src/nvim/os/win_defs.h index 024719806c..852059f78b 100644 --- a/src/nvim/os/win_defs.h +++ b/src/nvim/os/win_defs.h @@ -16,9 +16,6 @@ #include <sys/stat.h> #include <windows.h> -// vterm.h defines an `unsigned int small` in a struct, triggering error C2632 -#undef small - // Windows does not have S_IFLNK but libuv defines it // and sets the flag for us when calling uv_fs_stat. #include <uv.h> diff --git a/src/nvim/profile.c b/src/nvim/profile.c index 7ea7061b46..b397de353b 100644 --- a/src/nvim/profile.c +++ b/src/nvim/profile.c @@ -383,19 +383,19 @@ void set_context_in_profile_cmd(expand_T *xp, const char *arg) xp->xp_context = EXPAND_NOTHING; } -static proftime_T inchar_time; +static proftime_T wait_time; /// Called when starting to wait for the user to type a character. -void prof_inchar_enter(void) +void prof_input_start(void) { - inchar_time = profile_start(); + wait_time = profile_start(); } /// Called when finished waiting for the user to type a character. -void prof_inchar_exit(void) +void prof_input_end(void) { - inchar_time = profile_end(inchar_time); - profile_set_wait(profile_add(profile_get_wait(), inchar_time)); + wait_time = profile_end(wait_time); + profile_set_wait(profile_add(profile_get_wait(), wait_time)); } /// @return true when a function defined in the current script should be diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index 3aa558f305..030bda4fa5 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -1559,7 +1559,7 @@ static inline char *add_env_sep_dirs(char *dest, const char *const val, const ch return dest; } const void *iter = NULL; - const char *appname = get_appname(); + const char *appname = get_appname(false); const size_t appname_len = strlen(appname); do { size_t dir_len; @@ -1626,7 +1626,7 @@ static inline char *add_dir(char *dest, const char *const dir, const size_t dir_ if (!after_pathsep(dest - 1, dest)) { *dest++ = PATHSEP; } - const char *appname = get_appname(); + const char *appname = get_appname(false); size_t appname_len = strlen(appname); assert(appname_len < (IOSIZE - sizeof("-data"))); xmemcpyz(IObuff, appname, appname_len); @@ -1697,7 +1697,7 @@ char *runtimepath_default(bool clean_arg) size_t config_len = 0; size_t vimruntime_len = 0; size_t libdir_len = 0; - const char *appname = get_appname(); + const char *appname = get_appname(false); size_t appname_len = strlen(appname); if (data_home != NULL) { data_len = strlen(data_home); diff --git a/src/nvim/state.c b/src/nvim/state.c index eab976927c..1b08a08fba 100644 --- a/src/nvim/state.c +++ b/src/nvim/state.c @@ -74,10 +74,9 @@ getkey: } // Flush screen updates before blocking. ui_flush(); - // Call `os_inchar` directly to block for events or user input without - // consuming anything from `input_buffer`(os/input.c) or calling the - // mapping engine. - os_inchar(NULL, 0, -1, typebuf.tb_change_cnt, main_loop.events); + // Call `input_get` directly to block for events or user input without consuming anything from + // `os/input.c:input_buffer` or calling the mapping engine. + input_get(NULL, 0, -1, typebuf.tb_change_cnt, main_loop.events); // If an event was put into the queue, we send K_EVENT directly. if (!input_available() && !multiqueue_empty(main_loop.events)) { key = K_EVENT; diff --git a/src/nvim/textformat.c b/src/nvim/textformat.c index 30c7d0ee92..9095d4e8c9 100644 --- a/src/nvim/textformat.c +++ b/src/nvim/textformat.c @@ -400,7 +400,7 @@ void internal_format(int textwidth, int second_indent, int flags, bool format_on } if (second_indent >= 0) { if (State & VREPLACE_FLAG) { - change_indent(INDENT_SET, second_indent, false, NUL, true); + change_indent(INDENT_SET, second_indent, false, true); } else if (leader_len > 0 && second_indent - leader_len > 0) { int padding = second_indent - leader_len; diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index a6e27c9391..3eb8d4ba2e 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -7,24 +7,27 @@ #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/event/loop.h" +#include "nvim/event/rstream.h" #include "nvim/event/stream.h" #include "nvim/macros_defs.h" #include "nvim/main.h" #include "nvim/map_defs.h" #include "nvim/memory.h" +#include "nvim/msgpack_rpc/channel.h" #include "nvim/option_vars.h" #include "nvim/os/os.h" #include "nvim/os/os_defs.h" #include "nvim/strings.h" #include "nvim/tui/input.h" #include "nvim/tui/input_defs.h" +#include "nvim/tui/termkey/driver-csi.h" +#include "nvim/tui/termkey/termkey.h" #include "nvim/tui/tui.h" #include "nvim/ui_client.h" + #ifdef MSWIN # include "nvim/os/os_win_console.h" #endif -#include "nvim/event/rstream.h" -#include "nvim/msgpack_rpc/channel.h" #define READ_STREAM_SIZE 0xfff @@ -261,7 +264,7 @@ static size_t handle_more_modifiers(TermKeyKey *key, char *buf, size_t buflen) static void handle_kitty_key_protocol(TermInput *input, TermKeyKey *key) { - const char *name = pmap_get(int)(&kitty_key_map, (int)key->code.codepoint); + const char *name = pmap_get(int)(&kitty_key_map, key->code.codepoint); if (name) { char buf[64]; size_t len = 0; @@ -598,7 +601,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) // contain, so just allocate enough space for a large upper bound TermKeyCsiParam params[16]; size_t nparams = 16; - unsigned long cmd; + unsigned cmd; if (termkey_interpret_csi(input->tk, key, params, &nparams, &cmd) != TERMKEY_RES_KEY) { return; } @@ -641,7 +644,7 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) case 't': if (nparams == 5) { // We only care about the first 3 parameters, and we ignore subparameters - long args[3]; + int args[3]; for (size_t i = 0; i < ARRAY_SIZE(args); i++) { if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { return; @@ -650,8 +653,8 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) if (args[0] == 48) { // In-band resize event (DEC private mode 2048) - int height_chars = (int)args[1]; - int width_chars = (int)args[2]; + int height_chars = args[1]; + int width_chars = args[2]; tui_set_size(input->tui_data, width_chars, height_chars); ui_client_set_size(width_chars, height_chars); } diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index 8d0c0c20e9..4c2baf908e 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -6,9 +6,9 @@ #include "nvim/event/defs.h" #include "nvim/tui/input_defs.h" // IWYU pragma: keep +#include "nvim/tui/termkey/termkey_defs.h" #include "nvim/tui/tui_defs.h" #include "nvim/types_defs.h" -#include "termkey/termkey.h" typedef enum { kKeyEncodingLegacy, ///< Legacy key encoding diff --git a/src/nvim/tui/termkey/README b/src/nvim/tui/termkey/README new file mode 100644 index 0000000000..fd081025fc --- /dev/null +++ b/src/nvim/tui/termkey/README @@ -0,0 +1 @@ +// Adapted from libtermkey: https://github.com/neovim/libtermkey diff --git a/src/termkey/driver-csi.c b/src/nvim/tui/termkey/driver-csi.c index f9c39757b7..28c7eaccfd 100644 --- a/src/termkey/driver-csi.c +++ b/src/nvim/tui/termkey/driver-csi.c @@ -1,36 +1,37 @@ -#include "termkey.h" -#include "termkey-internal.h" - #include <assert.h> #include <stdio.h> #include <string.h> +#include "nvim/memory.h" +#include "nvim/tui/termkey/driver-csi.h" +#include "nvim/tui/termkey/termkey-internal.h" +#include "nvim/tui/termkey/termkey.h" +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-csi.c.generated.h" +#endif + // There are 64 codes 0x40 - 0x7F static int keyinfo_initialised = 0; static struct keyinfo ss3s[64]; static char ss3_kpalts[64]; -typedef struct { - TermKey *tk; - int saved_string_id; - char *saved_string; -} TermKeyCsi; - -typedef TermKeyResult CsiHandler(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams); +typedef TermKeyResult CsiHandler(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams); static CsiHandler *csi_handlers[64]; -/* - * Handler for CSI/SS3 cmd keys - */ +// Handler for CSI/SS3 cmd keys static struct keyinfo csi_ss3s[64]; -static TermKeyResult handle_csi_ss3_full(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams) +static TermKeyResult handle_csi_ss3_full(TermKey *tk, TermKeyKey *key, int cmd, + TermKeyCsiParam *params, int nparams) { TermKeyResult result = TERMKEY_RES_KEY; - if(nparams > 1 && params[1].param != NULL) { - long arg = 0; + if (nparams > 1 && params[1].param != NULL) { + int arg = 0; result = termkey_interpret_csi_param(params[1], &arg, NULL, NULL); if (result != TERMKEY_RES_KEY) { return result; @@ -46,15 +47,17 @@ static TermKeyResult handle_csi_ss3_full(TermKey *tk, TermKeyKey *key, int cmd, key->modifiers &= ~(csi_ss3s[cmd - 0x40].modifier_mask); key->modifiers |= csi_ss3s[cmd - 0x40].modifier_set; - if(key->code.sym == TERMKEY_SYM_UNKNOWN) + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { result = TERMKEY_RES_NONE; + } return result; } -static void register_csi_ss3_full(TermKeyType type, TermKeySym sym, int modifier_set, int modifier_mask, unsigned char cmd) +static void register_csi_ss3_full(TermKeyType type, TermKeySym sym, int modifier_set, + int modifier_mask, unsigned char cmd) { - if(cmd < 0x40 || cmd >= 0x80) { + if (cmd < 0x40 || cmd >= 0x80) { return; } @@ -71,13 +74,10 @@ static void register_csi_ss3(TermKeyType type, TermKeySym sym, unsigned char cmd register_csi_ss3_full(type, sym, 0, 0, cmd); } -/* - * Handler for SS3 keys with kpad alternate representations - */ - +/// Handler for SS3 keys with kpad alternate representations static void register_ss3kpalt(TermKeyType type, TermKeySym sym, unsigned char cmd, char kpalt) { - if(cmd < 0x40 || cmd >= 0x80) { + if (cmd < 0x40 || cmd >= 0x80) { return; } @@ -88,23 +88,22 @@ static void register_ss3kpalt(TermKeyType type, TermKeySym sym, unsigned char cm ss3_kpalts[cmd - 0x40] = kpalt; } -/* - * Handler for CSI number ~ function keys - */ +// Handler for CSI number ~ function keys -static struct keyinfo csifuncs[35]; /* This value must be increased if more CSI function keys are added */ -#define NCSIFUNCS (sizeof(csifuncs)/sizeof(csifuncs[0])) +#define NCSIFUNCS 35 // This value must be increased if more CSI function keys are added +static struct keyinfo csifuncs[NCSIFUNCS]; -static TermKeyResult handle_csifunc(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams) +static TermKeyResult handle_csifunc(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) { if (nparams == 0) { return TERMKEY_RES_NONE; } TermKeyResult result = TERMKEY_RES_KEY; - long args[3]; + int args[3]; - if(nparams > 1 && params[1].param != NULL) { + if (nparams > 1 && params[1].param != NULL) { result = termkey_interpret_csi_param(params[1], &args[1], NULL, NULL); if (result != TERMKEY_RES_KEY) { return result; @@ -122,7 +121,7 @@ static TermKeyResult handle_csifunc(TermKey *tk, TermKeyKey *key, int cmd, TermK return result; } - if(args[0] == 27 && nparams > 2 && params[2].param != NULL) { + if (args[0] == 27 && nparams > 2 && params[2].param != NULL) { result = termkey_interpret_csi_param(params[2], &args[2], NULL, NULL); if (result != TERMKEY_RES_KEY) { return result; @@ -131,17 +130,16 @@ static TermKeyResult handle_csifunc(TermKey *tk, TermKeyKey *key, int cmd, TermK int mod = key->modifiers; (*tk->method.emit_codepoint)(tk, args[2], key); key->modifiers |= mod; - } - else if(args[0] >= 0 && args[0] < NCSIFUNCS) { + } else if (args[0] >= 0 && args[0] < NCSIFUNCS) { key->type = csifuncs[args[0]].type; key->code.sym = csifuncs[args[0]].sym; key->modifiers &= ~(csifuncs[args[0]].modifier_mask); key->modifiers |= csifuncs[args[0]].modifier_set; - } - else + } else { key->code.sym = TERMKEY_SYM_UNKNOWN; + } - if(key->code.sym == TERMKEY_SYM_UNKNOWN) { + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { #ifdef DEBUG fprintf(stderr, "CSI: Unknown function key %ld\n", arg[0]); #endif @@ -153,7 +151,7 @@ static TermKeyResult handle_csifunc(TermKey *tk, TermKeyKey *key, int cmd, TermK static void register_csifunc(TermKeyType type, TermKeySym sym, int number) { - if(number >= NCSIFUNCS) { + if (number >= NCSIFUNCS) { return; } @@ -165,82 +163,79 @@ static void register_csifunc(TermKeyType type, TermKeySym sym, int number) csi_handlers['~' - 0x40] = &handle_csifunc; } -/* - * Handler for CSI u extended Unicode keys - */ - -static TermKeyResult handle_csi_u(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams) +/// Handler for CSI u extended Unicode keys +static TermKeyResult handle_csi_u(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) { - switch(cmd) { - case 'u': { - long args[2]; - if(nparams > 1 && params[1].param != NULL) { - long subparam = 0; - size_t nsubparams = 1; - if (termkey_interpret_csi_param(params[1], &args[1], &subparam, &nsubparams) != TERMKEY_RES_KEY) { - return TERMKEY_RES_ERROR; - } - - if (nsubparams > 0 && subparam != 1) { - // Not a press event. Ignore for now - return TERMKEY_RES_NONE; - } - - key->modifiers = args[1] - 1; - } else { - key->modifiers = 0; + switch (cmd) { + case 'u': { + int args[2]; + if (nparams > 1 && params[1].param != NULL) { + int subparam = 0; + size_t nsubparams = 1; + if (termkey_interpret_csi_param(params[1], &args[1], &subparam, + &nsubparams) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; } - if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { - return TERMKEY_RES_ERROR; + if (nsubparams > 0 && subparam != 1) { + // Not a press event. Ignore for now + return TERMKEY_RES_NONE; } - int mod = key->modifiers; - key->type = TERMKEY_TYPE_KEYSYM; - (*tk->method.emit_codepoint)(tk, args[0], key); - key->modifiers |= mod; + key->modifiers = args[1] - 1; + } else { + key->modifiers = 0; + } - return TERMKEY_RES_KEY; + if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; } - default: - return TERMKEY_RES_NONE; + + int mod = key->modifiers; + key->type = TERMKEY_TYPE_KEYSYM; + (*tk->method.emit_codepoint)(tk, args[0], key); + key->modifiers |= mod; + + return TERMKEY_RES_KEY; + } + default: + return TERMKEY_RES_NONE; } } -/* - * Handler for CSI M / CSI m mouse events in SGR and rxvt encodings - * Note: This does not handle X10 encoding - */ - -static TermKeyResult handle_csi_m(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams) +/// Handler for CSI M / CSI m mouse events in SGR and rxvt encodings +/// Note: This does not handle X10 encoding +static TermKeyResult handle_csi_m(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) { int initial = cmd >> 8; cmd &= 0xff; - switch(cmd) { - case 'M': - case 'm': - break; - default: - return TERMKEY_RES_NONE; + switch (cmd) { + case 'M': + case 'm': + break; + default: + return TERMKEY_RES_NONE; } if (nparams < 3) { return TERMKEY_RES_NONE; } - long args[3]; + int args[3]; for (size_t i = 0; i < 3; i++) { if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { return TERMKEY_RES_ERROR; } } - if(!initial) { // rxvt protocol + if (!initial) { // rxvt protocol key->type = TERMKEY_TYPE_MOUSE; - key->code.mouse[0] = args[0]; + key->code.mouse[0] = (char)args[0]; - key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; + key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; key->code.mouse[0] &= ~0x1c; termkey_key_set_linecol(key, args[1], args[2]); @@ -248,17 +243,18 @@ static TermKeyResult handle_csi_m(TermKey *tk, TermKeyKey *key, int cmd, TermKey return TERMKEY_RES_KEY; } - if(initial == '<') { // SGR protocol + if (initial == '<') { // SGR protocol key->type = TERMKEY_TYPE_MOUSE; - key->code.mouse[0] = args[0]; + key->code.mouse[0] = (char)args[0]; - key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; + key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; key->code.mouse[0] &= ~0x1c; termkey_key_set_linecol(key, args[1], args[2]); - if(cmd == 'm') // release + if (cmd == 'm') { // release key->code.mouse[3] |= 0x80; + } return TERMKEY_RES_KEY; } @@ -266,28 +262,32 @@ static TermKeyResult handle_csi_m(TermKey *tk, TermKeyKey *key, int cmd, TermKey return TERMKEY_RES_NONE; } -TermKeyResult termkey_interpret_mouse(TermKey *tk, const TermKeyKey *key, TermKeyMouseEvent *event, int *button, int *line, int *col) +TermKeyResult termkey_interpret_mouse(TermKey *tk, const TermKeyKey *key, TermKeyMouseEvent *event, + int *button, int *line, int *col) { - if(key->type != TERMKEY_TYPE_MOUSE) + if (key->type != TERMKEY_TYPE_MOUSE) { return TERMKEY_RES_NONE; + } - if(button) + if (button) { *button = 0; + } termkey_key_get_linecol(key, line, col); - if(!event) + if (!event) { return TERMKEY_RES_KEY; + } int btn = 0; - int code = key->code.mouse[0]; + int code = (unsigned char)key->code.mouse[0]; int drag = code & 0x20; code &= ~0x3c; - switch(code) { + switch (code) { case 0: case 1: case 2: @@ -312,119 +312,128 @@ TermKeyResult termkey_interpret_mouse(TermKey *tk, const TermKeyKey *key, TermKe *event = TERMKEY_MOUSE_UNKNOWN; } - if(button) + if (button) { *button = btn; + } - if(key->code.mouse[3] & 0x80) + if (key->code.mouse[3] & 0x80) { *event = TERMKEY_MOUSE_RELEASE; + } return TERMKEY_RES_KEY; } -/* - * Handler for CSI ? R position reports - * A plain CSI R with no arguments is probably actually <F3> - */ - -static TermKeyResult handle_csi_R(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams) +/// Handler for CSI ? R position reports +/// A plain CSI R with no arguments is probably actually <F3> +static TermKeyResult handle_csi_R(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) { - switch(cmd) { - case 'R'|'?'<<8: - if(nparams < 2) - return TERMKEY_RES_NONE; - - long args[2]; - if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { - return TERMKEY_RES_ERROR; + switch (cmd) { + case 'R'|'?' << 8: + if (nparams < 2) { + return TERMKEY_RES_NONE; } - if (termkey_interpret_csi_param(params[1], &args[1], NULL, NULL) != TERMKEY_RES_KEY) { - return TERMKEY_RES_ERROR; - } + int args[2]; + if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + if (termkey_interpret_csi_param(params[1], &args[1], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } - key->type = TERMKEY_TYPE_POSITION; - termkey_key_set_linecol(key, args[1], args[0]); - return TERMKEY_RES_KEY; + key->type = TERMKEY_TYPE_POSITION; + termkey_key_set_linecol(key, args[1], args[0]); + return TERMKEY_RES_KEY; - default: - return handle_csi_ss3_full(tk, key, cmd, params, nparams); + default: + return handle_csi_ss3_full(tk, key, cmd, params, nparams); } } TermKeyResult termkey_interpret_position(TermKey *tk, const TermKeyKey *key, int *line, int *col) { - if(key->type != TERMKEY_TYPE_POSITION) + if (key->type != TERMKEY_TYPE_POSITION) { return TERMKEY_RES_NONE; + } termkey_key_get_linecol(key, line, col); return TERMKEY_RES_KEY; } -/* - * Handler for CSI $y mode status reports - */ - -static TermKeyResult handle_csi_y(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, int nparams) +/// Handler for CSI $y mode status reports +static TermKeyResult handle_csi_y(TermKey *tk, TermKeyKey *key, int cmd, TermKeyCsiParam *params, + int nparams) { - switch(cmd) { - case 'y'|'$'<<16: - case 'y'|'$'<<16 | '?'<<8: - if(nparams < 2) - return TERMKEY_RES_NONE; - - long args[2]; - if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { - return TERMKEY_RES_ERROR; + switch (cmd) { + case 'y'|'$' << 16: + case 'y'|'$' << 16 | '?' << 8: + if (nparams < 2) { + return TERMKEY_RES_NONE; } - if (termkey_interpret_csi_param(params[1], &args[1], NULL, NULL) != TERMKEY_RES_KEY) { - return TERMKEY_RES_ERROR; - } + int args[2]; + if (termkey_interpret_csi_param(params[0], &args[0], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } + + if (termkey_interpret_csi_param(params[1], &args[1], NULL, NULL) != TERMKEY_RES_KEY) { + return TERMKEY_RES_ERROR; + } - key->type = TERMKEY_TYPE_MODEREPORT; - key->code.mouse[0] = (cmd >> 8); - key->code.mouse[1] = args[0] >> 8; - key->code.mouse[2] = args[0] & 0xff; - key->code.mouse[3] = args[1]; - return TERMKEY_RES_KEY; + key->type = TERMKEY_TYPE_MODEREPORT; + key->code.mouse[0] = (char)(cmd >> 8); + key->code.mouse[1] = (char)(args[0] >> 8); + key->code.mouse[2] = (char)(args[0] & 0xff); + key->code.mouse[3] = (char)args[1]; + return TERMKEY_RES_KEY; - default: - return TERMKEY_RES_NONE; + default: + return TERMKEY_RES_NONE; } } -TermKeyResult termkey_interpret_modereport(TermKey *tk, const TermKeyKey *key, int *initial, int *mode, int *value) +TermKeyResult termkey_interpret_modereport(TermKey *tk, const TermKeyKey *key, int *initial, + int *mode, int *value) { - if(key->type != TERMKEY_TYPE_MODEREPORT) + if (key->type != TERMKEY_TYPE_MODEREPORT) { return TERMKEY_RES_NONE; + } - if(initial) - *initial = key->code.mouse[0]; + if (initial) { + *initial = (unsigned char)key->code.mouse[0]; + } - if(mode) + if (mode) { *mode = ((uint8_t)key->code.mouse[1] << 8) | (uint8_t)key->code.mouse[2]; + } - if(value) - *value = key->code.mouse[3]; + if (value) { + *value = (unsigned char)key->code.mouse[3]; + } return TERMKEY_RES_KEY; } #define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) -static TermKeyResult parse_csi(TermKey *tk, size_t introlen, size_t *csi_len, TermKeyCsiParam params[], size_t *nargs, unsigned long *commandp) +static TermKeyResult parse_csi(TermKey *tk, size_t introlen, size_t *csi_len, + TermKeyCsiParam params[], size_t *nargs, unsigned *commandp) { size_t csi_end = introlen; - while(csi_end < tk->buffcount) { - if(CHARAT(csi_end) >= 0x40 && CHARAT(csi_end) < 0x80) + while (csi_end < tk->buffcount) { + if (CHARAT(csi_end) >= 0x40 && CHARAT(csi_end) < 0x80) { break; + } csi_end++; } - if(csi_end >= tk->buffcount) + if (csi_end >= tk->buffcount) { return TERMKEY_RES_AGAIN; + } unsigned char cmd = CHARAT(csi_end); *commandp = cmd; @@ -435,66 +444,69 @@ static TermKeyResult parse_csi(TermKey *tk, size_t introlen, size_t *csi_len, Te size_t p = introlen; // See if there is an initial byte - if(CHARAT(p) >= '<' && CHARAT(p) <= '?') { - *commandp |= (CHARAT(p) << 8); + if (CHARAT(p) >= '<' && CHARAT(p) <= '?') { + *commandp |= (unsigned)(CHARAT(p) << 8); p++; } // Now attempt to parse out up number;number;... separated values - while(p < csi_end) { + while (p < csi_end) { unsigned char c = CHARAT(p); - if(c >= '0' && c < ';') { - if(!present) { + if (c >= '0' && c < ';') { + if (!present) { params[argi].param = &CHARAT(p); present = 1; } - } - else if(c == ';') { - if(!present) { + } else if (c == ';') { + if (!present) { params[argi].param = NULL; params[argi].length = 0; } else { - params[argi].length = &CHARAT(p) - params[argi].param; + params[argi].length = (size_t)(&CHARAT(p) - params[argi].param); } present = 0; argi++; - if(argi > 16) + if (argi > 16) { break; - } - else if(c >= 0x20 && c <= 0x2f) { - *commandp |= c << 16; + } + } else if (c >= 0x20 && c <= 0x2f) { + *commandp |= (unsigned)(c << 16); break; } p++; } - if(present) { - params[argi].length = &CHARAT(p) - params[argi].param; + if (present) { + params[argi].length = (size_t)(&CHARAT(p) - params[argi].param); argi++; } - *nargs = argi; + *nargs = (size_t)argi; *csi_len = csi_end + 1; return TERMKEY_RES_KEY; } -TermKeyResult termkey_interpret_csi(TermKey *tk, const TermKeyKey *key, TermKeyCsiParam params[], size_t *nparams, unsigned long *cmd) +TermKeyResult termkey_interpret_csi(TermKey *tk, const TermKeyKey *key, TermKeyCsiParam params[], + size_t *nparams, unsigned *cmd) { size_t dummy; - if(tk->hightide == 0) + if (tk->hightide == 0) { return TERMKEY_RES_NONE; - if(key->type != TERMKEY_TYPE_UNKNOWN_CSI) + } + if (key->type != TERMKEY_TYPE_UNKNOWN_CSI) { return TERMKEY_RES_NONE; + } return parse_csi(tk, 0, &dummy, params, nparams, cmd); } -TermKeyResult termkey_interpret_csi_param(TermKeyCsiParam param, long *paramp, long subparams[], size_t *nsubparams) +TermKeyResult termkey_interpret_csi_param(TermKeyCsiParam param, int *paramp, int subparams[], + size_t *nsubparams) { if (paramp == NULL) { return TERMKEY_RES_ERROR; @@ -508,7 +520,7 @@ TermKeyResult termkey_interpret_csi_param(TermKeyCsiParam param, long *paramp, l return TERMKEY_RES_KEY; } - long arg = 0; + int arg = 0; size_t i = 0; size_t capacity = nsubparams ? *nsubparams : 0; size_t length = 0; @@ -547,14 +559,15 @@ static int register_keys(void) { int i; - for(i = 0; i < 64; i++) { + for (i = 0; i < 64; i++) { csi_ss3s[i].sym = TERMKEY_SYM_UNKNOWN; - ss3s[i].sym = TERMKEY_SYM_UNKNOWN; + ss3s[i].sym = TERMKEY_SYM_UNKNOWN; ss3_kpalts[i] = 0; } - for(i = 0; i < NCSIFUNCS; i++) + for (i = 0; i < NCSIFUNCS; i++) { csifuncs[i].sym = TERMKEY_SYM_UNKNOWN; + } register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UP, 'A'); register_csi_ss3(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_DOWN, 'B'); @@ -568,7 +581,8 @@ static int register_keys(void) register_csi_ss3(TERMKEY_TYPE_FUNCTION, 3, 'R'); register_csi_ss3(TERMKEY_TYPE_FUNCTION, 4, 'S'); - register_csi_ss3_full(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_TAB, TERMKEY_KEYMOD_SHIFT, TERMKEY_KEYMOD_SHIFT, 'Z'); + register_csi_ss3_full(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_TAB, TERMKEY_KEYMOD_SHIFT, + TERMKEY_KEYMOD_SHIFT, 'Z'); register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPENTER, 'M', 0); register_ss3kpalt(TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_KPEQUALS, 'X', '='); @@ -632,15 +646,15 @@ static int register_keys(void) return 1; } -static void *new_driver(TermKey *tk, const char *term) +void *new_driver_csi(TermKey *tk, const char *term) { - if(!keyinfo_initialised) - if(!register_keys()) + if (!keyinfo_initialised) { + if (!register_keys()) { return NULL; + } + } - TermKeyCsi *csi = malloc(sizeof *csi); - if(!csi) - return NULL; + TermKeyCsi *csi = xmalloc(sizeof *csi); csi->tk = tk; csi->saved_string_id = 0; @@ -649,28 +663,31 @@ static void *new_driver(TermKey *tk, const char *term) return csi; } -static void free_driver(void *info) +void free_driver_csi(void *info) { TermKeyCsi *csi = info; - if(csi->saved_string) - free(csi->saved_string); + if (csi->saved_string) { + xfree(csi->saved_string); + } - free(csi); + xfree(csi); } -static TermKeyResult peekkey_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, int force, size_t *nbytep) +static TermKeyResult peekkey_csi_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, + int force, size_t *nbytep) { size_t csi_len; size_t nparams = 16; TermKeyCsiParam params[16]; - unsigned long cmd; + unsigned cmd; TermKeyResult ret = parse_csi(tk, introlen, &csi_len, params, &nparams, &cmd); - if(ret == TERMKEY_RES_AGAIN) { - if(!force) + if (ret == TERMKEY_RES_AGAIN) { + if (!force) { return TERMKEY_RES_AGAIN; + } (*tk->method.emit_codepoint)(tk, '[', key); key->modifiers |= TERMKEY_KEYMOD_ALT; @@ -678,7 +695,7 @@ static TermKeyResult peekkey_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, return TERMKEY_RES_KEY; } - if(cmd == 'M' && nparams < 3) { // Mouse in X10 encoding consumes the next 3 bytes also + if (cmd == 'M' && nparams < 3) { // Mouse in X10 encoding consumes the next 3 bytes also tk->buffstart += csi_len; tk->buffcount -= csi_len; @@ -687,8 +704,9 @@ static TermKeyResult peekkey_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, tk->buffstart -= csi_len; tk->buffcount += csi_len; - if(mouse_result == TERMKEY_RES_KEY) + if (mouse_result == TERMKEY_RES_KEY) { *nbytep += csi_len; + } return mouse_result; } @@ -696,35 +714,38 @@ static TermKeyResult peekkey_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyResult result = TERMKEY_RES_NONE; // We know from the logic above that cmd must be >= 0x40 and < 0x80 - if(csi_handlers[(cmd & 0xff) - 0x40]) - result = (*csi_handlers[(cmd & 0xff) - 0x40])(tk, key, cmd, params, nparams); + if (csi_handlers[(cmd & 0xff) - 0x40]) { + result = (*csi_handlers[(cmd & 0xff) - 0x40])(tk, key, (int)cmd, params, (int)nparams); + } - if(result == TERMKEY_RES_NONE) { + if (result == TERMKEY_RES_NONE) { #ifdef DEBUG - switch(args) { - case 0: - fprintf(stderr, "CSI: Unknown cmd=%c\n", (char)cmd); - break; - case 1: - fprintf(stderr, "CSI: Unknown arg1=%ld cmd=%c\n", arg[0], (char)cmd); - break; - case 2: - fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld cmd=%c\n", arg[0], arg[1], (char)cmd); - break; - case 3: - fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld arg3=%ld cmd=%c\n", arg[0], arg[1], arg[2], (char)cmd); - break; - default: - fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld arg3=%ld ... args=%d cmd=%c\n", arg[0], arg[1], arg[2], args, (char)cmd); - break; + switch (args) { + case 0: + fprintf(stderr, "CSI: Unknown cmd=%c\n", (char)cmd); + break; + case 1: + fprintf(stderr, "CSI: Unknown arg1=%ld cmd=%c\n", arg[0], (char)cmd); + break; + case 2: + fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld cmd=%c\n", arg[0], arg[1], (char)cmd); + break; + case 3: + fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld arg3=%ld cmd=%c\n", arg[0], arg[1], arg[2], + (char)cmd); + break; + default: + fprintf(stderr, "CSI: Unknown arg1=%ld arg2=%ld arg3=%ld ... args=%d cmd=%c\n", arg[0], + arg[1], arg[2], args, (char)cmd); + break; } #endif key->type = TERMKEY_TYPE_UNKNOWN_CSI; - key->code.number = cmd; + key->code.number = (int)cmd; key->modifiers = 0; tk->hightide = csi_len - introlen; - *nbytep = introlen; // Do not yet eat the data bytes + *nbytep = introlen; // Do not yet eat the data bytes return TERMKEY_RES_KEY; } @@ -732,11 +753,13 @@ static TermKeyResult peekkey_csi(TermKey *tk, TermKeyCsi *csi, size_t introlen, return result; } -static TermKeyResult peekkey_ss3(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, int force, size_t *nbytep) +static TermKeyResult peekkey_ss3(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, + int force, size_t *nbytep) { - if(tk->buffcount < introlen + 1) { - if(!force) + if (tk->buffcount < introlen + 1) { + if (!force) { return TERMKEY_RES_AGAIN; + } (*tk->method.emit_codepoint)(tk, 'O', key); key->modifiers |= TERMKEY_KEYMOD_ALT; @@ -746,30 +769,30 @@ static TermKeyResult peekkey_ss3(TermKey *tk, TermKeyCsi *csi, size_t introlen, unsigned char cmd = CHARAT(introlen); - if(cmd < 0x40 || cmd >= 0x80) + if (cmd < 0x40 || cmd >= 0x80) { return TERMKEY_RES_NONE; + } key->type = csi_ss3s[cmd - 0x40].type; key->code.sym = csi_ss3s[cmd - 0x40].sym; key->modifiers = csi_ss3s[cmd - 0x40].modifier_set; - if(key->code.sym == TERMKEY_SYM_UNKNOWN) { - if(tk->flags & TERMKEY_FLAG_CONVERTKP && ss3_kpalts[cmd - 0x40]) { + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { + if (tk->flags & TERMKEY_FLAG_CONVERTKP && ss3_kpalts[cmd - 0x40]) { key->type = TERMKEY_TYPE_UNICODE; - key->code.codepoint = ss3_kpalts[cmd - 0x40]; + key->code.codepoint = (unsigned char)ss3_kpalts[cmd - 0x40]; key->modifiers = 0; - key->utf8[0] = key->code.codepoint; + key->utf8[0] = (char)key->code.codepoint; key->utf8[1] = 0; - } - else { + } else { key->type = ss3s[cmd - 0x40].type; key->code.sym = ss3s[cmd - 0x40].sym; key->modifiers = ss3s[cmd - 0x40].modifier_set; } } - if(key->code.sym == TERMKEY_SYM_UNKNOWN) { + if (key->code.sym == TERMKEY_SYM_UNKNOWN) { #ifdef DEBUG fprintf(stderr, "CSI: Unknown SS3 %c (0x%02x)\n", (char)cmd, cmd); #endif @@ -781,123 +804,99 @@ static TermKeyResult peekkey_ss3(TermKey *tk, TermKeyCsi *csi, size_t introlen, return TERMKEY_RES_KEY; } -static TermKeyResult peekkey_ctrlstring(TermKey *tk, TermKeyCsi *csi, size_t introlen, TermKeyKey *key, int force, size_t *nbytep) +static TermKeyResult peekkey_ctrlstring(TermKey *tk, TermKeyCsi *csi, size_t introlen, + TermKeyKey *key, int force, size_t *nbytep) { size_t str_end = introlen; - while(str_end < tk->buffcount) { - if(CHARAT(str_end) == 0x07) // BEL + while (str_end < tk->buffcount) { + if (CHARAT(str_end) == 0x07) { // BEL break; - if(CHARAT(str_end) == 0x9c) // ST + } + if (CHARAT(str_end) == 0x9c) { // ST break; - if(CHARAT(str_end) == 0x1b && - (str_end + 1) < tk->buffcount && - CHARAT(str_end+1) == 0x5c) // ESC-prefixed ST + } + if (CHARAT(str_end) == 0x1b + && (str_end + 1) < tk->buffcount + && CHARAT(str_end + 1) == 0x5c) { // ESC-prefixed ST break; + } str_end++; } - if(str_end >= tk->buffcount) + if (str_end >= tk->buffcount) { return TERMKEY_RES_AGAIN; + } #ifdef DEBUG fprintf(stderr, "Found a control string: %*s", - str_end - introlen, tk->buffer + tk->buffstart + introlen); + str_end - introlen, tk->buffer + tk->buffstart + introlen); #endif *nbytep = str_end + 1; - if(CHARAT(str_end) == 0x1b) + if (CHARAT(str_end) == 0x1b) { (*nbytep)++; + } - if(csi->saved_string) - free(csi->saved_string); + if (csi->saved_string) { + xfree(csi->saved_string); + } size_t len = str_end - introlen; csi->saved_string_id++; - csi->saved_string = malloc(len + 1); + csi->saved_string = xmalloc(len + 1); - strncpy(csi->saved_string, (char *)tk->buffer + tk->buffstart + introlen, len); + strncpy(csi->saved_string, (char *)tk->buffer + tk->buffstart + introlen, len); // NOLINT(runtime/printf) csi->saved_string[len] = 0; - key->type = (CHARAT(introlen-1) & 0x1f) == 0x10 ? - TERMKEY_TYPE_DCS : TERMKEY_TYPE_OSC; + key->type = (CHARAT(introlen - 1) & 0x1f) == 0x10 + ? TERMKEY_TYPE_DCS : TERMKEY_TYPE_OSC; key->code.number = csi->saved_string_id; key->modifiers = 0; return TERMKEY_RES_KEY; } -static TermKeyResult peekkey(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) +TermKeyResult peekkey_csi(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) { - if(tk->buffcount == 0) + if (tk->buffcount == 0) { return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + } TermKeyCsi *csi = info; - switch(CHARAT(0)) { - case 0x1b: - if(tk->buffcount < 2) - return TERMKEY_RES_NONE; - - switch(CHARAT(1)) { - case 0x4f: // ESC-prefixed SS3 - return peekkey_ss3(tk, csi, 2, key, force, nbytep); - - case 0x50: // ESC-prefixed DCS - case 0x5d: // ESC-prefixed OSC - return peekkey_ctrlstring(tk, csi, 2, key, force, nbytep); - - case 0x5b: // ESC-prefixed CSI - return peekkey_csi(tk, csi, 2, key, force, nbytep); - } - + switch (CHARAT(0)) { + case 0x1b: + if (tk->buffcount < 2) { return TERMKEY_RES_NONE; + } - case 0x8f: // SS3 - return peekkey_ss3(tk, csi, 1, key, force, nbytep); - - case 0x90: // DCS - case 0x9d: // OSC - return peekkey_ctrlstring(tk, csi, 1, key, force, nbytep); - - case 0x9b: // CSI - return peekkey_csi(tk, csi, 1, key, force, nbytep); - } - - return TERMKEY_RES_NONE; -} - -struct TermKeyDriver termkey_driver_csi = { - .name = "CSI", - - .new_driver = new_driver, - .free_driver = free_driver, + switch (CHARAT(1)) { + case 0x4f: // ESC-prefixed SS3 + return peekkey_ss3(tk, csi, 2, key, force, nbytep); - .peekkey = peekkey, -}; + case 0x50: // ESC-prefixed DCS + case 0x5d: // ESC-prefixed OSC + return peekkey_ctrlstring(tk, csi, 2, key, force, nbytep); -TermKeyResult termkey_interpret_string(TermKey *tk, const TermKeyKey *key, const char **strp) -{ - struct TermKeyDriverNode *p; - for(p = tk->drivers; p; p = p->next) - if(p->driver == &termkey_driver_csi) - break; - - if(!p) - return TERMKEY_RES_NONE; + case 0x5b: // ESC-prefixed CSI + return peekkey_csi_csi(tk, csi, 2, key, force, nbytep); + } - if(key->type != TERMKEY_TYPE_DCS && - key->type != TERMKEY_TYPE_OSC) return TERMKEY_RES_NONE; - TermKeyCsi *csi = p->info; + case 0x8f: // SS3 + return peekkey_ss3(tk, csi, 1, key, force, nbytep); - if(csi->saved_string_id != key->code.number) - return TERMKEY_RES_NONE; + case 0x90: // DCS + case 0x9d: // OSC + return peekkey_ctrlstring(tk, csi, 1, key, force, nbytep); - *strp = csi->saved_string; + case 0x9b: // CSI + return peekkey_csi_csi(tk, csi, 1, key, force, nbytep); + } - return TERMKEY_RES_KEY; + return TERMKEY_RES_NONE; } diff --git a/src/nvim/tui/termkey/driver-csi.h b/src/nvim/tui/termkey/driver-csi.h new file mode 100644 index 0000000000..0abd8b5c2e --- /dev/null +++ b/src/nvim/tui/termkey/driver-csi.h @@ -0,0 +1,7 @@ +#pragma once + +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-csi.h.generated.h" +#endif diff --git a/src/termkey/driver-ti.c b/src/nvim/tui/termkey/driver-ti.c index 1f8ee10808..09c6a35004 100644 --- a/src/termkey/driver-ti.c +++ b/src/nvim/tui/termkey/driver-ti.c @@ -1,33 +1,28 @@ -// we want strdup() -#define _XOPEN_SOURCE 600 - -#include "termkey.h" -#include "termkey-internal.h" - -#ifdef HAVE_UNIBILIUM -# include <unibilium.h> -#else -# include <curses.h> -# include <term.h> - -/* curses.h has just polluted our namespace. We want this back */ -# undef buttons -#endif - #include <ctype.h> #include <errno.h> #include <stdbool.h> #include <stdio.h> #include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unibilium.h> + +#include "nvim/memory.h" +#include "nvim/tui/termkey/driver-ti.h" +#include "nvim/tui/termkey/termkey-internal.h" +#include "nvim/tui/termkey/termkey.h" + #ifndef _WIN32 # include <unistd.h> #else # include <io.h> #endif -#include <sys/types.h> -#include <sys/stat.h> -#define streq(a,b) (!strcmp(a,b)) +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-ti.c.generated.h" +#endif + +#define streq(a, b) (!strcmp(a, b)) #define MAX_FUNCNAME 9 @@ -36,9 +31,8 @@ static struct { TermKeyType type; TermKeySym sym; int mods; -} funcs[] = -{ - /* THIS LIST MUST REMAIN SORTED! */ +} funcs[] = { + // THIS LIST MUST REMAIN SORTED! { "backspace", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BACKSPACE, 0 }, { "begin", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 0 }, { "beg", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_BEGIN, 0 }, @@ -61,12 +55,12 @@ static struct { { "mark", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MARK, 0 }, { "message", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MESSAGE, 0 }, { "move", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_MOVE, 0 }, - { "next", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, // Not quite, but it's the best we can do + { "next", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, // Not quite, but it's the best we can do { "npage", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEDOWN, 0 }, { "open", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPEN, 0 }, { "options", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_OPTIONS, 0 }, { "ppage", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, - { "previous", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, // Not quite, but it's the best we can do + { "previous", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PAGEUP, 0 }, // Not quite, but it's the best we can do { "print", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_PRINT, 0 }, { "redo", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REDO, 0 }, { "reference", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_REFERENCE, 0 }, @@ -80,34 +74,34 @@ static struct { { "suspend", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_SUSPEND, 0 }, { "undo", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UNDO, 0 }, { "up", TERMKEY_TYPE_KEYSYM, TERMKEY_SYM_UP, 0 }, - { NULL }, + { NULL, 0, 0, 0 }, }; -#ifdef HAVE_UNIBILIUM static enum unibi_string unibi_lookup_str(const char *name) { - for(enum unibi_string ret = unibi_string_begin_+1; ret < unibi_string_end_; ret++) - if(streq(unibi_name_str(ret), name)) + for (enum unibi_string ret = unibi_string_begin_ + 1; ret < unibi_string_end_; ret++) { + if (streq(unibi_name_str(ret), name)) { return ret; + } + } - return -1; + return (enum unibi_string)-1; } static const char *unibi_get_str_by_name(const unibi_term *ut, const char *name) { enum unibi_string idx = unibi_lookup_str(name); - if(idx == (enum unibi_string)-1) + if (idx == (enum unibi_string)-1) { return NULL; + } return unibi_get_str(ut, idx); } -#endif -/* To be efficient at lookups, we store the byte sequence => keyinfo mapping - * in a trie. This avoids a slow linear search through a flat list of - * sequences. Because it is likely most nodes will be very sparse, we optimise - * vector to store an extent map after the database is loaded. - */ +// To be efficient at lookups, we store the byte sequence => keyinfo mapping +// in a trie. This avoids a slow linear search through a flat list of +// sequences. Because it is likely most nodes will be very sparse, we optimise +// vector to store an extent map after the database is loaded. typedef enum { TYPE_KEY, @@ -125,126 +119,110 @@ struct trie_node_key { struct trie_node_arr { trie_nodetype type; - unsigned char min, max; /* INCLUSIVE endpoints of the extent range */ - struct trie_node *arr[]; /* dynamic size at allocation time */ + unsigned char min, max; // INCLUSIVE endpoints of the extent range + struct trie_node *arr[]; // dynamic size at allocation time }; -typedef struct { - TermKey *tk; - -#ifdef HAVE_UNIBILIUM - unibi_term *unibi; /* only valid until first 'start' call */ -#else - char *term; /* only valid until first 'start' call */ -#endif - - struct trie_node *root; - - char *start_string; - char *stop_string; -} TermKeyTI; - static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node); static struct trie_node *new_node_key(TermKeyType type, TermKeySym sym, int modmask, int modset) { - struct trie_node_key *n = malloc(sizeof(*n)); - if(!n) - return NULL; + struct trie_node_key *n = xmalloc(sizeof(*n)); n->type = TYPE_KEY; n->key.type = type; - n->key.sym = sym; + n->key.sym = sym; n->key.modifier_mask = modmask; - n->key.modifier_set = modset; + n->key.modifier_set = modset; - return (struct trie_node*)n; + return (struct trie_node *)n; } static struct trie_node *new_node_arr(unsigned char min, unsigned char max) { - struct trie_node_arr *n = malloc(sizeof(*n) + ((int)max-min+1) * sizeof(n->arr[0])); - if(!n) - return NULL; + struct trie_node_arr *n = xmalloc(sizeof(*n) + (max - min + 1) * sizeof(n->arr[0])); n->type = TYPE_ARR; n->min = min; n->max = max; int i; - for(i = min; i <= max; i++) - n->arr[i-min] = NULL; + for (i = min; i <= max; i++) { + n->arr[i - min] = NULL; + } - return (struct trie_node*)n; + return (struct trie_node *)n; } static struct trie_node *lookup_next(struct trie_node *n, unsigned char b) { - switch(n->type) { + switch (n->type) { case TYPE_KEY: fprintf(stderr, "ABORT: lookup_next within a TYPE_KEY node\n"); abort(); - case TYPE_ARR: - { - struct trie_node_arr *nar = (struct trie_node_arr*)n; - if(b < nar->min || b > nar->max) - return NULL; - return nar->arr[b - nar->min]; + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)n; + if (b < nar->min || b > nar->max) { + return NULL; } + return nar->arr[b - nar->min]; + } } - return NULL; // Never reached but keeps compiler happy + return NULL; // Never reached but keeps compiler happy } static void free_trie(struct trie_node *n) { - switch(n->type) { + switch (n->type) { case TYPE_KEY: break; - case TYPE_ARR: - { - struct trie_node_arr *nar = (struct trie_node_arr*)n; - int i; - for(i = nar->min; i <= nar->max; i++) - if(nar->arr[i - nar->min]) - free_trie(nar->arr[i - nar->min]); - break; + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)n; + int i; + for (i = nar->min; i <= nar->max; i++) { + if (nar->arr[i - nar->min]) { + free_trie(nar->arr[i - nar->min]); + } } + break; + } } - free(n); + xfree(n); } static struct trie_node *compress_trie(struct trie_node *n) { - if(!n) + if (!n) { return NULL; + } - switch(n->type) { + switch (n->type) { case TYPE_KEY: return n; - case TYPE_ARR: - { - struct trie_node_arr *nar = (struct trie_node_arr*)n; - unsigned char min, max; - // Find the real bounds - for(min = 0; !nar->arr[min]; min++) - if(min == 255 && !nar->arr[min]) { - free(nar); - return new_node_arr(1, 0); - } - - for(max = 0xff; !nar->arr[max]; max--) - ; - - struct trie_node_arr *new = (struct trie_node_arr*)new_node_arr(min, max); - int i; - for(i = min; i <= max; i++) - new->arr[i - min] = compress_trie(nar->arr[i]); - - free(nar); - return (struct trie_node*)new; + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)n; + unsigned char min, max; + // Find the real bounds + for (min = 0; !nar->arr[min]; min++) { + if (min == 255 && !nar->arr[min]) { + xfree(nar); + return new_node_arr(1, 0); + } + } + + for (max = 0xff; !nar->arr[max]; max--) {} + + struct trie_node_arr *new = (struct trie_node_arr *)new_node_arr(min, max); + int i; + for (i = min; i <= max; i++) { + new->arr[i - min] = compress_trie(nar->arr[i]); } + + xfree(nar); + return (struct trie_node *)new; + } } return n; @@ -254,21 +232,20 @@ static bool try_load_terminfo_key(TermKeyTI *ti, const char *name, struct keyinf { const char *value = NULL; -#ifdef HAVE_UNIBILIUM - if(ti->unibi) + if (ti->unibi) { value = unibi_get_str_by_name(ti->unibi, name); -#else - if(ti->term) - value = tigetstr(name); -#endif + } - if(ti->tk->ti_getstr_hook) + if (ti->tk->ti_getstr_hook) { value = (ti->tk->ti_getstr_hook)(name, value, ti->tk->ti_getstr_hook_data); + } - if(!value || value == (char*)-1 || !value[0]) + if (!value || value == (char *)-1 || !value[0]) { return false; + } - struct trie_node *node = new_node_key(info->type, info->sym, info->modifier_mask, info->modifier_set); + struct trie_node *node = new_node_key(info->type, info->sym, info->modifier_mask, + info->modifier_set); insert_seq(ti, value, node); return true; @@ -278,288 +255,257 @@ static int load_terminfo(TermKeyTI *ti) { int i; -#ifdef HAVE_UNIBILIUM unibi_term *unibi = ti->unibi; -#else - { - int err; - - /* Have to cast away the const. But it's OK - we know terminfo won't really - * modify term */ - if(setupterm((char*)ti->term, 1, &err) != OK) - return 0; - } -#endif ti->root = new_node_arr(0, 0xff); - if(!ti->root) + if (!ti->root) { return 0; + } - /* First the regular key strings - */ - for(i = 0; funcs[i].funcname; i++) { + // First the regular key strings + for (i = 0; funcs[i].funcname; i++) { char name[MAX_FUNCNAME + 5 + 1]; - sprintf(name, "key_%s", funcs[i].funcname); - if(!try_load_terminfo_key(ti, name, &(struct keyinfo){ - .type = funcs[i].type, - .sym = funcs[i].sym, - .modifier_mask = funcs[i].mods, - .modifier_set = funcs[i].mods, - })) + sprintf(name, "key_%s", funcs[i].funcname); // NOLINT(runtime/printf) + if (!try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = funcs[i].type, + .sym = funcs[i].sym, + .modifier_mask = funcs[i].mods, + .modifier_set = funcs[i].mods, + })) { continue; + } - /* Maybe it has a shifted version */ - sprintf(name, "key_s%s", funcs[i].funcname); + // Maybe it has a shifted version + sprintf(name, "key_s%s", funcs[i].funcname); // NOLINT(runtime/printf) try_load_terminfo_key(ti, name, &(struct keyinfo){ - .type = funcs[i].type, - .sym = funcs[i].sym, - .modifier_mask = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, - .modifier_set = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, + .type = funcs[i].type, + .sym = funcs[i].sym, + .modifier_mask = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, + .modifier_set = funcs[i].mods | TERMKEY_KEYMOD_SHIFT, }); } - /* Now the F<digit> keys - */ - for(i = 1; i < 255; i++) { + // Now the F<digit> keys + for (i = 1; i < 255; i++) { char name[9]; - sprintf(name, "key_f%d", i); - if(!try_load_terminfo_key(ti, name, &(struct keyinfo){ - .type = TERMKEY_TYPE_FUNCTION, - .sym = i, - .modifier_mask = 0, - .modifier_set = 0, - })) + sprintf(name, "key_f%d", i); // NOLINT(runtime/printf) + if (!try_load_terminfo_key(ti, name, &(struct keyinfo){ + .type = TERMKEY_TYPE_FUNCTION, + .sym = i, + .modifier_mask = 0, + .modifier_set = 0, + })) { break; + } } - /* Finally mouse mode */ + // Finally mouse mode { const char *value = NULL; -#ifdef HAVE_UNIBILIUM - if(ti->unibi) + if (ti->unibi) { value = unibi_get_str_by_name(ti->unibi, "key_mouse"); -#else - if(ti->term) - value = tigetstr("key_mouse"); -#endif + } - if(ti->tk->ti_getstr_hook) + if (ti->tk->ti_getstr_hook) { value = (ti->tk->ti_getstr_hook)("key_mouse", value, ti->tk->ti_getstr_hook_data); + } - /* Some terminfos (e.g. xterm-1006) claim a different key_mouse that won't - * give X10 encoding. We'll only accept this if it's exactly "\e[M" - */ - if(value && streq(value, "\x1b[M")) { + // Some terminfos (e.g. xterm-1006) claim a different key_mouse that won't + // give X10 encoding. We'll only accept this if it's exactly "\e[M" + if (value && streq(value, "\x1b[M")) { struct trie_node *node = new_node_key(TERMKEY_TYPE_MOUSE, 0, 0, 0); insert_seq(ti, value, node); } } - /* Take copies of these terminfo strings, in case we build multiple termkey - * instances for multiple different termtypes, and it's different by the - * time we want to use it - */ -#ifdef HAVE_UNIBILIUM - const char *keypad_xmit = unibi ? - unibi_get_str(unibi, unibi_keypad_xmit) : - NULL; -#endif + // Take copies of these terminfo strings, in case we build multiple termkey + // instances for multiple different termtypes, and it's different by the + // time we want to use it + const char *keypad_xmit = unibi + ? unibi_get_str(unibi, unibi_keypad_xmit) + : NULL; - if(keypad_xmit) - ti->start_string = strdup(keypad_xmit); - else + if (keypad_xmit) { + ti->start_string = xstrdup(keypad_xmit); + } else { ti->start_string = NULL; + } -#ifdef HAVE_UNIBILIUM - const char *keypad_local = unibi ? - unibi_get_str(unibi, unibi_keypad_local) : - NULL; -#endif + const char *keypad_local = unibi + ? unibi_get_str(unibi, unibi_keypad_local) + : NULL; - if(keypad_local) - ti->stop_string = strdup(keypad_local); - else + if (keypad_local) { + ti->stop_string = xstrdup(keypad_local); + } else { ti->stop_string = NULL; + } -#ifdef HAVE_UNIBILIUM - if(unibi) + if (unibi) { unibi_destroy(unibi); + } ti->unibi = NULL; -#else - if(ti->term) - free(ti->term); - - ti->term = NULL; -#endif ti->root = compress_trie(ti->root); return 1; } -static void *new_driver(TermKey *tk, const char *term) +void *new_driver_ti(TermKey *tk, const char *term) { - TermKeyTI *ti = malloc(sizeof *ti); - if(!ti) - return NULL; + TermKeyTI *ti = xmalloc(sizeof *ti); ti->tk = tk; ti->root = NULL; ti->start_string = NULL; ti->stop_string = NULL; -#ifdef HAVE_UNIBILIUM ti->unibi = unibi_from_term(term); int saved_errno = errno; - if(!ti->unibi && saved_errno != ENOENT) { - free(ti); + if (!ti->unibi && saved_errno != ENOENT) { + xfree(ti); return NULL; } - /* ti->unibi may be NULL if errno == ENOENT. That means the terminal wasn't - * known. Lets keep going because if we get getstr hook that might invent - * new strings for us - */ -#else - { - int err; - - ti->term = NULL; - - /* Have to cast away the const. But it's OK - we know terminfo won't really - * modify term */ - if(setupterm((char*)term, 1, &err) == OK) - ti->term = strdup(term); - } -#endif + // ti->unibi may be NULL if errno == ENOENT. That means the terminal wasn't + // known. Lets keep going because if we get getstr hook that might invent + // new strings for us return ti; } -static int start_driver(TermKey *tk, void *info) +int start_driver_ti(TermKey *tk, void *info) { TermKeyTI *ti = info; struct stat statbuf; char *start_string; size_t len; - if(!ti->root) + if (!ti->root) { load_terminfo(ti); + } start_string = ti->start_string; - if(tk->fd == -1 || !start_string) + if (tk->fd == -1 || !start_string) { return 1; + } - /* The terminfo database will contain keys in application cursor key mode. - * We may need to enable that mode - */ + // The terminfo database will contain keys in application cursor key mode. + // We may need to enable that mode - /* There's no point trying to write() to a pipe */ - if(fstat(tk->fd, &statbuf) == -1) + // There's no point trying to write() to a pipe + if (fstat(tk->fd, &statbuf) == -1) { return 0; + } #ifndef _WIN32 - if(S_ISFIFO(statbuf.st_mode)) + if (S_ISFIFO(statbuf.st_mode)) { return 1; + } #endif // Can't call putp or tputs because they suck and don't give us fd control len = strlen(start_string); - while(len) { - size_t written = write(tk->fd, start_string, len); - if(written == -1) + while (len) { + size_t written = (size_t)write(tk->fd, start_string, (unsigned)len); + if (written == (size_t)-1) { return 0; + } start_string += written; len -= written; } return 1; } -static int stop_driver(TermKey *tk, void *info) +int stop_driver_ti(TermKey *tk, void *info) { TermKeyTI *ti = info; struct stat statbuf; char *stop_string = ti->stop_string; size_t len; - if(tk->fd == -1 || !stop_string) + if (tk->fd == -1 || !stop_string) { return 1; + } - /* There's no point trying to write() to a pipe */ - if(fstat(tk->fd, &statbuf) == -1) + // There's no point trying to write() to a pipe + if (fstat(tk->fd, &statbuf) == -1) { return 0; + } #ifndef _WIN32 - if(S_ISFIFO(statbuf.st_mode)) + if (S_ISFIFO(statbuf.st_mode)) { return 1; + } #endif - /* The terminfo database will contain keys in application cursor key mode. - * We may need to enable that mode - */ + // The terminfo database will contain keys in application cursor key mode. + // We may need to enable that mode // Can't call putp or tputs because they suck and don't give us fd control len = strlen(stop_string); - while(len) { - size_t written = write(tk->fd, stop_string, len); - if(written == -1) + while (len) { + size_t written = (size_t)write(tk->fd, stop_string, (unsigned)len); + if (written == (size_t)-1) { return 0; + } stop_string += written; len -= written; } return 1; } -static void free_driver(void *info) +void free_driver_ti(void *info) { TermKeyTI *ti = info; free_trie(ti->root); - if(ti->start_string) - free(ti->start_string); + if (ti->start_string) { + xfree(ti->start_string); + } - if(ti->stop_string) - free(ti->stop_string); + if (ti->stop_string) { + xfree(ti->stop_string); + } -#ifdef HAVE_UNIBILIUM - if(ti->unibi) + if (ti->unibi) { unibi_destroy(ti->unibi); -#else - if(ti->term) - free(ti->term); -#endif + } - free(ti); + xfree(ti); } #define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) -static TermKeyResult peekkey(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) +TermKeyResult peekkey_ti(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytep) { TermKeyTI *ti = info; - if(tk->buffcount == 0) + if (tk->buffcount == 0) { return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + } struct trie_node *p = ti->root; - unsigned int pos = 0; - while(pos < tk->buffcount) { + unsigned pos = 0; + while (pos < tk->buffcount) { p = lookup_next(p, CHARAT(pos)); - if(!p) + if (!p) { break; + } pos++; - if(p->type != TYPE_KEY) + if (p->type != TYPE_KEY) { continue; + } - struct trie_node_key *nk = (struct trie_node_key*)p; - if(nk->key.type == TERMKEY_TYPE_MOUSE) { + struct trie_node_key *nk = (struct trie_node_key *)p; + if (nk->key.type == TERMKEY_TYPE_MOUSE) { tk->buffstart += pos; tk->buffcount -= pos; @@ -568,14 +514,15 @@ static TermKeyResult peekkey(TermKey *tk, void *info, TermKeyKey *key, int force tk->buffstart -= pos; tk->buffcount += pos; - if(mouse_result == TERMKEY_RES_KEY) + if (mouse_result == TERMKEY_RES_KEY) { *nbytep += pos; + } return mouse_result; } - key->type = nk->key.type; - key->code.sym = nk->key.sym; + key->type = nk->key.type; + key->code.sym = nk->key.sym; key->modifiers = nk->key.modifier_set; *nbytep = pos; return TERMKEY_RES_KEY; @@ -583,8 +530,9 @@ static TermKeyResult peekkey(TermKey *tk, void *info, TermKeyKey *key, int force // If p is not NULL then we hadn't walked off the end yet, so we have a // partial match - if(p && !force) + if (p && !force) { return TERMKEY_RES_AGAIN; + } return TERMKEY_RES_NONE; } @@ -597,39 +545,42 @@ static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node) // Unsigned because we'll be using it as an array subscript unsigned char b; - while((b = seq[pos])) { + while ((b = (unsigned char)seq[pos])) { struct trie_node *next = lookup_next(p, b); - if(!next) + if (!next) { break; + } p = next; pos++; } - while((b = seq[pos])) { + while ((b = (unsigned char)seq[pos])) { struct trie_node *next; - if(seq[pos+1]) + if (seq[pos + 1]) { // Intermediate node next = new_node_arr(0, 0xff); - else + } else { // Final key node next = node; + } - if(!next) + if (!next) { return 0; + } - switch(p->type) { - case TYPE_ARR: - { - struct trie_node_arr *nar = (struct trie_node_arr*)p; - if(b < nar->min || b > nar->max) { - fprintf(stderr, "ASSERT FAIL: Trie insert at 0x%02x is outside of extent bounds (0x%02x..0x%02x)\n", - b, nar->min, nar->max); - abort(); - } - nar->arr[b - nar->min] = next; - p = next; - break; + switch (p->type) { + case TYPE_ARR: { + struct trie_node_arr *nar = (struct trie_node_arr *)p; + if (b < nar->min || b > nar->max) { + fprintf(stderr, + "ASSERT FAIL: Trie insert at 0x%02x is outside of extent bounds (0x%02x..0x%02x)\n", + b, nar->min, nar->max); + abort(); } + nar->arr[b - nar->min] = next; + p = next; + break; + } case TYPE_KEY: fprintf(stderr, "ASSERT FAIL: Tried to insert child node in TYPE_KEY\n"); abort(); @@ -640,15 +591,3 @@ static int insert_seq(TermKeyTI *ti, const char *seq, struct trie_node *node) return 1; } - -struct TermKeyDriver termkey_driver_ti = { - .name = "terminfo", - - .new_driver = new_driver, - .free_driver = free_driver, - - .start_driver = start_driver, - .stop_driver = stop_driver, - - .peekkey = peekkey, -}; diff --git a/src/nvim/tui/termkey/driver-ti.h b/src/nvim/tui/termkey/driver-ti.h new file mode 100644 index 0000000000..df9bd72d5b --- /dev/null +++ b/src/nvim/tui/termkey/driver-ti.h @@ -0,0 +1,7 @@ +#pragma once + +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/driver-ti.h.generated.h" +#endif diff --git a/src/termkey/termkey-internal.h b/src/nvim/tui/termkey/termkey-internal.h index c300b02616..107591f950 100644 --- a/src/termkey/termkey-internal.h +++ b/src/nvim/tui/termkey/termkey-internal.h @@ -1,31 +1,29 @@ -#ifndef GUARD_TERMKEY_INTERNAL_H_ -#define GUARD_TERMKEY_INTERNAL_H_ +#pragma once -#define HAVE_TERMIOS +#include <stdint.h> + +#include "nvim/tui/termkey/termkey_defs.h" +#define HAVE_TERMIOS #ifdef _WIN32 # undef HAVE_TERMIOS #endif -#include "termkey.h" - -#include <stdint.h> #ifdef HAVE_TERMIOS # include <termios.h> #endif #ifdef _MSC_VER -#include <BaseTsd.h> +# include <BaseTsd.h> typedef SSIZE_T ssize_t; #endif -struct TermKeyDriver -{ - const char *name; - void *(*new_driver)(TermKey *tk, const char *term); - void (*free_driver)(void *info); - int (*start_driver)(TermKey *tk, void *info); - int (*stop_driver)(TermKey *tk, void *info); +struct TermKeyDriver { + const char *name; + void *(*new_driver)(TermKey *tk, const char *term); + void (*free_driver)(void *info); + int (*start_driver)(TermKey *tk, void *info); + int (*stop_driver)(TermKey *tk, void *info); TermKeyResult (*peekkey)(TermKey *tk, void *info, TermKeyKey *key, int force, size_t *nbytes); }; @@ -38,21 +36,21 @@ struct keyinfo { struct TermKeyDriverNode; struct TermKeyDriverNode { - struct TermKeyDriver *driver; - void *info; + struct TermKeyDriver *driver; + void *info; struct TermKeyDriverNode *next; }; struct TermKey { - int fd; - int flags; - int canonflags; + int fd; + int flags; + int canonflags; unsigned char *buffer; - size_t buffstart; // First offset in buffer - size_t buffcount; // NUMBER of entires valid in buffer - size_t buffsize; // Total malloc'ed size - size_t hightide; /* Position beyond buffstart at which peekkey() should next start - * normally 0, but see also termkey_interpret_csi */ + size_t buffstart; // First offset in buffer + size_t buffcount; // NUMBER of entires valid in buffer + size_t buffsize; // Total malloc'ed size + size_t hightide; // Position beyond buffstart at which peekkey() should next start + // normally 0, but see also termkey_interpret_csi #ifdef HAVE_TERMIOS struct termios restore_termios; @@ -62,12 +60,12 @@ struct TermKey { TermKey_Terminfo_Getstr_Hook *ti_getstr_hook; void *ti_getstr_hook_data; - int waittime; // msec + int waittime; // msec - char is_closed; - char is_started; + char is_closed; + char is_started; - int nkeynames; + int nkeynames; const char **keynames; // There are 32 C0 codes @@ -78,7 +76,7 @@ struct TermKey { // Now some "protected" methods for the driver to call but which we don't // want exported as real symbols in the library struct { - void (*emit_codepoint)(TermKey *tk, long codepoint, TermKeyKey *key); + void (*emit_codepoint)(TermKey *tk, int codepoint, TermKeyKey *key); TermKeyResult (*peekkey_simple)(TermKey *tk, TermKeyKey *key, int force, size_t *nbytes); TermKeyResult (*peekkey_mouse)(TermKey *tk, TermKeyKey *key, size_t *nbytes); } method; @@ -86,27 +84,26 @@ struct TermKey { static inline void termkey_key_get_linecol(const TermKeyKey *key, int *line, int *col) { - if(col) - *col = (unsigned char)key->code.mouse[1] | ((unsigned char)key->code.mouse[3] & 0x0f) << 8; + if (col) { + *col = (unsigned char)key->code.mouse[1] | ((unsigned char)key->code.mouse[3] & 0x0f) << 8; + } - if(line) + if (line) { *line = (unsigned char)key->code.mouse[2] | ((unsigned char)key->code.mouse[3] & 0x70) << 4; + } } static inline void termkey_key_set_linecol(TermKeyKey *key, int line, int col) { - if(line > 0xfff) + if (line > 0xfff) { line = 0xfff; + } - if(col > 0x7ff) + if (col > 0x7ff) { col = 0x7ff; + } - key->code.mouse[1] = (line & 0x0ff); - key->code.mouse[2] = (col & 0x0ff); + key->code.mouse[1] = (char)(line & 0x0ff); + key->code.mouse[2] = (char)(col & 0x0ff); key->code.mouse[3] = (line & 0xf00) >> 8 | (col & 0x300) >> 4; } - -extern struct TermKeyDriver termkey_driver_csi; -extern struct TermKeyDriver termkey_driver_ti; - -#endif diff --git a/src/nvim/tui/termkey/termkey.c b/src/nvim/tui/termkey/termkey.c new file mode 100644 index 0000000000..e6440118f3 --- /dev/null +++ b/src/nvim/tui/termkey/termkey.c @@ -0,0 +1,1315 @@ +#include <ctype.h> +#include <errno.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> + +#include "nvim/mbyte.h" +#include "nvim/memory.h" +#include "nvim/tui/termkey/driver-csi.h" +#include "nvim/tui/termkey/driver-ti.h" +#include "nvim/tui/termkey/termkey-internal.h" +#include "nvim/tui/termkey/termkey.h" +#include "nvim/tui/termkey/termkey_defs.h" + +#ifndef _WIN32 +# include <poll.h> +# include <strings.h> +# include <unistd.h> +#else +# include <io.h> +#endif + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/termkey.c.generated.h" +#endif + +#ifdef _MSC_VER +# define strcaseeq(a, b) (_stricmp(a, b) == 0) +#else +# define strcaseeq(a, b) (strcasecmp(a, b) == 0) +#endif + +struct TermKeyDriver termkey_driver_ti = { + .name = "terminfo", + + .new_driver = new_driver_ti, + .free_driver = free_driver_ti, + + .start_driver = start_driver_ti, + .stop_driver = stop_driver_ti, + + .peekkey = peekkey_ti, +}; + +struct TermKeyDriver termkey_driver_csi = { + .name = "CSI", + + .new_driver = new_driver_csi, + .free_driver = free_driver_csi, + + .peekkey = peekkey_csi, +}; + +static struct TermKeyDriver *drivers[] = { + &termkey_driver_ti, + &termkey_driver_csi, + NULL, +}; + +static struct { + TermKeySym sym; + const char *name; +} keynames[] = { + { TERMKEY_SYM_NONE, "NONE" }, + { TERMKEY_SYM_BACKSPACE, "Backspace" }, + { TERMKEY_SYM_TAB, "Tab" }, + { TERMKEY_SYM_ENTER, "Enter" }, + { TERMKEY_SYM_ESCAPE, "Escape" }, + { TERMKEY_SYM_SPACE, "Space" }, + { TERMKEY_SYM_DEL, "DEL" }, + { TERMKEY_SYM_UP, "Up" }, + { TERMKEY_SYM_DOWN, "Down" }, + { TERMKEY_SYM_LEFT, "Left" }, + { TERMKEY_SYM_RIGHT, "Right" }, + { TERMKEY_SYM_BEGIN, "Begin" }, + { TERMKEY_SYM_FIND, "Find" }, + { TERMKEY_SYM_INSERT, "Insert" }, + { TERMKEY_SYM_DELETE, "Delete" }, + { TERMKEY_SYM_SELECT, "Select" }, + { TERMKEY_SYM_PAGEUP, "PageUp" }, + { TERMKEY_SYM_PAGEDOWN, "PageDown" }, + { TERMKEY_SYM_HOME, "Home" }, + { TERMKEY_SYM_END, "End" }, + { TERMKEY_SYM_CANCEL, "Cancel" }, + { TERMKEY_SYM_CLEAR, "Clear" }, + { TERMKEY_SYM_CLOSE, "Close" }, + { TERMKEY_SYM_COMMAND, "Command" }, + { TERMKEY_SYM_COPY, "Copy" }, + { TERMKEY_SYM_EXIT, "Exit" }, + { TERMKEY_SYM_HELP, "Help" }, + { TERMKEY_SYM_MARK, "Mark" }, + { TERMKEY_SYM_MESSAGE, "Message" }, + { TERMKEY_SYM_MOVE, "Move" }, + { TERMKEY_SYM_OPEN, "Open" }, + { TERMKEY_SYM_OPTIONS, "Options" }, + { TERMKEY_SYM_PRINT, "Print" }, + { TERMKEY_SYM_REDO, "Redo" }, + { TERMKEY_SYM_REFERENCE, "Reference" }, + { TERMKEY_SYM_REFRESH, "Refresh" }, + { TERMKEY_SYM_REPLACE, "Replace" }, + { TERMKEY_SYM_RESTART, "Restart" }, + { TERMKEY_SYM_RESUME, "Resume" }, + { TERMKEY_SYM_SAVE, "Save" }, + { TERMKEY_SYM_SUSPEND, "Suspend" }, + { TERMKEY_SYM_UNDO, "Undo" }, + { TERMKEY_SYM_KP0, "KP0" }, + { TERMKEY_SYM_KP1, "KP1" }, + { TERMKEY_SYM_KP2, "KP2" }, + { TERMKEY_SYM_KP3, "KP3" }, + { TERMKEY_SYM_KP4, "KP4" }, + { TERMKEY_SYM_KP5, "KP5" }, + { TERMKEY_SYM_KP6, "KP6" }, + { TERMKEY_SYM_KP7, "KP7" }, + { TERMKEY_SYM_KP8, "KP8" }, + { TERMKEY_SYM_KP9, "KP9" }, + { TERMKEY_SYM_KPENTER, "KPEnter" }, + { TERMKEY_SYM_KPPLUS, "KPPlus" }, + { TERMKEY_SYM_KPMINUS, "KPMinus" }, + { TERMKEY_SYM_KPMULT, "KPMult" }, + { TERMKEY_SYM_KPDIV, "KPDiv" }, + { TERMKEY_SYM_KPCOMMA, "KPComma" }, + { TERMKEY_SYM_KPPERIOD, "KPPeriod" }, + { TERMKEY_SYM_KPEQUALS, "KPEquals" }, + { 0, NULL }, +}; + +// Mouse event names +static const char *evnames[] = { "Unknown", "Press", "Drag", "Release" }; + +#define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) + +#ifdef DEBUG +// Some internal debugging functions + +static void print_buffer(TermKey *tk) +{ + int i; + for (i = 0; i < tk->buffcount && i < 20; i++) { + fprintf(stderr, "%02x ", CHARAT(i)); + } + if (tk->buffcount > 20) { + fprintf(stderr, "..."); + } +} + +static void print_key(TermKey *tk, TermKeyKey *key) +{ + switch (key->type) { + case TERMKEY_TYPE_UNICODE: + fprintf(stderr, "Unicode codepoint=U+%04lx utf8='%s'", key->code.codepoint, key->utf8); + break; + case TERMKEY_TYPE_FUNCTION: + fprintf(stderr, "Function F%d", key->code.number); + break; + case TERMKEY_TYPE_KEYSYM: + fprintf(stderr, "Keysym sym=%d(%s)", key->code.sym, termkey_get_keyname(tk, key->code.sym)); + break; + case TERMKEY_TYPE_MOUSE: { + TermKeyMouseEvent ev; + int button, line, col; + termkey_interpret_mouse(tk, key, &ev, &button, &line, &col); + fprintf(stderr, "Mouse ev=%d button=%d pos=(%d,%d)\n", ev, button, line, col); + } + break; + case TERMKEY_TYPE_POSITION: { + int line, col; + termkey_interpret_position(tk, key, &line, &col); + fprintf(stderr, "Position report pos=(%d,%d)\n", line, col); + } + break; + case TERMKEY_TYPE_MODEREPORT: { + int initial, mode, value; + termkey_interpret_modereport(tk, key, &initial, &mode, &value); + fprintf(stderr, "Mode report mode=%s %d val=%d\n", initial == '?' ? "DEC" : "ANSI", mode, + value); + } + break; + case TERMKEY_TYPE_DCS: + fprintf(stderr, "Device Control String"); + break; + case TERMKEY_TYPE_OSC: + fprintf(stderr, "Operating System Control"); + break; + case TERMKEY_TYPE_UNKNOWN_CSI: + fprintf(stderr, "unknown CSI\n"); + break; + } + + int m = key->modifiers; + fprintf(stderr, " mod=%s%s%s+%02x", + (m & TERMKEY_KEYMOD_CTRL ? "C" : ""), + (m & TERMKEY_KEYMOD_ALT ? "A" : ""), + (m & TERMKEY_KEYMOD_SHIFT ? "S" : ""), + m & ~(TERMKEY_KEYMOD_CTRL|TERMKEY_KEYMOD_ALT|TERMKEY_KEYMOD_SHIFT)); +} + +static const char *res2str(TermKeyResult res) +{ + static char errorbuffer[256]; + + switch (res) { + case TERMKEY_RES_KEY: + return "TERMKEY_RES_KEY"; + case TERMKEY_RES_EOF: + return "TERMKEY_RES_EOF"; + case TERMKEY_RES_AGAIN: + return "TERMKEY_RES_AGAIN"; + case TERMKEY_RES_NONE: + return "TERMKEY_RES_NONE"; + case TERMKEY_RES_ERROR: + snprintf(errorbuffer, sizeof errorbuffer, "TERMKEY_RES_ERROR(errno=%d)\n", errno); + return (const char *)errorbuffer; + } + + return "unknown"; +} +#endif + +TermKeyResult termkey_interpret_string(TermKey *tk, const TermKeyKey *key, const char **strp) +{ + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + if (p->driver == &termkey_driver_csi) { + break; + } + } + + if (!p) { + return TERMKEY_RES_NONE; + } + + if (key->type != TERMKEY_TYPE_DCS + && key->type != TERMKEY_TYPE_OSC) { + return TERMKEY_RES_NONE; + } + + TermKeyCsi *csi = p->info; + + if (csi->saved_string_id != key->code.number) { + return TERMKEY_RES_NONE; + } + + *strp = csi->saved_string; + + return TERMKEY_RES_KEY; +} + +/// Similar to snprintf(str, size, "%s", src) except it turns CamelCase into +/// space separated values +static int snprint_cameltospaces(char *str, size_t size, const char *src) +{ + int prev_lower = 0; + size_t l = 0; + while (*src && l < size - 1) { + if (isupper(*src) && prev_lower) { + if (str) { + str[l++] = ' '; + } + if (l >= size - 1) { + break; + } + } + prev_lower = islower(*src); + str[l++] = (char)tolower(*src++); + } + str[l] = 0; + // For consistency with snprintf, return the number of bytes that would have + // been written, excluding '\0' + while (*src) { + if (isupper(*src) && prev_lower) { + l++; + } + prev_lower = islower(*src); + src++; l++; + } + return (int)l; +} + +/// Similar to strcmp(str, strcamel, n) except that: +/// it compares CamelCase in strcamel with space separated values in str; +/// it takes char**s and updates them +/// n counts bytes of strcamel, not str +static int strpncmp_camel(const char **strp, const char **strcamelp, size_t n) +{ + const char *str = *strp, *strcamel = *strcamelp; + int prev_lower = 0; + + for (; (*str || *strcamel) && n; n--) { + char b = (char)tolower(*strcamel); + if (isupper(*strcamel) && prev_lower) { + if (*str != ' ') { + break; + } + str++; + if (*str != b) { + break; + } + } else if (*str != b) { + break; + } + + prev_lower = islower(*strcamel); + + str++; + strcamel++; + } + + *strp = str; + *strcamelp = strcamel; + return *str - *strcamel; +} + +static TermKey *termkey_alloc(void) +{ + TermKey *tk = xmalloc(sizeof(TermKey)); + + // Default all the object fields but don't allocate anything + + tk->fd = -1; + tk->flags = 0; + tk->canonflags = 0; + + tk->buffer = NULL; + tk->buffstart = 0; + tk->buffcount = 0; + tk->buffsize = 256; // bytes + tk->hightide = 0; + +#ifdef HAVE_TERMIOS + tk->restore_termios_valid = 0; +#endif + + tk->ti_getstr_hook = NULL; + tk->ti_getstr_hook_data = NULL; + + tk->waittime = 50; // msec + + tk->is_closed = 0; + tk->is_started = 0; + + tk->nkeynames = 64; + tk->keynames = NULL; + + for (int i = 0; i < 32; i++) { + tk->c0[i].sym = TERMKEY_SYM_NONE; + } + + tk->drivers = NULL; + + tk->method.emit_codepoint = &emit_codepoint; + tk->method.peekkey_simple = &peekkey_simple; + tk->method.peekkey_mouse = &peekkey_mouse; + + return tk; +} + +static int termkey_init(TermKey *tk, const char *term) +{ + tk->buffer = xmalloc(tk->buffsize); + tk->keynames = xmalloc(sizeof(tk->keynames[0]) * (size_t)tk->nkeynames); + + int i; + for (i = 0; i < tk->nkeynames; i++) { + tk->keynames[i] = NULL; + } + + for (i = 0; keynames[i].name; i++) { + if (termkey_register_keyname(tk, keynames[i].sym, keynames[i].name) == -1) { + goto abort_free_keynames; + } + } + + register_c0(tk, TERMKEY_SYM_TAB, 0x09, NULL); + register_c0(tk, TERMKEY_SYM_ENTER, 0x0d, NULL); + register_c0(tk, TERMKEY_SYM_ESCAPE, 0x1b, NULL); + + struct TermKeyDriverNode *tail = NULL; + + for (i = 0; drivers[i]; i++) { + void *info = (*drivers[i]->new_driver)(tk, term); + if (!info) { + continue; + } + +#ifdef DEBUG + fprintf(stderr, "Loading the %s driver...\n", drivers[i]->name); +#endif + + struct TermKeyDriverNode *thisdrv = xmalloc(sizeof(*thisdrv)); + if (!thisdrv) { + goto abort_free_drivers; + } + + thisdrv->driver = drivers[i]; + thisdrv->info = info; + thisdrv->next = NULL; + + if (!tail) { + tk->drivers = thisdrv; + } else { + tail->next = thisdrv; + } + + tail = thisdrv; + +#ifdef DEBUG + fprintf(stderr, "Loaded %s driver\n", drivers[i]->name); +#endif + } + + if (!tk->drivers) { + errno = ENOENT; + goto abort_free_keynames; + } + + return 1; + +abort_free_drivers: + for (struct TermKeyDriverNode *p = tk->drivers; p;) { + (*p->driver->free_driver)(p->info); + struct TermKeyDriverNode *next = p->next; + xfree(p); + p = next; + } + +abort_free_keynames: + xfree(tk->keynames); + xfree(tk->buffer); + + return 0; +} + +TermKey *termkey_new_abstract(const char *term, int flags) +{ + TermKey *tk = termkey_alloc(); + if (!tk) { + return NULL; + } + + tk->fd = -1; + + termkey_set_flags(tk, flags); + + if (!termkey_init(tk, term)) { + xfree(tk); + return NULL; + } + + if (!(flags & TERMKEY_FLAG_NOSTART) && !termkey_start(tk)) { + goto abort; + } + + return tk; + +abort: + xfree(tk); + return NULL; +} + +void termkey_free(TermKey *tk) +{ + xfree(tk->buffer); tk->buffer = NULL; + xfree(tk->keynames); tk->keynames = NULL; + + struct TermKeyDriverNode *p; + for (p = tk->drivers; p;) { + (*p->driver->free_driver)(p->info); + struct TermKeyDriverNode *next = p->next; + xfree(p); + p = next; + } + + xfree(tk); +} + +void termkey_destroy(TermKey *tk) +{ + if (tk->is_started) { + termkey_stop(tk); + } + + termkey_free(tk); +} + +void termkey_hook_terminfo_getstr(TermKey *tk, TermKey_Terminfo_Getstr_Hook *hookfn, void *data) +{ + tk->ti_getstr_hook = hookfn; + tk->ti_getstr_hook_data = data; +} + +int termkey_start(TermKey *tk) +{ + if (tk->is_started) { + return 1; + } + +#ifdef HAVE_TERMIOS + if (tk->fd != -1 && !(tk->flags & TERMKEY_FLAG_NOTERMIOS)) { + struct termios termios; + if (tcgetattr(tk->fd, &termios) == 0) { + tk->restore_termios = termios; + tk->restore_termios_valid = 1; + + termios.c_iflag &= (tcflag_t) ~(IXON|INLCR|ICRNL); + termios.c_lflag &= (tcflag_t) ~(ICANON|ECHO +# ifdef IEXTEN + | IEXTEN +# endif + ); + termios.c_cc[VMIN] = 1; + termios.c_cc[VTIME] = 0; + + if (tk->flags & TERMKEY_FLAG_CTRLC) { + // want no signal keys at all, so just disable ISIG + termios.c_lflag &= (tcflag_t) ~ISIG; + } else { + // Disable Ctrl-\==VQUIT and Ctrl-D==VSUSP but leave Ctrl-C as SIGINT + termios.c_cc[VQUIT] = _POSIX_VDISABLE; + termios.c_cc[VSUSP] = _POSIX_VDISABLE; + // Some OSes have Ctrl-Y==VDSUSP +# ifdef VDSUSP + termios.c_cc[VDSUSP] = _POSIX_VDISABLE; +# endif + } + +# ifdef DEBUG + fprintf(stderr, "Setting termios(3) flags\n"); +# endif + tcsetattr(tk->fd, TCSANOW, &termios); + } + } +#endif + + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + if (p->driver->start_driver) { + if (!(*p->driver->start_driver)(tk, p->info)) { + return 0; + } + } + } + +#ifdef DEBUG + fprintf(stderr, "Drivers started; termkey instance %p is ready\n", tk); +#endif + + tk->is_started = 1; + return 1; +} + +int termkey_stop(TermKey *tk) +{ + if (!tk->is_started) { + return 1; + } + + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + if (p->driver->stop_driver) { + (*p->driver->stop_driver)(tk, p->info); + } + } + +#ifdef HAVE_TERMIOS + if (tk->restore_termios_valid) { + tcsetattr(tk->fd, TCSANOW, &tk->restore_termios); + } +#endif + + tk->is_started = 0; + + return 1; +} + +void termkey_set_flags(TermKey *tk, int newflags) +{ + tk->flags = newflags; + + if (tk->flags & TERMKEY_FLAG_SPACESYMBOL) { + tk->canonflags |= TERMKEY_CANON_SPACESYMBOL; + } else { + tk->canonflags &= ~TERMKEY_CANON_SPACESYMBOL; + } +} + +int termkey_get_canonflags(TermKey *tk) +{ + return tk->canonflags; +} + +void termkey_set_canonflags(TermKey *tk, int flags) +{ + tk->canonflags = flags; + + if (tk->canonflags & TERMKEY_CANON_SPACESYMBOL) { + tk->flags |= TERMKEY_FLAG_SPACESYMBOL; + } else { + tk->flags &= ~TERMKEY_FLAG_SPACESYMBOL; + } +} + +size_t termkey_get_buffer_size(TermKey *tk) +{ + return tk->buffsize; +} + +int termkey_set_buffer_size(TermKey *tk, size_t size) +{ + unsigned char *buffer = xrealloc(tk->buffer, size); + + tk->buffer = buffer; + tk->buffsize = size; + + return 1; +} + +size_t termkey_get_buffer_remaining(TermKey *tk) +{ + // Return the total number of free bytes in the buffer, because that's what + // is available to the user. + return tk->buffsize - tk->buffcount; +} + +static void eat_bytes(TermKey *tk, size_t count) +{ + if (count >= tk->buffcount) { + tk->buffstart = 0; + tk->buffcount = 0; + return; + } + + tk->buffstart += count; + tk->buffcount -= count; +} + +// TODO(dundargoc): we should be able to replace this with utf_char2bytes from mbyte.c +int fill_utf8(int codepoint, char *str) +{ + int nbytes = utf_char2len(codepoint); + + str[nbytes] = 0; + + // This is easier done backwards + int b = nbytes; + while (b > 1) { + b--; + str[b] = (char)0x80 | (codepoint & 0x3f); + codepoint >>= 6; + } + + switch (nbytes) { + case 1: + str[0] = (codepoint & 0x7f); break; + case 2: + str[0] = (char)0xc0 | (codepoint & 0x1f); break; + case 3: + str[0] = (char)0xe0 | (codepoint & 0x0f); break; + case 4: + str[0] = (char)0xf0 | (codepoint & 0x07); break; + case 5: + str[0] = (char)0xf8 | (codepoint & 0x03); break; + case 6: + str[0] = (char)0xfc | (codepoint & 0x01); break; + } + + return nbytes; +} + +#define UTF8_INVALID 0xFFFD +static TermKeyResult parse_utf8(const unsigned char *bytes, size_t len, int *cp, size_t *nbytep) +{ + unsigned nbytes; + + unsigned char b0 = bytes[0]; + + if (b0 < 0x80) { + // Single byte ASCII + *cp = b0; + *nbytep = 1; + return TERMKEY_RES_KEY; + } else if (b0 < 0xc0) { + // Starts with a continuation byte - that's not right + *cp = UTF8_INVALID; + *nbytep = 1; + return TERMKEY_RES_KEY; + } else if (b0 < 0xe0) { + nbytes = 2; + *cp = b0 & 0x1f; + } else if (b0 < 0xf0) { + nbytes = 3; + *cp = b0 & 0x0f; + } else if (b0 < 0xf8) { + nbytes = 4; + *cp = b0 & 0x07; + } else if (b0 < 0xfc) { + nbytes = 5; + *cp = b0 & 0x03; + } else if (b0 < 0xfe) { + nbytes = 6; + *cp = b0 & 0x01; + } else { + *cp = UTF8_INVALID; + *nbytep = 1; + return TERMKEY_RES_KEY; + } + + for (unsigned b = 1; b < nbytes; b++) { + unsigned char cb; + + if (b >= len) { + return TERMKEY_RES_AGAIN; + } + + cb = bytes[b]; + if (cb < 0x80 || cb >= 0xc0) { + *cp = UTF8_INVALID; + *nbytep = b; + return TERMKEY_RES_KEY; + } + + *cp <<= 6; + *cp |= cb & 0x3f; + } + + // Check for overlong sequences + if ((int)nbytes > utf_char2len(*cp)) { + *cp = UTF8_INVALID; + } + + // Check for UTF-16 surrogates or invalid *cps + if ((*cp >= 0xD800 && *cp <= 0xDFFF) + || *cp == 0xFFFE + || *cp == 0xFFFF) { + *cp = UTF8_INVALID; + } + + *nbytep = nbytes; + return TERMKEY_RES_KEY; +} + +static void emit_codepoint(TermKey *tk, int codepoint, TermKeyKey *key) +{ + if (codepoint == 0) { + // ASCII NUL = Ctrl-Space + key->type = TERMKEY_TYPE_KEYSYM; + key->code.sym = TERMKEY_SYM_SPACE; + key->modifiers = TERMKEY_KEYMOD_CTRL; + } else if (codepoint < 0x20) { + // C0 range + key->code.codepoint = 0; + key->modifiers = 0; + + if (!(tk->flags & TERMKEY_FLAG_NOINTERPRET) && tk->c0[codepoint].sym != TERMKEY_SYM_UNKNOWN) { + key->code.sym = tk->c0[codepoint].sym; + key->modifiers |= tk->c0[codepoint].modifier_set; + } + + if (!key->code.sym) { + key->type = TERMKEY_TYPE_UNICODE; + // Generically modified Unicode ought not report the SHIFT state, or else + // we get into complications trying to report Shift-; vs : and so on... + // In order to be able to represent Ctrl-Shift-A as CTRL modified + // unicode A, we need to call Ctrl-A simply 'a', lowercase + if (codepoint + 0x40 >= 'A' && codepoint + 0x40 <= 'Z') { + // it's a letter - use lowercase instead + key->code.codepoint = codepoint + 0x60; + } else { + key->code.codepoint = codepoint + 0x40; + } + key->modifiers = TERMKEY_KEYMOD_CTRL; + } else { + key->type = TERMKEY_TYPE_KEYSYM; + } + } else if (codepoint == 0x7f && !(tk->flags & TERMKEY_FLAG_NOINTERPRET)) { + // ASCII DEL + key->type = TERMKEY_TYPE_KEYSYM; + key->code.sym = TERMKEY_SYM_DEL; + key->modifiers = 0; + } else if (codepoint >= 0x20 && codepoint < 0x80) { + // ASCII lowbyte range + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = codepoint; + key->modifiers = 0; + } else if (codepoint >= 0x80 && codepoint < 0xa0) { + // UTF-8 never starts with a C1 byte. So we can be sure of these + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = codepoint - 0x40; + key->modifiers = TERMKEY_KEYMOD_CTRL|TERMKEY_KEYMOD_ALT; + } else { + // UTF-8 codepoint + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = codepoint; + key->modifiers = 0; + } + + termkey_canonicalise(tk, key); + + if (key->type == TERMKEY_TYPE_UNICODE) { + fill_utf8(key->code.codepoint, key->utf8); + } +} + +void termkey_canonicalise(TermKey *tk, TermKeyKey *key) +{ + int flags = tk->canonflags; + + if (flags & TERMKEY_CANON_SPACESYMBOL) { + if (key->type == TERMKEY_TYPE_UNICODE && key->code.codepoint == 0x20) { + key->type = TERMKEY_TYPE_KEYSYM; + key->code.sym = TERMKEY_SYM_SPACE; + } + } else { + if (key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_SPACE) { + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = 0x20; + fill_utf8(key->code.codepoint, key->utf8); + } + } + + if (flags & TERMKEY_CANON_DELBS) { + if (key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_DEL) { + key->code.sym = TERMKEY_SYM_BACKSPACE; + } + } +} + +static TermKeyResult peekkey(TermKey *tk, TermKeyKey *key, int force, size_t *nbytep) +{ + int again = 0; + + if (!tk->is_started) { + errno = EINVAL; + return TERMKEY_RES_ERROR; + } + +#ifdef DEBUG + fprintf(stderr, "getkey(force=%d): buffer ", force); + print_buffer(tk); + fprintf(stderr, "\n"); +#endif + + if (tk->hightide) { + tk->buffstart += tk->hightide; + tk->buffcount -= tk->hightide; + tk->hightide = 0; + } + + TermKeyResult ret; + struct TermKeyDriverNode *p; + for (p = tk->drivers; p; p = p->next) { + ret = (p->driver->peekkey)(tk, p->info, key, force, nbytep); + +#ifdef DEBUG + fprintf(stderr, "Driver %s yields %s\n", p->driver->name, res2str(ret)); +#endif + + switch (ret) { + case TERMKEY_RES_KEY: +#ifdef DEBUG + print_key(tk, key); fprintf(stderr, "\n"); +#endif + // Slide the data down to stop it running away + { + size_t halfsize = tk->buffsize / 2; + + if (tk->buffstart > halfsize) { + memcpy(tk->buffer, tk->buffer + halfsize, halfsize); + tk->buffstart -= halfsize; + } + } + FALLTHROUGH; + case TERMKEY_RES_EOF: + case TERMKEY_RES_ERROR: + return ret; + + case TERMKEY_RES_AGAIN: + if (!force) { + again = 1; + } + FALLTHROUGH; + case TERMKEY_RES_NONE: + break; + } + } + + if (again) { + return TERMKEY_RES_AGAIN; + } + + ret = peekkey_simple(tk, key, force, nbytep); + +#ifdef DEBUG + fprintf(stderr, "getkey_simple(force=%d) yields %s\n", force, res2str(ret)); + if (ret == TERMKEY_RES_KEY) { + print_key(tk, key); fprintf(stderr, "\n"); + } +#endif + + return ret; +} + +static TermKeyResult peekkey_simple(TermKey *tk, TermKeyKey *key, int force, size_t *nbytep) +{ + if (tk->buffcount == 0) { + return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; + } + + unsigned char b0 = CHARAT(0); + + if (b0 == 0x1b) { + // Escape-prefixed value? Might therefore be Alt+key + if (tk->buffcount == 1) { + // This might be an <Esc> press, or it may want to be part of a longer + // sequence + if (!force) { + return TERMKEY_RES_AGAIN; + } + + (*tk->method.emit_codepoint)(tk, b0, key); + *nbytep = 1; + return TERMKEY_RES_KEY; + } + + // Try another key there + tk->buffstart++; + tk->buffcount--; + + // Run the full driver + TermKeyResult metakey_result = peekkey(tk, key, force, nbytep); + + tk->buffstart--; + tk->buffcount++; + + switch (metakey_result) { + case TERMKEY_RES_KEY: + key->modifiers |= TERMKEY_KEYMOD_ALT; + (*nbytep)++; + break; + + case TERMKEY_RES_NONE: + case TERMKEY_RES_EOF: + case TERMKEY_RES_AGAIN: + case TERMKEY_RES_ERROR: + break; + } + + return metakey_result; + } else if (b0 < 0xa0) { + // Single byte C0, G0 or C1 - C1 is never UTF-8 initial byte + (*tk->method.emit_codepoint)(tk, b0, key); + *nbytep = 1; + return TERMKEY_RES_KEY; + } else if (tk->flags & TERMKEY_FLAG_UTF8) { + // Some UTF-8 + int codepoint; + TermKeyResult res = parse_utf8(tk->buffer + tk->buffstart, tk->buffcount, &codepoint, nbytep); + + if (res == TERMKEY_RES_AGAIN && force) { + // There weren't enough bytes for a complete UTF-8 sequence but caller + // demands an answer. About the best thing we can do here is eat as many + // bytes as we have, and emit a UTF8_INVALID. If the remaining bytes + // arrive later, they'll be invalid too. + codepoint = UTF8_INVALID; + *nbytep = tk->buffcount; + res = TERMKEY_RES_KEY; + } + + key->type = TERMKEY_TYPE_UNICODE; + key->modifiers = 0; + (*tk->method.emit_codepoint)(tk, codepoint, key); + return res; + } else { + // Non UTF-8 case - just report the raw byte + key->type = TERMKEY_TYPE_UNICODE; + key->code.codepoint = b0; + key->modifiers = 0; + + key->utf8[0] = (char)key->code.codepoint; + key->utf8[1] = 0; + + *nbytep = 1; + + return TERMKEY_RES_KEY; + } +} + +static TermKeyResult peekkey_mouse(TermKey *tk, TermKeyKey *key, size_t *nbytep) +{ + if (tk->buffcount < 3) { + return TERMKEY_RES_AGAIN; + } + + key->type = TERMKEY_TYPE_MOUSE; + key->code.mouse[0] = (char)CHARAT(0) - 0x20; + key->code.mouse[1] = (char)CHARAT(1) - 0x20; + key->code.mouse[2] = (char)CHARAT(2) - 0x20; + key->code.mouse[3] = 0; + + key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; + key->code.mouse[0] &= ~0x1c; + + *nbytep = 3; + return TERMKEY_RES_KEY; +} + +TermKeyResult termkey_getkey(TermKey *tk, TermKeyKey *key) +{ + size_t nbytes = 0; + TermKeyResult ret = peekkey(tk, key, 0, &nbytes); + + if (ret == TERMKEY_RES_KEY) { + eat_bytes(tk, nbytes); + } + + if (ret == TERMKEY_RES_AGAIN) { + // Call peekkey() again in force mode to obtain whatever it can + (void)peekkey(tk, key, 1, &nbytes); + } + // Don't eat it yet though + + return ret; +} + +TermKeyResult termkey_getkey_force(TermKey *tk, TermKeyKey *key) +{ + size_t nbytes = 0; + TermKeyResult ret = peekkey(tk, key, 1, &nbytes); + + if (ret == TERMKEY_RES_KEY) { + eat_bytes(tk, nbytes); + } + + return ret; +} + +size_t termkey_push_bytes(TermKey *tk, const char *bytes, size_t len) +{ + if (tk->buffstart) { + memmove(tk->buffer, tk->buffer + tk->buffstart, tk->buffcount); + tk->buffstart = 0; + } + + // Not expecting it ever to be greater but doesn't hurt to handle that + if (tk->buffcount >= tk->buffsize) { + errno = ENOMEM; + return (size_t)-1; + } + + if (len > tk->buffsize - tk->buffcount) { + len = tk->buffsize - tk->buffcount; + } + + // memcpy(), not strncpy() in case of null bytes in input + memcpy(tk->buffer + tk->buffcount, bytes, len); + tk->buffcount += len; + + return len; +} + +TermKeySym termkey_register_keyname(TermKey *tk, TermKeySym sym, const char *name) +{ + if (!sym) { + sym = tk->nkeynames; + } + + if (sym >= tk->nkeynames) { + const char **new_keynames = xrealloc(tk->keynames, sizeof(new_keynames[0]) * ((size_t)sym + 1)); + + tk->keynames = new_keynames; + + // Fill in the hole + for (int i = tk->nkeynames; i < sym; i++) { + tk->keynames[i] = NULL; + } + + tk->nkeynames = sym + 1; + } + + tk->keynames[sym] = name; + + return sym; +} + +const char *termkey_get_keyname(TermKey *tk, TermKeySym sym) +{ + if (sym == TERMKEY_SYM_UNKNOWN) { + return "UNKNOWN"; + } + + if (sym < tk->nkeynames) { + return tk->keynames[sym]; + } + + return "UNKNOWN"; +} + +static const char *termkey_lookup_keyname_format(TermKey *tk, const char *str, TermKeySym *sym, + TermKeyFormat format) +{ + // We store an array, so we can't do better than a linear search. Doesn't + // matter because user won't be calling this too often + + for (*sym = 0; *sym < tk->nkeynames; (*sym)++) { + const char *thiskey = tk->keynames[*sym]; + if (!thiskey) { + continue; + } + size_t len = strlen(thiskey); + if (format & TERMKEY_FORMAT_LOWERSPACE) { + const char *thisstr = str; + if (strpncmp_camel(&thisstr, &thiskey, len) == 0) { + return thisstr; + } + } else { + if (strncmp(str, thiskey, len) == 0) { + return (char *)str + len; + } + } + } + + return NULL; +} + +const char *termkey_lookup_keyname(TermKey *tk, const char *str, TermKeySym *sym) +{ + return termkey_lookup_keyname_format(tk, str, sym, 0); +} + +static TermKeySym register_c0(TermKey *tk, TermKeySym sym, unsigned char ctrl, const char *name) +{ + return register_c0_full(tk, sym, 0, 0, ctrl, name); +} + +static TermKeySym register_c0_full(TermKey *tk, TermKeySym sym, int modifier_set, int modifier_mask, + unsigned char ctrl, const char *name) +{ + if (ctrl >= 0x20) { + errno = EINVAL; + return -1; + } + + if (name) { + sym = termkey_register_keyname(tk, sym, name); + } + + tk->c0[ctrl].sym = sym; + tk->c0[ctrl].modifier_set = modifier_set; + tk->c0[ctrl].modifier_mask = modifier_mask; + + return sym; +} + +static struct modnames { + const char *shift, *alt, *ctrl; +} +modnames[] = { + { "S", "A", "C" }, // 0 + { "Shift", "Alt", "Ctrl" }, // LONGMOD + { "S", "M", "C" }, // ALTISMETA + { "Shift", "Meta", "Ctrl" }, // ALTISMETA+LONGMOD + { "s", "a", "c" }, // LOWERMOD + { "shift", "alt", "ctrl" }, // LOWERMOD+LONGMOD + { "s", "m", "c" }, // LOWERMOD+ALTISMETA + { "shift", "meta", "ctrl" }, // LOWERMOD+ALTISMETA+LONGMOD +}; + +size_t termkey_strfkey(TermKey *tk, char *buffer, size_t len, TermKeyKey *key, TermKeyFormat format) +{ + size_t pos = 0; + size_t l = 0; + + struct modnames *mods = &modnames[!!(format & TERMKEY_FORMAT_LONGMOD) + + !!(format & TERMKEY_FORMAT_ALTISMETA) * 2 + + !!(format & TERMKEY_FORMAT_LOWERMOD) * 4]; + + int wrapbracket = (format & TERMKEY_FORMAT_WRAPBRACKET) + && (key->type != TERMKEY_TYPE_UNICODE || key->modifiers != 0); + + char sep = (format & TERMKEY_FORMAT_SPACEMOD) ? ' ' : '-'; + + if (format & TERMKEY_FORMAT_CARETCTRL + && key->type == TERMKEY_TYPE_UNICODE + && key->modifiers == TERMKEY_KEYMOD_CTRL) { + long codepoint = key->code.codepoint; + + // Handle some of the special cases first + if (codepoint >= 'a' && codepoint <= 'z') { + l = (size_t)snprintf(buffer + pos, len - pos, wrapbracket ? "<^%c>" : "^%c", + (char)codepoint - 0x20); + if (l <= 0) { + return pos; + } + pos += l; + return pos; + } else if ((codepoint >= '@' && codepoint < 'A') + || (codepoint > 'Z' && codepoint <= '_')) { + l = (size_t)snprintf(buffer + pos, len - pos, wrapbracket ? "<^%c>" : "^%c", (char)codepoint); + if (l <= 0) { + return pos; + } + pos += l; + return pos; + } + } + + if (wrapbracket) { + l = (size_t)snprintf(buffer + pos, len - pos, "<"); + if (l <= 0) { + return pos; + } + pos += l; + } + + if (key->modifiers & TERMKEY_KEYMOD_ALT) { + l = (size_t)snprintf(buffer + pos, len - pos, "%s%c", mods->alt, sep); + if (l <= 0) { + return pos; + } + pos += l; + } + + if (key->modifiers & TERMKEY_KEYMOD_CTRL) { + l = (size_t)snprintf(buffer + pos, len - pos, "%s%c", mods->ctrl, sep); + if (l <= 0) { + return pos; + } + pos += l; + } + + if (key->modifiers & TERMKEY_KEYMOD_SHIFT) { + l = (size_t)snprintf(buffer + pos, len - pos, "%s%c", mods->shift, sep); + if (l <= 0) { + return pos; + } + pos += l; + } + + switch (key->type) { + case TERMKEY_TYPE_UNICODE: + if (!key->utf8[0]) { // In case of user-supplied key structures + fill_utf8(key->code.codepoint, key->utf8); + } + l = (size_t)snprintf(buffer + pos, len - pos, "%s", key->utf8); + break; + case TERMKEY_TYPE_KEYSYM: { + const char *name = termkey_get_keyname(tk, key->code.sym); + if (format & TERMKEY_FORMAT_LOWERSPACE) { + l = (size_t)snprint_cameltospaces(buffer + pos, len - pos, name); + } else { + l = (size_t)snprintf(buffer + pos, len - pos, "%s", name); + } + } + break; + case TERMKEY_TYPE_FUNCTION: + l = (size_t)snprintf(buffer + pos, len - pos, "%c%d", + (format & TERMKEY_FORMAT_LOWERSPACE ? 'f' : 'F'), key->code.number); + break; + case TERMKEY_TYPE_MOUSE: { + TermKeyMouseEvent ev; + int button; + int line, col; + termkey_interpret_mouse(tk, key, &ev, &button, &line, &col); + + l = (size_t)snprintf(buffer + pos, len - pos, "Mouse%s(%d)", + evnames[ev], button); + + if (format & TERMKEY_FORMAT_MOUSE_POS) { + if (l <= 0) { + return pos; + } + pos += l; + + l = (size_t)snprintf(buffer + pos, len - pos, " @ (%u,%u)", col, line); + } + } + break; + case TERMKEY_TYPE_POSITION: + l = (size_t)snprintf(buffer + pos, len - pos, "Position"); + break; + case TERMKEY_TYPE_MODEREPORT: { + int initial, mode, value; + termkey_interpret_modereport(tk, key, &initial, &mode, &value); + if (initial) { + l = (size_t)snprintf(buffer + pos, len - pos, "Mode(%c%d=%d)", initial, mode, value); + } else { + l = (size_t)snprintf(buffer + pos, len - pos, "Mode(%d=%d)", mode, value); + } + } + break; + case TERMKEY_TYPE_DCS: + l = (size_t)snprintf(buffer + pos, len - pos, "DCS"); + break; + case TERMKEY_TYPE_OSC: + l = (size_t)snprintf(buffer + pos, len - pos, "OSC"); + break; + case TERMKEY_TYPE_UNKNOWN_CSI: + l = (size_t)snprintf(buffer + pos, len - pos, "CSI %c", key->code.number & 0xff); + break; + } + + if (l <= 0) { + return pos; + } + pos += l; + + if (wrapbracket) { + l = (size_t)snprintf(buffer + pos, len - pos, ">"); + if (l <= 0) { + return pos; + } + pos += l; + } + + return pos; +} diff --git a/src/nvim/tui/termkey/termkey.h b/src/nvim/tui/termkey/termkey.h new file mode 100644 index 0000000000..21ed141346 --- /dev/null +++ b/src/nvim/tui/termkey/termkey.h @@ -0,0 +1,10 @@ +#pragma once + +#include <stdint.h> +#include <stdlib.h> + +#include "nvim/tui/termkey/termkey_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "tui/termkey/termkey.h.generated.h" +#endif diff --git a/src/nvim/tui/termkey/termkey_defs.h b/src/nvim/tui/termkey/termkey_defs.h new file mode 100644 index 0000000000..7c218ba7c2 --- /dev/null +++ b/src/nvim/tui/termkey/termkey_defs.h @@ -0,0 +1,199 @@ +#pragma once + +#include <stddef.h> +#include <stdint.h> +#include <unibilium.h> +#include <uv.h> + +#include "nvim/event/defs.h" +#include "nvim/tui/tui_defs.h" +#include "nvim/types_defs.h" + +typedef struct TermKey TermKey; + +typedef struct { + TermKey *tk; + int saved_string_id; + char *saved_string; +} TermKeyCsi; + +typedef enum { + TERMKEY_RES_NONE, + TERMKEY_RES_KEY, + TERMKEY_RES_EOF, + TERMKEY_RES_AGAIN, + TERMKEY_RES_ERROR, +} TermKeyResult; + +typedef enum { + TERMKEY_SYM_UNKNOWN = -1, + TERMKEY_SYM_NONE = 0, + + // Special names in C0 + TERMKEY_SYM_BACKSPACE, + TERMKEY_SYM_TAB, + TERMKEY_SYM_ENTER, + TERMKEY_SYM_ESCAPE, + + // Special names in G0 + TERMKEY_SYM_SPACE, + TERMKEY_SYM_DEL, + + // Special keys + TERMKEY_SYM_UP, + TERMKEY_SYM_DOWN, + TERMKEY_SYM_LEFT, + TERMKEY_SYM_RIGHT, + TERMKEY_SYM_BEGIN, + TERMKEY_SYM_FIND, + TERMKEY_SYM_INSERT, + TERMKEY_SYM_DELETE, + TERMKEY_SYM_SELECT, + TERMKEY_SYM_PAGEUP, + TERMKEY_SYM_PAGEDOWN, + TERMKEY_SYM_HOME, + TERMKEY_SYM_END, + + // Special keys from terminfo + TERMKEY_SYM_CANCEL, + TERMKEY_SYM_CLEAR, + TERMKEY_SYM_CLOSE, + TERMKEY_SYM_COMMAND, + TERMKEY_SYM_COPY, + TERMKEY_SYM_EXIT, + TERMKEY_SYM_HELP, + TERMKEY_SYM_MARK, + TERMKEY_SYM_MESSAGE, + TERMKEY_SYM_MOVE, + TERMKEY_SYM_OPEN, + TERMKEY_SYM_OPTIONS, + TERMKEY_SYM_PRINT, + TERMKEY_SYM_REDO, + TERMKEY_SYM_REFERENCE, + TERMKEY_SYM_REFRESH, + TERMKEY_SYM_REPLACE, + TERMKEY_SYM_RESTART, + TERMKEY_SYM_RESUME, + TERMKEY_SYM_SAVE, + TERMKEY_SYM_SUSPEND, + TERMKEY_SYM_UNDO, + + // Numeric keypad special keys + TERMKEY_SYM_KP0, + TERMKEY_SYM_KP1, + TERMKEY_SYM_KP2, + TERMKEY_SYM_KP3, + TERMKEY_SYM_KP4, + TERMKEY_SYM_KP5, + TERMKEY_SYM_KP6, + TERMKEY_SYM_KP7, + TERMKEY_SYM_KP8, + TERMKEY_SYM_KP9, + TERMKEY_SYM_KPENTER, + TERMKEY_SYM_KPPLUS, + TERMKEY_SYM_KPMINUS, + TERMKEY_SYM_KPMULT, + TERMKEY_SYM_KPDIV, + TERMKEY_SYM_KPCOMMA, + TERMKEY_SYM_KPPERIOD, + TERMKEY_SYM_KPEQUALS, + + // et cetera ad nauseum + TERMKEY_N_SYMS, +} TermKeySym; + +typedef enum { + TERMKEY_TYPE_UNICODE, + TERMKEY_TYPE_FUNCTION, + TERMKEY_TYPE_KEYSYM, + TERMKEY_TYPE_MOUSE, + TERMKEY_TYPE_POSITION, + TERMKEY_TYPE_MODEREPORT, + TERMKEY_TYPE_DCS, + TERMKEY_TYPE_OSC, + // add other recognised types here + + TERMKEY_TYPE_UNKNOWN_CSI = -1, +} TermKeyType; + +typedef enum { + TERMKEY_MOUSE_UNKNOWN, + TERMKEY_MOUSE_PRESS, + TERMKEY_MOUSE_DRAG, + TERMKEY_MOUSE_RELEASE, +} TermKeyMouseEvent; + +enum { + TERMKEY_KEYMOD_SHIFT = 1 << 0, + TERMKEY_KEYMOD_ALT = 1 << 1, + TERMKEY_KEYMOD_CTRL = 1 << 2, +}; + +typedef struct { + const unsigned char *param; + size_t length; +} TermKeyCsiParam; + +enum { + TERMKEY_FLAG_NOINTERPRET = 1 << 0, // Do not interpret C0//DEL codes if possible + TERMKEY_FLAG_CONVERTKP = 1 << 1, // Convert KP codes to regular keypresses + TERMKEY_FLAG_RAW = 1 << 2, // Input is raw bytes, not UTF-8 + TERMKEY_FLAG_UTF8 = 1 << 3, // Input is definitely UTF-8 + TERMKEY_FLAG_NOTERMIOS = 1 << 4, // Do not make initial termios calls on construction + TERMKEY_FLAG_SPACESYMBOL = 1 << 5, // Sets TERMKEY_CANON_SPACESYMBOL + TERMKEY_FLAG_CTRLC = 1 << 6, // Allow Ctrl-C to be read as normal, disabling SIGINT + TERMKEY_FLAG_EINTR = 1 << 7, // Return ERROR on signal (EINTR) rather than retry + TERMKEY_FLAG_NOSTART = 1 << 8, // Do not call termkey_start() in constructor +}; + +enum { + TERMKEY_CANON_SPACESYMBOL = 1 << 0, // Space is symbolic rather than Unicode + TERMKEY_CANON_DELBS = 1 << 1, // Del is converted to Backspace +}; + +typedef struct { + TermKeyType type; + union { + int codepoint; // TERMKEY_TYPE_UNICODE + int number; // TERMKEY_TYPE_FUNCTION + TermKeySym sym; // TERMKEY_TYPE_KEYSYM + char mouse[4]; // TERMKEY_TYPE_MOUSE + // opaque. see termkey_interpret_mouse + } code; + + int modifiers; + + // Any Unicode character can be UTF-8 encoded in no more than 6 bytes, plus + // terminating NUL + char utf8[7]; +} TermKeyKey; + +// Mostly-undocumented hooks for doing evil evil things +typedef const char *TermKey_Terminfo_Getstr_Hook(const char *name, const char *value, void *data); + +typedef enum { + TERMKEY_FORMAT_LONGMOD = 1 << 0, // Shift-... instead of S-... + TERMKEY_FORMAT_CARETCTRL = 1 << 1, // ^X instead of C-X + TERMKEY_FORMAT_ALTISMETA = 1 << 2, // Meta- or M- instead of Alt- or A- + TERMKEY_FORMAT_WRAPBRACKET = 1 << 3, // Wrap special keys in brackets like <Escape> + TERMKEY_FORMAT_SPACEMOD = 1 << 4, // M Foo instead of M-Foo + TERMKEY_FORMAT_LOWERMOD = 1 << 5, // meta or m instead of Meta or M + TERMKEY_FORMAT_LOWERSPACE = 1 << 6, // page down instead of PageDown + + TERMKEY_FORMAT_MOUSE_POS = 1 << 8, // Include mouse position if relevant; @ col,line +} TermKeyFormat; + +// Some useful combinations + +#define TERMKEY_FORMAT_VIM (TermKeyFormat)(TERMKEY_FORMAT_ALTISMETA|TERMKEY_FORMAT_WRAPBRACKET) + +typedef struct { + TermKey *tk; + + unibi_term *unibi; // only valid until first 'start' call + + struct trie_node *root; + + char *start_string; + char *stop_string; +} TermKeyTI; diff --git a/src/termkey/termkey.c b/src/termkey/termkey.c deleted file mode 100644 index 832e5a9a9e..0000000000 --- a/src/termkey/termkey.c +++ /dev/null @@ -1,1606 +0,0 @@ -#include "termkey.h" -#include "termkey-internal.h" - -#include <ctype.h> -#include <errno.h> -#ifndef _WIN32 -# include <poll.h> -# include <unistd.h> -# include <strings.h> -#else -# include <io.h> -#endif -#include <string.h> - -#include <stdio.h> - -#ifdef _MSC_VER -# define strcaseeq(a,b) (_stricmp(a,b) == 0) -#else -# define strcaseeq(a,b) (strcasecmp(a,b) == 0) -#endif - -void termkey_check_version(int major, int minor) -{ - if(major != TERMKEY_VERSION_MAJOR) { - fprintf(stderr, "libtermkey major version mismatch; %d (wants) != %d (library)\n", - major, TERMKEY_VERSION_MAJOR); - exit(1); - } - - if(minor > TERMKEY_VERSION_MINOR) { - fprintf(stderr, "libtermkey minor version mismatch; %d (wants) > %d (library)\n", - minor, TERMKEY_VERSION_MINOR); - exit(1); - } - - // Happy -} - -static struct TermKeyDriver *drivers[] = { - &termkey_driver_ti, - &termkey_driver_csi, - NULL, -}; - -// Forwards for the "protected" methods -// static void eat_bytes(TermKey *tk, size_t count); -static void emit_codepoint(TermKey *tk, long codepoint, TermKeyKey *key); -static TermKeyResult peekkey_simple(TermKey *tk, TermKeyKey *key, int force, size_t *nbytes); -static TermKeyResult peekkey_mouse(TermKey *tk, TermKeyKey *key, size_t *nbytes); - -static TermKeySym register_c0(TermKey *tk, TermKeySym sym, unsigned char ctrl, const char *name); -static TermKeySym register_c0_full(TermKey *tk, TermKeySym sym, int modifier_set, int modifier_mask, unsigned char ctrl, const char *name); - -static struct { - TermKeySym sym; - const char *name; -} keynames[] = { - { TERMKEY_SYM_NONE, "NONE" }, - { TERMKEY_SYM_BACKSPACE, "Backspace" }, - { TERMKEY_SYM_TAB, "Tab" }, - { TERMKEY_SYM_ENTER, "Enter" }, - { TERMKEY_SYM_ESCAPE, "Escape" }, - { TERMKEY_SYM_SPACE, "Space" }, - { TERMKEY_SYM_DEL, "DEL" }, - { TERMKEY_SYM_UP, "Up" }, - { TERMKEY_SYM_DOWN, "Down" }, - { TERMKEY_SYM_LEFT, "Left" }, - { TERMKEY_SYM_RIGHT, "Right" }, - { TERMKEY_SYM_BEGIN, "Begin" }, - { TERMKEY_SYM_FIND, "Find" }, - { TERMKEY_SYM_INSERT, "Insert" }, - { TERMKEY_SYM_DELETE, "Delete" }, - { TERMKEY_SYM_SELECT, "Select" }, - { TERMKEY_SYM_PAGEUP, "PageUp" }, - { TERMKEY_SYM_PAGEDOWN, "PageDown" }, - { TERMKEY_SYM_HOME, "Home" }, - { TERMKEY_SYM_END, "End" }, - { TERMKEY_SYM_CANCEL, "Cancel" }, - { TERMKEY_SYM_CLEAR, "Clear" }, - { TERMKEY_SYM_CLOSE, "Close" }, - { TERMKEY_SYM_COMMAND, "Command" }, - { TERMKEY_SYM_COPY, "Copy" }, - { TERMKEY_SYM_EXIT, "Exit" }, - { TERMKEY_SYM_HELP, "Help" }, - { TERMKEY_SYM_MARK, "Mark" }, - { TERMKEY_SYM_MESSAGE, "Message" }, - { TERMKEY_SYM_MOVE, "Move" }, - { TERMKEY_SYM_OPEN, "Open" }, - { TERMKEY_SYM_OPTIONS, "Options" }, - { TERMKEY_SYM_PRINT, "Print" }, - { TERMKEY_SYM_REDO, "Redo" }, - { TERMKEY_SYM_REFERENCE, "Reference" }, - { TERMKEY_SYM_REFRESH, "Refresh" }, - { TERMKEY_SYM_REPLACE, "Replace" }, - { TERMKEY_SYM_RESTART, "Restart" }, - { TERMKEY_SYM_RESUME, "Resume" }, - { TERMKEY_SYM_SAVE, "Save" }, - { TERMKEY_SYM_SUSPEND, "Suspend" }, - { TERMKEY_SYM_UNDO, "Undo" }, - { TERMKEY_SYM_KP0, "KP0" }, - { TERMKEY_SYM_KP1, "KP1" }, - { TERMKEY_SYM_KP2, "KP2" }, - { TERMKEY_SYM_KP3, "KP3" }, - { TERMKEY_SYM_KP4, "KP4" }, - { TERMKEY_SYM_KP5, "KP5" }, - { TERMKEY_SYM_KP6, "KP6" }, - { TERMKEY_SYM_KP7, "KP7" }, - { TERMKEY_SYM_KP8, "KP8" }, - { TERMKEY_SYM_KP9, "KP9" }, - { TERMKEY_SYM_KPENTER, "KPEnter" }, - { TERMKEY_SYM_KPPLUS, "KPPlus" }, - { TERMKEY_SYM_KPMINUS, "KPMinus" }, - { TERMKEY_SYM_KPMULT, "KPMult" }, - { TERMKEY_SYM_KPDIV, "KPDiv" }, - { TERMKEY_SYM_KPCOMMA, "KPComma" }, - { TERMKEY_SYM_KPPERIOD, "KPPeriod" }, - { TERMKEY_SYM_KPEQUALS, "KPEquals" }, - { 0, NULL }, -}; - -// Mouse event names -static const char *evnames[] = { "Unknown", "Press", "Drag", "Release" }; - -#define CHARAT(i) (tk->buffer[tk->buffstart + (i)]) - -#ifdef DEBUG -/* Some internal debugging functions */ - -static void print_buffer(TermKey *tk) -{ - int i; - for(i = 0; i < tk->buffcount && i < 20; i++) - fprintf(stderr, "%02x ", CHARAT(i)); - if(tk->buffcount > 20) - fprintf(stderr, "..."); -} - -static void print_key(TermKey *tk, TermKeyKey *key) -{ - switch(key->type) { - case TERMKEY_TYPE_UNICODE: - fprintf(stderr, "Unicode codepoint=U+%04lx utf8='%s'", key->code.codepoint, key->utf8); - break; - case TERMKEY_TYPE_FUNCTION: - fprintf(stderr, "Function F%d", key->code.number); - break; - case TERMKEY_TYPE_KEYSYM: - fprintf(stderr, "Keysym sym=%d(%s)", key->code.sym, termkey_get_keyname(tk, key->code.sym)); - break; - case TERMKEY_TYPE_MOUSE: - { - TermKeyMouseEvent ev; - int button, line, col; - termkey_interpret_mouse(tk, key, &ev, &button, &line, &col); - fprintf(stderr, "Mouse ev=%d button=%d pos=(%d,%d)\n", ev, button, line, col); - } - break; - case TERMKEY_TYPE_POSITION: - { - int line, col; - termkey_interpret_position(tk, key, &line, &col); - fprintf(stderr, "Position report pos=(%d,%d)\n", line, col); - } - break; - case TERMKEY_TYPE_MODEREPORT: - { - int initial, mode, value; - termkey_interpret_modereport(tk, key, &initial, &mode, &value); - fprintf(stderr, "Mode report mode=%s %d val=%d\n", initial == '?' ? "DEC" : "ANSI", mode, value); - } - break; - case TERMKEY_TYPE_DCS: - fprintf(stderr, "Device Control String"); - break; - case TERMKEY_TYPE_OSC: - fprintf(stderr, "Operating System Control"); - break; - case TERMKEY_TYPE_UNKNOWN_CSI: - fprintf(stderr, "unknown CSI\n"); - break; - } - - int m = key->modifiers; - fprintf(stderr, " mod=%s%s%s+%02x", - (m & TERMKEY_KEYMOD_CTRL ? "C" : ""), - (m & TERMKEY_KEYMOD_ALT ? "A" : ""), - (m & TERMKEY_KEYMOD_SHIFT ? "S" : ""), - m & ~(TERMKEY_KEYMOD_CTRL|TERMKEY_KEYMOD_ALT|TERMKEY_KEYMOD_SHIFT)); -} - -static const char *res2str(TermKeyResult res) -{ - static char errorbuffer[256]; - - switch(res) { - case TERMKEY_RES_KEY: - return "TERMKEY_RES_KEY"; - case TERMKEY_RES_EOF: - return "TERMKEY_RES_EOF"; - case TERMKEY_RES_AGAIN: - return "TERMKEY_RES_AGAIN"; - case TERMKEY_RES_NONE: - return "TERMKEY_RES_NONE"; - case TERMKEY_RES_ERROR: - snprintf(errorbuffer, sizeof errorbuffer, "TERMKEY_RES_ERROR(errno=%d)\n", errno); - return (const char*)errorbuffer; - } - - return "unknown"; -} -#endif - -/* Similar to snprintf(str, size, "%s", src) except it turns CamelCase into - * space separated values - */ -static int snprint_cameltospaces(char *str, size_t size, const char *src) -{ - int prev_lower = 0; - size_t l = 0; - while(*src && l < size - 1) { - if(isupper(*src) && prev_lower) { - if(str) - str[l++] = ' '; - if(l >= size - 1) - break; - } - prev_lower = islower(*src); - str[l++] = tolower(*src++); - } - str[l] = 0; - /* For consistency with snprintf, return the number of bytes that would have - * been written, excluding '\0' */ - while(*src) { - if(isupper(*src) && prev_lower) { - l++; - } - prev_lower = islower(*src); - src++; l++; - } - return l; -} - -/* Similar to strcmp(str, strcamel, n) except that: - * it compares CamelCase in strcamel with space separated values in str; - * it takes char**s and updates them - * n counts bytes of strcamel, not str - */ -static int strpncmp_camel(const char **strp, const char **strcamelp, size_t n) -{ - const char *str = *strp, *strcamel = *strcamelp; - int prev_lower = 0; - - for( ; (*str || *strcamel) && n; n--) { - char b = tolower(*strcamel); - if(isupper(*strcamel) && prev_lower) { - if(*str != ' ') - break; - str++; - if(*str != b) - break; - } - else - if(*str != b) - break; - - prev_lower = islower(*strcamel); - - str++; - strcamel++; - } - - *strp = str; - *strcamelp = strcamel; - return *str - *strcamel; -} - -static TermKey *termkey_alloc(void) -{ - TermKey *tk = malloc(sizeof(TermKey)); - if(!tk) - return NULL; - - /* Default all the object fields but don't allocate anything */ - - tk->fd = -1; - tk->flags = 0; - tk->canonflags = 0; - - tk->buffer = NULL; - tk->buffstart = 0; - tk->buffcount = 0; - tk->buffsize = 256; /* bytes */ - tk->hightide = 0; - -#ifdef HAVE_TERMIOS - tk->restore_termios_valid = 0; -#endif - - tk->ti_getstr_hook = NULL; - tk->ti_getstr_hook_data = NULL; - - tk->waittime = 50; /* msec */ - - tk->is_closed = 0; - tk->is_started = 0; - - tk->nkeynames = 64; - tk->keynames = NULL; - - for(int i = 0; i < 32; i++) - tk->c0[i].sym = TERMKEY_SYM_NONE; - - tk->drivers = NULL; - - tk->method.emit_codepoint = &emit_codepoint; - tk->method.peekkey_simple = &peekkey_simple; - tk->method.peekkey_mouse = &peekkey_mouse; - - return tk; -} - -static int termkey_init(TermKey *tk, const char *term) -{ - tk->buffer = malloc(tk->buffsize); - if(!tk->buffer) - return 0; - - tk->keynames = malloc(sizeof(tk->keynames[0]) * tk->nkeynames); - if(!tk->keynames) - goto abort_free_buffer; - - int i; - for(i = 0; i < tk->nkeynames; i++) - tk->keynames[i] = NULL; - - for(i = 0; keynames[i].name; i++) - if(termkey_register_keyname(tk, keynames[i].sym, keynames[i].name) == -1) - goto abort_free_keynames; - - register_c0(tk, TERMKEY_SYM_TAB, 0x09, NULL); - register_c0(tk, TERMKEY_SYM_ENTER, 0x0d, NULL); - register_c0(tk, TERMKEY_SYM_ESCAPE, 0x1b, NULL); - - struct TermKeyDriverNode *tail = NULL; - - for(i = 0; drivers[i]; i++) { - void *info = (*drivers[i]->new_driver)(tk, term); - if(!info) - continue; - -#ifdef DEBUG - fprintf(stderr, "Loading the %s driver...\n", drivers[i]->name); -#endif - - struct TermKeyDriverNode *thisdrv = malloc(sizeof(*thisdrv)); - if(!thisdrv) - goto abort_free_drivers; - - thisdrv->driver = drivers[i]; - thisdrv->info = info; - thisdrv->next = NULL; - - if(!tail) - tk->drivers = thisdrv; - else - tail->next = thisdrv; - - tail = thisdrv; - -#ifdef DEBUG - fprintf(stderr, "Loaded %s driver\n", drivers[i]->name); -#endif - } - - if(!tk->drivers) { - errno = ENOENT; - goto abort_free_keynames; - } - - return 1; - -abort_free_drivers: - for(struct TermKeyDriverNode *p = tk->drivers; p; ) { - (*p->driver->free_driver)(p->info); - struct TermKeyDriverNode *next = p->next; - free(p); - p = next; - } - -abort_free_keynames: - free(tk->keynames); - -abort_free_buffer: - free(tk->buffer); - - return 0; -} - -TermKey *termkey_new(int fd, int flags) -{ - TermKey *tk = termkey_alloc(); - if(!tk) - return NULL; - - tk->fd = fd; - - if(!(flags & (TERMKEY_FLAG_RAW|TERMKEY_FLAG_UTF8))) { - char *e; - - /* Most OSes will set .UTF-8. Some will set .utf8. Try to be fairly - * generous in parsing these - */ - if(((e = getenv("LANG")) || (e = getenv("LC_MESSAGES")) || (e = getenv("LC_ALL"))) && - (e = strchr(e, '.')) && e++ && - (strcaseeq(e, "UTF-8") || strcaseeq(e, "UTF8"))) - flags |= TERMKEY_FLAG_UTF8; - else - flags |= TERMKEY_FLAG_RAW; - } - - termkey_set_flags(tk, flags); - - const char *term = getenv("TERM"); - - if(!termkey_init(tk, term)) - goto abort; - - if(!(flags & TERMKEY_FLAG_NOSTART) && !termkey_start(tk)) - goto abort; - - return tk; - -abort: - free(tk); - return NULL; -} - -TermKey *termkey_new_abstract(const char *term, int flags) -{ - TermKey *tk = termkey_alloc(); - if(!tk) - return NULL; - - tk->fd = -1; - - termkey_set_flags(tk, flags); - - if(!termkey_init(tk, term)) { - free(tk); - return NULL; - } - - if(!(flags & TERMKEY_FLAG_NOSTART) && !termkey_start(tk)) - goto abort; - - return tk; - -abort: - free(tk); - return NULL; -} - -void termkey_free(TermKey *tk) -{ - free(tk->buffer); tk->buffer = NULL; - free(tk->keynames); tk->keynames = NULL; - - struct TermKeyDriverNode *p; - for(p = tk->drivers; p; ) { - (*p->driver->free_driver)(p->info); - struct TermKeyDriverNode *next = p->next; - free(p); - p = next; - } - - free(tk); -} - -void termkey_destroy(TermKey *tk) -{ - if(tk->is_started) - termkey_stop(tk); - - termkey_free(tk); -} - -void termkey_hook_terminfo_getstr(TermKey *tk, TermKey_Terminfo_Getstr_Hook *hookfn, void *data) -{ - tk->ti_getstr_hook = hookfn; - tk->ti_getstr_hook_data = data; -} - -int termkey_start(TermKey *tk) -{ - if(tk->is_started) - return 1; - -#ifdef HAVE_TERMIOS - if(tk->fd != -1 && !(tk->flags & TERMKEY_FLAG_NOTERMIOS)) { - struct termios termios; - if(tcgetattr(tk->fd, &termios) == 0) { - tk->restore_termios = termios; - tk->restore_termios_valid = 1; - - termios.c_iflag &= ~(IXON|INLCR|ICRNL); - termios.c_lflag &= ~(ICANON|ECHO -#ifdef IEXTEN - | IEXTEN -#endif - ); - termios.c_cc[VMIN] = 1; - termios.c_cc[VTIME] = 0; - - if(tk->flags & TERMKEY_FLAG_CTRLC) - /* want no signal keys at all, so just disable ISIG */ - termios.c_lflag &= ~ISIG; - else { - /* Disable Ctrl-\==VQUIT and Ctrl-D==VSUSP but leave Ctrl-C as SIGINT */ - termios.c_cc[VQUIT] = _POSIX_VDISABLE; - termios.c_cc[VSUSP] = _POSIX_VDISABLE; - /* Some OSes have Ctrl-Y==VDSUSP */ -#ifdef VDSUSP - termios.c_cc[VDSUSP] = _POSIX_VDISABLE; -#endif - } - -#ifdef DEBUG - fprintf(stderr, "Setting termios(3) flags\n"); -#endif - tcsetattr(tk->fd, TCSANOW, &termios); - } - } -#endif - - struct TermKeyDriverNode *p; - for(p = tk->drivers; p; p = p->next) - if(p->driver->start_driver) - if(!(*p->driver->start_driver)(tk, p->info)) - return 0; - -#ifdef DEBUG - fprintf(stderr, "Drivers started; termkey instance %p is ready\n", tk); -#endif - - tk->is_started = 1; - return 1; -} - -int termkey_stop(TermKey *tk) -{ - if(!tk->is_started) - return 1; - - struct TermKeyDriverNode *p; - for(p = tk->drivers; p; p = p->next) - if(p->driver->stop_driver) - (*p->driver->stop_driver)(tk, p->info); - -#ifdef HAVE_TERMIOS - if(tk->restore_termios_valid) - tcsetattr(tk->fd, TCSANOW, &tk->restore_termios); -#endif - - tk->is_started = 0; - - return 1; -} - -int termkey_is_started(TermKey *tk) -{ - return tk->is_started; -} - -int termkey_get_fd(TermKey *tk) -{ - return tk->fd; -} - -int termkey_get_flags(TermKey *tk) -{ - return tk->flags; -} - -void termkey_set_flags(TermKey *tk, int newflags) -{ - tk->flags = newflags; - - if(tk->flags & TERMKEY_FLAG_SPACESYMBOL) - tk->canonflags |= TERMKEY_CANON_SPACESYMBOL; - else - tk->canonflags &= ~TERMKEY_CANON_SPACESYMBOL; -} - -void termkey_set_waittime(TermKey *tk, int msec) -{ - tk->waittime = msec; -} - -int termkey_get_waittime(TermKey *tk) -{ - return tk->waittime; -} - -int termkey_get_canonflags(TermKey *tk) -{ - return tk->canonflags; -} - -void termkey_set_canonflags(TermKey *tk, int flags) -{ - tk->canonflags = flags; - - if(tk->canonflags & TERMKEY_CANON_SPACESYMBOL) - tk->flags |= TERMKEY_FLAG_SPACESYMBOL; - else - tk->flags &= ~TERMKEY_FLAG_SPACESYMBOL; -} - -size_t termkey_get_buffer_size(TermKey *tk) -{ - return tk->buffsize; -} - -int termkey_set_buffer_size(TermKey *tk, size_t size) -{ - unsigned char *buffer = realloc(tk->buffer, size); - if(!buffer) - return 0; - - tk->buffer = buffer; - tk->buffsize = size; - - return 1; -} - -size_t termkey_get_buffer_remaining(TermKey *tk) -{ - /* Return the total number of free bytes in the buffer, because that's what - * is available to the user. */ - return tk->buffsize - tk->buffcount; -} - -static void eat_bytes(TermKey *tk, size_t count) -{ - if(count >= tk->buffcount) { - tk->buffstart = 0; - tk->buffcount = 0; - return; - } - - tk->buffstart += count; - tk->buffcount -= count; -} - -static inline unsigned int utf8_seqlen(long codepoint) -{ - if(codepoint < 0x0000080) return 1; - if(codepoint < 0x0000800) return 2; - if(codepoint < 0x0010000) return 3; - if(codepoint < 0x0200000) return 4; - if(codepoint < 0x4000000) return 5; - return 6; -} - -static void fill_utf8(TermKeyKey *key) -{ - long codepoint = key->code.codepoint; - int nbytes = utf8_seqlen(codepoint); - - key->utf8[nbytes] = 0; - - // This is easier done backwards - int b = nbytes; - while(b > 1) { - b--; - key->utf8[b] = 0x80 | (codepoint & 0x3f); - codepoint >>= 6; - } - - switch(nbytes) { - case 1: key->utf8[0] = (codepoint & 0x7f); break; - case 2: key->utf8[0] = 0xc0 | (codepoint & 0x1f); break; - case 3: key->utf8[0] = 0xe0 | (codepoint & 0x0f); break; - case 4: key->utf8[0] = 0xf0 | (codepoint & 0x07); break; - case 5: key->utf8[0] = 0xf8 | (codepoint & 0x03); break; - case 6: key->utf8[0] = 0xfc | (codepoint & 0x01); break; - } -} - -#define UTF8_INVALID 0xFFFD -static TermKeyResult parse_utf8(const unsigned char *bytes, size_t len, long *cp, size_t *nbytep) -{ - unsigned int nbytes; - - unsigned char b0 = bytes[0]; - - if(b0 < 0x80) { - // Single byte ASCII - *cp = b0; - *nbytep = 1; - return TERMKEY_RES_KEY; - } - else if(b0 < 0xc0) { - // Starts with a continuation byte - that's not right - *cp = UTF8_INVALID; - *nbytep = 1; - return TERMKEY_RES_KEY; - } - else if(b0 < 0xe0) { - nbytes = 2; - *cp = b0 & 0x1f; - } - else if(b0 < 0xf0) { - nbytes = 3; - *cp = b0 & 0x0f; - } - else if(b0 < 0xf8) { - nbytes = 4; - *cp = b0 & 0x07; - } - else if(b0 < 0xfc) { - nbytes = 5; - *cp = b0 & 0x03; - } - else if(b0 < 0xfe) { - nbytes = 6; - *cp = b0 & 0x01; - } - else { - *cp = UTF8_INVALID; - *nbytep = 1; - return TERMKEY_RES_KEY; - } - - for(unsigned int b = 1; b < nbytes; b++) { - unsigned char cb; - - if(b >= len) - return TERMKEY_RES_AGAIN; - - cb = bytes[b]; - if(cb < 0x80 || cb >= 0xc0) { - *cp = UTF8_INVALID; - *nbytep = b; - return TERMKEY_RES_KEY; - } - - *cp <<= 6; - *cp |= cb & 0x3f; - } - - // Check for overlong sequences - if(nbytes > utf8_seqlen(*cp)) - *cp = UTF8_INVALID; - - // Check for UTF-16 surrogates or invalid *cps - if((*cp >= 0xD800 && *cp <= 0xDFFF) || - *cp == 0xFFFE || - *cp == 0xFFFF) - *cp = UTF8_INVALID; - - *nbytep = nbytes; - return TERMKEY_RES_KEY; -} - -static void emit_codepoint(TermKey *tk, long codepoint, TermKeyKey *key) -{ - if(codepoint == 0) { - // ASCII NUL = Ctrl-Space - key->type = TERMKEY_TYPE_KEYSYM; - key->code.sym = TERMKEY_SYM_SPACE; - key->modifiers = TERMKEY_KEYMOD_CTRL; - } - else if(codepoint < 0x20) { - // C0 range - key->code.codepoint = 0; - key->modifiers = 0; - - if(!(tk->flags & TERMKEY_FLAG_NOINTERPRET) && tk->c0[codepoint].sym != TERMKEY_SYM_UNKNOWN) { - key->code.sym = tk->c0[codepoint].sym; - key->modifiers |= tk->c0[codepoint].modifier_set; - } - - if(!key->code.sym) { - key->type = TERMKEY_TYPE_UNICODE; - /* Generically modified Unicode ought not report the SHIFT state, or else - * we get into complications trying to report Shift-; vs : and so on... - * In order to be able to represent Ctrl-Shift-A as CTRL modified - * unicode A, we need to call Ctrl-A simply 'a', lowercase - */ - if(codepoint+0x40 >= 'A' && codepoint+0x40 <= 'Z') - // it's a letter - use lowercase instead - key->code.codepoint = codepoint + 0x60; - else - key->code.codepoint = codepoint + 0x40; - key->modifiers = TERMKEY_KEYMOD_CTRL; - } - else { - key->type = TERMKEY_TYPE_KEYSYM; - } - } - else if(codepoint == 0x7f && !(tk->flags & TERMKEY_FLAG_NOINTERPRET)) { - // ASCII DEL - key->type = TERMKEY_TYPE_KEYSYM; - key->code.sym = TERMKEY_SYM_DEL; - key->modifiers = 0; - } - else if(codepoint >= 0x20 && codepoint < 0x80) { - // ASCII lowbyte range - key->type = TERMKEY_TYPE_UNICODE; - key->code.codepoint = codepoint; - key->modifiers = 0; - } - else if(codepoint >= 0x80 && codepoint < 0xa0) { - // UTF-8 never starts with a C1 byte. So we can be sure of these - key->type = TERMKEY_TYPE_UNICODE; - key->code.codepoint = codepoint - 0x40; - key->modifiers = TERMKEY_KEYMOD_CTRL|TERMKEY_KEYMOD_ALT; - } - else { - // UTF-8 codepoint - key->type = TERMKEY_TYPE_UNICODE; - key->code.codepoint = codepoint; - key->modifiers = 0; - } - - termkey_canonicalise(tk, key); - - if(key->type == TERMKEY_TYPE_UNICODE) - fill_utf8(key); -} - -void termkey_canonicalise(TermKey *tk, TermKeyKey *key) -{ - int flags = tk->canonflags; - - if(flags & TERMKEY_CANON_SPACESYMBOL) { - if(key->type == TERMKEY_TYPE_UNICODE && key->code.codepoint == 0x20) { - key->type = TERMKEY_TYPE_KEYSYM; - key->code.sym = TERMKEY_SYM_SPACE; - } - } - else { - if(key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_SPACE) { - key->type = TERMKEY_TYPE_UNICODE; - key->code.codepoint = 0x20; - fill_utf8(key); - } - } - - if(flags & TERMKEY_CANON_DELBS) { - if(key->type == TERMKEY_TYPE_KEYSYM && key->code.sym == TERMKEY_SYM_DEL) { - key->code.sym = TERMKEY_SYM_BACKSPACE; - } - } -} - -static TermKeyResult peekkey(TermKey *tk, TermKeyKey *key, int force, size_t *nbytep) -{ - int again = 0; - - if(!tk->is_started) { - errno = EINVAL; - return TERMKEY_RES_ERROR; - } - -#ifdef DEBUG - fprintf(stderr, "getkey(force=%d): buffer ", force); - print_buffer(tk); - fprintf(stderr, "\n"); -#endif - - if(tk->hightide) { - tk->buffstart += tk->hightide; - tk->buffcount -= tk->hightide; - tk->hightide = 0; - } - - TermKeyResult ret; - struct TermKeyDriverNode *p; - for(p = tk->drivers; p; p = p->next) { - ret = (p->driver->peekkey)(tk, p->info, key, force, nbytep); - -#ifdef DEBUG - fprintf(stderr, "Driver %s yields %s\n", p->driver->name, res2str(ret)); -#endif - - switch(ret) { - case TERMKEY_RES_KEY: -#ifdef DEBUG - print_key(tk, key); fprintf(stderr, "\n"); -#endif - // Slide the data down to stop it running away - { - size_t halfsize = tk->buffsize / 2; - - if(tk->buffstart > halfsize) { - memcpy(tk->buffer, tk->buffer + halfsize, halfsize); - tk->buffstart -= halfsize; - } - } - - /* fallthrough */ - case TERMKEY_RES_EOF: - case TERMKEY_RES_ERROR: - return ret; - - case TERMKEY_RES_AGAIN: - if(!force) - again = 1; - - /* fallthrough */ - case TERMKEY_RES_NONE: - break; - } - } - - if(again) - return TERMKEY_RES_AGAIN; - - ret = peekkey_simple(tk, key, force, nbytep); - -#ifdef DEBUG - fprintf(stderr, "getkey_simple(force=%d) yields %s\n", force, res2str(ret)); - if(ret == TERMKEY_RES_KEY) { - print_key(tk, key); fprintf(stderr, "\n"); - } -#endif - - return ret; -} - -static TermKeyResult peekkey_simple(TermKey *tk, TermKeyKey *key, int force, size_t *nbytep) -{ - if(tk->buffcount == 0) - return tk->is_closed ? TERMKEY_RES_EOF : TERMKEY_RES_NONE; - - unsigned char b0 = CHARAT(0); - - if(b0 == 0x1b) { - // Escape-prefixed value? Might therefore be Alt+key - if(tk->buffcount == 1) { - // This might be an <Esc> press, or it may want to be part of a longer - // sequence - if(!force) - return TERMKEY_RES_AGAIN; - - (*tk->method.emit_codepoint)(tk, b0, key); - *nbytep = 1; - return TERMKEY_RES_KEY; - } - - // Try another key there - tk->buffstart++; - tk->buffcount--; - - // Run the full driver - TermKeyResult metakey_result = peekkey(tk, key, force, nbytep); - - tk->buffstart--; - tk->buffcount++; - - switch(metakey_result) { - case TERMKEY_RES_KEY: - key->modifiers |= TERMKEY_KEYMOD_ALT; - (*nbytep)++; - break; - - case TERMKEY_RES_NONE: - case TERMKEY_RES_EOF: - case TERMKEY_RES_AGAIN: - case TERMKEY_RES_ERROR: - break; - } - - return metakey_result; - } - else if(b0 < 0xa0) { - // Single byte C0, G0 or C1 - C1 is never UTF-8 initial byte - (*tk->method.emit_codepoint)(tk, b0, key); - *nbytep = 1; - return TERMKEY_RES_KEY; - } - else if(tk->flags & TERMKEY_FLAG_UTF8) { - // Some UTF-8 - long codepoint; - TermKeyResult res = parse_utf8(tk->buffer + tk->buffstart, tk->buffcount, &codepoint, nbytep); - - if(res == TERMKEY_RES_AGAIN && force) { - /* There weren't enough bytes for a complete UTF-8 sequence but caller - * demands an answer. About the best thing we can do here is eat as many - * bytes as we have, and emit a UTF8_INVALID. If the remaining bytes - * arrive later, they'll be invalid too. - */ - codepoint = UTF8_INVALID; - *nbytep = tk->buffcount; - res = TERMKEY_RES_KEY; - } - - key->type = TERMKEY_TYPE_UNICODE; - key->modifiers = 0; - (*tk->method.emit_codepoint)(tk, codepoint, key); - return res; - } - else { - // Non UTF-8 case - just report the raw byte - key->type = TERMKEY_TYPE_UNICODE; - key->code.codepoint = b0; - key->modifiers = 0; - - key->utf8[0] = key->code.codepoint; - key->utf8[1] = 0; - - *nbytep = 1; - - return TERMKEY_RES_KEY; - } -} - -static TermKeyResult peekkey_mouse(TermKey *tk, TermKeyKey *key, size_t *nbytep) -{ - if(tk->buffcount < 3) - return TERMKEY_RES_AGAIN; - - key->type = TERMKEY_TYPE_MOUSE; - key->code.mouse[0] = CHARAT(0) - 0x20; - key->code.mouse[1] = CHARAT(1) - 0x20; - key->code.mouse[2] = CHARAT(2) - 0x20; - key->code.mouse[3] = 0; - - key->modifiers = (key->code.mouse[0] & 0x1c) >> 2; - key->code.mouse[0] &= ~0x1c; - - *nbytep = 3; - return TERMKEY_RES_KEY; -} - -TermKeyResult termkey_getkey(TermKey *tk, TermKeyKey *key) -{ - size_t nbytes = 0; - TermKeyResult ret = peekkey(tk, key, 0, &nbytes); - - if(ret == TERMKEY_RES_KEY) - eat_bytes(tk, nbytes); - - if(ret == TERMKEY_RES_AGAIN) - /* Call peekkey() again in force mode to obtain whatever it can */ - (void)peekkey(tk, key, 1, &nbytes); - /* Don't eat it yet though */ - - return ret; -} - -TermKeyResult termkey_getkey_force(TermKey *tk, TermKeyKey *key) -{ - size_t nbytes = 0; - TermKeyResult ret = peekkey(tk, key, 1, &nbytes); - - if(ret == TERMKEY_RES_KEY) - eat_bytes(tk, nbytes); - - return ret; -} - -#ifndef _WIN32 -TermKeyResult termkey_waitkey(TermKey *tk, TermKeyKey *key) -{ - if(tk->fd == -1) { - errno = EBADF; - return TERMKEY_RES_ERROR; - } - - while(1) { - TermKeyResult ret = termkey_getkey(tk, key); - - switch(ret) { - case TERMKEY_RES_KEY: - case TERMKEY_RES_EOF: - case TERMKEY_RES_ERROR: - return ret; - - case TERMKEY_RES_NONE: - ret = termkey_advisereadable(tk); - if(ret == TERMKEY_RES_ERROR) - return ret; - break; - - case TERMKEY_RES_AGAIN: - { - if(tk->is_closed) - // We're closed now. Never going to get more bytes so just go with - // what we have - return termkey_getkey_force(tk, key); - - struct pollfd fd; - -retry: - fd.fd = tk->fd; - fd.events = POLLIN; - - int pollret = poll(&fd, 1, tk->waittime); - if(pollret == -1) { - if(errno == EINTR && !(tk->flags & TERMKEY_FLAG_EINTR)) - goto retry; - - return TERMKEY_RES_ERROR; - } - - if(fd.revents & (POLLIN|POLLHUP|POLLERR)) - ret = termkey_advisereadable(tk); - else - ret = TERMKEY_RES_NONE; - - if(ret == TERMKEY_RES_ERROR) - return ret; - if(ret == TERMKEY_RES_NONE) - return termkey_getkey_force(tk, key); - } - break; - } - } - - /* UNREACHABLE */ -} -#endif - -TermKeyResult termkey_advisereadable(TermKey *tk) -{ - ssize_t len; - - if(tk->fd == -1) { - errno = EBADF; - return TERMKEY_RES_ERROR; - } - - if(tk->buffstart) { - memmove(tk->buffer, tk->buffer + tk->buffstart, tk->buffcount); - tk->buffstart = 0; - } - - /* Not expecting it ever to be greater but doesn't hurt to handle that */ - if(tk->buffcount >= tk->buffsize) { - errno = ENOMEM; - return TERMKEY_RES_ERROR; - } - -retry: - len = read(tk->fd, tk->buffer + tk->buffcount, tk->buffsize - tk->buffcount); - - if(len == -1) { - if(errno == EAGAIN) - return TERMKEY_RES_NONE; - else if(errno == EINTR && !(tk->flags & TERMKEY_FLAG_EINTR)) - goto retry; - else - return TERMKEY_RES_ERROR; - } - else if(len < 1) { - tk->is_closed = 1; - return TERMKEY_RES_NONE; - } - else { - tk->buffcount += len; - return TERMKEY_RES_AGAIN; - } -} - -size_t termkey_push_bytes(TermKey *tk, const char *bytes, size_t len) -{ - if(tk->buffstart) { - memmove(tk->buffer, tk->buffer + tk->buffstart, tk->buffcount); - tk->buffstart = 0; - } - - /* Not expecting it ever to be greater but doesn't hurt to handle that */ - if(tk->buffcount >= tk->buffsize) { - errno = ENOMEM; - return (size_t)-1; - } - - if(len > tk->buffsize - tk->buffcount) - len = tk->buffsize - tk->buffcount; - - // memcpy(), not strncpy() in case of null bytes in input - memcpy(tk->buffer + tk->buffcount, bytes, len); - tk->buffcount += len; - - return len; -} - -TermKeySym termkey_register_keyname(TermKey *tk, TermKeySym sym, const char *name) -{ - if(!sym) - sym = tk->nkeynames; - - if(sym >= tk->nkeynames) { - const char **new_keynames = realloc(tk->keynames, sizeof(new_keynames[0]) * (sym + 1)); - if(!new_keynames) - return -1; - - tk->keynames = new_keynames; - - // Fill in the hole - for(int i = tk->nkeynames; i < sym; i++) - tk->keynames[i] = NULL; - - tk->nkeynames = sym + 1; - } - - tk->keynames[sym] = name; - - return sym; -} - -const char *termkey_get_keyname(TermKey *tk, TermKeySym sym) -{ - if(sym == TERMKEY_SYM_UNKNOWN) - return "UNKNOWN"; - - if(sym < tk->nkeynames) - return tk->keynames[sym]; - - return "UNKNOWN"; -} - -static const char *termkey_lookup_keyname_format(TermKey *tk, const char *str, TermKeySym *sym, TermKeyFormat format) -{ - /* We store an array, so we can't do better than a linear search. Doesn't - * matter because user won't be calling this too often */ - - for(*sym = 0; *sym < tk->nkeynames; (*sym)++) { - const char *thiskey = tk->keynames[*sym]; - if(!thiskey) - continue; - size_t len = strlen(thiskey); - if(format & TERMKEY_FORMAT_LOWERSPACE) { - const char *thisstr = str; - if(strpncmp_camel(&thisstr, &thiskey, len) == 0) - return thisstr; - } - else { - if(strncmp(str, thiskey, len) == 0) - return (char *)str + len; - } - } - - return NULL; -} - -const char *termkey_lookup_keyname(TermKey *tk, const char *str, TermKeySym *sym) -{ - return termkey_lookup_keyname_format(tk, str, sym, 0); -} - -TermKeySym termkey_keyname2sym(TermKey *tk, const char *keyname) -{ - TermKeySym sym; - const char *endp = termkey_lookup_keyname(tk, keyname, &sym); - if(!endp || endp[0]) - return TERMKEY_SYM_UNKNOWN; - return sym; -} - -static TermKeySym register_c0(TermKey *tk, TermKeySym sym, unsigned char ctrl, const char *name) -{ - return register_c0_full(tk, sym, 0, 0, ctrl, name); -} - -static TermKeySym register_c0_full(TermKey *tk, TermKeySym sym, int modifier_set, int modifier_mask, unsigned char ctrl, const char *name) -{ - if(ctrl >= 0x20) { - errno = EINVAL; - return -1; - } - - if(name) - sym = termkey_register_keyname(tk, sym, name); - - tk->c0[ctrl].sym = sym; - tk->c0[ctrl].modifier_set = modifier_set; - tk->c0[ctrl].modifier_mask = modifier_mask; - - return sym; -} - -/* Previous name for this function - * No longer declared in termkey.h but it remains in the compiled library for - * backward-compatibility reasons. - */ -size_t termkey_snprint_key(TermKey *tk, char *buffer, size_t len, TermKeyKey *key, TermKeyFormat format) -{ - return termkey_strfkey(tk, buffer, len, key, format); -} - -static struct modnames { - const char *shift, *alt, *ctrl; -} -modnames[] = { - { "S", "A", "C" }, // 0 - { "Shift", "Alt", "Ctrl" }, // LONGMOD - { "S", "M", "C" }, // ALTISMETA - { "Shift", "Meta", "Ctrl" }, // ALTISMETA+LONGMOD - { "s", "a", "c" }, // LOWERMOD - { "shift", "alt", "ctrl" }, // LOWERMOD+LONGMOD - { "s", "m", "c" }, // LOWERMOD+ALTISMETA - { "shift", "meta", "ctrl" }, // LOWERMOD+ALTISMETA+LONGMOD -}; - -size_t termkey_strfkey(TermKey *tk, char *buffer, size_t len, TermKeyKey *key, TermKeyFormat format) -{ - size_t pos = 0; - size_t l = 0; - - struct modnames *mods = &modnames[!!(format & TERMKEY_FORMAT_LONGMOD) + - !!(format & TERMKEY_FORMAT_ALTISMETA) * 2 + - !!(format & TERMKEY_FORMAT_LOWERMOD) * 4]; - - int wrapbracket = (format & TERMKEY_FORMAT_WRAPBRACKET) && - (key->type != TERMKEY_TYPE_UNICODE || key->modifiers != 0); - - char sep = (format & TERMKEY_FORMAT_SPACEMOD) ? ' ' : '-'; - - if(format & TERMKEY_FORMAT_CARETCTRL && - key->type == TERMKEY_TYPE_UNICODE && - key->modifiers == TERMKEY_KEYMOD_CTRL) { - long codepoint = key->code.codepoint; - - // Handle some of the special cases first - if(codepoint >= 'a' && codepoint <= 'z') { - l = snprintf(buffer + pos, len - pos, wrapbracket ? "<^%c>" : "^%c", (char)codepoint - 0x20); - if(l <= 0) return pos; - pos += l; - return pos; - } - else if((codepoint >= '@' && codepoint < 'A') || - (codepoint > 'Z' && codepoint <= '_')) { - l = snprintf(buffer + pos, len - pos, wrapbracket ? "<^%c>" : "^%c", (char)codepoint); - if(l <= 0) return pos; - pos += l; - return pos; - } - } - - if(wrapbracket) { - l = snprintf(buffer + pos, len - pos, "<"); - if(l <= 0) return pos; - pos += l; - } - - if(key->modifiers & TERMKEY_KEYMOD_ALT) { - l = snprintf(buffer + pos, len - pos, "%s%c", mods->alt, sep); - if(l <= 0) return pos; - pos += l; - } - - if(key->modifiers & TERMKEY_KEYMOD_CTRL) { - l = snprintf(buffer + pos, len - pos, "%s%c", mods->ctrl, sep); - if(l <= 0) return pos; - pos += l; - } - - if(key->modifiers & TERMKEY_KEYMOD_SHIFT) { - l = snprintf(buffer + pos, len - pos, "%s%c", mods->shift, sep); - if(l <= 0) return pos; - pos += l; - } - - switch(key->type) { - case TERMKEY_TYPE_UNICODE: - if(!key->utf8[0]) // In case of user-supplied key structures - fill_utf8(key); - l = snprintf(buffer + pos, len - pos, "%s", key->utf8); - break; - case TERMKEY_TYPE_KEYSYM: - { - const char *name = termkey_get_keyname(tk, key->code.sym); - if(format & TERMKEY_FORMAT_LOWERSPACE) - l = snprint_cameltospaces(buffer + pos, len - pos, name); - else - l = snprintf(buffer + pos, len - pos, "%s", name); - } - break; - case TERMKEY_TYPE_FUNCTION: - l = snprintf(buffer + pos, len - pos, "%c%d", - (format & TERMKEY_FORMAT_LOWERSPACE ? 'f' : 'F'), key->code.number); - break; - case TERMKEY_TYPE_MOUSE: - { - TermKeyMouseEvent ev; - int button; - int line, col; - termkey_interpret_mouse(tk, key, &ev, &button, &line, &col); - - l = snprintf(buffer + pos, len - pos, "Mouse%s(%d)", - evnames[ev], button); - - if(format & TERMKEY_FORMAT_MOUSE_POS) { - if(l <= 0) return pos; - pos += l; - - l = snprintf(buffer + pos, len - pos, " @ (%u,%u)", col, line); - } - } - break; - case TERMKEY_TYPE_POSITION: - l = snprintf(buffer + pos, len - pos, "Position"); - break; - case TERMKEY_TYPE_MODEREPORT: - { - int initial, mode, value; - termkey_interpret_modereport(tk, key, &initial, &mode, &value); - if(initial) - l = snprintf(buffer + pos, len - pos, "Mode(%c%d=%d)", initial, mode, value); - else - l = snprintf(buffer + pos, len - pos, "Mode(%d=%d)", mode, value); - } - case TERMKEY_TYPE_DCS: - l = snprintf(buffer + pos, len - pos, "DCS"); - break; - case TERMKEY_TYPE_OSC: - l = snprintf(buffer + pos, len - pos, "OSC"); - break; - case TERMKEY_TYPE_UNKNOWN_CSI: - l = snprintf(buffer + pos, len - pos, "CSI %c", key->code.number & 0xff); - break; - } - - if(l <= 0) return pos; - pos += l; - - if(wrapbracket) { - l = snprintf(buffer + pos, len - pos, ">"); - if(l <= 0) return pos; - pos += l; - } - - return pos; -} - -const char *termkey_strpkey(TermKey *tk, const char *str, TermKeyKey *key, TermKeyFormat format) -{ - struct modnames *mods = &modnames[!!(format & TERMKEY_FORMAT_LONGMOD) + - !!(format & TERMKEY_FORMAT_ALTISMETA) * 2 + - !!(format & TERMKEY_FORMAT_LOWERMOD) * 4]; - - key->modifiers = 0; - - if((format & TERMKEY_FORMAT_CARETCTRL) && str[0] == '^' && str[1]) { - str = termkey_strpkey(tk, str+1, key, format & ~TERMKEY_FORMAT_CARETCTRL); - - if(!str || - key->type != TERMKEY_TYPE_UNICODE || - key->code.codepoint < '@' || key->code.codepoint > '_' || - key->modifiers != 0) - return NULL; - - if(key->code.codepoint >= 'A' && key->code.codepoint <= 'Z') - key->code.codepoint += 0x20; - key->modifiers = TERMKEY_KEYMOD_CTRL; - fill_utf8(key); - return (char *)str; - } - - const char *sep_at; - - while((sep_at = strchr(str, (format & TERMKEY_FORMAT_SPACEMOD) ? ' ' : '-'))) { - size_t n = sep_at - str; - - if(n == strlen(mods->alt) && strncmp(mods->alt, str, n) == 0) - key->modifiers |= TERMKEY_KEYMOD_ALT; - else if(n == strlen(mods->ctrl) && strncmp(mods->ctrl, str, n) == 0) - key->modifiers |= TERMKEY_KEYMOD_CTRL; - else if(n == strlen(mods->shift) && strncmp(mods->shift, str, n) == 0) - key->modifiers |= TERMKEY_KEYMOD_SHIFT; - - else - break; - - str = sep_at + 1; - } - - size_t nbytes; - ssize_t snbytes; - const char *endstr; - int button; - char event_name[32]; - - if((endstr = termkey_lookup_keyname_format(tk, str, &key->code.sym, format))) { - key->type = TERMKEY_TYPE_KEYSYM; - str = endstr; - } - else if(sscanf(str, "F%d%zn", &key->code.number, &snbytes) == 1) { - key->type = TERMKEY_TYPE_FUNCTION; - str += snbytes; - } - else if(sscanf(str, "Mouse%31[^(](%d)%zn", event_name, &button, &snbytes) == 2) { - str += snbytes; - key->type = TERMKEY_TYPE_MOUSE; - - TermKeyMouseEvent ev = TERMKEY_MOUSE_UNKNOWN; - for(size_t i = 0; i < sizeof(evnames)/sizeof(evnames[0]); i++) { - if(strcmp(evnames[i], event_name) == 0) { - ev = TERMKEY_MOUSE_UNKNOWN + i; - break; - } - } - - int code; - switch(ev) { - case TERMKEY_MOUSE_PRESS: - case TERMKEY_MOUSE_DRAG: - code = button - 1; - if(ev == TERMKEY_MOUSE_DRAG) { - code |= 0x20; - } - break; - case TERMKEY_MOUSE_RELEASE: - code = 3; - break; - default: - code = 128; - break; - } - key->code.mouse[0] = code; - - unsigned int line = 0, col = 0; - if((format & TERMKEY_FORMAT_MOUSE_POS) && sscanf(str, " @ (%u,%u)%zn", &col, &line, &snbytes) == 2) { - str += snbytes; - } - termkey_key_set_linecol(key, col, line); - } - // Unicode must be last - else if(parse_utf8((unsigned const char *)str, strlen(str), &key->code.codepoint, &nbytes) == TERMKEY_RES_KEY) { - key->type = TERMKEY_TYPE_UNICODE; - fill_utf8(key); - str += nbytes; - } - else - return NULL; - - termkey_canonicalise(tk, key); - - return (char *)str; -} - -int termkey_keycmp(TermKey *tk, const TermKeyKey *key1p, const TermKeyKey *key2p) -{ - /* Copy the key structs since we'll be modifying them */ - TermKeyKey key1 = *key1p, key2 = *key2p; - - termkey_canonicalise(tk, &key1); - termkey_canonicalise(tk, &key2); - - if(key1.type != key2.type) - return key1.type - key2.type; - - switch(key1.type) { - case TERMKEY_TYPE_UNICODE: - if(key1.code.codepoint != key2.code.codepoint) - return key1.code.codepoint - key2.code.codepoint; - break; - case TERMKEY_TYPE_KEYSYM: - if(key1.code.sym != key2.code.sym) - return key1.code.sym - key2.code.sym; - break; - case TERMKEY_TYPE_FUNCTION: - case TERMKEY_TYPE_UNKNOWN_CSI: - if(key1.code.number != key2.code.number) - return key1.code.number - key2.code.number; - break; - case TERMKEY_TYPE_MOUSE: - { - int cmp = strncmp(key1.code.mouse, key2.code.mouse, 4); - if(cmp != 0) - return cmp; - } - break; - case TERMKEY_TYPE_POSITION: - { - int line1, col1, line2, col2; - termkey_interpret_position(tk, &key1, &line1, &col1); - termkey_interpret_position(tk, &key2, &line2, &col2); - if(line1 != line2) - return line1 - line2; - return col1 - col2; - } - break; - case TERMKEY_TYPE_DCS: - case TERMKEY_TYPE_OSC: - return key1p - key2p; - case TERMKEY_TYPE_MODEREPORT: - { - int initial1, initial2, mode1, mode2, value1, value2; - termkey_interpret_modereport(tk, &key1, &initial1, &mode1, &value1); - termkey_interpret_modereport(tk, &key2, &initial2, &mode2, &value2); - if(initial1 != initial2) - return initial1 - initial2; - if(mode1 != mode2) - return mode1 - mode2; - return value1 - value2; - } - } - - return key1.modifiers - key2.modifiers; -} diff --git a/src/termkey/termkey.h b/src/termkey/termkey.h deleted file mode 100644 index 94405f6516..0000000000 --- a/src/termkey/termkey.h +++ /dev/null @@ -1,256 +0,0 @@ -#ifdef __cplusplus -extern "C" { -#endif - -#ifndef GUARD_TERMKEY_H_ -#define GUARD_TERMKEY_H_ - -#include <stdint.h> -#include <stdlib.h> - -#define TERMKEY_VERSION_MAJOR 0 -#define TERMKEY_VERSION_MINOR 22 - -#define TERMKEY_CHECK_VERSION \ - termkey_check_version(TERMKEY_VERSION_MAJOR, TERMKEY_VERSION_MINOR) - -typedef enum { - TERMKEY_SYM_UNKNOWN = -1, - TERMKEY_SYM_NONE = 0, - - /* Special names in C0 */ - TERMKEY_SYM_BACKSPACE, - TERMKEY_SYM_TAB, - TERMKEY_SYM_ENTER, - TERMKEY_SYM_ESCAPE, - - /* Special names in G0 */ - TERMKEY_SYM_SPACE, - TERMKEY_SYM_DEL, - - /* Special keys */ - TERMKEY_SYM_UP, - TERMKEY_SYM_DOWN, - TERMKEY_SYM_LEFT, - TERMKEY_SYM_RIGHT, - TERMKEY_SYM_BEGIN, - TERMKEY_SYM_FIND, - TERMKEY_SYM_INSERT, - TERMKEY_SYM_DELETE, - TERMKEY_SYM_SELECT, - TERMKEY_SYM_PAGEUP, - TERMKEY_SYM_PAGEDOWN, - TERMKEY_SYM_HOME, - TERMKEY_SYM_END, - - /* Special keys from terminfo */ - TERMKEY_SYM_CANCEL, - TERMKEY_SYM_CLEAR, - TERMKEY_SYM_CLOSE, - TERMKEY_SYM_COMMAND, - TERMKEY_SYM_COPY, - TERMKEY_SYM_EXIT, - TERMKEY_SYM_HELP, - TERMKEY_SYM_MARK, - TERMKEY_SYM_MESSAGE, - TERMKEY_SYM_MOVE, - TERMKEY_SYM_OPEN, - TERMKEY_SYM_OPTIONS, - TERMKEY_SYM_PRINT, - TERMKEY_SYM_REDO, - TERMKEY_SYM_REFERENCE, - TERMKEY_SYM_REFRESH, - TERMKEY_SYM_REPLACE, - TERMKEY_SYM_RESTART, - TERMKEY_SYM_RESUME, - TERMKEY_SYM_SAVE, - TERMKEY_SYM_SUSPEND, - TERMKEY_SYM_UNDO, - - /* Numeric keypad special keys */ - TERMKEY_SYM_KP0, - TERMKEY_SYM_KP1, - TERMKEY_SYM_KP2, - TERMKEY_SYM_KP3, - TERMKEY_SYM_KP4, - TERMKEY_SYM_KP5, - TERMKEY_SYM_KP6, - TERMKEY_SYM_KP7, - TERMKEY_SYM_KP8, - TERMKEY_SYM_KP9, - TERMKEY_SYM_KPENTER, - TERMKEY_SYM_KPPLUS, - TERMKEY_SYM_KPMINUS, - TERMKEY_SYM_KPMULT, - TERMKEY_SYM_KPDIV, - TERMKEY_SYM_KPCOMMA, - TERMKEY_SYM_KPPERIOD, - TERMKEY_SYM_KPEQUALS, - - /* et cetera ad nauseum */ - TERMKEY_N_SYMS -} TermKeySym; - -typedef enum { - TERMKEY_TYPE_UNICODE, - TERMKEY_TYPE_FUNCTION, - TERMKEY_TYPE_KEYSYM, - TERMKEY_TYPE_MOUSE, - TERMKEY_TYPE_POSITION, - TERMKEY_TYPE_MODEREPORT, - TERMKEY_TYPE_DCS, - TERMKEY_TYPE_OSC, - /* add other recognised types here */ - - TERMKEY_TYPE_UNKNOWN_CSI = -1 -} TermKeyType; - -typedef enum { - TERMKEY_RES_NONE, - TERMKEY_RES_KEY, - TERMKEY_RES_EOF, - TERMKEY_RES_AGAIN, - TERMKEY_RES_ERROR -} TermKeyResult; - -typedef enum { - TERMKEY_MOUSE_UNKNOWN, - TERMKEY_MOUSE_PRESS, - TERMKEY_MOUSE_DRAG, - TERMKEY_MOUSE_RELEASE -} TermKeyMouseEvent; - -enum { - TERMKEY_KEYMOD_SHIFT = 1 << 0, - TERMKEY_KEYMOD_ALT = 1 << 1, - TERMKEY_KEYMOD_CTRL = 1 << 2 -}; - -typedef struct { - TermKeyType type; - union { - long codepoint; /* TERMKEY_TYPE_UNICODE */ - int number; /* TERMKEY_TYPE_FUNCTION */ - TermKeySym sym; /* TERMKEY_TYPE_KEYSYM */ - char mouse[4]; /* TERMKEY_TYPE_MOUSE */ - /* opaque. see termkey_interpret_mouse */ - } code; - - int modifiers; - - /* Any Unicode character can be UTF-8 encoded in no more than 6 bytes, plus - * terminating NUL */ - char utf8[7]; -} TermKeyKey; - -typedef struct { - const unsigned char *param; - size_t length; -} TermKeyCsiParam; - -typedef struct TermKey TermKey; - -enum { - TERMKEY_FLAG_NOINTERPRET = 1 << 0, /* Do not interpret C0//DEL codes if possible */ - TERMKEY_FLAG_CONVERTKP = 1 << 1, /* Convert KP codes to regular keypresses */ - TERMKEY_FLAG_RAW = 1 << 2, /* Input is raw bytes, not UTF-8 */ - TERMKEY_FLAG_UTF8 = 1 << 3, /* Input is definitely UTF-8 */ - TERMKEY_FLAG_NOTERMIOS = 1 << 4, /* Do not make initial termios calls on construction */ - TERMKEY_FLAG_SPACESYMBOL = 1 << 5, /* Sets TERMKEY_CANON_SPACESYMBOL */ - TERMKEY_FLAG_CTRLC = 1 << 6, /* Allow Ctrl-C to be read as normal, disabling SIGINT */ - TERMKEY_FLAG_EINTR = 1 << 7, /* Return ERROR on signal (EINTR) rather than retry */ - TERMKEY_FLAG_NOSTART = 1 << 8 /* Do not call termkey_start() in constructor */ -}; - -enum { - TERMKEY_CANON_SPACESYMBOL = 1 << 0, /* Space is symbolic rather than Unicode */ - TERMKEY_CANON_DELBS = 1 << 1 /* Del is converted to Backspace */ -}; - -void termkey_check_version(int major, int minor); - -TermKey *termkey_new(int fd, int flags); -TermKey *termkey_new_abstract(const char *term, int flags); -void termkey_free(TermKey *tk); -void termkey_destroy(TermKey *tk); - -/* Mostly-undocumented hooks for doing evil evil things */ -typedef const char *TermKey_Terminfo_Getstr_Hook(const char *name, const char *value, void *data); -void termkey_hook_terminfo_getstr(TermKey *tk, TermKey_Terminfo_Getstr_Hook *hookfn, void *data); - -int termkey_start(TermKey *tk); -int termkey_stop(TermKey *tk); -int termkey_is_started(TermKey *tk); - -int termkey_get_fd(TermKey *tk); - -int termkey_get_flags(TermKey *tk); -void termkey_set_flags(TermKey *tk, int newflags); - -int termkey_get_waittime(TermKey *tk); -void termkey_set_waittime(TermKey *tk, int msec); - -int termkey_get_canonflags(TermKey *tk); -void termkey_set_canonflags(TermKey *tk, int); - -size_t termkey_get_buffer_size(TermKey *tk); -int termkey_set_buffer_size(TermKey *tk, size_t size); - -size_t termkey_get_buffer_remaining(TermKey *tk); - -void termkey_canonicalise(TermKey *tk, TermKeyKey *key); - -TermKeyResult termkey_getkey(TermKey *tk, TermKeyKey *key); -TermKeyResult termkey_getkey_force(TermKey *tk, TermKeyKey *key); -TermKeyResult termkey_waitkey(TermKey *tk, TermKeyKey *key); - -TermKeyResult termkey_advisereadable(TermKey *tk); - -size_t termkey_push_bytes(TermKey *tk, const char *bytes, size_t len); - -TermKeySym termkey_register_keyname(TermKey *tk, TermKeySym sym, const char *name); -const char *termkey_get_keyname(TermKey *tk, TermKeySym sym); -const char *termkey_lookup_keyname(TermKey *tk, const char *str, TermKeySym *sym); - -TermKeySym termkey_keyname2sym(TermKey *tk, const char *keyname); - -TermKeyResult termkey_interpret_mouse(TermKey *tk, const TermKeyKey *key, TermKeyMouseEvent *event, int *button, int *line, int *col); - -TermKeyResult termkey_interpret_position(TermKey *tk, const TermKeyKey *key, int *line, int *col); - -TermKeyResult termkey_interpret_modereport(TermKey *tk, const TermKeyKey *key, int *initial, int *mode, int *value); - -TermKeyResult termkey_interpret_csi(TermKey *tk, const TermKeyKey *key, TermKeyCsiParam params[], size_t *nparams, unsigned long *cmd); - -TermKeyResult termkey_interpret_csi_param(TermKeyCsiParam param, long *paramp, long subparams[], size_t *nsubparams); - -TermKeyResult termkey_interpret_string(TermKey *tk, const TermKeyKey *key, const char **strp); - -typedef enum { - TERMKEY_FORMAT_LONGMOD = 1 << 0, /* Shift-... instead of S-... */ - TERMKEY_FORMAT_CARETCTRL = 1 << 1, /* ^X instead of C-X */ - TERMKEY_FORMAT_ALTISMETA = 1 << 2, /* Meta- or M- instead of Alt- or A- */ - TERMKEY_FORMAT_WRAPBRACKET = 1 << 3, /* Wrap special keys in brackets like <Escape> */ - TERMKEY_FORMAT_SPACEMOD = 1 << 4, /* M Foo instead of M-Foo */ - TERMKEY_FORMAT_LOWERMOD = 1 << 5, /* meta or m instead of Meta or M */ - TERMKEY_FORMAT_LOWERSPACE = 1 << 6, /* page down instead of PageDown */ - - TERMKEY_FORMAT_MOUSE_POS = 1 << 8 /* Include mouse position if relevant; @ col,line */ -} TermKeyFormat; - -/* Some useful combinations */ - -#define TERMKEY_FORMAT_VIM (TermKeyFormat)(TERMKEY_FORMAT_ALTISMETA|TERMKEY_FORMAT_WRAPBRACKET) -#define TERMKEY_FORMAT_URWID (TermKeyFormat)(TERMKEY_FORMAT_LONGMOD|TERMKEY_FORMAT_ALTISMETA| \ - TERMKEY_FORMAT_LOWERMOD|TERMKEY_FORMAT_SPACEMOD|TERMKEY_FORMAT_LOWERSPACE) - -size_t termkey_strfkey(TermKey *tk, char *buffer, size_t len, TermKeyKey *key, TermKeyFormat format); -const char *termkey_strpkey(TermKey *tk, const char *str, TermKeyKey *key, TermKeyFormat format); - -int termkey_keycmp(TermKey *tk, const TermKeyKey *key1, const TermKeyKey *key2); - -#endif - -#ifdef __cplusplus -} -#endif diff --git a/src/vterm/keyboard.c b/src/vterm/keyboard.c index d31c8be12d..7e062c8c02 100644 --- a/src/vterm/keyboard.c +++ b/src/vterm/keyboard.c @@ -1,8 +1,7 @@ #include "vterm_internal.h" - #include <stdio.h> -#include "utf8.h" +#include "nvim/tui/termkey/termkey.h" void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod) { diff --git a/src/vterm/mouse.c b/src/vterm/mouse.c index f74abc3d9d..a9d3fe4ca9 100644 --- a/src/vterm/mouse.c +++ b/src/vterm/mouse.c @@ -1,6 +1,6 @@ #include "vterm_internal.h" -#include "utf8.h" +#include "nvim/tui/termkey/termkey.h" static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row) { diff --git a/src/vterm/screen.c b/src/vterm/screen.c index bd3cbd6bd0..1f5bb36114 100644 --- a/src/vterm/screen.c +++ b/src/vterm/screen.c @@ -2,9 +2,10 @@ #include <stdio.h> #include <string.h> +#include "nvim/mbyte.h" +#include "nvim/tui/termkey/termkey.h" #include "rect.h" -#include "utf8.h" #define UNICODE_SPACE 0x20 #define UNICODE_LINEFEED 0x0a @@ -508,8 +509,6 @@ static int line_popcount(ScreenCell *buffer, int row, int rows, int cols) return col + 1; } -#define REFLOW (screen->reflow) - static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new_cols, bool active, VTermStateFields *statefields) { int old_rows = screen->rows; @@ -540,13 +539,13 @@ static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new while(old_row >= 0) { int old_row_end = old_row; /* TODO: Stop if dwl or dhl */ - while(REFLOW && old_lineinfo && old_row > 0 && old_lineinfo[old_row].continuation) + while(screen->reflow && old_lineinfo && old_row > 0 && old_lineinfo[old_row].continuation) old_row--; int old_row_start = old_row; int width = 0; for(int row = old_row_start; row <= old_row_end; row++) { - if(REFLOW && row < (old_rows - 1) && old_lineinfo[row + 1].continuation) + if(screen->reflow && row < (old_rows - 1) && old_lineinfo[row + 1].continuation) width += old_cols; else width += line_popcount(old_buffer, row, old_rows, old_cols); @@ -555,7 +554,7 @@ static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new if(final_blank_row == (new_row + 1) && width == 0) final_blank_row = new_row; - int new_height = REFLOW + int new_height = screen->reflow ? width ? (width + new_cols - 1) / new_cols : 1 : 1; @@ -628,7 +627,7 @@ static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new if(old_col == old_cols) { old_row++; - if(!REFLOW) { + if(!screen->reflow) { new_col++; break; } @@ -922,7 +921,7 @@ static size_t _get_chars(const VTermScreen *screen, const int utf8, void *buffer #define PUT(c) \ if(utf8) { \ - size_t thislen = utf8_seqlen(c); \ + size_t thislen = utf_char2len(c); \ if(buffer && outpos + thislen <= len) \ outpos += fill_utf8((c), (char *)buffer + outpos); \ else \ diff --git a/src/vterm/utf8.h b/src/vterm/utf8.h deleted file mode 100644 index 9a336d357f..0000000000 --- a/src/vterm/utf8.h +++ /dev/null @@ -1,39 +0,0 @@ -/* The following functions copied and adapted from libtermkey - * - * http://www.leonerd.org.uk/code/libtermkey/ - */ -static inline unsigned int utf8_seqlen(long codepoint) -{ - if(codepoint < 0x0000080) return 1; - if(codepoint < 0x0000800) return 2; - if(codepoint < 0x0010000) return 3; - if(codepoint < 0x0200000) return 4; - if(codepoint < 0x4000000) return 5; - return 6; -} - -/* Does NOT NUL-terminate the buffer */ -static int fill_utf8(long codepoint, char *str) -{ - int nbytes = utf8_seqlen(codepoint); - - // This is easier done backwards - int b = nbytes; - while(b > 1) { - b--; - str[b] = 0x80 | (codepoint & 0x3f); - codepoint >>= 6; - } - - switch(nbytes) { - case 1: str[0] = (codepoint & 0x7f); break; - case 2: str[0] = 0xc0 | (codepoint & 0x1f); break; - case 3: str[0] = 0xe0 | (codepoint & 0x0f); break; - case 4: str[0] = 0xf0 | (codepoint & 0x07); break; - case 5: str[0] = 0xf8 | (codepoint & 0x03); break; - case 6: str[0] = 0xfc | (codepoint & 0x01); break; - } - - return nbytes; -} -/* end copy */ diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 074d3ac0a3..71703c9b05 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -3201,7 +3201,7 @@ describe('API', function() end) describe('nvim_get_runtime_file', function() - local p = n.alter_slashes + local p = t.fix_slashes it('can find files', function() eq({}, api.nvim_get_runtime_file('bork.borkbork', false)) eq({}, api.nvim_get_runtime_file('bork.borkbork', true)) @@ -3210,36 +3210,36 @@ describe('API', function() local val = api.nvim_get_runtime_file('autoload/remote/*.vim', true) eq(2, #val) if endswith(val[1], 'define.vim') then - ok(endswith(val[1], p 'autoload/remote/define.vim')) - ok(endswith(val[2], p 'autoload/remote/host.vim')) + ok(endswith(p(val[1]), 'autoload/remote/define.vim')) + ok(endswith(p(val[2]), 'autoload/remote/host.vim')) else - ok(endswith(val[1], p 'autoload/remote/host.vim')) - ok(endswith(val[2], p 'autoload/remote/define.vim')) + ok(endswith(p(val[1]), 'autoload/remote/host.vim')) + ok(endswith(p(val[2]), 'autoload/remote/define.vim')) end val = api.nvim_get_runtime_file('autoload/remote/*.vim', false) eq(1, #val) ok( - endswith(val[1], p 'autoload/remote/define.vim') - or endswith(val[1], p 'autoload/remote/host.vim') + endswith(p(val[1]), 'autoload/remote/define.vim') + or endswith(p(val[1]), 'autoload/remote/host.vim') ) val = api.nvim_get_runtime_file('lua', true) eq(1, #val) - ok(endswith(val[1], p 'lua')) + ok(endswith(p(val[1]), 'lua')) val = api.nvim_get_runtime_file('lua/vim', true) eq(1, #val) - ok(endswith(val[1], p 'lua/vim')) + ok(endswith(p(val[1]), 'lua/vim')) end) it('can find directories', function() local val = api.nvim_get_runtime_file('lua/', true) eq(1, #val) - ok(endswith(val[1], p 'lua/')) + ok(endswith(p(val[1]), 'lua/')) val = api.nvim_get_runtime_file('lua/vim/', true) eq(1, #val) - ok(endswith(val[1], p 'lua/vim/')) + ok(endswith(p(val[1]), 'lua/vim/')) eq({}, api.nvim_get_runtime_file('foobarlang/', true)) end) diff --git a/test/functional/autocmd/dirchanged_spec.lua b/test/functional/autocmd/dirchanged_spec.lua index 24ac737b5b..1cde0e0552 100644 --- a/test/functional/autocmd/dirchanged_spec.lua +++ b/test/functional/autocmd/dirchanged_spec.lua @@ -9,7 +9,7 @@ local request = n.request local is_os = t.is_os describe('autocmd DirChanged and DirChangedPre', function() - local curdir = vim.uv.cwd():gsub('\\', '/') + local curdir = t.fix_slashes(vim.uv.cwd()) local dirs = { curdir .. '/Xtest-functional-autocmd-dirchanged.dir1', curdir .. '/Xtest-functional-autocmd-dirchanged.dir2', diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua index 073041fced..d33710a63d 100644 --- a/test/functional/core/fileio_spec.lua +++ b/test/functional/core/fileio_spec.lua @@ -321,11 +321,11 @@ end) describe('tmpdir', function() local tmproot_pat = [=[.*[/\\]nvim%.[^/\\]+]=] local testlog = 'Xtest_tmpdir_log' - local os_tmpdir + local os_tmpdir ---@type string before_each(function() -- Fake /tmp dir so that we can mess it up. - os_tmpdir = vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX') + os_tmpdir = assert(vim.uv.fs_mkdtemp(vim.fs.dirname(t.tmpname(false)) .. '/nvim_XXXXXXXXXX')) end) after_each(function() @@ -414,15 +414,4 @@ describe('tmpdir', function() rm_tmpdir() eq('E5431: tempdir disappeared (3 times)', api.nvim_get_vvar('errmsg')) end) - - it('$NVIM_APPNAME relative path', function() - clear({ - env = { - NVIM_APPNAME = 'a/b', - NVIM_LOG_FILE = testlog, - TMPDIR = os_tmpdir, - }, - }) - matches([=[.*[/\\]a%%b%.[^/\\]+]=], fn.tempname()) - end) end) diff --git a/test/functional/vimscript/server_spec.lua b/test/functional/core/server_spec.lua index 2dabe1afc1..0ec11169e9 100644 --- a/test/functional/vimscript/server_spec.lua +++ b/test/functional/core/server_spec.lua @@ -1,7 +1,6 @@ local t = require('test.testutil') local n = require('test.functional.testnvim')() -local assert_log = t.assert_log local eq, neq, eval = t.eq, t.neq, n.eval local clear, fn, api = n.clear, n.fn, n.api local matches = t.matches @@ -19,12 +18,16 @@ local function clear_serverlist() end end -describe('server', function() - after_each(function() - check_close() - os.remove(testlog) - end) +after_each(function() + check_close() + os.remove(testlog) +end) + +before_each(function() + os.remove(testlog) +end) +describe('server', function() it('serverstart() stores sockets in $XDG_RUNTIME_DIR', function() local dir = 'Xtest_xdg_run' mkdir(dir) @@ -38,6 +41,21 @@ describe('server', function() end end) + it('broken $XDG_RUNTIME_DIR is not fatal #30282', function() + clear { + args_rm = { '--listen' }, + env = { NVIM_LOG_FILE = testlog, XDG_RUNTIME_DIR = '/non-existent-dir/subdir//' }, + } + + if is_os('win') then + -- Windows pipes have a special namespace and thus aren't decided by $XDG_RUNTIME_DIR. + matches('nvim', api.nvim_get_vvar('servername')) + else + eq('', api.nvim_get_vvar('servername')) + t.assert_log('Failed to start server%: no such file or directory', testlog, 100) + end + end) + it('serverstart(), serverstop() does not set $NVIM', function() clear() local s = eval('serverstart()') @@ -88,7 +106,7 @@ describe('server', function() } eq(0, eval("serverstop('')")) eq(0, eval("serverstop('bogus-socket-name')")) - assert_log('Not listening on bogus%-socket%-name', testlog, 10) + t.assert_log('Not listening on bogus%-socket%-name', testlog, 10) end) it('parses endpoints', function() @@ -122,7 +140,7 @@ describe('server', function() if status then table.insert(expected, v4) pcall(fn.serverstart, v4) -- exists already; ignore - assert_log('Failed to start server: address already in use: 127%.0%.0%.1', testlog, 10) + t.assert_log('Failed to start server: address already in use: 127%.0%.0%.1', testlog, 10) end local v6 = '::1:12345' @@ -130,13 +148,13 @@ describe('server', function() if status then table.insert(expected, v6) pcall(fn.serverstart, v6) -- exists already; ignore - assert_log('Failed to start server: address already in use: ::1', testlog, 10) + t.assert_log('Failed to start server: address already in use: ::1', testlog, 10) end eq(expected, fn.serverlist()) clear_serverlist() -- Address without slashes is a "name" which is appended to a generated path. #8519 - matches([[.*[/\\]xtest1%.2%.3%.4[^/\\]*]], fn.serverstart('xtest1.2.3.4')) + matches([[[/\\]xtest1%.2%.3%.4[^/\\]*]], fn.serverstart('xtest1.2.3.4')) clear_serverlist() eq('Vim:Failed to start server: invalid argument', pcall_err(fn.serverstart, '127.0.0.1:65536')) -- invalid port @@ -172,34 +190,77 @@ describe('server', function() end) describe('startup --listen', function() - it('validates', function() - clear() + -- Tests Nvim output when failing to start, with and without "--headless". + -- TODO(justinmk): clear() should have a way to get stdout if Nvim fails to start. + local function _test(args, env, expected) + local function run(cmd) + return n.exec_lua(function(cmd_, env_) + return vim + .system(cmd_, { + text = true, + env = vim.tbl_extend( + 'force', + -- Avoid noise in the logs; we expect failures for these tests. + { NVIM_LOG_FILE = testlog }, + env_ or {} + ), + }) + :wait() + end, cmd, env) --[[@as vim.SystemCompleted]] + end + + local cmd = vim.list_extend({ n.nvim_prog, '+qall!', '--headless' }, args) + local r = run(cmd) + eq(1, r.code) + matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' ')) - -- Tests args with and without "--headless". - local function _test(args, expected) - -- XXX: clear v:shell_error, sigh... - fn.system({ n.nvim_prog, '-es', '+qall!' }) - assert(0 == eval('v:shell_error')) - local cmd = vim.list_extend({ unpack(n.nvim_argv) }, vim.list_extend({ '--headless' }, args)) - local output = fn.system(cmd) - assert(0 ~= eval('v:shell_error')) - -- TODO(justinmk): output not properly captured on Windows? - if is_os('win') then - return - end - matches(expected, output) - matches(expected, fn.system(vim.list_extend({ unpack(n.nvim_argv) }, args))) + if is_os('win') then + return -- On Windows, output without --headless is garbage. end + table.remove(cmd, 3) -- Remove '--headless'. + assert(not vim.tbl_contains(cmd, '--headless')) + r = run(cmd) + eq(1, r.code) + matches(expected, (r.stderr .. r.stdout):gsub('\\n', ' ')) + end + + it('validates', function() + clear { env = { NVIM_LOG_FILE = testlog } } + local in_use = n.eval('v:servername') ---@type string Address already used by another server. + + t.assert_nolog('Failed to start server', testlog, 100) + t.assert_nolog('Host lookup failed', testlog, 100) - _test({ '--listen' }, 'nvim.*: Argument missing after: "%-%-listen"') - _test({ '--listen2' }, 'nvim.*: Garbage after option argument: "%-%-listen2"') - _test({ '--listen', n.eval('v:servername') }, 'nvim.*: Failed to %-%-listen: ".* already .*"') - _test({ '--listen', '/' }, 'nvim.*: Failed to %-%-listen: ".*"') + _test({ '--listen' }, nil, 'nvim.*: Argument missing after: "%-%-listen"') + _test({ '--listen2' }, nil, 'nvim.*: Garbage after option argument: "%-%-listen2"') + _test( + { '--listen', in_use }, + nil, + ('nvim.*: Failed to %%-%%-listen: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use)) + ) + _test({ '--listen', '/' }, nil, 'nvim.*: Failed to %-%-listen: [^:]+: "/"') _test( { '--listen', 'https://example.com' }, - ('nvim.*: Failed to %%-%%-listen: "%s"'):format( - (is_os('mac') or is_os('win')) and 'unknown node or service' - or 'service not available for socket type' + nil, + ('nvim.*: Failed to %%-%%-listen: %s: "https://example.com"'):format( + is_os('mac') and 'unknown node or service' or 'service not available for socket type' + ) + ) + + t.assert_log('Failed to start server', testlog, 100) + t.assert_log('Host lookup failed', testlog, 100) + + _test( + {}, + { NVIM_LISTEN_ADDRESS = in_use }, + ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+ already [^:]+: "%s"'):format(vim.pesc(in_use)) + ) + _test({}, { NVIM_LISTEN_ADDRESS = '/' }, 'nvim.*: Failed $NVIM_LISTEN_ADDRESS: [^:]+: "/"') + _test( + {}, + { NVIM_LISTEN_ADDRESS = 'https://example.com' }, + ('nvim.*: Failed $NVIM_LISTEN_ADDRESS: %s: "https://example.com"'):format( + is_os('mac') and 'unknown node or service' or 'service not available for socket type' ) ) end) @@ -212,6 +273,6 @@ describe('startup --listen', function() -- Address without slashes is a "name" which is appended to a generated path. #8519 clear({ args = { '--listen', 'test-name' } }) - matches([[.*[/\\]test%-name[^/\\]*]], api.nvim_get_vvar('servername')) + matches([[[/\\]test%-name[^/\\]*]], api.nvim_get_vvar('servername')) end) end) diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 8d6ba9712a..f48bcb9360 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -27,7 +27,6 @@ local sleep = vim.uv.sleep local startswith = vim.startswith local write_file = t.write_file local api = n.api -local alter_slashes = n.alter_slashes local is_os = t.is_os local dedent = t.dedent local tbl_map = vim.tbl_map @@ -40,22 +39,15 @@ local testlog = 'Xtest-startupspec-log' describe('startup', function() it('--clean', function() clear() - ok( - string.find( - alter_slashes(api.nvim_get_option_value('runtimepath', {})), - fn.stdpath('config'), - 1, - true - ) ~= nil + matches( + vim.pesc(t.fix_slashes(fn.stdpath('config'))), + t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) ) + clear('--clean') ok( - string.find( - alter_slashes(api.nvim_get_option_value('runtimepath', {})), - fn.stdpath('config'), - 1, - true - ) == nil + not t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) + :match(vim.pesc(t.fix_slashes(fn.stdpath('config')))) ) end) diff --git a/test/functional/editor/defaults_spec.lua b/test/functional/editor/defaults_spec.lua new file mode 100644 index 0000000000..47fd177f7b --- /dev/null +++ b/test/functional/editor/defaults_spec.lua @@ -0,0 +1,100 @@ +-- +-- Tests for default autocmds, mappings, commands, and menus. +-- +-- See options/defaults_spec.lua for default options and environment decisions. +-- + +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local Screen = require('test.functional.ui.screen') + +describe('default', function() + describe('autocommands', function() + it('nvim_terminal.TermClose closes terminal with default shell on success', function() + n.clear() + n.api.nvim_set_option_value('shell', n.testprg('shell-test'), {}) + n.command('set shellcmdflag=EXIT shellredir= shellpipe= shellquote= shellxquote=') + + -- Should not block other events + n.command('let g:n=0') + n.command('au BufEnter * let g:n = g:n + 1') + + n.command('terminal') + t.eq(1, n.eval('get(g:, "n", 0)')) + + t.retry(nil, 1000, function() + t.neq('terminal', n.api.nvim_get_option_value('buftype', { buf = 0 })) + t.eq(2, n.eval('get(g:, "n", 0)')) + end) + end) + end) + + describe('popupmenu', function() + it('can be disabled by user', function() + n.clear { + args = { '+autocmd! nvim_popupmenu', '+aunmenu PopUp' }, + } + local screen = Screen.new(40, 8) + screen:attach() + n.insert([[ + 1 line 1 + 2 https://example.com + 3 line 3 + 4 line 4]]) + + n.api.nvim_input_mouse('right', 'press', '', 0, 1, 4) + screen:expect({ + grid = [[ + 1 line 1 | + 2 ht^tps://example.com | + 3 line 3 | + 4 line 4 | + {1:~ }|*3 + | + ]], + }) + end) + + it('right-click on URL shows "Open in web browser"', function() + n.clear() + local screen = Screen.new(40, 8) + screen:attach() + n.insert([[ + 1 line 1 + 2 https://example.com + 3 line 3 + 4 line 4]]) + + n.api.nvim_input_mouse('right', 'press', '', 0, 3, 4) + screen:expect({ + grid = [[ + 1 line 1 | + 2 https://example.com | + 3 line 3 | + 4 li^ne 4 | + {1:~ }{4: Inspect }{1: }| + {1:~ }{4: }{1: }| + {1:~ }{4: Paste }{1: }| + {4: Select All } | + ]], + }) + + n.api.nvim_input_mouse('right', 'press', '', 0, 1, 4) + screen:expect({ + grid = [[ + 1 line 1 | + 2 ht^tps://example.com | + 3 l{4: Open in web browser } | + 4 l{4: Inspect } | + {1:~ }{4: }{1: }| + {1:~ }{4: Paste }{1: }| + {1:~ }{4: Select All }{1: }| + {4: } | + ]], + }) + end) + end) + + -- describe('key mappings', function() + -- end) +end) diff --git a/test/functional/editor/mode_insert_spec.lua b/test/functional/editor/mode_insert_spec.lua index fc1e6c4ee4..87d5c46134 100644 --- a/test/functional/editor/mode_insert_spec.lua +++ b/test/functional/editor/mode_insert_spec.lua @@ -351,4 +351,97 @@ describe('insert-mode', function() eq(2, api.nvim_win_get_cursor(0)[1]) end) end) + + it('backspace after replacing multibyte chars', function() + local screen = Screen.new(30, 3) + screen:attach() + api.nvim_buf_set_lines(0, 0, -1, true, { 'test ȧ̟̜̝̅̚m̆̉̐̐̇̈ å' }) + feed('^Rabcdefghi') + screen:expect([[ + abcdefghi^ | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcdefgh^å | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcdefg^ å | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcdef^m̆̉̐̐̇̈ å | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcde^ȧ̟̜̝̅̚m̆̉̐̐̇̈ å | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcd^ ȧ̟̜̝̅̚m̆̉̐̐̇̈ å | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<esc>') + + api.nvim_buf_set_lines(0, 0, -1, true, { 'wow 🧑🌾🏳️⚧️x' }) + feed('^Rabcd') + + screen:expect([[ + abcd^🧑🌾🏳️⚧️x | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('e') + screen:expect([[ + abcde^🏳️⚧️x | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('f') + screen:expect([[ + abcdef^x | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcde^🏳️⚧️x | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abcd^🧑🌾🏳️⚧️x | + {1:~ }| + {5:-- REPLACE --} | + ]]) + + feed('<bs>') + screen:expect([[ + abc^ 🧑🌾🏳️⚧️x | + {1:~ }| + {5:-- REPLACE --} | + ]]) + end) end) diff --git a/test/functional/ex_cmds/mksession_spec.lua b/test/functional/ex_cmds/mksession_spec.lua index 9b24854362..8f09e802eb 100644 --- a/test/functional/ex_cmds/mksession_spec.lua +++ b/test/functional/ex_cmds/mksession_spec.lua @@ -170,7 +170,7 @@ describe(':mksession', function() skip(is_os('win'), 'causes rmdir() to fail') local cwd_dir = fn.fnamemodify('.', ':p:~'):gsub([[[\/]*$]], '') - cwd_dir = cwd_dir:gsub([[\]], '/') -- :mksession always uses unix slashes. + cwd_dir = t.fix_slashes(cwd_dir) -- :mksession always uses unix slashes. local session_path = cwd_dir .. '/' .. session_file command('cd ' .. tab_dir) diff --git a/test/functional/lua/uri_spec.lua b/test/functional/lua/uri_spec.lua index 553afb35d3..258b96bc43 100644 --- a/test/functional/lua/uri_spec.lua +++ b/test/functional/lua/uri_spec.lua @@ -217,7 +217,7 @@ describe('URI methods', function() ]], file ) - local expected_uri = 'file:///' .. file:gsub('\\', '/') + local expected_uri = 'file:///' .. t.fix_slashes(file) eq(expected_uri, exec_lua(test_case)) os.remove(file) end) diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 7bba24483e..599b688bf4 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -1071,42 +1071,28 @@ describe('lua stdlib', function() ]]) ) - -- Fix github issue #23654 ok(exec_lua([[ - local a = { sub = { [1] = 'a' } } - local b = { sub = { b = 'a' } } + local a = { sub = { 'a', 'b' } } + local b = { sub = { 'b', 'c' } } local c = vim.tbl_deep_extend('force', a, b) - return vim.deep_equal(c, { sub = { [1] = 'a', b = 'a' } }) + return vim.deep_equal(c, { sub = { 'b', 'c' } }) ]])) - matches( - 'invalid "behavior": nil', - pcall_err( - exec_lua, - [[ - return vim.tbl_deep_extend() - ]] - ) - ) + matches('invalid "behavior": nil', pcall_err(exec_lua, [[return vim.tbl_deep_extend()]])) matches( 'wrong number of arguments %(given 1, expected at least 3%)', - pcall_err( - exec_lua, - [[ - return vim.tbl_deep_extend("keep") - ]] - ) + pcall_err(exec_lua, [[return vim.tbl_deep_extend("keep")]]) ) matches( 'wrong number of arguments %(given 2, expected at least 3%)', - pcall_err( - exec_lua, - [[ - return vim.tbl_deep_extend("keep", {}) - ]] - ) + pcall_err(exec_lua, [[return vim.tbl_deep_extend("keep", {})]]) + ) + + matches( + 'after the second argument%: expected table, got number', + pcall_err(exec_lua, [[return vim.tbl_deep_extend("keep", {}, 42)]]) ) end) diff --git a/test/functional/options/autochdir_spec.lua b/test/functional/options/autochdir_spec.lua index c490ab67a9..a409262d84 100644 --- a/test/functional/options/autochdir_spec.lua +++ b/test/functional/options/autochdir_spec.lua @@ -22,7 +22,7 @@ describe("'autochdir'", function() end) it('is not overwritten by getwinvar() call #17609', function() - local curdir = vim.uv.cwd():gsub('\\', '/') + local curdir = t.fix_slashes(vim.uv.cwd()) local dir_a = curdir .. '/Xtest-functional-options-autochdir.dir_a' local dir_b = curdir .. '/Xtest-functional-options-autochdir.dir_b' mkdir(dir_a) diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua index 0faced5149..e3d15fa30f 100644 --- a/test/functional/options/defaults_spec.lua +++ b/test/functional/options/defaults_spec.lua @@ -1,3 +1,9 @@ +-- +-- Tests for default options and environment decisions. +-- +-- See editor/defaults_spec.lua for default autocmds, mappings, commands, and menus. +-- + local t = require('test.testutil') local n = require('test.functional.testnvim')() local Screen = require('test.functional.ui.screen') @@ -17,7 +23,6 @@ local insert = n.insert local neq = t.neq local mkdir = t.mkdir local rmdir = n.rmdir -local alter_slashes = n.alter_slashes local tbl_contains = vim.tbl_contains local expect_exit = n.expect_exit local check_close = n.check_close @@ -256,7 +261,7 @@ describe('startup defaults', function() NVIM_LOG_FILE = '', -- Empty is invalid. }, }) - eq(xdgstatedir .. '/log', string.gsub(eval('$NVIM_LOG_FILE'), '\\', '/')) + eq(xdgstatedir .. '/log', t.fix_slashes(eval('$NVIM_LOG_FILE'))) end) it('defaults to stdpath("log")/log if invalid', function() @@ -267,7 +272,7 @@ describe('startup defaults', function() NVIM_LOG_FILE = '.', -- Any directory is invalid. }, }) - eq(xdgstatedir .. '/log', string.gsub(eval('$NVIM_LOG_FILE'), '\\', '/')) + eq(xdgstatedir .. '/log', t.fix_slashes(eval('$NVIM_LOG_FILE'))) -- Avoid "failed to open $NVIM_LOG_FILE" noise in test output. expect_exit(command, 'qall!') end) @@ -377,69 +382,69 @@ describe('XDG defaults', function() eq( ( - ( + t.fix_slashes( root_path - .. ('/x'):rep(4096) - .. '/nvim' - .. ',' - .. root_path - .. ('/a'):rep(2048) - .. '/nvim' - .. ',' - .. root_path - .. ('/b'):rep(2048) - .. '/nvim' - .. (',' .. root_path .. '/c/nvim') - .. ',' - .. root_path - .. ('/X'):rep(4096) - .. '/' - .. data_dir - .. '/site' - .. ',' - .. root_path - .. ('/A'):rep(2048) - .. '/nvim/site' - .. ',' - .. root_path - .. ('/B'):rep(2048) - .. '/nvim/site' - .. (',' .. root_path .. '/C/nvim/site') - .. ',' - .. vimruntime - .. ',' - .. libdir - .. (',' .. root_path .. '/C/nvim/site/after') - .. ',' - .. root_path - .. ('/B'):rep(2048) - .. '/nvim/site/after' - .. ',' - .. root_path - .. ('/A'):rep(2048) - .. '/nvim/site/after' - .. ',' - .. root_path - .. ('/X'):rep(4096) - .. '/' - .. data_dir - .. '/site/after' - .. (',' .. root_path .. '/c/nvim/after') - .. ',' - .. root_path - .. ('/b'):rep(2048) - .. '/nvim/after' - .. ',' - .. root_path - .. ('/a'):rep(2048) - .. '/nvim/after' - .. ',' - .. root_path - .. ('/x'):rep(4096) - .. '/nvim/after' - ):gsub('\\', '/') + .. ('/x'):rep(4096) + .. '/nvim' + .. ',' + .. root_path + .. ('/a'):rep(2048) + .. '/nvim' + .. ',' + .. root_path + .. ('/b'):rep(2048) + .. '/nvim' + .. (',' .. root_path .. '/c/nvim') + .. ',' + .. root_path + .. ('/X'):rep(4096) + .. '/' + .. data_dir + .. '/site' + .. ',' + .. root_path + .. ('/A'):rep(2048) + .. '/nvim/site' + .. ',' + .. root_path + .. ('/B'):rep(2048) + .. '/nvim/site' + .. (',' .. root_path .. '/C/nvim/site') + .. ',' + .. vimruntime + .. ',' + .. libdir + .. (',' .. root_path .. '/C/nvim/site/after') + .. ',' + .. root_path + .. ('/B'):rep(2048) + .. '/nvim/site/after' + .. ',' + .. root_path + .. ('/A'):rep(2048) + .. '/nvim/site/after' + .. ',' + .. root_path + .. ('/X'):rep(4096) + .. '/' + .. data_dir + .. '/site/after' + .. (',' .. root_path .. '/c/nvim/after') + .. ',' + .. root_path + .. ('/b'):rep(2048) + .. '/nvim/after' + .. ',' + .. root_path + .. ('/a'):rep(2048) + .. '/nvim/after' + .. ',' + .. root_path + .. ('/x'):rep(4096) + .. '/nvim/after' + ) ), - (api.nvim_get_option_value('runtimepath', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) ) command('set runtimepath&') command('set backupdir&') @@ -448,85 +453,85 @@ describe('XDG defaults', function() command('set viewdir&') eq( ( - ( + t.fix_slashes( root_path - .. ('/x'):rep(4096) - .. '/nvim' - .. ',' - .. root_path - .. ('/a'):rep(2048) - .. '/nvim' - .. ',' - .. root_path - .. ('/b'):rep(2048) - .. '/nvim' - .. (',' .. root_path .. '/c/nvim') - .. ',' - .. root_path - .. ('/X'):rep(4096) - .. '/' - .. data_dir - .. '/site' - .. ',' - .. root_path - .. ('/A'):rep(2048) - .. '/nvim/site' - .. ',' - .. root_path - .. ('/B'):rep(2048) - .. '/nvim/site' - .. (',' .. root_path .. '/C/nvim/site') - .. ',' - .. vimruntime - .. ',' - .. libdir - .. (',' .. root_path .. '/C/nvim/site/after') - .. ',' - .. root_path - .. ('/B'):rep(2048) - .. '/nvim/site/after' - .. ',' - .. root_path - .. ('/A'):rep(2048) - .. '/nvim/site/after' - .. ',' - .. root_path - .. ('/X'):rep(4096) - .. '/' - .. data_dir - .. '/site/after' - .. (',' .. root_path .. '/c/nvim/after') - .. ',' - .. root_path - .. ('/b'):rep(2048) - .. '/nvim/after' - .. ',' - .. root_path - .. ('/a'):rep(2048) - .. '/nvim/after' - .. ',' - .. root_path - .. ('/x'):rep(4096) - .. '/nvim/after' - ):gsub('\\', '/') + .. ('/x'):rep(4096) + .. '/nvim' + .. ',' + .. root_path + .. ('/a'):rep(2048) + .. '/nvim' + .. ',' + .. root_path + .. ('/b'):rep(2048) + .. '/nvim' + .. (',' .. root_path .. '/c/nvim') + .. ',' + .. root_path + .. ('/X'):rep(4096) + .. '/' + .. data_dir + .. '/site' + .. ',' + .. root_path + .. ('/A'):rep(2048) + .. '/nvim/site' + .. ',' + .. root_path + .. ('/B'):rep(2048) + .. '/nvim/site' + .. (',' .. root_path .. '/C/nvim/site') + .. ',' + .. vimruntime + .. ',' + .. libdir + .. (',' .. root_path .. '/C/nvim/site/after') + .. ',' + .. root_path + .. ('/B'):rep(2048) + .. '/nvim/site/after' + .. ',' + .. root_path + .. ('/A'):rep(2048) + .. '/nvim/site/after' + .. ',' + .. root_path + .. ('/X'):rep(4096) + .. '/' + .. data_dir + .. '/site/after' + .. (',' .. root_path .. '/c/nvim/after') + .. ',' + .. root_path + .. ('/b'):rep(2048) + .. '/nvim/after' + .. ',' + .. root_path + .. ('/a'):rep(2048) + .. '/nvim/after' + .. ',' + .. root_path + .. ('/x'):rep(4096) + .. '/nvim/after' + ) ), - (api.nvim_get_option_value('runtimepath', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) ) eq( '.,' .. root_path .. ('/X'):rep(4096) .. '/' .. state_dir .. '/backup//', - (api.nvim_get_option_value('backupdir', {}):gsub('\\', '/')) + t.fix_slashes(api.nvim_get_option_value('backupdir', {})) ) eq( root_path .. ('/X'):rep(4096) .. '/' .. state_dir .. '/swap//', - (api.nvim_get_option_value('directory', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('directory', {})) ) eq( root_path .. ('/X'):rep(4096) .. '/' .. state_dir .. '/undo//', - (api.nvim_get_option_value('undodir', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('undodir', {})) ) eq( root_path .. ('/X'):rep(4096) .. '/' .. state_dir .. '/view//', - (api.nvim_get_option_value('viewdir', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('viewdir', {})) ) end) end) @@ -568,26 +573,26 @@ describe('XDG defaults', function() local vimruntime, libdir = vimruntime_and_libdir() eq( ( - ( + t.fix_slashes( '$XDG_DATA_HOME/nvim' - .. ',$XDG_DATA_DIRS/nvim' - .. ',$XDG_CONFIG_HOME/' - .. data_dir - .. '/site' - .. ',$XDG_CONFIG_DIRS/nvim/site' - .. ',' - .. vimruntime - .. ',' - .. libdir - .. ',$XDG_CONFIG_DIRS/nvim/site/after' - .. ',$XDG_CONFIG_HOME/' - .. data_dir - .. '/site/after' - .. ',$XDG_DATA_DIRS/nvim/after' - .. ',$XDG_DATA_HOME/nvim/after' - ):gsub('\\', '/') + .. ',$XDG_DATA_DIRS/nvim' + .. ',$XDG_CONFIG_HOME/' + .. data_dir + .. '/site' + .. ',$XDG_CONFIG_DIRS/nvim/site' + .. ',' + .. vimruntime + .. ',' + .. libdir + .. ',$XDG_CONFIG_DIRS/nvim/site/after' + .. ',$XDG_CONFIG_HOME/' + .. data_dir + .. '/site/after' + .. ',$XDG_DATA_DIRS/nvim/after' + .. ',$XDG_DATA_HOME/nvim/after' + ) ), - (api.nvim_get_option_value('runtimepath', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) ) command('set runtimepath&') command('set backupdir&') @@ -596,80 +601,80 @@ describe('XDG defaults', function() command('set viewdir&') eq( ( - ( + t.fix_slashes( '$XDG_DATA_HOME/nvim' - .. ',$XDG_DATA_DIRS/nvim' - .. ',$XDG_CONFIG_HOME/' - .. data_dir - .. '/site' - .. ',$XDG_CONFIG_DIRS/nvim/site' - .. ',' - .. vimruntime - .. ',' - .. libdir - .. ',$XDG_CONFIG_DIRS/nvim/site/after' - .. ',$XDG_CONFIG_HOME/' - .. data_dir - .. '/site/after' - .. ',$XDG_DATA_DIRS/nvim/after' - .. ',$XDG_DATA_HOME/nvim/after' - ):gsub('\\', '/') + .. ',$XDG_DATA_DIRS/nvim' + .. ',$XDG_CONFIG_HOME/' + .. data_dir + .. '/site' + .. ',$XDG_CONFIG_DIRS/nvim/site' + .. ',' + .. vimruntime + .. ',' + .. libdir + .. ',$XDG_CONFIG_DIRS/nvim/site/after' + .. ',$XDG_CONFIG_HOME/' + .. data_dir + .. '/site/after' + .. ',$XDG_DATA_DIRS/nvim/after' + .. ',$XDG_DATA_HOME/nvim/after' + ) ), - (api.nvim_get_option_value('runtimepath', {})):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) ) eq( ('.,$XDG_CONFIG_HOME/' .. state_dir .. '/backup//'), - api.nvim_get_option_value('backupdir', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('backupdir', {})) ) eq( ('$XDG_CONFIG_HOME/' .. state_dir .. '/swap//'), - api.nvim_get_option_value('directory', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('directory', {})) ) eq( ('$XDG_CONFIG_HOME/' .. state_dir .. '/undo//'), - api.nvim_get_option_value('undodir', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('undodir', {})) ) eq( ('$XDG_CONFIG_HOME/' .. state_dir .. '/view//'), - api.nvim_get_option_value('viewdir', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('viewdir', {})) ) command('set all&') eq( - ( + t.fix_slashes( '$XDG_DATA_HOME/nvim' - .. ',$XDG_DATA_DIRS/nvim' - .. ',$XDG_CONFIG_HOME/' - .. data_dir - .. '/site' - .. ',$XDG_CONFIG_DIRS/nvim/site' - .. ',' - .. vimruntime - .. ',' - .. libdir - .. ',$XDG_CONFIG_DIRS/nvim/site/after' - .. ',$XDG_CONFIG_HOME/' - .. data_dir - .. '/site/after' - .. ',$XDG_DATA_DIRS/nvim/after' - .. ',$XDG_DATA_HOME/nvim/after' - ):gsub('\\', '/'), - (api.nvim_get_option_value('runtimepath', {})):gsub('\\', '/') + .. ',$XDG_DATA_DIRS/nvim' + .. ',$XDG_CONFIG_HOME/' + .. data_dir + .. '/site' + .. ',$XDG_CONFIG_DIRS/nvim/site' + .. ',' + .. vimruntime + .. ',' + .. libdir + .. ',$XDG_CONFIG_DIRS/nvim/site/after' + .. ',$XDG_CONFIG_HOME/' + .. data_dir + .. '/site/after' + .. ',$XDG_DATA_DIRS/nvim/after' + .. ',$XDG_DATA_HOME/nvim/after' + ), + t.fix_slashes(api.nvim_get_option_value('runtimepath', {})) ) eq( ('.,$XDG_CONFIG_HOME/' .. state_dir .. '/backup//'), - api.nvim_get_option_value('backupdir', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('backupdir', {})) ) eq( ('$XDG_CONFIG_HOME/' .. state_dir .. '/swap//'), - api.nvim_get_option_value('directory', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('directory', {})) ) eq( ('$XDG_CONFIG_HOME/' .. state_dir .. '/undo//'), - api.nvim_get_option_value('undodir', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('undodir', {})) ) eq( ('$XDG_CONFIG_HOME/' .. state_dir .. '/view//'), - api.nvim_get_option_value('viewdir', {}):gsub('\\', '/') + t.fix_slashes(api.nvim_get_option_value('viewdir', {})) ) eq(nil, (fn.tempname()):match('XDG_RUNTIME_DIR')) end) @@ -909,7 +914,7 @@ describe('stdpath()', function() assert_alive() -- Check for crash. #8393 end) - it('supports $NVIM_APPNAME', function() + it('$NVIM_APPNAME', function() local appname = 'NVIM_APPNAME_TEST' .. ('_'):rep(106) clear({ env = { NVIM_APPNAME = appname, NVIM_LOG_FILE = testlog } }) eq(appname, fn.fnamemodify(fn.stdpath('config'), ':t')) @@ -933,7 +938,7 @@ describe('stdpath()', function() local child = vim.fn.jobstart({ vim.v.progpath, '--clean', '--headless', '--listen', 'x', '+qall!' }, { env = { NVIM_APPNAME = %q } }) return vim.fn.jobwait({ child }, %d)[1] ]], - alter_slashes(testAppname), + testAppname, 3000 ) eq(expected_exitcode, exec_lua(lua_code)) @@ -951,24 +956,43 @@ describe('stdpath()', function() test_appname('a/b\\c', 0) end) + it('$NVIM_APPNAME relative path', function() + local tmpdir = t.tmpname(false) + t.mkdir(tmpdir) + + clear({ + args_rm = { '--listen' }, + env = { + NVIM_APPNAME = 'relative/appname', + NVIM_LOG_FILE = testlog, + TMPDIR = tmpdir, + }, + }) + + t.matches(vim.pesc(tmpdir), t.fix_slashes(fn.tempname())) + t.assert_nolog('tempdir', testlog, 100) + t.assert_nolog('TMPDIR', testlog, 100) + t.matches([=[[/\\]relative%-appname.[^/\\]+]=], api.nvim_get_vvar('servername')) + end) + describe('returns a String', function() describe('with "config"', function() it('knows XDG_CONFIG_HOME', function() clear({ env = { - XDG_CONFIG_HOME = alter_slashes('/home/docwhat/.config'), + XDG_CONFIG_HOME = '/home/docwhat/.config', }, }) - eq(alter_slashes('/home/docwhat/.config/nvim'), fn.stdpath('config')) + eq('/home/docwhat/.config/nvim', t.fix_slashes(fn.stdpath('config'))) end) it('handles changes during runtime', function() clear({ env = { - XDG_CONFIG_HOME = alter_slashes('/home/original'), + XDG_CONFIG_HOME = '/home/original', } }) - eq(alter_slashes('/home/original/nvim'), fn.stdpath('config')) - command("let $XDG_CONFIG_HOME='" .. alter_slashes('/home/new') .. "'") - eq(alter_slashes('/home/new/nvim'), fn.stdpath('config')) + eq('/home/original/nvim', t.fix_slashes(fn.stdpath('config'))) + command("let $XDG_CONFIG_HOME='/home/new'") + eq('/home/new/nvim', t.fix_slashes(fn.stdpath('config'))) end) it("doesn't expand $VARIABLES", function() @@ -978,32 +1002,32 @@ describe('stdpath()', function() VARIABLES = 'this-should-not-happen', }, }) - eq(alter_slashes('$VARIABLES/nvim'), fn.stdpath('config')) + eq('$VARIABLES/nvim', t.fix_slashes(fn.stdpath('config'))) end) it("doesn't expand ~/", function() clear({ env = { - XDG_CONFIG_HOME = alter_slashes('~/frobnitz'), + XDG_CONFIG_HOME = '~/frobnitz', } }) - eq(alter_slashes('~/frobnitz/nvim'), fn.stdpath('config')) + eq('~/frobnitz/nvim', t.fix_slashes(fn.stdpath('config'))) end) end) describe('with "data"', function() it('knows XDG_DATA_HOME', function() clear({ env = { - XDG_DATA_HOME = alter_slashes('/home/docwhat/.local'), + XDG_DATA_HOME = '/home/docwhat/.local', } }) - eq(alter_slashes('/home/docwhat/.local/' .. datadir), fn.stdpath('data')) + eq('/home/docwhat/.local/' .. datadir, t.fix_slashes(fn.stdpath('data'))) end) it('handles changes during runtime', function() clear({ env = { - XDG_DATA_HOME = alter_slashes('/home/original'), + XDG_DATA_HOME = '/home/original', } }) - eq(alter_slashes('/home/original/' .. datadir), fn.stdpath('data')) - command("let $XDG_DATA_HOME='" .. alter_slashes('/home/new') .. "'") - eq(alter_slashes('/home/new/' .. datadir), fn.stdpath('data')) + eq('/home/original/' .. datadir, t.fix_slashes(fn.stdpath('data'))) + command("let $XDG_DATA_HOME='/home/new'") + eq('/home/new/' .. datadir, t.fix_slashes(fn.stdpath('data'))) end) it("doesn't expand $VARIABLES", function() @@ -1013,14 +1037,14 @@ describe('stdpath()', function() VARIABLES = 'this-should-not-happen', }, }) - eq(alter_slashes('$VARIABLES/' .. datadir), fn.stdpath('data')) + eq('$VARIABLES/' .. datadir, t.fix_slashes(fn.stdpath('data'))) end) it("doesn't expand ~/", function() clear({ env = { - XDG_DATA_HOME = alter_slashes('~/frobnitz'), + XDG_DATA_HOME = '~/frobnitz', } }) - eq(alter_slashes('~/frobnitz/' .. datadir), fn.stdpath('data')) + eq('~/frobnitz/' .. datadir, t.fix_slashes(fn.stdpath('data'))) end) end) @@ -1028,19 +1052,19 @@ describe('stdpath()', function() it('knows XDG_STATE_HOME', function() clear({ env = { - XDG_STATE_HOME = alter_slashes('/home/docwhat/.local'), + XDG_STATE_HOME = '/home/docwhat/.local', }, }) - eq(alter_slashes('/home/docwhat/.local/' .. statedir), fn.stdpath('state')) + eq('/home/docwhat/.local/' .. statedir, t.fix_slashes(fn.stdpath('state'))) end) it('handles changes during runtime', function() clear({ env = { - XDG_STATE_HOME = alter_slashes('/home/original'), + XDG_STATE_HOME = '/home/original', } }) - eq(alter_slashes('/home/original/' .. statedir), fn.stdpath('state')) - command("let $XDG_STATE_HOME='" .. alter_slashes('/home/new') .. "'") - eq(alter_slashes('/home/new/' .. statedir), fn.stdpath('state')) + eq('/home/original/' .. statedir, t.fix_slashes(fn.stdpath('state'))) + command("let $XDG_STATE_HOME='" .. '/home/new' .. "'") + eq('/home/new/' .. statedir, t.fix_slashes(fn.stdpath('state'))) end) it("doesn't expand $VARIABLES", function() @@ -1050,14 +1074,14 @@ describe('stdpath()', function() VARIABLES = 'this-should-not-happen', }, }) - eq(alter_slashes('$VARIABLES/' .. statedir), fn.stdpath('state')) + eq('$VARIABLES/' .. statedir, t.fix_slashes(fn.stdpath('state'))) end) it("doesn't expand ~/", function() clear({ env = { - XDG_STATE_HOME = alter_slashes('~/frobnitz'), + XDG_STATE_HOME = '~/frobnitz', } }) - eq(alter_slashes('~/frobnitz/' .. statedir), fn.stdpath('state')) + eq('~/frobnitz/' .. statedir, t.fix_slashes(fn.stdpath('state'))) end) end) @@ -1065,19 +1089,19 @@ describe('stdpath()', function() it('knows XDG_CACHE_HOME', function() clear({ env = { - XDG_CACHE_HOME = alter_slashes('/home/docwhat/.cache'), + XDG_CACHE_HOME = '/home/docwhat/.cache', }, }) - eq(alter_slashes('/home/docwhat/.cache/nvim'), fn.stdpath('cache')) + eq('/home/docwhat/.cache/nvim', t.fix_slashes(fn.stdpath('cache'))) end) it('handles changes during runtime', function() clear({ env = { - XDG_CACHE_HOME = alter_slashes('/home/original'), + XDG_CACHE_HOME = '/home/original', } }) - eq(alter_slashes('/home/original/nvim'), fn.stdpath('cache')) - command("let $XDG_CACHE_HOME='" .. alter_slashes('/home/new') .. "'") - eq(alter_slashes('/home/new/nvim'), fn.stdpath('cache')) + eq('/home/original/nvim', t.fix_slashes(fn.stdpath('cache'))) + command("let $XDG_CACHE_HOME='" .. '/home/new' .. "'") + eq('/home/new/nvim', t.fix_slashes(fn.stdpath('cache'))) end) it("doesn't expand $VARIABLES", function() @@ -1087,14 +1111,14 @@ describe('stdpath()', function() VARIABLES = 'this-should-not-happen', }, }) - eq(alter_slashes('$VARIABLES/nvim'), fn.stdpath('cache')) + eq('$VARIABLES/nvim', t.fix_slashes(fn.stdpath('cache'))) end) it("doesn't expand ~/", function() clear({ env = { - XDG_CACHE_HOME = alter_slashes('~/frobnitz'), + XDG_CACHE_HOME = '~/frobnitz', } }) - eq(alter_slashes('~/frobnitz/nvim'), fn.stdpath('cache')) + eq('~/frobnitz/nvim', t.fix_slashes(fn.stdpath('cache'))) end) end) end) @@ -1108,6 +1132,7 @@ describe('stdpath()', function() HOMEDRIVE = 'C:', HOMEPATH = '\\Users\\docwhat', LOCALAPPDATA = 'C:\\Users\\docwhat\\AppData\\Local', + NVIM_LOG_FILE = testlog, TEMP = 'C:\\Users\\docwhat\\AppData\\Local\\Temp', TMPDIR = 'C:\\Users\\docwhat\\AppData\\Local\\Temp', TMP = 'C:\\Users\\docwhat\\AppData\\Local\\Temp', @@ -1118,6 +1143,7 @@ describe('stdpath()', function() HOMEDRIVE = 'HOMEDRIVE-should-be-ignored', HOMEPATH = 'HOMEPATH-should-be-ignored', LOCALAPPDATA = 'LOCALAPPDATA-should-be-ignored', + NVIM_LOG_FILE = testlog, TEMP = 'TEMP-should-be-ignored', TMPDIR = 'TMPDIR-should-be-ignored', TMP = 'TMP-should-be-ignored', @@ -1141,12 +1167,18 @@ describe('stdpath()', function() describe(msg, function() it('set via system', function() set_paths_via_system(env_var_name, paths) - eq(expected_paths, fn.stdpath(stdpath_arg)) + eq(expected_paths, t.fix_slashes(fn.stdpath(stdpath_arg))) + if not is_os('win') then + assert_log('$TMPDIR tempdir not a directory.*TMPDIR%-should%-be%-ignored', testlog, 100) + end end) it('set at runtime', function() set_paths_at_runtime(env_var_name, paths) - eq(expected_paths, fn.stdpath(stdpath_arg)) + eq(expected_paths, t.fix_slashes(fn.stdpath(stdpath_arg))) + if not is_os('win') then + assert_log('$TMPDIR tempdir not a directory.*TMPDIR%-should%-be%-ignored', testlog, 100) + end end) end) end @@ -1157,10 +1189,10 @@ describe('stdpath()', function() 'config_dirs', 'XDG_CONFIG_DIRS', { - alter_slashes('/home/docwhat/.config'), + t.fix_slashes('/home/docwhat/.config'), }, { - alter_slashes('/home/docwhat/.config/nvim'), + t.fix_slashes('/home/docwhat/.config/nvim'), } ) @@ -1169,12 +1201,12 @@ describe('stdpath()', function() 'config_dirs', 'XDG_CONFIG_DIRS', { - alter_slashes('/home/docwhat/.config'), - alter_slashes('/etc/config'), + t.fix_slashes('/home/docwhat/.config'), + t.fix_slashes('/etc/config'), }, { - alter_slashes('/home/docwhat/.config/nvim'), - alter_slashes('/etc/config/nvim'), + t.fix_slashes('/home/docwhat/.config/nvim'), + t.fix_slashes('/etc/config/nvim'), } ) @@ -1184,25 +1216,25 @@ describe('stdpath()', function() 'XDG_CONFIG_DIRS', { '$HOME', '$TMP' }, { - alter_slashes('$HOME/nvim'), - alter_slashes('$TMP/nvim'), + t.fix_slashes('$HOME/nvim'), + t.fix_slashes('$TMP/nvim'), } ) behaves_like_dir_list_env("doesn't expand ~/", 'config_dirs', 'XDG_CONFIG_DIRS', { - alter_slashes('~/.oldconfig'), - alter_slashes('~/.olderconfig'), + t.fix_slashes('~/.oldconfig'), + t.fix_slashes('~/.olderconfig'), }, { - alter_slashes('~/.oldconfig/nvim'), - alter_slashes('~/.olderconfig/nvim'), + t.fix_slashes('~/.oldconfig/nvim'), + t.fix_slashes('~/.olderconfig/nvim'), }) end) describe('with "data_dirs"', function() behaves_like_dir_list_env('knows XDG_DATA_DIRS with one path', 'data_dirs', 'XDG_DATA_DIRS', { - alter_slashes('/home/docwhat/.data'), + t.fix_slashes('/home/docwhat/.data'), }, { - alter_slashes('/home/docwhat/.data/nvim'), + t.fix_slashes('/home/docwhat/.data/nvim'), }) behaves_like_dir_list_env( @@ -1210,12 +1242,12 @@ describe('stdpath()', function() 'data_dirs', 'XDG_DATA_DIRS', { - alter_slashes('/home/docwhat/.data'), - alter_slashes('/etc/local'), + t.fix_slashes('/home/docwhat/.data'), + t.fix_slashes('/etc/local'), }, { - alter_slashes('/home/docwhat/.data/nvim'), - alter_slashes('/etc/local/nvim'), + t.fix_slashes('/home/docwhat/.data/nvim'), + t.fix_slashes('/etc/local/nvim'), } ) @@ -1225,17 +1257,17 @@ describe('stdpath()', function() 'XDG_DATA_DIRS', { '$HOME', '$TMP' }, { - alter_slashes('$HOME/nvim'), - alter_slashes('$TMP/nvim'), + t.fix_slashes('$HOME/nvim'), + t.fix_slashes('$TMP/nvim'), } ) behaves_like_dir_list_env("doesn't expand ~/", 'data_dirs', 'XDG_DATA_DIRS', { - alter_slashes('~/.oldconfig'), - alter_slashes('~/.olderconfig'), + t.fix_slashes('~/.oldconfig'), + t.fix_slashes('~/.olderconfig'), }, { - alter_slashes('~/.oldconfig/nvim'), - alter_slashes('~/.olderconfig/nvim'), + t.fix_slashes('~/.oldconfig/nvim'), + t.fix_slashes('~/.olderconfig/nvim'), }) end) end) @@ -1255,23 +1287,3 @@ describe('stdpath()', function() end) end) end) - -describe('autocommands', function() - it('closes terminal with default shell on success', function() - clear() - api.nvim_set_option_value('shell', n.testprg('shell-test'), {}) - command('set shellcmdflag=EXIT shellredir= shellpipe= shellquote= shellxquote=') - - -- Should not block other events - command('let g:n=0') - command('au BufEnter * let g:n = g:n + 1') - - command('terminal') - eq(1, eval('get(g:, "n", 0)')) - - t.retry(nil, 1000, function() - neq('terminal', api.nvim_get_option_value('buftype', { buf = 0 })) - eq(2, eval('get(g:, "n", 0)')) - end) - end) -end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 1347ea9745..88b0e0c991 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -2673,7 +2673,7 @@ describe('LSP', function() describe('lsp.util.locations_to_items', function() it('Convert Location[] to items', function() - local expected = { + local expected_template = { { filename = '/fake/uri', lnum = 1, @@ -2681,7 +2681,12 @@ describe('LSP', function() col = 3, end_col = 4, text = 'testing', - user_data = { + user_data = {}, + }, + } + local test_params = { + { + { uri = 'file:///fake/uri', range = { start = { line = 0, character = 2 }, @@ -2689,23 +2694,28 @@ describe('LSP', function() }, }, }, - } - local actual = exec_lua(function() - local bufnr = vim.uri_to_bufnr('file:///fake/uri') - local lines = { 'testing', '123' } - vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) - local locations = { + { { uri = 'file:///fake/uri', range = { start = { line = 0, character = 2 }, - ['end'] = { line = 1, character = 3 }, + -- LSP spec: if character > line length, default to the line length. + ['end'] = { line = 1, character = 10000 }, }, }, - } - return vim.lsp.util.locations_to_items(locations, 'utf-16') - end) - eq(expected, actual) + }, + } + for _, params in ipairs(test_params) do + local actual = exec_lua(function(params0) + local bufnr = vim.uri_to_bufnr('file:///fake/uri') + local lines = { 'testing', '123' } + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return vim.lsp.util.locations_to_items(params0, 'utf-16') + end, params) + local expected = vim.deepcopy(expected_template) + expected[1].user_data = params[1] + eq(expected, actual) + end end) it('Convert LocationLink[] to items', function() @@ -3597,21 +3607,21 @@ describe('LSP', function() range = { ['end'] = { character = 8, - line = 9, + line = 3, }, start = { character = 6, - line = 9, + line = 3, }, }, selectionRange = { ['end'] = { character = 8, - line = 9, + line = 3, }, start = { character = 6, - line = 9, + line = 3, }, }, uri = 'file:///home/jiangyinzuo/hello.cpp', @@ -3641,21 +3651,21 @@ describe('LSP', function() range = { ['end'] = { character = 8, - line = 8, + line = 2, }, start = { character = 6, - line = 8, + line = 2, }, }, selectionRange = { ['end'] = { character = 8, - line = 8, + line = 2, }, start = { character = 6, - line = 8, + line = 2, }, }, uri = 'file:///home/jiangyinzuo/hello.cpp', @@ -3669,7 +3679,15 @@ describe('LSP', function() }) local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes'] - handler(nil, clangd_response, { client_id = client_id, bufnr = 1 }) + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'class B : public A{};', + 'class C : public B{};', + 'class D1 : public C{};', + 'class D2 : public C{};', + 'class E : public D1, D2 {};', + }) + handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr }) return vim.fn.getqflist() end) @@ -3679,7 +3697,7 @@ describe('LSP', function() col = 7, end_col = 0, end_lnum = 0, - lnum = 10, + lnum = 4, module = '', nr = 0, pattern = '', @@ -3693,7 +3711,7 @@ describe('LSP', function() col = 7, end_col = 0, end_lnum = 0, - lnum = 9, + lnum = 3, module = '', nr = 0, pattern = '', @@ -3753,7 +3771,15 @@ describe('LSP', function() }) local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) local handler = require 'vim.lsp.handlers'['typeHierarchy/subtypes'] - handler(nil, jdtls_response, { client_id = client_id, bufnr = 1 }) + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'package mylist;', + '', + 'public class MyList {', + ' static class Inner extends MyList{}', + '~}', + }) + handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr }) return vim.fn.getqflist() end) @@ -3830,21 +3856,21 @@ describe('LSP', function() range = { ['end'] = { character = 8, - line = 9, + line = 3, }, start = { character = 6, - line = 9, + line = 3, }, }, selectionRange = { ['end'] = { character = 8, - line = 9, + line = 3, }, start = { character = 6, - line = 9, + line = 3, }, }, uri = 'file:///home/jiangyinzuo/hello.cpp', @@ -3874,21 +3900,21 @@ describe('LSP', function() range = { ['end'] = { character = 8, - line = 8, + line = 2, }, start = { character = 6, - line = 8, + line = 2, }, }, selectionRange = { ['end'] = { character = 8, - line = 8, + line = 2, }, start = { character = 6, - line = 8, + line = 2, }, }, uri = 'file:///home/jiangyinzuo/hello.cpp', @@ -3902,7 +3928,16 @@ describe('LSP', function() }) local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes'] - handler(nil, clangd_response, { client_id = client_id, bufnr = 1 }) + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'class B : public A{};', + 'class C : public B{};', + 'class D1 : public C{};', + 'class D2 : public C{};', + 'class E : public D1, D2 {};', + }) + + handler(nil, clangd_response, { client_id = client_id, bufnr = bufnr }) return vim.fn.getqflist() end) @@ -3912,7 +3947,7 @@ describe('LSP', function() col = 7, end_col = 0, end_lnum = 0, - lnum = 10, + lnum = 4, module = '', nr = 0, pattern = '', @@ -3926,7 +3961,7 @@ describe('LSP', function() col = 7, end_col = 0, end_lnum = 0, - lnum = 9, + lnum = 3, module = '', nr = 0, pattern = '', @@ -3986,7 +4021,15 @@ describe('LSP', function() }) local client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) local handler = require 'vim.lsp.handlers'['typeHierarchy/supertypes'] - handler(nil, jdtls_response, { client_id = client_id, bufnr = 1 }) + local bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { + 'package mylist;', + '', + 'public class MyList {', + ' static class Inner extends MyList{}', + '~}', + }) + handler(nil, jdtls_response, { client_id = client_id, bufnr = bufnr }) return vim.fn.getqflist() end) diff --git a/test/functional/plugin/tohtml_spec.lua b/test/functional/plugin/tohtml_spec.lua index dbf385c0c3..98a422935c 100644 --- a/test/functional/plugin/tohtml_spec.lua +++ b/test/functional/plugin/tohtml_spec.lua @@ -136,6 +136,50 @@ local function run_tohtml_and_assert(screen, func) screen:expect({ grid = expected.grid, attr_ids = expected.attr_ids }) end +---@param guifont boolean +local function test_generates_html(guifont, expect_font) + insert([[line]]) + exec('set termguicolors') + local bg = fn.synIDattr(fn.hlID('Normal'), 'bg#', 'gui') + local fg = fn.synIDattr(fn.hlID('Normal'), 'fg#', 'gui') + local tmpfile = t.tmpname() + + exec_lua( + [[ + local guifont, outfile = ... + local html = (guifont + and require('tohtml').tohtml(0,{title="title"}) + or require('tohtml').tohtml(0,{title="title",font={ "dumyfont","anotherfont" }})) + vim.fn.writefile(html, outfile) + vim.cmd.split(outfile) + ]], + guifont, + tmpfile + ) + + local out_file = api.nvim_buf_get_name(api.nvim_get_current_buf()) + eq({ + '<!DOCTYPE html>', + '<html>', + '<head>', + '<meta charset="UTF-8">', + '<title>title</title>', + ('<meta name="colorscheme" content="%s"></meta>'):format(api.nvim_get_var('colors_name')), + '<style>', + ('* {font-family: %s,monospace}'):format(expect_font), + ('body {background-color: %s; color: %s}'):format(bg, fg), + '</style>', + '</head>', + '<body style="display: flex">', + '<pre>', + 'line', + '', + '</pre>', + '</body>', + '</html>', + }, fn.readfile(out_file)) +end + describe(':TOhtml', function() --- @type test.functional.ui.screen local screen @@ -146,41 +190,16 @@ describe(':TOhtml', function() exec('colorscheme default') end) - it('expected internal html generated', function() - insert([[line]]) - exec('set termguicolors') - local bg = fn.synIDattr(fn.hlID('Normal'), 'bg#', 'gui') - local fg = fn.synIDattr(fn.hlID('Normal'), 'fg#', 'gui') - exec_lua [[ - local outfile = vim.fn.tempname() .. '.html' - local html = require('tohtml').tohtml(0,{title="title",font="dumyfont"}) - vim.fn.writefile(html, outfile) - vim.cmd.split(outfile) - ]] - local out_file = api.nvim_buf_get_name(api.nvim_get_current_buf()) - eq({ - '<!DOCTYPE html>', - '<html>', - '<head>', - '<meta charset="UTF-8">', - '<title>title</title>', - ('<meta name="colorscheme" content="%s"></meta>'):format(api.nvim_get_var('colors_name')), - '<style>', - '* {font-family: dumyfont,monospace}', - ('body {background-color: %s; color: %s}'):format(bg, fg), - '</style>', - '</head>', - '<body style="display: flex">', - '<pre>', - 'line', - '', - '</pre>', - '</body>', - '</html>', - }, fn.readfile(out_file)) + it('generates html with given font', function() + test_generates_html(false, '"dumyfont","anotherfont"') + end) + + it("generates html, respects 'guifont'", function() + exec_lua [[vim.o.guifont='Font,Escape\\,comma, Ignore space after comma']] + test_generates_html(true, '"Font","Escape,comma","Ignore space after comma"') end) - it('expected internal html generated from range', function() + it('generates html from range', function() insert([[ line1 line2 @@ -218,7 +237,7 @@ describe(':TOhtml', function() }, fn.readfile(out_file)) end) - it('highlight attributes generated', function() + it('generates highlight attributes', function() --Make sure to uncomment the attribute in `html_syntax_match()` exec('hi LINE guisp=#00ff00 gui=' .. table.concat({ 'bold', diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index 767a3dc205..888c4538af 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -342,7 +342,7 @@ describe(':terminal buffer', function() command('wincmd p') -- cwd will be inserted in a file URI, which cannot contain backs - local cwd = fn.getcwd():gsub('\\', '/') + local cwd = t.fix_slashes(fn.getcwd()) local parent = cwd:match('^(.+/)') local expected = '\027]7;file://host' .. parent api.nvim_chan_send(term, string.format('%s\027\\', expected)) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 50199bd83d..bba1436bdc 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -630,6 +630,8 @@ describe('TUI', function() set mouse=a mousemodel=popup aunmenu PopUp + " Delete the default MenuPopup event handler. + autocmd! nvim_popupmenu menu PopUp.foo :let g:menustr = 'foo'<CR> menu PopUp.bar :let g:menustr = 'bar'<CR> menu PopUp.baz :let g:menustr = 'baz'<CR> diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua index 66ce6daacb..8c8b239cd8 100644 --- a/test/functional/testnvim.lua +++ b/test/functional/testnvim.lua @@ -14,8 +14,7 @@ local is_os = t.is_os local ok = t.ok local sleep = uv.sleep ---- This module uses functions from the context of the test session, i.e. in the context of the ---- nvim being tests. +--- Functions executing in the current nvim session/process being tested. local M = {} local runtime_set = 'set runtimepath^=./build/lib/nvim/' @@ -903,26 +902,6 @@ function M.missing_provider(provider) assert(false, 'Unknown provider: ' .. provider) end ---- @param obj string|table ---- @return any -function M.alter_slashes(obj) - if not is_os('win') then - return obj - end - if type(obj) == 'string' then - local ret = obj:gsub('/', '\\') - return ret - elseif type(obj) == 'table' then - --- @cast obj table<any,any> - local ret = {} --- @type table<any,any> - for k, v in pairs(obj) do - ret[k] = M.alter_slashes(v) - end - return ret - end - assert(false, 'expected string or table of strings, got ' .. type(obj)) -end - local load_factor = 1 if t.is_ci() then -- Compute load factor only once (but outside of any tests). diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index 61a5e1d6f7..042975f898 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -5641,6 +5641,19 @@ l5 ]]) eq("Invalid 'sign_text'", pcall_err(api.nvim_buf_set_extmark, 0, ns, 5, 0, {sign_text='❤️x'})) end) + + it('auto signcolumn hides with invalidated sign', function() + api.nvim_set_option_value('signcolumn', 'auto', {}) + api.nvim_buf_set_extmark(0, ns, 0, 0, {sign_text='S1', invalidate=true}) + feed('ia<cr>b<esc>dd') + screen:expect({ + grid = [[ + ^a | + {1:~ }|*8 + | + ]] + }) + end) end) describe('decorations: virt_text', function() diff --git a/test/functional/ui/multigrid_spec.lua b/test/functional/ui/multigrid_spec.lua index dc48061a6c..63ae38d3c3 100644 --- a/test/functional/ui/multigrid_spec.lua +++ b/test/functional/ui/multigrid_spec.lua @@ -1095,6 +1095,7 @@ describe('ext_multigrid', function() end) it('supports mouse', function() + command('autocmd! nvim_popupmenu') -- Delete the default MenuPopup event handler. insert('some text\nto be clicked') screen:expect{grid=[[ ## grid 1 diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index 370a18b908..3acbd5d987 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -851,6 +851,8 @@ describe('ui/ext_popupmenu', function() set mouse=a mousemodel=popup aunmenu PopUp + " Delete the default MenuPopup event handler. + autocmd! nvim_popupmenu menu PopUp.foo :let g:menustr = 'foo'<CR> menu PopUp.bar :let g:menustr = 'bar'<CR> menu PopUp.baz :let g:menustr = 'baz'<CR> @@ -3805,6 +3807,8 @@ describe('builtin popupmenu', function() call setline(1, 'popup menu test') set mouse=a mousemodel=popup + " Delete the default MenuPopup event handler. + autocmd! nvim_popupmenu aunmenu PopUp menu PopUp.foo :let g:menustr = 'foo'<CR> menu PopUp.bar :let g:menustr = 'bar'<CR> @@ -4489,6 +4493,9 @@ describe('builtin popupmenu', function() -- oldtest: Test_popup_command_dump() it(':popup command', function() exec([[ + " Delete the default MenuPopup event handler. + autocmd! nvim_popupmenu + func ChangeMenu() aunmenu PopUp.&Paste nnoremenu 1.40 PopUp.&Paste :echomsg "pasted"<CR> @@ -4646,6 +4653,8 @@ describe('builtin popupmenu', function() screen:try_resize(50, 20) exec([[ set mousemodel=popup_setpos + " Delete the default MenuPopup event handler. + autocmd! nvim_popupmenu aunmenu * source $VIMRUNTIME/menu.vim call setline(1, join(range(20))) diff --git a/test/functional/vimscript/fnamemodify_spec.lua b/test/functional/vimscript/fnamemodify_spec.lua index 51b1e8489a..f2cee9b83e 100644 --- a/test/functional/vimscript/fnamemodify_spec.lua +++ b/test/functional/vimscript/fnamemodify_spec.lua @@ -7,11 +7,10 @@ local fnamemodify = n.fn.fnamemodify local getcwd = n.fn.getcwd local command = n.command local write_file = t.write_file -local alter_slashes = n.alter_slashes local is_os = t.is_os local function eq_slashconvert(expected, got) - eq(alter_slashes(expected), alter_slashes(got)) + eq(t.fix_slashes(expected), t.fix_slashes(got)) end describe('fnamemodify()', function() diff --git a/test/old/testdir/setup.vim b/test/old/testdir/setup.vim index 6f400c5e32..e7b4bb1a88 100644 --- a/test/old/testdir/setup.vim +++ b/test/old/testdir/setup.vim @@ -66,6 +66,7 @@ mapclear mapclear! aunmenu * tlunmenu * +autocmd! nvim_popupmenu " Undo the 'grepprg' and 'grepformat' setting in _defaults.lua. set grepprg& grepformat& diff --git a/test/old/testdir/test_breakindent.vim b/test/old/testdir/test_breakindent.vim index f09ab8684f..3787747104 100644 --- a/test/old/testdir/test_breakindent.vim +++ b/test/old/testdir/test_breakindent.vim @@ -837,18 +837,73 @@ func Test_breakindent20_list() \ ] let lines = s:screen_lines2(1, 9, 20) call s:compare_lines(expect, lines) + + " check with TABs + call setline(1, ["\t1.\tCongress shall make no law", + \ "\t2.) Congress shall make no law", + \ "\t3.] Congress shall make no law"]) + setl tabstop=4 list listchars=tab:<-> + norm! 1gg + redraw! + let expect = [ + \ "<-->1.<>Congress ", + \ " shall make ", + \ " no law ", + \ "<-->2.) Congress ", + \ " shall make ", + \ " no law ", + \ "<-->3.] Congress ", + \ " shall make ", + \ " no law ", + \ ] + let lines = s:screen_lines2(1, 9, 20) + call s:compare_lines(expect, lines) + + setl tabstop=2 nolist + redraw! + let expect = [ + \ " 1. Congress ", + \ " shall make no ", + \ " law ", + \ " 2.) Congress ", + \ " shall make no ", + \ " law ", + \ " 3.] Congress ", + \ " shall make no ", + \ " law ", + \ ] + let lines = s:screen_lines2(1, 9, 20) + call s:compare_lines(expect, lines) + + setl tabstop& list listchars=space:_ + redraw! + let expect = [ + \ "^I1.^ICongress_ ", + \ " shall_make_no_", + \ " law ", + \ "^I2.)_Congress_ ", + \ " shall_make_no_", + \ " law ", + \ "^I3.]_Congress_ ", + \ " shall_make_no_", + \ " law ", + \ ] + let lines = s:screen_lines2(1, 9, 20) + call s:compare_lines(expect, lines) + " check formatlistpat indent with different list levels - let &l:flp = '^\s*\*\+\s\+' + let &l:flp = '^\s*\(\*\|•\)\+\s\+' + setl list&vim listchars&vim %delete _ call setline(1, ['* Congress shall make no law', - \ '*** Congress shall make no law', + \ '••• Congress shall make no law', \ '**** Congress shall make no law']) norm! 1gg redraw! let expect = [ \ "* Congress shall ", \ " make no law ", - \ "*** Congress shall ", + \ "••• Congress shall ", \ " make no law ", \ "**** Congress shall ", \ " make no law ", @@ -864,7 +919,7 @@ func Test_breakindent20_list() let expect = [ \ "* Congress shall ", \ "> make no law ", - \ "*** Congress shall ", + \ "••• Congress shall ", \ "> make no law ", \ "**** Congress shall ", \ "> make no law ", @@ -880,7 +935,7 @@ func Test_breakindent20_list() let expect = [ \ "* Congress shall ", \ "> make no law ", - \ "*** Congress shall ", + \ "••• Congress shall ", \ "> make no law ", \ "**** Congress shall ", \ "> make no law ", diff --git a/test/old/testdir/test_utf8.vim b/test/old/testdir/test_utf8.vim index 3248dc9d98..5cac4066ea 100644 --- a/test/old/testdir/test_utf8.vim +++ b/test/old/testdir/test_utf8.vim @@ -228,6 +228,9 @@ func Test_setcellwidths() call setcellwidths([[0x2103, 0x2103, 2]]) redraw call assert_equal(19, wincol()) + call setcellwidths([]) + redraw + call assert_equal((aw == 'single') ? 10 : 19, wincol()) endfor set ambiwidth& isprint& @@ -252,15 +255,21 @@ func Test_setcellwidths() call assert_fails('call setcellwidths([[0x33, 0x44, 2]])', 'E1114:') - set listchars=tab:--\\u2192 + set listchars=tab:--\\u2192 fillchars=stl:\\u2501 call assert_fails('call setcellwidths([[0x2192, 0x2192, 2]])', 'E834:') - - set fillchars=stl:\\u2501 call assert_fails('call setcellwidths([[0x2501, 0x2501, 2]])', 'E835:') + call setcellwidths([[0x201c, 0x201d, 1]]) + set listchars& fillchars& ambiwidth=double + + set listchars=nbsp:\\u201c fillchars=vert:\\u201d + call assert_fails('call setcellwidths([])', 'E834:') set listchars& + call assert_fails('call setcellwidths([])', 'E835:') set fillchars& + call setcellwidths([]) + set ambiwidth& bwipe! endfunc diff --git a/test/testutil.lua b/test/testutil.lua index 01eaf25406..02f343891d 100644 --- a/test/testutil.lua +++ b/test/testutil.lua @@ -16,7 +16,7 @@ local function shell_quote(str) return str end ---- This module uses functions from the context of the test runner. +--- Functions executing in the context of the test runner (not the current nvim test session). --- @class test.testutil local M = { paths = Paths, @@ -42,6 +42,29 @@ function M.isdir(path) return stat.type == 'directory' end +--- (Only on Windows) Replaces yucky "\\" slashes with delicious "/" slashes in a string, or all +--- string values in a table (recursively). +--- +--- @param obj string|table +--- @return any +function M.fix_slashes(obj) + if not M.is_os('win') then + return obj + end + if type(obj) == 'string' then + local ret = obj:gsub('\\', '/') + return ret + elseif type(obj) == 'table' then + --- @cast obj table<any,any> + local ret = {} --- @type table<any,any> + for k, v in pairs(obj) do + ret[k] = M.fix_slashes(v) + end + return ret + end + assert(false, 'expected string or table of strings, got ' .. type(obj)) +end + --- @param ... string|string[] --- @return string function M.argss_to_cmd(...) @@ -143,7 +166,7 @@ end --- ---@param pat (string) Lua pattern to match lines in the log file ---@param logfile? (string) Full path to log file (default=$NVIM_LOG_FILE) ----@param nrlines? (number) Search up to this many log lines +---@param nrlines? (number) Search up to this many log lines (default 10) ---@param inverse? (boolean) Assert that the pattern does NOT match. function M.assert_log(pat, logfile, nrlines, inverse) logfile = logfile or os.getenv('NVIM_LOG_FILE') or '.nvimlog' diff --git a/test/unit/mbyte_spec.lua b/test/unit/mbyte_spec.lua index 787a8862ae..62390c8794 100644 --- a/test/unit/mbyte_spec.lua +++ b/test/unit/mbyte_spec.lua @@ -4,7 +4,6 @@ local itp = t.gen_itp(it) local ffi = t.ffi local eq = t.eq local to_cstr = t.to_cstr -local ok = t.ok local lib = t.cimport( './src/nvim/mbyte.h', @@ -302,7 +301,10 @@ describe('mbyte', function() local mb_glyphs = {} while pos < len do local clen = lib.utfc_ptr2len(cstr + pos) - ok(clen > 0) -- otherwise we get stuck + if clen == 0 then + eq(0, string.byte(str, pos + 1)) -- only NUL bytes can has length zery + clen = 1 -- but skip it, otherwise we get stuck + end if clen > 1 then table.insert(mb_glyphs, string.sub(str, pos + 1, pos + clen)) end @@ -325,13 +327,18 @@ describe('mbyte', function() -- stylua doesn't like ZWJ chars.. -- stylua: ignore start check('hej och hå 🧑🌾!', { 'å', '🧑🌾' }) - -- emoji only (various kinds of combinations, use g8 to see them) + + -- emoji (various kinds of combinations, use g8 to see them) check("🏳️⚧️🧑🌾❤️😂🏴☠️", {"🏳️⚧️", "🧑🌾", "❤️", "😂", "🏴☠️"}) check('🏳️⚧️xy🧑🌾\r❤️😂å🏴☠️', { '🏳️⚧️', '🧑🌾', '❤️', '😂', 'å', '🏴☠️', '' }) + check('🏳️⚧️\000🧑🌾\000❤️\000😂\000å\000🏴☠️\000', { '🏳️⚧️', '🧑🌾', '❤️', '😂', 'å', '🏴☠️', '' }) + check('\195🏳️⚧️\198🧑🌾\165❤️\168\195😂\255🏴☠️\129\165', { '🏳️⚧️', '🧑🌾', '❤️', '😂', '🏴☠️', '' }) check('🇦🅱️ 🇦🇽 🇦🇨🇦 🇲🇽🇹🇱',{'🇦', '🅱️', '🇦🇽', '🇦🇨', '🇦', '🇲🇽', '🇹🇱'}) check('🏴🏴', {'🏴', '🏴'}) + check('å\165ü\195aëq\168β\000\169本\255', {'å', 'ü', 'ë', 'β', '本'}) + lib.p_arshape = true -- default check('سلام', { 'س', 'لا', 'م' }) lib.p_arshape = false |