diff options
158 files changed, 9495 insertions, 7807 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index a19c04c26f..b9ceb05aba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,11 +149,6 @@ set(NVIM_API_LEVEL 13) # Bump this after any API/stdlib change. set(NVIM_API_LEVEL_COMPAT 0) # Adjust this after a _breaking_ API change. set(NVIM_API_PRERELEASE true) -# Build-type: RelWithDebInfo -# /Og means something different in MSVC -if(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_C_COMPILER_ID MATCHES "Clang") - set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} -Og -g") -endif() # We _want_ assertions in RelWithDebInfo build-type. if(CMAKE_C_FLAGS_RELWITHDEBINFO MATCHES DNDEBUG) string(REPLACE "-DNDEBUG" "" CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}") diff --git a/CMakePresets.json b/CMakePresets.json index 1d214d7801..b863d88608 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -10,7 +10,7 @@ { "name": "default", "displayName": "RelWithDebInfo", - "description": "Enables optimizations (-Og or -O2) with debug information", + "description": "Enables optimizations (-O2) with debug information", "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo" }, diff --git a/MAINTAIN.md b/MAINTAIN.md index cd3dacb964..eb58664ed2 100644 --- a/MAINTAIN.md +++ b/MAINTAIN.md @@ -143,11 +143,11 @@ These dependencies are "vendored" (inlined), we must update the sources manually * `src/mpack/`: [libmpack](https://github.com/libmpack/libmpack) * send improvements upstream! +* `src/mpack/lmpack.c`: [libmpack-lua](https://github.com/libmpack/libmpack-lua) + * send improvements upstream! * `src/xdiff/`: [xdiff](https://github.com/git/git/tree/master/xdiff) * `src/cjson/`: [lua-cjson](https://github.com/openresty/lua-cjson) * `src/klib/`: [Klib](https://github.com/attractivechaos/klib) -* `src/vterm/`: [libvterm](https://www.leonerd.org.uk/code/libvterm/), - [mirror](https://github.com/neovim/libvterm) * `runtime/lua/vim/inspect.lua`: [inspect.lua](https://github.com/kikito/inspect.lua) * `src/nvim/tui/terminfo_defs.h`: terminfo definitions * Run `scripts/update_terminfo.sh` to update these definitions. diff --git a/contrib/local.mk.example b/contrib/local.mk.example index 718bf9f2a3..f3024a147c 100644 --- a/contrib/local.mk.example +++ b/contrib/local.mk.example @@ -14,7 +14,7 @@ # # - Debug: Disables optimizations (-O0), enables debug information. # -# - RelWithDebInfo: Enables optimizations (-Og or -O2) with debug information. +# - RelWithDebInfo: Enables optimizations (-O2) with debug information. # # - MinSizeRel: Enables all -O2 optimization that do not typically # increase code size, and performs further optimizations diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index f321c880a4..f466dde861 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -4182,6 +4182,21 @@ getscriptinfo([{opts}]) *getscriptinfo()* Return: ~ (`vim.fn.getscriptinfo.ret[]`) +getstacktrace() *getstacktrace()* + Returns the current stack trace of Vim scripts. + Stack trace is a |List|, of which each item is a |Dictionary| + with the following items: + funcref The funcref if the stack is at a function, + otherwise this item is omitted. + event The string of the event description if the + stack is at an autocmd event, otherwise this + item is omitted. + lnum The line number in the script on the stack. + filepath The file path of the script on the stack. + + Return: ~ + (`table[]`) + gettabinfo([{tabnr}]) *gettabinfo()* If {tabnr} is not specified, then information about all the tab pages is returned as a |List|. Each List item is a diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index e0c45503cc..60238bc90d 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2848,7 +2848,8 @@ in the variable |v:exception|: > : echo "Number thrown. Value is" v:exception You may also be interested where an exception was thrown. This is stored in -|v:throwpoint|. Note that "v:exception" and "v:throwpoint" are valid for the +|v:throwpoint|. And you can obtain the stack trace from |v:stacktrace|. +Note that "v:exception", "v:stacktrace" and "v:throwpoint" are valid for the exception most recently caught as long it is not finished. Example: > diff --git a/runtime/doc/gui.txt b/runtime/doc/gui.txt index eb787af3c9..6fc5b27580 100644 --- a/runtime/doc/gui.txt +++ b/runtime/doc/gui.txt @@ -68,6 +68,192 @@ Example: this sets "g:gui" to the value of the UI's "rgb" field: > Obsolete, use ":set lines=11 columns=22". ============================================================================== +Using the mouse *mouse-using* + + *mouse-mode-table* *mouse-overview* +Overview of what the mouse buttons do, when 'mousemodel' is "extend": + + *<S-LeftMouse>* *<A-RightMouse>* *<S-RightMouse>* *<RightDrag>* + *<RightRelease>* *<LeftDrag>* +Normal Mode: > + event position selection change action + cursor window + --------------------------------------------------------------------------- + <LeftMouse> yes end yes + <C-LeftMouse> yes end yes "CTRL-]" (2) + <S-LeftMouse> yes no change yes "*" (2) + <LeftDrag> yes start or extend (1) no + <LeftRelease> yes start or extend (1) no + <MiddleMouse> yes if not active no put + <MiddleMouse> yes if active no yank and put + <RightMouse> yes start or extend yes + <A-RightMouse> yes start or extend blockw. yes + <S-RightMouse> yes no change yes "#" (2) + <C-RightMouse> no no change no "CTRL-T" + <RightDrag> yes extend no + <RightRelease> yes extend no + +Insert or Replace Mode: > + event position selection change action + cursor window + --------------------------------------------------------------------------- + <LeftMouse> yes (cannot be active) yes + <C-LeftMouse> yes (cannot be active) yes "CTRL-O^]" (2) + <S-LeftMouse> yes (cannot be active) yes "CTRL-O*" (2) + <LeftDrag> yes start or extend (1) no like CTRL-O (1) + <LeftRelease> yes start or extend (1) no like CTRL-O (1) + <MiddleMouse> no (cannot be active) no put register + <RightMouse> yes start or extend yes like CTRL-O + <A-RightMouse> yes start or extend blockw. yes + <S-RightMouse> yes (cannot be active) yes "CTRL-O#" (2) + <C-RightMouse> no (cannot be active) no "CTRL-O CTRL-T" + +In a help window: > + event position selection change action + cursor window + --------------------------------------------------------------------------- + <2-LeftMouse> yes (cannot be active) no "^]" (jump to help tag) + +When 'mousemodel' is "popup", these are different: + + *<A-LeftMouse>* +Normal Mode: > + event position selection change action + cursor window + --------------------------------------------------------------------------- + <S-LeftMouse> yes start or extend (1) no + <A-LeftMouse> yes start/extend blockw no + <RightMouse> no popup menu no + +Insert or Replace Mode: > + event position selection change action + cursor window + --------------------------------------------------------------------------- + <S-LeftMouse> yes start or extend (1) no like CTRL-O (1) + <A-LeftMouse> yes start/extend blockw no + <RightMouse> no popup menu no + +(1) only if mouse pointer moved since press +(2) only if click is in same buffer + +Clicking the left mouse button causes the cursor to be positioned. If the +click is in another window that window is made the active window. When +editing the command-line the cursor can only be positioned on the +command-line. When in Insert mode Vim remains in Insert mode. If 'scrolloff' +is set, and the cursor is positioned within 'scrolloff' lines from the window +border, the text is scrolled. + +A selection can be started by pressing the left mouse button on the first +character, moving the mouse to the last character, then releasing the mouse +button. You will not always see the selection until you release the button, +only in some versions (GUI, Win32) will the dragging be shown immediately. +Note that you can make the text scroll by moving the mouse at least one +character in the first/last line in the window when 'scrolloff' is non-zero. + +In Normal, Visual and Select mode clicking the right mouse button causes the +Visual area to be extended. When 'mousemodel' is "popup", the left button has +to be used while keeping the shift key pressed. When clicking in a window +which is editing another buffer, the Visual or Select mode is stopped. + +In Normal, Visual and Select mode clicking the right mouse button with the alt +key pressed causes the Visual area to become blockwise. When 'mousemodel' is +"popup" the left button has to be used with the alt key. Note that this won't +work on systems where the window manager consumes the mouse events when the +alt key is pressed (it may move the window). + + *double-click* *<2-LeftMouse>* *<3-LeftMouse>* *<4-LeftMouse>* +Double, triple and quadruple clicks are supported. For selecting text, extra +clicks extend the selection: > + + click select + --------------------------------- + double word or % match + triple line + quadruple rectangular block + +Exception: In a :help window, double-click jumps to help for the word that is +clicked on. + +Double-click on a word selects that word. 'iskeyword' is used to specify +which characters are included in a word. Double-click on a character that has +a match selects until that match (like using "v%"). If the match is an +#if/#else/#endif block, the selection becomes linewise. The time for +double-clicking can be set with the 'mousetime' option. + +Example: configure double-click to jump to the tag under the cursor: >vim + :map <2-LeftMouse> :exe "tag " .. expand("<cword>")<CR> + +Dragging the mouse with a double-click (button-down, button-up, button-down +and then drag) will result in whole words to be selected. This continues +until the button is released, at which point the selection is per character +again. + +For scrolling with the mouse see |scroll-mouse-wheel|. + +In Insert mode, when a selection is started, Vim goes into Normal mode +temporarily. When Visual or Select mode ends, it returns to Insert mode. +This is like using CTRL-O in Insert mode. Select mode is used when the +'selectmode' option contains "mouse". + + *X1Mouse* *X1Drag* *X1Release* + *X2Mouse* *X2Drag* *X2Release* + *<MiddleRelease>* *<MiddleDrag>* +Mouse clicks can be mapped using these |keycodes|: > + code mouse button normal action + --------------------------------------------------------------------------- + <LeftMouse> left pressed set cursor position + <LeftDrag> left moved while pressed extend selection + <LeftRelease> left released set selection end + <MiddleMouse> middle pressed paste text at cursor position + <MiddleDrag> middle moved while pressed - + <MiddleRelease> middle released - + <RightMouse> right pressed extend selection + <RightDrag> right moved while pressed extend selection + <RightRelease> right released set selection end + <X1Mouse> X1 button pressed - + <X1Drag> X1 moved while pressed - + <X1Release> X1 button release - + <X2Mouse> X2 button pressed - + <X2Drag> X2 moved while pressed - + <X2Release> X2 button release - + +The X1 and X2 buttons refer to the extra buttons found on some mice (e.g. the +right thumb). + +Examples: >vim + :noremap <MiddleMouse> <LeftMouse><MiddleMouse> +Paste at the position of the middle mouse button click (otherwise the paste +would be done at the cursor position). >vim + + :noremap <LeftRelease> <LeftRelease>y +Immediately yank the selection, when using Visual mode. + +Note the use of ":noremap" instead of "map" to avoid a recursive mapping. +>vim + :map <X1Mouse> <C-O> + :map <X2Mouse> <C-I> +Map the X1 and X2 buttons to go forwards and backwards in the jump list, see +|CTRL-O| and |CTRL-I|. + + *mouse-swap-buttons* +To swap the meaning of the left and right mouse buttons: >vim + :noremap <LeftMouse> <RightMouse> + :noremap <LeftDrag> <RightDrag> + :noremap <LeftRelease> <RightRelease> + :noremap <RightMouse> <LeftMouse> + :noremap <RightDrag> <LeftDrag> + :noremap <RightRelease> <LeftRelease> + :noremap g<LeftMouse> <C-RightMouse> + :noremap g<RightMouse> <C-LeftMouse> + :noremap! <LeftMouse> <RightMouse> + :noremap! <LeftDrag> <RightDrag> + :noremap! <LeftRelease> <RightRelease> + :noremap! <RightMouse> <LeftMouse> + :noremap! <RightDrag> <LeftDrag> + :noremap! <RightRelease> <LeftRelease> +< + +============================================================================== Scrollbars *gui-scrollbars* There are vertical scrollbars and a horizontal scrollbar. You may diff --git a/runtime/doc/health.txt b/runtime/doc/health.txt index cb70961b55..bca145bd8e 100644 --- a/runtime/doc/health.txt +++ b/runtime/doc/health.txt @@ -21,6 +21,17 @@ To run all healthchecks, use: >vim < Plugin authors are encouraged to write new healthchecks. |health-dev| + *g:health* +g:health This global variable controls the behavior and appearance of the + `health` floating window. It should be a dictionary containing the + following optional keys: + - `style`: string? Determines the display style of the `health` window. + Set to `'float'` to enable a floating window. Other + styles are not currently supported. + + Example: >lua + vim.g.health = { style = 'float' } + Commands *health-commands* *:che* *:checkhealth* diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt index 0182123a12..0256707420 100644 --- a/runtime/doc/index.txt +++ b/runtime/doc/index.txt @@ -1190,7 +1190,7 @@ tag command action ~ |:breakdel| :breakd[el] delete a debugger breakpoint |:breaklist| :breakl[ist] list debugger breakpoints |:browse| :bro[wse] use file selection dialog -|:bufdo| :bufdo execute command in each listed buffer +|:bufdo| :bufd[o] execute command in each listed buffer |:buffers| :buffers list all files in the buffer list |:bunload| :bun[load] unload a specific buffer |:bwipeout| :bw[ipeout] really delete a buffer @@ -1206,7 +1206,7 @@ tag command action ~ |:cafter| :caf[ter] go to error after current cursor |:call| :cal[l] call a function |:catch| :cat[ch] part of a :try command -|:cbefore| :cbef[ore] go to error before current cursor +|:cbefore| :cbe[fore] go to error before current cursor |:cbelow| :cbel[ow] go to error below current line |:cbottom| :cbo[ttom] scroll to the bottom of the quickfix window |:cbuffer| :cb[uffer] parse error messages and jump to first error @@ -1262,6 +1262,7 @@ tag command action ~ |:delete| :d[elete] delete lines |:debug| :deb[ug] run a command in debugging mode |:debuggreedy| :debugg[reedy] read debug mode commands from normal input +|:defer| :defe[r] call function when current function is done |:delcommand| :delc[ommand] delete user-defined command |:delfunction| :delf[unction] delete a user function |:delmarks| :delm[arks] delete marks @@ -1271,7 +1272,7 @@ tag command action ~ |:diffpatch| :diffp[atch] apply a patch and show differences |:diffput| :diffpu[t] remove differences in other buffer |:diffsplit| :diffs[plit] show differences with another file -|:diffthis| :diffthis make current window a diff window +|:diffthis| :difft[his] make current window a diff window |:digraphs| :dig[raphs] show or enter digraphs |:display| :di[splay] display registers |:djump| :dj[ump] jump to #define @@ -1372,7 +1373,7 @@ tag command action ~ |:last| :la[st] go to the last file in the argument list |:language| :lan[guage] set the language (locale) |:later| :lat[er] go to newer change, redo -|:lbefore| :lbef[ore] go to location before current cursor +|:lbefore| :lbe[fore] go to location before current cursor |:lbelow| :lbel[ow] go to location below current line |:lbottom| :lbo[ttom] scroll to the bottom of the location window |:lbuffer| :lb[uffer] parse locations and jump to first location @@ -1410,7 +1411,7 @@ tag command action ~ |:lockmarks| :loc[kmarks] following command keeps marks where they are |:lockvar| :lockv[ar] lock variables |:lolder| :lol[der] go to older location list -|:lopen| :lope[n] open location window +|:lopen| :lop[en] open location window |:lprevious| :lp[revious] go to previous location |:lpfile| :lpf[ile] go to last location in previous file |:lrewind| :lr[ewind] go to the specified location, default first one @@ -1618,7 +1619,7 @@ tag command action ~ |:tNext| :tN[ext] jump to previous matching tag |:tabNext| :tabN[ext] go to previous tab page |:tabclose| :tabc[lose] close current tab page -|:tabdo| :tabdo execute command in each tab page +|:tabdo| :tabd[o] execute command in each tab page |:tabedit| :tabe[dit] edit a file in a new tab page |:tabfind| :tabf[ind] find file in 'path', edit it in a new tab page |:tabfirst| :tabfir[st] go to first tab page @@ -1687,7 +1688,7 @@ tag command action ~ |:vsplit| :vs[plit] split current window vertically |:vunmap| :vu[nmap] like ":unmap" but for Visual+Select mode |:vunmenu| :vunme[nu] remove menu for Visual+Select mode -|:windo| :windo execute command in each window +|:windo| :wind[o] execute command in each window |:write| :w[rite] write to a file |:wNext| :wN[ext] write to a file and go to previous file in argument list diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 71e8a12ca3..16e6abe294 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -2024,7 +2024,7 @@ Lua module: vim.lsp.util *lsp-util* • {zindex}? (`integer`) override `zindex`, defaults to 50 • {title}? (`string`) • {title_pos}? (`'left'|'center'|'right'`) - • {relative}? (`'mouse'|'cursor'`) (default: `'cursor'`) + • {relative}? (`'mouse'|'cursor'|'editor'`) (default: `'cursor'`) • {anchor_bias}? (`'auto'|'above'|'below'`, default: `'auto'`) - "auto": place window based on which side of the cursor has more lines diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 463389ed65..6547a76f56 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -618,20 +618,20 @@ Example: TCP echo-server *tcp-server* Multithreading *lua-loop-threading* Plugins can perform work in separate (os-level) threads using the threading -APIs in luv, for instance `vim.uv.new_thread`. Note that every thread -gets its own separate Lua interpreter state, with no access to Lua globals -in the main thread. Neither can the state of the editor (buffers, windows, -etc) be directly accessed from threads. +APIs in luv, for instance `vim.uv.new_thread`. Each thread has its own +separate Lua interpreter state, with no access to Lua globals on the main +thread. Neither can the editor state (buffers, windows, etc) be directly +accessed from threads. -A subset of the `vim.*` API is available in threads. This includes: +A subset of the `vim.*` stdlib is available in threads, including: - `vim.uv` with a separate event loop per thread. - `vim.mpack` and `vim.json` (useful for serializing messages between threads) - `require` in threads can use Lua packages from the global |package.path| - `print()` and `vim.inspect` - `vim.diff` -- most utility functions in `vim.*` for working with pure Lua values - like `vim.split`, `vim.tbl_*`, `vim.list_*`, and so on. +- Most utility functions in `vim.*` that work with pure Lua values, like + `vim.split`, `vim.tbl_*`, `vim.list_*`, etc. - `vim.is_thread()` returns true from a non-main thread. @@ -1473,10 +1473,8 @@ vim.go *vim.go* < vim.o *vim.o* - Get or set |options|. Like `:set`. Invalid key is an error. - - Note: this works on both buffer-scoped and window-scoped options using the - current buffer and window. + Get or set |options|. Works like `:set`, so buffer/window-scoped options + target the current buffer/window. Invalid key is an error. Example: >lua vim.o.cmdheight = 4 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index c2f1a6a2ee..931f5e117c 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -40,7 +40,6 @@ These changes may require adaptations in your config or plugins. API -• Improved API "meta" docstrings and :help documentation. • `vim.rpcnotify(0)` and `rpcnotify(0)` broadcast to ALL channels. Previously they would "multicast" only to subscribed channels (controlled by `nvim_subscribe()`). Plugins and clients that want "multicast" behavior must @@ -62,16 +61,11 @@ On Windows, only building with the UCRT runtime is supported. DEFAULTS -• |]d-default| and |[d-default| accept a count. -• |[D-default| and |]D-default| jump to the first and last diagnostic in the - current buffer, respectively. • 'number', 'relativenumber', 'signcolumn', and 'foldcolumn' are disabled in |terminal| buffers. See |terminal-config| for an example of changing these defaults. DIAGNOSTICS -• |vim.diagnostic.config()| accepts a "jump" table to specify defaults for - |vim.diagnostic.jump()|. • The "underline" diagnostics handler sorts diagnostics by severity when using the "severity_sort" option. @@ -109,11 +103,6 @@ HIGHLIGHTS LSP -• Improved rendering of LSP hover docs. |K-lsp-default| -• |vim.lsp.completion.enable()| gained the `convert` callback which enables - customizing the transformation of an LSP CompletionItem to |complete-items|. -• |vim.lsp.diagnostic.from()| can be used to convert a list of - |vim.Diagnostic| objects into their LSP diagnostic representation. • |vim.lsp.buf.references()|, |vim.lsp.buf.declaration()|, |vim.lsp.buf.definition()|, |vim.lsp.buf.type_definition()|, |vim.lsp.buf.implementation()| and |vim.lsp.buf.hover()| now support merging the results of multiple clients @@ -130,15 +119,11 @@ LSP • |vim.lsp.util.make_position_params()|, |vim.lsp.util.make_range_params()| and |vim.lsp.util.make_given_range_params()| now require the `position_encoding` parameter. -• `:checkhealth vim.lsp` displays the server version (if available). LUA • API functions now consistently return an empty dictionary as |vim.empty_dict()|. Earlier, a |lua-special-tbl| was sometimes used. -• Command-line completions for: `vim.g`, `vim.t`, `vim.w`, `vim.b`, `vim.v`, - `vim.o`, `vim.wo`, `vim.bo`, `vim.opt`, `vim.opt_local`, `vim.opt_global`, - and `vim.fn`. • |vim.json.encode()| no longer escapes forward slashes "/" by default OPTIONS @@ -171,9 +156,6 @@ TREESITTER if no languages are explicitly registered. • |vim.treesitter.language.add()| returns `true` if a parser was loaded successfully and `nil,errmsg` otherwise instead of throwing an error. -• New |TSNode:child_with_descendant()|, which is nearly identical to - |TSNode:child_containing_descendant()| except that it can return the - descendant itself. TUI @@ -194,6 +176,7 @@ The following new features were added. API +• Improved API "meta" docstrings and :help documentation. • |nvim__ns_set()| can set properties for a namespace DEFAULTS @@ -212,6 +195,9 @@ DEFAULTS on a URL. • Mouse |popup-menu| includes a "Go to definition" item when LSP is active in the buffer. + • |]d-default| and |[d-default| accept a count. + • |[D-default| and |]D-default| jump to the first and last diagnostic in the + current buffer, respectively. • Mappings inspired by Tim Pope's vim-unimpaired: • |[q|, |]q|, |[Q|, |]Q|, |[CTRL-Q|, |]CTRL-Q| navigate through the |quickfix| list • |[l|, |]l|, |[L|, |]L|, |[CTRL-L|, |]CTRL-L| navigate through the |location-list| @@ -226,6 +212,11 @@ DEFAULTS • `<S-Tab>` in Insert and Select mode maps to `vim.snippet.jump({ direction = -1 })` when a snippet is active and jumpable backwards. +DIAGNOSTICS + +• |vim.diagnostic.config()| accepts a "jump" table to specify defaults for + |vim.diagnostic.jump()|. + EDITOR • Improved |paste| handling for redo (dot-repeat) and macros (|recording|): @@ -245,6 +236,12 @@ EVENTS LSP +• Improved rendering of LSP hover docs. |K-lsp-default| +• |vim.lsp.completion.enable()| gained the `convert` callback which enables + customizing the transformation of an LSP CompletionItem to |complete-items|. +• |vim.lsp.diagnostic.from()| can be used to convert a list of + |vim.Diagnostic| objects into their LSP diagnostic representation. +• `:checkhealth vim.lsp` displays the server version (if available). • Completion side effects (including snippet expansion, execution of commands and application of additional text edits) is now built-in. • |vim.lsp.util.locations_to_items()| sets `end_col` and `end_lnum` fields. @@ -267,6 +264,9 @@ LSP LUA +• Command-line completions for: `vim.g`, `vim.t`, `vim.w`, `vim.b`, `vim.v`, + `vim.o`, `vim.wo`, `vim.bo`, `vim.opt`, `vim.opt_local`, `vim.opt_global`, + and `vim.fn`. • |vim.fs.rm()| can delete files and directories. • |vim.validate()| now has a new signature which uses less tables, is more performant and easier to read. @@ -287,6 +287,8 @@ PERFORMANCE highlighting. • LSP diagnostics and inlay hints are de-duplicated (new requests cancel inflight requests). This greatly improves performance with slow LSP servers. +• 10x speedup for |vim.treesitter.foldexpr()| (when no parser exists for the + buffer). PLUGINS @@ -295,6 +297,7 @@ PLUGINS STARTUP +• |-es| ("script mode") disables shada by default. • Nvim will fail if the |--listen| or |$NVIM_LISTEN_ADDRESS| address is invalid, instead of silently skipping an invalid address. @@ -325,6 +328,9 @@ TREESITTER • |treesitter-directive-trim!| can trim all whitespace (not just empty lines) from both sides of a node. • |vim.treesitter.get_captures_at_pos()| now returns the `id` of each capture +• New |TSNode:child_with_descendant()|, which is nearly identical to + |TSNode:child_containing_descendant()| except that it can return the + descendant itself. TUI @@ -353,6 +359,8 @@ UI • |vim.diagnostic.setqflist()| updates an existing quickfix list with the given title if found • |ui-messages| content chunks now also contain the highlight group ID. +• |:checkhealth| can be display in a floating window and controlled by + the |g:health| variable. ============================================================================== CHANGED FEATURES *news-changed* diff --git a/runtime/doc/starting.txt b/runtime/doc/starting.txt index 3b0fa2b371..0bfbea75fb 100644 --- a/runtime/doc/starting.txt +++ b/runtime/doc/starting.txt @@ -207,12 +207,12 @@ argument. :print :set With |:verbose| or 'verbose', other commands display on stderr: > - nvim -es +":verbose echo 'foo'" - nvim -V1 -es +foo - -< User |config| is skipped unless |-u| was given. - Swap file is skipped (like |-n|). - User |shada| is loaded (unless "-i NONE" is given). + nvim -es +"verbose echo 'foo'" + nvim -V1 -es +"echo 'foo'" +< + Skips user |config| unless |-u| was given. + Disables |shada| unless |-i| was given. + Disables swapfile (like |-n|). *-l* -l {script} [args] @@ -235,6 +235,11 @@ argument. nvim +q -l foo.lua < This loads Lua module "bar" before executing "foo.lua": > nvim +"lua require('bar')" -l foo.lua +< *lua-shebang* + You can set the "shebang" of the script so that Nvim executes + the script when called with "./" from a shell (remember to + "chmod u+x"): > + #!/usr/bin/env -S nvim -l < Skips user |config| unless |-u| was given. Disables plugins unless 'loadplugins' was set. @@ -243,7 +248,7 @@ argument. *-ll* -ll {script} [args] - Execute a Lua script, similarly to |-l|, but the editor is not + Executes a Lua script, similarly to |-l|, but the editor is not initialized. This gives a Lua environment similar to a worker thread. See |lua-loop-threading|. diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 956fb80e73..28fe943359 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -157,12 +157,12 @@ The following predicates are built in: (field_identifier) @method)) @_parent (#has-parent? @_parent template_method function_declarator)) < - *lua-treesitter-not-predicate* + *treesitter-predicate-not* Each predicate has a `not-` prefixed predicate that is just the negation of the predicate. - *lua-treesitter-all-predicate* - *lua-treesitter-any-predicate* + *treesitter-predicate-all* + *treesitter-predicate-any* Queries can use quantifiers to capture multiple nodes. When a capture contains multiple nodes, predicates match only if ALL nodes contained by the capture match the predicate. Some predicates (`eq?`, `match?`, `lua-match?`, @@ -1114,7 +1114,7 @@ stop({bufnr}) *vim.treesitter.stop()* ============================================================================== -Lua module: vim.treesitter.language *lua-treesitter-language* +Lua module: vim.treesitter.language *treesitter-language* add({lang}, {opts}) *vim.treesitter.language.add()* Load parser with name {lang} @@ -1463,7 +1463,7 @@ set({lang}, {query_name}, {text}) *vim.treesitter.query.set()* ============================================================================== -Lua module: vim.treesitter.languagetree *lua-treesitter-languagetree* +Lua module: vim.treesitter.languagetree *treesitter-languagetree* A *LanguageTree* contains a tree of parsers: the root treesitter parser for {lang} and any "injected" language parsers, which themselves may inject other diff --git a/runtime/doc/tui.txt b/runtime/doc/tui.txt index 9493f91b1e..96ac54abc5 100644 --- a/runtime/doc/tui.txt +++ b/runtime/doc/tui.txt @@ -280,191 +280,5 @@ colours of whitespace are immaterial, in practice they change the colours of cursors and selections that cross them. This may have a visible, but minor, effect on some UIs. -============================================================================== -Using the mouse *mouse-using* - - *mouse-mode-table* *mouse-overview* -Overview of what the mouse buttons do, when 'mousemodel' is "extend": - - *<S-LeftMouse>* *<A-RightMouse>* *<S-RightMouse>* *<RightDrag>* - *<RightRelease>* *<LeftDrag>* -Normal Mode: > - event position selection change action - cursor window - --------------------------------------------------------------------------- - <LeftMouse> yes end yes - <C-LeftMouse> yes end yes "CTRL-]" (2) - <S-LeftMouse> yes no change yes "*" (2) - <LeftDrag> yes start or extend (1) no - <LeftRelease> yes start or extend (1) no - <MiddleMouse> yes if not active no put - <MiddleMouse> yes if active no yank and put - <RightMouse> yes start or extend yes - <A-RightMouse> yes start or extend blockw. yes - <S-RightMouse> yes no change yes "#" (2) - <C-RightMouse> no no change no "CTRL-T" - <RightDrag> yes extend no - <RightRelease> yes extend no - -Insert or Replace Mode: > - event position selection change action - cursor window - --------------------------------------------------------------------------- - <LeftMouse> yes (cannot be active) yes - <C-LeftMouse> yes (cannot be active) yes "CTRL-O^]" (2) - <S-LeftMouse> yes (cannot be active) yes "CTRL-O*" (2) - <LeftDrag> yes start or extend (1) no like CTRL-O (1) - <LeftRelease> yes start or extend (1) no like CTRL-O (1) - <MiddleMouse> no (cannot be active) no put register - <RightMouse> yes start or extend yes like CTRL-O - <A-RightMouse> yes start or extend blockw. yes - <S-RightMouse> yes (cannot be active) yes "CTRL-O#" (2) - <C-RightMouse> no (cannot be active) no "CTRL-O CTRL-T" - -In a help window: > - event position selection change action - cursor window - --------------------------------------------------------------------------- - <2-LeftMouse> yes (cannot be active) no "^]" (jump to help tag) - -When 'mousemodel' is "popup", these are different: - - *<A-LeftMouse>* -Normal Mode: > - event position selection change action - cursor window - --------------------------------------------------------------------------- - <S-LeftMouse> yes start or extend (1) no - <A-LeftMouse> yes start/extend blockw no - <RightMouse> no popup menu no - -Insert or Replace Mode: > - event position selection change action - cursor window - --------------------------------------------------------------------------- - <S-LeftMouse> yes start or extend (1) no like CTRL-O (1) - <A-LeftMouse> yes start/extend blockw no - <RightMouse> no popup menu no - -(1) only if mouse pointer moved since press -(2) only if click is in same buffer - -Clicking the left mouse button causes the cursor to be positioned. If the -click is in another window that window is made the active window. When -editing the command-line the cursor can only be positioned on the -command-line. When in Insert mode Vim remains in Insert mode. If 'scrolloff' -is set, and the cursor is positioned within 'scrolloff' lines from the window -border, the text is scrolled. - -A selection can be started by pressing the left mouse button on the first -character, moving the mouse to the last character, then releasing the mouse -button. You will not always see the selection until you release the button, -only in some versions (GUI, Win32) will the dragging be shown immediately. -Note that you can make the text scroll by moving the mouse at least one -character in the first/last line in the window when 'scrolloff' is non-zero. - -In Normal, Visual and Select mode clicking the right mouse button causes the -Visual area to be extended. When 'mousemodel' is "popup", the left button has -to be used while keeping the shift key pressed. When clicking in a window -which is editing another buffer, the Visual or Select mode is stopped. - -In Normal, Visual and Select mode clicking the right mouse button with the alt -key pressed causes the Visual area to become blockwise. When 'mousemodel' is -"popup" the left button has to be used with the alt key. Note that this won't -work on systems where the window manager consumes the mouse events when the -alt key is pressed (it may move the window). - - *double-click* *<2-LeftMouse>* *<3-LeftMouse>* *<4-LeftMouse>* -Double, triple and quadruple clicks are supported when the GUI is active, for -Win32 and for an xterm. For selecting text, extra clicks extend the -selection: > - - click select - --------------------------------- - double word or % match - triple line - quadruple rectangular block - -Exception: In a Help window a double click jumps to help for the word that is -clicked on. - -A double click on a word selects that word. 'iskeyword' is used to specify -which characters are included in a word. A double click on a character -that has a match selects until that match (like using "v%"). If the match is -an #if/#else/#endif block, the selection becomes linewise. -For MS-Windows and xterm the time for double clicking can be set with the -'mousetime' option. For the other systems this time is defined outside of Vim. -An example, for using a double click to jump to the tag under the cursor: >vim - :map <2-LeftMouse> :exe "tag " .. expand("<cword>")<CR> - -Dragging the mouse with a double click (button-down, button-up, button-down -and then drag) will result in whole words to be selected. This continues -until the button is released, at which point the selection is per character -again. - -For scrolling with the mouse see |scroll-mouse-wheel|. - -In Insert mode, when a selection is started, Vim goes into Normal mode -temporarily. When Visual or Select mode ends, it returns to Insert mode. -This is like using CTRL-O in Insert mode. Select mode is used when the -'selectmode' option contains "mouse". - - *X1Mouse* *X1Drag* *X1Release* - *X2Mouse* *X2Drag* *X2Release* - *<MiddleRelease>* *<MiddleDrag>* -Mouse clicks can be mapped. The codes for mouse clicks are: > - code mouse button normal action - --------------------------------------------------------------------------- - <LeftMouse> left pressed set cursor position - <LeftDrag> left moved while pressed extend selection - <LeftRelease> left released set selection end - <MiddleMouse> middle pressed paste text at cursor position - <MiddleDrag> middle moved while pressed - - <MiddleRelease> middle released - - <RightMouse> right pressed extend selection - <RightDrag> right moved while pressed extend selection - <RightRelease> right released set selection end - <X1Mouse> X1 button pressed - - <X1Drag> X1 moved while pressed - - <X1Release> X1 button release - - <X2Mouse> X2 button pressed - - <X2Drag> X2 moved while pressed - - <X2Release> X2 button release - - -The X1 and X2 buttons refer to the extra buttons found on some mice. The -'Microsoft Explorer' mouse has these buttons available to the right thumb. -Currently X1 and X2 only work on Win32 and X11 environments. - -Examples: >vim - :noremap <MiddleMouse> <LeftMouse><MiddleMouse> -Paste at the position of the middle mouse button click (otherwise the paste -would be done at the cursor position). >vim - - :noremap <LeftRelease> <LeftRelease>y -Immediately yank the selection, when using Visual mode. - -Note the use of ":noremap" instead of "map" to avoid a recursive mapping. ->vim - :map <X1Mouse> <C-O> - :map <X2Mouse> <C-I> -Map the X1 and X2 buttons to go forwards and backwards in the jump list, see -|CTRL-O| and |CTRL-I|. - - *mouse-swap-buttons* -To swap the meaning of the left and right mouse buttons: >vim - :noremap <LeftMouse> <RightMouse> - :noremap <LeftDrag> <RightDrag> - :noremap <LeftRelease> <RightRelease> - :noremap <RightMouse> <LeftMouse> - :noremap <RightDrag> <LeftDrag> - :noremap <RightRelease> <LeftRelease> - :noremap g<LeftMouse> <C-RightMouse> - :noremap g<RightMouse> <C-LeftMouse> - :noremap! <LeftMouse> <RightMouse> - :noremap! <LeftDrag> <RightDrag> - :noremap! <LeftRelease> <RightRelease> - :noremap! <RightMouse> <LeftMouse> - :noremap! <RightDrag> <LeftDrag> - :noremap! <RightRelease> <LeftRelease> -< + vim:et:sw=2:tw=78:ts=8:ft=help:norl: diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index 3202a70b76..f958491ccf 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -1103,7 +1103,8 @@ Various: *various-functions* did_filetype() check if a FileType autocommand was used eventhandler() check if invoked by an event handler getpid() get process ID of Vim - getscriptinfo() get list of sourced vim scripts + getscriptinfo() get list of sourced Vim scripts + getstacktrace() get current stack trace of Vim scripts libcall() call a function in an external library libcallnr() idem, returning a number diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 9f28b373ee..a92ddf33e6 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -409,6 +409,8 @@ Startup: - |-es| and |-Es| have improved behavior: - Quits automatically, don't need "-c qa!". - Skips swap-file dialog. + - Optimized for non-interactive scripts: disables swapfile, shada. +- |-l| Executes Lua scripts non-interactively. - |-s| reads Normal commands from stdin if the script name is "-". - Reading text (instead of commands) from stdin |--|: - works by default: "-" file is optional diff --git a/runtime/doc/vvars.txt b/runtime/doc/vvars.txt index 32f3b96269..0ebb54e38a 100644 --- a/runtime/doc/vvars.txt +++ b/runtime/doc/vvars.txt @@ -6,7 +6,8 @@ Predefined variables *vvars* -Some variables can be set by the user, but the type cannot be changed. +Most variables are read-only, when a variable can be set by the user, it will +be mentioned at the variable description below. The type cannot be changed. Type |gO| to see the table of contents. @@ -195,7 +196,8 @@ v:event *v:exception* *exception-variable* v:exception The value of the exception most recently caught and not - finished. See also |v:throwpoint| and |throw-variables|. + finished. See also |v:stacktrace|, |v:throwpoint|, and + |throw-variables|. Example: >vim try throw "oops" @@ -586,6 +588,13 @@ v:shell_error endif < + *v:stacktrace* *stacktrace-variable* +v:stacktrace + The stack trace of the exception most recently caught and + not finished. Refer to |getstacktrace()| for the structure of + stack trace. See also |v:exception|, |v:throwpoint|, and + |throw-variables|. + *v:statusmsg* *statusmsg-variable* v:statusmsg Last given status message. @@ -679,7 +688,7 @@ v:this_session v:throwpoint The point where the exception most recently caught and not finished was thrown. Not set when commands are typed. See - also |v:exception| and |throw-variables|. + also |v:exception|, |v:stacktrace|, and |throw-variables|. Example: >vim try throw "oops" diff --git a/runtime/doc/windows.txt b/runtime/doc/windows.txt index d3482ff3ec..6dd90f7e49 100644 --- a/runtime/doc/windows.txt +++ b/runtime/doc/windows.txt @@ -941,8 +941,8 @@ set in the preview window to be able to recognize it. The 'winfixheight' option is set to have it keep the same height when opening/closing other windows. - *:pta* *:ptag* -:pta[g][!] [tagname] + *:pt* *:ptag* +:pt[ag][!] [tagname] Does ":tag[!] [tagname]" and shows the found tag in a "Preview" window without changing the current buffer or cursor position. If a "Preview" window already exists, it is re-used diff --git a/runtime/ftplugin/shaderslang.vim b/runtime/ftplugin/shaderslang.vim new file mode 100644 index 0000000000..f3d1ab8c1c --- /dev/null +++ b/runtime/ftplugin/shaderslang.vim @@ -0,0 +1,54 @@ +" Vim filetype plugin file +" Language: Slang +" Maintainer: Austin Shijo <epestr@proton.me> +" Last Change: 2025 Jan 05 + +" Only do this when not done yet for this buffer +if exists("b:did_ftplugin") + finish +endif + +" Don't load another plugin for this buffer +let b:did_ftplugin = 1 + +" Using line continuation here. +let s:cpo_save = &cpo +set cpo-=C + +let b:undo_ftplugin = "setl fo< com< cms< inc<" + +" Set 'formatoptions' to break comment lines but not other lines, +" and insert the comment leader when hitting <CR> or using "o". +setlocal fo-=t fo+=croql + +" Set comment string (Slang uses C-style comments) +setlocal commentstring=//\ %s + +" Set 'comments' to format dashed lists in comments +setlocal comments=sO:*\ -,mO:*\ \ ,exO:*/,s1:/*,mb:*,ex:*/,:///,:// + +" When the matchit plugin is loaded, this makes the % command skip parens and +" braces in comments properly, and adds support for shader-specific keywords +if exists("loaded_matchit") + " Add common shader control structures + let b:match_words = '{\|^\s*\<\(if\|for\|while\|switch\|struct\|class\)\>:}\|^\s*\<break\>,' .. + \ '^\s*#\s*if\(\|def\|ndef\)\>:^\s*#\s*elif\>:^\s*#\s*else\>:^\s*#\s*endif\>,' .. + \ '\[:\]' + let b:match_skip = 's:comment\|string\|character\|special' + let b:match_ignorecase = 0 + let b:undo_ftplugin ..= " | unlet! b:match_skip b:match_words b:match_ignorecase" +endif + +" Win32 and GTK can filter files in the browse dialog +if (has("gui_win32") || has("gui_gtk")) && !exists("b:browsefilter") + let b:browsefilter = "Slang Source Files (*.slang)\t*.slang\n" + if has("win32") + let b:browsefilter ..= "All Files (*.*)\t*\n" + else + let b:browsefilter ..= "All Files (*)\t*\n" + endif + let b:undo_ftplugin ..= " | unlet! b:browsefilter" +endif + +let &cpo = s:cpo_save +unlet s:cpo_save diff --git a/runtime/ftplugin/tiasm.vim b/runtime/ftplugin/tiasm.vim new file mode 100644 index 0000000000..13a3dc64f7 --- /dev/null +++ b/runtime/ftplugin/tiasm.vim @@ -0,0 +1,18 @@ +" Vim filetype plugin file +" Language: TI linear assembly language +" Maintainer: Wu, Zhenyu <wuzhenyu@ustc.edu> +" Last Change: 2025 Jan 08 + +if exists("b:did_ftplugin") | finish | endif +let b:did_ftplugin = 1 + +setlocal comments=:; +setlocal commentstring=;\ %s + +let b:undo_ftplugin = "setl commentstring< comments<" + +if exists("loaded_matchit") + let b:match_words = '^\s\+\.if\>:^\s\+\.elseif:^\s\+\.else\>:^\s\+\.endif\>,^\s\+\.group:^\s\+\.gmember:^\s\+\.endgroup,^\s\+\.loop:^\s\+\.break:^\s\+\.endloop,^\s\+\.macro:^\s\+\.mexit:^\s\+\.endm,^\s\+\.asmfunc:^\s\+\.endasmfunc,^\s\+\.c\?struct:^\s\+\.endstruct,^\s\+\.c\?union:^\s\+\.endunion,^\s\+\.c\?proc:^\s\+\.return:^\s\+\.endproc' + let b:match_ignorecase = 1 + let b:undo_ftplugin ..= " | unlet! b:match_ignorecase b:match_words" +endif diff --git a/runtime/ftplugin/vim.vim b/runtime/ftplugin/vim.vim index b5e8e693f6..6ba057fc03 100644 --- a/runtime/ftplugin/vim.vim +++ b/runtime/ftplugin/vim.vim @@ -1,9 +1,9 @@ " Vim filetype plugin " Language: Vim " Maintainer: Doug Kearns <dougkearns@gmail.com> -" Last Change: 2024 Apr 13 -" 2024 May 23 by Riley Bruins <ribru17@gmail.com> ('commentstring') +" Last Change: 2025 Jan 06 " Former Maintainer: Bram Moolenaar <Bram@vim.org> +" Contributors: Riley Bruins <ribru17@gmail.com> ('commentstring') " Only do this when not done yet for this buffer if exists("b:did_ftplugin") @@ -103,8 +103,8 @@ if exists("loaded_matchit") \ '\<try\>:\%(\%(^\||\)\s*\)\@<=\<cat\%[ch]\>:\%(\%(^\||\)\s*\)\@<=\<fina\%[lly]\>:\%(\%(^\||\)\s*\)\@<=\<endt\%[ry]\>,' .. \ '\<aug\%[roup]\s\+\%(END\>\)\@!\S:\<aug\%[roup]\s\+END\>,' .. \ '\<class\>:\<endclass\>,' .. - \ '\<inte\%[rface]\>:\<endinterface\>,' .. - \ '\<enu\%[m]\>:\<endenum\>,' + \ '\<interface\>:\<endinterface\>,' .. + \ '\<enum\>:\<endenum\>' " Ignore syntax region commands and settings, any 'en*' would clobber " if-endif. diff --git a/runtime/lua/coxpcall.lua b/runtime/lua/coxpcall.lua index 6b179f1ef0..75e7a43567 100644 --- a/runtime/lua/coxpcall.lua +++ b/runtime/lua/coxpcall.lua @@ -1,6 +1,10 @@ ------------------------------------------------------------------------------- +-- (Not needed for LuaJIT or Lua 5.2+) +-- -- Coroutine safe xpcall and pcall versions -- +-- https://keplerproject.github.io/coxpcall/ +-- -- Encapsulates the protected calls with a coroutine based loop, so errors can -- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines -- yielding inside the call to pcall or xpcall. diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index e11dddb2d3..c08ab0663b 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -227,10 +227,10 @@ error('Cannot require a meta file') --- @field do_source? boolean --- @class vim.api.keyset.set_decoration_provider ---- @field on_start? fun(_: "start", tick: integer) +--- @field on_start? fun(_: "start", tick: integer): boolean? --- @field on_buf? fun(_: "buf", bufnr: integer, tick: integer) ---- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer) ---- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer) +--- @field on_win? fun(_: "win", winid: integer, bufnr: integer, toprow: integer, botrow: integer): boolean? +--- @field on_line? fun(_: "line", winid: integer, bufnr: integer, row: integer): boolean? --- @field on_end? fun(_: "end", tick: integer) --- @field _on_hl_def? fun(_: "hl_def") --- @field _on_spell_nav? fun(_: "spell_nav") diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 6662fca84f..031b109b38 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -3770,6 +3770,20 @@ function vim.fn.getregtype(regname) end --- @return vim.fn.getscriptinfo.ret[] function vim.fn.getscriptinfo(opts) end +--- Returns the current stack trace of Vim scripts. +--- Stack trace is a |List|, of which each item is a |Dictionary| +--- with the following items: +--- funcref The funcref if the stack is at a function, +--- otherwise this item is omitted. +--- event The string of the event description if the +--- stack is at an autocmd event, otherwise this +--- item is omitted. +--- lnum The line number in the script on the stack. +--- filepath The file path of the script on the stack. +--- +--- @return table[] +function vim.fn.getstacktrace() end + --- If {tabnr} is not specified, then information about all the --- tab pages is returned as a |List|. Each List item is a --- |Dictionary|. Otherwise, {tabnr} specifies the tab page diff --git a/runtime/lua/vim/_meta/vvars.lua b/runtime/lua/vim/_meta/vvars.lua index 445da4e02f..c1b8695bbf 100644 --- a/runtime/lua/vim/_meta/vvars.lua +++ b/runtime/lua/vim/_meta/vvars.lua @@ -203,7 +203,8 @@ vim.v.errors = ... vim.v.event = ... --- The value of the exception most recently caught and not ---- finished. See also `v:throwpoint` and `throw-variables`. +--- finished. See also `v:stacktrace`, `v:throwpoint`, and +--- `throw-variables`. --- Example: --- --- ```vim @@ -616,6 +617,13 @@ vim.v.servername = ... --- @type integer vim.v.shell_error = ... +--- The stack trace of the exception most recently caught and +--- not finished. Refer to `getstacktrace()` for the structure of +--- stack trace. See also `v:exception`, `v:throwpoint`, and +--- `throw-variables`. +--- @type table[] +vim.v.stacktrace = ... + --- Last given status message. --- Modifiable (can be set). --- @type string @@ -718,7 +726,7 @@ vim.v.this_session = ... --- The point where the exception most recently caught and not --- finished was thrown. Not set when commands are typed. See ---- also `v:exception` and `throw-variables`. +--- also `v:exception`, `v:stacktrace`, and `throw-variables`. --- Example: --- --- ```vim diff --git a/runtime/lua/vim/_options.lua b/runtime/lua/vim/_options.lua index 77d7054626..dc37595578 100644 --- a/runtime/lua/vim/_options.lua +++ b/runtime/lua/vim/_options.lua @@ -229,10 +229,8 @@ end --- global value of a |global-local| option, see |:setglobal|. --- </pre> ---- Get or set |options|. Like `:set`. Invalid key is an error. ---- ---- Note: this works on both buffer-scoped and window-scoped options using the ---- current buffer and window. +--- Get or set |options|. Works like `:set`, so buffer/window-scoped options target the current +--- buffer/window. Invalid key is an error. --- --- Example: --- diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index b6d6906589..dee1bd88ca 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -1064,11 +1064,11 @@ local extension = { builder = 'ruby', rake = 'ruby', rs = 'rust', + sa = detect.sa, sage = 'sage', sls = 'salt', sas = 'sas', sass = 'sass', - sa = 'sather', sbt = 'sbt', scala = 'scala', ss = 'scheme', @@ -1097,6 +1097,7 @@ local extension = { la = 'sh', lai = 'sh', mdd = 'sh', + slang = 'shaderslang', sieve = 'sieve', siv = 'sieve', sig = detect.sig, diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index a1c17bc1af..2d989fdbac 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -34,6 +34,12 @@ local matchregex = vim.filetype._matchregex -- can be detected from the first five lines of the file. --- @type vim.filetype.mapfn function M.asm(path, bufnr) + -- tiasm uses `* commment` + local lines = table.concat(getlines(bufnr, 1, 10), '\n') + if findany(lines, { '^%*', '\n%*', 'Texas Instruments Incorporated' }) then + return 'tiasm' + end + local syntax = vim.b[bufnr].asmsyntax if not syntax or syntax == '' then syntax = M.asm_syntax(path, bufnr) @@ -1424,6 +1430,15 @@ function M.sig(_, bufnr) end end +--- @type vim.filetype.mapfn +function M.sa(_, bufnr) + local lines = table.concat(getlines(bufnr, 1, 4), '\n') + if findany(lines, { '^;', '\n;' }) then + return 'tiasm' + end + return 'sather' +end + -- This function checks the first 25 lines of file extension "sc" to resolve -- detection between scala and SuperCollider --- @type vim.filetype.mapfn diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index ee713e4b58..04a6e43db1 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -629,8 +629,8 @@ function M.normalize(path, opts) return prefix .. path end - -- Remove extraneous slashes from the prefix - prefix = prefix:gsub('/+', '/') + -- Ensure capital drive and remove extraneous slashes from the prefix + prefix = prefix:gsub('^%a:', string.upper):gsub('/+', '/') end if not opts._fast then diff --git a/runtime/lua/vim/func.lua b/runtime/lua/vim/func.lua index f71659ffb4..fc8fa62c71 100644 --- a/runtime/lua/vim/func.lua +++ b/runtime/lua/vim/func.lua @@ -3,9 +3,6 @@ local M = {} -- TODO(lewis6991): Private for now until: -- - There are other places in the codebase that could benefit from this -- (e.g. LSP), but might require other changes to accommodate. --- - Invalidation of the cache needs to be controllable. Using weak tables --- is an acceptable invalidation policy, but it shouldn't be the only --- one. -- - I don't think the story around `hash` is completely thought out. We -- may be able to have a good default hash by hashing each argument, -- so basically a better 'concat'. @@ -17,6 +14,10 @@ local M = {} --- Internally uses a |lua-weaktable| to cache the results of {fn} meaning the --- cache will be invalidated whenever Lua does garbage collection. --- +--- The cache can also be manually invalidated by calling `:clear()` on the returned object. +--- Calling this function with no arguments clears the entire cache; otherwise, the arguments will +--- be interpreted as function inputs, and only the cache entry at their hash will be cleared. +--- --- The memoized function returns shared references so be wary about --- mutating return values. --- @@ -32,11 +33,12 @@ local M = {} --- first n arguments passed to {fn}. --- --- @param fn F Function to memoize. ---- @param strong? boolean Do not use a weak table +--- @param weak? boolean Use a weak table (default `true`) --- @return F # Memoized version of {fn} --- @nodoc -function M._memoize(hash, fn, strong) - return require('vim.func._memoize')(hash, fn, strong) +function M._memoize(hash, fn, weak) + -- this is wrapped in a function to lazily require the module + return require('vim.func._memoize')(hash, fn, weak) end return M diff --git a/runtime/lua/vim/func/_memoize.lua b/runtime/lua/vim/func/_memoize.lua index 6e557905a7..c46f878067 100644 --- a/runtime/lua/vim/func/_memoize.lua +++ b/runtime/lua/vim/func/_memoize.lua @@ -1,5 +1,7 @@ --- Module for private utility functions +--- @alias vim.func.MemoObj { _hash: (fun(...): any), _weak: boolean?, _cache: table<any> } + --- @param argc integer? --- @return fun(...): any local function concat_hash(argc) @@ -33,29 +35,49 @@ local function resolve_hash(hash) return hash end +--- @param weak boolean? +--- @return table +local create_cache = function(weak) + return setmetatable({}, { + __mode = weak ~= false and 'kv', + }) +end + --- @generic F: function --- @param hash integer|string|fun(...): any --- @param fn F ---- @param strong? boolean +--- @param weak? boolean --- @return F -return function(hash, fn, strong) +return function(hash, fn, weak) vim.validate('hash', hash, { 'number', 'string', 'function' }) vim.validate('fn', fn, 'function') + vim.validate('weak', weak, 'boolean', true) - ---@type table<any,table<any,any>> - local cache = {} - if not strong then - setmetatable(cache, { __mode = 'kv' }) - end - - hash = resolve_hash(hash) + --- @type vim.func.MemoObj + local obj = { + _cache = create_cache(weak), + _hash = resolve_hash(hash), + _weak = weak, + --- @param self vim.func.MemoObj + clear = function(self, ...) + if select('#', ...) == 0 then + self._cache = create_cache(self._weak) + return + end + local key = self._hash(...) + self._cache[key] = nil + end, + } - return function(...) - local key = hash(...) - if cache[key] == nil then - cache[key] = vim.F.pack_len(fn(...)) - end - - return vim.F.unpack_len(cache[key]) - end + return setmetatable(obj, { + --- @param self vim.func.MemoObj + __call = function(self, ...) + local key = self._hash(...) + local cache = self._cache + if cache[key] == nil then + cache[key] = vim.F.pack_len(fn(...)) + end + return vim.F.unpack_len(cache[key]) + end, + }) end diff --git a/runtime/lua/vim/health.lua b/runtime/lua/vim/health.lua index 52a7a13966..6dc902489f 100644 --- a/runtime/lua/vim/health.lua +++ b/runtime/lua/vim/health.lua @@ -11,6 +11,17 @@ --- < --- Plugin authors are encouraged to write new healthchecks. |health-dev| --- +--- *g:health* +--- g:health This global variable controls the behavior and appearance of the +--- `health` floating window. It should be a dictionary containing the +--- following optional keys: +--- - `style`: string? Determines the display style of the `health` window. +--- Set to `'float'` to enable a floating window. Other +--- styles are not currently supported. +--- +--- Example: >lua +--- vim.g.health = { style = 'float' } +--- --- Commands *health-commands* --- --- *:che* *:checkhealth* @@ -331,13 +342,31 @@ function M._check(mods, plugin_names) local emptybuf = vim.fn.bufnr('$') == 1 and vim.fn.getline(1) == '' and 1 == vim.fn.line('$') - -- When no command modifiers are used: - -- - If the current buffer is empty, open healthcheck directly. - -- - If not specified otherwise open healthcheck in a tab. - local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer' - local bufnr = vim.api.nvim_create_buf(true, true) - vim.cmd(buf_cmd .. ' ' .. bufnr) + if + vim.g.health + and type(vim.g.health) == 'table' + and vim.tbl_get(vim.g.health, 'style') == 'float' + then + local max_height = math.floor(vim.o.lines * 0.8) + local max_width = 80 + local float_bufnr, float_winid = vim.lsp.util.open_floating_preview({}, '', { + height = max_height, + width = max_width, + offset_x = math.floor((vim.o.columns - max_width) / 2), + offset_y = math.floor((vim.o.lines - max_height) / 2) - 1, + relative = 'editor', + }) + vim.api.nvim_set_current_win(float_winid) + vim.bo[float_bufnr].modifiable = true + vim.wo[float_winid].list = false + else + -- When no command modifiers are used: + -- - If the current buffer is empty, open healthcheck directly. + -- - If not specified otherwise open healthcheck in a tab. + local buf_cmd = #mods > 0 and (mods .. ' sbuffer') or emptybuf and 'buffer' or 'tab sbuffer' + vim.cmd(buf_cmd .. ' ' .. bufnr) + end if vim.fn.bufexists('health://') == 1 then vim.cmd.bwipe('health://') @@ -407,6 +436,17 @@ function M._check(mods, plugin_names) -- Clear the 'Running healthchecks...' message. vim.cmd.redraw() vim.print('') + + -- Quit with 'q' inside healthcheck buffers. + vim.keymap.set('n', 'q', function() + local ok, _ = pcall(vim.cmd.close) + if not ok then + vim.cmd.bdelete() + end + end, { buffer = bufnr, silent = true, noremap = true, nowait = true }) + + -- Once we're done writing checks, set nomodifiable. + vim.bo[bufnr].modifiable = false end return M diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 6bee5bc31f..5cccb3321f 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -875,15 +875,17 @@ function M.make_floating_popup_options(width, height, opts) return { anchor = anchor, + row = row + (opts.offset_y or 0), col = col + (opts.offset_x or 0), height = height, focusable = opts.focusable, - relative = opts.relative == 'mouse' and 'mouse' or 'cursor', - row = row + (opts.offset_y or 0), + relative = opts.relative == 'mouse' and 'mouse' + or opts.relative == 'editor' and 'editor' + or 'cursor', style = 'minimal', width = width, border = opts.border or default_border, - zindex = opts.zindex or 50, + zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1, title = title, title_pos = title_pos, } @@ -1494,7 +1496,7 @@ end --- @field title_pos? 'left'|'center'|'right' --- --- (default: `'cursor'`) ---- @field relative? 'mouse'|'cursor' +--- @field relative? 'mouse'|'cursor'|'editor' --- --- - "auto": place window based on which side of the cursor has more lines --- - "above": place the window above the cursor unless there are not enough lines diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 207ac1ab67..d16013eca2 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -19,14 +19,19 @@ local api = vim.api ---The range on which to evaluate foldexpr. ---When in insert mode, the evaluation is deferred to InsertLeave. ---@field foldupdate_range? Range2 +--- +---The treesitter parser associated with this buffer. +---@field parser? vim.treesitter.LanguageTree local FoldInfo = {} FoldInfo.__index = FoldInfo ---@private -function FoldInfo.new() +---@param bufnr integer +function FoldInfo.new(bufnr) return setmetatable({ levels0 = {}, levels = {}, + parser = ts.get_parser(bufnr, nil, { error = false }), }, FoldInfo) end @@ -69,7 +74,10 @@ local function compute_folds_levels(bufnr, info, srow, erow, parse_injections) srow = srow or 0 erow = erow or api.nvim_buf_line_count(bufnr) - local parser = assert(ts.get_parser(bufnr, nil, { error = false })) + local parser = info.parser + if not parser then + return + end parser:parse(parse_injections and { srow, erow } or nil) @@ -347,13 +355,21 @@ function M.foldexpr(lnum) lnum = lnum or vim.v.lnum local bufnr = api.nvim_get_current_buf() - local parser = ts.get_parser(bufnr, nil, { error = false }) - if not parser then - return '0' - end - if not foldinfos[bufnr] then - foldinfos[bufnr] = FoldInfo.new() + foldinfos[bufnr] = FoldInfo.new(bufnr) + api.nvim_create_autocmd('BufUnload', { + buffer = bufnr, + once = true, + callback = function() + foldinfos[bufnr] = nil + end, + }) + + local parser = foldinfos[bufnr].parser + if not parser then + return '0' + end + compute_folds_levels(bufnr, foldinfos[bufnr]) parser:register_cbs({ @@ -383,7 +399,7 @@ api.nvim_create_autocmd('OptionSet', { or foldinfos[buf] and { buf } or {} for _, bufnr in ipairs(bufs) do - foldinfos[bufnr] = FoldInfo.new() + foldinfos[bufnr] = FoldInfo.new(bufnr) api.nvim_buf_call(bufnr, function() compute_folds_levels(bufnr, foldinfos[bufnr]) end) diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 8ce8652f7d..96503c38ea 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -299,6 +299,8 @@ local function on_line_impl(self, buf, line, is_spell_nav) state.highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) end + local captures = state.highlighter_query:query().captures + while line >= state.next_row do local capture, node, metadata, match = state.iter(line) @@ -311,7 +313,7 @@ local function on_line_impl(self, buf, line, is_spell_nav) if capture then local hl = state.highlighter_query:get_hl_from_capture(capture) - local capture_name = state.highlighter_query:query().captures[capture] + local capture_name = captures[capture] local spell, spell_pri_offset = get_spell(capture_name) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4b42164dc8..330eb45749 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -443,7 +443,7 @@ function LanguageTree:parse(range) end end - if not self._injections_processed and range ~= false and range ~= nil then + if not self._injections_processed and range then query_time = self:_add_injections() self._injections_processed = true end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index f9c497337f..1fc001b39f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -7,6 +7,59 @@ local memoize = vim.func._memoize local M = {} +local function is_directive(name) + return string.sub(name, -1) == '!' +end + +---@nodoc +---@class vim.treesitter.query.ProcessedPredicate +---@field [1] string predicate name +---@field [2] boolean should match +---@field [3] (integer|string)[] the original predicate + +---@alias vim.treesitter.query.ProcessedDirective (integer|string)[] + +---@nodoc +---@class vim.treesitter.query.ProcessedPattern { +---@field predicates vim.treesitter.query.ProcessedPredicate[] +---@field directives vim.treesitter.query.ProcessedDirective[] + +--- Splits the query patterns into predicates and directives. +---@param patterns table<integer, (integer|string)[][]> +---@return table<integer, vim.treesitter.query.ProcessedPattern> +local function process_patterns(patterns) + ---@type table<integer, vim.treesitter.query.ProcessedPattern> + local processed_patterns = {} + + for k, pattern_list in pairs(patterns) do + ---@type vim.treesitter.query.ProcessedPredicate[] + local predicates = {} + ---@type vim.treesitter.query.ProcessedDirective[] + local directives = {} + + for _, pattern in ipairs(pattern_list) do + -- Note: tree-sitter strips the leading # from predicates for us. + local pred_name = pattern[1] + ---@cast pred_name string + + if is_directive(pred_name) then + table.insert(directives, pattern) + else + local should_match = true + if pred_name:match('^not%-') then + pred_name = pred_name:sub(5) + should_match = false + end + table.insert(predicates, { pred_name, should_match, pattern }) + end + end + + processed_patterns[k] = { predicates = predicates, directives = directives } + end + + return processed_patterns +end + ---@nodoc ---Parsed query, see |vim.treesitter.query.parse()| --- @@ -15,6 +68,7 @@ local M = {} ---@field captures string[] list of (unique) capture names defined in query ---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives) ---@field query TSQuery userdata query object +---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern> local Query = {} Query.__index = Query @@ -33,6 +87,7 @@ function Query.new(lang, ts_query) patterns = query_info.patterns, } self.captures = self.info.captures + self._processed_patterns = process_patterns(self.info.patterns) return self end @@ -751,84 +806,50 @@ function M.list_predicates() return vim.tbl_keys(predicate_handlers) end -local function xor(x, y) - return (x or y) and not (x and y) -end - -local function is_directive(name) - return string.sub(name, -1) == '!' -end - ---@private ----@param match TSQueryMatch +---@param pattern_i integer +---@param predicates vim.treesitter.query.ProcessedPredicate[] +---@param captures table<integer, TSNode[]> ---@param source integer|string -function Query:match_preds(match, source) - local _, pattern = match:info() - local preds = self.info.patterns[pattern] - - if not preds then - return true - end - - local captures = match:captures() - - for _, pred in pairs(preds) do - -- Here we only want to return if a predicate DOES NOT match, and - -- continue on the other case. This way unknown predicates will not be considered, - -- which allows some testing and easier user extensibility (#12173). - -- Also, tree-sitter strips the leading # from predicates for us. - local is_not = false - - -- Skip over directives... they will get processed after all the predicates. - if not is_directive(pred[1]) then - local pred_name = pred[1] - if pred_name:match('^not%-') then - pred_name = pred_name:sub(5) - is_not = true - end - - local handler = predicate_handlers[pred_name] - - if not handler then - error(string.format('No handler for %s', pred[1])) - return false - end - - local pred_matches = handler(captures, pattern, source, pred) +---@return boolean whether the predicates match +function Query:_match_predicates(predicates, pattern_i, captures, source) + for _, predicate in ipairs(predicates) do + local processed_name = predicate[1] + local should_match = predicate[2] + local orig_predicate = predicate[3] + + local handler = predicate_handlers[processed_name] + if not handler then + error(string.format('No handler for %s', orig_predicate[1])) + return false + end - if not xor(is_not, pred_matches) then - return false - end + local does_match = handler(captures, pattern_i, source, orig_predicate) + if does_match ~= should_match then + return false end end return true end ---@private ----@param match TSQueryMatch +---@param pattern_i integer +---@param directives vim.treesitter.query.ProcessedDirective[] +---@param source integer|string +---@param captures table<integer, TSNode[]> ---@return vim.treesitter.query.TSMetadata metadata -function Query:apply_directives(match, source) +function Query:_apply_directives(directives, pattern_i, captures, source) ---@type vim.treesitter.query.TSMetadata local metadata = {} - local _, pattern = match:info() - local preds = self.info.patterns[pattern] - if not preds then - return metadata - end + for _, directive in pairs(directives) do + local handler = directive_handlers[directive[1]] - local captures = match:captures() - - for _, pred in pairs(preds) do - if is_directive(pred[1]) then - local handler = directive_handlers[pred[1]] - - if not handler then - error(string.format('No handler for %s', pred[1])) - end - - handler(captures, pattern, source, pred, metadata) + if not handler then + error(string.format('No handler for %s', directive[1])) end + + handler(captures, pattern_i, source, directive, metadata) end return metadata @@ -852,12 +873,6 @@ local function value_or_node_range(start, stop, node) return start, stop end ---- @param match TSQueryMatch ---- @return integer -local function match_id_hash(_, match) - return (match:info()) -end - --- Iterates over all captures from all matches in {node}. --- --- {source} is required if the query contains predicates; then the caller @@ -902,8 +917,10 @@ function Query:iter_captures(node, source, start, stop) local cursor = vim._create_ts_querycursor(node, self.query, start, stop, { match_limit = 256 }) - local apply_directives = memoize(match_id_hash, self.apply_directives, true) - local match_preds = memoize(match_id_hash, self.match_preds, true) + -- For faster checks that a match is not in the cache. + local highest_cached_match_id = -1 + ---@type table<integer, vim.treesitter.query.TSMetadata> + local match_cache = {} local function iter(end_line) local capture, captured_node, match = cursor:next_capture() @@ -912,16 +929,37 @@ function Query:iter_captures(node, source, start, stop) return end - if not match_preds(self, match, source) then - local match_id = match:info() - cursor:remove_match(match_id) - if end_line and captured_node:range() > end_line then - return nil, captured_node, nil, nil - end - return iter(end_line) -- tail call: try next match + local match_id, pattern_i = match:info() + + --- @type vim.treesitter.query.TSMetadata + local metadata + if match_id <= highest_cached_match_id then + metadata = match_cache[match_id] end - local metadata = apply_directives(self, match, source) + if not metadata then + metadata = {} + + local processed_pattern = self._processed_patterns[pattern_i] + if processed_pattern then + local captures = match:captures() + + local predicates = processed_pattern.predicates + if not self:_match_predicates(predicates, pattern_i, captures, source) then + cursor:remove_match(match_id) + if end_line and captured_node:range() > end_line then + return nil, captured_node, nil, nil + end + return iter(end_line) -- tail call: try next match + end + + local directives = processed_pattern.directives + metadata = self:_apply_directives(directives, pattern_i, captures, source) + end + + highest_cached_match_id = math.max(highest_cached_match_id, match_id) + match_cache[match_id] = metadata + end return capture, captured_node, metadata, match end @@ -984,17 +1022,22 @@ function Query:iter_matches(node, source, start, stop, opts) return end - local match_id, pattern = match:info() + local match_id, pattern_i = match:info() + local processed_pattern = self._processed_patterns[pattern_i] + local captures = match:captures() - if not self:match_preds(match, source) then - cursor:remove_match(match_id) - return iter() -- tail call: try next match + --- @type vim.treesitter.query.TSMetadata + local metadata = {} + if processed_pattern then + local predicates = processed_pattern.predicates + if not self:_match_predicates(predicates, pattern_i, captures, source) then + cursor:remove_match(match_id) + return iter() -- tail call: try next match + end + local directives = processed_pattern.directives + metadata = self:_apply_directives(directives, pattern_i, captures, source) end - local metadata = self:apply_directives(match, source) - - local captures = match:captures() - if opts.all == false then -- Convert the match table into the old buggy version for backward -- compatibility. This is slow, but we only do it when the caller explicitly opted into it by @@ -1003,11 +1046,11 @@ function Query:iter_matches(node, source, start, stop, opts) for k, v in pairs(captures or {}) do old_match[k] = v[#v] end - return pattern, old_match, metadata + return pattern_i, old_match, metadata end -- TODO(lewis6991): create a new function that returns {match, metadata} - return pattern, captures, metadata + return pattern_i, captures, metadata end return iter end diff --git a/runtime/pack/dist/opt/matchit/autoload/matchit.vim b/runtime/pack/dist/opt/matchit/autoload/matchit.vim index aa977488e5..8a220eeb9b 100644 --- a/runtime/pack/dist/opt/matchit/autoload/matchit.vim +++ b/runtime/pack/dist/opt/matchit/autoload/matchit.vim @@ -222,6 +222,7 @@ function matchit#Match_wrapper(word, forward, mode) range let view = winsaveview() call cursor(0, curcol + 1) if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on")) + \ || skip =~ 'v:lua.vim.treesitter' && !exists('b:ts_highlight') let skip = "0" else execute "if " .. skip .. "| let skip = '0' | endif" @@ -678,6 +679,7 @@ fun! matchit#MultiMatch(spflag, mode) let middlepat = substitute(middlepat, ',', '\\|', 'g') if skip =~ 'synID' && !(has("syntax") && exists("g:syntax_on")) + \ || skip =~ 'v:lua.vim.treesitter' && !exists('b:ts_highlight') let skip = '0' else try @@ -760,10 +762,16 @@ endfun " S:foo becomes (current syntax item) !~ foo " r:foo becomes (line before cursor) =~ foo " R:foo becomes (line before cursor) !~ foo +" t:foo becomes (current treesitter captures) =~ foo +" T:foo becomes (current treesitter captures) !~ foo fun! s:ParseSkip(str) let skip = a:str if skip[1] == ":" - if skip[0] ==# "s" + if skip[0] ==# "t" || skip[0] ==# "s" && &syntax != 'on' && exists("b:ts_highlight") + let skip = "match(v:lua.vim.treesitter.get_captures_at_cursor(), '" .. strpart(skip,2) .. "') != -1" + elseif skip[0] ==# "T" || skip[0] ==# "S" && &syntax != 'on' && exists("b:ts_highlight") + let skip = "match(v:lua.vim.treesitter.get_captures_at_cursor(), '" .. strpart(skip,2) .. "') == -1" + elseif skip[0] ==# "s" let skip = "synIDattr(synID(line('.'),col('.'),1),'name') =~? '" .. \ strpart(skip,2) .. "'" elseif skip[0] ==# "S" diff --git a/runtime/pack/dist/opt/matchit/doc/matchit.txt b/runtime/pack/dist/opt/matchit/doc/matchit.txt index 8d56df6ddc..58031d8a8a 100644 --- a/runtime/pack/dist/opt/matchit/doc/matchit.txt +++ b/runtime/pack/dist/opt/matchit/doc/matchit.txt @@ -237,6 +237,8 @@ supported by matchit.vim: S:foo becomes (current syntax item) !~ foo r:foo becomes (line before cursor) =~ foo R:foo becomes (line before cursor) !~ foo + t:foo becomes (current treesitter captures) =~ foo + T:foo becomes (current treesitter captures) !~ foo (The "s" is meant to suggest "syntax", and the "r" is meant to suggest "regular expression".) diff --git a/runtime/plugin/matchparen.vim b/runtime/plugin/matchparen.vim index 661a34b578..13d1b6824f 100644 --- a/runtime/plugin/matchparen.vim +++ b/runtime/plugin/matchparen.vim @@ -109,6 +109,10 @@ func s:Highlight_Matching_Pair() if !has("syntax") || !exists("g:syntax_on") let s_skip = "0" + elseif exists("b:ts_highlight") && &syntax != 'on' + let s_skip = "match(v:lua.vim.treesitter.get_captures_at_cursor(), '" + \ .. 'string\|character\|singlequote\|escape\|symbol\|comment' + \ .. "') != -1" else " Build an expression that detects whether the current cursor position is " in certain syntax types (string, comment, etc.), for use as diff --git a/runtime/syntax/java.vim b/runtime/syntax/java.vim index b3e17b55f6..9b38ccd4dc 100644 --- a/runtime/syntax/java.vim +++ b/runtime/syntax/java.vim @@ -3,7 +3,7 @@ " Maintainer: Aliaksei Budavei <0x000c70 AT gmail DOT com> " Former Maintainer: Claudio Fleiner <claudio@fleiner.com> " Repository: https://github.com/zzzyxwvut/java-vim.git -" Last Change: 2024 Oct 10 +" Last Change: 2025 Jan 02 " Please check ":help java.vim" for comments on some of the options " available. @@ -391,18 +391,32 @@ if !exists("g:java_ignore_javadoc") && (s:with_html || s:with_markdown) && g:mai if s:with_markdown try syntax include @javaMarkdown syntax/markdown.vim - let s:ff.WithMarkdown = s:ff.LeftConstant + + try + syn clear markdownId markdownLineStart markdownH1 markdownH2 markdownHeadingRule markdownRule markdownCode markdownCodeBlock markdownIdDeclaration + let s:ff.WithMarkdown = s:ff.LeftConstant + catch /\<E28:/ + call s:ReportOnce(v:exception) + let s:no_support = 1 + unlet! g:java_ignore_markdown + let g:java_ignore_markdown = 28 + endtry catch /\<E48[45]:/ call s:ReportOnce(v:exception) - unlockvar s:with_markdown - let s:with_markdown = 0 - lockvar s:with_markdown - hi clear markdownCode - hi clear markdownCodeBlock - hi clear markdownCodeDelimiter - hi clear markdownLinkDelimiter + let s:no_support = 1 finally unlet! b:current_syntax + + if exists("s:no_support") + unlet s:no_support + unlockvar s:with_markdown + let s:with_markdown = 0 + lockvar s:with_markdown + hi clear markdownCode + hi clear markdownCodeBlock + hi clear markdownCodeDelimiter + hi clear markdownLinkDelimiter + endif endtry endif @@ -422,7 +436,6 @@ if !exists("g:java_ignore_javadoc") && (s:with_html || s:with_markdown) && g:mai exec 'syn region javaMarkdownCommentTitle contained matchgroup=javaMarkdownComment start="\%(///.*\r\=\n\s*\)\@' . s:ff.Peek('80', '') . '<!///\s*\%({@return\>\)\@=" matchgroup=javaMarkdownCommentTitle end="}\%(\s*\.*\)*" contains=javaMarkdownShortcutLink,@javaMarkdown,javaMarkdownCommentMask,javaTodo,@Spell,@javaDocTags,javaTitleSkipBlock' exec 'syn region javaMarkdownCommentTitle contained matchgroup=javaMarkdownComment start="\%(///.*\r\=\n\s*\)\@' . s:ff.Peek('80', '') . '<!///\s*\%({@summary\>\)\@=" matchgroup=javaMarkdownCommentTitle end="}" contains=javaMarkdownShortcutLink,@javaMarkdown,javaMarkdownCommentMask,javaTodo,@Spell,@javaDocTags,javaTitleSkipBlock' - syn clear markdownId markdownLineStart markdownH1 markdownH2 markdownHeadingRule markdownRule markdownCode markdownCodeBlock markdownIdDeclaration " REDEFINE THE MARKDOWN ITEMS ANCHORED WITH "^", OBSERVING THE " DEFINITION ORDER. syn match markdownLineStart contained "^\s*///\s*[<@]\@!" contains=@markdownBlock,javaMarkdownCommentTitle,javaMarkdownCommentMask nextgroup=@markdownBlock,htmlSpecialChar diff --git a/runtime/syntax/sh.vim b/runtime/syntax/sh.vim index 0a8fb47b7d..d71a966553 100644 --- a/runtime/syntax/sh.vim +++ b/runtime/syntax/sh.vim @@ -5,6 +5,7 @@ " Lennart Schultz <Lennart.Schultz@ecmwf.int> " Last Change: 2024 Mar 04 by Vim Project " 2024 Nov 03 by Aliaksei Budavei <0x000c70 AT gmail DOT com> (improved bracket expressions, #15941) +" 2025 Jan 06 add $PS0 to bashSpecialVariables (#16394) " Version: 208 " Former URL: http://www.drchip.org/astronaut/vim/index.html#SYNTAX_SH " For options and settings, please use: :help ft-sh-syntax @@ -404,7 +405,7 @@ syn region shCmdParenRegion matchgroup=shCmdSubRegion start="((\@!" skip='\\\\\| if exists("b:is_bash") syn cluster shCommandSubList add=bashSpecialVariables,bashStatement syn cluster shCaseList add=bashAdminStatement,bashStatement - syn keyword bashSpecialVariables contained auto_resume BASH BASH_ALIASES BASH_ALIASES BASH_ARGC BASH_ARGC BASH_ARGV BASH_ARGV BASH_CMDS BASH_CMDS BASH_COMMAND BASH_COMMAND BASH_ENV BASH_EXECUTION_STRING BASH_EXECUTION_STRING BASH_LINENO BASH_LINENO BASHOPTS BASHOPTS BASHPID BASHPID BASH_REMATCH BASH_REMATCH BASH_SOURCE BASH_SOURCE BASH_SUBSHELL BASH_SUBSHELL BASH_VERSINFO BASH_VERSION BASH_XTRACEFD BASH_XTRACEFD CDPATH COLUMNS COLUMNS COMP_CWORD COMP_CWORD COMP_KEY COMP_KEY COMP_LINE COMP_LINE COMP_POINT COMP_POINT COMPREPLY COMPREPLY COMP_TYPE COMP_TYPE COMP_WORDBREAKS COMP_WORDBREAKS COMP_WORDS COMP_WORDS COPROC COPROC DIRSTACK EMACS EMACS ENV ENV EUID FCEDIT FIGNORE FUNCNAME FUNCNAME FUNCNEST FUNCNEST GLOBIGNORE GROUPS histchars HISTCMD HISTCONTROL HISTFILE HISTFILESIZE HISTIGNORE HISTSIZE HISTTIMEFORMAT HISTTIMEFORMAT HOME HOSTFILE HOSTNAME HOSTTYPE IFS IGNOREEOF INPUTRC LANG LC_ALL LC_COLLATE LC_CTYPE LC_CTYPE LC_MESSAGES LC_NUMERIC LC_NUMERIC LINENO LINES LINES MACHTYPE MAIL MAILCHECK MAILPATH MAPFILE MAPFILE OLDPWD OPTARG OPTERR OPTIND OSTYPE PATH PIPESTATUS POSIXLY_CORRECT POSIXLY_CORRECT PPID PROMPT_COMMAND PS1 PS2 PS3 PS4 PWD RANDOM READLINE_LINE READLINE_LINE READLINE_POINT READLINE_POINT REPLY SECONDS SHELL SHELL SHELLOPTS SHLVL TIMEFORMAT TIMEOUT TMPDIR TMPDIR UID + syn keyword bashSpecialVariables contained auto_resume BASH BASH_ALIASES BASH_ALIASES BASH_ARGC BASH_ARGC BASH_ARGV BASH_ARGV BASH_CMDS BASH_CMDS BASH_COMMAND BASH_COMMAND BASH_ENV BASH_EXECUTION_STRING BASH_EXECUTION_STRING BASH_LINENO BASH_LINENO BASHOPTS BASHOPTS BASHPID BASHPID BASH_REMATCH BASH_REMATCH BASH_SOURCE BASH_SOURCE BASH_SUBSHELL BASH_SUBSHELL BASH_VERSINFO BASH_VERSION BASH_XTRACEFD BASH_XTRACEFD CDPATH COLUMNS COLUMNS COMP_CWORD COMP_CWORD COMP_KEY COMP_KEY COMP_LINE COMP_LINE COMP_POINT COMP_POINT COMPREPLY COMPREPLY COMP_TYPE COMP_TYPE COMP_WORDBREAKS COMP_WORDBREAKS COMP_WORDS COMP_WORDS COPROC COPROC DIRSTACK EMACS EMACS ENV ENV EUID FCEDIT FIGNORE FUNCNAME FUNCNAME FUNCNEST FUNCNEST GLOBIGNORE GROUPS histchars HISTCMD HISTCONTROL HISTFILE HISTFILESIZE HISTIGNORE HISTSIZE HISTTIMEFORMAT HISTTIMEFORMAT HOME HOSTFILE HOSTNAME HOSTTYPE IFS IGNOREEOF INPUTRC LANG LC_ALL LC_COLLATE LC_CTYPE LC_CTYPE LC_MESSAGES LC_NUMERIC LC_NUMERIC LINENO LINES LINES MACHTYPE MAIL MAILCHECK MAILPATH MAPFILE MAPFILE OLDPWD OPTARG OPTERR OPTIND OSTYPE PATH PIPESTATUS POSIXLY_CORRECT POSIXLY_CORRECT PPID PROMPT_COMMAND PS0 PS1 PS2 PS3 PS4 PWD RANDOM READLINE_LINE READLINE_LINE READLINE_POINT READLINE_POINT REPLY SECONDS SHELL SHELL SHELLOPTS SHLVL TIMEFORMAT TIMEOUT TMPDIR TMPDIR UID syn keyword bashStatement chmod clear complete du egrep expr fgrep find gnufind gnugrep grep head less ls mkdir mv rm rmdir rpm sed sleep sort strip tail syn keyword bashAdminStatement daemon killall killproc nice reload restart start status stop syn keyword bashStatement command compgen diff --git a/runtime/syntax/shaderslang.vim b/runtime/syntax/shaderslang.vim new file mode 100644 index 0000000000..1cae202b04 --- /dev/null +++ b/runtime/syntax/shaderslang.vim @@ -0,0 +1,360 @@ +" Vim syntax file +" Language: Slang +" Maintainer: Austin Shijo <epestr@proton.me> +" Last Change: 2024 Jan 05 + +if exists("b:current_syntax") + finish +endif + +" Read the C syntax to start with +runtime! syntax/c.vim +unlet b:current_syntax + +" Annotations +syn match shaderslangAnnotation /<.*;>/ + +" Attributes +syn match shaderslangAttribute /^\s*\[maxvertexcount(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[domain(\s*"\(tri\|quad\|isoline\)"\s*)\]/ +syn match shaderslangAttribute /^\s*\[earlydepthstencil\]/ +syn match shaderslangAttribute /^\s*\[instance(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[maxtessfactor(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[numthreads(\s*\w\+\s*,\s*\w\+\s*,\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[outputcontrolpoints(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[outputtopology(\s*"\(point\|line\|triangle_cw\|triangle_ccw\|triangle\)"\s*)\]/ +syn match shaderslangAttribute /^\s*\[partitioning(\s*"\(integer\|fractional_even\|fractional_odd\|pow2\)"\s*)\]/ +syn match shaderslangAttribute /^\s*\[patchconstantfunc(\s*"\(\d\|\w\|_\)\+"\s*)\]/ +syn match shaderslangAttribute /^\s*\[WaveSize(\s*\w\+\(\s*,\s*\w\+\(\s*,\s*\w\+\)\?\)\?\s*)\]/ +syn match shaderslangAttribute /^\s*\[shader(\s*"\(anyhit\|callable\|closesthit\|intersection\|miss\|raygeneration\)"\s*)\]/ + +syn match shaderslangAttribute /^\s*\[fastopt\]/ +syn match shaderslangAttribute /^\s*\[loop\]/ +syn match shaderslangAttribute /^\s*\[unroll\]/ +syn match shaderslangAttribute /^\s*\[allow_uav_condition\]/ +syn match shaderslangAttribute /^\s*\[branch\]/ +syn match shaderslangAttribute /^\s*\[flatten\]/ +syn match shaderslangAttribute /^\s*\[forcecase\]/ +syn match shaderslangAttribute /^\s*\[call\]/ +syn match shaderslangAttribute /^\s*\[WaveOpsIncludeHelperLanes\]/ + +syn match shaderslangAttribute /\[raypayload\]/ + +" Work graph shader target attributes +syn match shaderslangAttribute /^\s*\[Shader(\s*"\(\d\|\w\|_\)\+"\s*)\]/ + +" Work graph shader function attributes +syn match shaderslangAttribute /^\s*\[NodeLaunch(\s*"\(broadcasting\|coalescing\|thread\)"\s*)\]/ +syn match shaderslangAttribute /^\s*\[NodeIsProgramEntry\]/ +syn match shaderslangAttribute /^\s*\[NodeLocalRootArgumentsTableIndex(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[NumThreads(\s*\w\+\s*,\s*\w\+\s*,\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[NodeShareInputOf(\s*"\w\+"\(\s*,\s*\w\+\)\?\s*)\]/ +syn match shaderslangAttribute /^\s*\[NodeDispatchGrid(\s*\w\+\s*,\s*\w\+\s*,\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[NodeMaxDispatchGrid(\s*\w\+\s*,\s*\w\+\s*,\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[NodeMaxRecursionDepth(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /^\s*\[NodeMaxInputRecordsPerGraphEntryRecord(\s*\w\+\s*,\s*\(true\|false\)\s*)\]/ + +" Work graph record attributes +syn match shaderslangAttribute /\[NodeTrackRWInputSharing\]/ +syn match shaderslangAttribute /\[MaxRecords(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /\[NodeID(\s*"\w\+"\(\s*,\s*\w\+\)\?\s*)\]/ +syn match shaderslangAttribute /\[MaxRecordsSharedWith(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /\[AllowSparseNodes\]/ +syn match shaderslangAttribute /\[NodeArraySize(\s*\w\+\s*)\]/ +syn match shaderslangAttribute /\[UnboundedSparseNodes\]/ + +" Intrinsic functions +syn keyword shaderslangFunc abs acos acosh asin asinh atan atanh cos cosh exp exp2 floor log log10 log2 round rsqrt sin sincos sinh sqrt tan tanh trunc +syn keyword shaderslangFunc AllMemoryBarrier AllMemoryBarrierWithGroupSync DeviceMemoryBarrier DeviceMemoryBarrierWithGroupSync GroupMemoryBarrier GroupMemoryBarrierWithGroupSync +syn keyword shaderslangFunc abort clip errorf printf +syn keyword shaderslangFunc all any countbits faceforward firstbithigh firstbitlow isfinite isinf isnan max min noise pow reversebits sign +syn keyword shaderslangFunc asdouble asfloat asint asuint D3DCOLORtoUBYTE4 f16tof32 f32tof16 +syn keyword shaderslangFunc ceil clamp degrees fma fmod frac frexp ldexp lerp mad modf radiants saturate smoothstep step +syn keyword shaderslangFunc cross determinant distance dot dst length lit msad4 mul normalize rcp reflect refract transpose +syn keyword shaderslangFunc ddx ddx_coarse ddx_fine ddy ddy_coarse ddy_fine fwidth +syn keyword shaderslangFunc EvaluateAttributeAtCentroid EvaluateAttributeAtSample EvaluateAttributeSnapped +syn keyword shaderslangFunc GetRenderTargetSampleCount GetRenderTargetSamplePosition +syn keyword shaderslangFunc InterlockedAdd InterlockedAnd InterlockedCompareExchange InterlockedCompareStore InterlockedExchange InterlockedMax InterlockedMin InterlockedOr InterlockedXor +syn keyword shaderslangFunc InterlockedCompareStoreFloatBitwise InterlockedCompareExchangeFloatBitwise +syn keyword shaderslangFunc Process2DQuadTessFactorsAvg Process2DQuadTessFactorsMax Process2DQuadTessFactorsMin ProcessIsolineTessFactors +syn keyword shaderslangFunc ProcessQuadTessFactorsAvg ProcessQuadTessFactorsMax ProcessQuadTessFactorsMin ProcessTriTessFactorsAvg ProcessTriTessFactorsMax ProcessTriTessFactorsMin +syn keyword shaderslangFunc tex1D tex1Dbias tex1Dgrad tex1Dlod tex1Dproj +syn keyword shaderslangFunc tex2D tex2Dbias tex2Dgrad tex2Dlod tex2Dproj +syn keyword shaderslangFunc tex3D tex3Dbias tex3Dgrad tex3Dlod tex3Dproj +syn keyword shaderslangFunc texCUBE texCUBEbias texCUBEgrad texCUBElod texCUBEproj +syn keyword shaderslangFunc WaveIsFirstLane WaveGetLaneCount WaveGetLaneIndex +syn keyword shaderslangFunc IsHelperLane +syn keyword shaderslangFunc WaveActiveAnyTrue WaveActiveAllTrue WaveActiveBallot +syn keyword shaderslangFunc WaveReadLaneFirst WaveReadLaneAt +syn keyword shaderslangFunc WaveActiveAllEqual WaveActiveAllEqualBool WaveActiveCountBits +syn keyword shaderslangFunc WaveActiveSum WaveActiveProduct WaveActiveBitAnd WaveActiveBitOr WaveActiveBitXor WaveActiveMin WaveActiveMax +syn keyword shaderslangFunc WavePrefixCountBits WavePrefixProduct WavePrefixSum +syn keyword shaderslangFunc QuadReadAcrossX QuadReadAcrossY QuadReadAcrossDiagonal QuadReadLaneAt +syn keyword shaderslangFunc QuadAny QuadAll +syn keyword shaderslangFunc WaveMatch WaveMultiPrefixSum WaveMultiPrefixProduct WaveMultiPrefixCountBits WaveMultiPrefixAnd WaveMultiPrefixOr WaveMultiPrefixXor +syn keyword shaderslangFunc NonUniformResourceIndex +syn keyword shaderslangFunc DispatchMesh SetMeshOutputCounts +syn keyword shaderslangFunc dot4add_u8packed dot4add_i8packed dot2add + +syn keyword shaderslangFunc RestartStrip +syn keyword shaderslangFunc CalculateLevelOfDetail CalculateLevelOfDetailUnclamped Gather GetDimensions GetSamplePosition Load Sample SampleBias SampleCmp SampleCmpLevelZero SampleGrad SampleLevel GatherRaw SampleCmpLevel +syn keyword shaderslangFunc SampleCmpBias SampleCmpGrad +syn keyword shaderslangFunc WriteSamplerFeedback WriteSamplerFeedbackBias WriteSamplerFeedbackGrad WriteSamplerFeedbackLevel +syn keyword shaderslangFunc Append Consume DecrementCounter IncrementCounter +syn keyword shaderslangFunc Load2 Load3 Load4 Store Store2 Store3 Store4 +syn keyword shaderslangFunc GatherRed GatherGreen GatherBlue GatherAlpha GatherCmp GatherCmpRed GatherCmpGreen GatherCmpBlue GatherCmpAlpha +syn match shaderslangFunc /\.mips\[\d\+\]\[\d\+\]/ +syn match shaderslangFunc /\.sample\[\d\+\]\[\d\+\]/ + +" Ray intrinsics +syn keyword shaderslangFunc AcceptHitAndEndSearch CallShader IgnoreHit ReportHit TraceRay +syn keyword shaderslangFunc DispatchRaysIndex DispatchRaysDimensions +syn keyword shaderslangFunc WorldRayOrigin WorldRayDirection RayTMin RayTCurrent RayFlags +syn keyword shaderslangFunc InstanceIndex InstanceID GeometryIndex PrimitiveIndex ObjectRayOrigin ObjectRayDirection ObjectToWorld3x4 ObjectToWorld4x3 WorldToObject3x4 WorldToObject4x3 +syn keyword shaderslangFunc HitKind + +" RayQuery intrinsics +syn keyword shaderslangFunc TraceRayInline Proceed Abort CommittedStatus +syn keyword shaderslangFunc CandidateType CandidateProceduralPrimitiveNonOpaque CandidateTriangleRayT CandidateInstanceIndex CandidateInstanceID CandidateInstanceContributionToHitGroupIndex CandidateGeometryIndex +syn keyword shaderslangFunc CandidatePrimitiveIndex CandidateObjectRayOrigin CandidateObjectRayDirection CandidateObjectToWorld3x4 CandidateObjectToWorld4x3 CandidateWorldToObject3x4 CandidateWorldToObject4x3 +syn keyword shaderslangFunc CommitNonOpaqueTriangleHit CommitProceduralPrimitiveHit CommittedStatus CommittedRayT CommittedInstanceIndex CommittedInstanceID CommittedInstanceContributionToHitGroupIndex +syn keyword shaderslangFunc CommittedGeometryIndex CommittedPrimitiveIndex CommittedObjectRayOrigin CommittedObjectRayDirection CommittedObjectToWorld3x4 CommittedObjectToWorld4x3 CommittedWorldToObject3x4 +syn keyword shaderslangFunc CommittedWorldToObject4x3 CandidateTriangleBarycentrics CandidateTriangleFrontFace CommittedTriangleBarycentrics CommittedTriangleFrontFace + +" Pack/unpack math intrinsics +syn keyword shaderslangFunc unpack_s8s16 unpack_u8u16 unpack_s8s32 unpack_u8u32 +syn keyword shaderslangFunc pack_u8 pack_s8 pack_clamp_u8 pack_clamp_s8 + +" Work graph object methods +syn keyword shaderslangFunc Get FinishedCrossGroupSharing Count GetThreadNodeOutputRecords GetGroupNodeOutputRecords IsValid GroupIncrementOutputCount ThreadIncrementOutputCount OutputComplete + +" Work graph free intrinsics +syn keyword shaderslangFunc GetRemainingRecursionLevels Barrier + +" Layout Qualifiers +syn keyword shaderslangLayoutQual const row_major column_major +syn keyword shaderslangLayoutQual point line triangle lineadj triangleadj +syn keyword shaderslangLayoutQual InputPatch OutputPatch +syn match shaderslangLayoutQual /PointStream<\s*\w\+\s*>/ +syn match shaderslangLayoutQual /LineStream<\s*\w\+\s*>/ +syn match shaderslangLayoutQual /TriangleStream<\s*\w\+\s*>/ + +" User defined Semantics +syn match shaderslangSemantic /:\s*[A-Z]\w*/ +syn match shaderslangSemantic /:\s*packoffset(\s*c\d\+\(\.[xyzw]\)\?\s*)/ " packoffset +syn match shaderslangSemantic /:\s*register(\s*\(r\|x\|v\|t\|s\|cb\|icb\|b\|c\|u\)\d\+\s*)/ " register +syn match shaderslangSemantic /:\s*read(\s*\(\(anyhit\|closesthit\|miss\|caller\)\s*,\s*\)*\(anyhit\|closesthit\|miss\|caller\)\?\s*)/ " read +syn match shaderslangSemantic /:\s*write(\s*\(\(anyhit\|closesthit\|miss\|caller\)\s*,\s*\)*\(anyhit\|closesthit\|miss\|caller\)\?\s*)/ " write + +" System-Value Semantics +" Vertex Shader +syn match shaderslangSemantic /SV_ClipDistance\d\+/ +syn match shaderslangSemantic /SV_CullDistance\d\+/ +syn keyword shaderslangSemantic SV_Position SV_InstanceID SV_PrimitiveID SV_VertexID +syn keyword shaderslangSemantic SV_StartVertexLocation SV_StartInstanceLocation +" Tessellation pipeline +syn keyword shaderslangSemantic SV_DomainLocation SV_InsideTessFactor SV_OutputControlPointID SV_TessFactor +" Geometry Shader +syn keyword shaderslangSemantic SV_GSInstanceID SV_RenderTargetArrayIndex +" Pixel Shader - MSAA +syn keyword shaderslangSemantic SV_Coverage SV_Depth SV_IsFrontFace SV_SampleIndex +syn match shaderslangSemantic /SV_Target[0-7]/ +syn keyword shaderslangSemantic SV_ShadingRate SV_ViewID +syn match shaderslangSemantic /SV_Barycentrics[0-1]/ +" Compute Shader +syn keyword shaderslangSemantic SV_DispatchThreadID SV_GroupID SV_GroupIndex SV_GroupThreadID +" Mesh shading pipeline +syn keyword shaderslangSemantic SV_CullPrimitive +" Work graph record system values +syn keyword shaderslangSemantic SV_DispatchGrid + +" slang structures +syn keyword shaderslangStructure cbuffer + +" Shader profiles +" Cg profiles +syn keyword shaderslangProfile arbfp1 arbvp1 fp20 vp20 fp30 vp30 ps_1_1 ps_1_2 ps_1_3 +" Shader Model 1 +syn keyword shaderslangProfile vs_1_1 +" Shader Model 2 +syn keyword shaderslangProfile ps_2_0 ps_2_x vs_2_0 vs_2_x +" Shader Model 3 +syn keyword shaderslangProfile ps_3_0 vs_3_0 +" Shader Model 4 +syn keyword shaderslangProfile gs_4_0 ps_4_0 vs_4_0 gs_4_1 ps_4_1 vs_4_1 +" Shader Model 5 +syn keyword shaderslangProfile cs_4_0 cs_4_1 cs_5_0 ds_5_0 gs_5_0 hs_5_0 ps_5_0 vs_5_0 +" Shader Model 6 +syn keyword shaderslangProfile cs_6_0 ds_6_0 gs_6_0 hs_6_0 ps_6_0 vs_6_0 lib_6_0 + +" Swizzling +syn match shaderslangSwizzle /\.[xyzw]\{1,4\}\>/ +syn match shaderslangSwizzle /\.[rgba]\{1,4\}\>/ +syn match shaderslangSwizzle /\.\(_m[0-3]\{2}\)\{1,4\}/ +syn match shaderslangSwizzle /\.\(_[1-4]\{2}\)\{1,4\}/ + +" Other Statements +syn keyword shaderslangStatement discard + +" Storage class +syn match shaderslangStorageClass /\<in\(\s+pipeline\)\?\>/ +syn match shaderslangStorageClass /\<out\(\s\+indices\|\s\+vertices\|\s\+primitives\)\?\>/ +syn keyword shaderslangStorageClass inout +syn keyword shaderslangStorageClass extern nointerpolation precise shared groupshared static uniform volatile +syn keyword shaderslangStorageClass snorm unorm +syn keyword shaderslangStorageClass linear centroid nointerpolation noperspective sample +syn keyword shaderslangStorageClass globallycoherent + +" Types +" Buffer types +syn keyword shaderslangType ConstantBuffer Buffer ByteAddressBuffer ConsumeStructuredBuffer StructuredBuffer +syn keyword shaderslangType AppendStructuredBuffer RWBuffer RWByteAddressBuffer RWStructuredBuffer +syn keyword shaderslangType RasterizerOrderedBuffer RasterizerOrderedByteAddressBuffer RasterizerOrderedStructuredBuffer + +" Scalar types +syn keyword shaderslangType bool int uint dword half float double +syn keyword shaderslangType min16float min10float min16int min12int min16uint +syn keyword shaderslangType float16_t float32_t float64_t + +" Vector types +syn match shaderslangType /vector<\s*\w\+,\s*[1-4]\s*>/ +syn keyword shaderslangType bool1 bool2 bool3 bool4 +syn keyword shaderslangType int1 int2 int3 int4 +syn keyword shaderslangType uint1 uint2 uint3 uint4 +syn keyword shaderslangType dword1 dword2 dword3 dword4 +syn keyword shaderslangType half1 half2 half3 half4 +syn keyword shaderslangType float1 float2 float3 float4 +syn keyword shaderslangType double1 double2 double3 double4 +syn keyword shaderslangType min16float1 min16float2 min16float3 min16float4 +syn keyword shaderslangType min10float1 min10float2 min10float3 min10float4 +syn keyword shaderslangType min16int1 min16int2 min16int3 min16int4 +syn keyword shaderslangType min12int1 min12int2 min12int3 min12int4 +syn keyword shaderslangType min16uint1 min16uint2 min16uint3 min16uint4 +syn keyword shaderslangType float16_t1 float16_t2 float16_t3 float16_t4 +syn keyword shaderslangType float32_t1 float32_t2 float32_t3 float32_t4 +syn keyword shaderslangType float64_t1 float64_t2 float64_t3 float64_t4 +syn keyword shaderslangType int16_t1 int16_t2 int16_t3 int16_t4 +syn keyword shaderslangType int32_t1 int32_t2 int32_t3 int32_t4 +syn keyword shaderslangType int64_t1 int64_t2 int64_t3 int64_t4 +syn keyword shaderslangType uint16_t1 uint16_t2 uint16_t3 uint16_t4 +syn keyword shaderslangType uint32_t1 uint32_t2 uint32_t3 uint32_t4 +syn keyword shaderslangType uint64_t1 uint64_t2 uint64_t3 uint64_t4 + +" Packed types +syn keyword shaderslangType uint8_t4_packed int8_t4_packed + +" Matrix types +syn match shaderslangType /matrix<\s*\w\+\s*,\s*[1-4]\s*,\s*[1-4]\s*>/ +syn keyword shaderslangType bool1x1 bool2x1 bool3x1 bool4x1 bool1x2 bool2x2 bool3x2 bool4x2 bool1x3 bool2x3 bool3x3 bool4x3 bool1x4 bool2x4 bool3x4 bool4x4 +syn keyword shaderslangType int1x1 int2x1 int3x1 int4x1 int1x2 int2x2 int3x2 int4x2 int1x3 int2x3 int3x3 int4x3 int1x4 int2x4 int3x4 int4x4 +syn keyword shaderslangType uint1x1 uint2x1 uint3x1 uint4x1 uint1x2 uint2x2 uint3x2 uint4x2 uint1x3 uint2x3 uint3x3 uint4x3 uint1x4 uint2x4 uint3x4 uint4x4 +syn keyword shaderslangType dword1x1 dword2x1 dword3x1 dword4x1 dword1x2 dword2x2 dword3x2 dword4x2 dword1x3 dword2x3 dword3x3 dword4x3 dword1x4 dword2x4 dword3x4 dword4x4 +syn keyword shaderslangType half1x1 half2x1 half3x1 half4x1 half1x2 half2x2 half3x2 half4x2 half1x3 half2x3 half3x3 half4x3 half1x4 half2x4 half3x4 half4x4 +syn keyword shaderslangType float1x1 float2x1 float3x1 float4x1 float1x2 float2x2 float3x2 float4x2 float1x3 float2x3 float3x3 float4x3 float1x4 float2x4 float3x4 float4x4 +syn keyword shaderslangType double1x1 double2x1 double3x1 double4x1 double1x2 double2x2 double3x2 double4x2 double1x3 double2x3 double3x3 double4x3 double1x4 double2x4 double3x4 double4x4 +syn keyword shaderslangType min16float1x1 min16float2x1 min16float3x1 min16float4x1 min16float1x2 min16float2x2 min16float3x2 min16float4x2 min16float1x3 min16float2x3 min16float3x3 min16float4x3 min16float1x4 min16float2x4 min16float3x4 min16float4x4 +syn keyword shaderslangType min10float1x1 min10float2x1 min10float3x1 min10float4x1 min10float1x2 min10float2x2 min10float3x2 min10float4x2 min10float1x3 min10float2x3 min10float3x3 min10float4x3 min10float1x4 min10float2x4 min10float3x4 min10float4x4 +syn keyword shaderslangType min16int1x1 min16int2x1 min16int3x1 min16int4x1 min16int1x2 min16int2x2 min16int3x2 min16int4x2 min16int1x3 min16int2x3 min16int3x3 min16int4x3 min16int1x4 min16int2x4 min16int3x4 min16int4x4 +syn keyword shaderslangType min12int1x1 min12int2x1 min12int3x1 min12int4x1 min12int1x2 min12int2x2 min12int3x2 min12int4x2 min12int1x3 min12int2x3 min12int3x3 min12int4x3 min12int1x4 min12int2x4 min12int3x4 min12int4x4 +syn keyword shaderslangType min16uint1x1 min16uint2x1 min16uint3x1 min16uint4x1 min16uint1x2 min16uint2x2 min16uint3x2 min16uint4x2 min16uint1x3 min16uint2x3 min16uint3x3 min16uint4x3 min16uint1x4 min16uint2x4 min16uint3x4 min16uint4x4 +syn keyword shaderslangType float16_t1x1 float16_t2x1 float16_t3x1 float16_t4x1 float16_t1x2 float16_t2x2 float16_t3x2 float16_t4x2 float16_t1x3 float16_t2x3 float16_t3x3 float16_t4x3 float16_t1x4 float16_t2x4 float16_t3x4 float16_t4x4 +syn keyword shaderslangType float32_t1x1 float32_t2x1 float32_t3x1 float32_t4x1 float32_t1x2 float32_t2x2 float32_t3x2 float32_t4x2 float32_t1x3 float32_t2x3 float32_t3x3 float32_t4x3 float32_t1x4 float32_t2x4 float32_t3x4 float32_t4x4 +syn keyword shaderslangType float64_t1x1 float64_t2x1 float64_t3x1 float64_t4x1 float64_t1x2 float64_t2x2 float64_t3x2 float64_t4x2 float64_t1x3 float64_t2x3 float64_t3x3 float64_t4x3 float64_t1x4 float64_t2x4 float64_t3x4 float64_t4x4 +syn keyword shaderslangType int16_t1x1 int16_t2x1 int16_t3x1 int16_t4x1 int16_t1x2 int16_t2x2 int16_t3x2 int16_t4x2 int16_t1x3 int16_t2x3 int16_t3x3 int16_t4x3 int16_t1x4 int16_t2x4 int16_t3x4 int16_t4x4 +syn keyword shaderslangType int32_t1x1 int32_t2x1 int32_t3x1 int32_t4x1 int32_t1x2 int32_t2x2 int32_t3x2 int32_t4x2 int32_t1x3 int32_t2x3 int32_t3x3 int32_t4x3 int32_t1x4 int32_t2x4 int32_t3x4 int32_t4x4 +syn keyword shaderslangType int64_t1x1 int64_t2x1 int64_t3x1 int64_t4x1 int64_t1x2 int64_t2x2 int64_t3x2 int64_t4x2 int64_t1x3 int64_t2x3 int64_t3x3 int64_t4x3 int64_t1x4 int64_t2x4 int64_t3x4 int64_t4x4 +syn keyword shaderslangType uint16_t1x1 uint16_t2x1 uint16_t3x1 uint16_t4x1 uint16_t1x2 uint16_t2x2 uint16_t3x2 uint16_t4x2 uint16_t1x3 uint16_t2x3 uint16_t3x3 uint16_t4x3 uint16_t1x4 uint16_t2x4 uint16_t3x4 uint16_t4x4 +syn keyword shaderslangType uint32_t1x1 uint32_t2x1 uint32_t3x1 uint32_t4x1 uint32_t1x2 uint32_t2x2 uint32_t3x2 uint32_t4x2 uint32_t1x3 uint32_t2x3 uint32_t3x3 uint32_t4x3 uint32_t1x4 uint32_t2x4 uint32_t3x4 uint32_t4x4 +syn keyword shaderslangType uint64_t1x1 uint64_t2x1 uint64_t3x1 uint64_t4x1 uint64_t1x2 uint64_t2x2 uint64_t3x2 uint64_t4x2 uint64_t1x3 uint64_t2x3 uint64_t3x3 uint64_t4x3 uint64_t1x4 uint64_t2x4 uint64_t3x4 uint64_t4x4 + +" Sampler types +syn keyword shaderslangType SamplerState SamplerComparisonState +syn keyword shaderslangType sampler sampler1D sampler2D sampler3D samplerCUBE sampler_state + +" Texture types +syn keyword shaderslangType Texture1D Texture1DArray Texture2D Texture2DArray Texture2DMS Texture2DMSArray Texture3D TextureCube TextureCubeArray +syn keyword shaderslangType RWTexture1D RWTexture2D RWTexture2DArray RWTexture3D RWTextureCubeArray RWTexture2DMS RWTexture2DMSArray +syn keyword shaderslangType FeedbackTexture2D FeedbackTexture2DArray +syn keyword shaderslangType RasterizerOrderedTexture1D RasterizerOrderedTexture1DArray RasterizerOrderedTexture2D RasterizerOrderedTexture2DArray RasterizerOrderedTexture3D +syn keyword shaderslangTypeDeprec texture texture1D texture2D texture3D + +" Raytracing types +syn keyword shaderslangType RaytracingAccelerationStructure RayDesc RayQuery BuiltInTriangleIntersectionAttributes + +" Work graph input record objects +syn keyword shaderslangType DispatchNodeInputRecord RWDispatchNodeInputRecord GroupNodeInputRecords RWGroupNodeInputRecords ThreadNodeInputRecord RWThreadNodeInputRecord EmptyNodeInput + +" Work graph output node objects +syn keyword shaderslangType NodeOutput NodeOutputArray EmptyNodeOutput EmptyNodeOutputArray + +" Work graph output record objects +syn keyword shaderslangType ThreadNodeOutputRecords GroupNodeOutputRecords + +" State Groups args +syn case ignore " This section case insensitive + +" Blend state group +syn keyword shaderslangStateGroupArg AlphaToCoverageEnable BlendEnable SrcBlend DestBlend BlendOp SrcBlendAlpha DestBlendAlpha BlendOpAlpha RenderTargetWriteMask +syn keyword shaderslangStateGroupVal ZERO ONE SRC_COLOR INV_SRC_COLOR SRC_ALPHA INV_SRC_ALPHA DEST_ALPHA INV_DEST_ALPHA DEST_COLOR INV_DEST_COLOR SRC_ALPHA_SAT BLEND_FACTOR INV_BLEND_FACTOR SRC1_COLOR INV_SRC1_COLOR SRC1_ALPHA INV_SRC1_ALPHA +syn keyword shaderslangStateGroupVal ADD SUBSTRACT REV_SUBSTRACT MIN MAX + +" Rasterizer state group +syn keyword shaderslangStateGroupArg FillMode CullMode FrontCounterClockwise DepthBias DepthBiasClamp SlopeScaledDepthBias ZClipEnable DepthClipEnable ScissorEnable MultisampleEnable AntialiasedLineEnable +syn keyword shaderslangStateGroupVal SOLID WIREFRAME +syn keyword shaderslangStateGroupVal NONE FRONT BACK + +" Sampler state group +syn keyword shaderslangStateGroupArg Filter AddressU AddressV AddressW MipLODBias MaxAnisotropy ComparisonFunc BorderColor MinLOD MaxLOD ComparisonFilter +syn keyword shaderslangStateGroupVal MIN_MAG_MIP_POINT MIN_MAG_POINT_MIP_LINEAR MIN_POINT_MAG_LINEAR_MIP_POINT MIN_POINT_MAG_MIP_LINEAR MIN_LINEAR_MAG_MIP_POINT MIN_LINEAR_MAG_POINT_MIP_LINEAR MIN_MAG_LINEAR_MIP_POINT MIN_MAG_MIP_LINEAR ANISOTROPIC +syn keyword shaderslangStateGroupVal COMPARISON_MIN_MAG_MIP_POINT COMPARISON_MIN_MAG_POINT_MIP_LINEAR COMPARISON_MIN_POINT_MAG_LINEAR_MIP_POINT COMPARISON_MIN_POINT_MAG_MIP_LINEAR COMPARISON_MIN_LINEAR_MAG_MIP_POINT +syn keyword shaderslangStateGroupVal COMPARISON_MIN_LINEAR_MAG_POINT_MIP_LINEAR COMPARISON_MIN_MAG_LINEAR_MIP_POINT COMPARISON_MIN_MAG_MIP_LINEAR COMPARISON_ANISOTROPIC +syn keyword shaderslangStateGroupVal COMPARISON_NEVER COMPARISON_LESS COMPARISON_EQUAL COMPARISON_LESS_EQUAL COMPARISON_GREATER COMPARISON_NOT_EQUAL COMPARISON_GREATER_EQUAL COMPARISON_ALWAYS +syn keyword shaderslangStateGroupVal WRAP MIRROR CLAMP BORDER MIRROR_ONCE +syn keyword shaderslangStateGroupVal SAMPLER_FEEDBACK_MIN_MIP SAMPLER_FEEDBACK_MIP_REGION_USED + +" Ray flags +syn keyword shaderslangStateGroupVal RAY_FLAG_NONE RAY_FLAG_FORCE_OPAQUE RAY_FLAG_FORCE_NON_OPAQUE RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH RAY_FLAG_SKIP_CLOSEST_HIT_SHADER +syn keyword shaderslangStateGroupVal RAY_FLAG_CULL_BACK_FACING_TRIANGLES RAY_FLAG_CULL_FRONT_FACING_TRIANGLES RAY_FLAG_CULL_OPAQUE RAY_FLAG_CULL_NON_OPAQUE +syn keyword shaderslangStateGroupVal RAY_FLAG_SKIP_TRIANGLES RAY_FLAG_SKIP_PROCEDURAL_PRIMITIVES + +" HitKind enum +syn keyword shaderslangStateGroupVal HIT_KIND_TRIANGLE_FRONT_FACE HIT_KIND_TRIANGLE_BACK_FACE + +" RayQuery enums +syn keyword shaderslangStateGroupVal COMMITTED_NOTHING COMMITTED_TRIANGLE_HIT COMMITTED_PROCEDURAL_PRIMITIVE_HIT +syn keyword shaderslangStateGroupVal CANDIDATE_NON_OPAQUE_TRIANGLE CANDIDATE_PROCEDURAL_PRIMITIVE + +" Heap objects +syn keyword shaderslangStateGroupVal ResourceDescriptorHeap SamplerDescriptorHeap + +" Work graph constants +syn keyword shaderslangStateGroupVal UAV_MEMORY GROUP_SHARED_MEMORY NODE_INPUT_MEMORY NODE_OUTPUT_MEMORY ALL_MEMORY GROUP_SYNC GROUP_SCOPE DEVICE_SCOPE + +syn case match " Case sensitive from now on + +" Effect files declarations and functions +" Effect groups, techniques passes +syn keyword shaderslangEffectGroup fxgroup technique11 pass +" Effect functions +syn keyword shaderslangEffectFunc SetBlendState SetDepthStencilState SetRasterizerState SetVertexShader SetHullShader SetDomainShader SetGeometryShader SetPixelShader SetComputeShader CompileShader ConstructGSWithSO SetRenderTargets + +" Default highlighting +hi def link shaderslangProfile shaderslangStatement +hi def link shaderslangStateGroupArg shaderslangStatement +hi def link shaderslangStateGroupVal Number +hi def link shaderslangStatement Statement +hi def link shaderslangType Type +hi def link shaderslangTypeDeprec WarningMsg +hi def link shaderslangStorageClass StorageClass +hi def link shaderslangSemantic PreProc +hi def link shaderslangFunc shaderslangStatement +hi def link shaderslangLayoutQual shaderslangFunc +hi def link shaderslangAnnotation PreProc +hi def link shaderslangStructure Structure +hi def link shaderslangSwizzle SpecialChar +hi def link shaderslangAttribute Statement + +hi def link shaderslangEffectGroup Type +hi def link shaderslangEffectFunc Statement + +let b:current_syntax = "shaderslang" diff --git a/runtime/syntax/tiasm.vim b/runtime/syntax/tiasm.vim new file mode 100644 index 0000000000..bdadc4a0a7 --- /dev/null +++ b/runtime/syntax/tiasm.vim @@ -0,0 +1,102 @@ +" Vim syntax file +" Language: TI linear assembly language +" Document: https://downloads.ti.com/docs/esd/SPRUI03B/#SPRUI03B_HTML/assembler-description.html +" Maintainer: Wu, Zhenyu <wuzhenyu@ustc.edu> +" Last Change: 2025 Jan 08 + +if exists("b:current_syntax") + finish +endif + +syn case ignore + +" storage types +syn match tiasmType "\.bits" +syn match tiasmType "\.byte" +syn match tiasmType "\.char" +syn match tiasmType "\.cstring" +syn match tiasmType "\.double" +syn match tiasmType "\.field" +syn match tiasmType "\.float" +syn match tiasmType "\.half" +syn match tiasmType "\.int" +syn match tiasmType "\.long" +syn match tiasmType "\.short" +syn match tiasmType "\.string" +syn match tiasmType "\.ubyte" +syn match tiasmType "\.uchar" +syn match tiasmType "\.uhalf" +syn match tiasmType "\.uint" +syn match tiasmType "\.ulong" +syn match tiasmType "\.ushort" +syn match tiasmType "\.uword" +syn match tiasmType "\.word" + +syn match tiasmIdentifier "[a-z_][a-z0-9_]*" + +syn match tiasmDecimal "\<[1-9]\d*\>" display +syn match tiasmOctal "\<0[0-7][0-7]\+\>\|\<[0-7]\+[oO]\>" display +syn match tiasmHexadecimal "\<0[xX][0-9a-fA-F]\+\>\|\<[0-9][0-9a-fA-F]*[hH]\>" display +syn match tiasmBinary "\<0[bB][0-1]\+\>\|\<[01]\+[bB]\>" display + +syn match tiasmFloat "\<\d\+\.\d*\%(e[+-]\=\d\+\)\=\>" display +syn match tiasmFloat "\<\d\%(e[+-]\=\d\+\)\>" display + +syn match tiasmCharacter "'.'\|''\|'[^']'" + +syn region tiasmString start="\"" end="\"" skip="\"\"" + +syn match tiasmFunction "\$[a-zA-Z_][a-zA-Z_0-9]*\ze(" + +syn keyword tiasmTodo contained TODO FIXME XXX NOTE +syn region tiasmComment start=";" end="$" keepend contains=tiasmTodo,@Spell +syn match tiasmComment "^[*!].*" contains=tiasmTodo,@Spell +syn match tiasmLabel "^[^ *!;][^ :]*" + +syn match tiasmInclude "\.include" +syn match tiasmCond "\.if" +syn match tiasmCond "\.else" +syn match tiasmCond "\.endif" +syn match tiasmMacro "\.macro" +syn match tiasmMacro "\.endm" + +syn match tiasmDirective "\.[A-Za-z][0-9A-Za-z-_]*" + +syn case match + +hi def link tiasmLabel Label +hi def link tiasmComment Comment +hi def link tiasmTodo Todo +hi def link tiasmDirective Statement + +hi def link tiasmInclude Include +hi def link tiasmCond PreCondit +hi def link tiasmMacro Macro + +if exists('g:tiasm_legacy_syntax_groups') + hi def link hexNumber Number + hi def link decNumber Number + hi def link octNumber Number + hi def link binNumber Number + hi def link tiasmHexadecimal hexNumber + hi def link tiasmDecimal decNumber + hi def link tiasmOctal octNumber + hi def link tiasmBinary binNumber +else + hi def link tiasmHexadecimal Number + hi def link tiasmDecimal Number + hi def link tiasmOctal Number + hi def link tiasmBinary Number +endif +hi def link tiasmFloat Float + +hi def link tiasmString String +hi def link tiasmStringEscape Special +hi def link tiasmCharacter Character +hi def link tiasmCharacterEscape Special + +hi def link tiasmIdentifier Identifier +hi def link tiasmType Type +hi def link tiasmFunction Function + +let b:current_syntax = "lineartiasm" diff --git a/runtime/syntax/vim.vim b/runtime/syntax/vim.vim index 361bf6369d..12798201e2 100644 --- a/runtime/syntax/vim.vim +++ b/runtime/syntax/vim.vim @@ -60,7 +60,7 @@ syn case ignore syn keyword vimGroup contained Comment Constant String Character Number Boolean Float Identifier Function Statement Conditional Repeat Label Operator Keyword Exception PreProc Include Define Macro PreCondit Type StorageClass Structure Typedef Special SpecialChar Tag Delimiter SpecialComment Debug Underlined Ignore Error Todo " Default highlighting groups {{{2 -syn keyword vimHLGroup contained ErrorMsg IncSearch ModeMsg NonText StatusLine StatusLineNC EndOfBuffer VertSplit DiffText PmenuSbar TabLineSel TabLineFill Cursor lCursor QuickFixLine CursorLineSign CursorLineFold CurSearch PmenuKind PmenuKindSel PmenuMatch PmenuMatchSel PmenuExtra PmenuExtraSel Normal Directory LineNr CursorLineNr MoreMsg Question Search SpellBad SpellCap SpellRare SpellLocal PmenuThumb Pmenu PmenuSel SpecialKey Title WarningMsg WildMenu Folded FoldColumn SignColumn Visual DiffAdd DiffChange DiffDelete TabLine CursorColumn CursorLine ColorColumn MatchParen StatusLineTerm StatusLineTermNC CursorIM LineNrAbove LineNrBelow +syn keyword vimHLGroup contained ErrorMsg IncSearch ModeMsg NonText StatusLine StatusLineNC EndOfBuffer VertSplit DiffText PmenuSbar TabLineSel TabLineFill Cursor lCursor QuickFixLine CursorLineSign CursorLineFold CurSearch PmenuKind PmenuKindSel PmenuMatch PmenuMatchSel PmenuExtra PmenuExtraSel ComplMatchIns Normal Directory LineNr CursorLineNr MoreMsg Question Search SpellBad SpellCap SpellRare SpellLocal PmenuThumb Pmenu PmenuSel SpecialKey Title WarningMsg WildMenu Folded FoldColumn SignColumn Visual DiffAdd DiffChange DiffDelete TabLine CursorColumn CursorLine ColorColumn MatchParen StatusLineTerm StatusLineTermNC CursorIM LineNrAbove LineNrBelow syn match vimHLGroup contained "\<Conceal\>" syn keyword vimOnlyHLGroup contained Menu Scrollbar ToolbarButton ToolbarLine Tooltip VisualNOS syn keyword nvimHLGroup contained FloatBorder FloatFooter FloatTitle MsgSeparator NormalFloat NormalNC Substitute TermCursor VisualNC Whitespace WinBar WinBarNC WinSeparator @@ -196,9 +196,9 @@ syn case match " All vimCommands are contained by vimIsCommand. {{{2 syn cluster vimCmdList contains=vimAbb,vimAddress,vimAutoCmd,vimAugroup,vimBehave,vimCall,vimCatch,vimConst,vimDef,vimDefFold,vimDelcommand,@vimEcho,vimEnddef,vimEndfunction,vimExecute,vimIsCommand,vimExtCmd,vimFor,vimFunction,vimFuncFold,vimGlobal,vimHighlight,vimLet,vimLoadkeymap,vimMap,vimMark,vimMatch,vimNotFunc,vimNormal,vimSet,vimSleep,vimSyntax,vimThrow,vimUnlet,vimUnmap,vimUserCmd,vimMenu,vimMenutranslate,@vim9CmdList -syn cluster vim9CmdList contains=vim9Class,vim9Const,vim9Enum,vim9Export,vim9Final,vim9For,vim9Interface,vim9Type,vim9Var +syn cluster vim9CmdList contains=vim9Abstract,vim9Class,vim9Const,vim9Enum,vim9Export,vim9Final,vim9For,vim9Interface,vim9Type,vim9Var syn match vimCmdSep "[:|]\+" skipwhite nextgroup=@vimCmdList,vimSubst1 -syn match vimIsCommand "\<\%(\h\w*\|[23]mat\%[ch]\)\>" contains=vimCommand +syn match vimIsCommand "\<\%(\h\w*\|[23]mat\%[ch]\)\>" nextgroup=vimBang contains=vimCommand syn match vimBang contained "!" syn match vimVar contained "\<\h[a-zA-Z0-9#_]*\>" syn match vimVar "\<[bwglstav]:\h[a-zA-Z0-9#_]*\>" @@ -207,7 +207,6 @@ syn match vimVar "\s\zs&t_\S[a-zA-Z0-9]\>" syn match vimVar "\s\zs&t_k;" syn match vimFBVar contained "\<[bwglstav]:\h[a-zA-Z0-9#_]*\>" syn keyword vimCommand contained in -syn match vimBang contained "!" syn cluster vimExprList contains=vimEnvvar,vimFunc,vimNumber,vimOper,vimOperParen,vimLetRegister,vimString,vimVar,@vim9ExprList syn cluster vim9ExprList contains=vim9Boolean,vim9Null @@ -275,7 +274,8 @@ syn keyword vimAugroupKey contained aug[roup] skipwhite nextgroup=vimAugroupBan " Operators: {{{2 " ========= syn cluster vimOperGroup contains=vimEnvvar,vimFunc,vimFuncVar,vimOper,vimOperParen,vimNumber,vimString,vimRegister,@vimContinue,vim9Comment,vimVar,vimBoolean,vimNull -syn match vimOper "||\|&&\|[-+*/%.!]" skipwhite nextgroup=vimString,vimSpecFile +syn match vimOper "\a\@<!!" skipwhite nextgroup=vimString,vimSpecFile +syn match vimOper "||\|&&\|[-+*/%.]" skipwhite nextgroup=vimString,vimSpecFile syn match vimOper "\%#=1\(==\|!=\|>=\|<=\|=\~\|!\~\|>\|<\|=\|!\~#\)[?#]\{0,2}" skipwhite nextgroup=vimString,vimSpecFile syn match vimOper "\(\<is\|\<isnot\)[?#]\{0,2}\>" skipwhite nextgroup=vimString,vimSpecFile syn region vimOperParen matchgroup=vimParenSep start="(" end=")" contains=@vimOperGroup @@ -553,8 +553,8 @@ syn region vimPatSepZone oneline contained matchgroup=vimPatSepZ start="\\%\ syn region vimPatRegion contained transparent matchgroup=vimPatSepR start="\\[z%]\=(" end="\\)" contains=@vimSubstList oneline syn match vimNotPatSep contained "\\\\" syn cluster vimStringGroup contains=vimEscape,vimEscapeBrace,vimPatSep,vimNotPatSep,vimPatSepErr,vimPatSepZone,@Spell -syn region vimString oneline keepend start=+[^a-zA-Z>!\\@]"+lc=1 skip=+\\\\\|\\"+ matchgroup=vimStringEnd end=+"+ contains=@vimStringGroup extend -syn region vimString oneline keepend start=+[^a-zA-Z>!\\@]'+lc=1 end=+'+ extend +syn region vimString oneline keepend start=+[^a-zA-Z>\\@]"+lc=1 skip=+\\\\\|\\"+ matchgroup=vimStringEnd end=+"+ contains=@vimStringGroup extend +syn region vimString oneline keepend start=+[^a-zA-Z>\\@]'+lc=1 end=+'+ extend "syn region vimString oneline start="\s/\s*\A"lc=1 skip="\\\\\|\\+" end="/" contains=@vimStringGroup " see tst45.vim syn match vimString contained +"[^"]*\\$+ skipnl nextgroup=vimStringCont syn match vimStringCont contained +\(\\\\\|.\)\{-}[^\\]"+ @@ -678,10 +678,12 @@ syn keyword vimAbb abc[lear] cabc[lear] iabc[lear] skipwhite nextgroup=vimMapMod " Autocmd: {{{2 " ======= -syn match vimAutoEventList contained "\(!\s\+\)\=\(\a\+,\)*\a\+" contains=vimAutoEvent,nvimAutoEvent nextgroup=vimAutoCmdSpace +syn match vimAutoCmdBang contained "\a\@1<=!" skipwhite nextgroup=vimAutoEventList +syn match vimAutoEventList contained "\%(\a\+,\)*\a\+" contains=vimAutoEvent,nvimAutoEvent nextgroup=vimAutoCmdSpace syn match vimAutoCmdSpace contained "\s\+" nextgroup=vimAutoCmdSfxList syn match vimAutoCmdSfxList contained "\S*" skipwhite nextgroup=vimAutoCmdMod,vimAutoCmdBlock -syn keyword vimAutoCmd au[tocmd] do[autocmd] doautoa[ll] skipwhite nextgroup=vimAutoEventList +syn keyword vimAutoCmd au[tocmd] skipwhite nextgroup=vimAutoCmdBang,vimAutoEventList +syn keyword vimAutoCmd do[autocmd] doautoa[ll] skipwhite nextgroup=vimAutoEventList syn match vimAutoCmdMod "\(++\)\=\(once\|nested\)" skipwhite nextgroup=vimAutoCmdBlock syn region vimAutoCmdBlock contained matchgroup=vimSep start="{" end="}" contains=@vimDefBodyList @@ -1275,6 +1277,7 @@ if !exists("skip_vim_syntax_inits") hi def link vimAugroupError vimError hi def link vimAugroupKey vimCommand hi def link vimAutoCmd vimCommand + hi def link vimAutoCmdBang vimBang hi def link vimAutoEvent Type hi def link vimAutoCmdMod Special hi def link vimBang vimOper diff --git a/runtime/syntax/xf86conf.vim b/runtime/syntax/xf86conf.vim index e8162f3a35..0f4e5036ff 100644 --- a/runtime/syntax/xf86conf.vim +++ b/runtime/syntax/xf86conf.vim @@ -1,9 +1,9 @@ " Vim syntax file " Language: XF86Config (XFree86 configuration file) +" Maintainer: This runtime file is looking for a new maintainer. +" Last Change: 2025 Jan 06 by Jan-Arvid Harrach (#16397) " Former Maintainer: David Ne\v{c}as (Yeti) <yeti@physics.muni.cz> " Last Change By David: 2010 Nov 01 -" Last Change: 2023 Jan 23 -" Required Vim Version: 6.0 " " Options: let xf86conf_xfree86_version = 3 or 4 " to force XFree86 3.x or 4.x XF86Config syntax @@ -58,7 +58,7 @@ syn match xf86confModeLineValue "\"[^\"]\+\"\(\_s\+[0-9.]\+\)\{9}" nextgroup=xf8 " Sections and subsections if b:xf86conf_xfree86_version >= 4 - syn region xf86confSection matchgroup=xf86confSectionDelim start="^\s*Section\s\+\"\(Files\|Server[_ ]*Flags\|Input[_ ]*Device\|Device\|Video[_ ]*Adaptor\|Server[_ ]*Layout\|DRI\|Extensions\|Vendor\|Keyboard\|Pointer\|InputClass\)\"" end="^\s*EndSection\>" skip="#.*$\|\"[^\"]*\"" contains=xf86confComment,xf86confOption,xf86confKeyword,xf86confSectionError + syn region xf86confSection matchgroup=xf86confSectionDelim start="^\s*Section\s\+\"\(Files\|Server[_ ]*Flags\|Input[_ ]*Device\|Device\|Video[_ ]*Adaptor\|Server[_ ]*Layout\|DRI\|Extensions\|Vendor\|Keyboard\|Pointer\|InputClass\|OutputClass\)\"" end="^\s*EndSection\>" skip="#.*$\|\"[^\"]*\"" contains=xf86confComment,xf86confOption,xf86confKeyword,xf86confSectionError syn region xf86confSectionModule matchgroup=xf86confSectionDelim start="^\s*Section\s\+\"Module\"" end="^\s*EndSection\>" skip="#.*$\|\"[^\"]*\"" contains=xf86confSubsectionAny,xf86confComment,xf86confOption,xf86confKeyword syn region xf86confSectionMonitor matchgroup=xf86confSectionDelim start="^\s*Section\s\+\"Monitor\"" end="^\s*EndSection\>" skip="#.*$\|\"[^\"]*\"" contains=xf86confSubsectionMode,xf86confModeLine,xf86confComment,xf86confOption,xf86confKeyword syn region xf86confSectionModes matchgroup=xf86confSectionDelim start="^\s*Section\s\+\"Modes\"" end="^\s*EndSection\>" skip="#.*$\|\"[^\"]*\"" contains=xf86confSubsectionMode,xf86confModeLine,xf86confComment @@ -162,7 +162,7 @@ syn match xf86confSync "\(\s\+[+-][CHV]_*Sync\)\+" contained " Synchronization if b:xf86conf_xfree86_version >= 4 - syn sync match xf86confSyncSection grouphere xf86confSection "^\s*Section\s\+\"\(Files\|Server[_ ]*Flags\|Input[_ ]*Device\|Device\|Video[_ ]*Adaptor\|Server[_ ]*Layout\|DRI\|Extensions\|Vendor\|Keyboard\|Pointer\|InputClass\)\"" + syn sync match xf86confSyncSection grouphere xf86confSection "^\s*Section\s\+\"\(Files\|Server[_ ]*Flags\|Input[_ ]*Device\|Device\|Video[_ ]*Adaptor\|Server[_ ]*Layout\|DRI\|Extensions\|Vendor\|Keyboard\|Pointer\|InputClass\|OutputClass\)\"" syn sync match xf86confSyncSectionModule grouphere xf86confSectionModule "^\s*Section\s\+\"Module\"" syn sync match xf86confSyncSectionModes groupthere xf86confSectionModes "^\s*Section\s\+\"Modes\"" else diff --git a/runtime/syntax/zsh.vim b/runtime/syntax/zsh.vim index 084f8cdb41..04b39aeac0 100644 --- a/runtime/syntax/zsh.vim +++ b/runtime/syntax/zsh.vim @@ -2,7 +2,7 @@ " Language: Zsh shell script " Maintainer: Christian Brabandt <cb@256bit.org> " Previous Maintainer: Nikolai Weibull <now@bitwi.se> -" Latest Revision: 2022-07-26 +" Latest Revision: 2024 Jan 04 " License: Vim (see :h license) " Repository: https://github.com/chrisbra/vim-zsh @@ -48,8 +48,9 @@ syn match zshPOSIXQuoted '\\u[0-9a-fA-F]\{1,4}' syn match zshPOSIXQuoted '\\U[1-9a-fA-F]\{1,8}' syn region zshString matchgroup=zshStringDelimiter start=+"+ end=+"+ - \ contains=zshQuoted,@zshDerefs,@zshSubstQuoted fold + \ contains=@Spell,zshQuoted,@zshDerefs,@zshSubstQuoted fold syn region zshString matchgroup=zshStringDelimiter start=+'+ end=+'+ fold + \ contains=@Spell syn region zshPOSIXString matchgroup=zshStringDelimiter start=+\$'+ \ skip=+\\[\\']+ end=+'+ contains=zshPOSIXQuoted,zshQuoted syn match zshJobSpec '%\(\d\+\|?\=\w\+\|[%+-]\)' @@ -68,7 +69,7 @@ syn keyword zshConditional if then elif else fi esac select syn keyword zshCase case nextgroup=zshCaseWord skipwhite syn match zshCaseWord /\S\+/ nextgroup=zshCaseIn skipwhite contained transparent -syn keyword zshCaseIn in nextgroup=zshCasePattern skipwhite skipnl contained +syn keyword zshCaseIn in nextgroup=zshComment,zshCasePattern skipwhite skipnl contained syn match zshCasePattern /\S[^)]*)/ contained syn keyword zshRepeat while until repeat @@ -94,22 +95,24 @@ syn match zshRedir '|\@1<!|&\=|\@!' syn region zshHereDoc matchgroup=zshRedir \ start='<\@<!<<\s*\z([^<]\S*\)' - \ end='^\z1\>' - \ contains=@zshSubst,@zshDerefs,zshQuoted,zshPOSIXString + \ end='^\z1$' + \ contains=@Spell,@zshSubst,@zshDerefs,zshQuoted,zshPOSIXString syn region zshHereDoc matchgroup=zshRedir \ start='<\@<!<<\s*\\\z(\S\+\)' - \ end='^\z1\>' - \ contains=@zshSubst,@zshDerefs,zshQuoted,zshPOSIXString + \ end='^\z1$' + \ contains=@Spell syn region zshHereDoc matchgroup=zshRedir \ start='<\@<!<<-\s*\\\=\z(\S\+\)' - \ end='^\s*\z1\>' - \ contains=@zshSubst,@zshDerefs,zshQuoted,zshPOSIXString + \ end='^\t*\z1$' + \ contains=@Spell syn region zshHereDoc matchgroup=zshRedir \ start=+<\@<!<<\s*\(["']\)\z(\S\+\)\1+ - \ end='^\z1\>' + \ end='^\z1$' + \ contains=@Spell syn region zshHereDoc matchgroup=zshRedir \ start=+<\@<!<<-\s*\(["']\)\z(\S\+\)\1+ - \ end='^\s*\z1\>' + \ end='^\t*\z1$' + \ contains=@Spell syn match zshVariable '\<\h\w*' contained diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua index 34f1dc9e38..d200050fe1 100755 --- a/scripts/gen_vimdoc.lua +++ b/scripts/gen_vimdoc.lua @@ -350,12 +350,14 @@ local config = { helptag_fmt = function(name) if name:lower() == 'treesitter' then return 'lua-treesitter-core' + elseif name:lower() == 'query' then + return 'lua-treesitter-query' elseif name:lower() == 'tstree' then return { 'treesitter-tree', 'TSTree' } elseif name:lower() == 'tsnode' then return { 'treesitter-node', 'TSNode' } end - return 'lua-treesitter-' .. name:lower() + return 'treesitter-' .. name:lower() end, }, editorconfig = { diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index cce8ab5b25..767f5087c1 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -361,8 +361,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 ../vterm/*.c) -file(GLOB EXTERNAL_HEADERS CONFIGURE_DEPENDS ../xdiff/*.h ../mpack/*.h ../cjson/*.h ../klib/*.h ../vterm/*.h) +file(GLOB EXTERNAL_SOURCES CONFIGURE_DEPENDS ../xdiff/*.c ../mpack/*.c ../cjson/*.c ../klib/*.c) +file(GLOB EXTERNAL_HEADERS CONFIGURE_DEPENDS ../xdiff/*.h ../mpack/*.h ../cjson/*.h ../klib/*.h) file(GLOB NLUA0_SOURCES CONFIGURE_DEPENDS ../mpack/*.c) @@ -391,6 +391,7 @@ foreach(subdir msgpack_rpc tui tui/termkey + vterm event eval lua diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index 7e79133ed4..23e08bd3fe 100644 --- a/src/nvim/api/command.c +++ b/src/nvim/api/command.c @@ -931,6 +931,9 @@ void nvim_buf_del_user_command(Buffer buffer, String name, Error *err) gap = &ucmds; } else { buf_T *buf = find_buffer_by_handle(buffer, err); + if (ERROR_SET(err)) { + return; + } gap = &buf->b_ucmds; } diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index 48f5f7246c..40e89d230d 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -13,10 +13,11 @@ typedef struct { typedef struct { OptionalKeys is_set__set_decoration_provider_; - LuaRefOf(("start" _, Integer tick)) on_start; + LuaRefOf(("start" _, Integer tick), *Boolean) on_start; LuaRefOf(("buf" _, Integer bufnr, Integer tick)) on_buf; - LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow)) on_win; - LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row)) on_line; + LuaRefOf(("win" _, Integer winid, Integer bufnr, Integer toprow, Integer botrow), + *Boolean) on_win; + LuaRefOf(("line" _, Integer winid, Integer bufnr, Integer row), *Boolean) on_line; LuaRefOf(("end" _, Integer tick)) on_end; LuaRefOf(("hl_def" _)) _on_hl_def; LuaRefOf(("spell_nav" _)) _on_spell_nav; diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 25f44bb4eb..e3e69f4ff6 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -44,6 +44,7 @@ #include "nvim/highlight.h" #include "nvim/highlight_defs.h" #include "nvim/highlight_group.h" +#include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/log.h" #include "nvim/lua/executor.h" @@ -2255,9 +2256,13 @@ void nvim_error_event(uint64_t channel_id, Integer lvl, String data) /// @return Dict containing these keys: /// - winid: (number) floating window id /// - bufnr: (number) buffer id in floating window -Dict nvim__complete_set(Integer index, Dict(complete_set) *opts, Arena *arena) +Dict nvim__complete_set(Integer index, Dict(complete_set) *opts, Arena *arena, Error *err) { Dict rv = arena_dict(arena, 2); + if ((get_cot_flags() & kOptCotFlagPopup) == 0) { + api_set_error(err, kErrorTypeException, "completeopt option does not include popup"); + return rv; + } if (HAS_KEY(opts, complete_set, info)) { win_T *wp = pum_set_info((int)index, opts->info.data); if (wp) { diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 5dcf7d8f49..9e6877cbfa 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -3257,8 +3257,8 @@ void fileinfo(int fullname, int shorthelp, bool dont_truncate) n); validate_virtcol(curwin); size_t len = strlen(buffer); - col_print(buffer + len, IOSIZE - len, - (int)curwin->w_cursor.col + 1, (int)curwin->w_virtcol + 1); + (void)col_print(buffer + len, IOSIZE - len, + (int)curwin->w_cursor.col + 1, (int)curwin->w_virtcol + 1); } append_arg_number(curwin, buffer, IOSIZE); @@ -3286,13 +3286,13 @@ void fileinfo(int fullname, int shorthelp, bool dont_truncate) xfree(buffer); } -void col_print(char *buf, size_t buflen, int col, int vcol) +int col_print(char *buf, size_t buflen, int col, int vcol) { if (col == vcol) { - vim_snprintf(buf, buflen, "%d", col); - } else { - vim_snprintf(buf, buflen, "%d-%d", col, vcol); + return vim_snprintf(buf, buflen, "%d", col); } + + return vim_snprintf(buf, buflen, "%d-%d", col, vcol); } static char *lasttitle = NULL; @@ -3416,15 +3416,16 @@ void free_titles(void) /// Get relative cursor position in window into "buf[buflen]", in the localized /// percentage form like %99, 99%; using "Top", "Bot" or "All" when appropriate. -void get_rel_pos(win_T *wp, char *buf, int buflen) +int get_rel_pos(win_T *wp, char *buf, int buflen) { // Need at least 3 chars for writing. if (buflen < 3) { - return; + return 0; } linenr_T above; // number of lines above window linenr_T below; // number of lines below window + int len; above = wp->w_topline - 1; above += win_get_fill(wp, wp->w_topline) - wp->w_topfill; @@ -3435,25 +3436,24 @@ void get_rel_pos(win_T *wp, char *buf, int buflen) } below = wp->w_buffer->b_ml.ml_line_count - wp->w_botline + 1; if (below <= 0) { - xstrlcpy(buf, (above == 0 ? _("All") : _("Bot")), (size_t)buflen); + len = vim_snprintf(buf, (size_t)buflen, "%s", (above == 0) ? _("All") : _("Bot")); } else if (above <= 0) { - xstrlcpy(buf, _("Top"), (size_t)buflen); + len = vim_snprintf(buf, (size_t)buflen, "%s", _("Top")); } else { int perc = (above > 1000000 ? (above / ((above + below) / 100)) : (above * 100 / (above + below))); - - char *p = buf; - size_t l = (size_t)buflen; - if (perc < 10) { - // prepend one space - buf[0] = ' '; - p++; - l--; - } // localized percentage value - vim_snprintf(p, l, _("%d%%"), perc); + len = vim_snprintf(buf, (size_t)buflen, _("%s%d%%"), (perc < 10) ? " " : "", perc); } + if (len < 0) { + buf[0] = NUL; + len = 0; + } else if (len > buflen - 1) { + len = buflen - 1; + } + + return len; } /// Append (2 of 8) to "buf[buflen]", if editing more than one file. diff --git a/src/nvim/digraph.c b/src/nvim/digraph.c index 876d38ea83..326e929fb6 100644 --- a/src/nvim/digraph.c +++ b/src/nvim/digraph.c @@ -1714,6 +1714,7 @@ void listdigraphs(bool use_headers) { result_T previous = 0; + msg_ext_set_kind("list_cmd"); msg_putchar('\n'); const digr_T *dp = digraphdefault; @@ -2182,22 +2183,22 @@ static void keymap_unload(void) /// @param fmt format string containing one %s item /// @param buf buffer for the result /// @param len length of buffer -bool get_keymap_str(win_T *wp, char *fmt, char *buf, int len) +int get_keymap_str(win_T *wp, char *fmt, char *buf, int len) { char *p; if (wp->w_buffer->b_p_iminsert != B_IMODE_LMAP) { - return false; + return 0; } buf_T *old_curbuf = curbuf; win_T *old_curwin = curwin; + char to_evaluate[] = "b:keymap_name"; curbuf = wp->w_buffer; curwin = wp; - STRCPY(buf, "b:keymap_name"); // must be writable emsg_skip++; - char *s = p = eval_to_string(buf, false, false); + char *s = p = eval_to_string(to_evaluate, false, false); emsg_skip--; curbuf = old_curbuf; curwin = old_curwin; @@ -2208,9 +2209,12 @@ bool get_keymap_str(win_T *wp, char *fmt, char *buf, int len) p = "lang"; } } - if (vim_snprintf(buf, (size_t)len, fmt, p) > len - 1) { + int plen = vim_snprintf(buf, (size_t)len, fmt, p); + xfree(s); + if (plen < 0 || plen > len - 1) { buf[0] = NUL; + plen = 0; } - xfree(s); - return buf[0] != NUL; + + return plen; } diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index f8aeb02229..7f4de9eab8 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -1045,7 +1045,7 @@ int showmode(void) if (State & MODE_LANGMAP) { if (curwin->w_p_arab) { msg_puts_hl(_(" Arabic"), hl_id, false); - } else if (get_keymap_str(curwin, " (%s)", NameBuff, MAXPATHL)) { + } else if (get_keymap_str(curwin, " (%s)", NameBuff, MAXPATHL) > 0) { msg_puts_hl(NameBuff, hl_id, false); } } @@ -1936,7 +1936,7 @@ static void win_update(win_T *wp) pos.lnum += cursor_above ? 1 : -1) { colnr_T t; - pos.col = (colnr_T)strlen(ml_get_buf(wp->w_buffer, pos.lnum)); + pos.col = ml_get_buf_len(wp->w_buffer, pos.lnum); getvvcol(wp, &pos, NULL, NULL, &t); toc = MAX(toc, t); } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 8bdd8dad4c..a90f275713 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -270,6 +270,7 @@ static struct vimvar { VV(VV_COLLATE, "collate", VAR_STRING, VV_RO), VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO), VV(VV_MAXCOL, "maxcol", VAR_NUMBER, VV_RO), + VV(VV_STACKTRACE, "stacktrace", VAR_LIST, VV_RO), // Neovim VV(VV_STDERR, "stderr", VAR_NUMBER, VV_RO), VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO), diff --git a/src/nvim/eval.h b/src/nvim/eval.h index bb9b00abc7..8b4aa8101a 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -167,6 +167,7 @@ typedef enum { VV_COLLATE, VV_EXITING, VV_MAXCOL, + VV_STACKTRACE, // Nvim VV_STDERR, VV_MSGPACK_TYPES, diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index c650dee306..4ce1960dcf 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -4670,6 +4670,25 @@ M.funcs = { returns = 'vim.fn.getscriptinfo.ret[]', signature = 'getscriptinfo([{opts}])', }, + getstacktrace = { + args = 0, + desc = [=[ + Returns the current stack trace of Vim scripts. + Stack trace is a |List|, of which each item is a |Dictionary| + with the following items: + funcref The funcref if the stack is at a function, + otherwise this item is omitted. + event The string of the event description if the + stack is at an autocmd event, otherwise this + item is omitted. + lnum The line number in the script on the stack. + filepath The file path of the script on the stack. + ]=], + name = 'getstacktrace', + params = {}, + returns = 'table[]', + signature = 'getstacktrace()', + }, gettabinfo = { args = { 0, 1 }, base = 1, diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index cbb6b5644f..ed1031577c 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -2633,6 +2633,30 @@ int tv_dict_add_allocated_str(dict_T *const d, const char *const key, const size return OK; } +/// Add a function entry to dictionary. +/// +/// @param[out] d Dictionary to add entry to. +/// @param[in] key Key to add. +/// @param[in] key_len Key length. +/// @param[in] fp Function to add. +/// +/// @return OK in case of success, FAIL when key already exists. +int tv_dict_add_func(dict_T *const d, const char *const key, const size_t key_len, + ufunc_T *const fp) + FUNC_ATTR_NONNULL_ARG(1, 2, 4) +{ + dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); + + item->di_tv.v_type = VAR_FUNC; + item->di_tv.vval.v_string = xstrdup(fp->uf_name); + if (tv_dict_add(d, item) == FAIL) { + tv_dict_item_free(item); + return FAIL; + } + func_ref(item->di_tv.vval.v_string); + return OK; +} + //{{{2 Operations on the whole dict /// Clear all the keys of a Dictionary. "d" remains a valid empty Dictionary. diff --git a/src/nvim/ex_eval.c b/src/nvim/ex_eval.c index f9936dd88e..18c691d076 100644 --- a/src/nvim/ex_eval.c +++ b/src/nvim/ex_eval.c @@ -479,6 +479,9 @@ static int throw_exception(void *value, except_type_T type, char *cmdname) excp->throw_lnum = SOURCING_LNUM; } + excp->stacktrace = stacktrace_create(); + tv_list_ref(excp->stacktrace); + if (p_verbose >= 13 || debug_break_level > 0) { int save_msg_silent = msg_silent; @@ -563,6 +566,7 @@ static void discard_exception(except_T *excp, bool was_finished) free_msglist(excp->messages); } xfree(excp->throw_name); + tv_list_unref(excp->stacktrace); xfree(excp); } @@ -584,6 +588,7 @@ static void catch_exception(except_T *excp) excp->caught = caught_stack; caught_stack = excp; set_vim_var_string(VV_EXCEPTION, excp->value, -1); + set_vim_var_list(VV_STACKTRACE, excp->stacktrace); if (*excp->throw_name != NUL) { if (excp->throw_lnum != 0) { vim_snprintf(IObuff, IOSIZE, _("%s, line %" PRId64), @@ -633,6 +638,7 @@ static void finish_exception(except_T *excp) caught_stack = caught_stack->caught; if (caught_stack != NULL) { set_vim_var_string(VV_EXCEPTION, caught_stack->value, -1); + set_vim_var_list(VV_STACKTRACE, caught_stack->stacktrace); if (*caught_stack->throw_name != NUL) { if (caught_stack->throw_lnum != 0) { vim_snprintf(IObuff, IOSIZE, @@ -651,6 +657,7 @@ static void finish_exception(except_T *excp) } else { set_vim_var_string(VV_EXCEPTION, NULL, -1); set_vim_var_string(VV_THROWPOINT, NULL, -1); + set_vim_var_list(VV_STACKTRACE, NULL); } // Discard the exception, but use the finish message for 'verbose'. diff --git a/src/nvim/ex_eval_defs.h b/src/nvim/ex_eval_defs.h index 3f5e510a20..e0d06f3e93 100644 --- a/src/nvim/ex_eval_defs.h +++ b/src/nvim/ex_eval_defs.h @@ -2,6 +2,7 @@ #include <stdbool.h> +#include "nvim/eval/typval_defs.h" #include "nvim/pos_defs.h" /// A list used for saving values of "emsg_silent". Used by ex_try() to save the @@ -107,6 +108,7 @@ struct vim_exception { msglist_T *messages; ///< message(s) causing error exception char *throw_name; ///< name of the throw point linenr_T throw_lnum; ///< line number of the throw point + list_T *stacktrace; ///< stacktrace except_T *caught; ///< next exception on the caught stack }; diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 09006484ca..0b5d0864e5 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -400,7 +400,7 @@ static bool do_incsearch_highlighting(int firstc, int *search_delim, incsearch_s parse_cmd_address(&ea, &dummy, true); if (ea.addr_count > 0) { // Allow for reverse match. - search_first_line = MIN(ea.line1, ea.line1); + search_first_line = MIN(ea.line2, ea.line1); search_last_line = MAX(ea.line2, ea.line1); } else if (cmd[0] == 's' && cmd[1] != 'o') { // :s defaults to the current line @@ -2815,8 +2815,10 @@ char *getcmdline_prompt(const int firstc, const char *const prompt, const int hl ccline.one_key = one_key; ccline.mouse_used = mouse_used; + const bool cmd_silent_saved = cmd_silent; int msg_silent_saved = msg_silent; msg_silent = 0; + cmd_silent = false; // Want to see the prompt. char *const ret = (char *)command_line_enter(firstc, 1, 0, false); ccline.redraw_state = kCmdRedrawNone; @@ -2825,6 +2827,7 @@ char *getcmdline_prompt(const int firstc, const char *const prompt, const int hl restore_cmdline(&save_ccline); } msg_silent = msg_silent_saved; + cmd_silent = cmd_silent_saved; // Restore msg_col, the prompt from input() may have changed it. // But only if called recursively and the commandline is therefore being // restored to an old one; if not, the input() prompt stays on the screen, @@ -4792,9 +4795,6 @@ void get_user_input(const typval_T *const argvars, typval_T *const rettv, const } } - const bool cmd_silent_save = cmd_silent; - - cmd_silent = false; // Want to see the prompt. // Only the part of the message after the last NL is considered as // prompt for the command line, unlsess cmdline is externalized const char *p = prompt; @@ -4829,5 +4829,4 @@ void get_user_input(const typval_T *const argvars, typval_T *const rettv, const // Since the user typed this, no need to wait for return. need_wait_return = false; msg_didout = false; - cmd_silent = cmd_silent_save; } diff --git a/src/nvim/generators/gen_vimvim.lua b/src/nvim/generators/gen_vimvim.lua index 0675f04b73..d8053822bf 100644 --- a/src/nvim/generators/gen_vimvim.lua +++ b/src/nvim/generators/gen_vimvim.lua @@ -52,11 +52,13 @@ local function is_special_cased_cmd(cmd) end local vimcmd_start = 'syn keyword vimCommand contained ' +local vimcmd_end = ' nextgroup=vimBang' w(vimcmd_start) + local prev_cmd = nil for _, cmd_desc in ipairs(ex_cmds.cmds) do if lld.line_length > 850 then - w('\n' .. vimcmd_start) + w(vimcmd_end .. '\n' .. vimcmd_start) end local cmd = cmd_desc.command if cmd:match('%w') and cmd ~= 'z' and not is_special_cased_cmd(cmd) then @@ -79,9 +81,11 @@ for _, cmd_desc in ipairs(ex_cmds.cmds) do prev_cmd = cmd end +w(vimcmd_end .. '\n') + local vimopt_start = 'syn keyword vimOption contained ' local vimopt_end = ' skipwhite nextgroup=vimSetEqual,vimSetMod' -w('\n\n' .. vimopt_start) +w('\n' .. vimopt_start) for _, opt_desc in ipairs(options.options) do if not opt_desc.immutable then diff --git a/src/nvim/grid.c b/src/nvim/grid.c index e863cb3476..df93ad1655 100644 --- a/src/nvim/grid.c +++ b/src/nvim/grid.c @@ -383,7 +383,8 @@ void grid_line_start(ScreenGrid *grid, int row) assert((size_t)grid_line_maxcol <= linebuf_size); - if (rdb_flags & kOptRdbFlagInvalid) { + if (full_screen && (rdb_flags & kOptRdbFlagInvalid)) { + assert(linebuf_char); // Current batch must not depend on previous contents of linebuf_char. // Set invalid values which will cause assertion failures later if they are used. memset(linebuf_char, 0xFF, sizeof(schar_T) * linebuf_size); diff --git a/src/nvim/highlight_group.c b/src/nvim/highlight_group.c index 6fd0b858a8..ad4b2732f6 100644 --- a/src/nvim/highlight_group.c +++ b/src/nvim/highlight_group.c @@ -150,7 +150,7 @@ static const char *highlight_init_both[] = { "PmenuMatchSel gui=bold cterm=bold", "PmenuSel gui=reverse cterm=reverse,underline blend=0", "RedrawDebugNormal gui=reverse cterm=reverse", - "TabLineSel gui=bold cterm=bold", + "TabLineSel gui=bold cterm=NONE", "TermCursor gui=reverse cterm=reverse", "Underlined gui=underline cterm=underline", "lCursor guifg=bg guibg=fg", diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 9b77644085..419c806592 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -10,6 +10,7 @@ #include <string.h> #include "klib/kvec.h" +#include "nvim/api/private/helpers.h" #include "nvim/ascii_defs.h" #include "nvim/autocmd.h" #include "nvim/autocmd_defs.h" @@ -156,7 +157,7 @@ struct compl_S { compl_T *cp_next; compl_T *cp_prev; compl_T *cp_match_next; ///< matched next compl_T - char *cp_str; ///< matched text + String cp_str; ///< matched text char *(cp_text[CPT_COUNT]); ///< text for the menu typval_T cp_user_data; char *cp_fname; ///< file containing the match, allocated when @@ -220,7 +221,7 @@ static bool compl_enter_selects = false; /// When "compl_leader" is not NULL only matches that start with this string /// are used. -static char *compl_leader = NULL; +static String compl_leader = STRING_INIT; static bool compl_get_longest = false; ///< put longest common string in compl_leader @@ -245,8 +246,7 @@ static bool compl_started = false; static int ctrl_x_mode = CTRL_X_NORMAL; static int compl_matches = 0; ///< number of completion matches -static char *compl_pattern = NULL; -static size_t compl_patternlen = 0; +static String compl_pattern = STRING_INIT; static Direction compl_direction = FORWARD; static Direction compl_shows_dir = FORWARD; static int compl_pending = 0; ///< > 1 for postponed CTRL-N @@ -257,8 +257,8 @@ static int compl_length = 0; static colnr_T compl_col = 0; ///< column where the text starts ///< that is being completed static colnr_T compl_ins_end_col = 0; -static char *compl_orig_text = NULL; ///< text as it was before - ///< completion started +static String compl_orig_text = STRING_INIT; ///< text as it was before + ///< completion started /// Undo information to restore extmarks for original text. static extmark_undo_vec_t compl_orig_extmarks; static int compl_cont_mode = 0; @@ -639,7 +639,7 @@ static char *ins_compl_infercase_gettext(const char *str, int char_len, int comp // Rule 1: Were any chars converted to lower? { - const char *p = compl_orig_text; + const char *p = compl_orig_text.data; for (int i = 0; i < min_len; i++) { const int c = mb_ptr2char_adv(&p); if (mb_islower(c)) { @@ -659,7 +659,7 @@ static char *ins_compl_infercase_gettext(const char *str, int char_len, int comp // upper case. if (!has_lower) { bool was_letter = false; - const char *p = compl_orig_text; + const char *p = compl_orig_text.data; for (int i = 0; i < min_len; i++) { const int c = mb_ptr2char_adv(&p); if (was_letter && mb_isupper(c) && mb_islower(wca[i])) { @@ -675,7 +675,7 @@ static char *ins_compl_infercase_gettext(const char *str, int char_len, int comp // Copy the original case of the part we typed. { - const char *p = compl_orig_text; + const char *p = compl_orig_text.data; for (int i = 0; i < min_len; i++) { const int c = mb_ptr2char_adv(&p); if (mb_islower(c)) { @@ -705,7 +705,7 @@ static char *ins_compl_infercase_gettext(const char *str, int char_len, int comp ga_grow(&gap, IOSIZE); *p = NUL; STRCPY(gap.ga_data, IObuff); - gap.ga_len = (int)strlen(IObuff); + gap.ga_len = (int)(p - IObuff); } else { p += utf_char2bytes(wca[i++], p); } @@ -752,7 +752,7 @@ int ins_compl_add_infercase(char *str_arg, int len, bool icase, char *fname, Dir // Find actual length of original text. { - const char *p = compl_orig_text; + const char *p = compl_orig_text.data; compl_char_len = 0; while (*p != NUL) { MB_PTR_ADV(p); @@ -841,8 +841,8 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons match = compl_first_match; do { if (!match_at_original_text(match) - && strncmp(match->cp_str, str, (size_t)len) == 0 - && ((int)strlen(match->cp_str) <= len || match->cp_str[len] == NUL)) { + && strncmp(match->cp_str.data, str, (size_t)len) == 0 + && ((int)match->cp_str.size <= len || match->cp_str.data[len] == NUL)) { if (cptext_allocated) { free_cptext(cptext); } @@ -862,7 +862,7 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons if (flags & CP_ORIGINAL_TEXT) { match->cp_number = 0; } - match->cp_str = xstrnsave(str, (size_t)len); + match->cp_str = cbuf_to_string(str, (size_t)len); // match-fname is: // - compl_curr_match->cp_fname if it is a string equal to fname. @@ -944,9 +944,9 @@ static bool ins_compl_equal(compl_T *match, char *str, size_t len) return true; } if (match->cp_flags & CP_ICASE) { - return STRNICMP(match->cp_str, str, len) == 0; + return STRNICMP(match->cp_str.data, str, len) == 0; } - return strncmp(match->cp_str, str, len) == 0; + return strncmp(match->cp_str.data, str, len) == 0; } /// when len is -1 mean use whole length of p otherwise part of p @@ -976,13 +976,13 @@ int ins_compl_col_range_attr(int col) /// Reduce the longest common string for match "match". static void ins_compl_longest_match(compl_T *match) { - if (compl_leader == NULL) { + if (compl_leader.data == NULL) { // First match, use it as a whole. - compl_leader = xstrdup(match->cp_str); + compl_leader = copy_string(match->cp_str, NULL); bool had_match = (curwin->w_cursor.col > compl_col); ins_compl_delete(false); - ins_compl_insert_bytes(compl_leader + get_compl_len(), -1); + ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); ins_redraw(false); // When the match isn't there (to avoid matching itself) remove it @@ -996,8 +996,8 @@ static void ins_compl_longest_match(compl_T *match) } // Reduce the text if this match differs from compl_leader. - char *p = compl_leader; - char *s = match->cp_str; + char *p = compl_leader.data; + char *s = match->cp_str.data; while (*p != NUL) { int c1 = utf_ptr2char(p); int c2 = utf_ptr2char(s); @@ -1014,9 +1014,11 @@ static void ins_compl_longest_match(compl_T *match) if (*p != NUL) { // Leader was shortened, need to change the inserted text. *p = NUL; + compl_leader.size = (size_t)(p - compl_leader.data); + bool had_match = (curwin->w_cursor.col > compl_col); ins_compl_delete(false); - ins_compl_insert_bytes(compl_leader + get_compl_len(), -1); + ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); ins_redraw(false); // When the match isn't there (to avoid matching itself) remove it @@ -1078,8 +1080,8 @@ bool ins_compl_has_shown_match(void) /// Return whether the shown match is long enough. bool ins_compl_long_shown_match(void) { - return compl_shown_match != NULL && compl_shown_match->cp_str != NULL - && (colnr_T)strlen(compl_shown_match->cp_str) > curwin->w_cursor.col - compl_col; + return compl_shown_match != NULL && compl_shown_match->cp_str.data != NULL + && (colnr_T)compl_shown_match->cp_str.size > curwin->w_cursor.col - compl_col; } /// Get the local or global value of 'completeopt' flags. @@ -1134,7 +1136,7 @@ static dict_T *ins_compl_dict_alloc(compl_T *match) { // { word, abbr, menu, kind, info } dict_T *dict = tv_dict_alloc_lock(VAR_FIXED); - tv_dict_add_str(dict, S_LEN("word"), match->cp_str); + tv_dict_add_str(dict, S_LEN("word"), match->cp_str.data); tv_dict_add_str(dict, S_LEN("abbr"), match->cp_text[CPT_ABBR]); tv_dict_add_str(dict, S_LEN("menu"), match->cp_text[CPT_MENU]); tv_dict_add_str(dict, S_LEN("kind"), match->cp_text[CPT_KIND]); @@ -1203,7 +1205,6 @@ static int ins_compl_build_pum(void) XFREE_CLEAR(compl_leader); } - const int lead_len = compl_leader != NULL ? (int)strlen(compl_leader) : 0; int max_fuzzy_score = 0; unsigned cur_cot_flags = get_cot_flags(); bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0; @@ -1214,7 +1215,7 @@ static int ins_compl_build_pum(void) // match after it, don't highlight anything. bool shown_match_ok = match_at_original_text(compl_shown_match); - if (strequal(compl_leader, compl_orig_text) && !shown_match_ok) { + if (strequal(compl_leader.data, compl_orig_text.data) && !shown_match_ok) { compl_shown_match = compl_no_select ? compl_first_match : compl_first_match->cp_next; } @@ -1227,13 +1228,13 @@ static int ins_compl_build_pum(void) comp->cp_in_match_array = false; // When 'completeopt' contains "fuzzy" and leader is not NULL or empty, // set the cp_score for later comparisons. - if (compl_fuzzy_match && compl_leader != NULL && lead_len > 0) { - comp->cp_score = fuzzy_match_str(comp->cp_str, compl_leader); + if (compl_fuzzy_match && compl_leader.data != NULL && compl_leader.size > 0) { + comp->cp_score = fuzzy_match_str(comp->cp_str.data, compl_leader.data); } if (!match_at_original_text(comp) - && (compl_leader == NULL - || ins_compl_equal(comp, compl_leader, (size_t)lead_len) + && (compl_leader.data == NULL + || ins_compl_equal(comp, compl_leader.data, compl_leader.size) || (compl_fuzzy_match && comp->cp_score > 0))) { compl_match_arraysize++; comp->cp_in_match_array = true; @@ -1305,7 +1306,7 @@ static int ins_compl_build_pum(void) comp = match_head; while (comp != NULL) { compl_match_array[i].pum_text = comp->cp_text[CPT_ABBR] != NULL - ? comp->cp_text[CPT_ABBR] : comp->cp_str; + ? comp->cp_text[CPT_ABBR] : comp->cp_str.data; compl_match_array[i].pum_kind = comp->cp_text[CPT_KIND]; compl_match_array[i].pum_info = comp->cp_text[CPT_INFO]; compl_match_array[i].pum_score = comp->cp_score; @@ -1318,7 +1319,7 @@ static int ins_compl_build_pum(void) comp = match_next; } - if (compl_fuzzy_match && compl_leader != NULL && lead_len > 0) { + if (compl_fuzzy_match && compl_leader.data != NULL && compl_leader.size > 0) { for (i = 0; i < compl_match_arraysize; i++) { compl_match_array[i].pum_idx = i; } @@ -1356,7 +1357,7 @@ void ins_compl_show_pum(void) } else { // popup menu already exists, only need to find the current item. for (int i = 0; i < compl_match_arraysize; i++) { - if (compl_match_array[i].pum_text == compl_shown_match->cp_str + if (compl_match_array[i].pum_text == compl_shown_match->cp_str.data || compl_match_array[i].pum_text == compl_shown_match->cp_text[CPT_ABBR]) { cur = i; break; @@ -1425,7 +1426,13 @@ bool compl_match_curr_select(int selected) /// Get current completion leader char *ins_compl_leader(void) { - return compl_leader != NULL ? compl_leader : compl_orig_text; + return compl_leader.data != NULL ? compl_leader.data : compl_orig_text.data; +} + +/// Get current completion leader length +size_t ins_compl_leader_len(void) +{ + return compl_leader.data != NULL ? compl_leader.size : compl_orig_text.size; } /// Add any identifiers that match the given pattern "pat" in the list of @@ -1671,9 +1678,8 @@ static char *find_line_end(char *ptr) /// Free the list of completions static void ins_compl_free(void) { - XFREE_CLEAR(compl_pattern); - compl_patternlen = 0; - XFREE_CLEAR(compl_leader); + API_CLEAR_STRING(compl_pattern); + API_CLEAR_STRING(compl_leader); if (compl_first_match == NULL) { return; @@ -1686,7 +1692,7 @@ static void ins_compl_free(void) do { compl_T *match = compl_curr_match; compl_curr_match = compl_curr_match->cp_next; - xfree(match->cp_str); + API_CLEAR_STRING(match->cp_str); // several entries may use the same fname, free it just once. if (match->cp_flags & CP_FREE_FNAME) { xfree(match->cp_fname); @@ -1707,12 +1713,11 @@ void ins_compl_clear(void) compl_started = false; compl_matches = 0; compl_ins_end_col = 0; - XFREE_CLEAR(compl_pattern); - compl_patternlen = 0; - XFREE_CLEAR(compl_leader); + API_CLEAR_STRING(compl_pattern); + API_CLEAR_STRING(compl_leader); edit_submode_extra = NULL; kv_destroy(compl_orig_extmarks); - XFREE_CLEAR(compl_orig_text); + API_CLEAR_STRING(compl_orig_text); compl_enter_selects = false; // clear v:completed_item set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc_lock(VAR_FIXED)); @@ -1802,8 +1807,9 @@ int ins_compl_bs(void) // TODO(bfredl): get rid of random update_screen() calls deep inside completion logic line = get_cursor_line_ptr(); - xfree(compl_leader); - compl_leader = xstrnsave(line + compl_col, (size_t)(p_off - (ptrdiff_t)compl_col)); + API_CLEAR_STRING(compl_leader); + compl_leader = cbuf_to_string(line + compl_col, + (size_t)(p_off - (ptrdiff_t)compl_col)); ins_compl_new_leader(); if (compl_shown_match != NULL) { @@ -1837,11 +1843,11 @@ static void ins_compl_new_leader(void) { ins_compl_del_pum(); ins_compl_delete(true); - ins_compl_insert_bytes(compl_leader + get_compl_len(), -1); + ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); compl_used_match = false; if (compl_started) { - ins_compl_set_original_text(compl_leader); + ins_compl_set_original_text(compl_leader.data, compl_leader.size); } else { spell_bad_len = 0; // need to redetect bad word // Matches were cleared, need to search for them now. @@ -1901,9 +1907,9 @@ void ins_compl_addleader(int c) ins_compl_restart(); } - xfree(compl_leader); - compl_leader = xstrnsave(get_cursor_line_ptr() + compl_col, - (size_t)(curwin->w_cursor.col - compl_col)); + API_CLEAR_STRING(compl_leader); + compl_leader = cbuf_to_string(get_cursor_line_ptr() + compl_col, + (size_t)(curwin->w_cursor.col - compl_col)); ins_compl_new_leader(); } @@ -1923,19 +1929,19 @@ static void ins_compl_restart(void) } /// Set the first match, the original text. -static void ins_compl_set_original_text(char *str) +static void ins_compl_set_original_text(char *str, size_t len) FUNC_ATTR_NONNULL_ALL { // Replace the original text entry. // The CP_ORIGINAL_TEXT flag is either at the first item or might possibly // be at the last item for backward completion if (match_at_original_text(compl_first_match)) { // safety check - xfree(compl_first_match->cp_str); - compl_first_match->cp_str = xstrdup(str); + API_CLEAR_STRING(compl_first_match->cp_str); + compl_first_match->cp_str = cbuf_to_string(str, len); } else if (compl_first_match->cp_prev != NULL && match_at_original_text(compl_first_match->cp_prev)) { - xfree(compl_first_match->cp_prev->cp_str); - compl_first_match->cp_prev->cp_str = xstrdup(str); + API_CLEAR_STRING(compl_first_match->cp_prev->cp_str); + compl_first_match->cp_prev->cp_str = cbuf_to_string(str, len); } } @@ -1945,8 +1951,8 @@ void ins_compl_addfrommatch(void) { int len = (int)curwin->w_cursor.col - (int)compl_col; assert(compl_shown_match != NULL); - char *p = compl_shown_match->cp_str; - if ((int)strlen(p) <= len) { // the match is too short + char *p = compl_shown_match->cp_str.data; + if ((int)compl_shown_match->cp_str.size <= len) { // the match is too short // When still at the original match use the first entry that matches // the leader. if (!match_at_original_text(compl_shown_match)) { @@ -1954,15 +1960,17 @@ void ins_compl_addfrommatch(void) } p = NULL; + size_t plen = 0; for (compl_T *cp = compl_shown_match->cp_next; cp != NULL && !is_first_match(cp); cp = cp->cp_next) { - if (compl_leader == NULL - || ins_compl_equal(cp, compl_leader, strlen(compl_leader))) { - p = cp->cp_str; + if (compl_leader.data == NULL + || ins_compl_equal(cp, compl_leader.data, compl_leader.size)) { + p = cp->cp_str.data; + plen = cp->cp_str.size; break; } } - if (p == NULL || (int)strlen(p) <= len) { + if (p == NULL || (int)plen <= len) { return; } } @@ -2102,7 +2110,7 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval) // Get here when we have finished typing a sequence of ^N and // ^P or other completion characters in CTRL-X mode. Free up // memory that was used, and make sure we can redo the insert. - if (compl_curr_match != NULL || compl_leader != NULL || c == Ctrl_E) { + if (compl_curr_match != NULL || compl_leader.data != NULL || c == Ctrl_E) { // If any of the original typed text has been changed, eg when // ignorecase is set, we must add back-spaces to the redo // buffer. We add as few as necessary to delete just the part @@ -2111,7 +2119,7 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval) // CTRL-E then don't use the current match. char *ptr; if (compl_curr_match != NULL && compl_used_match && c != Ctrl_E) { - ptr = compl_curr_match->cp_str; + ptr = compl_curr_match->cp_str.data; } else { ptr = NULL; } @@ -2154,7 +2162,7 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval) if ((c == Ctrl_Y || (compl_enter_selects && (c == CAR || c == K_KENTER || c == NL))) && pum_visible()) { - word = xstrdup(compl_shown_match->cp_str); + word = xstrdup(compl_shown_match->cp_str.data); retval = true; // May need to remove ComplMatchIns highlight. redrawWinline(curwin, curwin->w_cursor.lnum); @@ -2165,16 +2173,18 @@ static bool ins_compl_stop(const int c, const int prev_mode, bool retval) if (c == Ctrl_E) { ins_compl_delete(false); char *p = NULL; - if (compl_leader != NULL) { - p = compl_leader; + size_t plen = 0; + if (compl_leader.data != NULL) { + p = compl_leader.data; + plen = compl_leader.size; } else if (compl_first_match != NULL) { - p = compl_orig_text; + p = compl_orig_text.data; + plen = compl_orig_text.size; } if (p != NULL) { const int compl_len = get_compl_len(); - const int len = (int)strlen(p); - if (len > compl_len) { - ins_compl_insert_bytes(p + compl_len, len - compl_len); + if ((int)plen > compl_len) { + ins_compl_insert_bytes(p + compl_len, (int)plen - compl_len); } } restore_orig_extmarks(); @@ -2324,18 +2334,18 @@ bool ins_compl_prep(int c) /// "ptr" is the known leader text or NUL. static void ins_compl_fixRedoBufForLeader(char *ptr_arg) { - int len; + int len = 0; char *ptr = ptr_arg; if (ptr == NULL) { - if (compl_leader != NULL) { - ptr = compl_leader; + if (compl_leader.data != NULL) { + ptr = compl_leader.data; } else { return; // nothing to do } } - if (compl_orig_text != NULL) { - char *p = compl_orig_text; + if (compl_orig_text.data != NULL) { + char *p = compl_orig_text.data; for (len = 0; p[len] != NUL && p[len] == ptr[len]; len++) {} if (len > 0) { len -= utf_head_off(p, p + len); @@ -2343,8 +2353,6 @@ static void ins_compl_fixRedoBufForLeader(char *ptr_arg) for (p += len; *p != NUL; MB_PTR_ADV(p)) { AppendCharToRedobuff(K_BS); } - } else { - len = 0; } AppendToRedobuffLit(ptr + len, -1); } @@ -2730,12 +2738,14 @@ static void set_completion(colnr_T startcol, list_T *list) compl_col = startcol; compl_length = curwin->w_cursor.col - startcol; // compl_pattern doesn't need to be set - compl_orig_text = xstrnsave(get_cursor_line_ptr() + compl_col, (size_t)compl_length); + compl_orig_text = cbuf_to_string(get_cursor_line_ptr() + compl_col, + (size_t)compl_length); save_orig_extmarks(); if (p_ic) { flags |= CP_ICASE; } - if (ins_compl_add(compl_orig_text, -1, NULL, NULL, false, NULL, 0, + if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size, + NULL, NULL, false, NULL, 0, flags | CP_FAST, false, NULL) != OK) { return; } @@ -2940,7 +2950,7 @@ static void get_complete_info(list_T *what_list, dict_T *retdict) if (has_items || (has_matches && match->cp_in_match_array)) { dict_T *di = tv_dict_alloc(); tv_list_append_dict(li, di); - tv_dict_add_str(di, S_LEN("word"), match->cp_str); + tv_dict_add_str(di, S_LEN("word"), match->cp_str.data); tv_dict_add_str(di, S_LEN("abbr"), match->cp_text[CPT_ABBR]); tv_dict_add_str(di, S_LEN("menu"), match->cp_text[CPT_MENU]); tv_dict_add_str(di, S_LEN("kind"), match->cp_text[CPT_KIND]); @@ -3137,8 +3147,8 @@ done: /// included files. static void get_next_include_file_completion(int compl_type) { - find_pattern_in_path(compl_pattern, compl_direction, - compl_patternlen, false, false, + find_pattern_in_path(compl_pattern.data, compl_direction, + compl_pattern.size, false, false, ((compl_type == CTRL_X_PATH_DEFINES && !(compl_cont_status & CONT_SOL)) ? FIND_DEFINE : FIND_ANY), @@ -3150,14 +3160,14 @@ static void get_next_include_file_completion(int compl_type) static void get_next_dict_tsr_completion(int compl_type, char *dict, int dict_f) { if (thesaurus_func_complete(compl_type)) { - expand_by_function(compl_type, compl_pattern); + expand_by_function(compl_type, compl_pattern.data); } else { ins_compl_dictionaries(dict != NULL ? dict : (compl_type == CTRL_X_THESAURUS ? (*curbuf->b_p_tsr == NUL ? p_tsr : curbuf->b_p_tsr) : (*curbuf->b_p_dict == NUL ? p_dict : curbuf->b_p_dict)), - compl_pattern, + compl_pattern.data, dict != NULL ? dict_f : 0, compl_type == CTRL_X_THESAURUS); } @@ -3168,14 +3178,14 @@ static void get_next_tag_completion(void) { // set p_ic according to p_ic, p_scs and pat for find_tags(). const int save_p_ic = p_ic; - p_ic = ignorecase(compl_pattern); + p_ic = ignorecase(compl_pattern.data); // Find up to TAG_MANY matches. Avoids that an enormous number // of matches is found when compl_pattern is empty g_tag_at_cursor = true; char **matches; int num_matches; - if (find_tags(compl_pattern, &num_matches, &matches, + if (find_tags(compl_pattern.data, &num_matches, &matches, TAG_REGEXP | TAG_NAMES | TAG_NOIC | TAG_INS_COMP | (ctrl_x_mode_not_default() ? TAG_VERBOSE : 0), TAG_MANY, curbuf->b_ffname) == OK && num_matches > 0) { @@ -3190,13 +3200,13 @@ static void get_next_filename_completion(void) { char **matches; int num_matches; - if (expand_wildcards(1, &compl_pattern, &num_matches, &matches, + if (expand_wildcards(1, &compl_pattern.data, &num_matches, &matches, EW_FILE|EW_DIR|EW_ADDSLASH|EW_SILENT) != OK) { return; } // May change home directory back to "~". - tilde_replace(compl_pattern, num_matches, matches); + tilde_replace(compl_pattern.data, num_matches, matches); #ifdef BACKSLASH_IN_FILENAME if (curbuf->b_p_csl[0] != NUL) { for (int i = 0; i < num_matches; i++) { @@ -3220,8 +3230,8 @@ static void get_next_cmdline_completion(void) { char **matches; int num_matches; - if (expand_cmdline(&compl_xp, compl_pattern, - (int)compl_patternlen, &num_matches, &matches) == EXPAND_OK) { + if (expand_cmdline(&compl_xp, compl_pattern.data, + (int)compl_pattern.size, &num_matches, &matches) == EXPAND_OK) { ins_compl_add_matches(num_matches, matches, false); } } @@ -3230,7 +3240,7 @@ static void get_next_cmdline_completion(void) static void get_next_spell_completion(linenr_T lnum) { char **matches; - int num_matches = expand_spelling(lnum, compl_pattern, &matches); + int num_matches = expand_spelling(lnum, compl_pattern.data, &matches); if (num_matches > 0) { ins_compl_add_matches(num_matches, matches, p_ic); } else { @@ -3249,22 +3259,24 @@ static char *ins_compl_get_next_word_or_line(buf_T *ins_buf, pos_T *cur_match_po { *match_len = 0; char *ptr = ml_get_buf(ins_buf, cur_match_pos->lnum) + cur_match_pos->col; - int len; + int len = ml_get_buf_len(ins_buf, cur_match_pos->lnum) - cur_match_pos->col; if (ctrl_x_mode_line_or_eval()) { if (compl_status_adding()) { if (cur_match_pos->lnum >= ins_buf->b_ml.ml_line_count) { return NULL; } ptr = ml_get_buf(ins_buf, cur_match_pos->lnum + 1); + len = ml_get_buf_len(ins_buf, cur_match_pos->lnum + 1); if (!p_paste) { - ptr = skipwhite(ptr); + char *tmp_ptr = ptr; + ptr = skipwhite(tmp_ptr); + len -= (int)(ptr - tmp_ptr); } } - len = (int)strlen(ptr); } else { char *tmp_ptr = ptr; - if (compl_status_adding() && compl_length <= (int)strlen(tmp_ptr)) { + if (compl_status_adding() && compl_length <= len) { tmp_ptr += compl_length; // Skip if already inside a word. if (vim_iswordp(tmp_ptr)) { @@ -3360,10 +3372,11 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ // has added a word that was at the beginning of the line. if (ctrl_x_mode_line_or_eval() || (compl_cont_status & CONT_SOL)) { found_new_match = search_for_exact_line(st->ins_buf, st->cur_match_pos, - compl_direction, compl_pattern); + compl_direction, compl_pattern.data); } else { found_new_match = searchit(NULL, st->ins_buf, st->cur_match_pos, - NULL, compl_direction, compl_pattern, compl_patternlen, + NULL, compl_direction, compl_pattern.data, + compl_pattern.size, 1, SEARCH_KEEP + SEARCH_NFMSG, RE_LAST, NULL); } msg_silent--; @@ -3460,7 +3473,7 @@ static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_ case CTRL_X_FUNCTION: case CTRL_X_OMNI: - expand_by_function(type, compl_pattern); + expand_by_function(type, compl_pattern.data); break; case CTRL_X_SPELL: @@ -3491,7 +3504,7 @@ static void get_next_bufname_token(void) FOR_ALL_BUFFERS(b) { if (b->b_p_bl && b->b_sfname != NULL) { char *tail = path_tail(b->b_sfname); - if (strncmp(tail, compl_orig_text, strlen(compl_orig_text)) == 0) { + if (strncmp(tail, compl_orig_text.data, compl_orig_text.size) == 0) { ins_compl_add(tail, (int)strlen(tail), NULL, NULL, false, NULL, 0, p_ic ? CP_ICASE : 0, false, NULL); } @@ -3559,7 +3572,7 @@ static int ins_compl_get_exp(pos_T *ini) // If complete() was called then compl_pattern has been reset. // The following won't work then, bail out. - if (compl_pattern == NULL) { + if (compl_pattern.data == NULL) { break; } @@ -3627,7 +3640,7 @@ static int ins_compl_get_exp(pos_T *ini) static void ins_compl_update_shown_match(void) { while (!ins_compl_equal(compl_shown_match, - compl_leader, strlen(compl_leader)) + compl_leader.data, compl_leader.size) && compl_shown_match->cp_next != NULL && !is_first_match(compl_shown_match->cp_next)) { compl_shown_match = compl_shown_match->cp_next; @@ -3636,10 +3649,10 @@ static void ins_compl_update_shown_match(void) // If we didn't find it searching forward, and compl_shows_dir is // backward, find the last match. if (compl_shows_dir_backward() - && !ins_compl_equal(compl_shown_match, compl_leader, strlen(compl_leader)) + && !ins_compl_equal(compl_shown_match, compl_leader.data, compl_leader.size) && (compl_shown_match->cp_next == NULL || is_first_match(compl_shown_match->cp_next))) { - while (!ins_compl_equal(compl_shown_match, compl_leader, strlen(compl_leader)) + while (!ins_compl_equal(compl_shown_match, compl_leader.data, compl_leader.size) && compl_shown_match->cp_prev != NULL && !is_first_match(compl_shown_match->cp_prev)) { compl_shown_match = compl_shown_match->cp_prev; @@ -3654,13 +3667,13 @@ void ins_compl_delete(bool new_leader) // allows marks present on the original text to shrink/grow appropriately. int orig_col = 0; if (new_leader) { - char *orig = compl_orig_text; + char *orig = compl_orig_text.data; char *leader = ins_compl_leader(); while (*orig != NUL && utf_ptr2char(orig) == utf_ptr2char(leader)) { leader += utf_ptr2len(leader); orig += utf_ptr2len(orig); } - orig_col = (int)(orig - compl_orig_text); + orig_col = (int)(orig - compl_orig_text.data); } // In insert mode: Delete the typed part. @@ -3688,8 +3701,8 @@ void ins_compl_insert(bool in_compl_func) int compl_len = get_compl_len(); // Make sure we don't go over the end of the string, this can happen with // illegal bytes. - if (compl_len < (int)strlen(compl_shown_match->cp_str)) { - ins_compl_insert_bytes(compl_shown_match->cp_str + compl_len, -1); + if (compl_len < (int)compl_shown_match->cp_str.size) { + ins_compl_insert_bytes(compl_shown_match->cp_str.data + compl_len, -1); } compl_used_match = !match_at_original_text(compl_shown_match); @@ -3759,7 +3772,7 @@ static compl_T *find_comp_when_fuzzy(void) comp = compl_first_match; do { if (comp->cp_score == score - && (str == comp->cp_str || str == comp->cp_text[CPT_ABBR])) { + && (str == comp->cp_str.data || str == comp->cp_text[CPT_ABBR])) { return comp; } comp = comp->cp_next; @@ -3842,9 +3855,9 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a found_end = false; } if (!match_at_original_text(compl_shown_match) - && compl_leader != NULL + && compl_leader.data != NULL && !ins_compl_equal(compl_shown_match, - compl_leader, strlen(compl_leader)) + compl_leader.data, compl_leader.size) && !(compl_fuzzy_match && compl_shown_match->cp_score > 0)) { todo++; } else { @@ -3900,7 +3913,7 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match return -1; } - if (compl_leader != NULL + if (compl_leader.data != NULL && !match_at_original_text(compl_shown_match) && !compl_fuzzy_match) { // Update "compl_shown_match" to the actually shown match @@ -3938,17 +3951,17 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match // Insert the text of the new completion, or the compl_leader. if (compl_no_insert && !started) { - ins_compl_insert_bytes(compl_orig_text + get_compl_len(), -1); + ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); compl_used_match = false; restore_orig_extmarks(); } else if (insert_match) { if (!compl_get_longest || compl_used_match) { ins_compl_insert(in_compl_func); } else { - assert(compl_leader != NULL); - ins_compl_insert_bytes(compl_leader + get_compl_len(), -1); + assert(compl_leader.data != NULL); + ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1); } - if (!strcmp(compl_curr_match->cp_str, compl_orig_text)) { + if (strequal(compl_curr_match->cp_str.data, compl_orig_text.data)) { restore_orig_extmarks(); } } else { @@ -4112,8 +4125,7 @@ static bool ins_compl_use_match(int c) /// Get the pattern, column and length for normal completion (CTRL-N CTRL-P /// completion) -/// Sets the global variables: compl_col, compl_length, compl_pattern and -/// compl_patternlen. +/// Sets the global variables: compl_col, compl_length and compl_pattern. /// Uses the global variables: compl_cont_status and ctrl_x_mode static int get_normal_compl_info(char *line, int startcol, colnr_T curs_col) { @@ -4124,29 +4136,32 @@ static int get_normal_compl_info(char *line, int startcol, colnr_T curs_col) compl_length = curs_col - startcol; } if (p_ic) { - compl_pattern = str_foldcase(line + compl_col, compl_length, NULL, 0); + compl_pattern = cstr_as_string(str_foldcase(line + compl_col, + compl_length, NULL, 0)); } else { - compl_pattern = xstrnsave(line + compl_col, (size_t)compl_length); + compl_pattern = cbuf_to_string(line + compl_col, (size_t)compl_length); } } else if (compl_status_adding()) { char *prefix = "\\<"; size_t prefixlen = STRLEN_LITERAL("\\<"); - // we need up to 2 extra chars for the prefix - compl_pattern = xmalloc(quote_meta(NULL, line + compl_col, - compl_length) + prefixlen); if (!vim_iswordp(line + compl_col) || (compl_col > 0 && (vim_iswordp(mb_prevptr(line, line + compl_col))))) { prefix = ""; prefixlen = 0; } - STRCPY(compl_pattern, prefix); - quote_meta(compl_pattern + prefixlen, line + compl_col, compl_length); + + // we need up to 2 extra chars for the prefix + size_t n = quote_meta(NULL, line + compl_col, compl_length) + prefixlen; + compl_pattern.data = xmalloc(n); + STRCPY(compl_pattern.data, prefix); + quote_meta(compl_pattern.data + prefixlen, line + compl_col, compl_length); + compl_pattern.size = n - 1; } else if (--startcol < 0 || !vim_iswordp(mb_prevptr(line, line + startcol + 1))) { // Match any word of at least two chars - compl_pattern = xstrnsave(S_LEN("\\<\\k\\k")); + compl_pattern = cbuf_to_string(S_LEN("\\<\\k\\k")); compl_col += curs_col; compl_length = 0; } else { @@ -4167,19 +4182,20 @@ static int get_normal_compl_info(char *line, int startcol, colnr_T curs_col) // Only match word with at least two chars -- webb // there's no need to call quote_meta, // xmalloc(7) is enough -- Acevedo - compl_pattern = xmalloc(7); - STRCPY(compl_pattern, "\\<"); - quote_meta(compl_pattern + 2, line + compl_col, 1); - strcat(compl_pattern, "\\k"); + compl_pattern.data = xmalloc(7); + STRCPY(compl_pattern.data, "\\<"); + quote_meta(compl_pattern.data + 2, line + compl_col, 1); + strcat(compl_pattern.data, "\\k"); + compl_pattern.size = strlen(compl_pattern.data); } else { - compl_pattern = xmalloc(quote_meta(NULL, line + compl_col, compl_length) + 2); - STRCPY(compl_pattern, "\\<"); - quote_meta(compl_pattern + 2, line + compl_col, compl_length); + size_t n = quote_meta(NULL, line + compl_col, compl_length) + 2; + compl_pattern.data = xmalloc(n); + STRCPY(compl_pattern.data, "\\<"); + quote_meta(compl_pattern.data + 2, line + compl_col, compl_length); + compl_pattern.size = n - 1; } } - compl_patternlen = strlen(compl_pattern); - return OK; } @@ -4194,13 +4210,12 @@ static int get_wholeline_compl_info(char *line, colnr_T curs_col) compl_length = 0; } if (p_ic) { - compl_pattern = str_foldcase(line + compl_col, compl_length, NULL, 0); + compl_pattern = cstr_as_string(str_foldcase(line + compl_col, + compl_length, NULL, 0)); } else { - compl_pattern = xstrnsave(line + compl_col, (size_t)compl_length); + compl_pattern = cbuf_to_string(line + compl_col, (size_t)compl_length); } - compl_patternlen = strlen(compl_pattern); - return OK; } @@ -4225,8 +4240,8 @@ static int get_filename_compl_info(char *line, int startcol, colnr_T curs_col) compl_col += startcol; compl_length = (int)curs_col - startcol; - compl_pattern = addstar(line + compl_col, (size_t)compl_length, EXPAND_FILES); - compl_patternlen = strlen(compl_pattern); + compl_pattern = cstr_as_string(addstar(line + compl_col, + (size_t)compl_length, EXPAND_FILES)); return OK; } @@ -4235,9 +4250,9 @@ static int get_filename_compl_info(char *line, int startcol, colnr_T curs_col) /// Sets the global variables: compl_col, compl_length and compl_pattern. static int get_cmdline_compl_info(char *line, colnr_T curs_col) { - compl_pattern = xstrnsave(line, (size_t)curs_col); - compl_patternlen = (size_t)curs_col; - set_cmd_context(&compl_xp, compl_pattern, (int)compl_patternlen, curs_col, false); + compl_pattern = cbuf_to_string(line, (size_t)curs_col); + set_cmd_context(&compl_xp, compl_pattern.data, + (int)compl_pattern.size, curs_col, false); if (compl_xp.xp_context == EXPAND_LUA) { nlua_expand_pat(&compl_xp); } @@ -4247,7 +4262,7 @@ static int get_cmdline_compl_info(char *line, colnr_T curs_col) // "pattern not found" message. compl_col = curs_col; } else { - compl_col = (int)(compl_xp.xp_pattern - compl_pattern); + compl_col = (int)(compl_xp.xp_pattern - compl_pattern.data); } compl_length = curs_col - compl_col; @@ -4325,8 +4340,7 @@ static int get_userdefined_compl_info(colnr_T curs_col) // it may have become invalid. char *line = ml_get(curwin->w_cursor.lnum); compl_length = curs_col - compl_col; - compl_pattern = xstrnsave(line + compl_col, (size_t)compl_length); - compl_patternlen = (size_t)compl_length; + compl_pattern = cbuf_to_string(line + compl_col, (size_t)compl_length); return OK; } @@ -4351,8 +4365,7 @@ static int get_spell_compl_info(int startcol, colnr_T curs_col) } // Need to obtain "line" again, it may have become invalid. char *line = ml_get(curwin->w_cursor.lnum); - compl_pattern = xstrnsave(line + compl_col, (size_t)compl_length); - compl_patternlen = (size_t)compl_length; + compl_pattern = cbuf_to_string(line + compl_col, (size_t)compl_length); return OK; } @@ -4537,19 +4550,19 @@ static int ins_compl_start(void) ins_compl_fixRedoBufForLeader(NULL); // Always add completion for the original text. - xfree(compl_orig_text); + API_CLEAR_STRING(compl_orig_text); kv_destroy(compl_orig_extmarks); - compl_orig_text = xstrnsave(line + compl_col, (size_t)compl_length); + compl_orig_text = cbuf_to_string(line + compl_col, (size_t)compl_length); save_orig_extmarks(); int flags = CP_ORIGINAL_TEXT; if (p_ic) { flags |= CP_ICASE; } - if (ins_compl_add(compl_orig_text, -1, NULL, NULL, false, NULL, 0, + if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size, + NULL, NULL, false, NULL, 0, flags, false, NULL) != OK) { - XFREE_CLEAR(compl_pattern); - compl_patternlen = 0; - XFREE_CLEAR(compl_orig_text); + API_CLEAR_STRING(compl_pattern); + API_CLEAR_STRING(compl_orig_text); kv_destroy(compl_orig_extmarks); return FAIL; } @@ -4783,7 +4796,7 @@ static unsigned quote_meta(char *dest, char *src, int len) #if defined(EXITFREE) void free_insexpand_stuff(void) { - XFREE_CLEAR(compl_orig_text); + API_CLEAR_STRING(compl_orig_text); kv_destroy(compl_orig_extmarks); callback_free(&cfu_cb); callback_free(&ofu_cb); diff --git a/src/nvim/main.c b/src/nvim/main.c index 348f246d27..58d110e8b2 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -634,6 +634,7 @@ int main(int argc, char **argv) if (params.luaf != NULL) { // Like "--cmd", "+", "-c" and "-S", don't truncate messages. msg_scroll = true; + DLOG("executing Lua -l script"); bool lua_ok = nlua_exec_file(params.luaf); TIME_MSG("executing Lua -l script"); if (msg_didout) { @@ -1227,6 +1228,9 @@ static void command_line_scan(mparm_T *parmp) if (exmode_active) { // "-es" silent (batch) Ex-mode silent_mode = true; parmp->no_swap_file = true; + if (p_shadafile == NULL || *p_shadafile == NUL) { + set_option_value_give_err(kOptShadafile, STATIC_CSTR_AS_OPTVAL("NONE"), 0); + } } else { // "-s {scriptin}" read from script file want_argument = true; } @@ -2084,8 +2088,7 @@ static void source_startup_scripts(const mparm_T *const parmp) { // If -u given, use only the initializations from that file and nothing else. if (parmp->use_vimrc != NULL) { - if (strequal(parmp->use_vimrc, "NONE") - || strequal(parmp->use_vimrc, "NORC")) { + if (strequal(parmp->use_vimrc, "NONE") || strequal(parmp->use_vimrc, "NORC")) { // Do nothing. } else { if (do_source(parmp->use_vimrc, false, DOSO_NONE, NULL) != OK) { diff --git a/src/nvim/mbyte.h b/src/nvim/mbyte.h index 2da051fca2..674e0a2638 100644 --- a/src/nvim/mbyte.h +++ b/src/nvim/mbyte.h @@ -12,7 +12,6 @@ #include "nvim/mbyte_defs.h" // IWYU pragma: keep #include "nvim/types_defs.h" // IWYU pragma: keep -typedef utf8proc_int32_t GraphemeState; #define GRAPHEME_STATE_INIT 0 #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/mbyte_defs.h b/src/nvim/mbyte_defs.h index 7f2d1ba6ce..e308a81a5d 100644 --- a/src/nvim/mbyte_defs.h +++ b/src/nvim/mbyte_defs.h @@ -2,6 +2,7 @@ #include <stdbool.h> #include <stdint.h> +#include <utf8proc.h> #include "nvim/iconv_defs.h" @@ -71,3 +72,5 @@ typedef struct { int8_t begin_off; ///< Offset to the first byte of the codepoint. int8_t end_off; ///< Offset to one past the end byte of the codepoint. } CharBoundsOff; + +typedef utf8proc_int32_t GraphemeState; diff --git a/src/nvim/message.c b/src/nvim/message.c index 69e8f66bbe..d45bc147cb 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -2222,6 +2222,7 @@ static void msg_puts_display(const char *str, int maxlen, int hl_id, int recurse size_t len = maxlen < 0 ? strlen(str) : strnlen(str, (size_t)maxlen); ga_concat_len(&msg_ext_last_chunk, str, len); msg_ext_cur_len += len; + msg_col += (int)mb_string2cells(str); // When message ends in newline, reset variables used to format message: msg_advance(). assert(len > 0); if (str[len - 1] == '\n') { @@ -2718,7 +2719,7 @@ static msgchunk_T *disp_sb_line(int row, msgchunk_T *smp) } /// @return true when messages should be printed to stdout/stderr: -/// - "batch mode" ("silent mode", -es/-Es) +/// - "batch mode" ("silent mode", -es/-Es/-l) /// - no UI and not embedded int msg_use_printf(void) { diff --git a/src/nvim/option.c b/src/nvim/option.c index 9d9e70622a..551ea0be20 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -2120,14 +2120,14 @@ static const char *did_set_laststatus(optset_T *args) // When switching to global statusline, decrease topframe height // Also clear the cmdline to remove the ruler if there is one if (value == 3 && old_value != 3) { - frame_new_height(topframe, topframe->fr_height - STATUS_HEIGHT, false, false); + frame_new_height(topframe, topframe->fr_height - STATUS_HEIGHT, false, false, false); win_comp_pos(); clear_cmdline = true; } // When switching from global statusline, increase height of topframe by STATUS_HEIGHT // in order to to re-add the space that was previously taken by the global statusline if (old_value == 3 && value != 3) { - frame_new_height(topframe, topframe->fr_height + STATUS_HEIGHT, false, false); + frame_new_height(topframe, topframe->fr_height + STATUS_HEIGHT, false, false, false); win_comp_pos(); } @@ -2872,8 +2872,6 @@ static const char *validate_num_option(OptIndex opt_idx, OptInt *newval, char *e case kOptCmdheight: if (value < 0) { return e_positive; - } else { - p_ch_was_zero = value == 0; } break; case kOptHistory: diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index 75e5a52829..2b1dd22b1a 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -7,6 +7,7 @@ #include <stdint.h> #include <string.h> +#include "nvim/api/buffer.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" #include "nvim/ascii_defs.h" @@ -32,6 +33,7 @@ #include "nvim/highlight_defs.h" #include "nvim/insexpand.h" #include "nvim/keycodes.h" +#include "nvim/mark.h" #include "nvim/mbyte.h" #include "nvim/memline.h" #include "nvim/memory.h" @@ -787,36 +789,41 @@ void pum_redraw(void) } } -/// set info text to preview buffer. +/// Set the informational text in the preview buffer when the completion +/// item does not include a dedicated preview or popup window. +/// +/// @param[in] buf Buffer where the text will be set. +/// @param[in] info Informational text to display in the preview buffer. +/// @param[in] lnum Where to start the text. Incremented for each added line. +/// @param[out] max_width Maximum width of the displayed text. static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *max_width) { - bcount_t inserted_bytes = 0; - for (char *p = info; *p != NUL;) { - int text_width = 0; - char *e = vim_strchr(p, '\n'); - if (e == NULL) { - ml_append_buf(buf, (*lnum)++, p, 0, false); - text_width = (int)mb_string2cells(p); - if (text_width > *max_width) { - *max_width = text_width; - } - break; - } - *e = NUL; - ml_append_buf(buf, (*lnum)++, p, (int)(e - p + 1), false); - inserted_bytes += (bcount_t)strlen(p) + 1; - text_width = (int)mb_string2cells(p); - if (text_width > *max_width) { - *max_width = text_width; - } - *e = '\n'; - p = e + 1; + Error err = ERROR_INIT; + Arena arena = ARENA_EMPTY; + Array replacement = ARRAY_DICT_INIT; + char *token = NULL; + char *line = os_strtok(info, "\n", &token); + buf->b_p_ma = true; + while (line != NULL) { + ADD(replacement, STRING_OBJ(cstr_to_string(line))); + (*lnum)++; + (*max_width) = MAX(*max_width, (int)mb_string2cells(line)); + line = os_strtok(NULL, "\n", &token); } - // delete the empty last line - ml_delete_buf(buf, buf->b_ml.ml_line_count, false); - if (get_cot_flags() & kOptCotFlagPopup) { - extmark_splice(buf, 1, 0, 1, 0, 0, buf->b_ml.ml_line_count, 0, inserted_bytes, kExtmarkNoUndo); + + int original_textlock = textlock; + if (textlock > 0) { + textlock = 0; + } + nvim_buf_set_lines(0, buf->handle, 0, -1, false, replacement, &arena, &err); + textlock = original_textlock; + if (ERROR_SET(&err)) { + emsg(err.msg); + api_clear_error(&err); } + arena_mem_free(arena_finish(&arena)); + api_free_array(replacement); + buf->b_p_ma = false; } /// adjust floating info preview window position @@ -866,14 +873,6 @@ win_T *pum_set_info(int selected, char *info) if (!wp) { return NULL; } - } else { - // clean exist buffer - linenr_T count = wp->w_buffer->b_ml.ml_line_count; - while (!buf_is_empty(wp->w_buffer)) { - ml_delete_buf(wp->w_buffer, 1, false); - } - bcount_t deleted_bytes = get_region_bytecount(wp->w_buffer, 1, count, 0, 0); - extmark_splice(wp->w_buffer, 1, 0, count, 0, deleted_bytes, 1, 0, 0, kExtmarkNoUndo); } linenr_T lnum = 0; int max_info_width = 0; @@ -1011,7 +1010,8 @@ static bool pum_set_selected(int n, int repeat) && (curbuf->b_nwindows == 1) && (curbuf->b_fname == NULL) && bt_nofile(curbuf) - && (curbuf->b_p_bh[0] == 'w')) { + && (curbuf->b_p_bh[0] == 'w') + && !use_float) { // Already a "wipeout" buffer, make it empty. while (!buf_is_empty(curbuf)) { ml_delete(1, false); diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c index d849a18879..cdedf86977 100644 --- a/src/nvim/runtime.c +++ b/src/nvim/runtime.c @@ -228,6 +228,72 @@ char *estack_sfile(estack_arg_T which) return (char *)ga.ga_data; } +static void stacktrace_push_item(list_T *const l, ufunc_T *const fp, const char *const event, + const linenr_T lnum, char *const filepath, + const bool filepath_alloced) +{ + dict_T *const d = tv_dict_alloc_lock(VAR_FIXED); + typval_T tv = { + .v_type = VAR_DICT, + .v_lock = VAR_LOCKED, + .vval.v_dict = d, + }; + + if (fp != NULL) { + tv_dict_add_func(d, S_LEN("funcref"), fp); + } + if (event != NULL) { + tv_dict_add_str(d, S_LEN("event"), event); + } + tv_dict_add_nr(d, S_LEN("lnum"), lnum); + if (filepath_alloced) { + tv_dict_add_allocated_str(d, S_LEN("filepath"), filepath); + } else { + tv_dict_add_str(d, S_LEN("filepath"), filepath); + } + + tv_list_append_tv(l, &tv); +} + +/// Create the stacktrace from exestack. +list_T *stacktrace_create(void) +{ + list_T *const l = tv_list_alloc(exestack.ga_len); + + for (int i = 0; i < exestack.ga_len; i++) { + estack_T *const entry = &((estack_T *)exestack.ga_data)[i]; + linenr_T lnum = entry->es_lnum; + + if (entry->es_type == ETYPE_SCRIPT) { + stacktrace_push_item(l, NULL, NULL, lnum, entry->es_name, false); + } else if (entry->es_type == ETYPE_UFUNC) { + ufunc_T *const fp = entry->es_info.ufunc; + const sctx_T sctx = fp->uf_script_ctx; + bool filepath_alloced = false; + char *filepath = sctx.sc_sid > 0 + ? get_scriptname((LastSet){ .script_ctx = sctx }, + &filepath_alloced) : ""; + lnum += sctx.sc_lnum; + stacktrace_push_item(l, fp, NULL, lnum, filepath, filepath_alloced); + } else if (entry->es_type == ETYPE_AUCMD) { + const sctx_T sctx = entry->es_info.aucmd->script_ctx; + bool filepath_alloced = false; + char *filepath = sctx.sc_sid > 0 + ? get_scriptname((LastSet){ .script_ctx = sctx }, + &filepath_alloced) : ""; + lnum += sctx.sc_lnum; + stacktrace_push_item(l, NULL, entry->es_name, lnum, filepath, filepath_alloced); + } + } + return l; +} + +/// getstacktrace() function +void f_getstacktrace(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + tv_list_set_ret(rettv, stacktrace_create()); +} + static bool runtime_search_path_valid = false; static int *runtime_search_path_ref = NULL; static RuntimeSearchPath runtime_search_path; diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index d47340505a..fe6892cc27 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -96,53 +96,51 @@ void win_redr_status(win_T *wp) get_trans_bufname(wp->w_buffer); char *p = NameBuff; - int len = (int)strlen(p); + int plen = (int)strlen(p); if ((bt_help(wp->w_buffer) || wp->w_p_pvw || bufIsChanged(wp->w_buffer) || wp->w_buffer->b_p_ro) - && len < MAXPATHL - 1) { - *(p + len++) = ' '; + && plen < MAXPATHL - 1) { + *(p + plen++) = ' '; } if (bt_help(wp->w_buffer)) { - snprintf(p + len, MAXPATHL - (size_t)len, "%s", _("[Help]")); - len += (int)strlen(p + len); + plen += snprintf(p + plen, MAXPATHL - (size_t)plen, "%s", _("[Help]")); } if (wp->w_p_pvw) { - snprintf(p + len, MAXPATHL - (size_t)len, "%s", _("[Preview]")); - len += (int)strlen(p + len); + plen += snprintf(p + plen, MAXPATHL - (size_t)plen, "%s", _("[Preview]")); } if (bufIsChanged(wp->w_buffer)) { - snprintf(p + len, MAXPATHL - (size_t)len, "%s", "[+]"); - len += (int)strlen(p + len); + plen += snprintf(p + plen, MAXPATHL - (size_t)plen, "%s", "[+]"); } if (wp->w_buffer->b_p_ro) { - snprintf(p + len, MAXPATHL - (size_t)len, "%s", _("[RO]")); - // len += (int)strlen(p + len); // dead assignment + plen += snprintf(p + plen, MAXPATHL - (size_t)plen, "%s", _("[RO]")); } + (void)plen; - int this_ru_col = MAX(ru_col - (Columns - stl_width), (stl_width + 1) / 2); + int n = (stl_width + 1) / 2; + int this_ru_col = ru_col - (Columns - stl_width); + this_ru_col = MAX(this_ru_col, n); if (this_ru_col <= 1) { p = "<"; // No room for file name! - len = 1; + plen = 1; } else { int i; // Count total number of display cells. - int clen = (int)mb_string2cells(p); + plen = (int)mb_string2cells(p); // Find first character that will fit. // Going from start to end is much faster for DBCS. - for (i = 0; p[i] != NUL && clen >= this_ru_col - 1; + for (i = 0; p[i] != NUL && plen >= this_ru_col - 1; i += utfc_ptr2len(p + i)) { - clen -= utf_ptr2cells(p + i); + plen -= utf_ptr2cells(p + i); } - len = clen; if (i > 0) { p = p + i - 1; *p = '<'; - len++; + plen++; } } @@ -152,16 +150,17 @@ void win_redr_status(win_T *wp) int width = grid_line_puts(off, p, -1, attr); grid_line_fill(off + width, off + this_ru_col, fillchar, attr); - if (get_keymap_str(wp, "<%s>", NameBuff, MAXPATHL) - && this_ru_col - len > (int)strlen(NameBuff) + 1) { - grid_line_puts(off + this_ru_col - (int)strlen(NameBuff) - 1, NameBuff, -1, attr); + int NameBufflen = get_keymap_str(wp, "<%s>", NameBuff, MAXPATHL); + if (NameBufflen > 0 && this_ru_col - plen > NameBufflen + 1) { + grid_line_puts(off + this_ru_col - NameBufflen - 1, NameBuff, -1, attr); } win_redr_ruler(wp); // Draw the 'showcmd' information if 'showcmdloc' == "statusline". if (p_sc && *p_sloc == 's') { - const int sc_width = MIN(10, this_ru_col - len - 2); + n = this_ru_col - plen - 2; // perform the calculation here so we only do it once + const int sc_width = MIN(10, n); if (sc_width > 0) { grid_line_puts(off + this_ru_col - sc_width - 1, showcmd_buf, sc_width, attr); @@ -548,36 +547,40 @@ void win_redr_ruler(win_T *wp) #define RULER_BUF_LEN 70 char buffer[RULER_BUF_LEN]; - // Some sprintfs return the length, some return a pointer. - // To avoid portability problems we use strlen() here. - vim_snprintf(buffer, RULER_BUF_LEN, "%" PRId64 ",", - (wp->w_buffer->b_ml.ml_flags & - ML_EMPTY) ? 0 : (int64_t)wp->w_cursor.lnum); - size_t len = strlen(buffer); - col_print(buffer + len, RULER_BUF_LEN - len, - empty_line ? 0 : (int)wp->w_cursor.col + 1, - (int)virtcol + 1); + int bufferlen = vim_snprintf(buffer, RULER_BUF_LEN, "%" PRId64 ",", + (wp->w_buffer->b_ml.ml_flags & ML_EMPTY) + ? 0 + : (int64_t)wp->w_cursor.lnum); + bufferlen += col_print(buffer + bufferlen, RULER_BUF_LEN - (size_t)bufferlen, + empty_line ? 0 : (int)wp->w_cursor.col + 1, + (int)virtcol + 1); // Add a "50%" if there is room for it. // On the last line, don't print in the last column (scrolls the // screen up on some terminals). - int i = (int)strlen(buffer); - get_rel_pos(wp, buffer + i + 1, RULER_BUF_LEN - i - 1); - int o = i + vim_strsize(buffer + i + 1); + char rel_pos[RULER_BUF_LEN]; + int rel_poslen = get_rel_pos(wp, rel_pos, RULER_BUF_LEN); + int n1 = bufferlen + vim_strsize(rel_pos); if (wp->w_status_height == 0 && !is_stl_global) { // can't use last char of screen - o++; + n1++; } + + int this_ru_col = ru_col - (Columns - width); // Never use more than half the window/screen width, leave the other half // for the filename. - int this_ru_col = MAX(ru_col - (Columns - width), (width + 1) / 2); - if (this_ru_col + o < width) { - // Need at least 3 chars left for get_rel_pos() + NUL. - while (this_ru_col + o < width && RULER_BUF_LEN > i + 4) { - i += (int)schar_get(buffer + i, fillchar); - o++; - } - get_rel_pos(wp, buffer + i, RULER_BUF_LEN - i); + int n2 = (width + 1) / 2; + this_ru_col = MAX(this_ru_col, n2); + if (this_ru_col + n1 < width) { + // need at least space for rel_pos + NUL + while (this_ru_col + n1 < width + && RULER_BUF_LEN > bufferlen + rel_poslen + 1) { // +1 for NUL + bufferlen += (int)schar_get(buffer + bufferlen, fillchar); + n1++; + } + bufferlen += vim_snprintf(buffer + bufferlen, RULER_BUF_LEN - (size_t)bufferlen, + "%s", rel_pos); } + (void)bufferlen; if (ui_has(kUIMessages) && !part_of_status) { MAXSIZE_TEMP_ARRAY(content, 1); @@ -595,11 +598,11 @@ void win_redr_ruler(win_T *wp) did_show_ext_ruler = false; } // Truncate at window boundary. - o = 0; - for (i = 0; buffer[i] != NUL; i += utfc_ptr2len(buffer + i)) { - o += utf_ptr2cells(buffer + i); - if (this_ru_col + o > width) { - buffer[i] = NUL; + for (n1 = 0, n2 = 0; buffer[n1] != NUL; n1 += utfc_ptr2len(buffer + n1)) { + n2 += utf_ptr2cells(buffer + n1); + if (this_ru_col + n2 > width) { + bufferlen = n1; + buffer[bufferlen] = NUL; break; } } @@ -1536,7 +1539,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op // Store the position percentage in our temporary buffer. // Note: We cannot store the value in `num` because // `get_rel_pos` can return a named position. Ex: "Top" - get_rel_pos(wp, buf_tmp, TMPLEN); + (void)get_rel_pos(wp, buf_tmp, TMPLEN); str = buf_tmp; break; @@ -1564,7 +1567,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, OptIndex op case STL_KEYMAP: fillable = false; - if (get_keymap_str(wp, "<%s>", buf_tmp, TMPLEN)) { + if (get_keymap_str(wp, "<%s>", buf_tmp, TMPLEN) > 0) { str = buf_tmp; } break; diff --git a/src/nvim/terminal.c b/src/nvim/terminal.c index 0743eda374..d7ed709906 100644 --- a/src/nvim/terminal.c +++ b/src/nvim/terminal.c @@ -93,9 +93,15 @@ #include "nvim/types_defs.h" #include "nvim/ui.h" #include "nvim/vim_defs.h" +#include "nvim/vterm/keyboard.h" +#include "nvim/vterm/mouse.h" +#include "nvim/vterm/parser.h" +#include "nvim/vterm/pen.h" +#include "nvim/vterm/screen.h" +#include "nvim/vterm/state.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_keycodes_defs.h" #include "nvim/window.h" -#include "vterm/vterm.h" -#include "vterm/vterm_keycodes.h" typedef struct { VimState state; diff --git a/src/vterm/LICENSE b/src/nvim/vterm/LICENSE index 0d051634b2..0d051634b2 100644 --- a/src/vterm/LICENSE +++ b/src/nvim/vterm/LICENSE diff --git a/src/nvim/vterm/README.md b/src/nvim/vterm/README.md new file mode 100644 index 0000000000..4cc43bfb94 --- /dev/null +++ b/src/nvim/vterm/README.md @@ -0,0 +1 @@ +Adopted from [libvterm](https://www.leonerd.org.uk/code/libvterm/) diff --git a/src/nvim/vterm/encoding.c b/src/nvim/vterm/encoding.c new file mode 100644 index 0000000000..cc3208cfa2 --- /dev/null +++ b/src/nvim/vterm/encoding.c @@ -0,0 +1,278 @@ +#include "nvim/vterm/encoding.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/encoding.c.generated.h" +#endif + +#define UNICODE_INVALID 0xFFFD + +#if defined(DEBUG) && DEBUG > 1 +# define DEBUG_PRINT_UTF8 +#endif + +struct UTF8DecoderData { + // number of bytes remaining in this codepoint + int bytes_remaining; + + // number of bytes total in this codepoint once it's finished + // (for detecting overlongs) + int bytes_total; + + int this_cp; +}; + +static void init_utf8(VTermEncoding *enc, void *data_) +{ + struct UTF8DecoderData *data = data_; + + data->bytes_remaining = 0; + data->bytes_total = 0; +} + +static void decode_utf8(VTermEncoding *enc, void *data_, uint32_t cp[], int *cpi, int cplen, + const char bytes[], size_t *pos, size_t bytelen) +{ + struct UTF8DecoderData *data = data_; + +#ifdef DEBUG_PRINT_UTF8 + printf("BEGIN UTF-8\n"); +#endif + + for (; *pos < bytelen && *cpi < cplen; (*pos)++) { + uint8_t c = (uint8_t)bytes[*pos]; + +#ifdef DEBUG_PRINT_UTF8 + printf(" pos=%zd c=%02x rem=%d\n", *pos, c, data->bytes_remaining); +#endif + + if (c < 0x20) { // C0 + return; + } else if (c >= 0x20 && c < 0x7f) { + if (data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + } + + cp[(*cpi)++] = c; +#ifdef DEBUG_PRINT_UTF8 + printf(" UTF-8 char: U+%04x\n", c); +#endif + data->bytes_remaining = 0; + } else if (c == 0x7f) { // DEL + return; + } else if (c >= 0x80 && c < 0xc0) { + if (!data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + continue; + } + + data->this_cp <<= 6; + data->this_cp |= c & 0x3f; + data->bytes_remaining--; + + if (!data->bytes_remaining) { +#ifdef DEBUG_PRINT_UTF8 + printf(" UTF-8 raw char U+%04x bytelen=%d ", data->this_cp, data->bytes_total); +#endif + // Check for overlong sequences + switch (data->bytes_total) { + case 2: + if (data->this_cp < 0x0080) { + data->this_cp = UNICODE_INVALID; + } + break; + case 3: + if (data->this_cp < 0x0800) { + data->this_cp = UNICODE_INVALID; + } + break; + case 4: + if (data->this_cp < 0x10000) { + data->this_cp = UNICODE_INVALID; + } + break; + case 5: + if (data->this_cp < 0x200000) { + data->this_cp = UNICODE_INVALID; + } + break; + case 6: + if (data->this_cp < 0x4000000) { + data->this_cp = UNICODE_INVALID; + } + break; + } + // Now look for plain invalid ones + if ((data->this_cp >= 0xD800 && data->this_cp <= 0xDFFF) + || data->this_cp == 0xFFFE + || data->this_cp == 0xFFFF) { + data->this_cp = UNICODE_INVALID; + } +#ifdef DEBUG_PRINT_UTF8 + printf(" char: U+%04x\n", data->this_cp); +#endif + cp[(*cpi)++] = (uint32_t)data->this_cp; + } + } else if (c >= 0xc0 && c < 0xe0) { + if (data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + } + + data->this_cp = c & 0x1f; + data->bytes_total = 2; + data->bytes_remaining = 1; + } else if (c >= 0xe0 && c < 0xf0) { + if (data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + } + + data->this_cp = c & 0x0f; + data->bytes_total = 3; + data->bytes_remaining = 2; + } else if (c >= 0xf0 && c < 0xf8) { + if (data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + } + + data->this_cp = c & 0x07; + data->bytes_total = 4; + data->bytes_remaining = 3; + } else if (c >= 0xf8 && c < 0xfc) { + if (data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + } + + data->this_cp = c & 0x03; + data->bytes_total = 5; + data->bytes_remaining = 4; + } else if (c >= 0xfc && c < 0xfe) { + if (data->bytes_remaining) { + cp[(*cpi)++] = UNICODE_INVALID; + } + + data->this_cp = c & 0x01; + data->bytes_total = 6; + data->bytes_remaining = 5; + } else { + cp[(*cpi)++] = UNICODE_INVALID; + } + } +} + +static VTermEncoding encoding_utf8 = { + .init = &init_utf8, + .decode = &decode_utf8, +}; + +static void decode_usascii(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen, + const char bytes[], size_t *pos, size_t bytelen) +{ + int is_gr = bytes[*pos] & 0x80; + + for (; *pos < bytelen && *cpi < cplen; (*pos)++) { + uint8_t c = (uint8_t)(bytes[*pos] ^ is_gr); + + if (c < 0x20 || c == 0x7f || c >= 0x80) { + return; + } + + cp[(*cpi)++] = c; + } +} + +static VTermEncoding encoding_usascii = { + .decode = &decode_usascii, +}; + +struct StaticTableEncoding { + const VTermEncoding enc; + const uint32_t chars[128]; +}; + +static void decode_table(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen, + const char bytes[], size_t *pos, size_t bytelen) +{ + struct StaticTableEncoding *table = (struct StaticTableEncoding *)enc; + int is_gr = bytes[*pos] & 0x80; + + for (; *pos < bytelen && *cpi < cplen; (*pos)++) { + uint8_t c = (uint8_t)(bytes[*pos] ^ is_gr); + + if (c < 0x20 || c == 0x7f || c >= 0x80) { + return; + } + + if (table->chars[c]) { + cp[(*cpi)++] = table->chars[c]; + } else { + cp[(*cpi)++] = c; + } + } +} + +static const struct StaticTableEncoding encoding_DECdrawing = { + { .decode = &decode_table }, + { + [0x60] = 0x25C6, // BLACK DIAMOND + [0x61] = 0x2592, // MEDIUM SHADE (checkerboard) + [0x62] = 0x2409, // SYMBOL FOR HORIZONTAL TAB + [0x63] = 0x240C, // SYMBOL FOR FORM FEED + [0x64] = 0x240D, // SYMBOL FOR CARRIAGE RETURN + [0x65] = 0x240A, // SYMBOL FOR LINE FEED + [0x66] = 0x00B0, // DEGREE SIGN + [0x67] = 0x00B1, // PLUS-MINUS SIGN (plus or minus) + [0x68] = 0x2424, // SYMBOL FOR NEW LINE + [0x69] = 0x240B, // SYMBOL FOR VERTICAL TAB + [0x6a] = 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT (bottom-right corner) + [0x6b] = 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT (top-right corner) + [0x6c] = 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT (top-left corner) + [0x6d] = 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT (bottom-left corner) + [0x6e] = 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL (crossing lines) + [0x6f] = 0x23BA, // HORIZONTAL SCAN LINE-1 + [0x70] = 0x23BB, // HORIZONTAL SCAN LINE-3 + [0x71] = 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL + [0x72] = 0x23BC, // HORIZONTAL SCAN LINE-7 + [0x73] = 0x23BD, // HORIZONTAL SCAN LINE-9 + [0x74] = 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT + [0x75] = 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT + [0x76] = 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL + [0x77] = 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL + [0x78] = 0x2502, // BOX DRAWINGS LIGHT VERTICAL + [0x79] = 0x2A7D, // LESS-THAN OR SLANTED EQUAL-TO + [0x7a] = 0x2A7E, // GREATER-THAN OR SLANTED EQUAL-TO + [0x7b] = 0x03C0, // GREEK SMALL LETTER PI + [0x7c] = 0x2260, // NOT EQUAL TO + [0x7d] = 0x00A3, // POUND SIGN + [0x7e] = 0x00B7, // MIDDLE DOT + } +}; + +static const struct StaticTableEncoding encoding_uk = { + { .decode = &decode_table }, + { + [0x23] = 0x00a3, // £ + } +}; + +static struct { + VTermEncodingType type; + char designation; + VTermEncoding *enc; +} +encodings[] = { + { ENC_UTF8, 'u', &encoding_utf8 }, + { ENC_SINGLE_94, '0', (VTermEncoding *)&encoding_DECdrawing }, + { ENC_SINGLE_94, 'A', (VTermEncoding *)&encoding_uk }, + { ENC_SINGLE_94, 'B', &encoding_usascii }, + { 0 }, +}; + +VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation) +{ + for (int i = 0; encodings[i].designation; i++) { + if (encodings[i].type == type && encodings[i].designation == designation) { + return encodings[i].enc; + } + } + return NULL; +} diff --git a/src/nvim/vterm/encoding.h b/src/nvim/vterm/encoding.h new file mode 100644 index 0000000000..204b6d90c9 --- /dev/null +++ b/src/nvim/vterm/encoding.h @@ -0,0 +1,10 @@ +#pragma once + +#include <stddef.h> + +#include "nvim/vterm/vterm_defs.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/encoding.h.generated.h" +#endif diff --git a/src/nvim/vterm/keyboard.c b/src/nvim/vterm/keyboard.c new file mode 100644 index 0000000000..696b09157e --- /dev/null +++ b/src/nvim/vterm/keyboard.c @@ -0,0 +1,252 @@ +#include <stdio.h> + +#include "nvim/ascii_defs.h" +#include "nvim/tui/termkey/termkey.h" +#include "nvim/vterm/keyboard.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/keyboard.c.generated.h" +#endif + +void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod) +{ + // The shift modifier is never important for Unicode characters apart from Space + if (c != ' ') { + mod &= (unsigned)~VTERM_MOD_SHIFT; + } + + if (mod == 0) { + // Normal text - ignore just shift + char str[6]; + int seqlen = fill_utf8((int)c, str); + vterm_push_output_bytes(vt, str, (size_t)seqlen); + return; + } + + int needs_CSIu; + switch (c) { + // Special Ctrl- letters that can't be represented elsewise + case 'i': + case 'j': + case 'm': + case '[': + needs_CSIu = 1; + break; + // Ctrl-\ ] ^ _ don't need CSUu + case '\\': + case ']': + case '^': + case '_': + needs_CSIu = 0; + break; + // Shift-space needs CSIu + case ' ': + needs_CSIu = !!(mod & VTERM_MOD_SHIFT); + break; + // All other characters needs CSIu except for letters a-z + default: + needs_CSIu = (c < 'a' || c > 'z'); + } + + // ALT we can just prefix with ESC; anything else requires CSI u + if (needs_CSIu && (mod & (unsigned)~VTERM_MOD_ALT)) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", c, mod + 1); + return; + } + + if (mod & VTERM_MOD_CTRL) { + c &= 0x1f; + } + + vterm_push_output_sprintf(vt, "%s%c", mod & VTERM_MOD_ALT ? ESC_S : "", c); +} + +typedef struct { + enum { + KEYCODE_NONE, + KEYCODE_LITERAL, + KEYCODE_TAB, + KEYCODE_ENTER, + KEYCODE_SS3, + KEYCODE_CSI, + KEYCODE_CSI_CURSOR, + KEYCODE_CSINUM, + KEYCODE_KEYPAD, + } type; + char literal; + int csinum; +} keycodes_s; + +static keycodes_s keycodes[] = { + { KEYCODE_NONE, NUL, 0 }, // NONE + + { KEYCODE_ENTER, '\r', 0 }, // ENTER + { KEYCODE_TAB, '\t', 0 }, // TAB + { KEYCODE_LITERAL, '\x7f', 0 }, // BACKSPACE == ASCII DEL + { KEYCODE_LITERAL, '\x1b', 0 }, // ESCAPE + + { KEYCODE_CSI_CURSOR, 'A', 0 }, // UP + { KEYCODE_CSI_CURSOR, 'B', 0 }, // DOWN + { KEYCODE_CSI_CURSOR, 'D', 0 }, // LEFT + { KEYCODE_CSI_CURSOR, 'C', 0 }, // RIGHT + + { KEYCODE_CSINUM, '~', 2 }, // INS + { KEYCODE_CSINUM, '~', 3 }, // DEL + { KEYCODE_CSI_CURSOR, 'H', 0 }, // HOME + { KEYCODE_CSI_CURSOR, 'F', 0 }, // END + { KEYCODE_CSINUM, '~', 5 }, // PAGEUP + { KEYCODE_CSINUM, '~', 6 }, // PAGEDOWN +}; + +static keycodes_s keycodes_fn[] = { + { KEYCODE_NONE, NUL, 0 }, // F0 - shouldn't happen + { KEYCODE_SS3, 'P', 0 }, // F1 + { KEYCODE_SS3, 'Q', 0 }, // F2 + { KEYCODE_SS3, 'R', 0 }, // F3 + { KEYCODE_SS3, 'S', 0 }, // F4 + { KEYCODE_CSINUM, '~', 15 }, // F5 + { KEYCODE_CSINUM, '~', 17 }, // F6 + { KEYCODE_CSINUM, '~', 18 }, // F7 + { KEYCODE_CSINUM, '~', 19 }, // F8 + { KEYCODE_CSINUM, '~', 20 }, // F9 + { KEYCODE_CSINUM, '~', 21 }, // F10 + { KEYCODE_CSINUM, '~', 23 }, // F11 + { KEYCODE_CSINUM, '~', 24 }, // F12 +}; + +static keycodes_s keycodes_kp[] = { + { KEYCODE_KEYPAD, '0', 'p' }, // KP_0 + { KEYCODE_KEYPAD, '1', 'q' }, // KP_1 + { KEYCODE_KEYPAD, '2', 'r' }, // KP_2 + { KEYCODE_KEYPAD, '3', 's' }, // KP_3 + { KEYCODE_KEYPAD, '4', 't' }, // KP_4 + { KEYCODE_KEYPAD, '5', 'u' }, // KP_5 + { KEYCODE_KEYPAD, '6', 'v' }, // KP_6 + { KEYCODE_KEYPAD, '7', 'w' }, // KP_7 + { KEYCODE_KEYPAD, '8', 'x' }, // KP_8 + { KEYCODE_KEYPAD, '9', 'y' }, // KP_9 + { KEYCODE_KEYPAD, '*', 'j' }, // KP_MULT + { KEYCODE_KEYPAD, '+', 'k' }, // KP_PLUS + { KEYCODE_KEYPAD, ',', 'l' }, // KP_COMMA + { KEYCODE_KEYPAD, '-', 'm' }, // KP_MINUS + { KEYCODE_KEYPAD, '.', 'n' }, // KP_PERIOD + { KEYCODE_KEYPAD, '/', 'o' }, // KP_DIVIDE + { KEYCODE_KEYPAD, '\n', 'M' }, // KP_ENTER + { KEYCODE_KEYPAD, '=', 'X' }, // KP_EQUAL +}; + +void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod) +{ + if (key == VTERM_KEY_NONE) { + return; + } + + keycodes_s k; + if (key < VTERM_KEY_FUNCTION_0) { + if (key >= sizeof(keycodes)/sizeof(keycodes[0])) { + return; + } + k = keycodes[key]; + } else if (key >= VTERM_KEY_FUNCTION_0 && key <= VTERM_KEY_FUNCTION_MAX) { + if ((key - VTERM_KEY_FUNCTION_0) >= sizeof(keycodes_fn)/sizeof(keycodes_fn[0])) { + return; + } + k = keycodes_fn[key - VTERM_KEY_FUNCTION_0]; + } else if (key >= VTERM_KEY_KP_0) { + if ((key - VTERM_KEY_KP_0) >= sizeof(keycodes_kp)/sizeof(keycodes_kp[0])) { + return; + } + k = keycodes_kp[key - VTERM_KEY_KP_0]; + } + + switch (k.type) { + case KEYCODE_NONE: + break; + + case KEYCODE_TAB: + // Shift-Tab is CSI Z but plain Tab is 0x09 + if (mod == VTERM_MOD_SHIFT) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "Z"); + } else if (mod & VTERM_MOD_SHIFT) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%dZ", mod + 1); + } else { + goto case_LITERAL; + } + break; + + case KEYCODE_ENTER: + // Enter is CRLF in newline mode, but just LF in linefeed + if (vt->state->mode.newline) { + vterm_push_output_sprintf(vt, "\r\n"); + } else { + goto case_LITERAL; + } + break; + + case KEYCODE_LITERAL: + case_LITERAL: + if (mod & (VTERM_MOD_SHIFT|VTERM_MOD_CTRL)) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", k.literal, mod + 1); + } else { + vterm_push_output_sprintf(vt, mod & VTERM_MOD_ALT ? ESC_S "%c" : "%c", k.literal); + } + break; + + case KEYCODE_SS3: + case_SS3: + if (mod == 0) { + vterm_push_output_sprintf_ctrl(vt, C1_SS3, "%c", k.literal); + } else { + goto case_CSI; + } + break; + + case KEYCODE_CSI: + case_CSI: + if (mod == 0) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%c", k.literal); + } else { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%d%c", mod + 1, k.literal); + } + break; + + case KEYCODE_CSINUM: + if (mod == 0) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d%c", k.csinum, k.literal); + } else { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%d%c", k.csinum, mod + 1, k.literal); + } + break; + + case KEYCODE_CSI_CURSOR: + if (vt->state->mode.cursor) { + goto case_SS3; + } else { + goto case_CSI; + } + + case KEYCODE_KEYPAD: + if (vt->state->mode.keypad) { + k.literal = (char)k.csinum; + goto case_SS3; + } else { + goto case_LITERAL; + } + } +} + +void vterm_keyboard_start_paste(VTerm *vt) +{ + if (vt->state->mode.bracketpaste) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "200~"); + } +} + +void vterm_keyboard_end_paste(VTerm *vt) +{ + if (vt->state->mode.bracketpaste) { + vterm_push_output_sprintf_ctrl(vt, C1_CSI, "201~"); + } +} diff --git a/src/nvim/vterm/keyboard.h b/src/nvim/vterm/keyboard.h new file mode 100644 index 0000000000..af5f4a3ed1 --- /dev/null +++ b/src/nvim/vterm/keyboard.h @@ -0,0 +1,10 @@ +#pragma once + +#include <stddef.h> + +#include "nvim/vterm/vterm_defs.h" +#include "nvim/vterm/vterm_keycodes_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/keyboard.h.generated.h" +#endif diff --git a/src/nvim/vterm/mouse.c b/src/nvim/vterm/mouse.c new file mode 100644 index 0000000000..9b8be4b60a --- /dev/null +++ b/src/nvim/vterm/mouse.c @@ -0,0 +1,113 @@ +#include "nvim/tui/termkey/termkey.h" +#include "nvim/vterm/mouse.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/mouse.c.generated.h" +#endif + +static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row) +{ + modifiers <<= 2; + + switch (state->mouse_protocol) { + case MOUSE_X10: + if (col + 0x21 > 0xff) { + col = 0xff - 0x21; + } + if (row + 0x21 > 0xff) { + row = 0xff - 0x21; + } + + if (!pressed) { + code = 3; + } + + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c", + (code | modifiers) + 0x20, col + 0x21, row + 0x21); + break; + + case MOUSE_UTF8: { + char utf8[18]; + size_t len = 0; + + if (!pressed) { + code = 3; + } + + len += (size_t)fill_utf8((code | modifiers) + 0x20, utf8 + len); + len += (size_t)fill_utf8(col + 0x21, utf8 + len); + len += (size_t)fill_utf8(row + 0x21, utf8 + len); + utf8[len] = 0; + + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8); + } + break; + + case MOUSE_SGR: + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c", + code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm'); + break; + + case MOUSE_RXVT: + if (!pressed) { + code = 3; + } + + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM", + code | modifiers, col + 1, row + 1); + break; + } +} + +void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod) +{ + VTermState *state = vt->state; + + if (col == state->mouse_col && row == state->mouse_row) { + return; + } + + state->mouse_col = col; + state->mouse_row = row; + + if ((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons) + || (state->mouse_flags & MOUSE_WANT_MOVE)) { + int button = state->mouse_buttons & 0x01 ? 1 + : state->mouse_buttons & 0x02 ? 2 + : state->mouse_buttons & + 0x04 ? 3 : 4; + output_mouse(state, button - 1 + 0x20, 1, (int)mod, col, row); + } +} + +void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod) +{ + VTermState *state = vt->state; + + int old_buttons = state->mouse_buttons; + + if (button > 0 && button <= 3) { + if (pressed) { + state->mouse_buttons |= (1 << (button - 1)); + } else { + state->mouse_buttons &= ~(1 << (button - 1)); + } + } + + // Most of the time we don't get button releases from 4/5 + if (state->mouse_buttons == old_buttons && button < 4) { + return; + } + + if (!state->mouse_flags) { + return; + } + + if (button < 4) { + output_mouse(state, button - 1, pressed, (int)mod, state->mouse_col, state->mouse_row); + } else if (button < 8) { + output_mouse(state, button - 4 + 0x40, pressed, (int)mod, state->mouse_col, state->mouse_row); + } +} diff --git a/src/nvim/vterm/mouse.h b/src/nvim/vterm/mouse.h new file mode 100644 index 0000000000..477f4028a2 --- /dev/null +++ b/src/nvim/vterm/mouse.h @@ -0,0 +1,10 @@ +#pragma once + +#include <stddef.h> + +#include "nvim/vterm/vterm_defs.h" +#include "nvim/vterm/vterm_keycodes_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/mouse.h.generated.h" +#endif diff --git a/src/nvim/vterm/parser.c b/src/nvim/vterm/parser.c new file mode 100644 index 0000000000..79d348f2c1 --- /dev/null +++ b/src/nvim/vterm/parser.c @@ -0,0 +1,411 @@ +#include <assert.h> +#include <stdio.h> +#include <string.h> + +#include "nvim/vterm/parser.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/parser.c.generated.h" +#endif + +#undef DEBUG_PARSER + +static bool is_intermed(uint8_t c) +{ + return c >= 0x20 && c <= 0x2f; +} + +static void do_control(VTerm *vt, uint8_t control) +{ + if (vt->parser.callbacks && vt->parser.callbacks->control) { + if ((*vt->parser.callbacks->control)(control, vt->parser.cbdata)) { + return; + } + } + + DEBUG_LOG("libvterm: Unhandled control 0x%02x\n", control); +} + +static void do_csi(VTerm *vt, char command) +{ +#ifdef DEBUG_PARSER + printf("Parsed CSI args as:\n", arglen, args); + printf(" leader: %s\n", vt->parser.v.csi.leader); + for (int argi = 0; argi < vt->parser.v.csi.argi; argi++) { + printf(" %lu", CSI_ARG(vt->parser.v.csi.args[argi])); + if (!CSI_ARG_HAS_MORE(vt->parser.v.csi.args[argi])) { + printf("\n"); + } + printf(" intermed: %s\n", vt->parser.intermed); + } +#endif + + if (vt->parser.callbacks && vt->parser.callbacks->csi) { + if ((*vt->parser.callbacks->csi)(vt->parser.v.csi.leaderlen ? vt->parser.v.csi.leader : NULL, + vt->parser.v.csi.args, + vt->parser.v.csi.argi, + vt->parser.intermedlen ? vt->parser.intermed : NULL, + command, + vt->parser.cbdata)) { + return; + } + } + + DEBUG_LOG("libvterm: Unhandled CSI %c\n", command); +} + +static void do_escape(VTerm *vt, char command) +{ + char seq[INTERMED_MAX + 1]; + + size_t len = (size_t)vt->parser.intermedlen; + strncpy(seq, vt->parser.intermed, len); // NOLINT(runtime/printf) + seq[len++] = command; + seq[len] = 0; + + if (vt->parser.callbacks && vt->parser.callbacks->escape) { + if ((*vt->parser.callbacks->escape)(seq, len, vt->parser.cbdata)) { + return; + } + } + + DEBUG_LOG("libvterm: Unhandled escape ESC 0x%02x\n", command); +} + +static void string_fragment(VTerm *vt, const char *str, size_t len, bool final) +{ + VTermStringFragment frag = { + .str = str, + .len = len, + .initial = vt->parser.string_initial, + .final = final, + }; + + switch (vt->parser.state) { + case OSC: + if (vt->parser.callbacks && vt->parser.callbacks->osc) { + (*vt->parser.callbacks->osc)(vt->parser.v.osc.command, frag, vt->parser.cbdata); + } + break; + + case DCS_VTERM: + if (vt->parser.callbacks && vt->parser.callbacks->dcs) { + (*vt->parser.callbacks->dcs)(vt->parser.v.dcs.command, (size_t)vt->parser.v.dcs.commandlen, + frag, + vt->parser.cbdata); + } + break; + + case APC: + if (vt->parser.callbacks && vt->parser.callbacks->apc) { + (*vt->parser.callbacks->apc)(frag, vt->parser.cbdata); + } + break; + + case PM: + if (vt->parser.callbacks && vt->parser.callbacks->pm) { + (*vt->parser.callbacks->pm)(frag, vt->parser.cbdata); + } + break; + + case SOS: + if (vt->parser.callbacks && vt->parser.callbacks->sos) { + (*vt->parser.callbacks->sos)(frag, vt->parser.cbdata); + } + break; + + case NORMAL: + case CSI_LEADER: + case CSI_ARGS: + case CSI_INTERMED: + case OSC_COMMAND: + case DCS_COMMAND: + break; + } + + vt->parser.string_initial = false; +} + +size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len) +{ + size_t pos = 0; + const char *string_start; + + switch (vt->parser.state) { + case NORMAL: + case CSI_LEADER: + case CSI_ARGS: + case CSI_INTERMED: + case OSC_COMMAND: + case DCS_COMMAND: + string_start = NULL; + break; + case OSC: + case DCS_VTERM: + case APC: + case PM: + case SOS: + string_start = bytes; + break; + } + +#define ENTER_STATE(st) do { vt->parser.state = st; string_start = NULL; } while (0) +#define ENTER_NORMAL_STATE() ENTER_STATE(NORMAL) + +#define IS_STRING_STATE() (vt->parser.state >= OSC_COMMAND) + + for (; pos < len; pos++) { + uint8_t c = (uint8_t)bytes[pos]; + bool c1_allowed = !vt->mode.utf8; + + if (c == 0x00 || c == 0x7f) { // NUL, DEL + if (IS_STRING_STATE()) { + string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false); + string_start = bytes + pos + 1; + } + if (vt->parser.emit_nul) { + do_control(vt, c); + } + continue; + } + if (c == 0x18 || c == 0x1a) { // CAN, SUB + vt->parser.in_esc = false; + ENTER_NORMAL_STATE(); + if (vt->parser.emit_nul) { + do_control(vt, c); + } + continue; + } else if (c == 0x1b) { // ESC + vt->parser.intermedlen = 0; + if (!IS_STRING_STATE()) { + vt->parser.state = NORMAL; + } + vt->parser.in_esc = true; + continue; + } else if (c == 0x07 // BEL, can stand for ST in OSC or DCS state + && IS_STRING_STATE()) {} else if (c < 0x20) { // other C0 + if (vt->parser.state == SOS) { + continue; // All other C0s permitted in SOS + } + if (IS_STRING_STATE()) { + string_fragment(vt, string_start, (size_t)(bytes + pos - string_start), false); + } + do_control(vt, c); + if (IS_STRING_STATE()) { + string_start = bytes + pos + 1; + } + continue; + } + + size_t string_len = (size_t)(bytes + pos - string_start); + + if (vt->parser.in_esc) { + // Hoist an ESC letter into a C1 if we're not in a string mode + // Always accept ESC \ == ST even in string mode + if (!vt->parser.intermedlen + && c >= 0x40 && c < 0x60 + && ((!IS_STRING_STATE() || c == 0x5c))) { + c += 0x40; + c1_allowed = true; + if (string_len) { + assert(string_len > 0); + string_len -= 1; + } + vt->parser.in_esc = false; + } else { + string_start = NULL; + vt->parser.state = NORMAL; + } + } + + switch (vt->parser.state) { + case CSI_LEADER: + // Extract leader bytes 0x3c to 0x3f + if (c >= 0x3c && c <= 0x3f) { + if (vt->parser.v.csi.leaderlen < CSI_LEADER_MAX - 1) { + vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen++] = (char)c; + } + break; + } + + vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen] = 0; + + vt->parser.v.csi.argi = 0; + vt->parser.v.csi.args[0] = CSI_ARG_MISSING; + vt->parser.state = CSI_ARGS; + + FALLTHROUGH; + case CSI_ARGS: + // Numerical value of argument + if (c >= '0' && c <= '9') { + if (vt->parser.v.csi.args[vt->parser.v.csi.argi] == CSI_ARG_MISSING) { + vt->parser.v.csi.args[vt->parser.v.csi.argi] = 0; + } + vt->parser.v.csi.args[vt->parser.v.csi.argi] *= 10; + vt->parser.v.csi.args[vt->parser.v.csi.argi] += c - '0'; + break; + } + if (c == ':') { + vt->parser.v.csi.args[vt->parser.v.csi.argi] |= CSI_ARG_FLAG_MORE; + c = ';'; + } + if (c == ';') { + vt->parser.v.csi.argi++; + vt->parser.v.csi.args[vt->parser.v.csi.argi] = CSI_ARG_MISSING; + break; + } + + vt->parser.v.csi.argi++; + vt->parser.intermedlen = 0; + vt->parser.state = CSI_INTERMED; + FALLTHROUGH; + case CSI_INTERMED: + if (is_intermed(c)) { + if (vt->parser.intermedlen < INTERMED_MAX - 1) { + vt->parser.intermed[vt->parser.intermedlen++] = (char)c; + } + break; + } else if (c == 0x1b) { + // ESC in CSI cancels + } else if (c >= 0x40 && c <= 0x7e) { + vt->parser.intermed[vt->parser.intermedlen] = 0; + do_csi(vt, (char)c); + } + // else was invalid CSI + + ENTER_NORMAL_STATE(); + break; + + case OSC_COMMAND: + // Numerical value of command + if (c >= '0' && c <= '9') { + if (vt->parser.v.osc.command == -1) { + vt->parser.v.osc.command = 0; + } else { + vt->parser.v.osc.command *= 10; + } + vt->parser.v.osc.command += c - '0'; + break; + } + if (c == ';') { + vt->parser.state = OSC; + string_start = bytes + pos + 1; + break; + } + + string_start = bytes + pos; + string_len = 0; + vt->parser.state = OSC; + goto string_state; + + case DCS_COMMAND: + if (vt->parser.v.dcs.commandlen < CSI_LEADER_MAX) { + vt->parser.v.dcs.command[vt->parser.v.dcs.commandlen++] = (char)c; + } + + if (c >= 0x40 && c <= 0x7e) { + string_start = bytes + pos + 1; + vt->parser.state = DCS_VTERM; + } + break; + +string_state: + case OSC: + case DCS_VTERM: + case APC: + case PM: + case SOS: + if (c == 0x07 || (c1_allowed && c == 0x9c)) { + string_fragment(vt, string_start, string_len, true); + ENTER_NORMAL_STATE(); + } + break; + + case NORMAL: + if (vt->parser.in_esc) { + if (is_intermed(c)) { + if (vt->parser.intermedlen < INTERMED_MAX - 1) { + vt->parser.intermed[vt->parser.intermedlen++] = (char)c; + } + } else if (c >= 0x30 && c < 0x7f) { + do_escape(vt, (char)c); + vt->parser.in_esc = 0; + ENTER_NORMAL_STATE(); + } else { + DEBUG_LOG("TODO: Unhandled byte %02x in Escape\n", c); + } + break; + } + if (c1_allowed && c >= 0x80 && c < 0xa0) { + switch (c) { + case 0x90: // DCS + vt->parser.string_initial = true; + vt->parser.v.dcs.commandlen = 0; + ENTER_STATE(DCS_COMMAND); + break; + case 0x98: // SOS + vt->parser.string_initial = true; + ENTER_STATE(SOS); + string_start = bytes + pos + 1; + break; + case 0x9b: // CSI + vt->parser.v.csi.leaderlen = 0; + ENTER_STATE(CSI_LEADER); + break; + case 0x9d: // OSC + vt->parser.v.osc.command = -1; + vt->parser.string_initial = true; + ENTER_STATE(OSC_COMMAND); + break; + case 0x9e: // PM + vt->parser.string_initial = true; + ENTER_STATE(PM); + string_start = bytes + pos + 1; + break; + case 0x9f: // APC + vt->parser.string_initial = true; + ENTER_STATE(APC); + string_start = bytes + pos + 1; + break; + default: + do_control(vt, c); + break; + } + } else { + size_t eaten = 0; + if (vt->parser.callbacks && vt->parser.callbacks->text) { + eaten = (size_t)(*vt->parser.callbacks->text)(bytes + pos, len - pos, vt->parser.cbdata); + } + + if (!eaten) { + DEBUG_LOG("libvterm: Text callback did not consume any input\n"); + // force it to make progress + eaten = 1; + } + + pos += (eaten - 1); // we'll ++ it again in a moment + } + break; + } + } + + if (string_start) { + size_t string_len = (size_t)(bytes + pos - string_start); + if (string_len > 0) { + if (vt->parser.in_esc) { + string_len -= 1; + } + string_fragment(vt, string_start, string_len, false); + } + } + + return len; +} + +void vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user) +{ + vt->parser.callbacks = callbacks; + vt->parser.cbdata = user; +} diff --git a/src/nvim/vterm/parser.h b/src/nvim/vterm/parser.h new file mode 100644 index 0000000000..168be830c0 --- /dev/null +++ b/src/nvim/vterm/parser.h @@ -0,0 +1,9 @@ +#pragma once + +#include <stddef.h> + +#include "nvim/vterm/vterm_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/parser.h.generated.h" +#endif diff --git a/src/nvim/vterm/pen.c b/src/nvim/vterm/pen.c new file mode 100644 index 0000000000..e7f50078ae --- /dev/null +++ b/src/nvim/vterm/pen.c @@ -0,0 +1,644 @@ +#include <stdio.h> + +#include "nvim/vterm/pen.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/pen.c.generated.h" +#endif + +// Structure used to store RGB triples without the additional metadata stored in VTermColor. +typedef struct { + uint8_t red, green, blue; +} VTermRGB; + +static const VTermRGB ansi_colors[] = { + // R G B + { 0, 0, 0 }, // black + { 224, 0, 0 }, // red + { 0, 224, 0 }, // green + { 224, 224, 0 }, // yellow + { 0, 0, 224 }, // blue + { 224, 0, 224 }, // magenta + { 0, 224, 224 }, // cyan + { 224, 224, 224 }, // white == light grey + + // high intensity + { 128, 128, 128 }, // black + { 255, 64, 64 }, // red + { 64, 255, 64 }, // green + { 255, 255, 64 }, // yellow + { 64, 64, 255 }, // blue + { 255, 64, 255 }, // magenta + { 64, 255, 255 }, // cyan + { 255, 255, 255 }, // white for real +}; + +static uint8_t ramp6[] = { + 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF, +}; + +static uint8_t ramp24[] = { + 0x00, 0x0B, 0x16, 0x21, 0x2C, 0x37, 0x42, 0x4D, 0x58, 0x63, 0x6E, 0x79, + 0x85, 0x90, 0x9B, 0xA6, 0xB1, 0xBC, 0xC7, 0xD2, 0xDD, 0xE8, 0xF3, 0xFF, +}; + +static void lookup_default_colour_ansi(long idx, VTermColor *col) +{ + if (idx >= 0 && idx < 16) { + vterm_color_rgb(col, + ansi_colors[idx].red, ansi_colors[idx].green, ansi_colors[idx].blue); + } +} + +static bool lookup_colour_ansi(const VTermState *state, long index, VTermColor *col) +{ + if (index >= 0 && index < 16) { + *col = state->colors[index]; + return true; + } + + return false; +} + +static bool lookup_colour_palette(const VTermState *state, long index, VTermColor *col) +{ + if (index >= 0 && index < 16) { + // Normal 8 colours or high intensity - parse as palette 0 + return lookup_colour_ansi(state, index, col); + } else if (index >= 16 && index < 232) { + // 216-colour cube + index -= 16; + + vterm_color_rgb(col, ramp6[index/6/6 % 6], + ramp6[index/6 % 6], + ramp6[index % 6]); + + return true; + } else if (index >= 232 && index < 256) { + // 24 greyscales + index -= 232; + + vterm_color_rgb(col, ramp24[index], ramp24[index], ramp24[index]); + + return true; + } + + return false; +} + +static int lookup_colour(const VTermState *state, int palette, const long args[], int argcount, + VTermColor *col) +{ + switch (palette) { + case 2: // RGB mode - 3 args contain colour values directly + if (argcount < 3) { + return argcount; + } + + vterm_color_rgb(col, (uint8_t)CSI_ARG(args[0]), (uint8_t)CSI_ARG(args[1]), + (uint8_t)CSI_ARG(args[2])); + + return 3; + + case 5: // XTerm 256-colour mode + if (!argcount || CSI_ARG_IS_MISSING(args[0])) { + return argcount ? 1 : 0; + } + + vterm_color_indexed(col, (uint8_t)args[0]); + + return argcount ? 1 : 0; + + default: + DEBUG_LOG("Unrecognised colour palette %d\n", palette); + return 0; + } +} + +// Some conveniences + +static void setpenattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val) +{ +#ifdef DEBUG + if (type != vterm_get_attr_type(attr)) { + DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n", + attr, vterm_get_attr_type(attr), type); + return; + } +#endif + if (state->callbacks && state->callbacks->setpenattr) { + (*state->callbacks->setpenattr)(attr, val, state->cbdata); + } +} + +static void setpenattr_bool(VTermState *state, VTermAttr attr, int boolean) +{ + VTermValue val = { .boolean = boolean }; + setpenattr(state, attr, VTERM_VALUETYPE_BOOL, &val); +} + +static void setpenattr_int(VTermState *state, VTermAttr attr, int number) +{ + VTermValue val = { .number = number }; + setpenattr(state, attr, VTERM_VALUETYPE_INT, &val); +} + +static void setpenattr_col(VTermState *state, VTermAttr attr, VTermColor color) +{ + VTermValue val = { .color = color }; + setpenattr(state, attr, VTERM_VALUETYPE_COLOR, &val); +} + +static void set_pen_col_ansi(VTermState *state, VTermAttr attr, long col) +{ + VTermColor *colp = (attr == VTERM_ATTR_BACKGROUND) ? &state->pen.bg : &state->pen.fg; + + vterm_color_indexed(colp, (uint8_t)col); + + setpenattr_col(state, attr, *colp); +} + +void vterm_state_newpen(VTermState *state) +{ + // 90% grey so that pure white is brighter + vterm_color_rgb(&state->default_fg, 240, 240, 240); + vterm_color_rgb(&state->default_bg, 0, 0, 0); + vterm_state_set_default_colors(state, &state->default_fg, &state->default_bg); + + for (int col = 0; col < 16; col++) { + lookup_default_colour_ansi(col, &state->colors[col]); + } +} + +void vterm_state_resetpen(VTermState *state) +{ + state->pen.bold = 0; setpenattr_bool(state, VTERM_ATTR_BOLD, 0); + state->pen.underline = 0; setpenattr_int(state, VTERM_ATTR_UNDERLINE, 0); + state->pen.italic = 0; setpenattr_bool(state, VTERM_ATTR_ITALIC, 0); + state->pen.blink = 0; setpenattr_bool(state, VTERM_ATTR_BLINK, 0); + state->pen.reverse = 0; setpenattr_bool(state, VTERM_ATTR_REVERSE, 0); + state->pen.conceal = 0; setpenattr_bool(state, VTERM_ATTR_CONCEAL, 0); + state->pen.strike = 0; setpenattr_bool(state, VTERM_ATTR_STRIKE, 0); + state->pen.font = 0; setpenattr_int(state, VTERM_ATTR_FONT, 0); + state->pen.small = 0; setpenattr_bool(state, VTERM_ATTR_SMALL, 0); + state->pen.baseline = 0; setpenattr_int(state, VTERM_ATTR_BASELINE, 0); + + state->pen.fg = state->default_fg; + setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg); + state->pen.bg = state->default_bg; + setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg); + + state->pen.uri = 0; setpenattr_int(state, VTERM_ATTR_URI, 0); +} + +void vterm_state_savepen(VTermState *state, int save) +{ + if (save) { + state->saved.pen = state->pen; + } else { + state->pen = state->saved.pen; + + setpenattr_bool(state, VTERM_ATTR_BOLD, state->pen.bold); + setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); + setpenattr_bool(state, VTERM_ATTR_ITALIC, state->pen.italic); + setpenattr_bool(state, VTERM_ATTR_BLINK, state->pen.blink); + setpenattr_bool(state, VTERM_ATTR_REVERSE, state->pen.reverse); + setpenattr_bool(state, VTERM_ATTR_CONCEAL, state->pen.conceal); + setpenattr_bool(state, VTERM_ATTR_STRIKE, state->pen.strike); + setpenattr_int(state, VTERM_ATTR_FONT, state->pen.font); + setpenattr_bool(state, VTERM_ATTR_SMALL, state->pen.small); + setpenattr_int(state, VTERM_ATTR_BASELINE, state->pen.baseline); + + setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); + setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); + + setpenattr_int(state, VTERM_ATTR_URI, state->pen.uri); + } +} + +void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, + const VTermColor *default_bg) +{ + if (default_fg) { + state->default_fg = *default_fg; + state->default_fg.type = (state->default_fg.type & ~VTERM_COLOR_DEFAULT_MASK) + | VTERM_COLOR_DEFAULT_FG; + } + + if (default_bg) { + state->default_bg = *default_bg; + state->default_bg.type = (state->default_bg.type & ~VTERM_COLOR_DEFAULT_MASK) + | VTERM_COLOR_DEFAULT_BG; + } +} + +void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col) +{ + if (index >= 0 && index < 16) { + state->colors[index] = *col; + } +} + +/// Makes sure that the given color `col` is indeed an RGB colour. After this +/// function returns, VTERM_COLOR_IS_RGB(col) will return true, while all other +/// flags stored in `col->type` will have been reset. +/// +/// @param state is the VTermState instance from which the colour palette should +/// be extracted. +/// @param col is a pointer at the VTermColor instance that should be converted +/// to an RGB colour. +void vterm_state_convert_color_to_rgb(const VTermState *state, VTermColor *col) +{ + if (VTERM_COLOR_IS_INDEXED(col)) { // Convert indexed colors to RGB + lookup_colour_palette(state, col->indexed.idx, col); + } + col->type &= VTERM_COLOR_TYPE_MASK; // Reset any metadata but the type +} + +void vterm_state_setpen(VTermState *state, const long args[], int argcount) +{ + // SGR - ECMA-48 8.3.117 + + int argi = 0; + int value; + + while (argi < argcount) { + // This logic is easier to do 'done' backwards; set it true, and make it + // false again in the 'default' case + int done = 1; + + long arg; + switch (arg = CSI_ARG(args[argi])) { + case CSI_ARG_MISSING: + case 0: // Reset + vterm_state_resetpen(state); + break; + + case 1: { // Bold on + const VTermColor *fg = &state->pen.fg; + state->pen.bold = 1; + setpenattr_bool(state, VTERM_ATTR_BOLD, 1); + if (!VTERM_COLOR_IS_DEFAULT_FG(fg) && VTERM_COLOR_IS_INDEXED(fg) && fg->indexed.idx < 8 + && state->bold_is_highbright) { + set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, fg->indexed.idx + (state->pen.bold ? 8 : 0)); + } + break; + } + + case 3: // Italic on + state->pen.italic = 1; + setpenattr_bool(state, VTERM_ATTR_ITALIC, 1); + break; + + case 4: // Underline + state->pen.underline = VTERM_UNDERLINE_SINGLE; + if (CSI_ARG_HAS_MORE(args[argi])) { + argi++; + switch (CSI_ARG(args[argi])) { + case 0: + state->pen.underline = 0; + break; + case 1: + state->pen.underline = VTERM_UNDERLINE_SINGLE; + break; + case 2: + state->pen.underline = VTERM_UNDERLINE_DOUBLE; + break; + case 3: + state->pen.underline = VTERM_UNDERLINE_CURLY; + break; + } + } + setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); + break; + + case 5: // Blink + state->pen.blink = 1; + setpenattr_bool(state, VTERM_ATTR_BLINK, 1); + break; + + case 7: // Reverse on + state->pen.reverse = 1; + setpenattr_bool(state, VTERM_ATTR_REVERSE, 1); + break; + + case 8: // Conceal on + state->pen.conceal = 1; + setpenattr_bool(state, VTERM_ATTR_CONCEAL, 1); + break; + + case 9: // Strikethrough on + state->pen.strike = 1; + setpenattr_bool(state, VTERM_ATTR_STRIKE, 1); + break; + + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: // Select font + state->pen.font = CSI_ARG(args[argi]) - 10; + setpenattr_int(state, VTERM_ATTR_FONT, state->pen.font); + break; + + case 21: // Underline double + state->pen.underline = VTERM_UNDERLINE_DOUBLE; + setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); + break; + + case 22: // Bold off + state->pen.bold = 0; + setpenattr_bool(state, VTERM_ATTR_BOLD, 0); + break; + + case 23: // Italic and Gothic (currently unsupported) off + state->pen.italic = 0; + setpenattr_bool(state, VTERM_ATTR_ITALIC, 0); + break; + + case 24: // Underline off + state->pen.underline = 0; + setpenattr_int(state, VTERM_ATTR_UNDERLINE, 0); + break; + + case 25: // Blink off + state->pen.blink = 0; + setpenattr_bool(state, VTERM_ATTR_BLINK, 0); + break; + + case 27: // Reverse off + state->pen.reverse = 0; + setpenattr_bool(state, VTERM_ATTR_REVERSE, 0); + break; + + case 28: // Conceal off (Reveal) + state->pen.conceal = 0; + setpenattr_bool(state, VTERM_ATTR_CONCEAL, 0); + break; + + case 29: // Strikethrough off + state->pen.strike = 0; + setpenattr_bool(state, VTERM_ATTR_STRIKE, 0); + break; + + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: // Foreground colour palette + value = CSI_ARG(args[argi]) - 30; + if (state->pen.bold && state->bold_is_highbright) { + value += 8; + } + set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value); + break; + + case 38: // Foreground colour alternative palette + if (argcount - argi < 1) { + return; + } + argi += 1 + lookup_colour(state, CSI_ARG(args[argi + 1]), args + argi + 2, + argcount - argi - 2, &state->pen.fg); + setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); + break; + + case 39: // Foreground colour default + state->pen.fg = state->default_fg; + setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); + break; + + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: // Background colour palette + value = CSI_ARG(args[argi]) - 40; + set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value); + break; + + case 48: // Background colour alternative palette + if (argcount - argi < 1) { + return; + } + argi += 1 + lookup_colour(state, CSI_ARG(args[argi + 1]), args + argi + 2, + argcount - argi - 2, &state->pen.bg); + setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); + break; + + case 49: // Default background + state->pen.bg = state->default_bg; + setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); + break; + + case 73: // Superscript + case 74: // Subscript + case 75: // Superscript/subscript off + state->pen.small = (arg != 75); + state->pen.baseline = + (arg == 73) ? VTERM_BASELINE_RAISE + : (arg == 74) ? VTERM_BASELINE_LOWER + : VTERM_BASELINE_NORMAL; + setpenattr_bool(state, VTERM_ATTR_SMALL, state->pen.small); + setpenattr_int(state, VTERM_ATTR_BASELINE, state->pen.baseline); + break; + + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: // Foreground colour high-intensity palette + value = CSI_ARG(args[argi]) - 90 + 8; + set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value); + break; + + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: // Background colour high-intensity palette + value = CSI_ARG(args[argi]) - 100 + 8; + set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value); + break; + + default: + done = 0; + break; + } + + if (!done) { + DEBUG_LOG("libvterm: Unhandled CSI SGR %ld\n", arg); + } + + while (CSI_ARG_HAS_MORE(args[argi++])) {} + } +} + +static int vterm_state_getpen_color(const VTermColor *col, int argi, long args[], int fg) +{ + // Do nothing if the given color is the default color + if ((fg && VTERM_COLOR_IS_DEFAULT_FG(col)) + || (!fg && VTERM_COLOR_IS_DEFAULT_BG(col))) { + return argi; + } + + // Decide whether to send an indexed color or an RGB color + if (VTERM_COLOR_IS_INDEXED(col)) { + const uint8_t idx = col->indexed.idx; + if (idx < 8) { + args[argi++] = (idx + (fg ? 30 : 40)); + } else if (idx < 16) { + args[argi++] = (idx - 8 + (fg ? 90 : 100)); + } else { + args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48); + args[argi++] = CSI_ARG_FLAG_MORE | 5; + args[argi++] = idx; + } + } else if (VTERM_COLOR_IS_RGB(col)) { + args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48); + args[argi++] = CSI_ARG_FLAG_MORE | 2; + args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.red; + args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.green; + args[argi++] = col->rgb.blue; + } + return argi; +} + +int vterm_state_getpen(VTermState *state, long args[], int argcount) +{ + int argi = 0; + + if (state->pen.bold) { + args[argi++] = 1; + } + + if (state->pen.italic) { + args[argi++] = 3; + } + + if (state->pen.underline == VTERM_UNDERLINE_SINGLE) { + args[argi++] = 4; + } + if (state->pen.underline == VTERM_UNDERLINE_CURLY) { + args[argi++] = 4 | CSI_ARG_FLAG_MORE, args[argi++] = 3; + } + + if (state->pen.blink) { + args[argi++] = 5; + } + + if (state->pen.reverse) { + args[argi++] = 7; + } + + if (state->pen.conceal) { + args[argi++] = 8; + } + + if (state->pen.strike) { + args[argi++] = 9; + } + + if (state->pen.font) { + args[argi++] = 10 + state->pen.font; + } + + if (state->pen.underline == VTERM_UNDERLINE_DOUBLE) { + args[argi++] = 21; + } + + argi = vterm_state_getpen_color(&state->pen.fg, argi, args, true); + + argi = vterm_state_getpen_color(&state->pen.bg, argi, args, false); + + if (state->pen.small) { + if (state->pen.baseline == VTERM_BASELINE_RAISE) { + args[argi++] = 73; + } else if (state->pen.baseline == VTERM_BASELINE_LOWER) { + args[argi++] = 74; + } + } + + return argi; +} + +int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val) +{ + if (!val) { + return 0; + } + + if (type != vterm_get_attr_type(attr)) { + DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n", + attr, vterm_get_attr_type(attr), type); + return 0; + } + + switch (attr) { + case VTERM_ATTR_BOLD: + state->pen.bold = (unsigned)val->boolean; + break; + case VTERM_ATTR_UNDERLINE: + state->pen.underline = (unsigned)val->number; + break; + case VTERM_ATTR_ITALIC: + state->pen.italic = (unsigned)val->boolean; + break; + case VTERM_ATTR_BLINK: + state->pen.blink = (unsigned)val->boolean; + break; + case VTERM_ATTR_REVERSE: + state->pen.reverse = (unsigned)val->boolean; + break; + case VTERM_ATTR_CONCEAL: + state->pen.conceal = (unsigned)val->boolean; + break; + case VTERM_ATTR_STRIKE: + state->pen.strike = (unsigned)val->boolean; + break; + case VTERM_ATTR_FONT: + state->pen.font = (unsigned)val->number; + break; + case VTERM_ATTR_FOREGROUND: + state->pen.fg = val->color; + break; + case VTERM_ATTR_BACKGROUND: + state->pen.bg = val->color; + break; + case VTERM_ATTR_SMALL: + state->pen.small = (unsigned)val->boolean; + break; + case VTERM_ATTR_BASELINE: + state->pen.baseline = (unsigned)val->number; + break; + case VTERM_ATTR_URI: + state->pen.uri = val->number; + break; + default: + return 0; + } + + if (state->callbacks && state->callbacks->setpenattr) { + (*state->callbacks->setpenattr)(attr, val, state->cbdata); + } + + return 1; +} diff --git a/src/nvim/vterm/pen.h b/src/nvim/vterm/pen.h new file mode 100644 index 0000000000..c5f5217420 --- /dev/null +++ b/src/nvim/vterm/pen.h @@ -0,0 +1,9 @@ +#pragma once + +#include <stddef.h> + +#include "nvim/vterm/vterm_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/pen.h.generated.h" +#endif diff --git a/src/nvim/vterm/screen.c b/src/nvim/vterm/screen.c new file mode 100644 index 0000000000..c91c6fb84f --- /dev/null +++ b/src/nvim/vterm/screen.c @@ -0,0 +1,1103 @@ +#include <stdio.h> +#include <string.h> + +#include "nvim/grid.h" +#include "nvim/mbyte.h" +#include "nvim/tui/termkey/termkey.h" +#include "nvim/vterm/pen.h" +#include "nvim/vterm/screen.h" +#include "nvim/vterm/state.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_defs.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/screen.c.generated.h" +#endif + +#define UNICODE_SPACE 0x20 +#define UNICODE_LINEFEED 0x0a + +#undef DEBUG_REFLOW + +static inline void clearcell(const VTermScreen *screen, ScreenCell *cell) +{ + cell->schar = 0; + cell->pen = screen->pen; +} + +ScreenCell *getcell(const VTermScreen *screen, int row, int col) +{ + if (row < 0 || row >= screen->rows) { + return NULL; + } + if (col < 0 || col >= screen->cols) { + return NULL; + } + return screen->buffer + (screen->cols * row) + col; +} + +static ScreenCell *alloc_buffer(VTermScreen *screen, int rows, int cols) +{ + ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, + sizeof(ScreenCell) * (size_t)rows * (size_t)cols); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + clearcell(screen, &new_buffer[row * cols + col]); + } + } + + return new_buffer; +} + +static void damagerect(VTermScreen *screen, VTermRect rect) +{ + VTermRect emit; + + switch (screen->damage_merge) { + case VTERM_DAMAGE_CELL: + // Always emit damage event + emit = rect; + break; + + case VTERM_DAMAGE_ROW: + // Emit damage longer than one row. Try to merge with existing damage in the same row + if (rect.end_row > rect.start_row + 1) { + // Bigger than 1 line - flush existing, emit this + vterm_screen_flush_damage(screen); + emit = rect; + } else if (screen->damaged.start_row == -1) { + // None stored yet + screen->damaged = rect; + return; + } else if (rect.start_row == screen->damaged.start_row) { + // Merge with the stored line + if (screen->damaged.start_col > rect.start_col) { + screen->damaged.start_col = rect.start_col; + } + if (screen->damaged.end_col < rect.end_col) { + screen->damaged.end_col = rect.end_col; + } + return; + } else { + // Emit the currently stored line, store a new one + emit = screen->damaged; + screen->damaged = rect; + } + break; + + case VTERM_DAMAGE_SCREEN: + case VTERM_DAMAGE_SCROLL: + // Never emit damage event + if (screen->damaged.start_row == -1) { + screen->damaged = rect; + } else { + rect_expand(&screen->damaged, &rect); + } + return; + + default: + DEBUG_LOG("TODO: Maybe merge damage for level %d\n", screen->damage_merge); + return; + } + + if (screen->callbacks && screen->callbacks->damage) { + (*screen->callbacks->damage)(emit, screen->cbdata); + } +} + +static void damagescreen(VTermScreen *screen) +{ + VTermRect rect = { + .start_row = 0, + .end_row = screen->rows, + .start_col = 0, + .end_col = screen->cols, + }; + + damagerect(screen, rect); +} + +static int putglyph(VTermGlyphInfo *info, VTermPos pos, void *user) +{ + VTermScreen *screen = user; + ScreenCell *cell = getcell(screen, pos.row, pos.col); + + if (!cell) { + return 0; + } + + cell->schar = info->schar; + if (info->schar != 0) { + cell->pen = screen->pen; + } + + for (int col = 1; col < info->width; col++) { + getcell(screen, pos.row, pos.col + col)->schar = (uint32_t)-1; + } + + VTermRect rect = { + .start_row = pos.row, + .end_row = pos.row + 1, + .start_col = pos.col, + .end_col = pos.col + info->width, + }; + + cell->pen.protected_cell = info->protected_cell; + cell->pen.dwl = info->dwl; + cell->pen.dhl = info->dhl; + + damagerect(screen, rect); + + return 1; +} + +static void sb_pushline_from_row(VTermScreen *screen, int row) +{ + VTermPos pos = { .row = row }; + for (pos.col = 0; pos.col < screen->cols; pos.col++) { + vterm_screen_get_cell(screen, pos, screen->sb_buffer + pos.col); + } + + (screen->callbacks->sb_pushline)(screen->cols, screen->sb_buffer, screen->cbdata); +} + +static int moverect_internal(VTermRect dest, VTermRect src, void *user) +{ + VTermScreen *screen = user; + + if (screen->callbacks && screen->callbacks->sb_pushline + && dest.start_row == 0 && dest.start_col == 0 // starts top-left corner + && dest.end_col == screen->cols // full width + && screen->buffer == screen->buffers[BUFIDX_PRIMARY]) { // not altscreen + for (int row = 0; row < src.start_row; row++) { + sb_pushline_from_row(screen, row); + } + } + + int cols = src.end_col - src.start_col; + int downward = src.start_row - dest.start_row; + + int init_row, test_row, inc_row; + if (downward < 0) { + init_row = dest.end_row - 1; + test_row = dest.start_row - 1; + inc_row = -1; + } else { + init_row = dest.start_row; + test_row = dest.end_row; + inc_row = +1; + } + + for (int row = init_row; row != test_row; row += inc_row) { + memmove(getcell(screen, row, dest.start_col), + getcell(screen, row + downward, src.start_col), + (size_t)cols * sizeof(ScreenCell)); + } + + return 1; +} + +static int moverect_user(VTermRect dest, VTermRect src, void *user) +{ + VTermScreen *screen = user; + + if (screen->callbacks && screen->callbacks->moverect) { + if (screen->damage_merge != VTERM_DAMAGE_SCROLL) { + // Avoid an infinite loop + vterm_screen_flush_damage(screen); + } + + if ((*screen->callbacks->moverect)(dest, src, screen->cbdata)) { + return 1; + } + } + + damagerect(screen, dest); + + return 1; +} + +static int erase_internal(VTermRect rect, int selective, void *user) +{ + VTermScreen *screen = user; + + for (int row = rect.start_row; row < screen->state->rows && row < rect.end_row; row++) { + const VTermLineInfo *info = vterm_state_get_lineinfo(screen->state, row); + + for (int col = rect.start_col; col < rect.end_col; col++) { + ScreenCell *cell = getcell(screen, row, col); + + if (selective && cell->pen.protected_cell) { + continue; + } + + cell->schar = 0; + cell->pen = (ScreenPen){ + // Only copy .fg and .bg; leave things like rv in reset state + .fg = screen->pen.fg, + .bg = screen->pen.bg, + }; + cell->pen.dwl = info->doublewidth; + cell->pen.dhl = info->doubleheight; + } + } + + return 1; +} + +static int erase_user(VTermRect rect, int selective, void *user) +{ + VTermScreen *screen = user; + + damagerect(screen, rect); + + return 1; +} + +static int erase(VTermRect rect, int selective, void *user) +{ + erase_internal(rect, selective, user); + return erase_user(rect, 0, user); +} + +static int scrollrect(VTermRect rect, int downward, int rightward, void *user) +{ + VTermScreen *screen = user; + + if (screen->damage_merge != VTERM_DAMAGE_SCROLL) { + vterm_scroll_rect(rect, downward, rightward, + moverect_internal, erase_internal, screen); + + vterm_screen_flush_damage(screen); + + vterm_scroll_rect(rect, downward, rightward, + moverect_user, erase_user, screen); + + return 1; + } + + if (screen->damaged.start_row != -1 + && !rect_intersects(&rect, &screen->damaged)) { + vterm_screen_flush_damage(screen); + } + + if (screen->pending_scrollrect.start_row == -1) { + screen->pending_scrollrect = rect; + screen->pending_scroll_downward = downward; + screen->pending_scroll_rightward = rightward; + } else if (rect_equal(&screen->pending_scrollrect, &rect) + && ((screen->pending_scroll_downward == 0 && downward == 0) + || (screen->pending_scroll_rightward == 0 && rightward == 0))) { + screen->pending_scroll_downward += downward; + screen->pending_scroll_rightward += rightward; + } else { + vterm_screen_flush_damage(screen); + + screen->pending_scrollrect = rect; + screen->pending_scroll_downward = downward; + screen->pending_scroll_rightward = rightward; + } + + vterm_scroll_rect(rect, downward, rightward, + moverect_internal, erase_internal, screen); + + if (screen->damaged.start_row == -1) { + return 1; + } + + if (rect_contains(&rect, &screen->damaged)) { + // Scroll region entirely contains the damage; just move it + vterm_rect_move(&screen->damaged, -downward, -rightward); + rect_clip(&screen->damaged, &rect); + } + // There are a number of possible cases here, but lets restrict this to only the common case where + // we might actually gain some performance by optimising it. Namely, a vertical scroll that neatly + // cuts the damage region in half. + else if (rect.start_col <= screen->damaged.start_col + && rect.end_col >= screen->damaged.end_col + && rightward == 0) { + if (screen->damaged.start_row >= rect.start_row + && screen->damaged.start_row < rect.end_row) { + screen->damaged.start_row -= downward; + if (screen->damaged.start_row < rect.start_row) { + screen->damaged.start_row = rect.start_row; + } + if (screen->damaged.start_row > rect.end_row) { + screen->damaged.start_row = rect.end_row; + } + } + if (screen->damaged.end_row >= rect.start_row + && screen->damaged.end_row < rect.end_row) { + screen->damaged.end_row -= downward; + if (screen->damaged.end_row < rect.start_row) { + screen->damaged.end_row = rect.start_row; + } + if (screen->damaged.end_row > rect.end_row) { + screen->damaged.end_row = rect.end_row; + } + } + } else { + DEBUG_LOG("TODO: Just flush and redo damaged=" STRFrect " rect=" STRFrect "\n", + ARGSrect(screen->damaged), ARGSrect(rect)); + } + + return 1; +} + +static int movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) +{ + VTermScreen *screen = user; + + if (screen->callbacks && screen->callbacks->movecursor) { + return (*screen->callbacks->movecursor)(pos, oldpos, visible, screen->cbdata); + } + + return 0; +} + +static int setpenattr(VTermAttr attr, VTermValue *val, void *user) +{ + VTermScreen *screen = user; + + switch (attr) { + case VTERM_ATTR_BOLD: + screen->pen.bold = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_UNDERLINE: + screen->pen.underline = (unsigned)val->number; + return 1; + case VTERM_ATTR_ITALIC: + screen->pen.italic = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_BLINK: + screen->pen.blink = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_REVERSE: + screen->pen.reverse = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_CONCEAL: + screen->pen.conceal = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_STRIKE: + screen->pen.strike = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_FONT: + screen->pen.font = (unsigned)val->number; + return 1; + case VTERM_ATTR_FOREGROUND: + screen->pen.fg = val->color; + return 1; + case VTERM_ATTR_BACKGROUND: + screen->pen.bg = val->color; + return 1; + case VTERM_ATTR_SMALL: + screen->pen.small = (unsigned)val->boolean; + return 1; + case VTERM_ATTR_BASELINE: + screen->pen.baseline = (unsigned)val->number; + return 1; + case VTERM_ATTR_URI: + screen->pen.uri = val->number; + return 1; + + case VTERM_N_ATTRS: + return 0; + } + + return 0; +} + +static int settermprop(VTermProp prop, VTermValue *val, void *user) +{ + VTermScreen *screen = user; + + switch (prop) { + case VTERM_PROP_ALTSCREEN: + if (val->boolean && !screen->buffers[BUFIDX_ALTSCREEN]) { + return 0; + } + + screen->buffer = + val->boolean ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY]; + // only send a damage event on disable; because during enable there's an erase that sends a + // damage anyway + if (!val->boolean) { + damagescreen(screen); + } + break; + case VTERM_PROP_REVERSE: + screen->global_reverse = (unsigned)val->boolean; + damagescreen(screen); + break; + default: + ; // ignore + } + + if (screen->callbacks && screen->callbacks->settermprop) { + return (*screen->callbacks->settermprop)(prop, val, screen->cbdata); + } + + return 1; +} + +static int bell(void *user) +{ + VTermScreen *screen = user; + + if (screen->callbacks && screen->callbacks->bell) { + return (*screen->callbacks->bell)(screen->cbdata); + } + + return 0; +} + +/// How many cells are non-blank Returns the position of the first blank cell in the trailing blank +/// end +static int line_popcount(ScreenCell *buffer, int row, int rows, int cols) +{ + int col = cols - 1; + while (col >= 0 && buffer[row * cols + col].schar == 0) { + col--; + } + return col + 1; +} + +static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new_cols, bool active, + VTermStateFields *statefields) +{ + int old_rows = screen->rows; + int old_cols = screen->cols; + + ScreenCell *old_buffer = screen->buffers[bufidx]; + VTermLineInfo *old_lineinfo = statefields->lineinfos[bufidx]; + + ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, + sizeof(ScreenCell) * (size_t)new_rows * + (size_t)new_cols); + VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, + sizeof(new_lineinfo[0]) * (size_t)new_rows); + + int old_row = old_rows - 1; + int new_row = new_rows - 1; + + VTermPos old_cursor = statefields->pos; + VTermPos new_cursor = { -1, -1 }; + +#ifdef DEBUG_REFLOW + fprintf(stderr, "Resizing from %dx%d to %dx%d; cursor was at (%d,%d)\n", + old_cols, old_rows, new_cols, new_rows, old_cursor.col, old_cursor.row); +#endif + + // Keep track of the final row that is knonw to be blank, so we know what spare space we have for + // scrolling into + int final_blank_row = new_rows; + + while (old_row >= 0) { + int old_row_end = old_row; + // TODO(vterm): Stop if dwl or dhl + 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 (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); + } + } + + if (final_blank_row == (new_row + 1) && width == 0) { + final_blank_row = new_row; + } + + int new_height = screen->reflow + ? width ? (width + new_cols - 1) / new_cols : 1 + : 1; + + int new_row_end = new_row; + int new_row_start = new_row - new_height + 1; + + old_row = old_row_start; + int old_col = 0; + + int spare_rows = new_rows - final_blank_row; + + if (new_row_start < 0 // we'd fall off the top + && spare_rows >= 0 // we actually have spare rows + && (!active || new_cursor.row == -1 || (new_cursor.row - new_row_start) < new_rows)) { + // Attempt to scroll content down into the blank rows at the bottom to make it fit + int downwards = -new_row_start; + if (downwards > spare_rows) { + downwards = spare_rows; + } + int rowcount = new_rows - downwards; + +#ifdef DEBUG_REFLOW + fprintf(stderr, " scroll %d rows +%d downwards\n", rowcount, downwards); +#endif + + memmove(&new_buffer[downwards * new_cols], &new_buffer[0], + (size_t)rowcount * (size_t)new_cols * sizeof(ScreenCell)); + memmove(&new_lineinfo[downwards], &new_lineinfo[0], + (size_t)rowcount * sizeof(new_lineinfo[0])); + + new_row += downwards; + new_row_start += downwards; + new_row_end += downwards; + + if (new_cursor.row >= 0) { + new_cursor.row += downwards; + } + + final_blank_row += downwards; + } + +#ifdef DEBUG_REFLOW + fprintf(stderr, " rows [%d..%d] <- [%d..%d] width=%d\n", + new_row_start, new_row_end, old_row_start, old_row_end, width); +#endif + + if (new_row_start < 0) { + if (old_row_start <= old_cursor.row && old_cursor.row <= old_row_end) { + new_cursor.row = 0; + new_cursor.col = old_cursor.col; + if (new_cursor.col >= new_cols) { + new_cursor.col = new_cols - 1; + } + } + break; + } + + for (new_row = new_row_start, old_row = old_row_start; new_row <= new_row_end; new_row++) { + int count = width >= new_cols ? new_cols : width; + width -= count; + + int new_col = 0; + + while (count) { + // TODO(vterm): This could surely be done a lot faster by memcpy()'ing the entire range + new_buffer[new_row * new_cols + new_col] = old_buffer[old_row * old_cols + old_col]; + + if (old_cursor.row == old_row && old_cursor.col == old_col) { + new_cursor.row = new_row, new_cursor.col = new_col; + } + + old_col++; + if (old_col == old_cols) { + old_row++; + + if (!screen->reflow) { + new_col++; + break; + } + old_col = 0; + } + + new_col++; + count--; + } + + if (old_cursor.row == old_row && old_cursor.col >= old_col) { + new_cursor.row = new_row, new_cursor.col = (old_cursor.col - old_col + new_col); + if (new_cursor.col >= new_cols) { + new_cursor.col = new_cols - 1; + } + } + + while (new_col < new_cols) { + clearcell(screen, &new_buffer[new_row * new_cols + new_col]); + new_col++; + } + + new_lineinfo[new_row].continuation = (new_row > new_row_start); + } + + old_row = old_row_start - 1; + new_row = new_row_start - 1; + } + + if (old_cursor.row <= old_row) { + // cursor would have moved entirely off the top of the screen; lets just bring it within range + new_cursor.row = 0, new_cursor.col = old_cursor.col; + if (new_cursor.col >= new_cols) { + new_cursor.col = new_cols - 1; + } + } + + // We really expect the cursor position to be set by now + if (active && (new_cursor.row == -1 || new_cursor.col == -1)) { + fprintf(stderr, "screen_resize failed to update cursor position\n"); + abort(); + } + + if (old_row >= 0 && bufidx == BUFIDX_PRIMARY) { + // Push spare lines to scrollback buffer + if (screen->callbacks && screen->callbacks->sb_pushline) { + for (int row = 0; row <= old_row; row++) { + sb_pushline_from_row(screen, row); + } + } + if (active) { + statefields->pos.row -= (old_row + 1); + } + } + if (new_row >= 0 && bufidx == BUFIDX_PRIMARY + && screen->callbacks && screen->callbacks->sb_popline) { + // Try to backfill rows by popping scrollback buffer + while (new_row >= 0) { + if (!(screen->callbacks->sb_popline(old_cols, screen->sb_buffer, screen->cbdata))) { + break; + } + + VTermPos pos = { .row = new_row }; + for (pos.col = 0; pos.col < old_cols && pos.col < new_cols; + pos.col += screen->sb_buffer[pos.col].width) { + VTermScreenCell *src = &screen->sb_buffer[pos.col]; + ScreenCell *dst = &new_buffer[pos.row * new_cols + pos.col]; + + dst->schar = src->schar; + + dst->pen.bold = src->attrs.bold; + dst->pen.underline = src->attrs.underline; + dst->pen.italic = src->attrs.italic; + dst->pen.blink = src->attrs.blink; + dst->pen.reverse = src->attrs.reverse ^ screen->global_reverse; + dst->pen.conceal = src->attrs.conceal; + dst->pen.strike = src->attrs.strike; + dst->pen.font = src->attrs.font; + dst->pen.small = src->attrs.small; + dst->pen.baseline = src->attrs.baseline; + + dst->pen.fg = src->fg; + dst->pen.bg = src->bg; + + dst->pen.uri = src->uri; + + if (src->width == 2 && pos.col < (new_cols - 1)) { + (dst + 1)->schar = (uint32_t)-1; + } + } + for (; pos.col < new_cols; pos.col++) { + clearcell(screen, &new_buffer[pos.row * new_cols + pos.col]); + } + new_row--; + + if (active) { + statefields->pos.row++; + } + } + } + if (new_row >= 0) { + // Scroll new rows back up to the top and fill in blanks at the bottom + int moverows = new_rows - new_row - 1; + memmove(&new_buffer[0], &new_buffer[(new_row + 1) * new_cols], + (size_t)moverows * (size_t)new_cols * sizeof(ScreenCell)); + memmove(&new_lineinfo[0], &new_lineinfo[new_row + 1], + (size_t)moverows * sizeof(new_lineinfo[0])); + + new_cursor.row -= (new_row + 1); + + for (new_row = moverows; new_row < new_rows; new_row++) { + for (int col = 0; col < new_cols; col++) { + clearcell(screen, &new_buffer[new_row * new_cols + col]); + } + new_lineinfo[new_row] = (VTermLineInfo){ 0 }; + } + } + + vterm_allocator_free(screen->vt, old_buffer); + screen->buffers[bufidx] = new_buffer; + + vterm_allocator_free(screen->vt, old_lineinfo); + statefields->lineinfos[bufidx] = new_lineinfo; + + if (active) { + statefields->pos = new_cursor; + } +} + +static int resize(int new_rows, int new_cols, VTermStateFields *fields, void *user) +{ + VTermScreen *screen = user; + + int altscreen_active = (screen->buffers[BUFIDX_ALTSCREEN] + && screen->buffer == screen->buffers[BUFIDX_ALTSCREEN]); + + int old_rows = screen->rows; + int old_cols = screen->cols; + + if (new_cols > old_cols) { + // Ensure that ->sb_buffer is large enough for a new or and old row + if (screen->sb_buffer) { + vterm_allocator_free(screen->vt, screen->sb_buffer); + } + + screen->sb_buffer = vterm_allocator_malloc(screen->vt, + sizeof(VTermScreenCell) * (size_t)new_cols); + } + + resize_buffer(screen, 0, new_rows, new_cols, !altscreen_active, fields); + if (screen->buffers[BUFIDX_ALTSCREEN]) { + resize_buffer(screen, 1, new_rows, new_cols, altscreen_active, fields); + } else if (new_rows != old_rows) { + // We don't need a full resize of the altscreen because it isn't enabled but we should at least + // keep the lineinfo the right size + vterm_allocator_free(screen->vt, fields->lineinfos[BUFIDX_ALTSCREEN]); + + VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, + sizeof(new_lineinfo[0]) * + (size_t)new_rows); + for (int row = 0; row < new_rows; row++) { + new_lineinfo[row] = (VTermLineInfo){ 0 }; + } + + fields->lineinfos[BUFIDX_ALTSCREEN] = new_lineinfo; + } + + screen->buffer = + altscreen_active ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY]; + + screen->rows = new_rows; + screen->cols = new_cols; + + if (new_cols <= old_cols) { + if (screen->sb_buffer) { + vterm_allocator_free(screen->vt, screen->sb_buffer); + } + + screen->sb_buffer = vterm_allocator_malloc(screen->vt, + sizeof(VTermScreenCell) * (size_t)new_cols); + } + + // TODO(vterm): Maaaaybe we can optimise this if there's no reflow happening + damagescreen(screen); + + if (screen->callbacks && screen->callbacks->resize) { + return (*screen->callbacks->resize)(new_rows, new_cols, screen->cbdata); + } + + return 1; +} + +static int setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, + void *user) +{ + VTermScreen *screen = user; + + if (newinfo->doublewidth != oldinfo->doublewidth + || newinfo->doubleheight != oldinfo->doubleheight) { + for (int col = 0; col < screen->cols; col++) { + ScreenCell *cell = getcell(screen, row, col); + cell->pen.dwl = newinfo->doublewidth; + cell->pen.dhl = newinfo->doubleheight; + } + + VTermRect rect = { + .start_row = row, + .end_row = row + 1, + .start_col = 0, + .end_col = newinfo->doublewidth ? screen->cols / 2 : screen->cols, + }; + damagerect(screen, rect); + + if (newinfo->doublewidth) { + rect.start_col = screen->cols / 2; + rect.end_col = screen->cols; + + erase_internal(rect, 0, user); + } + } + + return 1; +} + +static int sb_clear(void *user) +{ + VTermScreen *screen = user; + + if (screen->callbacks && screen->callbacks->sb_clear) { + if ((*screen->callbacks->sb_clear)(screen->cbdata)) { + return 1; + } + } + + return 0; +} + +static VTermStateCallbacks state_cbs = { + .putglyph = &putglyph, + .movecursor = &movecursor, + .scrollrect = &scrollrect, + .erase = &erase, + .setpenattr = &setpenattr, + .settermprop = &settermprop, + .bell = &bell, + .resize = &resize, + .setlineinfo = &setlineinfo, + .sb_clear = &sb_clear, +}; + +static VTermScreen *screen_new(VTerm *vt) +{ + VTermState *state = vterm_obtain_state(vt); + if (!state) { + return NULL; + } + + VTermScreen *screen = vterm_allocator_malloc(vt, sizeof(VTermScreen)); + int rows, cols; + + vterm_get_size(vt, &rows, &cols); + + screen->vt = vt; + screen->state = state; + + screen->damage_merge = VTERM_DAMAGE_CELL; + screen->damaged.start_row = -1; + screen->pending_scrollrect.start_row = -1; + + screen->rows = rows; + screen->cols = cols; + + screen->global_reverse = false; + screen->reflow = false; + + screen->callbacks = NULL; + screen->cbdata = NULL; + + screen->buffers[BUFIDX_PRIMARY] = alloc_buffer(screen, rows, cols); + + screen->buffer = screen->buffers[BUFIDX_PRIMARY]; + + screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * (size_t)cols); + + vterm_state_set_callbacks(screen->state, &state_cbs, screen); + + return screen; +} + +void vterm_screen_free(VTermScreen *screen) +{ + vterm_allocator_free(screen->vt, screen->buffers[BUFIDX_PRIMARY]); + if (screen->buffers[BUFIDX_ALTSCREEN]) { + vterm_allocator_free(screen->vt, screen->buffers[BUFIDX_ALTSCREEN]); + } + + vterm_allocator_free(screen->vt, screen->sb_buffer); + + vterm_allocator_free(screen->vt, screen); +} + +void vterm_screen_reset(VTermScreen *screen, int hard) +{ + screen->damaged.start_row = -1; + screen->pending_scrollrect.start_row = -1; + vterm_state_reset(screen->state, hard); + vterm_screen_flush_damage(screen); +} + +// Copy internal to external representation of a screen cell +int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCell *cell) +{ + ScreenCell *intcell = getcell(screen, pos.row, pos.col); + if (!intcell) { + return 0; + } + + cell->schar = (intcell->schar == (uint32_t)-1) ? 0 : intcell->schar; + + cell->attrs.bold = intcell->pen.bold; + cell->attrs.underline = intcell->pen.underline; + cell->attrs.italic = intcell->pen.italic; + cell->attrs.blink = intcell->pen.blink; + cell->attrs.reverse = intcell->pen.reverse ^ screen->global_reverse; + cell->attrs.conceal = intcell->pen.conceal; + cell->attrs.strike = intcell->pen.strike; + cell->attrs.font = intcell->pen.font; + cell->attrs.small = intcell->pen.small; + cell->attrs.baseline = intcell->pen.baseline; + + cell->attrs.dwl = intcell->pen.dwl; + cell->attrs.dhl = intcell->pen.dhl; + + cell->fg = intcell->pen.fg; + cell->bg = intcell->pen.bg; + + cell->uri = intcell->pen.uri; + + if (pos.col < (screen->cols - 1) + && getcell(screen, pos.row, pos.col + 1)->schar == (uint32_t)-1) { + cell->width = 2; + } else { + cell->width = 1; + } + + return 1; +} + +VTermScreen *vterm_obtain_screen(VTerm *vt) +{ + if (vt->screen) { + return vt->screen; + } + + VTermScreen *screen = screen_new(vt); + vt->screen = screen; + + return screen; +} + +void vterm_screen_enable_reflow(VTermScreen *screen, bool reflow) +{ + screen->reflow = reflow; +} + +#undef vterm_screen_set_reflow +void vterm_screen_set_reflow(VTermScreen *screen, bool reflow) +{ + vterm_screen_enable_reflow(screen, reflow); +} + +void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen) +{ + if (!screen->buffers[BUFIDX_ALTSCREEN] && altscreen) { + int rows, cols; + vterm_get_size(screen->vt, &rows, &cols); + + screen->buffers[BUFIDX_ALTSCREEN] = alloc_buffer(screen, rows, cols); + } +} + +void vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, + void *user) +{ + screen->callbacks = callbacks; + screen->cbdata = user; +} + +void vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, + const VTermStateFallbacks *fallbacks, void *user) +{ + vterm_state_set_unrecognised_fallbacks(screen->state, fallbacks, user); +} + +void vterm_screen_flush_damage(VTermScreen *screen) +{ + if (screen->pending_scrollrect.start_row != -1) { + vterm_scroll_rect(screen->pending_scrollrect, screen->pending_scroll_downward, + screen->pending_scroll_rightward, + moverect_user, erase_user, screen); + + screen->pending_scrollrect.start_row = -1; + } + + if (screen->damaged.start_row != -1) { + if (screen->callbacks && screen->callbacks->damage) { + (*screen->callbacks->damage)(screen->damaged, screen->cbdata); + } + + screen->damaged.start_row = -1; + } +} + +void vterm_screen_set_damage_merge(VTermScreen *screen, VTermDamageSize size) +{ + vterm_screen_flush_damage(screen); + screen->damage_merge = size; +} + +/// Same as vterm_state_convert_color_to_rgb(), but takes a `screen` instead of a `state` instance. +void vterm_screen_convert_color_to_rgb(const VTermScreen *screen, VTermColor *col) +{ + vterm_state_convert_color_to_rgb(screen->state, col); +} + +// Some utility functions on VTermRect structures + +#define STRFrect "(%d,%d-%d,%d)" +#define ARGSrect(r) (r).start_row, (r).start_col, (r).end_row, (r).end_col + +// Expand dst to contain src as well +void rect_expand(VTermRect *dst, VTermRect *src) +{ + if (dst->start_row > src->start_row) { + dst->start_row = src->start_row; + } + if (dst->start_col > src->start_col) { + dst->start_col = src->start_col; + } + if (dst->end_row < src->end_row) { + dst->end_row = src->end_row; + } + if (dst->end_col < src->end_col) { + dst->end_col = src->end_col; + } +} + +// Clip the dst to ensure it does not step outside of bounds +void rect_clip(VTermRect *dst, VTermRect *bounds) +{ + if (dst->start_row < bounds->start_row) { + dst->start_row = bounds->start_row; + } + if (dst->start_col < bounds->start_col) { + dst->start_col = bounds->start_col; + } + if (dst->end_row > bounds->end_row) { + dst->end_row = bounds->end_row; + } + if (dst->end_col > bounds->end_col) { + dst->end_col = bounds->end_col; + } + // Ensure it doesn't end up negatively-sized + if (dst->end_row < dst->start_row) { + dst->end_row = dst->start_row; + } + if (dst->end_col < dst->start_col) { + dst->end_col = dst->start_col; + } +} + +// True if the two rectangles are equal +int rect_equal(VTermRect *a, VTermRect *b) +{ + return (a->start_row == b->start_row) + && (a->start_col == b->start_col) + && (a->end_row == b->end_row) + && (a->end_col == b->end_col); +} + +// True if small is contained entirely within big +int rect_contains(VTermRect *big, VTermRect *small) +{ + if (small->start_row < big->start_row) { + return 0; + } + if (small->start_col < big->start_col) { + return 0; + } + if (small->end_row > big->end_row) { + return 0; + } + if (small->end_col > big->end_col) { + return 0; + } + return 1; +} + +// True if the rectangles overlap at all +int rect_intersects(VTermRect *a, VTermRect *b) +{ + if (a->start_row > b->end_row || b->start_row > a->end_row) { + return 0; + } + if (a->start_col > b->end_col || b->start_col > a->end_col) { + return 0; + } + return 1; +} diff --git a/src/nvim/vterm/screen.h b/src/nvim/vterm/screen.h new file mode 100644 index 0000000000..fa7520d75a --- /dev/null +++ b/src/nvim/vterm/screen.h @@ -0,0 +1,9 @@ +#pragma once + +#include <stddef.h> + +#include "nvim/vterm/vterm_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/screen.h.generated.h" +#endif diff --git a/src/nvim/vterm/state.c b/src/nvim/vterm/state.c new file mode 100644 index 0000000000..9e787acd9b --- /dev/null +++ b/src/nvim/vterm/state.c @@ -0,0 +1,2310 @@ +#include <stdio.h> +#include <string.h> + +#include "nvim/grid.h" +#include "nvim/mbyte.h" +#include "nvim/vterm/encoding.h" +#include "nvim/vterm/parser.h" +#include "nvim/vterm/pen.h" +#include "nvim/vterm/state.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/state.c.generated.h" +#endif + +#define strneq(a, b, n) (strncmp(a, b, n) == 0) + +// Some convenient wrappers to make callback functions easier + +static void putglyph(VTermState *state, const schar_T schar, int width, VTermPos pos) +{ + VTermGlyphInfo info = { + .schar = schar, + .width = width, + .protected_cell = state->protected_cell, + .dwl = state->lineinfo[pos.row].doublewidth, + .dhl = state->lineinfo[pos.row].doubleheight, + }; + + if (state->callbacks && state->callbacks->putglyph) { + if ((*state->callbacks->putglyph)(&info, pos, state->cbdata)) { + return; + } + } + + DEBUG_LOG("libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row); +} + +static void updatecursor(VTermState *state, VTermPos *oldpos, int cancel_phantom) +{ + if (state->pos.col == oldpos->col && state->pos.row == oldpos->row) { + return; + } + + if (cancel_phantom) { + state->at_phantom = 0; + } + + if (state->callbacks && state->callbacks->movecursor) { + if ((*state->callbacks->movecursor)(state->pos, *oldpos, state->mode.cursor_visible, + state->cbdata)) { + return; + } + } +} + +static void erase(VTermState *state, VTermRect rect, int selective) +{ + if (rect.end_col == state->cols) { + // If we're erasing the final cells of any lines, cancel the continuation marker on the + // subsequent line + for (int row = rect.start_row + 1; row < rect.end_row + 1 && row < state->rows; row++) { + state->lineinfo[row].continuation = 0; + } + } + + if (state->callbacks && state->callbacks->erase) { + if ((*state->callbacks->erase)(rect, selective, state->cbdata)) { + return; + } + } +} + +static VTermState *vterm_state_new(VTerm *vt) +{ + VTermState *state = vterm_allocator_malloc(vt, sizeof(VTermState)); + + state->vt = vt; + + state->rows = vt->rows; + state->cols = vt->cols; + + state->mouse_col = 0; + state->mouse_row = 0; + state->mouse_buttons = 0; + + state->mouse_protocol = MOUSE_X10; + + state->callbacks = NULL; + state->cbdata = NULL; + + state->selection.callbacks = NULL; + state->selection.user = NULL; + state->selection.buffer = NULL; + + vterm_state_newpen(state); + + state->bold_is_highbright = 0; + + state->combine_pos.row = -1; + + state->tabstops = vterm_allocator_malloc(state->vt, ((size_t)state->cols + 7) / 8); + + state->lineinfos[BUFIDX_PRIMARY] = vterm_allocator_malloc(state->vt, + (size_t)state->rows * + sizeof(VTermLineInfo)); + // TODO(vterm): Make an 'enable' function + state->lineinfos[BUFIDX_ALTSCREEN] = vterm_allocator_malloc(state->vt, + (size_t)state->rows * + sizeof(VTermLineInfo)); + state->lineinfo = state->lineinfos[BUFIDX_PRIMARY]; + + state->encoding_utf8.enc = vterm_lookup_encoding(ENC_UTF8, 'u'); + if (*state->encoding_utf8.enc->init) { + (*state->encoding_utf8.enc->init)(state->encoding_utf8.enc, state->encoding_utf8.data); + } + + return state; +} + +void vterm_state_free(VTermState *state) +{ + vterm_allocator_free(state->vt, state->tabstops); + vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_PRIMARY]); + if (state->lineinfos[BUFIDX_ALTSCREEN]) { + vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_ALTSCREEN]); + } + vterm_allocator_free(state->vt, state); +} + +static void scroll(VTermState *state, VTermRect rect, int downward, int rightward) +{ + if (!downward && !rightward) { + return; + } + + int rows = rect.end_row - rect.start_row; + if (downward > rows) { + downward = rows; + } else if (downward < -rows) { + downward = -rows; + } + + int cols = rect.end_col - rect.start_col; + if (rightward > cols) { + rightward = cols; + } else if (rightward < -cols) { + rightward = -cols; + } + + // Update lineinfo if full line + if (rect.start_col == 0 && rect.end_col == state->cols && rightward == 0) { + int height = rect.end_row - rect.start_row - abs(downward); + + if (downward > 0) { + memmove(state->lineinfo + rect.start_row, + state->lineinfo + rect.start_row + downward, + (size_t)height * sizeof(state->lineinfo[0])); + for (int row = rect.end_row - downward; row < rect.end_row; row++) { + state->lineinfo[row] = (VTermLineInfo){ 0 }; + } + } else { + memmove(state->lineinfo + rect.start_row - downward, + state->lineinfo + rect.start_row, + (size_t)height * sizeof(state->lineinfo[0])); + for (int row = rect.start_row; row < rect.start_row - downward; row++) { + state->lineinfo[row] = (VTermLineInfo){ 0 }; + } + } + } + + if (state->callbacks && state->callbacks->scrollrect) { + if ((*state->callbacks->scrollrect)(rect, downward, rightward, state->cbdata)) { + return; + } + } + + if (state->callbacks) { + vterm_scroll_rect(rect, downward, rightward, + state->callbacks->moverect, state->callbacks->erase, state->cbdata); + } +} + +static void linefeed(VTermState *state) +{ + if (state->pos.row == SCROLLREGION_BOTTOM(state) - 1) { + VTermRect rect = { + .start_row = state->scrollregion_top, + .end_row = SCROLLREGION_BOTTOM(state), + .start_col = SCROLLREGION_LEFT(state), + .end_col = SCROLLREGION_RIGHT(state), + }; + + scroll(state, rect, 1, 0); + } else if (state->pos.row < state->rows - 1) { + state->pos.row++; + } +} + +static void set_col_tabstop(VTermState *state, int col) +{ + uint8_t mask = (uint8_t)(1 << (col & 7)); + state->tabstops[col >> 3] |= mask; +} + +static void clear_col_tabstop(VTermState *state, int col) +{ + uint8_t mask = (uint8_t)(1 << (col & 7)); + state->tabstops[col >> 3] &= ~mask; +} + +static int is_col_tabstop(VTermState *state, int col) +{ + uint8_t mask = (uint8_t)(1 << (col & 7)); + return state->tabstops[col >> 3] & mask; +} + +static int is_cursor_in_scrollregion(const VTermState *state) +{ + if (state->pos.row < state->scrollregion_top + || state->pos.row >= SCROLLREGION_BOTTOM(state)) { + return 0; + } + if (state->pos.col < SCROLLREGION_LEFT(state) + || state->pos.col >= SCROLLREGION_RIGHT(state)) { + return 0; + } + + return 1; +} + +static void tab(VTermState *state, int count, int direction) +{ + while (count > 0) { + if (direction > 0) { + if (state->pos.col >= THISROWWIDTH(state) - 1) { + return; + } + + state->pos.col++; + } else if (direction < 0) { + if (state->pos.col < 1) { + return; + } + + state->pos.col--; + } + + if (is_col_tabstop(state, state->pos.col)) { + count--; + } + } +} + +#define NO_FORCE 0 +#define FORCE 1 + +#define DWL_OFF 0 +#define DWL_ON 1 + +#define DHL_OFF 0 +#define DHL_TOP 1 +#define DHL_BOTTOM 2 + +static void set_lineinfo(VTermState *state, int row, int force, int dwl, int dhl) +{ + VTermLineInfo info = state->lineinfo[row]; + + if (dwl == DWL_OFF) { + info.doublewidth = DWL_OFF; + } else if (dwl == DWL_ON) { + info.doublewidth = DWL_ON; + } + // else -1 to ignore + + if (dhl == DHL_OFF) { + info.doubleheight = DHL_OFF; + } else if (dhl == DHL_TOP) { + info.doubleheight = DHL_TOP; + } else if (dhl == DHL_BOTTOM) { + info.doubleheight = DHL_BOTTOM; + } + + if ((state->callbacks + && state->callbacks->setlineinfo + && (*state->callbacks->setlineinfo)(row, &info, state->lineinfo + row, state->cbdata)) + || force) { + state->lineinfo[row] = info; + } +} + +static int on_text(const char bytes[], size_t len, void *user) +{ + VTermState *state = user; + + VTermPos oldpos = state->pos; + + uint32_t *codepoints = (uint32_t *)(state->vt->tmpbuffer); + size_t maxpoints = (state->vt->tmpbuffer_len) / sizeof(uint32_t); + + int npoints = 0; + size_t eaten = 0; + + VTermEncodingInstance *encoding = + state->gsingle_set ? &state->encoding[state->gsingle_set] + : !(bytes[eaten] & 0x80) ? &state->encoding[state->gl_set] + : state->vt->mode.utf8 ? &state->encoding_utf8 + : &state->encoding[state-> + gr_set]; + + (*encoding->enc->decode)(encoding->enc, encoding->data, + codepoints, &npoints, state->gsingle_set ? 1 : (int)maxpoints, + bytes, &eaten, len); + + // There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet for even a single codepoint + if (!npoints) { + return (int)eaten; + } + + if (state->gsingle_set && npoints) { + state->gsingle_set = 0; + } + + int i = 0; + GraphemeState grapheme_state = GRAPHEME_STATE_INIT; + size_t grapheme_len = 0; + bool recombine = false; + + // See if the cursor has moved since + if (state->pos.row == state->combine_pos.row + && state->pos.col == state->combine_pos.col + state->combine_width) { + // This is a combining char. that needs to be merged with the previous glyph output + if (utf_iscomposing((int)state->grapheme_last, (int)codepoints[i], &state->grapheme_state)) { + // Find where we need to append these combining chars + grapheme_len = state->grapheme_len; + grapheme_state = state->grapheme_state; + state->pos.col = state->combine_pos.col; + recombine = true; + } else { + DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n"); + } + } + + while (i < npoints) { + // Try to find combining characters following this + do { + if (grapheme_len < sizeof(state->grapheme_buf) - 4) { + grapheme_len += (size_t)utf_char2bytes((int)codepoints[i], + state->grapheme_buf + grapheme_len); + } + i++; + } while (i < npoints && utf_iscomposing((int)codepoints[i - 1], (int)codepoints[i], + &grapheme_state)); + + int width = utf_ptr2cells_len(state->grapheme_buf, (int)grapheme_len); + + if (state->at_phantom || state->pos.col + width > THISROWWIDTH(state)) { + linefeed(state); + state->pos.col = 0; + state->at_phantom = 0; + state->lineinfo[state->pos.row].continuation = 1; + } + + if (state->mode.insert && !recombine) { + // TODO(vterm): This will be a little inefficient for large bodies of text, as it'll have to + // 'ICH' effectively before every glyph. We should scan ahead and ICH as many times as + // required + VTermRect rect = { + .start_row = state->pos.row, + .end_row = state->pos.row + 1, + .start_col = state->pos.col, + .end_col = THISROWWIDTH(state), + }; + scroll(state, rect, 0, -1); + } + + schar_T sc = schar_from_buf(state->grapheme_buf, grapheme_len); + putglyph(state, sc, width, state->pos); + + if (i == npoints) { + // End of the buffer. Save the chars in case we have to combine with more on the next call + state->grapheme_len = grapheme_len; + state->grapheme_last = codepoints[i - 1]; + state->grapheme_state = grapheme_state; + state->combine_width = width; + state->combine_pos = state->pos; + } else { + grapheme_len = 0; + recombine = false; + } + + if (state->pos.col + width >= THISROWWIDTH(state)) { + if (state->mode.autowrap) { + state->at_phantom = 1; + } + } else { + state->pos.col += width; + } + } + + updatecursor(state, &oldpos, 0); + +#ifdef DEBUG + if (state->pos.row < 0 || state->pos.row >= state->rows + || state->pos.col < 0 || state->pos.col >= state->cols) { + fprintf(stderr, "Position out of bounds after text: (%d,%d)\n", + state->pos.row, state->pos.col); + abort(); + } +#endif + + return (int)eaten; +} + +static int on_control(uint8_t control, void *user) +{ + VTermState *state = user; + + VTermPos oldpos = state->pos; + + switch (control) { + case 0x07: // BEL - ECMA-48 8.3.3 + if (state->callbacks && state->callbacks->bell) { + (*state->callbacks->bell)(state->cbdata); + } + break; + + case 0x08: // BS - ECMA-48 8.3.5 + if (state->pos.col > 0) { + state->pos.col--; + } + break; + + case 0x09: // HT - ECMA-48 8.3.60 + tab(state, 1, +1); + break; + + case 0x0a: // LF - ECMA-48 8.3.74 + case 0x0b: // VT + case 0x0c: // FF + linefeed(state); + if (state->mode.newline) { + state->pos.col = 0; + } + break; + + case 0x0d: // CR - ECMA-48 8.3.15 + state->pos.col = 0; + break; + + case 0x0e: // LS1 - ECMA-48 8.3.76 + state->gl_set = 1; + break; + + case 0x0f: // LS0 - ECMA-48 8.3.75 + state->gl_set = 0; + break; + + case 0x84: // IND - DEPRECATED but implemented for completeness + linefeed(state); + break; + + case 0x85: // NEL - ECMA-48 8.3.86 + linefeed(state); + state->pos.col = 0; + break; + + case 0x88: // HTS - ECMA-48 8.3.62 + set_col_tabstop(state, state->pos.col); + break; + + case 0x8d: // RI - ECMA-48 8.3.104 + if (state->pos.row == state->scrollregion_top) { + VTermRect rect = { + .start_row = state->scrollregion_top, + .end_row = SCROLLREGION_BOTTOM(state), + .start_col = SCROLLREGION_LEFT(state), + .end_col = SCROLLREGION_RIGHT(state), + }; + + scroll(state, rect, -1, 0); + } else if (state->pos.row > 0) { + state->pos.row--; + } + break; + + case 0x8e: // SS2 - ECMA-48 8.3.141 + state->gsingle_set = 2; + break; + + case 0x8f: // SS3 - ECMA-48 8.3.142 + state->gsingle_set = 3; + break; + + default: + if (state->fallbacks && state->fallbacks->control) { + if ((*state->fallbacks->control)(control, state->fbdata)) { + return 1; + } + } + + return 0; + } + + updatecursor(state, &oldpos, 1); + +#ifdef DEBUG + if (state->pos.row < 0 || state->pos.row >= state->rows + || state->pos.col < 0 || state->pos.col >= state->cols) { + fprintf(stderr, "Position out of bounds after Ctrl %02x: (%d,%d)\n", + control, state->pos.row, state->pos.col); + abort(); + } +#endif + + return 1; +} + +static int settermprop_bool(VTermState *state, VTermProp prop, int v) +{ + VTermValue val = { .boolean = v }; + return vterm_state_set_termprop(state, prop, &val); +} + +static int settermprop_int(VTermState *state, VTermProp prop, int v) +{ + VTermValue val = { .number = v }; + return vterm_state_set_termprop(state, prop, &val); +} + +static int settermprop_string(VTermState *state, VTermProp prop, VTermStringFragment frag) +{ + VTermValue val = { .string = frag }; + return vterm_state_set_termprop(state, prop, &val); +} + +static void savecursor(VTermState *state, int save) +{ + if (save) { + state->saved.pos = state->pos; + state->saved.mode.cursor_visible = state->mode.cursor_visible; + state->saved.mode.cursor_blink = state->mode.cursor_blink; + state->saved.mode.cursor_shape = state->mode.cursor_shape; + + vterm_state_savepen(state, 1); + } else { + VTermPos oldpos = state->pos; + + state->pos = state->saved.pos; + + settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, state->saved.mode.cursor_visible); + settermprop_bool(state, VTERM_PROP_CURSORBLINK, state->saved.mode.cursor_blink); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, state->saved.mode.cursor_shape); + + vterm_state_savepen(state, 0); + + updatecursor(state, &oldpos, 1); + } +} + +static int on_escape(const char *bytes, size_t len, void *user) +{ + VTermState *state = user; + + // Easier to decode this from the first byte, even though the final byte terminates it + switch (bytes[0]) { + case ' ': + if (len != 2) { + return 0; + } + + switch (bytes[1]) { + case 'F': // S7C1T + state->vt->mode.ctrl8bit = 0; + break; + + case 'G': // S8C1T + state->vt->mode.ctrl8bit = 1; + break; + + default: + return 0; + } + return 2; + + case '#': + if (len != 2) { + return 0; + } + + switch (bytes[1]) { + case '3': // DECDHL top + if (state->mode.leftrightmargin) { + break; + } + set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_TOP); + break; + + case '4': // DECDHL bottom + if (state->mode.leftrightmargin) { + break; + } + set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_BOTTOM); + break; + + case '5': // DECSWL + if (state->mode.leftrightmargin) { + break; + } + set_lineinfo(state, state->pos.row, NO_FORCE, DWL_OFF, DHL_OFF); + break; + + case '6': // DECDWL + if (state->mode.leftrightmargin) { + break; + } + set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_OFF); + break; + + case '8': // DECALN + { + VTermPos pos; + schar_T E = schar_from_ascii('E'); // E + for (pos.row = 0; pos.row < state->rows; pos.row++) { + for (pos.col = 0; pos.col < ROWWIDTH(state, pos.row); pos.col++) { + putglyph(state, E, 1, pos); + } + } + break; + } + + default: + return 0; + } + return 2; + + case '(': + case ')': + case '*': + case '+': // SCS + if (len != 2) { + return 0; + } + + { + int setnum = bytes[0] - 0x28; + VTermEncoding *newenc = vterm_lookup_encoding(ENC_SINGLE_94, bytes[1]); + + if (newenc) { + state->encoding[setnum].enc = newenc; + + if (newenc->init) { + (*newenc->init)(newenc, state->encoding[setnum].data); + } + } + } + + return 2; + + case '7': // DECSC + savecursor(state, 1); + return 1; + + case '8': // DECRC + savecursor(state, 0); + return 1; + + case '<': // Ignored by VT100. Used in VT52 mode to switch up to VT100 + return 1; + + case '=': // DECKPAM + state->mode.keypad = 1; + return 1; + + case '>': // DECKPNM + state->mode.keypad = 0; + return 1; + + case 'c': // RIS - ECMA-48 8.3.105 + { + VTermPos oldpos = state->pos; + vterm_state_reset(state, 1); + if (state->callbacks && state->callbacks->movecursor) { + (*state->callbacks->movecursor)(state->pos, oldpos, state->mode.cursor_visible, + state->cbdata); + } + return 1; + } + + case 'n': // LS2 - ECMA-48 8.3.78 + state->gl_set = 2; + return 1; + + case 'o': // LS3 - ECMA-48 8.3.80 + state->gl_set = 3; + return 1; + + case '~': // LS1R - ECMA-48 8.3.77 + state->gr_set = 1; + return 1; + + case '}': // LS2R - ECMA-48 8.3.79 + state->gr_set = 2; + return 1; + + case '|': // LS3R - ECMA-48 8.3.81 + state->gr_set = 3; + return 1; + + default: + return 0; + } +} + +static void set_mode(VTermState *state, int num, int val) +{ + switch (num) { + case 4: // IRM - ECMA-48 7.2.10 + state->mode.insert = (unsigned)val; + break; + + case 20: // LNM - ANSI X3.4-1977 + state->mode.newline = (unsigned)val; + break; + + default: + DEBUG_LOG("libvterm: Unknown mode %d\n", num); + return; + } +} + +static void set_dec_mode(VTermState *state, int num, int val) +{ + switch (num) { + case 1: + state->mode.cursor = (unsigned)val; + break; + + case 5: // DECSCNM - screen mode + settermprop_bool(state, VTERM_PROP_REVERSE, val); + break; + + case 6: // DECOM - origin mode + { + VTermPos oldpos = state->pos; + state->mode.origin = (unsigned)val; + state->pos.row = state->mode.origin ? state->scrollregion_top : 0; + state->pos.col = state->mode.origin ? SCROLLREGION_LEFT(state) : 0; + updatecursor(state, &oldpos, 1); + } + break; + + case 7: + state->mode.autowrap = (unsigned)val; + break; + + case 12: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, val); + break; + + case 25: + settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, val); + break; + + case 69: // DECVSSM - vertical split screen mode + // DECLRMM - left/right margin mode + state->mode.leftrightmargin = (unsigned)val; + if (val) { + // Setting DECVSSM must clear doublewidth/doubleheight state of every line + for (int row = 0; row < state->rows; row++) { + set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF); + } + } + + break; + + case 1000: + case 1002: + case 1003: + settermprop_int(state, VTERM_PROP_MOUSE, + !val ? VTERM_PROP_MOUSE_NONE + : (num == 1000) ? VTERM_PROP_MOUSE_CLICK + : (num == 1002) ? VTERM_PROP_MOUSE_DRAG + : VTERM_PROP_MOUSE_MOVE); + break; + + case 1004: + settermprop_bool(state, VTERM_PROP_FOCUSREPORT, val); + state->mode.report_focus = (unsigned)val; + break; + + case 1005: + state->mouse_protocol = val ? MOUSE_UTF8 : MOUSE_X10; + break; + + case 1006: + state->mouse_protocol = val ? MOUSE_SGR : MOUSE_X10; + break; + + case 1015: + state->mouse_protocol = val ? MOUSE_RXVT : MOUSE_X10; + break; + + case 1047: + settermprop_bool(state, VTERM_PROP_ALTSCREEN, val); + break; + + case 1048: + savecursor(state, val); + break; + + case 1049: + settermprop_bool(state, VTERM_PROP_ALTSCREEN, val); + savecursor(state, val); + break; + + case 2004: + state->mode.bracketpaste = (unsigned)val; + break; + + default: + DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num); + return; + } +} + +static void request_dec_mode(VTermState *state, int num) +{ + int reply; + + switch (num) { + case 1: + reply = state->mode.cursor; + break; + + case 5: + reply = state->mode.screen; + break; + + case 6: + reply = state->mode.origin; + break; + + case 7: + reply = state->mode.autowrap; + break; + + case 12: + reply = state->mode.cursor_blink; + break; + + case 25: + reply = state->mode.cursor_visible; + break; + + case 69: + reply = state->mode.leftrightmargin; + break; + + case 1000: + reply = state->mouse_flags == MOUSE_WANT_CLICK; + break; + + case 1002: + reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_DRAG); + break; + + case 1003: + reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_MOVE); + break; + + case 1004: + reply = state->mode.report_focus; + break; + + case 1005: + reply = state->mouse_protocol == MOUSE_UTF8; + break; + + case 1006: + reply = state->mouse_protocol == MOUSE_SGR; + break; + + case 1015: + reply = state->mouse_protocol == MOUSE_RXVT; + break; + + case 1047: + reply = state->mode.alt_screen; + break; + + case 2004: + reply = state->mode.bracketpaste; + break; + + default: + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0); + return; + } + + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, reply ? 1 : 2); +} + +static void request_version_string(VTermState *state) +{ + vterm_push_output_sprintf_str(state->vt, C1_DCS, true, ">|libvterm(%d.%d)", + VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR); +} + +static int on_csi(const char *leader, const long args[], int argcount, const char *intermed, + char command, void *user) +{ + VTermState *state = user; + int leader_byte = 0; + int intermed_byte = 0; + int cancel_phantom = 1; + + if (leader && leader[0]) { + if (leader[1]) { // longer than 1 char + return 0; + } + + switch (leader[0]) { + case '?': + case '>': + leader_byte = (int)leader[0]; + break; + default: + return 0; + } + } + + if (intermed && intermed[0]) { + if (intermed[1]) { // longer than 1 char + return 0; + } + + switch (intermed[0]) { + case ' ': + case '!': + case '"': + case '$': + case '\'': + intermed_byte = (int)intermed[0]; + break; + default: + return 0; + } + } + + VTermPos oldpos = state->pos; + + // Some temporaries for later code + int count, val; + int row, col; + VTermRect rect; + int selective; + +#define LBOUND(v, min) if ((v) < (min))(v) = (min) +#define UBOUND(v, max) if ((v) > (max))(v) = (max) + +#define LEADER(l, b) ((l << 8) | b) +#define INTERMED(i, b) ((i << 16) | b) + + switch (intermed_byte << 16 | leader_byte << 8 | command) { + case 0x40: // ICH - ECMA-48 8.3.64 + count = CSI_ARG_COUNT(args[0]); + + if (!is_cursor_in_scrollregion(state)) { + break; + } + + rect.start_row = state->pos.row; + rect.end_row = state->pos.row + 1; + rect.start_col = state->pos.col; + if (state->mode.leftrightmargin) { + rect.end_col = SCROLLREGION_RIGHT(state); + } else { + rect.end_col = THISROWWIDTH(state); + } + + scroll(state, rect, 0, -count); + + break; + + case 0x41: // CUU - ECMA-48 8.3.22 + count = CSI_ARG_COUNT(args[0]); + state->pos.row -= count; + state->at_phantom = 0; + break; + + case 0x42: // CUD - ECMA-48 8.3.19 + count = CSI_ARG_COUNT(args[0]); + state->pos.row += count; + state->at_phantom = 0; + break; + + case 0x43: // CUF - ECMA-48 8.3.20 + count = CSI_ARG_COUNT(args[0]); + state->pos.col += count; + state->at_phantom = 0; + break; + + case 0x44: // CUB - ECMA-48 8.3.18 + count = CSI_ARG_COUNT(args[0]); + state->pos.col -= count; + state->at_phantom = 0; + break; + + case 0x45: // CNL - ECMA-48 8.3.12 + count = CSI_ARG_COUNT(args[0]); + state->pos.col = 0; + state->pos.row += count; + state->at_phantom = 0; + break; + + case 0x46: // CPL - ECMA-48 8.3.13 + count = CSI_ARG_COUNT(args[0]); + state->pos.col = 0; + state->pos.row -= count; + state->at_phantom = 0; + break; + + case 0x47: // CHA - ECMA-48 8.3.9 + val = CSI_ARG_OR(args[0], 1); + state->pos.col = val - 1; + state->at_phantom = 0; + break; + + case 0x48: // CUP - ECMA-48 8.3.21 + row = CSI_ARG_OR(args[0], 1); + col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); + // zero-based + state->pos.row = row - 1; + state->pos.col = col - 1; + if (state->mode.origin) { + state->pos.row += state->scrollregion_top; + state->pos.col += SCROLLREGION_LEFT(state); + } + state->at_phantom = 0; + break; + + case 0x49: // CHT - ECMA-48 8.3.10 + count = CSI_ARG_COUNT(args[0]); + tab(state, count, +1); + break; + + case 0x4a: // ED - ECMA-48 8.3.39 + case LEADER('?', 0x4a): // DECSED - Selective Erase in Display + selective = (leader_byte == '?'); + switch (CSI_ARG(args[0])) { + case CSI_ARG_MISSING: + case 0: + rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1; + rect.start_col = state->pos.col; rect.end_col = state->cols; + if (rect.end_col > rect.start_col) { + erase(state, rect, selective); + } + + rect.start_row = state->pos.row + 1; rect.end_row = state->rows; + rect.start_col = 0; + for (int row_ = rect.start_row; row_ < rect.end_row; row_++) { + set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); + } + if (rect.end_row > rect.start_row) { + erase(state, rect, selective); + } + break; + + case 1: + rect.start_row = 0; rect.end_row = state->pos.row; + rect.start_col = 0; rect.end_col = state->cols; + for (int row_ = rect.start_row; row_ < rect.end_row; row_++) { + set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); + } + if (rect.end_col > rect.start_col) { + erase(state, rect, selective); + } + + rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1; + rect.end_col = state->pos.col + 1; + if (rect.end_row > rect.start_row) { + erase(state, rect, selective); + } + break; + + case 2: + rect.start_row = 0; rect.end_row = state->rows; + rect.start_col = 0; rect.end_col = state->cols; + for (int row_ = rect.start_row; row_ < rect.end_row; row_++) { + set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); + } + erase(state, rect, selective); + break; + + case 3: + if (state->callbacks && state->callbacks->sb_clear) { + if ((*state->callbacks->sb_clear)(state->cbdata)) { + return 1; + } + } + break; + } + break; + + case 0x4b: // EL - ECMA-48 8.3.41 + case LEADER('?', 0x4b): // DECSEL - Selective Erase in Line + selective = (leader_byte == '?'); + rect.start_row = state->pos.row; + rect.end_row = state->pos.row + 1; + + switch (CSI_ARG(args[0])) { + case CSI_ARG_MISSING: + case 0: + rect.start_col = state->pos.col; rect.end_col = THISROWWIDTH(state); break; + case 1: + rect.start_col = 0; rect.end_col = state->pos.col + 1; break; + case 2: + rect.start_col = 0; rect.end_col = THISROWWIDTH(state); break; + default: + return 0; + } + + if (rect.end_col > rect.start_col) { + erase(state, rect, selective); + } + + break; + + case 0x4c: // IL - ECMA-48 8.3.67 + count = CSI_ARG_COUNT(args[0]); + + if (!is_cursor_in_scrollregion(state)) { + break; + } + + rect.start_row = state->pos.row; + rect.end_row = SCROLLREGION_BOTTOM(state); + rect.start_col = SCROLLREGION_LEFT(state); + rect.end_col = SCROLLREGION_RIGHT(state); + + scroll(state, rect, -count, 0); + + break; + + case 0x4d: // DL - ECMA-48 8.3.32 + count = CSI_ARG_COUNT(args[0]); + + if (!is_cursor_in_scrollregion(state)) { + break; + } + + rect.start_row = state->pos.row; + rect.end_row = SCROLLREGION_BOTTOM(state); + rect.start_col = SCROLLREGION_LEFT(state); + rect.end_col = SCROLLREGION_RIGHT(state); + + scroll(state, rect, count, 0); + + break; + + case 0x50: // DCH - ECMA-48 8.3.26 + count = CSI_ARG_COUNT(args[0]); + + if (!is_cursor_in_scrollregion(state)) { + break; + } + + rect.start_row = state->pos.row; + rect.end_row = state->pos.row + 1; + rect.start_col = state->pos.col; + if (state->mode.leftrightmargin) { + rect.end_col = SCROLLREGION_RIGHT(state); + } else { + rect.end_col = THISROWWIDTH(state); + } + + scroll(state, rect, 0, count); + + break; + + case 0x53: // SU - ECMA-48 8.3.147 + count = CSI_ARG_COUNT(args[0]); + + rect.start_row = state->scrollregion_top; + rect.end_row = SCROLLREGION_BOTTOM(state); + rect.start_col = SCROLLREGION_LEFT(state); + rect.end_col = SCROLLREGION_RIGHT(state); + + scroll(state, rect, count, 0); + + break; + + case 0x54: // SD - ECMA-48 8.3.113 + count = CSI_ARG_COUNT(args[0]); + + rect.start_row = state->scrollregion_top; + rect.end_row = SCROLLREGION_BOTTOM(state); + rect.start_col = SCROLLREGION_LEFT(state); + rect.end_col = SCROLLREGION_RIGHT(state); + + scroll(state, rect, -count, 0); + + break; + + case 0x58: // ECH - ECMA-48 8.3.38 + count = CSI_ARG_COUNT(args[0]); + + rect.start_row = state->pos.row; + rect.end_row = state->pos.row + 1; + rect.start_col = state->pos.col; + rect.end_col = state->pos.col + count; + UBOUND(rect.end_col, THISROWWIDTH(state)); + + erase(state, rect, 0); + break; + + case 0x5a: // CBT - ECMA-48 8.3.7 + count = CSI_ARG_COUNT(args[0]); + tab(state, count, -1); + break; + + case 0x60: // HPA - ECMA-48 8.3.57 + col = CSI_ARG_OR(args[0], 1); + state->pos.col = col - 1; + state->at_phantom = 0; + break; + + case 0x61: // HPR - ECMA-48 8.3.59 + count = CSI_ARG_COUNT(args[0]); + state->pos.col += count; + state->at_phantom = 0; + break; + + case 0x62: { // REP - ECMA-48 8.3.103 + const int row_width = THISROWWIDTH(state); + count = CSI_ARG_COUNT(args[0]); + col = state->pos.col + count; + UBOUND(col, row_width); + schar_T sc = schar_from_buf(state->grapheme_buf, state->grapheme_len); + while (state->pos.col < col) { + putglyph(state, sc, state->combine_width, state->pos); + state->pos.col += state->combine_width; + } + if (state->pos.col + state->combine_width >= row_width) { + if (state->mode.autowrap) { + state->at_phantom = 1; + cancel_phantom = 0; + } + } + break; + } + + case 0x63: // DA - ECMA-48 8.3.24 + val = CSI_ARG_OR(args[0], 0); + if (val == 0) { + // DEC VT100 response + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?1;2c"); + } + break; + + case LEADER('>', 0x63): // DEC secondary Device Attributes + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, ">%d;%d;%dc", 0, 100, 0); + break; + + case 0x64: // VPA - ECMA-48 8.3.158 + row = CSI_ARG_OR(args[0], 1); + state->pos.row = row - 1; + if (state->mode.origin) { + state->pos.row += state->scrollregion_top; + } + state->at_phantom = 0; + break; + + case 0x65: // VPR - ECMA-48 8.3.160 + count = CSI_ARG_COUNT(args[0]); + state->pos.row += count; + state->at_phantom = 0; + break; + + case 0x66: // HVP - ECMA-48 8.3.63 + row = CSI_ARG_OR(args[0], 1); + col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); + // zero-based + state->pos.row = row - 1; + state->pos.col = col - 1; + if (state->mode.origin) { + state->pos.row += state->scrollregion_top; + state->pos.col += SCROLLREGION_LEFT(state); + } + state->at_phantom = 0; + break; + + case 0x67: // TBC - ECMA-48 8.3.154 + val = CSI_ARG_OR(args[0], 0); + + switch (val) { + case 0: + clear_col_tabstop(state, state->pos.col); + break; + case 3: + case 5: + for (col = 0; col < state->cols; col++) { + clear_col_tabstop(state, col); + } + break; + case 1: + case 2: + case 4: + break; + // TODO(vterm): 1, 2 and 4 aren't meaningful yet without line tab stops + default: + return 0; + } + break; + + case 0x68: // SM - ECMA-48 8.3.125 + if (!CSI_ARG_IS_MISSING(args[0])) { + set_mode(state, CSI_ARG(args[0]), 1); + } + break; + + case LEADER('?', 0x68): // DEC private mode set + for (int i = 0; i < argcount; i++) { + if (!CSI_ARG_IS_MISSING(args[i])) { + set_dec_mode(state, CSI_ARG(args[i]), 1); + } + } + break; + + case 0x6a: // HPB - ECMA-48 8.3.58 + count = CSI_ARG_COUNT(args[0]); + state->pos.col -= count; + state->at_phantom = 0; + break; + + case 0x6b: // VPB - ECMA-48 8.3.159 + count = CSI_ARG_COUNT(args[0]); + state->pos.row -= count; + state->at_phantom = 0; + break; + + case 0x6c: // RM - ECMA-48 8.3.106 + if (!CSI_ARG_IS_MISSING(args[0])) { + set_mode(state, CSI_ARG(args[0]), 0); + } + break; + + case LEADER('?', 0x6c): // DEC private mode reset + for (int i = 0; i < argcount; i++) { + if (!CSI_ARG_IS_MISSING(args[i])) { + set_dec_mode(state, CSI_ARG(args[i]), 0); + } + } + break; + + case 0x6d: // SGR - ECMA-48 8.3.117 + vterm_state_setpen(state, args, argcount); + break; + + case LEADER('?', 0x6d): // DECSGR + // No actual DEC terminal recognised these, but some printers did. These are alternative ways to + // request subscript/superscript/off + for (int argi = 0; argi < argcount; argi++) { + long arg; + switch (arg = CSI_ARG(args[argi])) { + case 4: // Superscript on + arg = 73; + vterm_state_setpen(state, &arg, 1); + break; + case 5: // Subscript on + arg = 74; + vterm_state_setpen(state, &arg, 1); + break; + case 24: // Super+subscript off + arg = 75; + vterm_state_setpen(state, &arg, 1); + break; + } + } + break; + + case 0x6e: // DSR - ECMA-48 8.3.35 + case LEADER('?', 0x6e): // DECDSR + val = CSI_ARG_OR(args[0], 0); + + { + char *qmark = (leader_byte == '?') ? "?" : ""; + + switch (val) { + case 0: + case 1: + case 2: + case 3: + case 4: + // ignore - these are replies + break; + case 5: + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s0n", qmark); + break; + case 6: // CPR - cursor position report + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1, + state->pos.col + 1); + break; + } + } + break; + + case INTERMED('!', 0x70): // DECSTR - DEC soft terminal reset + vterm_state_reset(state, 0); + break; + + case LEADER('?', INTERMED('$', 0x70)): + request_dec_mode(state, CSI_ARG(args[0])); + break; + + case LEADER('>', 0x71): // XTVERSION - xterm query version string + request_version_string(state); + break; + + case INTERMED(' ', 0x71): // DECSCUSR - DEC set cursor shape + val = CSI_ARG_OR(args[0], 1); + + switch (val) { + case 0: + case 1: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); + break; + case 2: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); + break; + case 3: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE); + break; + case 4: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE); + break; + case 5: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT); + break; + case 6: + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT); + break; + } + + break; + + case INTERMED('"', 0x71): // DECSCA - DEC select character protection attribute + val = CSI_ARG_OR(args[0], 0); + + switch (val) { + case 0: + case 2: + state->protected_cell = 0; + break; + case 1: + state->protected_cell = 1; + break; + } + + break; + + case 0x72: // DECSTBM - DEC custom + state->scrollregion_top = CSI_ARG_OR(args[0], 1) - 1; + state->scrollregion_bottom = argcount < 2 + || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]); + LBOUND(state->scrollregion_top, 0); + UBOUND(state->scrollregion_top, state->rows); + LBOUND(state->scrollregion_bottom, -1); + if (state->scrollregion_top == 0 && state->scrollregion_bottom == state->rows) { + state->scrollregion_bottom = -1; + } else { + UBOUND(state->scrollregion_bottom, state->rows); + } + + if (SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) { + // Invalid + state->scrollregion_top = 0; + state->scrollregion_bottom = -1; + } + + // Setting the scrolling region restores the cursor to the home position + state->pos.row = 0; + state->pos.col = 0; + if (state->mode.origin) { + state->pos.row += state->scrollregion_top; + state->pos.col += SCROLLREGION_LEFT(state); + } + + break; + + case 0x73: // DECSLRM - DEC custom + // Always allow setting these margins, just they won't take effect without DECVSSM + state->scrollregion_left = CSI_ARG_OR(args[0], 1) - 1; + state->scrollregion_right = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]); + LBOUND(state->scrollregion_left, 0); + UBOUND(state->scrollregion_left, state->cols); + LBOUND(state->scrollregion_right, -1); + if (state->scrollregion_left == 0 && state->scrollregion_right == state->cols) { + state->scrollregion_right = -1; + } else { + UBOUND(state->scrollregion_right, state->cols); + } + + if (state->scrollregion_right > -1 + && state->scrollregion_right <= state->scrollregion_left) { + // Invalid + state->scrollregion_left = 0; + state->scrollregion_right = -1; + } + + // Setting the scrolling region restores the cursor to the home position + state->pos.row = 0; + state->pos.col = 0; + if (state->mode.origin) { + state->pos.row += state->scrollregion_top; + state->pos.col += SCROLLREGION_LEFT(state); + } + + break; + + case INTERMED('\'', 0x7D): // DECIC + count = CSI_ARG_COUNT(args[0]); + + if (!is_cursor_in_scrollregion(state)) { + break; + } + + rect.start_row = state->scrollregion_top; + rect.end_row = SCROLLREGION_BOTTOM(state); + rect.start_col = state->pos.col; + rect.end_col = SCROLLREGION_RIGHT(state); + + scroll(state, rect, 0, -count); + + break; + + case INTERMED('\'', 0x7E): // DECDC + count = CSI_ARG_COUNT(args[0]); + + if (!is_cursor_in_scrollregion(state)) { + break; + } + + rect.start_row = state->scrollregion_top; + rect.end_row = SCROLLREGION_BOTTOM(state); + rect.start_col = state->pos.col; + rect.end_col = SCROLLREGION_RIGHT(state); + + scroll(state, rect, 0, count); + + break; + + default: + if (state->fallbacks && state->fallbacks->csi) { + if ((*state->fallbacks->csi)(leader, args, argcount, intermed, command, state->fbdata)) { + return 1; + } + } + + return 0; + } + + if (state->mode.origin) { + LBOUND(state->pos.row, state->scrollregion_top); + UBOUND(state->pos.row, SCROLLREGION_BOTTOM(state) - 1); + LBOUND(state->pos.col, SCROLLREGION_LEFT(state)); + UBOUND(state->pos.col, SCROLLREGION_RIGHT(state) - 1); + } else { + LBOUND(state->pos.row, 0); + UBOUND(state->pos.row, state->rows - 1); + LBOUND(state->pos.col, 0); + UBOUND(state->pos.col, THISROWWIDTH(state) - 1); + } + + updatecursor(state, &oldpos, cancel_phantom); + +#ifdef DEBUG + if (state->pos.row < 0 || state->pos.row >= state->rows + || state->pos.col < 0 || state->pos.col >= state->cols) { + fprintf(stderr, "Position out of bounds after CSI %c: (%d,%d)\n", + command, state->pos.row, state->pos.col); + abort(); + } + + if (SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) { + fprintf(stderr, "Scroll region height out of bounds after CSI %c: %d <= %d\n", + command, SCROLLREGION_BOTTOM(state), state->scrollregion_top); + abort(); + } + + if (SCROLLREGION_RIGHT(state) <= SCROLLREGION_LEFT(state)) { + fprintf(stderr, "Scroll region width out of bounds after CSI %c: %d <= %d\n", + command, SCROLLREGION_RIGHT(state), SCROLLREGION_LEFT(state)); + abort(); + } +#endif + + return 1; +} + +static uint8_t unbase64one(char c) +{ + if (c >= 'A' && c <= 'Z') { + return (uint8_t)c - 'A'; + } else if (c >= 'a' && c <= 'z') { + return (uint8_t)c - 'a' + 26; + } else if (c >= '0' && c <= '9') { + return (uint8_t)c - '0' + 52; + } else if (c == '+') { + return 62; + } else if (c == '/') { + return 63; + } + + return 0xFF; +} + +static void osc_selection(VTermState *state, VTermStringFragment frag) +{ + if (frag.initial) { + state->tmp.selection.mask = 0; + state->tmp.selection.state = SELECTION_INITIAL; + } + + while (!state->tmp.selection.state && frag.len) { + // Parse selection parameter + switch (frag.str[0]) { + case 'c': + state->tmp.selection.mask |= VTERM_SELECTION_CLIPBOARD; + break; + case 'p': + state->tmp.selection.mask |= VTERM_SELECTION_PRIMARY; + break; + case 'q': + state->tmp.selection.mask |= VTERM_SELECTION_SECONDARY; + break; + case 's': + state->tmp.selection.mask |= VTERM_SELECTION_SELECT; + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + state->tmp.selection.mask |= (VTERM_SELECTION_CUT0 << (frag.str[0] - '0')); + break; + + case ';': + state->tmp.selection.state = SELECTION_SELECTED; + if (!state->tmp.selection.mask) { + state->tmp.selection.mask = VTERM_SELECTION_SELECT|VTERM_SELECTION_CUT0; + } + break; + } + + frag.str++; + frag.len--; + } + + if (!frag.len) { + // Clear selection if we're already finished but didn't do anything + if (frag.final && state->selection.callbacks->set) { + (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ + .str = NULL, + .len = 0, + .initial = state->tmp.selection.state != SELECTION_SET, + .final = true, + }, state->selection.user); + } + return; + } + + if (state->tmp.selection.state == SELECTION_SELECTED) { + if (frag.str[0] == '?') { + state->tmp.selection.state = SELECTION_QUERY; + } else { + state->tmp.selection.state = SELECTION_SET_INITIAL; + state->tmp.selection.recvpartial = 0; + } + } + + if (state->tmp.selection.state == SELECTION_QUERY) { + if (state->selection.callbacks->query) { + (*state->selection.callbacks->query)(state->tmp.selection.mask, state->selection.user); + } + return; + } + + if (state->tmp.selection.state == SELECTION_INVALID) { + return; + } + + if (state->selection.callbacks->set) { + size_t bufcur = 0; + char *buffer = state->selection.buffer; + + uint32_t x = 0; // Current decoding value + int n = 0; // Number of sextets consumed + + if (state->tmp.selection.recvpartial) { + n = state->tmp.selection.recvpartial >> 24; + x = state->tmp.selection.recvpartial & 0x03FFFF; // could be up to 18 bits of state in here + + state->tmp.selection.recvpartial = 0; + } + + while ((state->selection.buflen - bufcur) >= 3 && frag.len) { + if (frag.str[0] == '=') { + if (n == 2) { + buffer[0] = (char)(x >> 4 & 0xFF); + buffer += 1, bufcur += 1; + } + if (n == 3) { + buffer[0] = (char)(x >> 10 & 0xFF); + buffer[1] = (char)(x >> 2 & 0xFF); + buffer += 2, bufcur += 2; + } + + while (frag.len && frag.str[0] == '=') { + frag.str++, frag.len--; + } + + n = 0; + } else { + uint8_t b = unbase64one(frag.str[0]); + if (b == 0xFF) { + DEBUG_LOG("base64decode bad input %02X\n", (uint8_t)frag.str[0]); + + state->tmp.selection.state = SELECTION_INVALID; + if (state->selection.callbacks->set) { + (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ + .str = NULL, + .len = 0, + .initial = true, + .final = true, + }, state->selection.user); + } + break; + } + + x = (x << 6) | b; + n++; + frag.str++, frag.len--; + + if (n == 4) { + buffer[0] = (char)(x >> 16 & 0xFF); + buffer[1] = (char)(x >> 8 & 0xFF); + buffer[2] = (char)(x >> 0 & 0xFF); + + buffer += 3, bufcur += 3; + x = 0; + n = 0; + } + } + + if (!frag.len || (state->selection.buflen - bufcur) < 3) { + if (bufcur) { + (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ + .str = state->selection.buffer, + .len = bufcur, + .initial = state->tmp.selection.state == SELECTION_SET_INITIAL, + .final = frag.final && !frag.len, + }, state->selection.user); + state->tmp.selection.state = SELECTION_SET; + } + + buffer = state->selection.buffer; + bufcur = 0; + } + } + + if (n) { + state->tmp.selection.recvpartial = (uint32_t)(n << 24) | x; + } + } +} + +static int on_osc(int command, VTermStringFragment frag, void *user) +{ + VTermState *state = user; + + switch (command) { + case 0: + settermprop_string(state, VTERM_PROP_ICONNAME, frag); + settermprop_string(state, VTERM_PROP_TITLE, frag); + return 1; + + case 1: + settermprop_string(state, VTERM_PROP_ICONNAME, frag); + return 1; + + case 2: + settermprop_string(state, VTERM_PROP_TITLE, frag); + return 1; + + case 52: + if (state->selection.callbacks) { + osc_selection(state, frag); + } + + return 1; + + default: + if (state->fallbacks && state->fallbacks->osc) { + if ((*state->fallbacks->osc)(command, frag, state->fbdata)) { + return 1; + } + } + } + + return 0; +} + +static void request_status_string(VTermState *state, VTermStringFragment frag) +{ + VTerm *vt = state->vt; + + char *tmp = state->tmp.decrqss; + + if (frag.initial) { + tmp[0] = tmp[1] = tmp[2] = tmp[3] = 0; + } + + size_t i = 0; + while (i < sizeof(state->tmp.decrqss) - 1 && tmp[i]) { + i++; + } + while (i < sizeof(state->tmp.decrqss) - 1 && frag.len--) { + tmp[i++] = (frag.str++)[0]; + } + tmp[i] = 0; + + if (!frag.final) { + return; + } + + switch (tmp[0] | tmp[1] << 8 | tmp[2] << 16) { + case 'm': { + // Query SGR + long args[20]; + int argc = vterm_state_getpen(state, args, sizeof(args)/sizeof(args[0])); + size_t cur = 0; + + cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, + vt->mode.ctrl8bit ? "\x90" "1$r" : ESC_S "P" "1$r"); // DCS 1$r ... + if (cur >= vt->tmpbuffer_len) { + return; + } + + for (int argi = 0; argi < argc; argi++) { + cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, + argi == argc - 1 ? "%ld" + : CSI_ARG_HAS_MORE(args[argi]) ? "%ld:" + : "%ld;", + CSI_ARG(args[argi])); + if (cur >= vt->tmpbuffer_len) { + return; + } + } + + cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, + vt->mode.ctrl8bit ? "m" "\x9C" : "m" ESC_S "\\"); // ... m ST + if (cur >= vt->tmpbuffer_len) { + return; + } + + vterm_push_output_bytes(vt, vt->tmpbuffer, cur); + return; + } + + case 'r': + // Query DECSTBM + vterm_push_output_sprintf_str(vt, C1_DCS, true, + "1$r%d;%dr", state->scrollregion_top + 1, + SCROLLREGION_BOTTOM(state)); + return; + + case 's': + // Query DECSLRM + vterm_push_output_sprintf_str(vt, C1_DCS, true, + "1$r%d;%ds", SCROLLREGION_LEFT(state) + 1, + SCROLLREGION_RIGHT(state)); + return; + + case ' '|('q' << 8): { + // Query DECSCUSR + int reply = 0; + switch (state->mode.cursor_shape) { + case VTERM_PROP_CURSORSHAPE_BLOCK: + reply = 2; break; + case VTERM_PROP_CURSORSHAPE_UNDERLINE: + reply = 4; break; + case VTERM_PROP_CURSORSHAPE_BAR_LEFT: + reply = 6; break; + } + if (state->mode.cursor_blink) { + reply--; + } + vterm_push_output_sprintf_str(vt, C1_DCS, true, + "1$r%d q", reply); + return; + } + + case '\"'|('q' << 8): + // Query DECSCA + vterm_push_output_sprintf_str(vt, C1_DCS, true, + "1$r%d\"q", state->protected_cell ? 1 : 2); + return; + } + + vterm_push_output_sprintf_str(state->vt, C1_DCS, true, "0$r"); +} + +static int on_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) +{ + VTermState *state = user; + + if (commandlen == 2 && strneq(command, "$q", 2)) { + request_status_string(state, frag); + return 1; + } else if (state->fallbacks && state->fallbacks->dcs) { + if ((*state->fallbacks->dcs)(command, commandlen, frag, state->fbdata)) { + return 1; + } + } + + DEBUG_LOG("libvterm: Unhandled DCS %.*s\n", (int)commandlen, command); + return 0; +} + +static int on_apc(VTermStringFragment frag, void *user) +{ + VTermState *state = user; + + if (state->fallbacks && state->fallbacks->apc) { + if ((*state->fallbacks->apc)(frag, state->fbdata)) { + return 1; + } + } + + // No DEBUG_LOG because all APCs are unhandled + return 0; +} + +static int on_pm(VTermStringFragment frag, void *user) +{ + VTermState *state = user; + + if (state->fallbacks && state->fallbacks->pm) { + if ((*state->fallbacks->pm)(frag, state->fbdata)) { + return 1; + } + } + + // No DEBUG_LOG because all PMs are unhandled + return 0; +} + +static int on_sos(VTermStringFragment frag, void *user) +{ + VTermState *state = user; + + if (state->fallbacks && state->fallbacks->sos) { + if ((*state->fallbacks->sos)(frag, state->fbdata)) { + return 1; + } + } + + // No DEBUG_LOG because all SOSs are unhandled + return 0; +} + +static int on_resize(int rows, int cols, void *user) +{ + VTermState *state = user; + VTermPos oldpos = state->pos; + + if (cols != state->cols) { + uint8_t *newtabstops = vterm_allocator_malloc(state->vt, ((size_t)cols + 7) / 8); + + // TODO(vterm): This can all be done much more efficiently bytewise + int col; + for (col = 0; col < state->cols && col < cols; col++) { + uint8_t mask = (uint8_t)(1 << (col & 7)); + if (state->tabstops[col >> 3] & mask) { + newtabstops[col >> 3] |= mask; + } else { + newtabstops[col >> 3] &= ~mask; + } + } + + for (; col < cols; col++) { + uint8_t mask = (uint8_t)(1 << (col & 7)); + if (col % 8 == 0) { + newtabstops[col >> 3] |= mask; + } else { + newtabstops[col >> 3] &= ~mask; + } + } + + vterm_allocator_free(state->vt, state->tabstops); + state->tabstops = newtabstops; + } + + state->rows = rows; + state->cols = cols; + + if (state->scrollregion_bottom > -1) { + UBOUND(state->scrollregion_bottom, state->rows); + } + if (state->scrollregion_right > -1) { + UBOUND(state->scrollregion_right, state->cols); + } + + VTermStateFields fields = { + .pos = state->pos, + .lineinfos = {[0] = state->lineinfos[0], [1] = state->lineinfos[1] }, + }; + + if (state->callbacks && state->callbacks->resize) { + (*state->callbacks->resize)(rows, cols, &fields, state->cbdata); + state->pos = fields.pos; + + state->lineinfos[0] = fields.lineinfos[0]; + state->lineinfos[1] = fields.lineinfos[1]; + } else { + if (rows != state->rows) { + for (int bufidx = BUFIDX_PRIMARY; bufidx <= BUFIDX_ALTSCREEN; bufidx++) { + VTermLineInfo *oldlineinfo = state->lineinfos[bufidx]; + if (!oldlineinfo) { + continue; + } + + VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt, + (size_t)rows * sizeof(VTermLineInfo)); + + int row; + for (row = 0; row < state->rows && row < rows; row++) { + newlineinfo[row] = oldlineinfo[row]; + } + + for (; row < rows; row++) { + newlineinfo[row] = (VTermLineInfo){ + .doublewidth = 0, + }; + } + + vterm_allocator_free(state->vt, state->lineinfos[bufidx]); + state->lineinfos[bufidx] = newlineinfo; + } + } + } + + state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY]; + + if (state->at_phantom && state->pos.col < cols - 1) { + state->at_phantom = 0; + state->pos.col++; + } + + if (state->pos.row < 0) { + state->pos.row = 0; + } + if (state->pos.row >= rows) { + state->pos.row = rows - 1; + } + if (state->pos.col < 0) { + state->pos.col = 0; + } + if (state->pos.col >= cols) { + state->pos.col = cols - 1; + } + + updatecursor(state, &oldpos, 1); + + return 1; +} + +static const VTermParserCallbacks parser_callbacks = { + .text = on_text, + .control = on_control, + .escape = on_escape, + .csi = on_csi, + .osc = on_osc, + .dcs = on_dcs, + .apc = on_apc, + .pm = on_pm, + .sos = on_sos, + .resize = on_resize, +}; + +VTermState *vterm_obtain_state(VTerm *vt) +{ + if (vt->state) { + return vt->state; + } + + VTermState *state = vterm_state_new(vt); + vt->state = state; + + vterm_parser_set_callbacks(vt, &parser_callbacks, state); + + return state; +} + +void vterm_state_reset(VTermState *state, int hard) +{ + state->scrollregion_top = 0; + state->scrollregion_bottom = -1; + state->scrollregion_left = 0; + state->scrollregion_right = -1; + + state->mode.keypad = 0; + state->mode.cursor = 0; + state->mode.autowrap = 1; + state->mode.insert = 0; + state->mode.newline = 0; + state->mode.alt_screen = 0; + state->mode.origin = 0; + state->mode.leftrightmargin = 0; + state->mode.bracketpaste = 0; + state->mode.report_focus = 0; + + state->mouse_flags = 0; + + state->vt->mode.ctrl8bit = 0; + + for (int col = 0; col < state->cols; col++) { + if (col % 8 == 0) { + set_col_tabstop(state, col); + } else { + clear_col_tabstop(state, col); + } + } + + for (int row = 0; row < state->rows; row++) { + set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF); + } + + if (state->callbacks && state->callbacks->initpen) { + (*state->callbacks->initpen)(state->cbdata); + } + + vterm_state_resetpen(state); + + VTermEncoding *default_enc = state->vt->mode.utf8 + ? vterm_lookup_encoding(ENC_UTF8, 'u') + : vterm_lookup_encoding(ENC_SINGLE_94, 'B'); + + for (int i = 0; i < 4; i++) { + state->encoding[i].enc = default_enc; + if (default_enc->init) { + (*default_enc->init)(default_enc, state->encoding[i].data); + } + } + + state->gl_set = 0; + state->gr_set = 1; + state->gsingle_set = 0; + + state->protected_cell = 0; + + // Initialise the props + settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, 1); + settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); + settermprop_int(state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); + + if (hard) { + state->pos.row = 0; + state->pos.col = 0; + state->at_phantom = 0; + + VTermRect rect = { 0, state->rows, 0, state->cols }; + erase(state, rect, 0); + } +} + +void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user) +{ + if (callbacks) { + state->callbacks = callbacks; + state->cbdata = user; + + if (state->callbacks && state->callbacks->initpen) { + (*state->callbacks->initpen)(state->cbdata); + } + } else { + state->callbacks = NULL; + state->cbdata = NULL; + } +} + +void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermStateFallbacks *fallbacks, + void *user) +{ + if (fallbacks) { + state->fallbacks = fallbacks; + state->fbdata = user; + } else { + state->fallbacks = NULL; + state->fbdata = NULL; + } +} + +int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val) +{ + // Only store the new value of the property if usercode said it was happy. This is especially + // important for altscreen switching + if (state->callbacks && state->callbacks->settermprop) { + if (!(*state->callbacks->settermprop)(prop, val, state->cbdata)) { + return 0; + } + } + + switch (prop) { + case VTERM_PROP_TITLE: + case VTERM_PROP_ICONNAME: + // we don't store these, just transparently pass through + return 1; + case VTERM_PROP_CURSORVISIBLE: + state->mode.cursor_visible = (unsigned)val->boolean; + return 1; + case VTERM_PROP_CURSORBLINK: + state->mode.cursor_blink = (unsigned)val->boolean; + return 1; + case VTERM_PROP_CURSORSHAPE: + state->mode.cursor_shape = (unsigned)val->number; + return 1; + case VTERM_PROP_REVERSE: + state->mode.screen = (unsigned)val->boolean; + return 1; + case VTERM_PROP_ALTSCREEN: + state->mode.alt_screen = (unsigned)val->boolean; + state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY]; + if (state->mode.alt_screen) { + VTermRect rect = { + .start_row = 0, + .start_col = 0, + .end_row = state->rows, + .end_col = state->cols, + }; + erase(state, rect, 0); + } + return 1; + case VTERM_PROP_MOUSE: + state->mouse_flags = 0; + if (val->number) { + state->mouse_flags |= MOUSE_WANT_CLICK; + } + if (val->number == VTERM_PROP_MOUSE_DRAG) { + state->mouse_flags |= MOUSE_WANT_DRAG; + } + if (val->number == VTERM_PROP_MOUSE_MOVE) { + state->mouse_flags |= MOUSE_WANT_MOVE; + } + return 1; + case VTERM_PROP_FOCUSREPORT: + state->mode.report_focus = (unsigned)val->boolean; + return 1; + + case VTERM_N_PROPS: + return 0; + } + + return 0; +} + +void vterm_state_focus_in(VTermState *state) +{ + if (state->mode.report_focus) { + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "I"); + } +} + +void vterm_state_focus_out(VTermState *state) +{ + if (state->mode.report_focus) { + vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "O"); + } +} + +const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row) +{ + return state->lineinfo + row; +} + +void vterm_state_set_selection_callbacks(VTermState *state, + const VTermSelectionCallbacks *callbacks, void *user, + char *buffer, size_t buflen) +{ + if (buflen && !buffer) { + buffer = vterm_allocator_malloc(state->vt, buflen); + } + + state->selection.callbacks = callbacks; + state->selection.user = user; + state->selection.buffer = buffer; + state->selection.buflen = buflen; +} diff --git a/src/nvim/vterm/state.h b/src/nvim/vterm/state.h new file mode 100644 index 0000000000..2f59cf7eec --- /dev/null +++ b/src/nvim/vterm/state.h @@ -0,0 +1,7 @@ +#pragma once + +#include "nvim/vterm/vterm_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/state.h.generated.h" +#endif diff --git a/src/nvim/vterm/vterm.c b/src/nvim/vterm/vterm.c new file mode 100644 index 0000000000..76d5dc3808 --- /dev/null +++ b/src/nvim/vterm/vterm.c @@ -0,0 +1,335 @@ +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "auto/config.h" +#include "nvim/memory.h" +#include "nvim/vterm/screen.h" +#include "nvim/vterm/state.h" +#include "nvim/vterm/vterm.h" +#include "nvim/vterm/vterm_internal_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/vterm.c.generated.h" +#endif + +// ***************** +// * API functions * +// ***************** + +static void *default_malloc(size_t size, void *allocdata) +{ + void *ptr = xmalloc(size); + if (ptr) { + memset(ptr, 0, size); + } + return ptr; +} + +static void default_free(void *ptr, void *allocdata) +{ + xfree(ptr); +} + +static VTermAllocatorFunctions default_allocator = { + .malloc = &default_malloc, + .free = &default_free, +}; + +/// Convenient shortcut for default cases +VTerm *vterm_new(int rows, int cols) +{ + return vterm_build(&(const struct VTermBuilder){ + .rows = rows, + .cols = cols, + }); +} + +// A handy macro for defaulting values out of builder fields +#define DEFAULT(v, def) ((v) ? (v) : (def)) + +VTerm *vterm_build(const struct VTermBuilder *builder) +{ + const VTermAllocatorFunctions *allocator = DEFAULT(builder->allocator, &default_allocator); + + // Need to bootstrap using the allocator function directly + VTerm *vt = (*allocator->malloc)(sizeof(VTerm), builder->allocdata); + + vt->allocator = allocator; + vt->allocdata = builder->allocdata; + + vt->rows = builder->rows; + vt->cols = builder->cols; + + vt->parser.state = NORMAL; + + vt->parser.callbacks = NULL; + vt->parser.cbdata = NULL; + + vt->parser.emit_nul = false; + + vt->outfunc = NULL; + vt->outdata = NULL; + + vt->outbuffer_len = DEFAULT(builder->outbuffer_len, 4096); + vt->outbuffer_cur = 0; + vt->outbuffer = vterm_allocator_malloc(vt, vt->outbuffer_len); + + vt->tmpbuffer_len = DEFAULT(builder->tmpbuffer_len, 4096); + vt->tmpbuffer = vterm_allocator_malloc(vt, vt->tmpbuffer_len); + + return vt; +} + +void vterm_free(VTerm *vt) +{ + if (vt->screen) { + vterm_screen_free(vt->screen); + } + + if (vt->state) { + vterm_state_free(vt->state); + } + + vterm_allocator_free(vt, vt->outbuffer); + vterm_allocator_free(vt, vt->tmpbuffer); + + vterm_allocator_free(vt, vt); +} + +void *vterm_allocator_malloc(VTerm *vt, size_t size) +{ + return (*vt->allocator->malloc)(size, vt->allocdata); +} + +void vterm_allocator_free(VTerm *vt, void *ptr) +{ + (*vt->allocator->free)(ptr, vt->allocdata); +} + +void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp) +{ + if (rowsp) { + *rowsp = vt->rows; + } + if (colsp) { + *colsp = vt->cols; + } +} + +void vterm_set_size(VTerm *vt, int rows, int cols) +{ + if (rows < 1 || cols < 1) { + return; + } + + vt->rows = rows; + vt->cols = cols; + + if (vt->parser.callbacks && vt->parser.callbacks->resize) { + (*vt->parser.callbacks->resize)(rows, cols, vt->parser.cbdata); + } +} + +void vterm_set_utf8(VTerm *vt, int is_utf8) +{ + vt->mode.utf8 = (unsigned)is_utf8; +} + +void vterm_output_set_callback(VTerm *vt, VTermOutputCallback *func, void *user) +{ + vt->outfunc = func; + vt->outdata = user; +} + +void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len) +{ + if (vt->outfunc) { + (vt->outfunc)(bytes, len, vt->outdata); + return; + } + + if (len > vt->outbuffer_len - vt->outbuffer_cur) { + return; + } + + memcpy(vt->outbuffer + vt->outbuffer_cur, bytes, len); + vt->outbuffer_cur += len; +} + +void vterm_push_output_sprintf(VTerm *vt, const char *format, ...) + FUNC_ATTR_PRINTF(2, 3) +{ + va_list args; + va_start(args, format); + size_t len = (size_t)vsnprintf(vt->tmpbuffer, vt->tmpbuffer_len, format, args); + vterm_push_output_bytes(vt, vt->tmpbuffer, len); + va_end(args); +} + +void vterm_push_output_sprintf_ctrl(VTerm *vt, uint8_t ctrl, const char *fmt, ...) + FUNC_ATTR_PRINTF(3, 4) +{ + size_t cur; + + if (ctrl >= 0x80 && !vt->mode.ctrl8bit) { + cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, ESC_S "%c", ctrl - 0x40); + } else { + cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, "%c", ctrl); + } + + if (cur >= vt->tmpbuffer_len) { + return; + } + + va_list args; + va_start(args, fmt); + cur += (size_t)vsnprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, fmt, args); + va_end(args); + + if (cur >= vt->tmpbuffer_len) { + return; + } + + vterm_push_output_bytes(vt, vt->tmpbuffer, cur); +} + +void vterm_push_output_sprintf_str(VTerm *vt, uint8_t ctrl, bool term, const char *fmt, ...) + FUNC_ATTR_PRINTF(4, 5) +{ + size_t cur = 0; + + if (ctrl) { + if (ctrl >= 0x80 && !vt->mode.ctrl8bit) { + cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, ESC_S "%c", ctrl - 0x40); + } else { + cur = (size_t)snprintf(vt->tmpbuffer, vt->tmpbuffer_len, "%c", ctrl); + } + + if (cur >= vt->tmpbuffer_len) { + return; + } + } + + va_list args; + va_start(args, fmt); + cur += (size_t)vsnprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, fmt, args); + va_end(args); + + if (cur >= vt->tmpbuffer_len) { + return; + } + + if (term) { + cur += (size_t)snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, + vt->mode.ctrl8bit ? "\x9C" : ESC_S "\\"); // ST + + if (cur >= vt->tmpbuffer_len) { + return; + } + } + + vterm_push_output_bytes(vt, vt->tmpbuffer, cur); +} + +VTermValueType vterm_get_attr_type(VTermAttr attr) +{ + switch (attr) { + case VTERM_ATTR_BOLD: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_UNDERLINE: + return VTERM_VALUETYPE_INT; + case VTERM_ATTR_ITALIC: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_BLINK: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_REVERSE: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_CONCEAL: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_STRIKE: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_FONT: + return VTERM_VALUETYPE_INT; + case VTERM_ATTR_FOREGROUND: + return VTERM_VALUETYPE_COLOR; + case VTERM_ATTR_BACKGROUND: + return VTERM_VALUETYPE_COLOR; + case VTERM_ATTR_SMALL: + return VTERM_VALUETYPE_BOOL; + case VTERM_ATTR_BASELINE: + return VTERM_VALUETYPE_INT; + case VTERM_ATTR_URI: + return VTERM_VALUETYPE_INT; + + case VTERM_N_ATTRS: + return 0; + } + return 0; // UNREACHABLE +} + +void vterm_scroll_rect(VTermRect rect, int downward, int rightward, + int (*moverect)(VTermRect src, VTermRect dest, void *user), + int (*eraserect)(VTermRect rect, int selective, void *user), void *user) +{ + VTermRect src; + VTermRect dest; + + if (abs(downward) >= rect.end_row - rect.start_row + || abs(rightward) >= rect.end_col - rect.start_col) { + // Scroll more than area; just erase the lot + (*eraserect)(rect, 0, user); + return; + } + + if (rightward >= 0) { + // rect: [XXX................] + // src: [----------------] + // dest: [----------------] + dest.start_col = rect.start_col; + dest.end_col = rect.end_col - rightward; + src.start_col = rect.start_col + rightward; + src.end_col = rect.end_col; + } else { + // rect: [................XXX] + // src: [----------------] + // dest: [----------------] + int leftward = -rightward; + dest.start_col = rect.start_col + leftward; + dest.end_col = rect.end_col; + src.start_col = rect.start_col; + src.end_col = rect.end_col - leftward; + } + + if (downward >= 0) { + dest.start_row = rect.start_row; + dest.end_row = rect.end_row - downward; + src.start_row = rect.start_row + downward; + src.end_row = rect.end_row; + } else { + int upward = -downward; + dest.start_row = rect.start_row + upward; + dest.end_row = rect.end_row; + src.start_row = rect.start_row; + src.end_row = rect.end_row - upward; + } + + if (moverect) { + (*moverect)(dest, src, user); + } + + if (downward > 0) { + rect.start_row = rect.end_row - downward; + } else if (downward < 0) { + rect.end_row = rect.start_row - downward; + } + + if (rightward > 0) { + rect.start_col = rect.end_col - rightward; + } else if (rightward < 0) { + rect.end_col = rect.start_col - rightward; + } + + (*eraserect)(rect, 0, user); +} diff --git a/src/nvim/vterm/vterm.h b/src/nvim/vterm/vterm.h new file mode 100644 index 0000000000..e66f40425a --- /dev/null +++ b/src/nvim/vterm/vterm.h @@ -0,0 +1,161 @@ +#pragma once + +#include <stdarg.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> + +#include "nvim/macros_defs.h" +#include "nvim/types_defs.h" +#include "nvim/vterm/vterm_defs.h" +#include "nvim/vterm/vterm_keycodes_defs.h" + +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "vterm/vterm.h.generated.h" +#endif + +#define VTERM_VERSION_MAJOR 0 +#define VTERM_VERSION_MINOR 3 + +// move a rect +static inline void vterm_rect_move(VTermRect *rect, int row_delta, int col_delta) +{ + rect->start_row += row_delta; rect->end_row += row_delta; + rect->start_col += col_delta; rect->end_col += col_delta; +} + +// Bit-field describing the content of the tagged union `VTermColor`. +typedef enum { + // If the lower bit of `type` is not set, the colour is 24-bit RGB. + VTERM_COLOR_RGB = 0x00, + + // The colour is an index into a palette of 256 colours. + VTERM_COLOR_INDEXED = 0x01, + + // Mask that can be used to extract the RGB/Indexed bit. + VTERM_COLOR_TYPE_MASK = 0x01, + + // If set, indicates that this colour should be the default foreground color, i.e. there was no + // SGR request for another colour. When rendering this colour it is possible to ignore "idx" and + // just use a colour that is not in the palette. + VTERM_COLOR_DEFAULT_FG = 0x02, + + // If set, indicates that this colour should be the default background color, i.e. there was no + // SGR request for another colour. A common option when rendering this colour is to not render a + // background at all, for example by rendering the window transparently at this spot. + VTERM_COLOR_DEFAULT_BG = 0x04, + + // Mask that can be used to extract the default foreground/background bit. + VTERM_COLOR_DEFAULT_MASK = 0x06, +} VTermColorType; + +// Returns true if the VTERM_COLOR_RGB `type` flag is set, indicating that the given VTermColor +// instance is an indexed colour. +#define VTERM_COLOR_IS_INDEXED(col) \ + (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_INDEXED) + +// Returns true if the VTERM_COLOR_INDEXED `type` flag is set, indicating that the given VTermColor +// instance is an rgb colour. +#define VTERM_COLOR_IS_RGB(col) \ + (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_RGB) + +// Returns true if the VTERM_COLOR_DEFAULT_FG `type` flag is set, indicating that the given +// VTermColor instance corresponds to the default foreground color. +#define VTERM_COLOR_IS_DEFAULT_FG(col) \ + (!!((col)->type & VTERM_COLOR_DEFAULT_FG)) + +// Returns true if the VTERM_COLOR_DEFAULT_BG `type` flag is set, indicating that the given +// VTermColor instance corresponds to the default background color. +#define VTERM_COLOR_IS_DEFAULT_BG(col) \ + (!!((col)->type & VTERM_COLOR_DEFAULT_BG)) + +// Constructs a new VTermColor instance representing the given RGB values. +static inline void vterm_color_rgb(VTermColor *col, uint8_t red, uint8_t green, uint8_t blue) +{ + col->type = VTERM_COLOR_RGB; + col->rgb.red = red; + col->rgb.green = green; + col->rgb.blue = blue; +} + +// Construct a new VTermColor instance representing an indexed color with the given index. +static inline void vterm_color_indexed(VTermColor *col, uint8_t idx) +{ + col->type = VTERM_COLOR_INDEXED; + col->indexed.idx = idx; +} + +// ------------ +// Parser layer +// ------------ + +/// Flag to indicate non-final subparameters in a single CSI parameter. +/// Consider +/// CSI 1;2:3:4;5a +/// 1 4 and 5 are final. +/// 2 and 3 are non-final and will have this bit set +/// +/// Don't confuse this with the final byte of the CSI escape; 'a' in this case. +#define CSI_ARG_FLAG_MORE (1U << 31) +#define CSI_ARG_MASK (~(1U << 31)) + +#define CSI_ARG_HAS_MORE(a) ((a)& CSI_ARG_FLAG_MORE) +#define CSI_ARG(a) ((a)& CSI_ARG_MASK) + +// Can't use -1 to indicate a missing argument; use this instead +#define CSI_ARG_MISSING ((1UL<<31) - 1) + +#define CSI_ARG_IS_MISSING(a) (CSI_ARG(a) == CSI_ARG_MISSING) +#define CSI_ARG_OR(a, def) (CSI_ARG(a) == CSI_ARG_MISSING ? (def) : CSI_ARG(a)) +#define CSI_ARG_COUNT(a) (CSI_ARG(a) == CSI_ARG_MISSING || CSI_ARG(a) == 0 ? 1 : CSI_ARG(a)) + +enum { + VTERM_UNDERLINE_OFF, + VTERM_UNDERLINE_SINGLE, + VTERM_UNDERLINE_DOUBLE, + VTERM_UNDERLINE_CURLY, +}; + +enum { + VTERM_BASELINE_NORMAL, + VTERM_BASELINE_RAISE, + VTERM_BASELINE_LOWER, +}; + +// Back-compat alias for the brief time it was in 0.3-RC1 +#define vterm_screen_set_reflow vterm_screen_enable_reflow + +void vterm_scroll_rect(VTermRect rect, int downward, int rightward, + int (*moverect)(VTermRect src, VTermRect dest, void *user), + int (*eraserect)(VTermRect rect, int selective, void *user), void *user); + +struct VTermScreen { + VTerm *vt; + VTermState *state; + + const VTermScreenCallbacks *callbacks; + void *cbdata; + + VTermDamageSize damage_merge; + // start_row == -1 => no damage + VTermRect damaged; + VTermRect pending_scrollrect; + int pending_scroll_downward, pending_scroll_rightward; + + int rows; + int cols; + + unsigned global_reverse : 1; + unsigned reflow : 1; + + // Primary and Altscreen. buffers[1] is lazily allocated as needed + ScreenCell *buffers[2]; + + // buffer will == buffers[0] or buffers[1], depending on altscreen + ScreenCell *buffer; + + // buffer for a single screen row used in scrollback storage callbacks + VTermScreenCell *sb_buffer; + + ScreenPen pen; +}; diff --git a/src/nvim/vterm/vterm_defs.h b/src/nvim/vterm/vterm_defs.h new file mode 100644 index 0000000000..d0a8ba8814 --- /dev/null +++ b/src/nvim/vterm/vterm_defs.h @@ -0,0 +1,319 @@ +#pragma once +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#include "nvim/types_defs.h" + +typedef struct VTerm VTerm; +typedef struct VTermState VTermState; +typedef struct VTermScreen VTermScreen; + +typedef struct { + int row; + int col; +} VTermPos; + +// some small utility functions; we can just keep these static here + +typedef struct { + int start_row; + int end_row; + int start_col; + int end_col; +} VTermRect; + +// Tagged union storing either an RGB color or an index into a colour palette. In order to convert +// indexed colours to RGB, you may use the vterm_state_convert_color_to_rgb() or +// vterm_screen_convert_color_to_rgb() functions which lookup the RGB colour from the palette +// maintained by a VTermState or VTermScreen instance. +typedef union { + // Tag indicating which union member is actually valid. This variable coincides with the `type` + // member of the `rgb` and the `indexed` struct in memory. Please use the `VTERM_COLOR_IS_*` test + // macros to check whether a particular type flag is set. + uint8_t type; + + // Valid if `VTERM_COLOR_IS_RGB(type)` is true. Holds the RGB colour values. + struct { + // Same as the top-level `type` member stored in VTermColor. + uint8_t type; + + // The actual 8-bit red, green, blue colour values. + uint8_t red, green, blue; + } rgb; + + // If `VTERM_COLOR_IS_INDEXED(type)` is true, this member holds the index into the colour palette. + struct { + // Same as the top-level `type` member stored in VTermColor. + uint8_t type; + + // Index into the colour map. + uint8_t idx; + } indexed; +} VTermColor; + +typedef struct { + unsigned bold : 1; + unsigned underline : 2; + unsigned italic : 1; + unsigned blink : 1; + unsigned reverse : 1; + unsigned conceal : 1; + unsigned strike : 1; + unsigned font : 4; // 0 to 9 + unsigned dwl : 1; // On a DECDWL or DECDHL line + unsigned dhl : 2; // On a DECDHL line (1=top 2=bottom) + unsigned small : 1; + unsigned baseline : 2; +} VTermScreenCellAttrs; + +typedef struct { + schar_T schar; + char width; + VTermScreenCellAttrs attrs; + VTermColor fg, bg; + int uri; +} VTermScreenCell; + +typedef enum { + // VTERM_PROP_NONE = 0 + VTERM_PROP_CURSORVISIBLE = 1, // bool + VTERM_PROP_CURSORBLINK, // bool + VTERM_PROP_ALTSCREEN, // bool + VTERM_PROP_TITLE, // string + VTERM_PROP_ICONNAME, // string + VTERM_PROP_REVERSE, // bool + VTERM_PROP_CURSORSHAPE, // number + VTERM_PROP_MOUSE, // number + VTERM_PROP_FOCUSREPORT, // bool + + VTERM_N_PROPS, +} VTermProp; + +typedef struct { + const char *str; + size_t len : 30; + bool initial : 1; + bool final : 1; +} VTermStringFragment; + +typedef union { + int boolean; + int number; + VTermStringFragment string; + VTermColor color; +} VTermValue; + +typedef struct { + int (*damage)(VTermRect rect, void *user); + int (*moverect)(VTermRect dest, VTermRect src, void *user); + int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user); + int (*settermprop)(VTermProp prop, VTermValue *val, void *user); + int (*bell)(void *user); + int (*resize)(int rows, int cols, void *user); + int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user); + int (*sb_popline)(int cols, VTermScreenCell *cells, void *user); + int (*sb_clear)(void *user); +} VTermScreenCallbacks; + +typedef struct { + int (*control)(uint8_t control, void *user); + int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, + char command, void *user); + int (*osc)(int command, VTermStringFragment frag, void *user); + int (*dcs)(const char *command, size_t commandlen, VTermStringFragment frag, void *user); + int (*apc)(VTermStringFragment frag, void *user); + int (*pm)(VTermStringFragment frag, void *user); + int (*sos)(VTermStringFragment frag, void *user); +} VTermStateFallbacks; + +typedef enum { + VTERM_DAMAGE_CELL, // every cell + VTERM_DAMAGE_ROW, // entire rows + VTERM_DAMAGE_SCREEN, // entire screen + VTERM_DAMAGE_SCROLL, // entire screen + scrollrect + + VTERM_N_DAMAGES, +} VTermDamageSize; + +typedef enum { + VTERM_ATTR_BOLD_MASK = 1 << 0, + VTERM_ATTR_UNDERLINE_MASK = 1 << 1, + VTERM_ATTR_ITALIC_MASK = 1 << 2, + VTERM_ATTR_BLINK_MASK = 1 << 3, + VTERM_ATTR_REVERSE_MASK = 1 << 4, + VTERM_ATTR_STRIKE_MASK = 1 << 5, + VTERM_ATTR_FONT_MASK = 1 << 6, + VTERM_ATTR_FOREGROUND_MASK = 1 << 7, + VTERM_ATTR_BACKGROUND_MASK = 1 << 8, + VTERM_ATTR_CONCEAL_MASK = 1 << 9, + VTERM_ATTR_SMALL_MASK = 1 << 10, + VTERM_ATTR_BASELINE_MASK = 1 << 11, + VTERM_ATTR_URI_MASK = 1 << 12, + + VTERM_ALL_ATTRS_MASK = (1 << 13) - 1, +} VTermAttrMask; + +typedef enum { + // VTERM_VALUETYPE_NONE = 0 + VTERM_VALUETYPE_BOOL = 1, + VTERM_VALUETYPE_INT, + VTERM_VALUETYPE_STRING, + VTERM_VALUETYPE_COLOR, + + VTERM_N_VALUETYPES, +} VTermValueType; + +typedef enum { + // VTERM_ATTR_NONE = 0 + VTERM_ATTR_BOLD = 1, // bool: 1, 22 + VTERM_ATTR_UNDERLINE, // number: 4, 21, 24 + VTERM_ATTR_ITALIC, // bool: 3, 23 + VTERM_ATTR_BLINK, // bool: 5, 25 + VTERM_ATTR_REVERSE, // bool: 7, 27 + VTERM_ATTR_CONCEAL, // bool: 8, 28 + VTERM_ATTR_STRIKE, // bool: 9, 29 + VTERM_ATTR_FONT, // number: 10-19 + VTERM_ATTR_FOREGROUND, // color: 30-39 90-97 + VTERM_ATTR_BACKGROUND, // color: 40-49 100-107 + VTERM_ATTR_SMALL, // bool: 73, 74, 75 + VTERM_ATTR_BASELINE, // number: 73, 74, 75 + VTERM_ATTR_URI, // number + + VTERM_N_ATTRS, +} VTermAttr; + +enum { + VTERM_PROP_CURSORSHAPE_BLOCK = 1, + VTERM_PROP_CURSORSHAPE_UNDERLINE, + VTERM_PROP_CURSORSHAPE_BAR_LEFT, + + VTERM_N_PROP_CURSORSHAPES, +}; + +enum { + VTERM_PROP_MOUSE_NONE = 0, + VTERM_PROP_MOUSE_CLICK, + VTERM_PROP_MOUSE_DRAG, + VTERM_PROP_MOUSE_MOVE, + + VTERM_N_PROP_MOUSES, +}; + +typedef enum { + VTERM_SELECTION_CLIPBOARD = (1<<0), + VTERM_SELECTION_PRIMARY = (1<<1), + VTERM_SELECTION_SECONDARY = (1<<2), + VTERM_SELECTION_SELECT = (1<<3), + VTERM_SELECTION_CUT0 = (1<<4), // also CUT1 .. CUT7 by bitshifting +} VTermSelectionMask; + +typedef struct { + schar_T schar; + int width; + unsigned protected_cell:1; // DECSCA-protected against DECSEL/DECSED + unsigned dwl:1; // DECDWL or DECDHL double-width line + unsigned dhl:2; // DECDHL double-height line (1=top 2=bottom) +} VTermGlyphInfo; + +typedef struct { + unsigned doublewidth:1; // DECDWL or DECDHL line + unsigned doubleheight:2; // DECDHL line (1=top 2=bottom) + unsigned continuation:1; // Line is a flow continuation of the previous +} VTermLineInfo; + +// Copies of VTermState fields that the 'resize' callback might have reason to edit. 'resize' +// callback gets total control of these fields and may free-and-reallocate them if required. They +// will be copied back from the struct after the callback has returned. +typedef struct { + VTermPos pos; // current cursor position + VTermLineInfo *lineinfos[2]; // [1] may be NULL +} VTermStateFields; + +typedef struct { + // libvterm relies on this memory to be zeroed out before it is returned by the allocator. + void *(*malloc)(size_t size, void *allocdata); + void (*free)(void *ptr, void *allocdata); +} VTermAllocatorFunctions; + +// Setting output callback will override the buffer logic +typedef void VTermOutputCallback(const char *s, size_t len, void *user); + +struct VTermBuilder { + int ver; // currently unused but reserved for some sort of ABI version flag + + int rows, cols; + + const VTermAllocatorFunctions *allocator; + void *allocdata; + + // Override default sizes for various structures + size_t outbuffer_len; // default: 4096 + size_t tmpbuffer_len; // default: 4096 +}; + +typedef struct { + int (*putglyph)(VTermGlyphInfo *info, VTermPos pos, void *user); + int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user); + int (*scrollrect)(VTermRect rect, int downward, int rightward, void *user); + int (*moverect)(VTermRect dest, VTermRect src, void *user); + int (*erase)(VTermRect rect, int selective, void *user); + int (*initpen)(void *user); + int (*setpenattr)(VTermAttr attr, VTermValue *val, void *user); + int (*settermprop)(VTermProp prop, VTermValue *val, void *user); + int (*bell)(void *user); + int (*resize)(int rows, int cols, VTermStateFields *fields, void *user); + int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, + void *user); + int (*sb_clear)(void *user); +} VTermStateCallbacks; + +typedef struct { + int (*set)(VTermSelectionMask mask, VTermStringFragment frag, void *user); + int (*query)(VTermSelectionMask mask, void *user); +} VTermSelectionCallbacks; + +typedef struct { + int (*text)(const char *bytes, size_t len, void *user); + int (*control)(uint8_t control, void *user); + int (*escape)(const char *bytes, size_t len, void *user); + int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, + char command, void *user); + int (*osc)(int command, VTermStringFragment frag, void *user); + int (*dcs)(const char *command, size_t commandlen, VTermStringFragment frag, void *user); + int (*apc)(VTermStringFragment frag, void *user); + int (*pm)(VTermStringFragment frag, void *user); + int (*sos)(VTermStringFragment frag, void *user); + int (*resize)(int rows, int cols, void *user); +} VTermParserCallbacks; + +// State of the pen at some moment in time, also used in a cell +typedef struct { + // After the bitfield + VTermColor fg, bg; + + // Opaque ID that maps to a URI in a set + int uri; + + unsigned bold : 1; + unsigned underline : 2; + unsigned italic : 1; + unsigned blink : 1; + unsigned reverse : 1; + unsigned conceal : 1; + unsigned strike : 1; + unsigned font : 4; // 0 to 9 + unsigned small : 1; + unsigned baseline : 2; + + // Extra state storage that isn't strictly pen-related + unsigned protected_cell : 1; + unsigned dwl : 1; // on a DECDWL or DECDHL line + unsigned dhl : 2; // on a DECDHL line (1=top 2=bottom) +} ScreenPen; + +// Internal representation of a screen cell +typedef struct { + schar_T schar; + ScreenPen pen; +} ScreenCell; diff --git a/src/nvim/vterm/vterm_internal_defs.h b/src/nvim/vterm/vterm_internal_defs.h new file mode 100644 index 0000000000..770f862ce3 --- /dev/null +++ b/src/nvim/vterm/vterm_internal_defs.h @@ -0,0 +1,266 @@ +#pragma once + +#include <stdarg.h> + +#include "nvim/mbyte_defs.h" +#include "nvim/vterm/vterm_defs.h" + +#ifdef DEBUG +# define DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__) +#else +# define DEBUG_LOG(...) +#endif + +#define ESC_S "\x1b" + +#define INTERMED_MAX 16 + +#define CSI_ARGS_MAX 16 +#define CSI_LEADER_MAX 16 + +#define BUFIDX_PRIMARY 0 +#define BUFIDX_ALTSCREEN 1 + +typedef struct VTermEncoding VTermEncoding; + +typedef struct { + VTermEncoding *enc; + + // This size should be increased if required by other stateful encodings + char data[4 * sizeof(uint32_t)]; +} VTermEncodingInstance; + +struct VTermPen { + VTermColor fg; + VTermColor bg; + int uri; + unsigned bold:1; + unsigned underline:2; + unsigned italic:1; + unsigned blink:1; + unsigned reverse:1; + unsigned conceal:1; + unsigned strike:1; + unsigned font:4; // To store 0-9 + unsigned small:1; + unsigned baseline:2; +}; + +struct VTermState { + VTerm *vt; + + const VTermStateCallbacks *callbacks; + void *cbdata; + + const VTermStateFallbacks *fallbacks; + void *fbdata; + + int rows; + int cols; + + // Current cursor position + VTermPos pos; + + int at_phantom; // True if we're on the "81st" phantom column to defer a wraparound + + int scrollregion_top; + int scrollregion_bottom; // -1 means unbounded +#define SCROLLREGION_BOTTOM(state) ((state)->scrollregion_bottom > \ + -1 ? (state)->scrollregion_bottom : (state)->rows) + int scrollregion_left; +#define SCROLLREGION_LEFT(state) ((state)->mode.leftrightmargin ? (state)->scrollregion_left : 0) + int scrollregion_right; // -1 means unbounded +#define SCROLLREGION_RIGHT(state) ((state)->mode.leftrightmargin \ + && (state)->scrollregion_right > \ + -1 ? (state)->scrollregion_right : (state)->cols) + + // Bitvector of tab stops + uint8_t *tabstops; + + // Primary and Altscreen; lineinfos[1] is lazily allocated as needed + VTermLineInfo *lineinfos[2]; + + // lineinfo will == lineinfos[0] or lineinfos[1], depending on altscreen + VTermLineInfo *lineinfo; +#define ROWWIDTH(state, \ + row) ((state)->lineinfo[(row)].doublewidth ? ((state)->cols / 2) : (state)->cols) +#define THISROWWIDTH(state) ROWWIDTH(state, (state)->pos.row) + + // Mouse state + int mouse_col, mouse_row; + int mouse_buttons; + int mouse_flags; +#define MOUSE_WANT_CLICK 0x01 +#define MOUSE_WANT_DRAG 0x02 +#define MOUSE_WANT_MOVE 0x04 + + enum { MOUSE_X10, MOUSE_UTF8, MOUSE_SGR, MOUSE_RXVT, } mouse_protocol; + +// Last glyph output, for Unicode recombining purposes + char grapheme_buf[MAX_SCHAR_SIZE]; + size_t grapheme_len; + uint32_t grapheme_last; // last added UTF-32 char + GraphemeState grapheme_state; + int combine_width; // The width of the glyph above + VTermPos combine_pos; // Position before movement + + struct { + unsigned keypad:1; + unsigned cursor:1; + unsigned autowrap:1; + unsigned insert:1; + unsigned newline:1; + unsigned cursor_visible:1; + unsigned cursor_blink:1; + unsigned cursor_shape:2; + unsigned alt_screen:1; + unsigned origin:1; + unsigned screen:1; + unsigned leftrightmargin:1; + unsigned bracketpaste:1; + unsigned report_focus:1; + } mode; + + VTermEncodingInstance encoding[4], encoding_utf8; + int gl_set, gr_set, gsingle_set; + + struct VTermPen pen; + + VTermColor default_fg; + VTermColor default_bg; + VTermColor colors[16]; // Store the 8 ANSI and the 8 ANSI high-brights only + + int bold_is_highbright; + + unsigned protected_cell : 1; + +// Saved state under DEC mode 1048/1049 + struct { + VTermPos pos; + struct VTermPen pen; + + struct { + unsigned cursor_visible:1; + unsigned cursor_blink:1; + unsigned cursor_shape:2; + } mode; + } saved; + +// Temporary state for DECRQSS parsing + union { + char decrqss[4]; + struct { + uint16_t mask; + enum { + SELECTION_INITIAL, + SELECTION_SELECTED, + SELECTION_QUERY, + SELECTION_SET_INITIAL, + SELECTION_SET, + SELECTION_INVALID, + } state : 8; + uint32_t recvpartial; + uint32_t sendpartial; + } selection; + } tmp; + + struct { + const VTermSelectionCallbacks *callbacks; + void *user; + char *buffer; + size_t buflen; + } selection; +}; + +struct VTerm { + const VTermAllocatorFunctions *allocator; + void *allocdata; + + int rows; + int cols; + + struct { + unsigned utf8:1; + unsigned ctrl8bit:1; + } mode; + + struct { + enum VTermParserState { + NORMAL, + CSI_LEADER, + CSI_ARGS, + CSI_INTERMED, + DCS_COMMAND, + // below here are the "string states" + OSC_COMMAND, + OSC, + DCS_VTERM, + APC, + PM, + SOS, + } state; + + bool in_esc : 1; + + int intermedlen; + char intermed[INTERMED_MAX]; + + union { + struct { + int leaderlen; + char leader[CSI_LEADER_MAX]; + + int argi; + long args[CSI_ARGS_MAX]; + } csi; + struct { + int command; + } osc; + struct { + int commandlen; + char command[CSI_LEADER_MAX]; + } dcs; + } v; + + const VTermParserCallbacks *callbacks; + void *cbdata; + + bool string_initial; + + bool emit_nul; + } parser; + + // len == malloc()ed size; cur == number of valid bytes + + VTermOutputCallback *outfunc; + void *outdata; + + char *outbuffer; + size_t outbuffer_len; + size_t outbuffer_cur; + + char *tmpbuffer; + size_t tmpbuffer_len; + + VTermState *state; + VTermScreen *screen; +}; + +struct VTermEncoding { + void (*init)(VTermEncoding *enc, void *data); + void (*decode)(VTermEncoding *enc, void *data, uint32_t cp[], int *cpi, int cplen, + const char bytes[], size_t *pos, size_t len); +}; + +typedef enum { + ENC_UTF8, + ENC_SINGLE_94, +} VTermEncodingType; + +enum { + C1_SS3 = 0x8f, + C1_DCS = 0x90, + C1_CSI = 0x9b, + C1_ST = 0x9c, + C1_OSC = 0x9d, +}; diff --git a/src/vterm/vterm_keycodes.h b/src/nvim/vterm/vterm_keycodes_defs.h index 661759febd..70db05af54 100644 --- a/src/vterm/vterm_keycodes.h +++ b/src/nvim/vterm/vterm_keycodes_defs.h @@ -1,5 +1,4 @@ -#ifndef __VTERM_INPUT_H__ -#define __VTERM_INPUT_H__ +#pragma once typedef enum { VTERM_MOD_NONE = 0x00, @@ -7,7 +6,7 @@ typedef enum { VTERM_MOD_ALT = 0x02, VTERM_MOD_CTRL = 0x04, - VTERM_ALL_MODS_MASK = 0x07 + VTERM_ALL_MODS_MASK = 0x07, } VTermModifier; typedef enum { @@ -52,10 +51,8 @@ typedef enum { VTERM_KEY_KP_ENTER, VTERM_KEY_KP_EQUAL, - VTERM_KEY_MAX, // Must be last - VTERM_N_KEYS = VTERM_KEY_MAX + VTERM_KEY_MAX, // Must be last + VTERM_N_KEYS = VTERM_KEY_MAX, } VTermKey; -#define VTERM_KEY_FUNCTION(n) (VTERM_KEY_FUNCTION_0+(n)) - -#endif +#define VTERM_KEY_FUNCTION(n) (VTERM_KEY_FUNCTION_0 + (n)) diff --git a/src/nvim/vvars.lua b/src/nvim/vvars.lua index e705c02e83..056e281c0b 100644 --- a/src/nvim/vvars.lua +++ b/src/nvim/vvars.lua @@ -220,7 +220,8 @@ M.vars = { type = 'string', desc = [=[ The value of the exception most recently caught and not - finished. See also |v:throwpoint| and |throw-variables|. + finished. See also |v:stacktrace|, |v:throwpoint|, and + |throw-variables|. Example: >vim try throw "oops" @@ -701,6 +702,15 @@ M.vars = { < ]=], }, + stacktrace = { + type = 'table[]', + desc = [=[ + The stack trace of the exception most recently caught and + not finished. Refer to |getstacktrace()| for the structure of + stack trace. See also |v:exception|, |v:throwpoint|, and + |throw-variables|. + ]=], + }, statusmsg = { type = 'string', desc = [=[ @@ -823,7 +833,7 @@ M.vars = { desc = [=[ The point where the exception most recently caught and not finished was thrown. Not set when commands are typed. See - also |v:exception| and |throw-variables|. + also |v:exception|, |v:stacktrace|, and |throw-variables|. Example: >vim try throw "oops" diff --git a/src/nvim/window.c b/src/nvim/window.c index 8232d0aa6c..8c8df72590 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -1462,7 +1462,7 @@ win_T *win_split_ins(int size, int flags, win_T *new_wp, int dir, frame_T *to_fl frame_add_statusline(curfrp); } } - frame_new_height(curfrp, new_fr_height, flags & WSP_TOP, false); + frame_new_height(curfrp, new_fr_height, flags & WSP_TOP, false, false); } else { win_new_height(oldwin, oldwin_height - (new_size + STATUS_HEIGHT)); } @@ -2136,7 +2136,7 @@ static void win_equal_rec(win_T *next_curwin, bool current, frame_T *topfr, int || topfr->fr_width != width || topfr->fr_win->w_wincol != col) { topfr->fr_win->w_winrow = row; - frame_new_height(topfr, height, false, false); + frame_new_height(topfr, height, false, false, false); topfr->fr_win->w_wincol = col; frame_new_width(topfr, width, false, false); redraw_all_later(UPD_NOT_VALID); @@ -3154,7 +3154,7 @@ win_T *winframe_remove(win_T *win, int *dirp, tabpage_T *tp, frame_T **unflat_al if (*dirp == 'v') { frame_new_height(altfr, altfr->fr_height + frp_close->fr_height, - altfr == frp_close->fr_next, false); + altfr == frp_close->fr_next, false, false); } else { assert(*dirp == 'h'); frame_new_width(altfr, altfr->fr_width + frp_close->fr_width, @@ -3356,7 +3356,7 @@ void winframe_restore(win_T *wp, int dir, frame_T *unflat_altfr) // adjusts window sizes to fit restored statuslines/separators, if needed. if (dir == 'v') { frame_new_height(unflat_altfr, unflat_altfr->fr_height - frp->fr_height, - unflat_altfr == frp->fr_next, false); + unflat_altfr == frp->fr_next, false, false); } else if (dir == 'h') { frame_new_width(unflat_altfr, unflat_altfr->fr_width - frp->fr_width, unflat_altfr == frp->fr_next, false); @@ -3499,15 +3499,31 @@ static bool is_bottom_win(win_T *wp) } return true; } + +// 'cmdheight' value explicitly set by the user: window commands are allowed to +// resize the topframe to values higher than this minimum, but not lower. +static OptInt min_set_ch = 1; + /// Set a new height for a frame. Recursively sets the height for contained /// frames and windows. Caller must take care of positions. /// /// @param topfirst resize topmost contained frame first. /// @param wfh obey 'winfixheight' when there is a choice; /// may cause the height not to be set. -void frame_new_height(frame_T *topfrp, int height, bool topfirst, bool wfh) +/// @param set_ch set 'cmdheight' to resize topframe. +void frame_new_height(frame_T *topfrp, int height, bool topfirst, bool wfh, bool set_ch) FUNC_ATTR_NONNULL_ALL { + if (topfrp->fr_parent == NULL && set_ch) { + // topframe: update the command line height, with side effects. + OptInt new_ch = MAX(min_set_ch, p_ch + topfrp->fr_height - height); + if (new_ch != p_ch) { + const OptInt save_ch = min_set_ch; + set_option_value(kOptCmdheight, NUMBER_OPTVAL(new_ch), 0); + min_set_ch = save_ch; + } + height = (int)MIN(ROWS_AVAIL, height); + } if (topfrp->fr_win != NULL) { // Simple case: just one window. win_T *wp = topfrp->fr_win; @@ -3520,7 +3536,7 @@ void frame_new_height(frame_T *topfrp, int height, bool topfirst, bool wfh) do { // All frames in this row get the same new height. FOR_ALL_FRAMES(frp, topfrp->fr_child) { - frame_new_height(frp, height, topfirst, wfh); + frame_new_height(frp, height, topfirst, wfh, set_ch); if (frp->fr_height > height) { // Could not fit the windows, make the whole row higher. height = frp->fr_height; @@ -3562,10 +3578,9 @@ void frame_new_height(frame_T *topfrp, int height, bool topfirst, bool wfh) int h = frame_minheight(frp, NULL); if (frp->fr_height + extra_lines < h) { extra_lines += frp->fr_height - h; - frame_new_height(frp, h, topfirst, wfh); + frame_new_height(frp, h, topfirst, wfh, set_ch); } else { - frame_new_height(frp, frp->fr_height + extra_lines, - topfirst, wfh); + frame_new_height(frp, frp->fr_height + extra_lines, topfirst, wfh, set_ch); break; } if (topfirst) { @@ -3584,7 +3599,7 @@ void frame_new_height(frame_T *topfrp, int height, bool topfirst, bool wfh) } } else if (extra_lines > 0) { // increase height of bottom or top frame - frame_new_height(frp, frp->fr_height + extra_lines, topfirst, wfh); + frame_new_height(frp, frp->fr_height + extra_lines, topfirst, wfh, set_ch); } } topfrp->fr_height = height; @@ -4001,6 +4016,10 @@ void unuse_tabpage(tabpage_T *tp) tp->tp_curwin = curwin; } +// When switching tabpage, handle other side-effects in command_height(), but +// avoid setting frame sizes which are still correct. +static bool command_frame_height = true; + /// Set the relevant pointers to use tab page "tp". May want to call /// unuse_tabpage() first. void use_tabpage(tabpage_T *tp) @@ -4402,6 +4421,16 @@ static void enter_tabpage(tabpage_T *tp, buf_T *old_curbuf, bool trigger_enter_a use_tabpage(tp); + if (p_ch != curtab->tp_ch_used) { + // Use the stored value of p_ch, so that it can be different for each tab page. + // Handle other side-effects but avoid setting frame sizes, which are still correct. + OptInt new_ch = curtab->tp_ch_used; + curtab->tp_ch_used = p_ch; + command_frame_height = false; + set_option_value(kOptCmdheight, NUMBER_OPTVAL(new_ch), 0); + command_frame_height = true; + } + if (old_curtab != curtab) { tabpage_check_windows(old_curtab); } @@ -4415,27 +4444,9 @@ static void enter_tabpage(tabpage_T *tp, buf_T *old_curbuf, bool trigger_enter_a prevwin = next_prevwin; last_status(false); // status line may appear or disappear - const int row = win_comp_pos(); // recompute w_winrow for all windows + win_comp_pos(); // recompute w_winrow for all windows diff_need_scrollbind = true; - // Use the stored value of p_ch, so that it can be different for each tab page. - if (p_ch != curtab->tp_ch_used) { - clear_cmdline = true; - if (msg_grid.chars && p_ch < curtab->tp_ch_used) { - // TODO(bfredl): a bit expensive, should be enough to invalidate the - // region between the old and the new p_ch. - grid_invalidate(&msg_grid); - } - } - p_ch = curtab->tp_ch_used; - - // When cmdheight is changed in a tab page with '<C-w>-', cmdline_row is - // changed but p_ch and tp_ch_used are not changed. Thus we also need to - // check cmdline_row. - if (row < cmdline_row && cmdline_row <= Rows - p_ch) { - clear_cmdline = true; - } - // If there was a click in a window, it won't be usable for a following // drag. reset_dragwin(); @@ -5407,9 +5418,9 @@ void win_new_screen_rows(void) // First try setting the heights of windows with 'winfixheight'. If // that doesn't result in the right height, forget about that option. - frame_new_height(topframe, h, false, true); + frame_new_height(topframe, h, false, true, false); if (!frame_check_height(topframe, h)) { - frame_new_height(topframe, h, false, false); + frame_new_height(topframe, h, false, false, false); } win_comp_pos(); // recompute w_winrow and w_wincol @@ -5844,22 +5855,7 @@ void win_setheight_win(int height, win_T *win) frame_setheight(win->w_frame, height + win->w_hsep_height + win->w_status_height); // recompute the window positions - int row = win_comp_pos(); - - // If there is extra space created between the last window and the command - // line, clear it. - if (full_screen && msg_scrolled == 0 && row < cmdline_row) { - grid_clear(&default_grid, row, cmdline_row, 0, Columns, 0); - if (msg_grid.chars) { - clear_cmdline = true; - } - } - cmdline_row = row; - p_ch = MAX(Rows - cmdline_row, 0); - curtab->tp_ch_used = p_ch; - msg_row = row; - msg_col = 0; - + win_comp_pos(); win_fix_scroll(true); redraw_all_later(UPD_NOT_VALID); @@ -5887,14 +5883,8 @@ static void frame_setheight(frame_T *curfrp, int height) if (curfrp->fr_parent == NULL) { // topframe: can only change the command line height - if (height > ROWS_AVAIL) { - // If height is greater than the available space, try to create space for - // the frame by reducing 'cmdheight' if possible, while making sure - // `cmdheight` doesn't go below 1 if it wasn't set to 0 explicitly. - height = (int)MIN(ROWS_AVAIL + p_ch - !p_ch_was_zero, height); - } if (height > 0) { - frame_new_height(curfrp, height, false, false); + frame_new_height(curfrp, height, false, false, true); } } else if (curfrp->fr_parent->fr_layout == FR_ROW) { // Row of frames: Also need to resize frames left and right of this @@ -5973,7 +5963,7 @@ static void frame_setheight(frame_T *curfrp, int height) } // set the current frame to the new height - frame_new_height(curfrp, height, false, false); + frame_new_height(curfrp, height, false, false, true); // First take lines from the frames after the current frame. If // that is not enough, takes lines from frames above the current @@ -5995,15 +5985,15 @@ static void frame_setheight(frame_T *curfrp, int height) room_reserved = frp->fr_height - take; } take -= frp->fr_height - room_reserved; - frame_new_height(frp, room_reserved, false, false); + frame_new_height(frp, room_reserved, false, false, true); room_reserved = 0; } } else { if (frp->fr_height - take < h) { take -= frp->fr_height - h; - frame_new_height(frp, h, false, false); + frame_new_height(frp, h, false, false, true); } else { - frame_new_height(frp, frp->fr_height - take, false, false); + frame_new_height(frp, frp->fr_height - take, false, false, true); take = 0; } } @@ -6261,7 +6251,7 @@ void win_drag_status_line(win_T *dragwin, int offset) room = Rows - cmdline_row; if (curfr->fr_next != NULL) { room -= (int)p_ch + global_stl_height(); - } else if (!p_ch_was_zero) { + } else if (min_set_ch > 0) { room--; } room = MAX(room, 0); @@ -6281,7 +6271,7 @@ void win_drag_status_line(win_T *dragwin, int offset) // Grow frame fr by "offset" lines. // Doesn't happen when dragging the last status line up. if (fr != NULL) { - frame_new_height(fr, fr->fr_height + offset, up, false); + frame_new_height(fr, fr->fr_height + offset, up, false, true); } if (up) { @@ -6294,9 +6284,9 @@ void win_drag_status_line(win_T *dragwin, int offset) int n = frame_minheight(fr, NULL); if (fr->fr_height - offset <= n) { offset -= fr->fr_height - n; - frame_new_height(fr, n, !up, false); + frame_new_height(fr, n, !up, false, true); } else { - frame_new_height(fr, fr->fr_height - offset, !up, false); + frame_new_height(fr, fr->fr_height - offset, !up, false, true); break; } if (up) { @@ -6305,15 +6295,7 @@ void win_drag_status_line(win_T *dragwin, int offset) fr = fr->fr_next; } } - int row = win_comp_pos(); - grid_clear(&default_grid, row, cmdline_row, 0, Columns, 0); - if (msg_grid.chars) { - clear_cmdline = true; - } - cmdline_row = row; - p_ch = MAX(Rows - cmdline_row, p_ch_was_zero ? 0 : 1); - curtab->tp_ch_used = p_ch; - + win_comp_pos(); win_fix_scroll(true); redraw_all_later(UPD_SOME_VALID); @@ -6750,21 +6732,6 @@ void command_height(void) { int old_p_ch = (int)curtab->tp_ch_used; - // Use the value of p_ch that we remembered. This is needed for when the - // GUI starts up, we can't be sure in what order things happen. And when - // p_ch was changed in another tab page. - curtab->tp_ch_used = p_ch; - - // Update cmdline_row to what it should be: just below the last window. - cmdline_row = topframe->fr_height + tabline_height() + global_stl_height(); - - // If cmdline_row is smaller than what it is supposed to be for 'cmdheight' - // then set old_p_ch to what it would be, so that the windows get resized - // properly for the new value. - if (cmdline_row < Rows - p_ch) { - old_p_ch = Rows - cmdline_row; - } - // Find bottom frame with width of screen. frame_T *frp = lastwin_nofloating()->w_frame; while (frp->fr_width != Columns && frp->fr_parent != NULL) { @@ -6772,60 +6739,53 @@ void command_height(void) } // Avoid changing the height of a window with 'winfixheight' set. - while (frp->fr_prev != NULL && frp->fr_layout == FR_LEAF - && frp->fr_win->w_p_wfh) { + while (frp->fr_prev != NULL && frp->fr_layout == FR_LEAF && frp->fr_win->w_p_wfh) { frp = frp->fr_prev; } - if (starting != NO_SCREEN) { - cmdline_row = Rows - (int)p_ch; - - if (p_ch > old_p_ch) { // p_ch got bigger - while (p_ch > old_p_ch) { - if (frp == NULL) { - emsg(_(e_noroom)); - p_ch = old_p_ch; - curtab->tp_ch_used = p_ch; - cmdline_row = Rows - (int)p_ch; - break; - } - int h = frp->fr_height - frame_minheight(frp, NULL); - h = MIN(h, (int)p_ch - old_p_ch); - old_p_ch += h; - frame_add_height(frp, -h); - frp = frp->fr_prev; - } - - // Recompute window positions. - win_comp_pos(); - - if (!need_wait_return) { - // clear the lines added to cmdline - if (full_screen) { - grid_clear(&default_grid, cmdline_row, Rows, 0, Columns, 0); - } - msg_row = cmdline_row; - } - redraw_cmdline = true; - return; + while (p_ch > old_p_ch && command_frame_height) { + if (frp == NULL) { + emsg(_(e_noroom)); + p_ch = old_p_ch; + break; } - - msg_row = MAX(msg_row, cmdline_row); - redraw_cmdline = true; + int h = MIN((int)(p_ch - old_p_ch), frp->fr_height - frame_minheight(frp, NULL)); + frame_add_height(frp, -h); + old_p_ch += h; + frp = frp->fr_prev; + } + if (p_ch < old_p_ch && command_frame_height && frp != NULL) { + frame_add_height(frp, (int)(old_p_ch - p_ch)); } - frame_add_height(frp, (int)(old_p_ch - p_ch)); // Recompute window positions. - if (frp != lastwin->w_frame) { - win_comp_pos(); + win_comp_pos(); + cmdline_row = Rows - (int)p_ch; + redraw_cmdline = true; + + // Clear the cmdheight area. + if (msg_scrolled == 0 && full_screen) { + ScreenGrid *grid = &default_grid; + if (!ui_has(kUIMessages)) { + msg_grid_validate(); + grid = &msg_grid_adj; + } + grid_clear(grid, cmdline_row, Rows, 0, Columns, 0); + msg_row = cmdline_row; } + + // Use the value of p_ch that we remembered. This is needed for when the + // GUI starts up, we can't be sure in what order things happen. And when + // p_ch was changed in another tab page. + curtab->tp_ch_used = p_ch; + min_set_ch = p_ch; } // Resize frame "frp" to be "n" lines higher (negative for less high). // Also resize the frames it is contained in. static void frame_add_height(frame_T *frp, int n) { - frame_new_height(frp, frp->fr_height + n, false, false); + frame_new_height(frp, frp->fr_height + n, false, false, false); while (true) { frp = frp->fr_parent; if (frp == NULL) { @@ -6895,7 +6855,7 @@ static bool resize_frame_for_status(frame_T *fr) emsg(_(e_noroom)); return false; } else if (fp != fr) { - frame_new_height(fp, fp->fr_height - 1, false, false); + frame_new_height(fp, fp->fr_height - 1, false, false, false); frame_fix_height(wp); win_comp_pos(); } else { @@ -6916,7 +6876,7 @@ static bool resize_frame_for_winbar(frame_T *fr) emsg(_(e_noroom)); return false; } - frame_new_height(fp, fp->fr_height - 1, false, false); + frame_new_height(fp, fp->fr_height - 1, false, false, false); win_new_height(wp, wp->w_height + 1); frame_fix_height(wp); win_comp_pos(); @@ -7305,7 +7265,7 @@ static win_T *restore_snapshot_rec(frame_T *sn, frame_T *fr) fr->fr_height = sn->fr_height; fr->fr_width = sn->fr_width; if (fr->fr_layout == FR_LEAF) { - frame_new_height(fr, fr->fr_height, false, false); + frame_new_height(fr, fr->fr_height, false, false, false); frame_new_width(fr, fr->fr_width, false, false); wp = sn->fr_win; } diff --git a/src/nvim/window.h b/src/nvim/window.h index 9618ff1c2a..b5808d3451 100644 --- a/src/nvim/window.h +++ b/src/nvim/window.h @@ -35,9 +35,6 @@ enum { EXTERN int tabpage_move_disallowed INIT( = 0); ///< moving tabpages around disallowed -/// Set to true if 'cmdheight' was explicitly set to 0. -EXTERN bool p_ch_was_zero INIT( = false); - #ifdef INCLUDE_GENERATED_DECLARATIONS # include "window.h.generated.h" #endif diff --git a/src/vterm/encoding.c b/src/vterm/encoding.c deleted file mode 100644 index 434ac3f99b..0000000000 --- a/src/vterm/encoding.c +++ /dev/null @@ -1,230 +0,0 @@ -#include "vterm_internal.h" - -#define UNICODE_INVALID 0xFFFD - -#if defined(DEBUG) && DEBUG > 1 -# define DEBUG_PRINT_UTF8 -#endif - -struct UTF8DecoderData { - // number of bytes remaining in this codepoint - int bytes_remaining; - - // number of bytes total in this codepoint once it's finished - // (for detecting overlongs) - int bytes_total; - - int this_cp; -}; - -static void init_utf8(VTermEncoding *enc, void *data_) -{ - struct UTF8DecoderData *data = data_; - - data->bytes_remaining = 0; - data->bytes_total = 0; -} - -static void decode_utf8(VTermEncoding *enc, void *data_, - uint32_t cp[], int *cpi, int cplen, - const char bytes[], size_t *pos, size_t bytelen) -{ - struct UTF8DecoderData *data = data_; - -#ifdef DEBUG_PRINT_UTF8 - printf("BEGIN UTF-8\n"); -#endif - - for(; *pos < bytelen && *cpi < cplen; (*pos)++) { - unsigned char c = bytes[*pos]; - -#ifdef DEBUG_PRINT_UTF8 - printf(" pos=%zd c=%02x rem=%d\n", *pos, c, data->bytes_remaining); -#endif - - if(c < 0x20) // C0 - return; - - else if(c >= 0x20 && c < 0x7f) { - if(data->bytes_remaining) - cp[(*cpi)++] = UNICODE_INVALID; - - cp[(*cpi)++] = c; -#ifdef DEBUG_PRINT_UTF8 - printf(" UTF-8 char: U+%04x\n", c); -#endif - data->bytes_remaining = 0; - } - - else if(c == 0x7f) // DEL - return; - - else if(c >= 0x80 && c < 0xc0) { - if(!data->bytes_remaining) { - cp[(*cpi)++] = UNICODE_INVALID; - continue; - } - - data->this_cp <<= 6; - data->this_cp |= c & 0x3f; - data->bytes_remaining--; - - if(!data->bytes_remaining) { -#ifdef DEBUG_PRINT_UTF8 - printf(" UTF-8 raw char U+%04x bytelen=%d ", data->this_cp, data->bytes_total); -#endif - // Check for overlong sequences - switch(data->bytes_total) { - case 2: - if(data->this_cp < 0x0080) data->this_cp = UNICODE_INVALID; - break; - case 3: - if(data->this_cp < 0x0800) data->this_cp = UNICODE_INVALID; - break; - case 4: - if(data->this_cp < 0x10000) data->this_cp = UNICODE_INVALID; - break; - case 5: - if(data->this_cp < 0x200000) data->this_cp = UNICODE_INVALID; - break; - case 6: - if(data->this_cp < 0x4000000) data->this_cp = UNICODE_INVALID; - break; - } - // Now look for plain invalid ones - if((data->this_cp >= 0xD800 && data->this_cp <= 0xDFFF) || - data->this_cp == 0xFFFE || - data->this_cp == 0xFFFF) - data->this_cp = UNICODE_INVALID; -#ifdef DEBUG_PRINT_UTF8 - printf(" char: U+%04x\n", data->this_cp); -#endif - cp[(*cpi)++] = data->this_cp; - } - } - - else if(c >= 0xc0 && c < 0xe0) { - if(data->bytes_remaining) - cp[(*cpi)++] = UNICODE_INVALID; - - data->this_cp = c & 0x1f; - data->bytes_total = 2; - data->bytes_remaining = 1; - } - - else if(c >= 0xe0 && c < 0xf0) { - if(data->bytes_remaining) - cp[(*cpi)++] = UNICODE_INVALID; - - data->this_cp = c & 0x0f; - data->bytes_total = 3; - data->bytes_remaining = 2; - } - - else if(c >= 0xf0 && c < 0xf8) { - if(data->bytes_remaining) - cp[(*cpi)++] = UNICODE_INVALID; - - data->this_cp = c & 0x07; - data->bytes_total = 4; - data->bytes_remaining = 3; - } - - else if(c >= 0xf8 && c < 0xfc) { - if(data->bytes_remaining) - cp[(*cpi)++] = UNICODE_INVALID; - - data->this_cp = c & 0x03; - data->bytes_total = 5; - data->bytes_remaining = 4; - } - - else if(c >= 0xfc && c < 0xfe) { - if(data->bytes_remaining) - cp[(*cpi)++] = UNICODE_INVALID; - - data->this_cp = c & 0x01; - data->bytes_total = 6; - data->bytes_remaining = 5; - } - - else { - cp[(*cpi)++] = UNICODE_INVALID; - } - } -} - -static VTermEncoding encoding_utf8 = { - .init = &init_utf8, - .decode = &decode_utf8, -}; - -static void decode_usascii(VTermEncoding *enc, void *data, - uint32_t cp[], int *cpi, int cplen, - const char bytes[], size_t *pos, size_t bytelen) -{ - int is_gr = bytes[*pos] & 0x80; - - for(; *pos < bytelen && *cpi < cplen; (*pos)++) { - unsigned char c = bytes[*pos] ^ is_gr; - - if(c < 0x20 || c == 0x7f || c >= 0x80) - return; - - cp[(*cpi)++] = c; - } -} - -static VTermEncoding encoding_usascii = { - .decode = &decode_usascii, -}; - -struct StaticTableEncoding { - const VTermEncoding enc; - const uint32_t chars[128]; -}; - -static void decode_table(VTermEncoding *enc, void *data, - uint32_t cp[], int *cpi, int cplen, - const char bytes[], size_t *pos, size_t bytelen) -{ - struct StaticTableEncoding *table = (struct StaticTableEncoding *)enc; - int is_gr = bytes[*pos] & 0x80; - - for(; *pos < bytelen && *cpi < cplen; (*pos)++) { - unsigned char c = bytes[*pos] ^ is_gr; - - if(c < 0x20 || c == 0x7f || c >= 0x80) - return; - - if(table->chars[c]) - cp[(*cpi)++] = table->chars[c]; - else - cp[(*cpi)++] = c; - } -} - -#include "encoding/DECdrawing.inc" -#include "encoding/uk.inc" - -static struct { - VTermEncodingType type; - char designation; - VTermEncoding *enc; -} -encodings[] = { - { ENC_UTF8, 'u', &encoding_utf8 }, - { ENC_SINGLE_94, '0', (VTermEncoding*)&encoding_DECdrawing }, - { ENC_SINGLE_94, 'A', (VTermEncoding*)&encoding_uk }, - { ENC_SINGLE_94, 'B', &encoding_usascii }, - { 0 }, -}; - -/* This ought to be INTERNAL but isn't because it's used by unit testing */ -VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation) -{ - for(int i = 0; encodings[i].designation; i++) - if(encodings[i].type == type && encodings[i].designation == designation) - return encodings[i].enc; - return NULL; -} diff --git a/src/vterm/encoding/DECdrawing.inc b/src/vterm/encoding/DECdrawing.inc deleted file mode 100644 index 627397bcc2..0000000000 --- a/src/vterm/encoding/DECdrawing.inc +++ /dev/null @@ -1,36 +0,0 @@ -static const struct StaticTableEncoding encoding_DECdrawing = { - { .decode = &decode_table }, - { - [0x60] = 0x25C6, // BLACK DIAMOND - [0x61] = 0x2592, // MEDIUM SHADE (checkerboard) - [0x62] = 0x2409, // SYMBOL FOR HORIZONTAL TAB - [0x63] = 0x240C, // SYMBOL FOR FORM FEED - [0x64] = 0x240D, // SYMBOL FOR CARRIAGE RETURN - [0x65] = 0x240A, // SYMBOL FOR LINE FEED - [0x66] = 0x00B0, // DEGREE SIGN - [0x67] = 0x00B1, // PLUS-MINUS SIGN (plus or minus) - [0x68] = 0x2424, // SYMBOL FOR NEW LINE - [0x69] = 0x240B, // SYMBOL FOR VERTICAL TAB - [0x6a] = 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT (bottom-right corner) - [0x6b] = 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT (top-right corner) - [0x6c] = 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT (top-left corner) - [0x6d] = 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT (bottom-left corner) - [0x6e] = 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL (crossing lines) - [0x6f] = 0x23BA, // HORIZONTAL SCAN LINE-1 - [0x70] = 0x23BB, // HORIZONTAL SCAN LINE-3 - [0x71] = 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL - [0x72] = 0x23BC, // HORIZONTAL SCAN LINE-7 - [0x73] = 0x23BD, // HORIZONTAL SCAN LINE-9 - [0x74] = 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT - [0x75] = 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT - [0x76] = 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL - [0x77] = 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL - [0x78] = 0x2502, // BOX DRAWINGS LIGHT VERTICAL - [0x79] = 0x2A7D, // LESS-THAN OR SLANTED EQUAL-TO - [0x7a] = 0x2A7E, // GREATER-THAN OR SLANTED EQUAL-TO - [0x7b] = 0x03C0, // GREEK SMALL LETTER PI - [0x7c] = 0x2260, // NOT EQUAL TO - [0x7d] = 0x00A3, // POUND SIGN - [0x7e] = 0x00B7, // MIDDLE DOT - } -}; diff --git a/src/vterm/encoding/uk.inc b/src/vterm/encoding/uk.inc deleted file mode 100644 index 5c7700226b..0000000000 --- a/src/vterm/encoding/uk.inc +++ /dev/null @@ -1,6 +0,0 @@ -static const struct StaticTableEncoding encoding_uk = { - { .decode = &decode_table }, - { - [0x23] = 0x00a3, // £ - } -}; diff --git a/src/vterm/keyboard.c b/src/vterm/keyboard.c deleted file mode 100644 index 7e062c8c02..0000000000 --- a/src/vterm/keyboard.c +++ /dev/null @@ -1,225 +0,0 @@ -#include "vterm_internal.h" -#include <stdio.h> - -#include "nvim/tui/termkey/termkey.h" - -void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod) -{ - /* The shift modifier is never important for Unicode characters - * apart from Space - */ - if(c != ' ') - mod &= ~VTERM_MOD_SHIFT; - - if(mod == 0) { - // Normal text - ignore just shift - char str[6]; - int seqlen = fill_utf8(c, str); - vterm_push_output_bytes(vt, str, seqlen); - return; - } - - int needs_CSIu; - switch(c) { - /* Special Ctrl- letters that can't be represented elsewise */ - case 'i': case 'j': case 'm': case '[': - needs_CSIu = 1; - break; - /* Ctrl-\ ] ^ _ don't need CSUu */ - case '\\': case ']': case '^': case '_': - needs_CSIu = 0; - break; - /* Shift-space needs CSIu */ - case ' ': - needs_CSIu = !!(mod & VTERM_MOD_SHIFT); - break; - /* All other characters needs CSIu except for letters a-z */ - default: - needs_CSIu = (c < 'a' || c > 'z'); - } - - /* ALT we can just prefix with ESC; anything else requires CSI u */ - if(needs_CSIu && (mod & ~VTERM_MOD_ALT)) { - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", c, mod+1); - return; - } - - if(mod & VTERM_MOD_CTRL) - c &= 0x1f; - - vterm_push_output_sprintf(vt, "%s%c", mod & VTERM_MOD_ALT ? ESC_S : "", c); -} - -typedef struct { - enum { - KEYCODE_NONE, - KEYCODE_LITERAL, - KEYCODE_TAB, - KEYCODE_ENTER, - KEYCODE_SS3, - KEYCODE_CSI, - KEYCODE_CSI_CURSOR, - KEYCODE_CSINUM, - KEYCODE_KEYPAD, - } type; - char literal; - int csinum; -} keycodes_s; - -static keycodes_s keycodes[] = { - { KEYCODE_NONE }, // NONE - - { KEYCODE_ENTER, '\r' }, // ENTER - { KEYCODE_TAB, '\t' }, // TAB - { KEYCODE_LITERAL, '\x7f' }, // BACKSPACE == ASCII DEL - { KEYCODE_LITERAL, '\x1b' }, // ESCAPE - - { KEYCODE_CSI_CURSOR, 'A' }, // UP - { KEYCODE_CSI_CURSOR, 'B' }, // DOWN - { KEYCODE_CSI_CURSOR, 'D' }, // LEFT - { KEYCODE_CSI_CURSOR, 'C' }, // RIGHT - - { KEYCODE_CSINUM, '~', 2 }, // INS - { KEYCODE_CSINUM, '~', 3 }, // DEL - { KEYCODE_CSI_CURSOR, 'H' }, // HOME - { KEYCODE_CSI_CURSOR, 'F' }, // END - { KEYCODE_CSINUM, '~', 5 }, // PAGEUP - { KEYCODE_CSINUM, '~', 6 }, // PAGEDOWN -}; - -static keycodes_s keycodes_fn[] = { - { KEYCODE_NONE }, // F0 - shouldn't happen - { KEYCODE_SS3, 'P' }, // F1 - { KEYCODE_SS3, 'Q' }, // F2 - { KEYCODE_SS3, 'R' }, // F3 - { KEYCODE_SS3, 'S' }, // F4 - { KEYCODE_CSINUM, '~', 15 }, // F5 - { KEYCODE_CSINUM, '~', 17 }, // F6 - { KEYCODE_CSINUM, '~', 18 }, // F7 - { KEYCODE_CSINUM, '~', 19 }, // F8 - { KEYCODE_CSINUM, '~', 20 }, // F9 - { KEYCODE_CSINUM, '~', 21 }, // F10 - { KEYCODE_CSINUM, '~', 23 }, // F11 - { KEYCODE_CSINUM, '~', 24 }, // F12 -}; - -static keycodes_s keycodes_kp[] = { - { KEYCODE_KEYPAD, '0', 'p' }, // KP_0 - { KEYCODE_KEYPAD, '1', 'q' }, // KP_1 - { KEYCODE_KEYPAD, '2', 'r' }, // KP_2 - { KEYCODE_KEYPAD, '3', 's' }, // KP_3 - { KEYCODE_KEYPAD, '4', 't' }, // KP_4 - { KEYCODE_KEYPAD, '5', 'u' }, // KP_5 - { KEYCODE_KEYPAD, '6', 'v' }, // KP_6 - { KEYCODE_KEYPAD, '7', 'w' }, // KP_7 - { KEYCODE_KEYPAD, '8', 'x' }, // KP_8 - { KEYCODE_KEYPAD, '9', 'y' }, // KP_9 - { KEYCODE_KEYPAD, '*', 'j' }, // KP_MULT - { KEYCODE_KEYPAD, '+', 'k' }, // KP_PLUS - { KEYCODE_KEYPAD, ',', 'l' }, // KP_COMMA - { KEYCODE_KEYPAD, '-', 'm' }, // KP_MINUS - { KEYCODE_KEYPAD, '.', 'n' }, // KP_PERIOD - { KEYCODE_KEYPAD, '/', 'o' }, // KP_DIVIDE - { KEYCODE_KEYPAD, '\n', 'M' }, // KP_ENTER - { KEYCODE_KEYPAD, '=', 'X' }, // KP_EQUAL -}; - -void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod) -{ - if(key == VTERM_KEY_NONE) - return; - - keycodes_s k; - if(key < VTERM_KEY_FUNCTION_0) { - if(key >= sizeof(keycodes)/sizeof(keycodes[0])) - return; - k = keycodes[key]; - } - else if(key >= VTERM_KEY_FUNCTION_0 && key <= VTERM_KEY_FUNCTION_MAX) { - if((key - VTERM_KEY_FUNCTION_0) >= sizeof(keycodes_fn)/sizeof(keycodes_fn[0])) - return; - k = keycodes_fn[key - VTERM_KEY_FUNCTION_0]; - } - else if(key >= VTERM_KEY_KP_0) { - if((key - VTERM_KEY_KP_0) >= sizeof(keycodes_kp)/sizeof(keycodes_kp[0])) - return; - k = keycodes_kp[key - VTERM_KEY_KP_0]; - } - - switch(k.type) { - case KEYCODE_NONE: - break; - - case KEYCODE_TAB: - /* Shift-Tab is CSI Z but plain Tab is 0x09 */ - if(mod == VTERM_MOD_SHIFT) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "Z"); - else if(mod & VTERM_MOD_SHIFT) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%dZ", mod+1); - else - goto case_LITERAL; - break; - - case KEYCODE_ENTER: - /* Enter is CRLF in newline mode, but just LF in linefeed */ - if(vt->state->mode.newline) - vterm_push_output_sprintf(vt, "\r\n"); - else - goto case_LITERAL; - break; - - case KEYCODE_LITERAL: case_LITERAL: - if(mod & (VTERM_MOD_SHIFT|VTERM_MOD_CTRL)) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", k.literal, mod+1); - else - vterm_push_output_sprintf(vt, mod & VTERM_MOD_ALT ? ESC_S "%c" : "%c", k.literal); - break; - - case KEYCODE_SS3: case_SS3: - if(mod == 0) - vterm_push_output_sprintf_ctrl(vt, C1_SS3, "%c", k.literal); - else - goto case_CSI; - break; - - case KEYCODE_CSI: case_CSI: - if(mod == 0) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%c", k.literal); - else - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%d%c", mod + 1, k.literal); - break; - - case KEYCODE_CSINUM: - if(mod == 0) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d%c", k.csinum, k.literal); - else - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%d%c", k.csinum, mod + 1, k.literal); - break; - - case KEYCODE_CSI_CURSOR: - if(vt->state->mode.cursor) - goto case_SS3; - else - goto case_CSI; - - case KEYCODE_KEYPAD: - if(vt->state->mode.keypad) { - k.literal = k.csinum; - goto case_SS3; - } - else - goto case_LITERAL; - } -} - -void vterm_keyboard_start_paste(VTerm *vt) -{ - if(vt->state->mode.bracketpaste) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "200~"); -} - -void vterm_keyboard_end_paste(VTerm *vt) -{ - if(vt->state->mode.bracketpaste) - vterm_push_output_sprintf_ctrl(vt, C1_CSI, "201~"); -} diff --git a/src/vterm/mouse.c b/src/vterm/mouse.c deleted file mode 100644 index a9d3fe4ca9..0000000000 --- a/src/vterm/mouse.c +++ /dev/null @@ -1,99 +0,0 @@ -#include "vterm_internal.h" - -#include "nvim/tui/termkey/termkey.h" - -static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row) -{ - modifiers <<= 2; - - switch(state->mouse_protocol) { - case MOUSE_X10: - if(col + 0x21 > 0xff) - col = 0xff - 0x21; - if(row + 0x21 > 0xff) - row = 0xff - 0x21; - - if(!pressed) - code = 3; - - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c", - (code | modifiers) + 0x20, col + 0x21, row + 0x21); - break; - - case MOUSE_UTF8: - { - char utf8[18]; size_t len = 0; - - if(!pressed) - code = 3; - - len += fill_utf8((code | modifiers) + 0x20, utf8 + len); - len += fill_utf8(col + 0x21, utf8 + len); - len += fill_utf8(row + 0x21, utf8 + len); - utf8[len] = 0; - - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8); - } - break; - - case MOUSE_SGR: - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c", - code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm'); - break; - - case MOUSE_RXVT: - if(!pressed) - code = 3; - - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM", - code | modifiers, col + 1, row + 1); - break; - } -} - -void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod) -{ - VTermState *state = vt->state; - - if(col == state->mouse_col && row == state->mouse_row) - return; - - state->mouse_col = col; - state->mouse_row = row; - - if((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons) || - (state->mouse_flags & MOUSE_WANT_MOVE)) { - int button = state->mouse_buttons & 0x01 ? 1 : - state->mouse_buttons & 0x02 ? 2 : - state->mouse_buttons & 0x04 ? 3 : 4; - output_mouse(state, button-1 + 0x20, 1, mod, col, row); - } -} - -void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod) -{ - VTermState *state = vt->state; - - int old_buttons = state->mouse_buttons; - - if(button > 0 && button <= 3) { - if(pressed) - state->mouse_buttons |= (1 << (button-1)); - else - state->mouse_buttons &= ~(1 << (button-1)); - } - - /* Most of the time we don't get button releases from 4/5 */ - if(state->mouse_buttons == old_buttons && button < 4) - return; - - if(!state->mouse_flags) - return; - - if(button < 4) { - output_mouse(state, button-1, pressed, mod, state->mouse_col, state->mouse_row); - } - else if(button < 8) { - output_mouse(state, button-4 + 0x40, pressed, mod, state->mouse_col, state->mouse_row); - } -} diff --git a/src/vterm/parser.c b/src/vterm/parser.c deleted file mode 100644 index 84d017a791..0000000000 --- a/src/vterm/parser.c +++ /dev/null @@ -1,408 +0,0 @@ -#include "vterm_internal.h" - -#include <assert.h> -#include <stdio.h> -#include <string.h> - -#undef DEBUG_PARSER - -static bool is_intermed(unsigned char c) -{ - return c >= 0x20 && c <= 0x2f; -} - -static void do_control(VTerm *vt, unsigned char control) -{ - if(vt->parser.callbacks && vt->parser.callbacks->control) - if((*vt->parser.callbacks->control)(control, vt->parser.cbdata)) - return; - - DEBUG_LOG("libvterm: Unhandled control 0x%02x\n", control); -} - -static void do_csi(VTerm *vt, char command) -{ -#ifdef DEBUG_PARSER - printf("Parsed CSI args as:\n", arglen, args); - printf(" leader: %s\n", vt->parser.v.csi.leader); - for(int argi = 0; argi < vt->parser.v.csi.argi; argi++) { - printf(" %lu", CSI_ARG(vt->parser.v.csi.args[argi])); - if(!CSI_ARG_HAS_MORE(vt->parser.v.csi.args[argi])) - printf("\n"); - printf(" intermed: %s\n", vt->parser.intermed); - } -#endif - - if(vt->parser.callbacks && vt->parser.callbacks->csi) - if((*vt->parser.callbacks->csi)( - vt->parser.v.csi.leaderlen ? vt->parser.v.csi.leader : NULL, - vt->parser.v.csi.args, - vt->parser.v.csi.argi, - vt->parser.intermedlen ? vt->parser.intermed : NULL, - command, - vt->parser.cbdata)) - return; - - DEBUG_LOG("libvterm: Unhandled CSI %c\n", command); -} - -static void do_escape(VTerm *vt, char command) -{ - char seq[INTERMED_MAX+1]; - - size_t len = vt->parser.intermedlen; - strncpy(seq, vt->parser.intermed, len); - seq[len++] = command; - seq[len] = 0; - - if(vt->parser.callbacks && vt->parser.callbacks->escape) - if((*vt->parser.callbacks->escape)(seq, len, vt->parser.cbdata)) - return; - - DEBUG_LOG("libvterm: Unhandled escape ESC 0x%02x\n", command); -} - -static void string_fragment(VTerm *vt, const char *str, size_t len, bool final) -{ - VTermStringFragment frag = { - .str = str, - .len = len, - .initial = vt->parser.string_initial, - .final = final, - }; - - switch(vt->parser.state) { - case OSC: - if(vt->parser.callbacks && vt->parser.callbacks->osc) - (*vt->parser.callbacks->osc)(vt->parser.v.osc.command, frag, vt->parser.cbdata); - break; - - case DCS: - if(vt->parser.callbacks && vt->parser.callbacks->dcs) - (*vt->parser.callbacks->dcs)(vt->parser.v.dcs.command, vt->parser.v.dcs.commandlen, frag, vt->parser.cbdata); - break; - - case APC: - if(vt->parser.callbacks && vt->parser.callbacks->apc) - (*vt->parser.callbacks->apc)(frag, vt->parser.cbdata); - break; - - case PM: - if(vt->parser.callbacks && vt->parser.callbacks->pm) - (*vt->parser.callbacks->pm)(frag, vt->parser.cbdata); - break; - - case SOS: - if(vt->parser.callbacks && vt->parser.callbacks->sos) - (*vt->parser.callbacks->sos)(frag, vt->parser.cbdata); - break; - - case NORMAL: - case CSI_LEADER: - case CSI_ARGS: - case CSI_INTERMED: - case OSC_COMMAND: - case DCS_COMMAND: - break; - } - - vt->parser.string_initial = false; -} - -size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len) -{ - size_t pos = 0; - const char *string_start; - - switch(vt->parser.state) { - case NORMAL: - case CSI_LEADER: - case CSI_ARGS: - case CSI_INTERMED: - case OSC_COMMAND: - case DCS_COMMAND: - string_start = NULL; - break; - case OSC: - case DCS: - case APC: - case PM: - case SOS: - string_start = bytes; - break; - } - -#define ENTER_STATE(st) do { vt->parser.state = st; string_start = NULL; } while(0) -#define ENTER_NORMAL_STATE() ENTER_STATE(NORMAL) - -#define IS_STRING_STATE() (vt->parser.state >= OSC_COMMAND) - - for( ; pos < len; pos++) { - unsigned char c = bytes[pos]; - bool c1_allowed = !vt->mode.utf8; - - if(c == 0x00 || c == 0x7f) { // NUL, DEL - if(IS_STRING_STATE()) { - string_fragment(vt, string_start, bytes + pos - string_start, false); - string_start = bytes + pos + 1; - } - if(vt->parser.emit_nul) - do_control(vt, c); - continue; - } - if(c == 0x18 || c == 0x1a) { // CAN, SUB - vt->parser.in_esc = false; - ENTER_NORMAL_STATE(); - if(vt->parser.emit_nul) - do_control(vt, c); - continue; - } - else if(c == 0x1b) { // ESC - vt->parser.intermedlen = 0; - if(!IS_STRING_STATE()) - vt->parser.state = NORMAL; - vt->parser.in_esc = true; - continue; - } - else if(c == 0x07 && // BEL, can stand for ST in OSC or DCS state - IS_STRING_STATE()) { - // fallthrough - } - else if(c < 0x20) { // other C0 - if(vt->parser.state == SOS) - continue; // All other C0s permitted in SOS - - if(IS_STRING_STATE()) - string_fragment(vt, string_start, bytes + pos - string_start, false); - do_control(vt, c); - if(IS_STRING_STATE()) - string_start = bytes + pos + 1; - continue; - } - // else fallthrough - - size_t string_len = bytes + pos - string_start; - - if(vt->parser.in_esc) { - // Hoist an ESC letter into a C1 if we're not in a string mode - // Always accept ESC \ == ST even in string mode - if(!vt->parser.intermedlen && - c >= 0x40 && c < 0x60 && - ((!IS_STRING_STATE() || c == 0x5c))) { - c += 0x40; - c1_allowed = true; - if(string_len) { - assert(string_len > 0); - string_len -= 1; - } - vt->parser.in_esc = false; - } - else { - string_start = NULL; - vt->parser.state = NORMAL; - } - } - - switch(vt->parser.state) { - case CSI_LEADER: - /* Extract leader bytes 0x3c to 0x3f */ - if(c >= 0x3c && c <= 0x3f) { - if(vt->parser.v.csi.leaderlen < CSI_LEADER_MAX-1) - vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen++] = c; - break; - } - - /* else fallthrough */ - vt->parser.v.csi.leader[vt->parser.v.csi.leaderlen] = 0; - - vt->parser.v.csi.argi = 0; - vt->parser.v.csi.args[0] = CSI_ARG_MISSING; - vt->parser.state = CSI_ARGS; - - /* fallthrough */ - case CSI_ARGS: - /* Numerical value of argument */ - if(c >= '0' && c <= '9') { - if(vt->parser.v.csi.args[vt->parser.v.csi.argi] == CSI_ARG_MISSING) - vt->parser.v.csi.args[vt->parser.v.csi.argi] = 0; - vt->parser.v.csi.args[vt->parser.v.csi.argi] *= 10; - vt->parser.v.csi.args[vt->parser.v.csi.argi] += c - '0'; - break; - } - if(c == ':') { - vt->parser.v.csi.args[vt->parser.v.csi.argi] |= CSI_ARG_FLAG_MORE; - c = ';'; - } - if(c == ';') { - vt->parser.v.csi.argi++; - vt->parser.v.csi.args[vt->parser.v.csi.argi] = CSI_ARG_MISSING; - break; - } - - /* else fallthrough */ - vt->parser.v.csi.argi++; - vt->parser.intermedlen = 0; - vt->parser.state = CSI_INTERMED; - case CSI_INTERMED: - if(is_intermed(c)) { - if(vt->parser.intermedlen < INTERMED_MAX-1) - vt->parser.intermed[vt->parser.intermedlen++] = c; - break; - } - else if(c == 0x1b) { - /* ESC in CSI cancels */ - } - else if(c >= 0x40 && c <= 0x7e) { - vt->parser.intermed[vt->parser.intermedlen] = 0; - do_csi(vt, c); - } - /* else was invalid CSI */ - - ENTER_NORMAL_STATE(); - break; - - case OSC_COMMAND: - /* Numerical value of command */ - if(c >= '0' && c <= '9') { - if(vt->parser.v.osc.command == -1) - vt->parser.v.osc.command = 0; - else - vt->parser.v.osc.command *= 10; - vt->parser.v.osc.command += c - '0'; - break; - } - if(c == ';') { - vt->parser.state = OSC; - string_start = bytes + pos + 1; - break; - } - - /* else fallthrough */ - string_start = bytes + pos; - string_len = 0; - vt->parser.state = OSC; - goto string_state; - - case DCS_COMMAND: - if(vt->parser.v.dcs.commandlen < CSI_LEADER_MAX) - vt->parser.v.dcs.command[vt->parser.v.dcs.commandlen++] = c; - - if(c >= 0x40 && c<= 0x7e) { - string_start = bytes + pos + 1; - vt->parser.state = DCS; - } - break; - -string_state: - case OSC: - case DCS: - case APC: - case PM: - case SOS: - if(c == 0x07 || (c1_allowed && c == 0x9c)) { - string_fragment(vt, string_start, string_len, true); - ENTER_NORMAL_STATE(); - } - break; - - case NORMAL: - if(vt->parser.in_esc) { - if(is_intermed(c)) { - if(vt->parser.intermedlen < INTERMED_MAX-1) - vt->parser.intermed[vt->parser.intermedlen++] = c; - } - else if(c >= 0x30 && c < 0x7f) { - do_escape(vt, c); - vt->parser.in_esc = 0; - ENTER_NORMAL_STATE(); - } - else { - DEBUG_LOG("TODO: Unhandled byte %02x in Escape\n", c); - } - break; - } - if(c1_allowed && c >= 0x80 && c < 0xa0) { - switch(c) { - case 0x90: // DCS - vt->parser.string_initial = true; - vt->parser.v.dcs.commandlen = 0; - ENTER_STATE(DCS_COMMAND); - break; - case 0x98: // SOS - vt->parser.string_initial = true; - ENTER_STATE(SOS); - string_start = bytes + pos + 1; - string_len = 0; - break; - case 0x9b: // CSI - vt->parser.v.csi.leaderlen = 0; - ENTER_STATE(CSI_LEADER); - break; - case 0x9d: // OSC - vt->parser.v.osc.command = -1; - vt->parser.string_initial = true; - string_start = bytes + pos + 1; - ENTER_STATE(OSC_COMMAND); - break; - case 0x9e: // PM - vt->parser.string_initial = true; - ENTER_STATE(PM); - string_start = bytes + pos + 1; - string_len = 0; - break; - case 0x9f: // APC - vt->parser.string_initial = true; - ENTER_STATE(APC); - string_start = bytes + pos + 1; - string_len = 0; - break; - default: - do_control(vt, c); - break; - } - } - else { - size_t eaten = 0; - if(vt->parser.callbacks && vt->parser.callbacks->text) - eaten = (*vt->parser.callbacks->text)(bytes + pos, len - pos, vt->parser.cbdata); - - if(!eaten) { - DEBUG_LOG("libvterm: Text callback did not consume any input\n"); - /* force it to make progress */ - eaten = 1; - } - - pos += (eaten - 1); // we'll ++ it again in a moment - } - break; - } - } - - if(string_start) { - size_t string_len = bytes + pos - string_start; - if (string_len > 0) { - if(vt->parser.in_esc) { - string_len -= 1; - } - string_fragment(vt, string_start, string_len, false); - } - } - - return len; -} - -void vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user) -{ - vt->parser.callbacks = callbacks; - vt->parser.cbdata = user; -} - -void *vterm_parser_get_cbdata(VTerm *vt) -{ - return vt->parser.cbdata; -} - -void vterm_parser_set_emit_nul(VTerm *vt, bool emit) -{ - vt->parser.emit_nul = emit; -} diff --git a/src/vterm/pen.c b/src/vterm/pen.c deleted file mode 100644 index 1876eb9881..0000000000 --- a/src/vterm/pen.c +++ /dev/null @@ -1,678 +0,0 @@ -#include "vterm_internal.h" - -#include <stdio.h> - -/** - * Structure used to store RGB triples without the additional metadata stored in - * VTermColor. - */ -typedef struct { - uint8_t red, green, blue; -} VTermRGB; - -static const VTermRGB ansi_colors[] = { - /* R G B */ - { 0, 0, 0 }, // black - { 224, 0, 0 }, // red - { 0, 224, 0 }, // green - { 224, 224, 0 }, // yellow - { 0, 0, 224 }, // blue - { 224, 0, 224 }, // magenta - { 0, 224, 224 }, // cyan - { 224, 224, 224 }, // white == light grey - - // high intensity - { 128, 128, 128 }, // black - { 255, 64, 64 }, // red - { 64, 255, 64 }, // green - { 255, 255, 64 }, // yellow - { 64, 64, 255 }, // blue - { 255, 64, 255 }, // magenta - { 64, 255, 255 }, // cyan - { 255, 255, 255 }, // white for real -}; - -static int ramp6[] = { - 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF, -}; - -static int ramp24[] = { - 0x00, 0x0B, 0x16, 0x21, 0x2C, 0x37, 0x42, 0x4D, 0x58, 0x63, 0x6E, 0x79, - 0x85, 0x90, 0x9B, 0xA6, 0xB1, 0xBC, 0xC7, 0xD2, 0xDD, 0xE8, 0xF3, 0xFF, -}; - -static void lookup_default_colour_ansi(long idx, VTermColor *col) -{ - if (idx >= 0 && idx < 16) { - vterm_color_rgb( - col, - ansi_colors[idx].red, ansi_colors[idx].green, ansi_colors[idx].blue); - } -} - -static bool lookup_colour_ansi(const VTermState *state, long index, VTermColor *col) -{ - if(index >= 0 && index < 16) { - *col = state->colors[index]; - return true; - } - - return false; -} - -static bool lookup_colour_palette(const VTermState *state, long index, VTermColor *col) -{ - if(index >= 0 && index < 16) { - // Normal 8 colours or high intensity - parse as palette 0 - return lookup_colour_ansi(state, index, col); - } - else if(index >= 16 && index < 232) { - // 216-colour cube - index -= 16; - - vterm_color_rgb(col, ramp6[index/6/6 % 6], - ramp6[index/6 % 6], - ramp6[index % 6]); - - return true; - } - else if(index >= 232 && index < 256) { - // 24 greyscales - index -= 232; - - vterm_color_rgb(col, ramp24[index], ramp24[index], ramp24[index]); - - return true; - } - - return false; -} - -static int lookup_colour(const VTermState *state, int palette, const long args[], int argcount, VTermColor *col) -{ - switch(palette) { - case 2: // RGB mode - 3 args contain colour values directly - if(argcount < 3) - return argcount; - - vterm_color_rgb(col, CSI_ARG(args[0]), CSI_ARG(args[1]), CSI_ARG(args[2])); - - return 3; - - case 5: // XTerm 256-colour mode - if (!argcount || CSI_ARG_IS_MISSING(args[0])) { - return argcount ? 1 : 0; - } - - vterm_color_indexed(col, args[0]); - - return argcount ? 1 : 0; - - default: - DEBUG_LOG("Unrecognised colour palette %d\n", palette); - return 0; - } -} - -// Some conveniences - -static void setpenattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val) -{ -#ifdef DEBUG - if(type != vterm_get_attr_type(attr)) { - DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n", - attr, vterm_get_attr_type(attr), type); - return; - } -#endif - if(state->callbacks && state->callbacks->setpenattr) - (*state->callbacks->setpenattr)(attr, val, state->cbdata); -} - -static void setpenattr_bool(VTermState *state, VTermAttr attr, int boolean) -{ - VTermValue val = { .boolean = boolean }; - setpenattr(state, attr, VTERM_VALUETYPE_BOOL, &val); -} - -static void setpenattr_int(VTermState *state, VTermAttr attr, int number) -{ - VTermValue val = { .number = number }; - setpenattr(state, attr, VTERM_VALUETYPE_INT, &val); -} - -static void setpenattr_col(VTermState *state, VTermAttr attr, VTermColor color) -{ - VTermValue val = { .color = color }; - setpenattr(state, attr, VTERM_VALUETYPE_COLOR, &val); -} - -static void set_pen_col_ansi(VTermState *state, VTermAttr attr, long col) -{ - VTermColor *colp = (attr == VTERM_ATTR_BACKGROUND) ? &state->pen.bg : &state->pen.fg; - - vterm_color_indexed(colp, col); - - setpenattr_col(state, attr, *colp); -} - -INTERNAL void vterm_state_newpen(VTermState *state) -{ - // 90% grey so that pure white is brighter - vterm_color_rgb(&state->default_fg, 240, 240, 240); - vterm_color_rgb(&state->default_bg, 0, 0, 0); - vterm_state_set_default_colors(state, &state->default_fg, &state->default_bg); - - for(int col = 0; col < 16; col++) - lookup_default_colour_ansi(col, &state->colors[col]); -} - -INTERNAL void vterm_state_resetpen(VTermState *state) -{ - state->pen.bold = 0; setpenattr_bool(state, VTERM_ATTR_BOLD, 0); - state->pen.underline = 0; setpenattr_int (state, VTERM_ATTR_UNDERLINE, 0); - state->pen.italic = 0; setpenattr_bool(state, VTERM_ATTR_ITALIC, 0); - state->pen.blink = 0; setpenattr_bool(state, VTERM_ATTR_BLINK, 0); - state->pen.reverse = 0; setpenattr_bool(state, VTERM_ATTR_REVERSE, 0); - state->pen.conceal = 0; setpenattr_bool(state, VTERM_ATTR_CONCEAL, 0); - state->pen.strike = 0; setpenattr_bool(state, VTERM_ATTR_STRIKE, 0); - state->pen.font = 0; setpenattr_int (state, VTERM_ATTR_FONT, 0); - state->pen.small = 0; setpenattr_bool(state, VTERM_ATTR_SMALL, 0); - state->pen.baseline = 0; setpenattr_int (state, VTERM_ATTR_BASELINE, 0); - - state->pen.fg = state->default_fg; setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg); - state->pen.bg = state->default_bg; setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg); - - state->pen.uri = 0; setpenattr_int(state, VTERM_ATTR_URI, 0); -} - -INTERNAL void vterm_state_savepen(VTermState *state, int save) -{ - if(save) { - state->saved.pen = state->pen; - } - else { - state->pen = state->saved.pen; - - setpenattr_bool(state, VTERM_ATTR_BOLD, state->pen.bold); - setpenattr_int (state, VTERM_ATTR_UNDERLINE, state->pen.underline); - setpenattr_bool(state, VTERM_ATTR_ITALIC, state->pen.italic); - setpenattr_bool(state, VTERM_ATTR_BLINK, state->pen.blink); - setpenattr_bool(state, VTERM_ATTR_REVERSE, state->pen.reverse); - setpenattr_bool(state, VTERM_ATTR_CONCEAL, state->pen.conceal); - setpenattr_bool(state, VTERM_ATTR_STRIKE, state->pen.strike); - setpenattr_int (state, VTERM_ATTR_FONT, state->pen.font); - setpenattr_bool(state, VTERM_ATTR_SMALL, state->pen.small); - setpenattr_int (state, VTERM_ATTR_BASELINE, state->pen.baseline); - - setpenattr_col( state, VTERM_ATTR_FOREGROUND, state->pen.fg); - setpenattr_col( state, VTERM_ATTR_BACKGROUND, state->pen.bg); - - setpenattr_int( state, VTERM_ATTR_URI, state->pen.uri); - } -} - -int vterm_color_is_equal(const VTermColor *a, const VTermColor *b) -{ - /* First make sure that the two colours are of the same type (RGB/Indexed) */ - if (a->type != b->type) { - return false; - } - - /* Depending on the type inspect the corresponding members */ - if (VTERM_COLOR_IS_INDEXED(a)) { - return a->indexed.idx == b->indexed.idx; - } - else if (VTERM_COLOR_IS_RGB(a)) { - return (a->rgb.red == b->rgb.red) - && (a->rgb.green == b->rgb.green) - && (a->rgb.blue == b->rgb.blue); - } - - return 0; -} - -void vterm_state_get_default_colors(const VTermState *state, VTermColor *default_fg, VTermColor *default_bg) -{ - *default_fg = state->default_fg; - *default_bg = state->default_bg; -} - -void vterm_state_get_palette_color(const VTermState *state, int index, VTermColor *col) -{ - lookup_colour_palette(state, index, col); -} - -void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, const VTermColor *default_bg) -{ - if(default_fg) { - state->default_fg = *default_fg; - state->default_fg.type = (state->default_fg.type & ~VTERM_COLOR_DEFAULT_MASK) - | VTERM_COLOR_DEFAULT_FG; - } - - if(default_bg) { - state->default_bg = *default_bg; - state->default_bg.type = (state->default_bg.type & ~VTERM_COLOR_DEFAULT_MASK) - | VTERM_COLOR_DEFAULT_BG; - } -} - -void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col) -{ - if(index >= 0 && index < 16) - state->colors[index] = *col; -} - -void vterm_state_convert_color_to_rgb(const VTermState *state, VTermColor *col) -{ - if (VTERM_COLOR_IS_INDEXED(col)) { /* Convert indexed colors to RGB */ - lookup_colour_palette(state, col->indexed.idx, col); - } - col->type &= VTERM_COLOR_TYPE_MASK; /* Reset any metadata but the type */ -} - -void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright) -{ - state->bold_is_highbright = bold_is_highbright; -} - -INTERNAL void vterm_state_setpen(VTermState *state, const long args[], int argcount) -{ - // SGR - ECMA-48 8.3.117 - - int argi = 0; - int value; - - while(argi < argcount) { - // This logic is easier to do 'done' backwards; set it true, and make it - // false again in the 'default' case - int done = 1; - - long arg; - switch(arg = CSI_ARG(args[argi])) { - case CSI_ARG_MISSING: - case 0: // Reset - vterm_state_resetpen(state); - break; - - case 1: { // Bold on - const VTermColor *fg = &state->pen.fg; - state->pen.bold = 1; - setpenattr_bool(state, VTERM_ATTR_BOLD, 1); - if(!VTERM_COLOR_IS_DEFAULT_FG(fg) && VTERM_COLOR_IS_INDEXED(fg) && fg->indexed.idx < 8 && state->bold_is_highbright) - set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, fg->indexed.idx + (state->pen.bold ? 8 : 0)); - break; - } - - case 3: // Italic on - state->pen.italic = 1; - setpenattr_bool(state, VTERM_ATTR_ITALIC, 1); - break; - - case 4: // Underline - state->pen.underline = VTERM_UNDERLINE_SINGLE; - if(CSI_ARG_HAS_MORE(args[argi])) { - argi++; - switch(CSI_ARG(args[argi])) { - case 0: - state->pen.underline = 0; - break; - case 1: - state->pen.underline = VTERM_UNDERLINE_SINGLE; - break; - case 2: - state->pen.underline = VTERM_UNDERLINE_DOUBLE; - break; - case 3: - state->pen.underline = VTERM_UNDERLINE_CURLY; - break; - } - } - setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); - break; - - case 5: // Blink - state->pen.blink = 1; - setpenattr_bool(state, VTERM_ATTR_BLINK, 1); - break; - - case 7: // Reverse on - state->pen.reverse = 1; - setpenattr_bool(state, VTERM_ATTR_REVERSE, 1); - break; - - case 8: // Conceal on - state->pen.conceal = 1; - setpenattr_bool(state, VTERM_ATTR_CONCEAL, 1); - break; - - case 9: // Strikethrough on - state->pen.strike = 1; - setpenattr_bool(state, VTERM_ATTR_STRIKE, 1); - break; - - case 10: case 11: case 12: case 13: case 14: - case 15: case 16: case 17: case 18: case 19: // Select font - state->pen.font = CSI_ARG(args[argi]) - 10; - setpenattr_int(state, VTERM_ATTR_FONT, state->pen.font); - break; - - case 21: // Underline double - state->pen.underline = VTERM_UNDERLINE_DOUBLE; - setpenattr_int(state, VTERM_ATTR_UNDERLINE, state->pen.underline); - break; - - case 22: // Bold off - state->pen.bold = 0; - setpenattr_bool(state, VTERM_ATTR_BOLD, 0); - break; - - case 23: // Italic and Gothic (currently unsupported) off - state->pen.italic = 0; - setpenattr_bool(state, VTERM_ATTR_ITALIC, 0); - break; - - case 24: // Underline off - state->pen.underline = 0; - setpenattr_int(state, VTERM_ATTR_UNDERLINE, 0); - break; - - case 25: // Blink off - state->pen.blink = 0; - setpenattr_bool(state, VTERM_ATTR_BLINK, 0); - break; - - case 27: // Reverse off - state->pen.reverse = 0; - setpenattr_bool(state, VTERM_ATTR_REVERSE, 0); - break; - - case 28: // Conceal off (Reveal) - state->pen.conceal = 0; - setpenattr_bool(state, VTERM_ATTR_CONCEAL, 0); - break; - - case 29: // Strikethrough off - state->pen.strike = 0; - setpenattr_bool(state, VTERM_ATTR_STRIKE, 0); - break; - - case 30: case 31: case 32: case 33: - case 34: case 35: case 36: case 37: // Foreground colour palette - value = CSI_ARG(args[argi]) - 30; - if(state->pen.bold && state->bold_is_highbright) - value += 8; - set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value); - break; - - case 38: // Foreground colour alternative palette - if(argcount - argi < 1) - return; - argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.fg); - setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); - break; - - case 39: // Foreground colour default - state->pen.fg = state->default_fg; - setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg); - break; - - case 40: case 41: case 42: case 43: - case 44: case 45: case 46: case 47: // Background colour palette - value = CSI_ARG(args[argi]) - 40; - set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value); - break; - - case 48: // Background colour alternative palette - if(argcount - argi < 1) - return; - argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.bg); - setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); - break; - - case 49: // Default background - state->pen.bg = state->default_bg; - setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg); - break; - - case 73: // Superscript - case 74: // Subscript - case 75: // Superscript/subscript off - state->pen.small = (arg != 75); - state->pen.baseline = - (arg == 73) ? VTERM_BASELINE_RAISE : - (arg == 74) ? VTERM_BASELINE_LOWER : - VTERM_BASELINE_NORMAL; - setpenattr_bool(state, VTERM_ATTR_SMALL, state->pen.small); - setpenattr_int (state, VTERM_ATTR_BASELINE, state->pen.baseline); - break; - - case 90: case 91: case 92: case 93: - case 94: case 95: case 96: case 97: // Foreground colour high-intensity palette - value = CSI_ARG(args[argi]) - 90 + 8; - set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value); - break; - - case 100: case 101: case 102: case 103: - case 104: case 105: case 106: case 107: // Background colour high-intensity palette - value = CSI_ARG(args[argi]) - 100 + 8; - set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value); - break; - - default: - done = 0; - break; - } - - if(!done) { - DEBUG_LOG("libvterm: Unhandled CSI SGR %ld\n", arg); - } - - while(CSI_ARG_HAS_MORE(args[argi++])); - } -} - -static int vterm_state_getpen_color(const VTermColor *col, int argi, long args[], int fg) -{ - /* Do nothing if the given color is the default color */ - if (( fg && VTERM_COLOR_IS_DEFAULT_FG(col)) || - (!fg && VTERM_COLOR_IS_DEFAULT_BG(col))) { - return argi; - } - - /* Decide whether to send an indexed color or an RGB color */ - if (VTERM_COLOR_IS_INDEXED(col)) { - const uint8_t idx = col->indexed.idx; - if (idx < 8) { - args[argi++] = (idx + (fg ? 30 : 40)); - } - else if (idx < 16) { - args[argi++] = (idx - 8 + (fg ? 90 : 100)); - } - else { - args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48); - args[argi++] = CSI_ARG_FLAG_MORE | 5; - args[argi++] = idx; - } - } - else if (VTERM_COLOR_IS_RGB(col)) { - args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48); - args[argi++] = CSI_ARG_FLAG_MORE | 2; - args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.red; - args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.green; - args[argi++] = col->rgb.blue; - } - return argi; -} - -INTERNAL int vterm_state_getpen(VTermState *state, long args[], int argcount) -{ - int argi = 0; - - if(state->pen.bold) - args[argi++] = 1; - - if(state->pen.italic) - args[argi++] = 3; - - if(state->pen.underline == VTERM_UNDERLINE_SINGLE) - args[argi++] = 4; - if(state->pen.underline == VTERM_UNDERLINE_CURLY) - args[argi++] = 4 | CSI_ARG_FLAG_MORE, args[argi++] = 3; - - if(state->pen.blink) - args[argi++] = 5; - - if(state->pen.reverse) - args[argi++] = 7; - - if(state->pen.conceal) - args[argi++] = 8; - - if(state->pen.strike) - args[argi++] = 9; - - if(state->pen.font) - args[argi++] = 10 + state->pen.font; - - if(state->pen.underline == VTERM_UNDERLINE_DOUBLE) - args[argi++] = 21; - - argi = vterm_state_getpen_color(&state->pen.fg, argi, args, true); - - argi = vterm_state_getpen_color(&state->pen.bg, argi, args, false); - - if(state->pen.small) { - if(state->pen.baseline == VTERM_BASELINE_RAISE) - args[argi++] = 73; - else if(state->pen.baseline == VTERM_BASELINE_LOWER) - args[argi++] = 74; - } - - return argi; -} - -int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val) -{ - switch(attr) { - case VTERM_ATTR_BOLD: - val->boolean = state->pen.bold; - return 1; - - case VTERM_ATTR_UNDERLINE: - val->number = state->pen.underline; - return 1; - - case VTERM_ATTR_ITALIC: - val->boolean = state->pen.italic; - return 1; - - case VTERM_ATTR_BLINK: - val->boolean = state->pen.blink; - return 1; - - case VTERM_ATTR_REVERSE: - val->boolean = state->pen.reverse; - return 1; - - case VTERM_ATTR_CONCEAL: - val->boolean = state->pen.conceal; - return 1; - - case VTERM_ATTR_STRIKE: - val->boolean = state->pen.strike; - return 1; - - case VTERM_ATTR_FONT: - val->number = state->pen.font; - return 1; - - case VTERM_ATTR_FOREGROUND: - val->color = state->pen.fg; - return 1; - - case VTERM_ATTR_BACKGROUND: - val->color = state->pen.bg; - return 1; - - case VTERM_ATTR_SMALL: - val->boolean = state->pen.small; - return 1; - - case VTERM_ATTR_BASELINE: - val->number = state->pen.baseline; - return 1; - - case VTERM_ATTR_URI: - val->number = state->pen.uri; - return 1; - - case VTERM_N_ATTRS: - return 0; - } - - return 0; -} - -int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val) -{ - if (!val) { - return 0; - } - - if(type != vterm_get_attr_type(attr)) { - DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n", - attr, vterm_get_attr_type(attr), type); - return 0; - } - - switch (attr) { - case VTERM_ATTR_BOLD: - state->pen.bold = val->boolean; - break; - case VTERM_ATTR_UNDERLINE: - state->pen.underline = val->number; - break; - case VTERM_ATTR_ITALIC: - state->pen.italic = val->boolean; - break; - case VTERM_ATTR_BLINK: - state->pen.blink = val->boolean; - break; - case VTERM_ATTR_REVERSE: - state->pen.reverse = val->boolean; - break; - case VTERM_ATTR_CONCEAL: - state->pen.conceal = val->boolean; - break; - case VTERM_ATTR_STRIKE: - state->pen.strike = val->boolean; - break; - case VTERM_ATTR_FONT: - state->pen.font = val->number; - break; - case VTERM_ATTR_FOREGROUND: - state->pen.fg = val->color; - break; - case VTERM_ATTR_BACKGROUND: - state->pen.bg = val->color; - break; - case VTERM_ATTR_SMALL: - state->pen.small = val->boolean; - break; - case VTERM_ATTR_BASELINE: - state->pen.baseline = val->number; - break; - case VTERM_ATTR_URI: - state->pen.uri = val->number; - break; - default: - return 0; - } - - if(state->callbacks && state->callbacks->setpenattr) - (*state->callbacks->setpenattr)(attr, val, state->cbdata); - - return 1; -} diff --git a/src/vterm/rect.h b/src/vterm/rect.h deleted file mode 100644 index 2114f24c1b..0000000000 --- a/src/vterm/rect.h +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Some utility functions on VTermRect structures - */ - -#define STRFrect "(%d,%d-%d,%d)" -#define ARGSrect(r) (r).start_row, (r).start_col, (r).end_row, (r).end_col - -/* Expand dst to contain src as well */ -static void rect_expand(VTermRect *dst, VTermRect *src) -{ - if(dst->start_row > src->start_row) dst->start_row = src->start_row; - if(dst->start_col > src->start_col) dst->start_col = src->start_col; - if(dst->end_row < src->end_row) dst->end_row = src->end_row; - if(dst->end_col < src->end_col) dst->end_col = src->end_col; -} - -/* Clip the dst to ensure it does not step outside of bounds */ -static void rect_clip(VTermRect *dst, VTermRect *bounds) -{ - if(dst->start_row < bounds->start_row) dst->start_row = bounds->start_row; - if(dst->start_col < bounds->start_col) dst->start_col = bounds->start_col; - if(dst->end_row > bounds->end_row) dst->end_row = bounds->end_row; - if(dst->end_col > bounds->end_col) dst->end_col = bounds->end_col; - /* Ensure it doesn't end up negatively-sized */ - if(dst->end_row < dst->start_row) dst->end_row = dst->start_row; - if(dst->end_col < dst->start_col) dst->end_col = dst->start_col; -} - -/* True if the two rectangles are equal */ -static int rect_equal(VTermRect *a, VTermRect *b) -{ - return (a->start_row == b->start_row) && - (a->start_col == b->start_col) && - (a->end_row == b->end_row) && - (a->end_col == b->end_col); -} - -/* True if small is contained entirely within big */ -static int rect_contains(VTermRect *big, VTermRect *small) -{ - if(small->start_row < big->start_row) return 0; - if(small->start_col < big->start_col) return 0; - if(small->end_row > big->end_row) return 0; - if(small->end_col > big->end_col) return 0; - return 1; -} - -/* True if the rectangles overlap at all */ -static int rect_intersects(VTermRect *a, VTermRect *b) -{ - if(a->start_row > b->end_row || b->start_row > a->end_row) - return 0; - if(a->start_col > b->end_col || b->start_col > a->end_col) - return 0; - return 1; -} diff --git a/src/vterm/screen.c b/src/vterm/screen.c deleted file mode 100644 index 7de345ca39..0000000000 --- a/src/vterm/screen.c +++ /dev/null @@ -1,1174 +0,0 @@ -#include "vterm_internal.h" - -#include <stdio.h> -#include <string.h> -#include "nvim/grid.h" -#include "nvim/mbyte.h" -#include "nvim/tui/termkey/termkey.h" - -#include "rect.h" - -#define UNICODE_SPACE 0x20 -#define UNICODE_LINEFEED 0x0a - -#undef DEBUG_REFLOW - -/* State of the pen at some moment in time, also used in a cell */ -typedef struct -{ - /* After the bitfield */ - VTermColor fg, bg; - - /* Opaque ID that maps to a URI in a set */ - int uri; - - unsigned int bold : 1; - unsigned int underline : 2; - unsigned int italic : 1; - unsigned int blink : 1; - unsigned int reverse : 1; - unsigned int conceal : 1; - unsigned int strike : 1; - unsigned int font : 4; /* 0 to 9 */ - unsigned int small : 1; - unsigned int baseline : 2; - - /* Extra state storage that isn't strictly pen-related */ - unsigned int protected_cell : 1; - unsigned int dwl : 1; /* on a DECDWL or DECDHL line */ - unsigned int dhl : 2; /* on a DECDHL line (1=top 2=bottom) */ -} ScreenPen; - -/* Internal representation of a screen cell */ -typedef struct -{ - schar_T schar; - ScreenPen pen; -} ScreenCell; - -struct VTermScreen -{ - VTerm *vt; - VTermState *state; - - const VTermScreenCallbacks *callbacks; - void *cbdata; - - VTermDamageSize damage_merge; - /* start_row == -1 => no damage */ - VTermRect damaged; - VTermRect pending_scrollrect; - int pending_scroll_downward, pending_scroll_rightward; - - int rows; - int cols; - - unsigned int global_reverse : 1; - unsigned int reflow : 1; - - /* Primary and Altscreen. buffers[1] is lazily allocated as needed */ - ScreenCell *buffers[2]; - - /* buffer will == buffers[0] or buffers[1], depending on altscreen */ - ScreenCell *buffer; - - /* buffer for a single screen row used in scrollback storage callbacks */ - VTermScreenCell *sb_buffer; - - ScreenPen pen; -}; - -static inline void clearcell(const VTermScreen *screen, ScreenCell *cell) -{ - cell->schar = 0; - cell->pen = screen->pen; -} - -static inline ScreenCell *getcell(const VTermScreen *screen, int row, int col) -{ - if(row < 0 || row >= screen->rows) - return NULL; - if(col < 0 || col >= screen->cols) - return NULL; - return screen->buffer + (screen->cols * row) + col; -} - -static ScreenCell *alloc_buffer(VTermScreen *screen, int rows, int cols) -{ - ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, sizeof(ScreenCell) * rows * cols); - - for(int row = 0; row < rows; row++) { - for(int col = 0; col < cols; col++) { - clearcell(screen, &new_buffer[row * cols + col]); - } - } - - return new_buffer; -} - -static void damagerect(VTermScreen *screen, VTermRect rect) -{ - VTermRect emit; - - switch(screen->damage_merge) { - case VTERM_DAMAGE_CELL: - /* Always emit damage event */ - emit = rect; - break; - - case VTERM_DAMAGE_ROW: - /* Emit damage longer than one row. Try to merge with existing damage in - * the same row */ - if(rect.end_row > rect.start_row + 1) { - // Bigger than 1 line - flush existing, emit this - vterm_screen_flush_damage(screen); - emit = rect; - } - else if(screen->damaged.start_row == -1) { - // None stored yet - screen->damaged = rect; - return; - } - else if(rect.start_row == screen->damaged.start_row) { - // Merge with the stored line - if(screen->damaged.start_col > rect.start_col) - screen->damaged.start_col = rect.start_col; - if(screen->damaged.end_col < rect.end_col) - screen->damaged.end_col = rect.end_col; - return; - } - else { - // Emit the currently stored line, store a new one - emit = screen->damaged; - screen->damaged = rect; - } - break; - - case VTERM_DAMAGE_SCREEN: - case VTERM_DAMAGE_SCROLL: - /* Never emit damage event */ - if(screen->damaged.start_row == -1) - screen->damaged = rect; - else { - rect_expand(&screen->damaged, &rect); - } - return; - - default: - DEBUG_LOG("TODO: Maybe merge damage for level %d\n", screen->damage_merge); - return; - } - - if(screen->callbacks && screen->callbacks->damage) - (*screen->callbacks->damage)(emit, screen->cbdata); -} - -static void damagescreen(VTermScreen *screen) -{ - VTermRect rect = { - .start_row = 0, - .end_row = screen->rows, - .start_col = 0, - .end_col = screen->cols, - }; - - damagerect(screen, rect); -} - -static int putglyph(VTermGlyphInfo *info, VTermPos pos, void *user) -{ - VTermScreen *screen = user; - ScreenCell *cell = getcell(screen, pos.row, pos.col); - - if(!cell) - return 0; - - cell->schar = info->schar; - if (info->schar != 0) { - cell->pen = screen->pen; - } - - for(int col = 1; col < info->width; col++) - getcell(screen, pos.row, pos.col + col)->schar = (uint32_t)-1; - - VTermRect rect = { - .start_row = pos.row, - .end_row = pos.row+1, - .start_col = pos.col, - .end_col = pos.col+info->width, - }; - - cell->pen.protected_cell = info->protected_cell; - cell->pen.dwl = info->dwl; - cell->pen.dhl = info->dhl; - - damagerect(screen, rect); - - return 1; -} - -static void sb_pushline_from_row(VTermScreen *screen, int row) -{ - VTermPos pos = { .row = row }; - for(pos.col = 0; pos.col < screen->cols; pos.col++) - vterm_screen_get_cell(screen, pos, screen->sb_buffer + pos.col); - - (screen->callbacks->sb_pushline)(screen->cols, screen->sb_buffer, screen->cbdata); -} - -static int moverect_internal(VTermRect dest, VTermRect src, void *user) -{ - VTermScreen *screen = user; - - if(screen->callbacks && screen->callbacks->sb_pushline && - dest.start_row == 0 && dest.start_col == 0 && // starts top-left corner - dest.end_col == screen->cols && // full width - screen->buffer == screen->buffers[BUFIDX_PRIMARY]) { // not altscreen - for(int row = 0; row < src.start_row; row++) - sb_pushline_from_row(screen, row); - } - - int cols = src.end_col - src.start_col; - int downward = src.start_row - dest.start_row; - - int init_row, test_row, inc_row; - if(downward < 0) { - init_row = dest.end_row - 1; - test_row = dest.start_row - 1; - inc_row = -1; - } - else { - init_row = dest.start_row; - test_row = dest.end_row; - inc_row = +1; - } - - for(int row = init_row; row != test_row; row += inc_row) - memmove(getcell(screen, row, dest.start_col), - getcell(screen, row + downward, src.start_col), - cols * sizeof(ScreenCell)); - - return 1; -} - -static int moverect_user(VTermRect dest, VTermRect src, void *user) -{ - VTermScreen *screen = user; - - if(screen->callbacks && screen->callbacks->moverect) { - if(screen->damage_merge != VTERM_DAMAGE_SCROLL) - // Avoid an infinite loop - vterm_screen_flush_damage(screen); - - if((*screen->callbacks->moverect)(dest, src, screen->cbdata)) - return 1; - } - - damagerect(screen, dest); - - return 1; -} - -static int erase_internal(VTermRect rect, int selective, void *user) -{ - VTermScreen *screen = user; - - for(int row = rect.start_row; row < screen->state->rows && row < rect.end_row; row++) { - const VTermLineInfo *info = vterm_state_get_lineinfo(screen->state, row); - - for(int col = rect.start_col; col < rect.end_col; col++) { - ScreenCell *cell = getcell(screen, row, col); - - if(selective && cell->pen.protected_cell) - continue; - - cell->schar = 0; - cell->pen = (ScreenPen){ - /* Only copy .fg and .bg; leave things like rv in reset state */ - .fg = screen->pen.fg, - .bg = screen->pen.bg, - }; - cell->pen.dwl = info->doublewidth; - cell->pen.dhl = info->doubleheight; - } - } - - return 1; -} - -static int erase_user(VTermRect rect, int selective, void *user) -{ - VTermScreen *screen = user; - - damagerect(screen, rect); - - return 1; -} - -static int erase(VTermRect rect, int selective, void *user) -{ - erase_internal(rect, selective, user); - return erase_user(rect, 0, user); -} - -static int scrollrect(VTermRect rect, int downward, int rightward, void *user) -{ - VTermScreen *screen = user; - - if(screen->damage_merge != VTERM_DAMAGE_SCROLL) { - vterm_scroll_rect(rect, downward, rightward, - moverect_internal, erase_internal, screen); - - vterm_screen_flush_damage(screen); - - vterm_scroll_rect(rect, downward, rightward, - moverect_user, erase_user, screen); - - return 1; - } - - if(screen->damaged.start_row != -1 && - !rect_intersects(&rect, &screen->damaged)) { - vterm_screen_flush_damage(screen); - } - - if(screen->pending_scrollrect.start_row == -1) { - screen->pending_scrollrect = rect; - screen->pending_scroll_downward = downward; - screen->pending_scroll_rightward = rightward; - } - else if(rect_equal(&screen->pending_scrollrect, &rect) && - ((screen->pending_scroll_downward == 0 && downward == 0) || - (screen->pending_scroll_rightward == 0 && rightward == 0))) { - screen->pending_scroll_downward += downward; - screen->pending_scroll_rightward += rightward; - } - else { - vterm_screen_flush_damage(screen); - - screen->pending_scrollrect = rect; - screen->pending_scroll_downward = downward; - screen->pending_scroll_rightward = rightward; - } - - vterm_scroll_rect(rect, downward, rightward, - moverect_internal, erase_internal, screen); - - if(screen->damaged.start_row == -1) - return 1; - - if(rect_contains(&rect, &screen->damaged)) { - /* Scroll region entirely contains the damage; just move it */ - vterm_rect_move(&screen->damaged, -downward, -rightward); - rect_clip(&screen->damaged, &rect); - } - /* There are a number of possible cases here, but lets restrict this to only - * the common case where we might actually gain some performance by - * optimising it. Namely, a vertical scroll that neatly cuts the damage - * region in half. - */ - else if(rect.start_col <= screen->damaged.start_col && - rect.end_col >= screen->damaged.end_col && - rightward == 0) { - if(screen->damaged.start_row >= rect.start_row && - screen->damaged.start_row < rect.end_row) { - screen->damaged.start_row -= downward; - if(screen->damaged.start_row < rect.start_row) - screen->damaged.start_row = rect.start_row; - if(screen->damaged.start_row > rect.end_row) - screen->damaged.start_row = rect.end_row; - } - if(screen->damaged.end_row >= rect.start_row && - screen->damaged.end_row < rect.end_row) { - screen->damaged.end_row -= downward; - if(screen->damaged.end_row < rect.start_row) - screen->damaged.end_row = rect.start_row; - if(screen->damaged.end_row > rect.end_row) - screen->damaged.end_row = rect.end_row; - } - } - else { - DEBUG_LOG("TODO: Just flush and redo damaged=" STRFrect " rect=" STRFrect "\n", - ARGSrect(screen->damaged), ARGSrect(rect)); - } - - return 1; -} - -static int movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user) -{ - VTermScreen *screen = user; - - if(screen->callbacks && screen->callbacks->movecursor) - return (*screen->callbacks->movecursor)(pos, oldpos, visible, screen->cbdata); - - return 0; -} - -static int setpenattr(VTermAttr attr, VTermValue *val, void *user) -{ - VTermScreen *screen = user; - - switch(attr) { - case VTERM_ATTR_BOLD: - screen->pen.bold = val->boolean; - return 1; - case VTERM_ATTR_UNDERLINE: - screen->pen.underline = val->number; - return 1; - case VTERM_ATTR_ITALIC: - screen->pen.italic = val->boolean; - return 1; - case VTERM_ATTR_BLINK: - screen->pen.blink = val->boolean; - return 1; - case VTERM_ATTR_REVERSE: - screen->pen.reverse = val->boolean; - return 1; - case VTERM_ATTR_CONCEAL: - screen->pen.conceal = val->boolean; - return 1; - case VTERM_ATTR_STRIKE: - screen->pen.strike = val->boolean; - return 1; - case VTERM_ATTR_FONT: - screen->pen.font = val->number; - return 1; - case VTERM_ATTR_FOREGROUND: - screen->pen.fg = val->color; - return 1; - case VTERM_ATTR_BACKGROUND: - screen->pen.bg = val->color; - return 1; - case VTERM_ATTR_SMALL: - screen->pen.small = val->boolean; - return 1; - case VTERM_ATTR_BASELINE: - screen->pen.baseline = val->number; - return 1; - case VTERM_ATTR_URI: - screen->pen.uri = val->number; - return 1; - - case VTERM_N_ATTRS: - return 0; - } - - return 0; -} - -static int settermprop(VTermProp prop, VTermValue *val, void *user) -{ - VTermScreen *screen = user; - - switch(prop) { - case VTERM_PROP_ALTSCREEN: - if(val->boolean && !screen->buffers[BUFIDX_ALTSCREEN]) - return 0; - - screen->buffer = val->boolean ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY]; - /* only send a damage event on disable; because during enable there's an - * erase that sends a damage anyway - */ - if(!val->boolean) - damagescreen(screen); - break; - case VTERM_PROP_REVERSE: - screen->global_reverse = val->boolean; - damagescreen(screen); - break; - default: - ; /* ignore */ - } - - if(screen->callbacks && screen->callbacks->settermprop) - return (*screen->callbacks->settermprop)(prop, val, screen->cbdata); - - return 1; -} - -static int bell(void *user) -{ - VTermScreen *screen = user; - - if(screen->callbacks && screen->callbacks->bell) - return (*screen->callbacks->bell)(screen->cbdata); - - return 0; -} - -/* How many cells are non-blank - * Returns the position of the first blank cell in the trailing blank end */ -static int line_popcount(ScreenCell *buffer, int row, int rows, int cols) -{ - int col = cols - 1; - while(col >= 0 && buffer[row * cols + col].schar == 0) - col--; - return col + 1; -} - -static void resize_buffer(VTermScreen *screen, int bufidx, int new_rows, int new_cols, bool active, VTermStateFields *statefields) -{ - int old_rows = screen->rows; - int old_cols = screen->cols; - - ScreenCell *old_buffer = screen->buffers[bufidx]; - VTermLineInfo *old_lineinfo = statefields->lineinfos[bufidx]; - - ScreenCell *new_buffer = vterm_allocator_malloc(screen->vt, sizeof(ScreenCell) * new_rows * new_cols); - VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, sizeof(new_lineinfo[0]) * new_rows); - - int old_row = old_rows - 1; - int new_row = new_rows - 1; - - VTermPos old_cursor = statefields->pos; - VTermPos new_cursor = { -1, -1 }; - -#ifdef DEBUG_REFLOW - fprintf(stderr, "Resizing from %dx%d to %dx%d; cursor was at (%d,%d)\n", - old_cols, old_rows, new_cols, new_rows, old_cursor.col, old_cursor.row); -#endif - - /* Keep track of the final row that is knonw to be blank, so we know what - * spare space we have for scrolling into - */ - int final_blank_row = new_rows; - - while(old_row >= 0) { - int old_row_end = old_row; - /* TODO: Stop if dwl or dhl */ - 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(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); - } - - if(final_blank_row == (new_row + 1) && width == 0) - final_blank_row = new_row; - - int new_height = screen->reflow - ? width ? (width + new_cols - 1) / new_cols : 1 - : 1; - - int new_row_end = new_row; - int new_row_start = new_row - new_height + 1; - - old_row = old_row_start; - int old_col = 0; - - int spare_rows = new_rows - final_blank_row; - - if(new_row_start < 0 && /* we'd fall off the top */ - spare_rows >= 0 && /* we actually have spare rows */ - (!active || new_cursor.row == -1 || (new_cursor.row - new_row_start) < new_rows)) - { - /* Attempt to scroll content down into the blank rows at the bottom to - * make it fit - */ - int downwards = -new_row_start; - if(downwards > spare_rows) - downwards = spare_rows; - int rowcount = new_rows - downwards; - -#ifdef DEBUG_REFLOW - fprintf(stderr, " scroll %d rows +%d downwards\n", rowcount, downwards); -#endif - - memmove(&new_buffer[downwards * new_cols], &new_buffer[0], (unsigned long) rowcount * new_cols * sizeof(ScreenCell)); - memmove(&new_lineinfo[downwards], &new_lineinfo[0], rowcount * sizeof(new_lineinfo[0])); - - new_row += downwards; - new_row_start += downwards; - new_row_end += downwards; - - if(new_cursor.row >= 0) - new_cursor.row += downwards; - - final_blank_row += downwards; - } - -#ifdef DEBUG_REFLOW - fprintf(stderr, " rows [%d..%d] <- [%d..%d] width=%d\n", - new_row_start, new_row_end, old_row_start, old_row_end, width); -#endif - - if(new_row_start < 0) { - if(old_row_start <= old_cursor.row && old_cursor.row <= old_row_end) { - new_cursor.row = 0; - new_cursor.col = old_cursor.col; - if(new_cursor.col >= new_cols) - new_cursor.col = new_cols-1; - } - break; - } - - for(new_row = new_row_start, old_row = old_row_start; new_row <= new_row_end; new_row++) { - int count = width >= new_cols ? new_cols : width; - width -= count; - - int new_col = 0; - - while(count) { - /* TODO: This could surely be done a lot faster by memcpy()'ing the entire range */ - new_buffer[new_row * new_cols + new_col] = old_buffer[old_row * old_cols + old_col]; - - if(old_cursor.row == old_row && old_cursor.col == old_col) - new_cursor.row = new_row, new_cursor.col = new_col; - - old_col++; - if(old_col == old_cols) { - old_row++; - - if(!screen->reflow) { - new_col++; - break; - } - old_col = 0; - } - - new_col++; - count--; - } - - if(old_cursor.row == old_row && old_cursor.col >= old_col) { - new_cursor.row = new_row, new_cursor.col = (old_cursor.col - old_col + new_col); - if(new_cursor.col >= new_cols) - new_cursor.col = new_cols-1; - } - - while(new_col < new_cols) { - clearcell(screen, &new_buffer[new_row * new_cols + new_col]); - new_col++; - } - - new_lineinfo[new_row].continuation = (new_row > new_row_start); - } - - old_row = old_row_start - 1; - new_row = new_row_start - 1; - } - - if(old_cursor.row <= old_row) { - /* cursor would have moved entirely off the top of the screen; lets just - * bring it within range */ - new_cursor.row = 0, new_cursor.col = old_cursor.col; - if(new_cursor.col >= new_cols) - new_cursor.col = new_cols-1; - } - - /* We really expect the cursor position to be set by now */ - if(active && (new_cursor.row == -1 || new_cursor.col == -1)) { - fprintf(stderr, "screen_resize failed to update cursor position\n"); - abort(); - } - - if(old_row >= 0 && bufidx == BUFIDX_PRIMARY) { - /* Push spare lines to scrollback buffer */ - if(screen->callbacks && screen->callbacks->sb_pushline) - for(int row = 0; row <= old_row; row++) - sb_pushline_from_row(screen, row); - if(active) - statefields->pos.row -= (old_row + 1); - } - if(new_row >= 0 && bufidx == BUFIDX_PRIMARY && - screen->callbacks && screen->callbacks->sb_popline) { - /* Try to backfill rows by popping scrollback buffer */ - while(new_row >= 0) { - if(!(screen->callbacks->sb_popline(old_cols, screen->sb_buffer, screen->cbdata))) - break; - - VTermPos pos = { .row = new_row }; - for(pos.col = 0; pos.col < old_cols && pos.col < new_cols; pos.col += screen->sb_buffer[pos.col].width) { - VTermScreenCell *src = &screen->sb_buffer[pos.col]; - ScreenCell *dst = &new_buffer[pos.row * new_cols + pos.col]; - - dst->schar = src->schar; - - dst->pen.bold = src->attrs.bold; - dst->pen.underline = src->attrs.underline; - dst->pen.italic = src->attrs.italic; - dst->pen.blink = src->attrs.blink; - dst->pen.reverse = src->attrs.reverse ^ screen->global_reverse; - dst->pen.conceal = src->attrs.conceal; - dst->pen.strike = src->attrs.strike; - dst->pen.font = src->attrs.font; - dst->pen.small = src->attrs.small; - dst->pen.baseline = src->attrs.baseline; - - dst->pen.fg = src->fg; - dst->pen.bg = src->bg; - - dst->pen.uri = src->uri; - - if(src->width == 2 && pos.col < (new_cols-1)) - (dst + 1)->schar = (uint32_t) -1; - } - for( ; pos.col < new_cols; pos.col++) - clearcell(screen, &new_buffer[pos.row * new_cols + pos.col]); - new_row--; - - if(active) - statefields->pos.row++; - } - } - if(new_row >= 0) { - /* Scroll new rows back up to the top and fill in blanks at the bottom */ - int moverows = new_rows - new_row - 1; - memmove(&new_buffer[0], &new_buffer[(new_row + 1) * new_cols], (unsigned long) moverows * new_cols * sizeof(ScreenCell)); - memmove(&new_lineinfo[0], &new_lineinfo[new_row + 1], moverows * sizeof(new_lineinfo[0])); - - new_cursor.row -= (new_row + 1); - - for(new_row = moverows; new_row < new_rows; new_row++) { - for(int col = 0; col < new_cols; col++) - clearcell(screen, &new_buffer[new_row * new_cols + col]); - new_lineinfo[new_row] = (VTermLineInfo){ 0 }; - } - } - - vterm_allocator_free(screen->vt, old_buffer); - screen->buffers[bufidx] = new_buffer; - - vterm_allocator_free(screen->vt, old_lineinfo); - statefields->lineinfos[bufidx] = new_lineinfo; - - if(active) - statefields->pos = new_cursor; - - return; -} - -static int resize(int new_rows, int new_cols, VTermStateFields *fields, void *user) -{ - VTermScreen *screen = user; - - int altscreen_active = (screen->buffers[BUFIDX_ALTSCREEN] && screen->buffer == screen->buffers[BUFIDX_ALTSCREEN]); - - int old_rows = screen->rows; - int old_cols = screen->cols; - - if(new_cols > old_cols) { - /* Ensure that ->sb_buffer is large enough for a new or and old row */ - if(screen->sb_buffer) - vterm_allocator_free(screen->vt, screen->sb_buffer); - - screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * new_cols); - } - - resize_buffer(screen, 0, new_rows, new_cols, !altscreen_active, fields); - if(screen->buffers[BUFIDX_ALTSCREEN]) - resize_buffer(screen, 1, new_rows, new_cols, altscreen_active, fields); - else if(new_rows != old_rows) { - /* We don't need a full resize of the altscreen because it isn't enabled - * but we should at least keep the lineinfo the right size */ - vterm_allocator_free(screen->vt, fields->lineinfos[BUFIDX_ALTSCREEN]); - - VTermLineInfo *new_lineinfo = vterm_allocator_malloc(screen->vt, sizeof(new_lineinfo[0]) * new_rows); - for(int row = 0; row < new_rows; row++) - new_lineinfo[row] = (VTermLineInfo){ 0 }; - - fields->lineinfos[BUFIDX_ALTSCREEN] = new_lineinfo; - } - - screen->buffer = altscreen_active ? screen->buffers[BUFIDX_ALTSCREEN] : screen->buffers[BUFIDX_PRIMARY]; - - screen->rows = new_rows; - screen->cols = new_cols; - - if(new_cols <= old_cols) { - if(screen->sb_buffer) - vterm_allocator_free(screen->vt, screen->sb_buffer); - - screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * new_cols); - } - - /* TODO: Maaaaybe we can optimise this if there's no reflow happening */ - damagescreen(screen); - - if(screen->callbacks && screen->callbacks->resize) - return (*screen->callbacks->resize)(new_rows, new_cols, screen->cbdata); - - return 1; -} - -static int setlineinfo(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, void *user) -{ - VTermScreen *screen = user; - - if(newinfo->doublewidth != oldinfo->doublewidth || - newinfo->doubleheight != oldinfo->doubleheight) { - for(int col = 0; col < screen->cols; col++) { - ScreenCell *cell = getcell(screen, row, col); - cell->pen.dwl = newinfo->doublewidth; - cell->pen.dhl = newinfo->doubleheight; - } - - VTermRect rect = { - .start_row = row, - .end_row = row + 1, - .start_col = 0, - .end_col = newinfo->doublewidth ? screen->cols / 2 : screen->cols, - }; - damagerect(screen, rect); - - if(newinfo->doublewidth) { - rect.start_col = screen->cols / 2; - rect.end_col = screen->cols; - - erase_internal(rect, 0, user); - } - } - - return 1; -} - -static int sb_clear(void *user) { - VTermScreen *screen = user; - - if(screen->callbacks && screen->callbacks->sb_clear) - if((*screen->callbacks->sb_clear)(screen->cbdata)) - return 1; - - return 0; -} - -static VTermStateCallbacks state_cbs = { - .putglyph = &putglyph, - .movecursor = &movecursor, - .scrollrect = &scrollrect, - .erase = &erase, - .setpenattr = &setpenattr, - .settermprop = &settermprop, - .bell = &bell, - .resize = &resize, - .setlineinfo = &setlineinfo, - .sb_clear = &sb_clear, -}; - -static VTermScreen *screen_new(VTerm *vt) -{ - VTermState *state = vterm_obtain_state(vt); - if(!state) - return NULL; - - VTermScreen *screen = vterm_allocator_malloc(vt, sizeof(VTermScreen)); - int rows, cols; - - vterm_get_size(vt, &rows, &cols); - - screen->vt = vt; - screen->state = state; - - screen->damage_merge = VTERM_DAMAGE_CELL; - screen->damaged.start_row = -1; - screen->pending_scrollrect.start_row = -1; - - screen->rows = rows; - screen->cols = cols; - - screen->global_reverse = false; - screen->reflow = false; - - screen->callbacks = NULL; - screen->cbdata = NULL; - - screen->buffers[BUFIDX_PRIMARY] = alloc_buffer(screen, rows, cols); - - screen->buffer = screen->buffers[BUFIDX_PRIMARY]; - - screen->sb_buffer = vterm_allocator_malloc(screen->vt, sizeof(VTermScreenCell) * cols); - - vterm_state_set_callbacks(screen->state, &state_cbs, screen); - - return screen; -} - -INTERNAL void vterm_screen_free(VTermScreen *screen) -{ - vterm_allocator_free(screen->vt, screen->buffers[BUFIDX_PRIMARY]); - if(screen->buffers[BUFIDX_ALTSCREEN]) - vterm_allocator_free(screen->vt, screen->buffers[BUFIDX_ALTSCREEN]); - - vterm_allocator_free(screen->vt, screen->sb_buffer); - - vterm_allocator_free(screen->vt, screen); -} - -void vterm_screen_reset(VTermScreen *screen, int hard) -{ - screen->damaged.start_row = -1; - screen->pending_scrollrect.start_row = -1; - vterm_state_reset(screen->state, hard); - vterm_screen_flush_damage(screen); -} - -size_t vterm_screen_get_text(const VTermScreen *screen, char *buffer, size_t len, const VTermRect rect) -{ - size_t outpos = 0; - int padding = 0; - -#define PUT(bytes, thislen) \ - if(true) { \ - if(buffer && outpos + thislen <= len) \ - memcpy((char *)buffer + outpos, bytes, thislen); \ - outpos += thislen; \ - } \ - - for(int row = rect.start_row; row < rect.end_row; row++) { - for(int col = rect.start_col; col < rect.end_col; col++) { - ScreenCell *cell = getcell(screen, row, col); - - if(cell->schar == 0) - // Erased cell, might need a space - padding++; - else if(cell->schar == (uint32_t)-1) - // Gap behind a double-width char, do nothing - ; - else { - while(padding) { - PUT(" ", 1); - padding--; - } - char buf[MAX_SCHAR_SIZE + 1]; - size_t thislen = schar_get(buf, cell->schar); - PUT(buf, thislen); - } - } - - if(row < rect.end_row - 1) { - PUT("\n", 1); - padding = 0; - } - } - - return outpos; -} - -/* Copy internal to external representation of a screen cell */ -int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCell *cell) -{ - ScreenCell *intcell = getcell(screen, pos.row, pos.col); - if(!intcell) - return 0; - - cell->schar = intcell->schar; - - cell->attrs.bold = intcell->pen.bold; - cell->attrs.underline = intcell->pen.underline; - cell->attrs.italic = intcell->pen.italic; - cell->attrs.blink = intcell->pen.blink; - cell->attrs.reverse = intcell->pen.reverse ^ screen->global_reverse; - cell->attrs.conceal = intcell->pen.conceal; - cell->attrs.strike = intcell->pen.strike; - cell->attrs.font = intcell->pen.font; - cell->attrs.small = intcell->pen.small; - cell->attrs.baseline = intcell->pen.baseline; - - cell->attrs.dwl = intcell->pen.dwl; - cell->attrs.dhl = intcell->pen.dhl; - - cell->fg = intcell->pen.fg; - cell->bg = intcell->pen.bg; - - cell->uri = intcell->pen.uri; - - if(pos.col < (screen->cols - 1) && - getcell(screen, pos.row, pos.col + 1)->schar == (uint32_t)-1) - cell->width = 2; - else - cell->width = 1; - - return 1; -} - -int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos) -{ - /* This cell is EOL if this and every cell to the right is black */ - for(; pos.col < screen->cols; pos.col++) { - ScreenCell *cell = getcell(screen, pos.row, pos.col); - if(cell->schar != 0) - return 0; - } - - return 1; -} - -VTermScreen *vterm_obtain_screen(VTerm *vt) -{ - if(vt->screen) - return vt->screen; - - VTermScreen *screen = screen_new(vt); - vt->screen = screen; - - return screen; -} - -void vterm_screen_enable_reflow(VTermScreen *screen, bool reflow) -{ - screen->reflow = reflow; -} - -#undef vterm_screen_set_reflow -void vterm_screen_set_reflow(VTermScreen *screen, bool reflow) -{ - vterm_screen_enable_reflow(screen, reflow); -} - -void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen) -{ - if(!screen->buffers[BUFIDX_ALTSCREEN] && altscreen) { - int rows, cols; - vterm_get_size(screen->vt, &rows, &cols); - - screen->buffers[BUFIDX_ALTSCREEN] = alloc_buffer(screen, rows, cols); - } -} - -void vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, void *user) -{ - screen->callbacks = callbacks; - screen->cbdata = user; -} - -void *vterm_screen_get_cbdata(VTermScreen *screen) -{ - return screen->cbdata; -} - -void vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, const VTermStateFallbacks *fallbacks, void *user) -{ - vterm_state_set_unrecognised_fallbacks(screen->state, fallbacks, user); -} - -void *vterm_screen_get_unrecognised_fbdata(VTermScreen *screen) -{ - return vterm_state_get_unrecognised_fbdata(screen->state); -} - -void vterm_screen_flush_damage(VTermScreen *screen) -{ - if(screen->pending_scrollrect.start_row != -1) { - vterm_scroll_rect(screen->pending_scrollrect, screen->pending_scroll_downward, screen->pending_scroll_rightward, - moverect_user, erase_user, screen); - - screen->pending_scrollrect.start_row = -1; - } - - if(screen->damaged.start_row != -1) { - if(screen->callbacks && screen->callbacks->damage) - (*screen->callbacks->damage)(screen->damaged, screen->cbdata); - - screen->damaged.start_row = -1; - } -} - -void vterm_screen_set_damage_merge(VTermScreen *screen, VTermDamageSize size) -{ - vterm_screen_flush_damage(screen); - screen->damage_merge = size; -} - -static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b) -{ - if((attrs & VTERM_ATTR_BOLD_MASK) && (a->pen.bold != b->pen.bold)) - return 1; - if((attrs & VTERM_ATTR_UNDERLINE_MASK) && (a->pen.underline != b->pen.underline)) - return 1; - if((attrs & VTERM_ATTR_ITALIC_MASK) && (a->pen.italic != b->pen.italic)) - return 1; - if((attrs & VTERM_ATTR_BLINK_MASK) && (a->pen.blink != b->pen.blink)) - return 1; - if((attrs & VTERM_ATTR_REVERSE_MASK) && (a->pen.reverse != b->pen.reverse)) - return 1; - if((attrs & VTERM_ATTR_CONCEAL_MASK) && (a->pen.conceal != b->pen.conceal)) - return 1; - if((attrs & VTERM_ATTR_STRIKE_MASK) && (a->pen.strike != b->pen.strike)) - return 1; - if((attrs & VTERM_ATTR_FONT_MASK) && (a->pen.font != b->pen.font)) - return 1; - if((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_is_equal(&a->pen.fg, &b->pen.fg)) - return 1; - if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg)) - return 1; - if((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small)) - return 1; - if((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline)) - return 1; - if((attrs & VTERM_ATTR_URI_MASK) && (a->pen.uri != b->pen.uri)) - return 1; - - return 0; -} - -int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs) -{ - ScreenCell *target = getcell(screen, pos.row, pos.col); - - // TODO: bounds check - extent->start_row = pos.row; - extent->end_row = pos.row + 1; - - if(extent->start_col < 0) - extent->start_col = 0; - if(extent->end_col < 0) - extent->end_col = screen->cols; - - int col; - - for(col = pos.col - 1; col >= extent->start_col; col--) - if(attrs_differ(attrs, target, getcell(screen, pos.row, col))) - break; - extent->start_col = col + 1; - - for(col = pos.col + 1; col < extent->end_col; col++) - if(attrs_differ(attrs, target, getcell(screen, pos.row, col))) - break; - extent->end_col = col - 1; - - return 1; -} - -void vterm_screen_convert_color_to_rgb(const VTermScreen *screen, VTermColor *col) -{ - vterm_state_convert_color_to_rgb(screen->state, col); -} - -static void reset_default_colours(VTermScreen *screen, ScreenCell *buffer) -{ - for(int row = 0; row <= screen->rows - 1; row++) - for(int col = 0; col <= screen->cols - 1; col++) { - ScreenCell *cell = &buffer[row * screen->cols + col]; - if(VTERM_COLOR_IS_DEFAULT_FG(&cell->pen.fg)) - cell->pen.fg = screen->pen.fg; - if(VTERM_COLOR_IS_DEFAULT_BG(&cell->pen.bg)) - cell->pen.bg = screen->pen.bg; - } -} - -void vterm_screen_set_default_colors(VTermScreen *screen, const VTermColor *default_fg, const VTermColor *default_bg) -{ - vterm_state_set_default_colors(screen->state, default_fg, default_bg); - - if(default_fg && VTERM_COLOR_IS_DEFAULT_FG(&screen->pen.fg)) { - screen->pen.fg = *default_fg; - screen->pen.fg.type = (screen->pen.fg.type & ~VTERM_COLOR_DEFAULT_MASK) - | VTERM_COLOR_DEFAULT_FG; - } - - if(default_bg && VTERM_COLOR_IS_DEFAULT_BG(&screen->pen.bg)) { - screen->pen.bg = *default_bg; - screen->pen.bg.type = (screen->pen.bg.type & ~VTERM_COLOR_DEFAULT_MASK) - | VTERM_COLOR_DEFAULT_BG; - } - - reset_default_colours(screen, screen->buffers[0]); - if(screen->buffers[1]) - reset_default_colours(screen, screen->buffers[1]); -} diff --git a/src/vterm/state.c b/src/vterm/state.c deleted file mode 100644 index d546672e67..0000000000 --- a/src/vterm/state.c +++ /dev/null @@ -1,2281 +0,0 @@ -#include "vterm_internal.h" - -#include <stdio.h> -#include <string.h> - -#include "nvim/grid.h" -#include "nvim/mbyte.h" - -#define strneq(a,b,n) (strncmp(a,b,n)==0) - -#if defined(DEBUG) && DEBUG > 1 -# define DEBUG_GLYPH_COMBINE -#endif - -/* Some convenient wrappers to make callback functions easier */ - -static void putglyph(VTermState *state, const schar_T schar, int width, VTermPos pos) -{ - VTermGlyphInfo info = { - .schar = schar, - .width = width, - .protected_cell = state->protected_cell, - .dwl = state->lineinfo[pos.row].doublewidth, - .dhl = state->lineinfo[pos.row].doubleheight, - }; - - if(state->callbacks && state->callbacks->putglyph) - if((*state->callbacks->putglyph)(&info, pos, state->cbdata)) - return; - - DEBUG_LOG("libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row); -} - -static void updatecursor(VTermState *state, VTermPos *oldpos, int cancel_phantom) -{ - if(state->pos.col == oldpos->col && state->pos.row == oldpos->row) - return; - - if(cancel_phantom) - state->at_phantom = 0; - - if(state->callbacks && state->callbacks->movecursor) - if((*state->callbacks->movecursor)(state->pos, *oldpos, state->mode.cursor_visible, state->cbdata)) - return; -} - -static void erase(VTermState *state, VTermRect rect, int selective) -{ - if(rect.end_col == state->cols) { - /* If we're erasing the final cells of any lines, cancel the continuation - * marker on the subsequent line - */ - for(int row = rect.start_row + 1; row < rect.end_row + 1 && row < state->rows; row++) - state->lineinfo[row].continuation = 0; - } - - if(state->callbacks && state->callbacks->erase) - if((*state->callbacks->erase)(rect, selective, state->cbdata)) - return; -} - -static VTermState *vterm_state_new(VTerm *vt) -{ - VTermState *state = vterm_allocator_malloc(vt, sizeof(VTermState)); - - state->vt = vt; - - state->rows = vt->rows; - state->cols = vt->cols; - - state->mouse_col = 0; - state->mouse_row = 0; - state->mouse_buttons = 0; - - state->mouse_protocol = MOUSE_X10; - - state->callbacks = NULL; - state->cbdata = NULL; - - state->selection.callbacks = NULL; - state->selection.user = NULL; - state->selection.buffer = NULL; - - vterm_state_newpen(state); - - state->bold_is_highbright = 0; - - state->combine_pos.row = -1; - - state->tabstops = vterm_allocator_malloc(state->vt, (state->cols + 7) / 8); - - state->lineinfos[BUFIDX_PRIMARY] = vterm_allocator_malloc(state->vt, state->rows * sizeof(VTermLineInfo)); - /* TODO: Make an 'enable' function */ - state->lineinfos[BUFIDX_ALTSCREEN] = vterm_allocator_malloc(state->vt, state->rows * sizeof(VTermLineInfo)); - state->lineinfo = state->lineinfos[BUFIDX_PRIMARY]; - - state->encoding_utf8.enc = vterm_lookup_encoding(ENC_UTF8, 'u'); - if(*state->encoding_utf8.enc->init) - (*state->encoding_utf8.enc->init)(state->encoding_utf8.enc, state->encoding_utf8.data); - - return state; -} - -INTERNAL void vterm_state_free(VTermState *state) -{ - vterm_allocator_free(state->vt, state->tabstops); - vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_PRIMARY]); - if(state->lineinfos[BUFIDX_ALTSCREEN]) - vterm_allocator_free(state->vt, state->lineinfos[BUFIDX_ALTSCREEN]); - vterm_allocator_free(state->vt, state); -} - -static void scroll(VTermState *state, VTermRect rect, int downward, int rightward) -{ - if(!downward && !rightward) - return; - - int rows = rect.end_row - rect.start_row; - if(downward > rows) - downward = rows; - else if(downward < -rows) - downward = -rows; - - int cols = rect.end_col - rect.start_col; - if(rightward > cols) - rightward = cols; - else if(rightward < -cols) - rightward = -cols; - - // Update lineinfo if full line - if(rect.start_col == 0 && rect.end_col == state->cols && rightward == 0) { - int height = rect.end_row - rect.start_row - abs(downward); - - if(downward > 0) { - memmove(state->lineinfo + rect.start_row, - state->lineinfo + rect.start_row + downward, - height * sizeof(state->lineinfo[0])); - for(int row = rect.end_row - downward; row < rect.end_row; row++) - state->lineinfo[row] = (VTermLineInfo){ 0 }; - } - else { - memmove(state->lineinfo + rect.start_row - downward, - state->lineinfo + rect.start_row, - height * sizeof(state->lineinfo[0])); - for(int row = rect.start_row; row < rect.start_row - downward; row++) - state->lineinfo[row] = (VTermLineInfo){ 0 }; - } - } - - if(state->callbacks && state->callbacks->scrollrect) - if((*state->callbacks->scrollrect)(rect, downward, rightward, state->cbdata)) - return; - - if(state->callbacks) - vterm_scroll_rect(rect, downward, rightward, - state->callbacks->moverect, state->callbacks->erase, state->cbdata); -} - -static void linefeed(VTermState *state) -{ - if(state->pos.row == SCROLLREGION_BOTTOM(state) - 1) { - VTermRect rect = { - .start_row = state->scrollregion_top, - .end_row = SCROLLREGION_BOTTOM(state), - .start_col = SCROLLREGION_LEFT(state), - .end_col = SCROLLREGION_RIGHT(state), - }; - - scroll(state, rect, 1, 0); - } - else if(state->pos.row < state->rows-1) - state->pos.row++; -} - -static void set_col_tabstop(VTermState *state, int col) -{ - unsigned char mask = 1 << (col & 7); - state->tabstops[col >> 3] |= mask; -} - -static void clear_col_tabstop(VTermState *state, int col) -{ - unsigned char mask = 1 << (col & 7); - state->tabstops[col >> 3] &= ~mask; -} - -static int is_col_tabstop(VTermState *state, int col) -{ - unsigned char mask = 1 << (col & 7); - return state->tabstops[col >> 3] & mask; -} - -static int is_cursor_in_scrollregion(const VTermState *state) -{ - if(state->pos.row < state->scrollregion_top || - state->pos.row >= SCROLLREGION_BOTTOM(state)) - return 0; - if(state->pos.col < SCROLLREGION_LEFT(state) || - state->pos.col >= SCROLLREGION_RIGHT(state)) - return 0; - - return 1; -} - -static void tab(VTermState *state, int count, int direction) -{ - while(count > 0) { - if(direction > 0) { - if(state->pos.col >= THISROWWIDTH(state)-1) - return; - - state->pos.col++; - } - else if(direction < 0) { - if(state->pos.col < 1) - return; - - state->pos.col--; - } - - if(is_col_tabstop(state, state->pos.col)) - count--; - } -} - -#define NO_FORCE 0 -#define FORCE 1 - -#define DWL_OFF 0 -#define DWL_ON 1 - -#define DHL_OFF 0 -#define DHL_TOP 1 -#define DHL_BOTTOM 2 - -static void set_lineinfo(VTermState *state, int row, int force, int dwl, int dhl) -{ - VTermLineInfo info = state->lineinfo[row]; - - if(dwl == DWL_OFF) - info.doublewidth = DWL_OFF; - else if(dwl == DWL_ON) - info.doublewidth = DWL_ON; - // else -1 to ignore - - if(dhl == DHL_OFF) - info.doubleheight = DHL_OFF; - else if(dhl == DHL_TOP) - info.doubleheight = DHL_TOP; - else if(dhl == DHL_BOTTOM) - info.doubleheight = DHL_BOTTOM; - - if((state->callbacks && - state->callbacks->setlineinfo && - (*state->callbacks->setlineinfo)(row, &info, state->lineinfo + row, state->cbdata)) - || force) - state->lineinfo[row] = info; -} - -static int on_text(const char bytes[], size_t len, void *user) -{ - VTermState *state = user; - - VTermPos oldpos = state->pos; - - uint32_t *codepoints = (uint32_t *)(state->vt->tmpbuffer); - size_t maxpoints = (state->vt->tmpbuffer_len) / sizeof(uint32_t); - - int npoints = 0; - size_t eaten = 0; - - VTermEncodingInstance *encoding = - state->gsingle_set ? &state->encoding[state->gsingle_set] : - !(bytes[eaten] & 0x80) ? &state->encoding[state->gl_set] : - state->vt->mode.utf8 ? &state->encoding_utf8 : - &state->encoding[state->gr_set]; - - (*encoding->enc->decode)(encoding->enc, encoding->data, - codepoints, &npoints, state->gsingle_set ? 1 : maxpoints, - bytes, &eaten, len); - - /* There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet - * for even a single codepoint - */ - if(!npoints) - return eaten; - - if(state->gsingle_set && npoints) - state->gsingle_set = 0; - - int i = 0; - GraphemeState grapheme_state = GRAPHEME_STATE_INIT; - size_t grapheme_len = 0; - bool recombine = false; - - /* See if the cursor has moved since */ - if(state->pos.row == state->combine_pos.row && state->pos.col == state->combine_pos.col + state->combine_width) { - /* This is a combining char. that needs to be merged with the previous - * glyph output */ - if(utf_iscomposing(state->grapheme_last, codepoints[i], &state->grapheme_state)) { - /* Find where we need to append these combining chars */ - grapheme_len = state->grapheme_len; - grapheme_state = state->grapheme_state; - state->pos.col = state->combine_pos.col; - recombine = true; - } else { - DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n"); - } - } - - while(i < npoints) { - // Try to find combining characters following this - do { - if (grapheme_len < sizeof(state->grapheme_buf) - 4) { - grapheme_len += utf_char2bytes(codepoints[i], state->grapheme_buf + grapheme_len); - } - i++; - } while(i < npoints && utf_iscomposing(codepoints[i-1], codepoints[i], &grapheme_state)); - - int width = utf_ptr2cells_len(state->grapheme_buf, grapheme_len); - - if(state->at_phantom || state->pos.col + width > THISROWWIDTH(state)) { - linefeed(state); - state->pos.col = 0; - state->at_phantom = 0; - state->lineinfo[state->pos.row].continuation = 1; - } - - if(state->mode.insert && !recombine) { - /* TODO: This will be a little inefficient for large bodies of text, as - * it'll have to 'ICH' effectively before every glyph. We should scan - * ahead and ICH as many times as required - */ - VTermRect rect = { - .start_row = state->pos.row, - .end_row = state->pos.row + 1, - .start_col = state->pos.col, - .end_col = THISROWWIDTH(state), - }; - scroll(state, rect, 0, -1); - } - - schar_T sc = schar_from_buf(state->grapheme_buf, grapheme_len); - putglyph(state, sc, width, state->pos); - - if(i == npoints) { - /* End of the buffer. Save the chars in case we have to combine with - * more on the next call */ - state->grapheme_len = grapheme_len; - state->grapheme_last = codepoints[i-1]; - state->grapheme_state = grapheme_state; - state->combine_width = width; - state->combine_pos = state->pos; - } else { - grapheme_len = 0; - recombine = false; - } - - if(state->pos.col + width >= THISROWWIDTH(state)) { - if(state->mode.autowrap) - state->at_phantom = 1; - } - else { - state->pos.col += width; - } - } - - updatecursor(state, &oldpos, 0); - -#ifdef DEBUG - if(state->pos.row < 0 || state->pos.row >= state->rows || - state->pos.col < 0 || state->pos.col >= state->cols) { - fprintf(stderr, "Position out of bounds after text: (%d,%d)\n", - state->pos.row, state->pos.col); - abort(); - } -#endif - - return eaten; -} - -static int on_control(unsigned char control, void *user) -{ - VTermState *state = user; - - VTermPos oldpos = state->pos; - - switch(control) { - case 0x07: // BEL - ECMA-48 8.3.3 - if(state->callbacks && state->callbacks->bell) - (*state->callbacks->bell)(state->cbdata); - break; - - case 0x08: // BS - ECMA-48 8.3.5 - if(state->pos.col > 0) - state->pos.col--; - break; - - case 0x09: // HT - ECMA-48 8.3.60 - tab(state, 1, +1); - break; - - case 0x0a: // LF - ECMA-48 8.3.74 - case 0x0b: // VT - case 0x0c: // FF - linefeed(state); - if(state->mode.newline) - state->pos.col = 0; - break; - - case 0x0d: // CR - ECMA-48 8.3.15 - state->pos.col = 0; - break; - - case 0x0e: // LS1 - ECMA-48 8.3.76 - state->gl_set = 1; - break; - - case 0x0f: // LS0 - ECMA-48 8.3.75 - state->gl_set = 0; - break; - - case 0x84: // IND - DEPRECATED but implemented for completeness - linefeed(state); - break; - - case 0x85: // NEL - ECMA-48 8.3.86 - linefeed(state); - state->pos.col = 0; - break; - - case 0x88: // HTS - ECMA-48 8.3.62 - set_col_tabstop(state, state->pos.col); - break; - - case 0x8d: // RI - ECMA-48 8.3.104 - if(state->pos.row == state->scrollregion_top) { - VTermRect rect = { - .start_row = state->scrollregion_top, - .end_row = SCROLLREGION_BOTTOM(state), - .start_col = SCROLLREGION_LEFT(state), - .end_col = SCROLLREGION_RIGHT(state), - }; - - scroll(state, rect, -1, 0); - } - else if(state->pos.row > 0) - state->pos.row--; - break; - - case 0x8e: // SS2 - ECMA-48 8.3.141 - state->gsingle_set = 2; - break; - - case 0x8f: // SS3 - ECMA-48 8.3.142 - state->gsingle_set = 3; - break; - - default: - if(state->fallbacks && state->fallbacks->control) - if((*state->fallbacks->control)(control, state->fbdata)) - return 1; - - return 0; - } - - updatecursor(state, &oldpos, 1); - -#ifdef DEBUG - if(state->pos.row < 0 || state->pos.row >= state->rows || - state->pos.col < 0 || state->pos.col >= state->cols) { - fprintf(stderr, "Position out of bounds after Ctrl %02x: (%d,%d)\n", - control, state->pos.row, state->pos.col); - abort(); - } -#endif - - return 1; -} - -static int settermprop_bool(VTermState *state, VTermProp prop, int v) -{ - VTermValue val = { .boolean = v }; - return vterm_state_set_termprop(state, prop, &val); -} - -static int settermprop_int(VTermState *state, VTermProp prop, int v) -{ - VTermValue val = { .number = v }; - return vterm_state_set_termprop(state, prop, &val); -} - -static int settermprop_string(VTermState *state, VTermProp prop, VTermStringFragment frag) -{ - VTermValue val = { .string = frag }; - return vterm_state_set_termprop(state, prop, &val); -} - -static void savecursor(VTermState *state, int save) -{ - if(save) { - state->saved.pos = state->pos; - state->saved.mode.cursor_visible = state->mode.cursor_visible; - state->saved.mode.cursor_blink = state->mode.cursor_blink; - state->saved.mode.cursor_shape = state->mode.cursor_shape; - - vterm_state_savepen(state, 1); - } - else { - VTermPos oldpos = state->pos; - - state->pos = state->saved.pos; - - settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, state->saved.mode.cursor_visible); - settermprop_bool(state, VTERM_PROP_CURSORBLINK, state->saved.mode.cursor_blink); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, state->saved.mode.cursor_shape); - - vterm_state_savepen(state, 0); - - updatecursor(state, &oldpos, 1); - } -} - -static int on_escape(const char *bytes, size_t len, void *user) -{ - VTermState *state = user; - - /* Easier to decode this from the first byte, even though the final - * byte terminates it - */ - switch(bytes[0]) { - case ' ': - if(len != 2) - return 0; - - switch(bytes[1]) { - case 'F': // S7C1T - state->vt->mode.ctrl8bit = 0; - break; - - case 'G': // S8C1T - state->vt->mode.ctrl8bit = 1; - break; - - default: - return 0; - } - return 2; - - case '#': - if(len != 2) - return 0; - - switch(bytes[1]) { - case '3': // DECDHL top - if(state->mode.leftrightmargin) - break; - set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_TOP); - break; - - case '4': // DECDHL bottom - if(state->mode.leftrightmargin) - break; - set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_BOTTOM); - break; - - case '5': // DECSWL - if(state->mode.leftrightmargin) - break; - set_lineinfo(state, state->pos.row, NO_FORCE, DWL_OFF, DHL_OFF); - break; - - case '6': // DECDWL - if(state->mode.leftrightmargin) - break; - set_lineinfo(state, state->pos.row, NO_FORCE, DWL_ON, DHL_OFF); - break; - - case '8': // DECALN - { - VTermPos pos; - schar_T E = schar_from_ascii('E'); // E - for(pos.row = 0; pos.row < state->rows; pos.row++) - for(pos.col = 0; pos.col < ROWWIDTH(state, pos.row); pos.col++) - putglyph(state, E, 1, pos); - break; - } - - default: - return 0; - } - return 2; - - case '(': case ')': case '*': case '+': // SCS - if(len != 2) - return 0; - - { - int setnum = bytes[0] - 0x28; - VTermEncoding *newenc = vterm_lookup_encoding(ENC_SINGLE_94, bytes[1]); - - if(newenc) { - state->encoding[setnum].enc = newenc; - - if(newenc->init) - (*newenc->init)(newenc, state->encoding[setnum].data); - } - } - - return 2; - - case '7': // DECSC - savecursor(state, 1); - return 1; - - case '8': // DECRC - savecursor(state, 0); - return 1; - - case '<': // Ignored by VT100. Used in VT52 mode to switch up to VT100 - return 1; - - case '=': // DECKPAM - state->mode.keypad = 1; - return 1; - - case '>': // DECKPNM - state->mode.keypad = 0; - return 1; - - case 'c': // RIS - ECMA-48 8.3.105 - { - VTermPos oldpos = state->pos; - vterm_state_reset(state, 1); - if(state->callbacks && state->callbacks->movecursor) - (*state->callbacks->movecursor)(state->pos, oldpos, state->mode.cursor_visible, state->cbdata); - return 1; - } - - case 'n': // LS2 - ECMA-48 8.3.78 - state->gl_set = 2; - return 1; - - case 'o': // LS3 - ECMA-48 8.3.80 - state->gl_set = 3; - return 1; - - case '~': // LS1R - ECMA-48 8.3.77 - state->gr_set = 1; - return 1; - - case '}': // LS2R - ECMA-48 8.3.79 - state->gr_set = 2; - return 1; - - case '|': // LS3R - ECMA-48 8.3.81 - state->gr_set = 3; - return 1; - - default: - return 0; - } -} - -static void set_mode(VTermState *state, int num, int val) -{ - switch(num) { - case 4: // IRM - ECMA-48 7.2.10 - state->mode.insert = val; - break; - - case 20: // LNM - ANSI X3.4-1977 - state->mode.newline = val; - break; - - default: - DEBUG_LOG("libvterm: Unknown mode %d\n", num); - return; - } -} - -static void set_dec_mode(VTermState *state, int num, int val) -{ - switch(num) { - case 1: - state->mode.cursor = val; - break; - - case 5: // DECSCNM - screen mode - settermprop_bool(state, VTERM_PROP_REVERSE, val); - break; - - case 6: // DECOM - origin mode - { - VTermPos oldpos = state->pos; - state->mode.origin = val; - state->pos.row = state->mode.origin ? state->scrollregion_top : 0; - state->pos.col = state->mode.origin ? SCROLLREGION_LEFT(state) : 0; - updatecursor(state, &oldpos, 1); - } - break; - - case 7: - state->mode.autowrap = val; - break; - - case 12: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, val); - break; - - case 25: - settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, val); - break; - - case 69: // DECVSSM - vertical split screen mode - // DECLRMM - left/right margin mode - state->mode.leftrightmargin = val; - if(val) { - // Setting DECVSSM must clear doublewidth/doubleheight state of every line - for(int row = 0; row < state->rows; row++) - set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF); - } - - break; - - case 1000: - case 1002: - case 1003: - settermprop_int(state, VTERM_PROP_MOUSE, - !val ? VTERM_PROP_MOUSE_NONE : - (num == 1000) ? VTERM_PROP_MOUSE_CLICK : - (num == 1002) ? VTERM_PROP_MOUSE_DRAG : - VTERM_PROP_MOUSE_MOVE); - break; - - case 1004: - settermprop_bool(state, VTERM_PROP_FOCUSREPORT, val); - state->mode.report_focus = val; - break; - - case 1005: - state->mouse_protocol = val ? MOUSE_UTF8 : MOUSE_X10; - break; - - case 1006: - state->mouse_protocol = val ? MOUSE_SGR : MOUSE_X10; - break; - - case 1015: - state->mouse_protocol = val ? MOUSE_RXVT : MOUSE_X10; - break; - - case 1047: - settermprop_bool(state, VTERM_PROP_ALTSCREEN, val); - break; - - case 1048: - savecursor(state, val); - break; - - case 1049: - settermprop_bool(state, VTERM_PROP_ALTSCREEN, val); - savecursor(state, val); - break; - - case 2004: - state->mode.bracketpaste = val; - break; - - default: - DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num); - return; - } -} - -static void request_dec_mode(VTermState *state, int num) -{ - int reply; - - switch(num) { - case 1: - reply = state->mode.cursor; - break; - - case 5: - reply = state->mode.screen; - break; - - case 6: - reply = state->mode.origin; - break; - - case 7: - reply = state->mode.autowrap; - break; - - case 12: - reply = state->mode.cursor_blink; - break; - - case 25: - reply = state->mode.cursor_visible; - break; - - case 69: - reply = state->mode.leftrightmargin; - break; - - case 1000: - reply = state->mouse_flags == MOUSE_WANT_CLICK; - break; - - case 1002: - reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_DRAG); - break; - - case 1003: - reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_MOVE); - break; - - case 1004: - reply = state->mode.report_focus; - break; - - case 1005: - reply = state->mouse_protocol == MOUSE_UTF8; - break; - - case 1006: - reply = state->mouse_protocol == MOUSE_SGR; - break; - - case 1015: - reply = state->mouse_protocol == MOUSE_RXVT; - break; - - case 1047: - reply = state->mode.alt_screen; - break; - - case 2004: - reply = state->mode.bracketpaste; - break; - - default: - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0); - return; - } - - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, reply ? 1 : 2); -} - -static void request_version_string(VTermState *state) -{ - vterm_push_output_sprintf_str(state->vt, C1_DCS, true, ">|libvterm(%d.%d)", - VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR); -} - -static int on_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user) -{ - VTermState *state = user; - int leader_byte = 0; - int intermed_byte = 0; - int cancel_phantom = 1; - - if(leader && leader[0]) { - if(leader[1]) // longer than 1 char - return 0; - - switch(leader[0]) { - case '?': - case '>': - leader_byte = leader[0]; - break; - default: - return 0; - } - } - - if(intermed && intermed[0]) { - if(intermed[1]) // longer than 1 char - return 0; - - switch(intermed[0]) { - case ' ': - case '!': - case '"': - case '$': - case '\'': - intermed_byte = intermed[0]; - break; - default: - return 0; - } - } - - VTermPos oldpos = state->pos; - - // Some temporaries for later code - int count, val; - int row, col; - VTermRect rect; - int selective; - -#define LBOUND(v,min) if((v) < (min)) (v) = (min) -#define UBOUND(v,max) if((v) > (max)) (v) = (max) - -#define LEADER(l,b) ((l << 8) | b) -#define INTERMED(i,b) ((i << 16) | b) - - switch(intermed_byte << 16 | leader_byte << 8 | command) { - case 0x40: // ICH - ECMA-48 8.3.64 - count = CSI_ARG_COUNT(args[0]); - - if(!is_cursor_in_scrollregion(state)) - break; - - rect.start_row = state->pos.row; - rect.end_row = state->pos.row + 1; - rect.start_col = state->pos.col; - if(state->mode.leftrightmargin) - rect.end_col = SCROLLREGION_RIGHT(state); - else - rect.end_col = THISROWWIDTH(state); - - scroll(state, rect, 0, -count); - - break; - - case 0x41: // CUU - ECMA-48 8.3.22 - count = CSI_ARG_COUNT(args[0]); - state->pos.row -= count; - state->at_phantom = 0; - break; - - case 0x42: // CUD - ECMA-48 8.3.19 - count = CSI_ARG_COUNT(args[0]); - state->pos.row += count; - state->at_phantom = 0; - break; - - case 0x43: // CUF - ECMA-48 8.3.20 - count = CSI_ARG_COUNT(args[0]); - state->pos.col += count; - state->at_phantom = 0; - break; - - case 0x44: // CUB - ECMA-48 8.3.18 - count = CSI_ARG_COUNT(args[0]); - state->pos.col -= count; - state->at_phantom = 0; - break; - - case 0x45: // CNL - ECMA-48 8.3.12 - count = CSI_ARG_COUNT(args[0]); - state->pos.col = 0; - state->pos.row += count; - state->at_phantom = 0; - break; - - case 0x46: // CPL - ECMA-48 8.3.13 - count = CSI_ARG_COUNT(args[0]); - state->pos.col = 0; - state->pos.row -= count; - state->at_phantom = 0; - break; - - case 0x47: // CHA - ECMA-48 8.3.9 - val = CSI_ARG_OR(args[0], 1); - state->pos.col = val-1; - state->at_phantom = 0; - break; - - case 0x48: // CUP - ECMA-48 8.3.21 - row = CSI_ARG_OR(args[0], 1); - col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); - // zero-based - state->pos.row = row-1; - state->pos.col = col-1; - if(state->mode.origin) { - state->pos.row += state->scrollregion_top; - state->pos.col += SCROLLREGION_LEFT(state); - } - state->at_phantom = 0; - break; - - case 0x49: // CHT - ECMA-48 8.3.10 - count = CSI_ARG_COUNT(args[0]); - tab(state, count, +1); - break; - - case 0x4a: // ED - ECMA-48 8.3.39 - case LEADER('?', 0x4a): // DECSED - Selective Erase in Display - selective = (leader_byte == '?'); - switch(CSI_ARG(args[0])) { - case CSI_ARG_MISSING: - case 0: - rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1; - rect.start_col = state->pos.col; rect.end_col = state->cols; - if(rect.end_col > rect.start_col) - erase(state, rect, selective); - - rect.start_row = state->pos.row + 1; rect.end_row = state->rows; - rect.start_col = 0; - for(int row_ = rect.start_row; row_ < rect.end_row; row_++) - set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); - if(rect.end_row > rect.start_row) - erase(state, rect, selective); - break; - - case 1: - rect.start_row = 0; rect.end_row = state->pos.row; - rect.start_col = 0; rect.end_col = state->cols; - for(int row_ = rect.start_row; row_ < rect.end_row; row_++) - set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); - if(rect.end_col > rect.start_col) - erase(state, rect, selective); - - rect.start_row = state->pos.row; rect.end_row = state->pos.row + 1; - rect.end_col = state->pos.col + 1; - if(rect.end_row > rect.start_row) - erase(state, rect, selective); - break; - - case 2: - rect.start_row = 0; rect.end_row = state->rows; - rect.start_col = 0; rect.end_col = state->cols; - for(int row_ = rect.start_row; row_ < rect.end_row; row_++) - set_lineinfo(state, row_, FORCE, DWL_OFF, DHL_OFF); - erase(state, rect, selective); - break; - - case 3: - if(state->callbacks && state->callbacks->sb_clear) - if((*state->callbacks->sb_clear)(state->cbdata)) - return 1; - break; - } - break; - - case 0x4b: // EL - ECMA-48 8.3.41 - case LEADER('?', 0x4b): // DECSEL - Selective Erase in Line - selective = (leader_byte == '?'); - rect.start_row = state->pos.row; - rect.end_row = state->pos.row + 1; - - switch(CSI_ARG(args[0])) { - case CSI_ARG_MISSING: - case 0: - rect.start_col = state->pos.col; rect.end_col = THISROWWIDTH(state); break; - case 1: - rect.start_col = 0; rect.end_col = state->pos.col + 1; break; - case 2: - rect.start_col = 0; rect.end_col = THISROWWIDTH(state); break; - default: - return 0; - } - - if(rect.end_col > rect.start_col) - erase(state, rect, selective); - - break; - - case 0x4c: // IL - ECMA-48 8.3.67 - count = CSI_ARG_COUNT(args[0]); - - if(!is_cursor_in_scrollregion(state)) - break; - - rect.start_row = state->pos.row; - rect.end_row = SCROLLREGION_BOTTOM(state); - rect.start_col = SCROLLREGION_LEFT(state); - rect.end_col = SCROLLREGION_RIGHT(state); - - scroll(state, rect, -count, 0); - - break; - - case 0x4d: // DL - ECMA-48 8.3.32 - count = CSI_ARG_COUNT(args[0]); - - if(!is_cursor_in_scrollregion(state)) - break; - - rect.start_row = state->pos.row; - rect.end_row = SCROLLREGION_BOTTOM(state); - rect.start_col = SCROLLREGION_LEFT(state); - rect.end_col = SCROLLREGION_RIGHT(state); - - scroll(state, rect, count, 0); - - break; - - case 0x50: // DCH - ECMA-48 8.3.26 - count = CSI_ARG_COUNT(args[0]); - - if(!is_cursor_in_scrollregion(state)) - break; - - rect.start_row = state->pos.row; - rect.end_row = state->pos.row + 1; - rect.start_col = state->pos.col; - if(state->mode.leftrightmargin) - rect.end_col = SCROLLREGION_RIGHT(state); - else - rect.end_col = THISROWWIDTH(state); - - scroll(state, rect, 0, count); - - break; - - case 0x53: // SU - ECMA-48 8.3.147 - count = CSI_ARG_COUNT(args[0]); - - rect.start_row = state->scrollregion_top; - rect.end_row = SCROLLREGION_BOTTOM(state); - rect.start_col = SCROLLREGION_LEFT(state); - rect.end_col = SCROLLREGION_RIGHT(state); - - scroll(state, rect, count, 0); - - break; - - case 0x54: // SD - ECMA-48 8.3.113 - count = CSI_ARG_COUNT(args[0]); - - rect.start_row = state->scrollregion_top; - rect.end_row = SCROLLREGION_BOTTOM(state); - rect.start_col = SCROLLREGION_LEFT(state); - rect.end_col = SCROLLREGION_RIGHT(state); - - scroll(state, rect, -count, 0); - - break; - - case 0x58: // ECH - ECMA-48 8.3.38 - count = CSI_ARG_COUNT(args[0]); - - rect.start_row = state->pos.row; - rect.end_row = state->pos.row + 1; - rect.start_col = state->pos.col; - rect.end_col = state->pos.col + count; - UBOUND(rect.end_col, THISROWWIDTH(state)); - - erase(state, rect, 0); - break; - - case 0x5a: // CBT - ECMA-48 8.3.7 - count = CSI_ARG_COUNT(args[0]); - tab(state, count, -1); - break; - - case 0x60: // HPA - ECMA-48 8.3.57 - col = CSI_ARG_OR(args[0], 1); - state->pos.col = col-1; - state->at_phantom = 0; - break; - - case 0x61: // HPR - ECMA-48 8.3.59 - count = CSI_ARG_COUNT(args[0]); - state->pos.col += count; - state->at_phantom = 0; - break; - - case 0x62: { // REP - ECMA-48 8.3.103 - const int row_width = THISROWWIDTH(state); - count = CSI_ARG_COUNT(args[0]); - col = state->pos.col + count; - UBOUND(col, row_width); - schar_T sc = schar_from_buf(state->grapheme_buf, state->grapheme_len); - while (state->pos.col < col) { - putglyph(state, sc, state->combine_width, state->pos); - state->pos.col += state->combine_width; - } - if (state->pos.col + state->combine_width >= row_width) { - if (state->mode.autowrap) { - state->at_phantom = 1; - cancel_phantom = 0; - } - } - break; - } - - case 0x63: // DA - ECMA-48 8.3.24 - val = CSI_ARG_OR(args[0], 0); - if(val == 0) - // DEC VT100 response - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?1;2c"); - break; - - case LEADER('>', 0x63): // DEC secondary Device Attributes - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, ">%d;%d;%dc", 0, 100, 0); - break; - - case 0x64: // VPA - ECMA-48 8.3.158 - row = CSI_ARG_OR(args[0], 1); - state->pos.row = row-1; - if(state->mode.origin) - state->pos.row += state->scrollregion_top; - state->at_phantom = 0; - break; - - case 0x65: // VPR - ECMA-48 8.3.160 - count = CSI_ARG_COUNT(args[0]); - state->pos.row += count; - state->at_phantom = 0; - break; - - case 0x66: // HVP - ECMA-48 8.3.63 - row = CSI_ARG_OR(args[0], 1); - col = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? 1 : CSI_ARG(args[1]); - // zero-based - state->pos.row = row-1; - state->pos.col = col-1; - if(state->mode.origin) { - state->pos.row += state->scrollregion_top; - state->pos.col += SCROLLREGION_LEFT(state); - } - state->at_phantom = 0; - break; - - case 0x67: // TBC - ECMA-48 8.3.154 - val = CSI_ARG_OR(args[0], 0); - - switch(val) { - case 0: - clear_col_tabstop(state, state->pos.col); - break; - case 3: - case 5: - for(col = 0; col < state->cols; col++) - clear_col_tabstop(state, col); - break; - case 1: - case 2: - case 4: - break; - /* TODO: 1, 2 and 4 aren't meaningful yet without line tab stops */ - default: - return 0; - } - break; - - case 0x68: // SM - ECMA-48 8.3.125 - if(!CSI_ARG_IS_MISSING(args[0])) - set_mode(state, CSI_ARG(args[0]), 1); - break; - - case LEADER('?', 0x68): // DEC private mode set - for(int i = 0; i < argcount; i++) { - if(!CSI_ARG_IS_MISSING(args[i])) - set_dec_mode(state, CSI_ARG(args[i]), 1); - } - break; - - case 0x6a: // HPB - ECMA-48 8.3.58 - count = CSI_ARG_COUNT(args[0]); - state->pos.col -= count; - state->at_phantom = 0; - break; - - case 0x6b: // VPB - ECMA-48 8.3.159 - count = CSI_ARG_COUNT(args[0]); - state->pos.row -= count; - state->at_phantom = 0; - break; - - case 0x6c: // RM - ECMA-48 8.3.106 - if(!CSI_ARG_IS_MISSING(args[0])) - set_mode(state, CSI_ARG(args[0]), 0); - break; - - case LEADER('?', 0x6c): // DEC private mode reset - for(int i = 0; i < argcount; i++) { - if(!CSI_ARG_IS_MISSING(args[i])) - set_dec_mode(state, CSI_ARG(args[i]), 0); - } - break; - - case 0x6d: // SGR - ECMA-48 8.3.117 - vterm_state_setpen(state, args, argcount); - break; - - case LEADER('?', 0x6d): // DECSGR - /* No actual DEC terminal recognised these, but some printers did. These - * are alternative ways to request subscript/superscript/off - */ - for(int argi = 0; argi < argcount; argi++) { - long arg; - switch(arg = CSI_ARG(args[argi])) { - case 4: // Superscript on - arg = 73; - vterm_state_setpen(state, &arg, 1); - break; - case 5: // Subscript on - arg = 74; - vterm_state_setpen(state, &arg, 1); - break; - case 24: // Super+subscript off - arg = 75; - vterm_state_setpen(state, &arg, 1); - break; - } - } - break; - - case 0x6e: // DSR - ECMA-48 8.3.35 - case LEADER('?', 0x6e): // DECDSR - val = CSI_ARG_OR(args[0], 0); - - { - char *qmark = (leader_byte == '?') ? "?" : ""; - - switch(val) { - case 0: case 1: case 2: case 3: case 4: - // ignore - these are replies - break; - case 5: - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s0n", qmark); - break; - case 6: // CPR - cursor position report - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%s%d;%dR", qmark, state->pos.row + 1, state->pos.col + 1); - break; - } - } - break; - - - case INTERMED('!', 0x70): // DECSTR - DEC soft terminal reset - vterm_state_reset(state, 0); - break; - - case LEADER('?', INTERMED('$', 0x70)): - request_dec_mode(state, CSI_ARG(args[0])); - break; - - case LEADER('>', 0x71): // XTVERSION - xterm query version string - request_version_string(state); - break; - - case INTERMED(' ', 0x71): // DECSCUSR - DEC set cursor shape - val = CSI_ARG_OR(args[0], 1); - - switch(val) { - case 0: case 1: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); - break; - case 2: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); - break; - case 3: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE); - break; - case 4: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_UNDERLINE); - break; - case 5: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT); - break; - case 6: - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 0); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BAR_LEFT); - break; - } - - break; - - case INTERMED('"', 0x71): // DECSCA - DEC select character protection attribute - val = CSI_ARG_OR(args[0], 0); - - switch(val) { - case 0: case 2: - state->protected_cell = 0; - break; - case 1: - state->protected_cell = 1; - break; - } - - break; - - case 0x72: // DECSTBM - DEC custom - state->scrollregion_top = CSI_ARG_OR(args[0], 1) - 1; - state->scrollregion_bottom = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]); - LBOUND(state->scrollregion_top, 0); - UBOUND(state->scrollregion_top, state->rows); - LBOUND(state->scrollregion_bottom, -1); - if(state->scrollregion_top == 0 && state->scrollregion_bottom == state->rows) - state->scrollregion_bottom = -1; - else - UBOUND(state->scrollregion_bottom, state->rows); - - if(SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) { - // Invalid - state->scrollregion_top = 0; - state->scrollregion_bottom = -1; - } - - // Setting the scrolling region restores the cursor to the home position - state->pos.row = 0; - state->pos.col = 0; - if(state->mode.origin) { - state->pos.row += state->scrollregion_top; - state->pos.col += SCROLLREGION_LEFT(state); - } - - break; - - case 0x73: // DECSLRM - DEC custom - // Always allow setting these margins, just they won't take effect without DECVSSM - state->scrollregion_left = CSI_ARG_OR(args[0], 1) - 1; - state->scrollregion_right = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]); - LBOUND(state->scrollregion_left, 0); - UBOUND(state->scrollregion_left, state->cols); - LBOUND(state->scrollregion_right, -1); - if(state->scrollregion_left == 0 && state->scrollregion_right == state->cols) - state->scrollregion_right = -1; - else - UBOUND(state->scrollregion_right, state->cols); - - if(state->scrollregion_right > -1 && - state->scrollregion_right <= state->scrollregion_left) { - // Invalid - state->scrollregion_left = 0; - state->scrollregion_right = -1; - } - - // Setting the scrolling region restores the cursor to the home position - state->pos.row = 0; - state->pos.col = 0; - if(state->mode.origin) { - state->pos.row += state->scrollregion_top; - state->pos.col += SCROLLREGION_LEFT(state); - } - - break; - - case INTERMED('\'', 0x7D): // DECIC - count = CSI_ARG_COUNT(args[0]); - - if(!is_cursor_in_scrollregion(state)) - break; - - rect.start_row = state->scrollregion_top; - rect.end_row = SCROLLREGION_BOTTOM(state); - rect.start_col = state->pos.col; - rect.end_col = SCROLLREGION_RIGHT(state); - - scroll(state, rect, 0, -count); - - break; - - case INTERMED('\'', 0x7E): // DECDC - count = CSI_ARG_COUNT(args[0]); - - if(!is_cursor_in_scrollregion(state)) - break; - - rect.start_row = state->scrollregion_top; - rect.end_row = SCROLLREGION_BOTTOM(state); - rect.start_col = state->pos.col; - rect.end_col = SCROLLREGION_RIGHT(state); - - scroll(state, rect, 0, count); - - break; - - default: - if(state->fallbacks && state->fallbacks->csi) - if((*state->fallbacks->csi)(leader, args, argcount, intermed, command, state->fbdata)) - return 1; - - return 0; - } - - if(state->mode.origin) { - LBOUND(state->pos.row, state->scrollregion_top); - UBOUND(state->pos.row, SCROLLREGION_BOTTOM(state)-1); - LBOUND(state->pos.col, SCROLLREGION_LEFT(state)); - UBOUND(state->pos.col, SCROLLREGION_RIGHT(state)-1); - } - else { - LBOUND(state->pos.row, 0); - UBOUND(state->pos.row, state->rows-1); - LBOUND(state->pos.col, 0); - UBOUND(state->pos.col, THISROWWIDTH(state)-1); - } - - updatecursor(state, &oldpos, cancel_phantom); - -#ifdef DEBUG - if(state->pos.row < 0 || state->pos.row >= state->rows || - state->pos.col < 0 || state->pos.col >= state->cols) { - fprintf(stderr, "Position out of bounds after CSI %c: (%d,%d)\n", - command, state->pos.row, state->pos.col); - abort(); - } - - if(SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) { - fprintf(stderr, "Scroll region height out of bounds after CSI %c: %d <= %d\n", - command, SCROLLREGION_BOTTOM(state), state->scrollregion_top); - abort(); - } - - if(SCROLLREGION_RIGHT(state) <= SCROLLREGION_LEFT(state)) { - fprintf(stderr, "Scroll region width out of bounds after CSI %c: %d <= %d\n", - command, SCROLLREGION_RIGHT(state), SCROLLREGION_LEFT(state)); - abort(); - } -#endif - - return 1; -} - -static char base64_one(uint8_t b) -{ - if(b < 26) - return 'A' + b; - else if(b < 52) - return 'a' + b - 26; - else if(b < 62) - return '0' + b - 52; - else if(b == 62) - return '+'; - else if(b == 63) - return '/'; - return 0; -} - -static uint8_t unbase64one(char c) -{ - if(c >= 'A' && c <= 'Z') - return c - 'A'; - else if(c >= 'a' && c <= 'z') - return c - 'a' + 26; - else if(c >= '0' && c <= '9') - return c - '0' + 52; - else if(c == '+') - return 62; - else if(c == '/') - return 63; - - return 0xFF; -} - -static void osc_selection(VTermState *state, VTermStringFragment frag) -{ - if(frag.initial) { - state->tmp.selection.mask = 0; - state->tmp.selection.state = SELECTION_INITIAL; - } - - while(!state->tmp.selection.state && frag.len) { - /* Parse selection parameter */ - switch(frag.str[0]) { - case 'c': - state->tmp.selection.mask |= VTERM_SELECTION_CLIPBOARD; - break; - case 'p': - state->tmp.selection.mask |= VTERM_SELECTION_PRIMARY; - break; - case 'q': - state->tmp.selection.mask |= VTERM_SELECTION_SECONDARY; - break; - case 's': - state->tmp.selection.mask |= VTERM_SELECTION_SELECT; - break; - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - state->tmp.selection.mask |= (VTERM_SELECTION_CUT0 << (frag.str[0] - '0')); - break; - - case ';': - state->tmp.selection.state = SELECTION_SELECTED; - if(!state->tmp.selection.mask) - state->tmp.selection.mask = VTERM_SELECTION_SELECT|VTERM_SELECTION_CUT0; - break; - } - - frag.str++; - frag.len--; - } - - if(!frag.len) { - /* Clear selection if we're already finished but didn't do anything */ - if(frag.final && state->selection.callbacks->set) { - (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ - .str = NULL, - .len = 0, - .initial = state->tmp.selection.state != SELECTION_SET, - .final = true, - }, state->selection.user); - } - return; - } - - if(state->tmp.selection.state == SELECTION_SELECTED) { - if(frag.str[0] == '?') { - state->tmp.selection.state = SELECTION_QUERY; - } - else { - state->tmp.selection.state = SELECTION_SET_INITIAL; - state->tmp.selection.recvpartial = 0; - } - } - - if(state->tmp.selection.state == SELECTION_QUERY) { - if(state->selection.callbacks->query) - (*state->selection.callbacks->query)(state->tmp.selection.mask, state->selection.user); - return; - } - - if(state->tmp.selection.state == SELECTION_INVALID) - return; - - if(state->selection.callbacks->set) { - size_t bufcur = 0; - char *buffer = state->selection.buffer; - - uint32_t x = 0; /* Current decoding value */ - int n = 0; /* Number of sextets consumed */ - - if(state->tmp.selection.recvpartial) { - n = state->tmp.selection.recvpartial >> 24; - x = state->tmp.selection.recvpartial & 0x03FFFF; /* could be up to 18 bits of state in here */ - - state->tmp.selection.recvpartial = 0; - } - - while((state->selection.buflen - bufcur) >= 3 && frag.len) { - if(frag.str[0] == '=') { - if(n == 2) { - buffer[0] = (x >> 4) & 0xFF; - buffer += 1, bufcur += 1; - } - if(n == 3) { - buffer[0] = (x >> 10) & 0xFF; - buffer[1] = (x >> 2) & 0xFF; - buffer += 2, bufcur += 2; - } - - while(frag.len && frag.str[0] == '=') - frag.str++, frag.len--; - - n = 0; - } - else { - uint8_t b = unbase64one(frag.str[0]); - if(b == 0xFF) { - DEBUG_LOG("base64decode bad input %02X\n", (uint8_t)frag.str[0]); - - state->tmp.selection.state = SELECTION_INVALID; - if(state->selection.callbacks->set) { - (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ - .str = NULL, - .len = 0, - .initial = true, - .final = true, - }, state->selection.user); - } - break; - } - - x = (x << 6) | b; - n++; - frag.str++, frag.len--; - - if(n == 4) { - buffer[0] = (x >> 16) & 0xFF; - buffer[1] = (x >> 8) & 0xFF; - buffer[2] = (x >> 0) & 0xFF; - - buffer += 3, bufcur += 3; - x = 0; - n = 0; - } - } - - if(!frag.len || (state->selection.buflen - bufcur) < 3) { - if(bufcur) { - (*state->selection.callbacks->set)(state->tmp.selection.mask, (VTermStringFragment){ - .str = state->selection.buffer, - .len = bufcur, - .initial = state->tmp.selection.state == SELECTION_SET_INITIAL, - .final = frag.final && !frag.len, - }, state->selection.user); - state->tmp.selection.state = SELECTION_SET; - } - - buffer = state->selection.buffer; - bufcur = 0; - } - } - - if(n) - state->tmp.selection.recvpartial = (n << 24) | x; - } -} - -static int on_osc(int command, VTermStringFragment frag, void *user) -{ - VTermState *state = user; - - switch(command) { - case 0: - settermprop_string(state, VTERM_PROP_ICONNAME, frag); - settermprop_string(state, VTERM_PROP_TITLE, frag); - return 1; - - case 1: - settermprop_string(state, VTERM_PROP_ICONNAME, frag); - return 1; - - case 2: - settermprop_string(state, VTERM_PROP_TITLE, frag); - return 1; - - case 52: - if(state->selection.callbacks) - osc_selection(state, frag); - - return 1; - - default: - if(state->fallbacks && state->fallbacks->osc) - if((*state->fallbacks->osc)(command, frag, state->fbdata)) - return 1; - } - - return 0; -} - -static void request_status_string(VTermState *state, VTermStringFragment frag) -{ - VTerm *vt = state->vt; - - char *tmp = state->tmp.decrqss; - - if(frag.initial) - tmp[0] = tmp[1] = tmp[2] = tmp[3] = 0; - - int i = 0; - while(i < sizeof(state->tmp.decrqss)-1 && tmp[i]) - i++; - while(i < sizeof(state->tmp.decrqss)-1 && frag.len--) - tmp[i++] = (frag.str++)[0]; - tmp[i] = 0; - - if(!frag.final) - return; - - switch(tmp[0] | tmp[1]<<8 | tmp[2]<<16) { - case 'm': { - // Query SGR - long args[20]; - int argc = vterm_state_getpen(state, args, sizeof(args)/sizeof(args[0])); - size_t cur = 0; - - cur += snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, - vt->mode.ctrl8bit ? "\x90" "1$r" : ESC_S "P" "1$r"); // DCS 1$r ... - if(cur >= vt->tmpbuffer_len) - return; - - for(int argi = 0; argi < argc; argi++) { - cur += snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, - argi == argc - 1 ? "%ld" : - CSI_ARG_HAS_MORE(args[argi]) ? "%ld:" : - "%ld;", - CSI_ARG(args[argi])); - if(cur >= vt->tmpbuffer_len) - return; - } - - cur += snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, - vt->mode.ctrl8bit ? "m" "\x9C" : "m" ESC_S "\\"); // ... m ST - if(cur >= vt->tmpbuffer_len) - return; - - vterm_push_output_bytes(vt, vt->tmpbuffer, cur); - return; - } - - case 'r': - // Query DECSTBM - vterm_push_output_sprintf_str(vt, C1_DCS, true, - "1$r%d;%dr", state->scrollregion_top+1, SCROLLREGION_BOTTOM(state)); - return; - - case 's': - // Query DECSLRM - vterm_push_output_sprintf_str(vt, C1_DCS, true, - "1$r%d;%ds", SCROLLREGION_LEFT(state)+1, SCROLLREGION_RIGHT(state)); - return; - - case ' '|('q'<<8): { - // Query DECSCUSR - int reply; - switch(state->mode.cursor_shape) { - case VTERM_PROP_CURSORSHAPE_BLOCK: reply = 2; break; - case VTERM_PROP_CURSORSHAPE_UNDERLINE: reply = 4; break; - case VTERM_PROP_CURSORSHAPE_BAR_LEFT: reply = 6; break; - } - if(state->mode.cursor_blink) - reply--; - vterm_push_output_sprintf_str(vt, C1_DCS, true, - "1$r%d q", reply); - return; - } - - case '\"'|('q'<<8): - // Query DECSCA - vterm_push_output_sprintf_str(vt, C1_DCS, true, - "1$r%d\"q", state->protected_cell ? 1 : 2); - return; - } - - vterm_push_output_sprintf_str(state->vt, C1_DCS, true, "0$r"); -} - -static int on_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user) -{ - VTermState *state = user; - - if(commandlen == 2 && strneq(command, "$q", 2)) { - request_status_string(state, frag); - return 1; - } - else if(state->fallbacks && state->fallbacks->dcs) - if((*state->fallbacks->dcs)(command, commandlen, frag, state->fbdata)) - return 1; - - DEBUG_LOG("libvterm: Unhandled DCS %.*s\n", (int)commandlen, command); - return 0; -} - -static int on_apc(VTermStringFragment frag, void *user) -{ - VTermState *state = user; - - if(state->fallbacks && state->fallbacks->apc) - if((*state->fallbacks->apc)(frag, state->fbdata)) - return 1; - - /* No DEBUG_LOG because all APCs are unhandled */ - return 0; -} - -static int on_pm(VTermStringFragment frag, void *user) -{ - VTermState *state = user; - - if(state->fallbacks && state->fallbacks->pm) - if((*state->fallbacks->pm)(frag, state->fbdata)) - return 1; - - /* No DEBUG_LOG because all PMs are unhandled */ - return 0; -} - -static int on_sos(VTermStringFragment frag, void *user) -{ - VTermState *state = user; - - if(state->fallbacks && state->fallbacks->sos) - if((*state->fallbacks->sos)(frag, state->fbdata)) - return 1; - - /* No DEBUG_LOG because all SOSs are unhandled */ - return 0; -} - -static int on_resize(int rows, int cols, void *user) -{ - VTermState *state = user; - VTermPos oldpos = state->pos; - - if(cols != state->cols) { - unsigned char *newtabstops = vterm_allocator_malloc(state->vt, (cols + 7) / 8); - - /* TODO: This can all be done much more efficiently bytewise */ - int col; - for(col = 0; col < state->cols && col < cols; col++) { - unsigned char mask = 1 << (col & 7); - if(state->tabstops[col >> 3] & mask) - newtabstops[col >> 3] |= mask; - else - newtabstops[col >> 3] &= ~mask; - } - - for( ; col < cols; col++) { - unsigned char mask = 1 << (col & 7); - if(col % 8 == 0) - newtabstops[col >> 3] |= mask; - else - newtabstops[col >> 3] &= ~mask; - } - - vterm_allocator_free(state->vt, state->tabstops); - state->tabstops = newtabstops; - } - - state->rows = rows; - state->cols = cols; - - if(state->scrollregion_bottom > -1) - UBOUND(state->scrollregion_bottom, state->rows); - if(state->scrollregion_right > -1) - UBOUND(state->scrollregion_right, state->cols); - - VTermStateFields fields = { - .pos = state->pos, - .lineinfos = { [0] = state->lineinfos[0], [1] = state->lineinfos[1] }, - }; - - if(state->callbacks && state->callbacks->resize) { - (*state->callbacks->resize)(rows, cols, &fields, state->cbdata); - state->pos = fields.pos; - - state->lineinfos[0] = fields.lineinfos[0]; - state->lineinfos[1] = fields.lineinfos[1]; - } - else { - if(rows != state->rows) { - for(int bufidx = BUFIDX_PRIMARY; bufidx <= BUFIDX_ALTSCREEN; bufidx++) { - VTermLineInfo *oldlineinfo = state->lineinfos[bufidx]; - if(!oldlineinfo) - continue; - - VTermLineInfo *newlineinfo = vterm_allocator_malloc(state->vt, rows * sizeof(VTermLineInfo)); - - int row; - for(row = 0; row < state->rows && row < rows; row++) { - newlineinfo[row] = oldlineinfo[row]; - } - - for( ; row < rows; row++) { - newlineinfo[row] = (VTermLineInfo){ - .doublewidth = 0, - }; - } - - vterm_allocator_free(state->vt, state->lineinfos[bufidx]); - state->lineinfos[bufidx] = newlineinfo; - } - } - } - - state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY]; - - if(state->at_phantom && state->pos.col < cols-1) { - state->at_phantom = 0; - state->pos.col++; - } - - if(state->pos.row < 0) - state->pos.row = 0; - if(state->pos.row >= rows) - state->pos.row = rows - 1; - if(state->pos.col < 0) - state->pos.col = 0; - if(state->pos.col >= cols) - state->pos.col = cols - 1; - - updatecursor(state, &oldpos, 1); - - return 1; -} - -static const VTermParserCallbacks parser_callbacks = { - .text = on_text, - .control = on_control, - .escape = on_escape, - .csi = on_csi, - .osc = on_osc, - .dcs = on_dcs, - .apc = on_apc, - .pm = on_pm, - .sos = on_sos, - .resize = on_resize, -}; - -VTermState *vterm_obtain_state(VTerm *vt) -{ - if(vt->state) - return vt->state; - - VTermState *state = vterm_state_new(vt); - vt->state = state; - - vterm_parser_set_callbacks(vt, &parser_callbacks, state); - - return state; -} - -void vterm_state_reset(VTermState *state, int hard) -{ - state->scrollregion_top = 0; - state->scrollregion_bottom = -1; - state->scrollregion_left = 0; - state->scrollregion_right = -1; - - state->mode.keypad = 0; - state->mode.cursor = 0; - state->mode.autowrap = 1; - state->mode.insert = 0; - state->mode.newline = 0; - state->mode.alt_screen = 0; - state->mode.origin = 0; - state->mode.leftrightmargin = 0; - state->mode.bracketpaste = 0; - state->mode.report_focus = 0; - - state->mouse_flags = 0; - - state->vt->mode.ctrl8bit = 0; - - for(int col = 0; col < state->cols; col++) - if(col % 8 == 0) - set_col_tabstop(state, col); - else - clear_col_tabstop(state, col); - - for(int row = 0; row < state->rows; row++) - set_lineinfo(state, row, FORCE, DWL_OFF, DHL_OFF); - - if(state->callbacks && state->callbacks->initpen) - (*state->callbacks->initpen)(state->cbdata); - - vterm_state_resetpen(state); - - VTermEncoding *default_enc = state->vt->mode.utf8 ? - vterm_lookup_encoding(ENC_UTF8, 'u') : - vterm_lookup_encoding(ENC_SINGLE_94, 'B'); - - for(int i = 0; i < 4; i++) { - state->encoding[i].enc = default_enc; - if(default_enc->init) - (*default_enc->init)(default_enc, state->encoding[i].data); - } - - state->gl_set = 0; - state->gr_set = 1; - state->gsingle_set = 0; - - state->protected_cell = 0; - - // Initialise the props - settermprop_bool(state, VTERM_PROP_CURSORVISIBLE, 1); - settermprop_bool(state, VTERM_PROP_CURSORBLINK, 1); - settermprop_int (state, VTERM_PROP_CURSORSHAPE, VTERM_PROP_CURSORSHAPE_BLOCK); - - if(hard) { - state->pos.row = 0; - state->pos.col = 0; - state->at_phantom = 0; - - VTermRect rect = { 0, state->rows, 0, state->cols }; - erase(state, rect, 0); - } -} - -void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos) -{ - *cursorpos = state->pos; -} - -void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user) -{ - if(callbacks) { - state->callbacks = callbacks; - state->cbdata = user; - - if(state->callbacks && state->callbacks->initpen) - (*state->callbacks->initpen)(state->cbdata); - } - else { - state->callbacks = NULL; - state->cbdata = NULL; - } -} - -void *vterm_state_get_cbdata(VTermState *state) -{ - return state->cbdata; -} - -void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermStateFallbacks *fallbacks, void *user) -{ - if(fallbacks) { - state->fallbacks = fallbacks; - state->fbdata = user; - } - else { - state->fallbacks = NULL; - state->fbdata = NULL; - } -} - -void *vterm_state_get_unrecognised_fbdata(VTermState *state) -{ - return state->fbdata; -} - -int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val) -{ - /* Only store the new value of the property if usercode said it was happy. - * This is especially important for altscreen switching */ - if(state->callbacks && state->callbacks->settermprop) - if(!(*state->callbacks->settermprop)(prop, val, state->cbdata)) - return 0; - - switch(prop) { - case VTERM_PROP_TITLE: - case VTERM_PROP_ICONNAME: - // we don't store these, just transparently pass through - return 1; - case VTERM_PROP_CURSORVISIBLE: - state->mode.cursor_visible = val->boolean; - return 1; - case VTERM_PROP_CURSORBLINK: - state->mode.cursor_blink = val->boolean; - return 1; - case VTERM_PROP_CURSORSHAPE: - state->mode.cursor_shape = val->number; - return 1; - case VTERM_PROP_REVERSE: - state->mode.screen = val->boolean; - return 1; - case VTERM_PROP_ALTSCREEN: - state->mode.alt_screen = val->boolean; - state->lineinfo = state->lineinfos[state->mode.alt_screen ? BUFIDX_ALTSCREEN : BUFIDX_PRIMARY]; - if(state->mode.alt_screen) { - VTermRect rect = { - .start_row = 0, - .start_col = 0, - .end_row = state->rows, - .end_col = state->cols, - }; - erase(state, rect, 0); - } - return 1; - case VTERM_PROP_MOUSE: - state->mouse_flags = 0; - if(val->number) - state->mouse_flags |= MOUSE_WANT_CLICK; - if(val->number == VTERM_PROP_MOUSE_DRAG) - state->mouse_flags |= MOUSE_WANT_DRAG; - if(val->number == VTERM_PROP_MOUSE_MOVE) - state->mouse_flags |= MOUSE_WANT_MOVE; - return 1; - case VTERM_PROP_FOCUSREPORT: - state->mode.report_focus = val->boolean; - return 1; - - case VTERM_N_PROPS: - return 0; - } - - return 0; -} - -void vterm_state_focus_in(VTermState *state) -{ - if(state->mode.report_focus) - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "I"); -} - -void vterm_state_focus_out(VTermState *state) -{ - if(state->mode.report_focus) - vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "O"); -} - -const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row) -{ - return state->lineinfo + row; -} - -void vterm_state_set_selection_callbacks(VTermState *state, const VTermSelectionCallbacks *callbacks, void *user, - char *buffer, size_t buflen) -{ - if(buflen && !buffer) - buffer = vterm_allocator_malloc(state->vt, buflen); - - state->selection.callbacks = callbacks; - state->selection.user = user; - state->selection.buffer = buffer; - state->selection.buflen = buflen; -} - -void vterm_state_send_selection(VTermState *state, VTermSelectionMask mask, VTermStringFragment frag) -{ - VTerm *vt = state->vt; - - if(frag.initial) { - /* TODO: support sending more than one mask bit */ - static const char selection_chars[] = "cpqs"; - int idx; - for(idx = 0; idx < 4; idx++) - if(mask & (1 << idx)) - break; - - vterm_push_output_sprintf_str(vt, C1_OSC, false, "52;%c;", selection_chars[idx]); - - state->tmp.selection.sendpartial = 0; - } - - if(frag.len) { - size_t bufcur = 0; - char *buffer = state->selection.buffer; - - uint32_t x = 0; - int n = 0; - - if(state->tmp.selection.sendpartial) { - n = state->tmp.selection.sendpartial >> 24; - x = state->tmp.selection.sendpartial & 0xFFFFFF; - - state->tmp.selection.sendpartial = 0; - } - - while((state->selection.buflen - bufcur) >= 4 && frag.len) { - x = (x << 8) | frag.str[0]; - n++; - frag.str++, frag.len--; - - if(n == 3) { - buffer[0] = base64_one((x >> 18) & 0x3F); - buffer[1] = base64_one((x >> 12) & 0x3F); - buffer[2] = base64_one((x >> 6) & 0x3F); - buffer[3] = base64_one((x >> 0) & 0x3F); - - buffer += 4, bufcur += 4; - x = 0; - n = 0; - } - - if(!frag.len || (state->selection.buflen - bufcur) < 4) { - if(bufcur) - vterm_push_output_bytes(vt, state->selection.buffer, bufcur); - - buffer = state->selection.buffer; - bufcur = 0; - } - } - - if(n) - state->tmp.selection.sendpartial = (n << 24) | x; - } - - if(frag.final) { - if(state->tmp.selection.sendpartial) { - int n = state->tmp.selection.sendpartial >> 24; - uint32_t x = state->tmp.selection.sendpartial & 0xFFFFFF; - char *buffer = state->selection.buffer; - - /* n is either 1 or 2 now */ - x <<= (n == 1) ? 16 : 8; - - buffer[0] = base64_one((x >> 18) & 0x3F); - buffer[1] = base64_one((x >> 12) & 0x3F); - buffer[2] = (n == 1) ? '=' : base64_one((x >> 6) & 0x3F); - buffer[3] = '='; - - vterm_push_output_sprintf_str(vt, 0, true, "%.*s", 4, buffer); - } - else - vterm_push_output_sprintf_str(vt, 0, true, ""); - } -} diff --git a/src/vterm/vterm.c b/src/vterm/vterm.c deleted file mode 100644 index 530c513755..0000000000 --- a/src/vterm/vterm.c +++ /dev/null @@ -1,432 +0,0 @@ -#include "vterm_internal.h" - -#include "auto/config.h" -#include <stdarg.h> -#include <stdio.h> -#include <stdlib.h> -#include <string.h> - -/***************** - * API functions * - *****************/ - -static void *default_malloc(size_t size, void *allocdata) -{ - void *ptr = malloc(size); - if(ptr) - memset(ptr, 0, size); - return ptr; -} - -static void default_free(void *ptr, void *allocdata) -{ - free(ptr); -} - -static VTermAllocatorFunctions default_allocator = { - .malloc = &default_malloc, - .free = &default_free, -}; - -VTerm *vterm_new(int rows, int cols) -{ - return vterm_build(&(const struct VTermBuilder){ - .rows = rows, - .cols = cols, - }); -} - -VTerm *vterm_new_with_allocator(int rows, int cols, VTermAllocatorFunctions *funcs, void *allocdata) -{ - return vterm_build(&(const struct VTermBuilder){ - .rows = rows, - .cols = cols, - .allocator = funcs, - .allocdata = allocdata, - }); -} - -/* A handy macro for defaulting values out of builder fields */ -#define DEFAULT(v, def) ((v) ? (v) : (def)) - -VTerm *vterm_build(const struct VTermBuilder *builder) -{ - const VTermAllocatorFunctions *allocator = DEFAULT(builder->allocator, &default_allocator); - - /* Need to bootstrap using the allocator function directly */ - VTerm *vt = (*allocator->malloc)(sizeof(VTerm), builder->allocdata); - - vt->allocator = allocator; - vt->allocdata = builder->allocdata; - - vt->rows = builder->rows; - vt->cols = builder->cols; - - vt->parser.state = NORMAL; - - vt->parser.callbacks = NULL; - vt->parser.cbdata = NULL; - - vt->parser.emit_nul = false; - - vt->outfunc = NULL; - vt->outdata = NULL; - - vt->outbuffer_len = DEFAULT(builder->outbuffer_len, 4096); - vt->outbuffer_cur = 0; - vt->outbuffer = vterm_allocator_malloc(vt, vt->outbuffer_len); - - vt->tmpbuffer_len = DEFAULT(builder->tmpbuffer_len, 4096); - vt->tmpbuffer = vterm_allocator_malloc(vt, vt->tmpbuffer_len); - - return vt; -} - -void vterm_free(VTerm *vt) -{ - if(vt->screen) - vterm_screen_free(vt->screen); - - if(vt->state) - vterm_state_free(vt->state); - - vterm_allocator_free(vt, vt->outbuffer); - vterm_allocator_free(vt, vt->tmpbuffer); - - vterm_allocator_free(vt, vt); -} - -INTERNAL void *vterm_allocator_malloc(VTerm *vt, size_t size) -{ - return (*vt->allocator->malloc)(size, vt->allocdata); -} - -INTERNAL void vterm_allocator_free(VTerm *vt, void *ptr) -{ - (*vt->allocator->free)(ptr, vt->allocdata); -} - -void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp) -{ - if(rowsp) - *rowsp = vt->rows; - if(colsp) - *colsp = vt->cols; -} - -void vterm_set_size(VTerm *vt, int rows, int cols) -{ - if(rows < 1 || cols < 1) - return; - - vt->rows = rows; - vt->cols = cols; - - if(vt->parser.callbacks && vt->parser.callbacks->resize) - (*vt->parser.callbacks->resize)(rows, cols, vt->parser.cbdata); -} - -int vterm_get_utf8(const VTerm *vt) -{ - return vt->mode.utf8; -} - -void vterm_set_utf8(VTerm *vt, int is_utf8) -{ - vt->mode.utf8 = is_utf8; -} - -void vterm_output_set_callback(VTerm *vt, VTermOutputCallback *func, void *user) -{ - vt->outfunc = func; - vt->outdata = user; -} - -INTERNAL void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len) -{ - if(vt->outfunc) { - (vt->outfunc)(bytes, len, vt->outdata); - return; - } - - if(len > vt->outbuffer_len - vt->outbuffer_cur) - return; - - memcpy(vt->outbuffer + vt->outbuffer_cur, bytes, len); - vt->outbuffer_cur += len; -} - -INTERNAL void vterm_push_output_vsprintf(VTerm *vt, const char *format, va_list args) -{ - size_t len = vsnprintf(vt->tmpbuffer, vt->tmpbuffer_len, - format, args); - - vterm_push_output_bytes(vt, vt->tmpbuffer, len); -} - -INTERNAL void vterm_push_output_sprintf(VTerm *vt, const char *format, ...) -{ - va_list args; - va_start(args, format); - vterm_push_output_vsprintf(vt, format, args); - va_end(args); -} - -INTERNAL void vterm_push_output_sprintf_ctrl(VTerm *vt, unsigned char ctrl, const char *fmt, ...) -{ - size_t cur; - - if(ctrl >= 0x80 && !vt->mode.ctrl8bit) - cur = snprintf(vt->tmpbuffer, vt->tmpbuffer_len, - ESC_S "%c", ctrl - 0x40); - else - cur = snprintf(vt->tmpbuffer, vt->tmpbuffer_len, - "%c", ctrl); - - if(cur >= vt->tmpbuffer_len) - return; - - va_list args; - va_start(args, fmt); - cur += vsnprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, - fmt, args); - va_end(args); - - if(cur >= vt->tmpbuffer_len) - return; - - vterm_push_output_bytes(vt, vt->tmpbuffer, cur); -} - -INTERNAL void vterm_push_output_sprintf_str(VTerm *vt, unsigned char ctrl, bool term, const char *fmt, ...) -{ - size_t cur = 0; - - if(ctrl) { - if(ctrl >= 0x80 && !vt->mode.ctrl8bit) - cur = snprintf(vt->tmpbuffer, vt->tmpbuffer_len, - ESC_S "%c", ctrl - 0x40); - else - cur = snprintf(vt->tmpbuffer, vt->tmpbuffer_len, - "%c", ctrl); - - if(cur >= vt->tmpbuffer_len) - return; - } - - va_list args; - va_start(args, fmt); - cur += vsnprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, - fmt, args); - va_end(args); - - if(cur >= vt->tmpbuffer_len) - return; - - if(term) { - cur += snprintf(vt->tmpbuffer + cur, vt->tmpbuffer_len - cur, - vt->mode.ctrl8bit ? "\x9C" : ESC_S "\\"); // ST - - if(cur >= vt->tmpbuffer_len) - return; - } - - vterm_push_output_bytes(vt, vt->tmpbuffer, cur); -} - -size_t vterm_output_get_buffer_size(const VTerm *vt) -{ - return vt->outbuffer_len; -} - -size_t vterm_output_get_buffer_current(const VTerm *vt) -{ - return vt->outbuffer_cur; -} - -size_t vterm_output_get_buffer_remaining(const VTerm *vt) -{ - return vt->outbuffer_len - vt->outbuffer_cur; -} - -size_t vterm_output_read(VTerm *vt, char *buffer, size_t len) -{ - if(len > vt->outbuffer_cur) - len = vt->outbuffer_cur; - - memcpy(buffer, vt->outbuffer, len); - - if(len < vt->outbuffer_cur) - memmove(vt->outbuffer, vt->outbuffer + len, vt->outbuffer_cur - len); - - vt->outbuffer_cur -= len; - - return len; -} - -VTermValueType vterm_get_attr_type(VTermAttr attr) -{ - switch(attr) { - case VTERM_ATTR_BOLD: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_UNDERLINE: return VTERM_VALUETYPE_INT; - case VTERM_ATTR_ITALIC: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_BLINK: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_REVERSE: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_CONCEAL: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_STRIKE: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_FONT: return VTERM_VALUETYPE_INT; - case VTERM_ATTR_FOREGROUND: return VTERM_VALUETYPE_COLOR; - case VTERM_ATTR_BACKGROUND: return VTERM_VALUETYPE_COLOR; - case VTERM_ATTR_SMALL: return VTERM_VALUETYPE_BOOL; - case VTERM_ATTR_BASELINE: return VTERM_VALUETYPE_INT; - case VTERM_ATTR_URI: return VTERM_VALUETYPE_INT; - - case VTERM_N_ATTRS: return 0; - } - return 0; /* UNREACHABLE */ -} - -VTermValueType vterm_get_prop_type(VTermProp prop) -{ - switch(prop) { - case VTERM_PROP_CURSORVISIBLE: return VTERM_VALUETYPE_BOOL; - case VTERM_PROP_CURSORBLINK: return VTERM_VALUETYPE_BOOL; - case VTERM_PROP_ALTSCREEN: return VTERM_VALUETYPE_BOOL; - case VTERM_PROP_TITLE: return VTERM_VALUETYPE_STRING; - case VTERM_PROP_ICONNAME: return VTERM_VALUETYPE_STRING; - case VTERM_PROP_REVERSE: return VTERM_VALUETYPE_BOOL; - case VTERM_PROP_CURSORSHAPE: return VTERM_VALUETYPE_INT; - case VTERM_PROP_MOUSE: return VTERM_VALUETYPE_INT; - case VTERM_PROP_FOCUSREPORT: return VTERM_VALUETYPE_BOOL; - - case VTERM_N_PROPS: return 0; - } - return 0; /* UNREACHABLE */ -} - -void vterm_scroll_rect(VTermRect rect, - int downward, - int rightward, - int (*moverect)(VTermRect src, VTermRect dest, void *user), - int (*eraserect)(VTermRect rect, int selective, void *user), - void *user) -{ - VTermRect src; - VTermRect dest; - - if(abs(downward) >= rect.end_row - rect.start_row || - abs(rightward) >= rect.end_col - rect.start_col) { - /* Scroll more than area; just erase the lot */ - (*eraserect)(rect, 0, user); - return; - } - - if(rightward >= 0) { - /* rect: [XXX................] - * src: [----------------] - * dest: [----------------] - */ - dest.start_col = rect.start_col; - dest.end_col = rect.end_col - rightward; - src.start_col = rect.start_col + rightward; - src.end_col = rect.end_col; - } - else { - /* rect: [................XXX] - * src: [----------------] - * dest: [----------------] - */ - int leftward = -rightward; - dest.start_col = rect.start_col + leftward; - dest.end_col = rect.end_col; - src.start_col = rect.start_col; - src.end_col = rect.end_col - leftward; - } - - if(downward >= 0) { - dest.start_row = rect.start_row; - dest.end_row = rect.end_row - downward; - src.start_row = rect.start_row + downward; - src.end_row = rect.end_row; - } - else { - int upward = -downward; - dest.start_row = rect.start_row + upward; - dest.end_row = rect.end_row; - src.start_row = rect.start_row; - src.end_row = rect.end_row - upward; - } - - if(moverect) - (*moverect)(dest, src, user); - - if(downward > 0) - rect.start_row = rect.end_row - downward; - else if(downward < 0) - rect.end_row = rect.start_row - downward; - - if(rightward > 0) - rect.start_col = rect.end_col - rightward; - else if(rightward < 0) - rect.end_col = rect.start_col - rightward; - - (*eraserect)(rect, 0, user); -} - -void vterm_copy_cells(VTermRect dest, - VTermRect src, - void (*copycell)(VTermPos dest, VTermPos src, void *user), - void *user) -{ - int downward = src.start_row - dest.start_row; - int rightward = src.start_col - dest.start_col; - - int init_row, test_row, init_col, test_col; - int inc_row, inc_col; - - if(downward < 0) { - init_row = dest.end_row - 1; - test_row = dest.start_row - 1; - inc_row = -1; - } - else /* downward >= 0 */ { - init_row = dest.start_row; - test_row = dest.end_row; - inc_row = +1; - } - - if(rightward < 0) { - init_col = dest.end_col - 1; - test_col = dest.start_col - 1; - inc_col = -1; - } - else /* rightward >= 0 */ { - init_col = dest.start_col; - test_col = dest.end_col; - inc_col = +1; - } - - VTermPos pos; - for(pos.row = init_row; pos.row != test_row; pos.row += inc_row) - for(pos.col = init_col; pos.col != test_col; pos.col += inc_col) { - VTermPos srcpos = { pos.row + downward, pos.col + rightward }; - (*copycell)(pos, srcpos, user); - } -} - -void vterm_check_version(int major, int minor) -{ - if(major != VTERM_VERSION_MAJOR) { - fprintf(stderr, "libvterm major version mismatch; %d (wants) != %d (library)\n", - major, VTERM_VERSION_MAJOR); - exit(1); - } - - if(minor > VTERM_VERSION_MINOR) { - fprintf(stderr, "libvterm minor version mismatch; %d (wants) > %d (library)\n", - minor, VTERM_VERSION_MINOR); - exit(1); - } - - // Happy -} diff --git a/src/vterm/vterm.h b/src/vterm/vterm.h deleted file mode 100644 index 4de1d885b8..0000000000 --- a/src/vterm/vterm.h +++ /dev/null @@ -1,638 +0,0 @@ -#ifndef __VTERM_H__ -#define __VTERM_H__ - -#ifdef __cplusplus -extern "C" { -#endif - -#include <stdint.h> -#include <stdlib.h> -#include <stdbool.h> - -#include "nvim/macros_defs.h" -#include "nvim/types_defs.h" -#include "vterm_keycodes.h" - -#define VTERM_VERSION_MAJOR 0 -#define VTERM_VERSION_MINOR 3 -#define VTERM_VERSION_PATCH 3 - -#define VTERM_CHECK_VERSION \ - vterm_check_version(VTERM_VERSION_MAJOR, VTERM_VERSION_MINOR) - -typedef struct VTerm VTerm; -typedef struct VTermState VTermState; -typedef struct VTermScreen VTermScreen; - -typedef struct { - int row; - int col; -} VTermPos; - -/* some small utility functions; we can just keep these static here */ - -/* order points by on-screen flow order */ -static inline int vterm_pos_cmp(VTermPos a, VTermPos b) -{ - return (a.row == b.row) ? a.col - b.col : a.row - b.row; -} - -typedef struct { - int start_row; - int end_row; - int start_col; - int end_col; -} VTermRect; - -/* true if the rect contains the point */ -static inline int vterm_rect_contains(VTermRect r, VTermPos p) -{ - return p.row >= r.start_row && p.row < r.end_row && - p.col >= r.start_col && p.col < r.end_col; -} - -/* move a rect */ -static inline void vterm_rect_move(VTermRect *rect, int row_delta, int col_delta) -{ - rect->start_row += row_delta; rect->end_row += row_delta; - rect->start_col += col_delta; rect->end_col += col_delta; -} - -/** - * Bit-field describing the content of the tagged union `VTermColor`. - */ -typedef enum { - /** - * If the lower bit of `type` is not set, the colour is 24-bit RGB. - */ - VTERM_COLOR_RGB = 0x00, - - /** - * The colour is an index into a palette of 256 colours. - */ - VTERM_COLOR_INDEXED = 0x01, - - /** - * Mask that can be used to extract the RGB/Indexed bit. - */ - VTERM_COLOR_TYPE_MASK = 0x01, - - /** - * If set, indicates that this colour should be the default foreground - * color, i.e. there was no SGR request for another colour. When - * rendering this colour it is possible to ignore "idx" and just use a - * colour that is not in the palette. - */ - VTERM_COLOR_DEFAULT_FG = 0x02, - - /** - * If set, indicates that this colour should be the default background - * color, i.e. there was no SGR request for another colour. A common - * option when rendering this colour is to not render a background at - * all, for example by rendering the window transparently at this spot. - */ - VTERM_COLOR_DEFAULT_BG = 0x04, - - /** - * Mask that can be used to extract the default foreground/background bit. - */ - VTERM_COLOR_DEFAULT_MASK = 0x06 -} VTermColorType; - -/** - * Returns true if the VTERM_COLOR_RGB `type` flag is set, indicating that the - * given VTermColor instance is an indexed colour. - */ -#define VTERM_COLOR_IS_INDEXED(col) \ - (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_INDEXED) - -/** - * Returns true if the VTERM_COLOR_INDEXED `type` flag is set, indicating that - * the given VTermColor instance is an rgb colour. - */ -#define VTERM_COLOR_IS_RGB(col) \ - (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_RGB) - -/** - * Returns true if the VTERM_COLOR_DEFAULT_FG `type` flag is set, indicating - * that the given VTermColor instance corresponds to the default foreground - * color. - */ -#define VTERM_COLOR_IS_DEFAULT_FG(col) \ - (!!((col)->type & VTERM_COLOR_DEFAULT_FG)) - -/** - * Returns true if the VTERM_COLOR_DEFAULT_BG `type` flag is set, indicating - * that the given VTermColor instance corresponds to the default background - * color. - */ -#define VTERM_COLOR_IS_DEFAULT_BG(col) \ - (!!((col)->type & VTERM_COLOR_DEFAULT_BG)) - -/** - * Tagged union storing either an RGB color or an index into a colour palette. - * In order to convert indexed colours to RGB, you may use the - * vterm_state_convert_color_to_rgb() or vterm_screen_convert_color_to_rgb() - * functions which lookup the RGB colour from the palette maintained by a - * VTermState or VTermScreen instance. - */ -typedef union { - /** - * Tag indicating which union member is actually valid. This variable - * coincides with the `type` member of the `rgb` and the `indexed` struct - * in memory. Please use the `VTERM_COLOR_IS_*` test macros to check whether - * a particular type flag is set. - */ - uint8_t type; - - /** - * Valid if `VTERM_COLOR_IS_RGB(type)` is true. Holds the RGB colour values. - */ - struct { - /** - * Same as the top-level `type` member stored in VTermColor. - */ - uint8_t type; - - /** - * The actual 8-bit red, green, blue colour values. - */ - uint8_t red, green, blue; - } rgb; - - /** - * If `VTERM_COLOR_IS_INDEXED(type)` is true, this member holds the index into - * the colour palette. - */ - struct { - /** - * Same as the top-level `type` member stored in VTermColor. - */ - uint8_t type; - - /** - * Index into the colour map. - */ - uint8_t idx; - } indexed; -} VTermColor; - -/** - * Constructs a new VTermColor instance representing the given RGB values. - */ -static inline void vterm_color_rgb(VTermColor *col, uint8_t red, uint8_t green, - uint8_t blue) -{ - col->type = VTERM_COLOR_RGB; - col->rgb.red = red; - col->rgb.green = green; - col->rgb.blue = blue; -} - -/** - * Construct a new VTermColor instance representing an indexed color with the - * given index. - */ -static inline void vterm_color_indexed(VTermColor *col, uint8_t idx) -{ - col->type = VTERM_COLOR_INDEXED; - col->indexed.idx = idx; -} - -/** - * Compares two colours. Returns true if the colors are equal, false otherwise. - */ -int vterm_color_is_equal(const VTermColor *a, const VTermColor *b); - -typedef enum { - /* VTERM_VALUETYPE_NONE = 0 */ - VTERM_VALUETYPE_BOOL = 1, - VTERM_VALUETYPE_INT, - VTERM_VALUETYPE_STRING, - VTERM_VALUETYPE_COLOR, - - VTERM_N_VALUETYPES -} VTermValueType; - -typedef struct { - const char *str; - size_t len : 30; - bool initial : 1; - bool final : 1; -} VTermStringFragment; - -typedef union { - int boolean; - int number; - VTermStringFragment string; - VTermColor color; -} VTermValue; - -typedef enum { - /* VTERM_ATTR_NONE = 0 */ - VTERM_ATTR_BOLD = 1, // bool: 1, 22 - VTERM_ATTR_UNDERLINE, // number: 4, 21, 24 - VTERM_ATTR_ITALIC, // bool: 3, 23 - VTERM_ATTR_BLINK, // bool: 5, 25 - VTERM_ATTR_REVERSE, // bool: 7, 27 - VTERM_ATTR_CONCEAL, // bool: 8, 28 - VTERM_ATTR_STRIKE, // bool: 9, 29 - VTERM_ATTR_FONT, // number: 10-19 - VTERM_ATTR_FOREGROUND, // color: 30-39 90-97 - VTERM_ATTR_BACKGROUND, // color: 40-49 100-107 - VTERM_ATTR_SMALL, // bool: 73, 74, 75 - VTERM_ATTR_BASELINE, // number: 73, 74, 75 - VTERM_ATTR_URI, // number - - VTERM_N_ATTRS -} VTermAttr; - -typedef enum { - /* VTERM_PROP_NONE = 0 */ - VTERM_PROP_CURSORVISIBLE = 1, // bool - VTERM_PROP_CURSORBLINK, // bool - VTERM_PROP_ALTSCREEN, // bool - VTERM_PROP_TITLE, // string - VTERM_PROP_ICONNAME, // string - VTERM_PROP_REVERSE, // bool - VTERM_PROP_CURSORSHAPE, // number - VTERM_PROP_MOUSE, // number - VTERM_PROP_FOCUSREPORT, // bool - - VTERM_N_PROPS -} VTermProp; - -enum { - VTERM_PROP_CURSORSHAPE_BLOCK = 1, - VTERM_PROP_CURSORSHAPE_UNDERLINE, - VTERM_PROP_CURSORSHAPE_BAR_LEFT, - - VTERM_N_PROP_CURSORSHAPES -}; - -enum { - VTERM_PROP_MOUSE_NONE = 0, - VTERM_PROP_MOUSE_CLICK, - VTERM_PROP_MOUSE_DRAG, - VTERM_PROP_MOUSE_MOVE, - - VTERM_N_PROP_MOUSES -}; - -typedef enum { - VTERM_SELECTION_CLIPBOARD = (1<<0), - VTERM_SELECTION_PRIMARY = (1<<1), - VTERM_SELECTION_SECONDARY = (1<<2), - VTERM_SELECTION_SELECT = (1<<3), - VTERM_SELECTION_CUT0 = (1<<4), /* also CUT1 .. CUT7 by bitshifting */ -} VTermSelectionMask; - -typedef struct { - schar_T schar; - int width; - unsigned int protected_cell:1; /* DECSCA-protected against DECSEL/DECSED */ - unsigned int dwl:1; /* DECDWL or DECDHL double-width line */ - unsigned int dhl:2; /* DECDHL double-height line (1=top 2=bottom) */ -} VTermGlyphInfo; - -typedef struct { - unsigned int doublewidth:1; /* DECDWL or DECDHL line */ - unsigned int doubleheight:2; /* DECDHL line (1=top 2=bottom) */ - unsigned int continuation:1; /* Line is a flow continuation of the previous */ -} VTermLineInfo; - -/* Copies of VTermState fields that the 'resize' callback might have reason to - * edit. 'resize' callback gets total control of these fields and may - * free-and-reallocate them if required. They will be copied back from the - * struct after the callback has returned. - */ -typedef struct { - VTermPos pos; /* current cursor position */ - VTermLineInfo *lineinfos[2]; /* [1] may be NULL */ -} VTermStateFields; - -typedef struct { - /* libvterm relies on this memory to be zeroed out before it is returned - * by the allocator. */ - void *(*malloc)(size_t size, void *allocdata); - void (*free)(void *ptr, void *allocdata); -} VTermAllocatorFunctions; - -void vterm_check_version(int major, int minor); - -struct VTermBuilder { - int ver; /* currently unused but reserved for some sort of ABI version flag */ - - int rows, cols; - - const VTermAllocatorFunctions *allocator; - void *allocdata; - - /* Override default sizes for various structures */ - size_t outbuffer_len; /* default: 4096 */ - size_t tmpbuffer_len; /* default: 4096 */ -}; - -VTerm *vterm_build(const struct VTermBuilder *builder); - -/* A convenient shortcut for default cases */ -VTerm *vterm_new(int rows, int cols); -/* This shortcuts are generally discouraged in favour of just using vterm_build() */ -VTerm *vterm_new_with_allocator(int rows, int cols, VTermAllocatorFunctions *funcs, void *allocdata); - -void vterm_free(VTerm* vt); - -void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp); -void vterm_set_size(VTerm *vt, int rows, int cols); - -int vterm_get_utf8(const VTerm *vt); -void vterm_set_utf8(VTerm *vt, int is_utf8); - -size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len); - -/* Setting output callback will override the buffer logic */ -typedef void VTermOutputCallback(const char *s, size_t len, void *user); -void vterm_output_set_callback(VTerm *vt, VTermOutputCallback *func, void *user); - -/* These buffer functions only work if output callback is NOT set - * These are deprecated and will be removed in a later version */ -size_t vterm_output_get_buffer_size(const VTerm *vt); -size_t vterm_output_get_buffer_current(const VTerm *vt); -size_t vterm_output_get_buffer_remaining(const VTerm *vt); - -/* This too */ -size_t vterm_output_read(VTerm *vt, char *buffer, size_t len); - -void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod); -void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod); - -void vterm_keyboard_start_paste(VTerm *vt); -void vterm_keyboard_end_paste(VTerm *vt); - -void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod); -void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod); - -// ------------ -// Parser layer -// ------------ - -/* Flag to indicate non-final subparameters in a single CSI parameter. - * Consider - * CSI 1;2:3:4;5a - * 1 4 and 5 are final. - * 2 and 3 are non-final and will have this bit set - * - * Don't confuse this with the final byte of the CSI escape; 'a' in this case. - */ -#define CSI_ARG_FLAG_MORE (1U<<31) -#define CSI_ARG_MASK (~(1U<<31)) - -#define CSI_ARG_HAS_MORE(a) ((a) & CSI_ARG_FLAG_MORE) -#define CSI_ARG(a) ((a) & CSI_ARG_MASK) - -/* Can't use -1 to indicate a missing argument; use this instead */ -#define CSI_ARG_MISSING ((1UL<<31)-1) - -#define CSI_ARG_IS_MISSING(a) (CSI_ARG(a) == CSI_ARG_MISSING) -#define CSI_ARG_OR(a,def) (CSI_ARG(a) == CSI_ARG_MISSING ? (def) : CSI_ARG(a)) -#define CSI_ARG_COUNT(a) (CSI_ARG(a) == CSI_ARG_MISSING || CSI_ARG(a) == 0 ? 1 : CSI_ARG(a)) - -typedef struct { - int (*text)(const char *bytes, size_t len, void *user); - int (*control)(unsigned char control, void *user); - int (*escape)(const char *bytes, size_t len, void *user); - int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user); - int (*osc)(int command, VTermStringFragment frag, void *user); - int (*dcs)(const char *command, size_t commandlen, VTermStringFragment frag, void *user); - int (*apc)(VTermStringFragment frag, void *user); - int (*pm)(VTermStringFragment frag, void *user); - int (*sos)(VTermStringFragment frag, void *user); - int (*resize)(int rows, int cols, void *user); -} VTermParserCallbacks; - -void vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user); -void *vterm_parser_get_cbdata(VTerm *vt); - -/* Normally NUL, CAN, SUB and DEL are ignored. Setting this true causes them - * to be emitted by the 'control' callback - */ -void vterm_parser_set_emit_nul(VTerm *vt, bool emit); - -// ----------- -// State layer -// ----------- - -typedef struct { - int (*putglyph)(VTermGlyphInfo *info, VTermPos pos, void *user); - int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user); - int (*scrollrect)(VTermRect rect, int downward, int rightward, void *user); - int (*moverect)(VTermRect dest, VTermRect src, void *user); - int (*erase)(VTermRect rect, int selective, void *user); - int (*initpen)(void *user); - int (*setpenattr)(VTermAttr attr, VTermValue *val, void *user); - int (*settermprop)(VTermProp prop, VTermValue *val, void *user); - int (*bell)(void *user); - int (*resize)(int rows, int cols, VTermStateFields *fields, void *user); - int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, void *user); - int (*sb_clear)(void *user); -} VTermStateCallbacks; - -typedef struct { - int (*control)(unsigned char control, void *user); - int (*csi)(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user); - int (*osc)(int command, VTermStringFragment frag, void *user); - int (*dcs)(const char *command, size_t commandlen, VTermStringFragment frag, void *user); - int (*apc)(VTermStringFragment frag, void *user); - int (*pm)(VTermStringFragment frag, void *user); - int (*sos)(VTermStringFragment frag, void *user); -} VTermStateFallbacks; - -typedef struct { - int (*set)(VTermSelectionMask mask, VTermStringFragment frag, void *user); - int (*query)(VTermSelectionMask mask, void *user); -} VTermSelectionCallbacks; - -VTermState *vterm_obtain_state(VTerm *vt); - -void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user); -void *vterm_state_get_cbdata(VTermState *state); - -void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermStateFallbacks *fallbacks, void *user); -void *vterm_state_get_unrecognised_fbdata(VTermState *state); - -void vterm_state_reset(VTermState *state, int hard); -void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos); -void vterm_state_get_default_colors(const VTermState *state, VTermColor *default_fg, VTermColor *default_bg); -void vterm_state_get_palette_color(const VTermState *state, int index, VTermColor *col); -void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, const VTermColor *default_bg); -void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col); -void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright); -int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val); -int vterm_state_set_penattr(VTermState *state, VTermAttr attr, VTermValueType type, VTermValue *val); -int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val); -void vterm_state_focus_in(VTermState *state); -void vterm_state_focus_out(VTermState *state); -const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row); - -/** - * Makes sure that the given color `col` is indeed an RGB colour. After this - * function returns, VTERM_COLOR_IS_RGB(col) will return true, while all other - * flags stored in `col->type` will have been reset. - * - * @param state is the VTermState instance from which the colour palette should - * be extracted. - * @param col is a pointer at the VTermColor instance that should be converted - * to an RGB colour. - */ -void vterm_state_convert_color_to_rgb(const VTermState *state, VTermColor *col); - -void vterm_state_set_selection_callbacks(VTermState *state, const VTermSelectionCallbacks *callbacks, void *user, - char *buffer, size_t buflen); - -void vterm_state_send_selection(VTermState *state, VTermSelectionMask mask, VTermStringFragment frag); - -// ------------ -// Screen layer -// ------------ - -typedef struct { - unsigned int bold : 1; - unsigned int underline : 2; - unsigned int italic : 1; - unsigned int blink : 1; - unsigned int reverse : 1; - unsigned int conceal : 1; - unsigned int strike : 1; - unsigned int font : 4; /* 0 to 9 */ - unsigned int dwl : 1; /* On a DECDWL or DECDHL line */ - unsigned int dhl : 2; /* On a DECDHL line (1=top 2=bottom) */ - unsigned int small : 1; - unsigned int baseline : 2; -} VTermScreenCellAttrs; - -enum { - VTERM_UNDERLINE_OFF, - VTERM_UNDERLINE_SINGLE, - VTERM_UNDERLINE_DOUBLE, - VTERM_UNDERLINE_CURLY, -}; - -enum { - VTERM_BASELINE_NORMAL, - VTERM_BASELINE_RAISE, - VTERM_BASELINE_LOWER, -}; - -typedef struct { - schar_T schar; - char width; - VTermScreenCellAttrs attrs; - VTermColor fg, bg; - int uri; -} VTermScreenCell; - -typedef struct { - int (*damage)(VTermRect rect, void *user); - int (*moverect)(VTermRect dest, VTermRect src, void *user); - int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user); - int (*settermprop)(VTermProp prop, VTermValue *val, void *user); - int (*bell)(void *user); - int (*resize)(int rows, int cols, void *user); - int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user); - int (*sb_popline)(int cols, VTermScreenCell *cells, void *user); - int (*sb_clear)(void* user); -} VTermScreenCallbacks; - -VTermScreen *vterm_obtain_screen(VTerm *vt); - -void vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, void *user); -void *vterm_screen_get_cbdata(VTermScreen *screen); - -void vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, const VTermStateFallbacks *fallbacks, void *user); -void *vterm_screen_get_unrecognised_fbdata(VTermScreen *screen); - -void vterm_screen_enable_reflow(VTermScreen *screen, bool reflow); - -// Back-compat alias for the brief time it was in 0.3-RC1 -#define vterm_screen_set_reflow vterm_screen_enable_reflow - -void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen); - -typedef enum { - VTERM_DAMAGE_CELL, /* every cell */ - VTERM_DAMAGE_ROW, /* entire rows */ - VTERM_DAMAGE_SCREEN, /* entire screen */ - VTERM_DAMAGE_SCROLL, /* entire screen + scrollrect */ - - VTERM_N_DAMAGES -} VTermDamageSize; - -void vterm_screen_flush_damage(VTermScreen *screen); -void vterm_screen_set_damage_merge(VTermScreen *screen, VTermDamageSize size); - -void vterm_screen_reset(VTermScreen *screen, int hard); - -/* Neither of these functions NUL-terminate the buffer */ -size_t vterm_screen_get_chars(const VTermScreen *screen, uint32_t *chars, size_t len, const VTermRect rect); -size_t vterm_screen_get_text(const VTermScreen *screen, char *str, size_t len, const VTermRect rect); - -typedef enum { - VTERM_ATTR_BOLD_MASK = 1 << 0, - VTERM_ATTR_UNDERLINE_MASK = 1 << 1, - VTERM_ATTR_ITALIC_MASK = 1 << 2, - VTERM_ATTR_BLINK_MASK = 1 << 3, - VTERM_ATTR_REVERSE_MASK = 1 << 4, - VTERM_ATTR_STRIKE_MASK = 1 << 5, - VTERM_ATTR_FONT_MASK = 1 << 6, - VTERM_ATTR_FOREGROUND_MASK = 1 << 7, - VTERM_ATTR_BACKGROUND_MASK = 1 << 8, - VTERM_ATTR_CONCEAL_MASK = 1 << 9, - VTERM_ATTR_SMALL_MASK = 1 << 10, - VTERM_ATTR_BASELINE_MASK = 1 << 11, - VTERM_ATTR_URI_MASK = 1 << 12, - - VTERM_ALL_ATTRS_MASK = (1 << 13) - 1 -} VTermAttrMask; - -int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs); - -int vterm_screen_get_cell(const VTermScreen *screen, VTermPos pos, VTermScreenCell *cell); - -int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos); - -/** - * Same as vterm_state_convert_color_to_rgb(), but takes a `screen` instead of a `state` - * instance. - */ -void vterm_screen_convert_color_to_rgb(const VTermScreen *screen, VTermColor *col); - -/** - * Similar to vterm_state_set_default_colors(), but also resets colours in the - * screen buffer(s) - */ -void vterm_screen_set_default_colors(VTermScreen *screen, const VTermColor *default_fg, const VTermColor *default_bg); - -// --------- -// Utilities -// --------- - -VTermValueType vterm_get_attr_type(VTermAttr attr); -VTermValueType vterm_get_prop_type(VTermProp prop); - -void vterm_scroll_rect(VTermRect rect, - int downward, - int rightward, - int (*moverect)(VTermRect src, VTermRect dest, void *user), - int (*eraserect)(VTermRect rect, int selective, void *user), - void *user); - -void vterm_copy_cells(VTermRect dest, - VTermRect src, - void (*copycell)(VTermPos dest, VTermPos src, void *user), - void *user); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/vterm/vterm_internal.h b/src/vterm/vterm_internal.h deleted file mode 100644 index 8f1722dd93..0000000000 --- a/src/vterm/vterm_internal.h +++ /dev/null @@ -1,298 +0,0 @@ -#ifndef __VTERM_INTERNAL_H__ -#define __VTERM_INTERNAL_H__ - -#include "vterm.h" - -#include <stdarg.h> -#include "nvim/mbyte.h" - -#if defined(__GNUC__) -# define INTERNAL __attribute__((visibility("internal"))) -#else -# define INTERNAL -#endif - -#ifdef DEBUG -# define DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__) -#else -# define DEBUG_LOG(...) -#endif - -#define ESC_S "\x1b" - -#define INTERMED_MAX 16 - -#define CSI_ARGS_MAX 16 -#define CSI_LEADER_MAX 16 - -#define BUFIDX_PRIMARY 0 -#define BUFIDX_ALTSCREEN 1 - -typedef struct VTermEncoding VTermEncoding; - -typedef struct { - VTermEncoding *enc; - - // This size should be increased if required by other stateful encodings - char data[4*sizeof(uint32_t)]; -} VTermEncodingInstance; - -struct VTermPen -{ - VTermColor fg; - VTermColor bg; - int uri; - unsigned int bold:1; - unsigned int underline:2; - unsigned int italic:1; - unsigned int blink:1; - unsigned int reverse:1; - unsigned int conceal:1; - unsigned int strike:1; - unsigned int font:4; /* To store 0-9 */ - unsigned int small:1; - unsigned int baseline:2; -}; - -struct VTermState -{ - VTerm *vt; - - const VTermStateCallbacks *callbacks; - void *cbdata; - - const VTermStateFallbacks *fallbacks; - void *fbdata; - - int rows; - int cols; - - /* Current cursor position */ - VTermPos pos; - - int at_phantom; /* True if we're on the "81st" phantom column to defer a wraparound */ - - int scrollregion_top; - int scrollregion_bottom; /* -1 means unbounded */ -#define SCROLLREGION_BOTTOM(state) ((state)->scrollregion_bottom > -1 ? (state)->scrollregion_bottom : (state)->rows) - int scrollregion_left; -#define SCROLLREGION_LEFT(state) ((state)->mode.leftrightmargin ? (state)->scrollregion_left : 0) - int scrollregion_right; /* -1 means unbounded */ -#define SCROLLREGION_RIGHT(state) ((state)->mode.leftrightmargin && (state)->scrollregion_right > -1 ? (state)->scrollregion_right : (state)->cols) - - /* Bitvector of tab stops */ - unsigned char *tabstops; - - /* Primary and Altscreen; lineinfos[1] is lazily allocated as needed */ - VTermLineInfo *lineinfos[2]; - - /* lineinfo will == lineinfos[0] or lineinfos[1], depending on altscreen */ - VTermLineInfo *lineinfo; -#define ROWWIDTH(state,row) ((state)->lineinfo[(row)].doublewidth ? ((state)->cols / 2) : (state)->cols) -#define THISROWWIDTH(state) ROWWIDTH(state, (state)->pos.row) - - /* Mouse state */ - int mouse_col, mouse_row; - int mouse_buttons; - int mouse_flags; -#define MOUSE_WANT_CLICK 0x01 -#define MOUSE_WANT_DRAG 0x02 -#define MOUSE_WANT_MOVE 0x04 - - enum { MOUSE_X10, MOUSE_UTF8, MOUSE_SGR, MOUSE_RXVT } mouse_protocol; - - /* Last glyph output, for Unicode recombining purposes */ - char grapheme_buf[MAX_SCHAR_SIZE]; - size_t grapheme_len; - uint32_t grapheme_last; // last added UTF-32 char - GraphemeState grapheme_state; - int combine_width; // The width of the glyph above - VTermPos combine_pos; // Position before movement - - struct { - unsigned int keypad:1; - unsigned int cursor:1; - unsigned int autowrap:1; - unsigned int insert:1; - unsigned int newline:1; - unsigned int cursor_visible:1; - unsigned int cursor_blink:1; - unsigned int cursor_shape:2; - unsigned int alt_screen:1; - unsigned int origin:1; - unsigned int screen:1; - unsigned int leftrightmargin:1; - unsigned int bracketpaste:1; - unsigned int report_focus:1; - } mode; - - VTermEncodingInstance encoding[4], encoding_utf8; - int gl_set, gr_set, gsingle_set; - - struct VTermPen pen; - - VTermColor default_fg; - VTermColor default_bg; - VTermColor colors[16]; // Store the 8 ANSI and the 8 ANSI high-brights only - - int bold_is_highbright; - - unsigned int protected_cell : 1; - - /* Saved state under DEC mode 1048/1049 */ - struct { - VTermPos pos; - struct VTermPen pen; - - struct { - unsigned int cursor_visible:1; - unsigned int cursor_blink:1; - unsigned int cursor_shape:2; - } mode; - } saved; - - /* Temporary state for DECRQSS parsing */ - union { - char decrqss[4]; - struct { - uint16_t mask; - enum { - SELECTION_INITIAL, - SELECTION_SELECTED, - SELECTION_QUERY, - SELECTION_SET_INITIAL, - SELECTION_SET, - SELECTION_INVALID, - } state : 8; - uint32_t recvpartial; - uint32_t sendpartial; - } selection; - } tmp; - - struct { - const VTermSelectionCallbacks *callbacks; - void *user; - char *buffer; - size_t buflen; - } selection; -}; - -struct VTerm -{ - const VTermAllocatorFunctions *allocator; - void *allocdata; - - int rows; - int cols; - - struct { - unsigned int utf8:1; - unsigned int ctrl8bit:1; - } mode; - - struct { - enum VTermParserState { - NORMAL, - CSI_LEADER, - CSI_ARGS, - CSI_INTERMED, - DCS_COMMAND, - /* below here are the "string states" */ - OSC_COMMAND, - OSC, - DCS, - APC, - PM, - SOS, - } state; - - bool in_esc : 1; - - int intermedlen; - char intermed[INTERMED_MAX]; - - union { - struct { - int leaderlen; - char leader[CSI_LEADER_MAX]; - - int argi; - long args[CSI_ARGS_MAX]; - } csi; - struct { - int command; - } osc; - struct { - int commandlen; - char command[CSI_LEADER_MAX]; - } dcs; - } v; - - const VTermParserCallbacks *callbacks; - void *cbdata; - - bool string_initial; - - bool emit_nul; - } parser; - - /* len == malloc()ed size; cur == number of valid bytes */ - - VTermOutputCallback *outfunc; - void *outdata; - - char *outbuffer; - size_t outbuffer_len; - size_t outbuffer_cur; - - char *tmpbuffer; - size_t tmpbuffer_len; - - VTermState *state; - VTermScreen *screen; -}; - -struct VTermEncoding { - void (*init) (VTermEncoding *enc, void *data); - void (*decode)(VTermEncoding *enc, void *data, - uint32_t cp[], int *cpi, int cplen, - const char bytes[], size_t *pos, size_t len); -}; - -typedef enum { - ENC_UTF8, - ENC_SINGLE_94 -} VTermEncodingType; - -void *vterm_allocator_malloc(VTerm *vt, size_t size); -void vterm_allocator_free(VTerm *vt, void *ptr); - -void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len); -void vterm_push_output_vsprintf(VTerm *vt, const char *format, va_list args); -void vterm_push_output_sprintf(VTerm *vt, const char *format, ...); -void vterm_push_output_sprintf_ctrl(VTerm *vt, unsigned char ctrl, const char *fmt, ...); -void vterm_push_output_sprintf_str(VTerm *vt, unsigned char ctrl, bool term, const char *fmt, ...); - -void vterm_state_free(VTermState *state); - -void vterm_state_newpen(VTermState *state); -void vterm_state_resetpen(VTermState *state); -void vterm_state_setpen(VTermState *state, const long args[], int argcount); -int vterm_state_getpen(VTermState *state, long args[], int argcount); -void vterm_state_savepen(VTermState *state, int save); - -enum { - C1_SS3 = 0x8f, - C1_DCS = 0x90, - C1_CSI = 0x9b, - C1_ST = 0x9c, - C1_OSC = 0x9d, -}; - -void vterm_state_push_output_sprintf_CSI(VTermState *vts, const char *format, ...); - -void vterm_screen_free(VTermScreen *screen); - -VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation); - -#endif diff --git a/test/benchmark/decor_spec.lua b/test/benchmark/decor_spec.lua index 0994023c2d..1b7e763a09 100644 --- a/test/benchmark/decor_spec.lua +++ b/test/benchmark/decor_spec.lua @@ -6,8 +6,7 @@ describe('decor perf', function() before_each(n.clear) it('can handle long lines', function() - local screen = Screen.new(100, 101) - screen:attach() + Screen.new(100, 101) local result = exec_lua [==[ local ephemeral_pattern = { @@ -99,4 +98,43 @@ describe('decor perf', function() print('\nTotal ' .. fmt(total) .. '\nDecoration provider: ' .. fmt(provider)) end) + + it('can handle full screen of highlighting', function() + Screen.new(100, 51) + + local result = exec_lua(function() + local long_line = 'local a={' .. ('a=5,'):rep(22) .. '}' + local lines = {} + for _ = 1, 50 do + table.insert(lines, long_line) + end + vim.api.nvim_buf_set_lines(0, 0, 0, false, lines) + vim.api.nvim_win_set_cursor(0, { 1, 0 }) + vim.treesitter.start(0, 'lua') + + local total = {} + for _ = 1, 100 do + local tic = vim.uv.hrtime() + vim.cmd 'redraw!' + local toc = vim.uv.hrtime() + table.insert(total, toc - tic) + end + + return { total } + end) + + local total = unpack(result) + table.sort(total) + + local ms = 1 / 1000000 + local res = string.format( + 'min, 25%%, median, 75%%, max:\n\t%0.1fms,\t%0.1fms,\t%0.1fms,\t%0.1fms,\t%0.1fms', + total[1] * ms, + total[1 + math.floor(#total * 0.25)] * ms, + total[1 + math.floor(#total * 0.5)] * ms, + total[1 + math.floor(#total * 0.75)] * ms, + total[#total] * ms + ) + print('\nTotal ' .. res) + end) end) diff --git a/test/client/msgpack_rpc_stream.lua b/test/client/rpc_stream.lua index 7131940a58..9f2672bcf9 100644 --- a/test/client/msgpack_rpc_stream.lua +++ b/test/client/rpc_stream.lua @@ -1,34 +1,41 @@ +--- +--- Reading/writing of msgpack over any of the stream types from `uv_stream.lua`. +--- Does not implement the RPC protocol, see `session.lua` for that. +--- + local mpack = vim.mpack local Response = {} Response.__index = Response -function Response.new(msgpack_rpc_stream, request_id) +function Response.new(rpc_stream, request_id) return setmetatable({ - _msgpack_rpc_stream = msgpack_rpc_stream, + _rpc_stream = rpc_stream, _request_id = request_id, }, Response) end function Response:send(value, is_error) - local data = self._msgpack_rpc_stream._session:reply(self._request_id) + local data = self._rpc_stream._session:reply(self._request_id) if is_error then - data = data .. self._msgpack_rpc_stream._pack(value) - data = data .. self._msgpack_rpc_stream._pack(mpack.NIL) + data = data .. self._rpc_stream._pack(value) + data = data .. self._rpc_stream._pack(mpack.NIL) else - data = data .. self._msgpack_rpc_stream._pack(mpack.NIL) - data = data .. self._msgpack_rpc_stream._pack(value) + data = data .. self._rpc_stream._pack(mpack.NIL) + data = data .. self._rpc_stream._pack(value) end - self._msgpack_rpc_stream._stream:write(data) + self._rpc_stream._stream:write(data) end ---- @class test.MsgpackRpcStream +--- Nvim msgpack RPC stream. +--- +--- @class test.RpcStream --- @field private _stream test.Stream --- @field private __pack table -local MsgpackRpcStream = {} -MsgpackRpcStream.__index = MsgpackRpcStream +local RpcStream = {} +RpcStream.__index = RpcStream -function MsgpackRpcStream.new(stream) +function RpcStream.new(stream) return setmetatable({ _stream = stream, _pack = mpack.Packer(), @@ -50,10 +57,10 @@ function MsgpackRpcStream.new(stream) }, }), }), - }, MsgpackRpcStream) + }, RpcStream) end -function MsgpackRpcStream:write(method, args, response_cb) +function RpcStream:write(method, args, response_cb) local data if response_cb then assert(type(response_cb) == 'function') @@ -66,10 +73,10 @@ function MsgpackRpcStream:write(method, args, response_cb) self._stream:write(data) end -function MsgpackRpcStream:read_start(request_cb, notification_cb, eof_cb) +function RpcStream:read_start(on_request, on_notification, on_eof) self._stream:read_start(function(data) if not data then - return eof_cb() + return on_eof() end local type, id_or_cb, method_or_error, args_or_result local pos = 1 @@ -78,9 +85,9 @@ function MsgpackRpcStream:read_start(request_cb, notification_cb, eof_cb) type, id_or_cb, method_or_error, args_or_result, pos = self._session:receive(data, pos) if type == 'request' or type == 'notification' then if type == 'request' then - request_cb(method_or_error, args_or_result, Response.new(self, id_or_cb)) + on_request(method_or_error, args_or_result, Response.new(self, id_or_cb)) else - notification_cb(method_or_error, args_or_result) + on_notification(method_or_error, args_or_result) end elseif type == 'response' then if method_or_error == mpack.NIL then @@ -94,12 +101,12 @@ function MsgpackRpcStream:read_start(request_cb, notification_cb, eof_cb) end) end -function MsgpackRpcStream:read_stop() +function RpcStream:read_stop() self._stream:read_stop() end -function MsgpackRpcStream:close(signal) +function RpcStream:close(signal) self._stream:close(signal) end -return MsgpackRpcStream +return RpcStream diff --git a/test/client/session.lua b/test/client/session.lua index f1f46c5efe..a5839e012a 100644 --- a/test/client/session.lua +++ b/test/client/session.lua @@ -1,12 +1,18 @@ +--- +--- Nvim msgpack-RPC protocol session. Manages requests/notifications/responses. +--- + local uv = vim.uv -local MsgpackRpcStream = require('test.client.msgpack_rpc_stream') +local RpcStream = require('test.client.rpc_stream') +--- Nvim msgpack-RPC protocol session. Manages requests/notifications/responses. +--- --- @class test.Session ---- @field private _pending_messages string[] ---- @field private _msgpack_rpc_stream test.MsgpackRpcStream +--- @field private _pending_messages string[] Requests/notifications received from the remote end. +--- @field private _rpc_stream test.RpcStream --- @field private _prepare uv.uv_prepare_t --- @field private _timer uv.uv_timer_t ---- @field private _is_running boolean +--- @field private _is_running boolean true during `Session:run()` scope. --- @field exec_lua_setup boolean local Session = {} Session.__index = Session @@ -51,9 +57,10 @@ local function coroutine_exec(func, ...) end)) end +--- Creates a new msgpack-RPC session. function Session.new(stream) return setmetatable({ - _msgpack_rpc_stream = MsgpackRpcStream.new(stream), + _rpc_stream = RpcStream.new(stream), _pending_messages = {}, _prepare = uv.new_prepare(), _timer = uv.new_timer(), @@ -91,10 +98,13 @@ function Session:next_message(timeout) return table.remove(self._pending_messages, 1) end +--- Sends a notification to the RPC endpoint. function Session:notify(method, ...) - self._msgpack_rpc_stream:write(method, { ... }) + self._rpc_stream:write(method, { ... }) end +--- Sends a request to the RPC endpoint. +--- --- @param method string --- @param ... any --- @return boolean, table @@ -114,8 +124,16 @@ function Session:request(method, ...) return true, result end ---- Runs the event loop. +--- Processes incoming RPC requests/notifications until exhausted. +--- +--- TODO(justinmk): luaclient2 avoids this via uvutil.cb_wait() + uvutil.add_idle_call()? +--- +--- @param request_cb function Handles requests from the sever to the local end. +--- @param notification_cb function Handles notifications from the sever to the local end. +--- @param setup_cb function +--- @param timeout number function Session:run(request_cb, notification_cb, setup_cb, timeout) + --- Handles an incoming request. local function on_request(method, args, response) coroutine_exec(request_cb, method, args, function(status, result, flag) if status then @@ -126,6 +144,7 @@ function Session:run(request_cb, notification_cb, setup_cb, timeout) end) end + --- Handles an incoming notification. local function on_notification(method, args) coroutine_exec(notification_cb, method, args) end @@ -160,39 +179,45 @@ function Session:close(signal) if not self._prepare:is_closing() then self._prepare:close() end - self._msgpack_rpc_stream:close(signal) + self._rpc_stream:close(signal) self.closed = true end +--- Sends a request to the RPC endpoint, without blocking (schedules a coroutine). function Session:_yielding_request(method, args) return coroutine.yield(function(co) - self._msgpack_rpc_stream:write(method, args, function(err, result) + self._rpc_stream:write(method, args, function(err, result) resume(co, err, result) end) end) end +--- Sends a request to the RPC endpoint, and blocks (polls event loop) until a response is received. function Session:_blocking_request(method, args) local err, result + -- Invoked when a request is received from the remote end. local function on_request(method_, args_, response) table.insert(self._pending_messages, { 'request', method_, args_, response }) end + -- Invoked when a notification is received from the remote end. local function on_notification(method_, args_) table.insert(self._pending_messages, { 'notification', method_, args_ }) end - self._msgpack_rpc_stream:write(method, args, function(e, r) + self._rpc_stream:write(method, args, function(e, r) err = e result = r uv.stop() end) + -- Poll for incoming requests/notifications received from the remote end. self:_run(on_request, on_notification) return (err or self.eof_err), result end +--- Polls for incoming requests/notifications received from the remote end. function Session:_run(request_cb, notification_cb, timeout) if type(timeout) == 'number' then self._prepare:start(function() @@ -202,14 +227,21 @@ function Session:_run(request_cb, notification_cb, timeout) self._prepare:stop() end) end - self._msgpack_rpc_stream:read_start(request_cb, notification_cb, function() + self._rpc_stream:read_start(request_cb, notification_cb, function() uv.stop() - self.eof_err = { 1, 'EOF was received from Nvim. Likely the Nvim process crashed.' } + + --- @diagnostic disable-next-line: invisible + local stderr = self._rpc_stream._stream.stderr --[[@as string?]] + -- See if `ProcStream.stderr` has anything useful. + stderr = '' ~= ((stderr or ''):match('^%s*(.*%S)') or '') and ' stderr:\n' .. stderr or '' + + self.eof_err = { 1, 'EOF was received from Nvim. Likely the Nvim process crashed.' .. stderr } end) uv.run() self._prepare:stop() self._timer:stop() - self._msgpack_rpc_stream:read_stop() + self._rpc_stream:read_stop() end +--- Nvim msgpack-RPC session. return Session diff --git a/test/client/uv_stream.lua b/test/client/uv_stream.lua index adf002ba1e..6e1a6995be 100644 --- a/test/client/uv_stream.lua +++ b/test/client/uv_stream.lua @@ -1,3 +1,8 @@ +--- +--- Basic stream types. +--- See `rpc_stream.lua` for the msgpack layer. +--- + local uv = vim.uv --- @class test.Stream @@ -6,6 +11,8 @@ local uv = vim.uv --- @field read_stop fun(self) --- @field close fun(self, signal?: string) +--- Stream over given pipes. +--- --- @class vim.StdioStream : test.Stream --- @field private _in uv.uv_pipe_t --- @field private _out uv.uv_pipe_t @@ -45,6 +52,8 @@ function StdioStream:close() self._out:close() end +--- Stream over a named pipe or TCP socket. +--- --- @class test.SocketStream : test.Stream --- @field package _stream_error? string --- @field package _socket uv.uv_pipe_t @@ -109,26 +118,54 @@ function SocketStream:close() uv.close(self._socket) end ---- @class test.ChildProcessStream : test.Stream +--- Stream over child process stdio. +--- +--- @class test.ProcStream : test.Stream --- @field private _proc uv.uv_process_t --- @field private _pid integer --- @field private _child_stdin uv.uv_pipe_t --- @field private _child_stdout uv.uv_pipe_t +--- @field private _child_stderr uv.uv_pipe_t +--- Collects stdout (if `collect_text=true`). Treats data as text (CRLF converted to LF). +--- @field stdout string +--- Collects stderr as raw data. +--- @field stderr string +--- Gets stderr+stdout as text (CRLF converted to LF). +--- @field output fun(): string +--- @field stdout_eof boolean +--- @field stderr_eof boolean +--- Collects text into the `stdout` field. +--- @field collect_text boolean +--- Exit code --- @field status integer --- @field signal integer -local ChildProcessStream = {} -ChildProcessStream.__index = ChildProcessStream +local ProcStream = {} +ProcStream.__index = ProcStream +--- Starts child process specified by `argv`. +--- --- @param argv string[] --- @param env string[]? --- @param io_extra uv.uv_pipe_t? ---- @return test.ChildProcessStream -function ChildProcessStream.spawn(argv, env, io_extra) +--- @return test.ProcStream +function ProcStream.spawn(argv, env, io_extra) local self = setmetatable({ - _child_stdin = uv.new_pipe(false), - _child_stdout = uv.new_pipe(false), + collect_text = false, + output = function(self) + if not self.collect_text then + error('set collect_text=true') + end + return (self.stderr .. self.stdout):gsub('\r\n', '\n') + end, + stdout = '', + stderr = '', + stdout_eof = false, + stderr_eof = false, + _child_stdin = assert(uv.new_pipe(false)), + _child_stdout = assert(uv.new_pipe(false)), + _child_stderr = assert(uv.new_pipe(false)), _exiting = false, - }, ChildProcessStream) + }, ProcStream) local prog = argv[1] local args = {} --- @type string[] for i = 2, #argv do @@ -136,13 +173,14 @@ function ChildProcessStream.spawn(argv, env, io_extra) end --- @diagnostic disable-next-line:missing-fields self._proc, self._pid = uv.spawn(prog, { - stdio = { self._child_stdin, self._child_stdout, 1, io_extra }, + stdio = { self._child_stdin, self._child_stdout, self._child_stderr, io_extra }, args = args, --- @diagnostic disable-next-line:assign-type-mismatch env = env, }, function(status, signal) - self.status = status self.signal = signal + -- "Abort" exit may not set status; force to nonzero in that case. + self.status = (0 ~= (status or 0) or 0 == (signal or 0)) and status or (128 + (signal or 0)) end) if not self._proc then @@ -153,24 +191,54 @@ function ChildProcessStream.spawn(argv, env, io_extra) return self end -function ChildProcessStream:write(data) +function ProcStream:write(data) self._child_stdin:write(data) end -function ChildProcessStream:read_start(cb) - self._child_stdout:read_start(function(err, chunk) - if err then - error(err) +function ProcStream:on_read(stream, cb, err, chunk) + if err then + error(err) -- stream read failed? + elseif chunk then + -- Always collect stderr, in case it gives useful info on failure. + if stream == 'stderr' then + self.stderr = self.stderr .. chunk --[[@as string]] + elseif stream == 'stdout' and self.collect_text then + -- Set `stdout` and convert CRLF => LF. + self.stdout = (self.stdout .. chunk):gsub('\r\n', '\n') end + else + -- stderr_eof/stdout_eof + self[stream .. '_eof'] = true ---@type boolean + end + + -- Handler provided by the caller. + if cb then cb(chunk) + end +end + +--- Collects output until the process exits. +function ProcStream:wait() + while not (self.stdout_eof and self.stderr_eof and (self.status or self.signal)) do + uv.run('once') + end +end + +function ProcStream:read_start(on_stdout, on_stderr) + self._child_stdout:read_start(function(err, chunk) + self:on_read('stdout', on_stdout, err, chunk) + end) + self._child_stderr:read_start(function(err, chunk) + self:on_read('stderr', on_stderr, err, chunk) end) end -function ChildProcessStream:read_stop() +function ProcStream:read_stop() self._child_stdout:read_stop() + self._child_stderr:read_stop() end -function ChildProcessStream:close(signal) +function ProcStream:close(signal) if self._closed then return end @@ -178,6 +246,7 @@ function ChildProcessStream:close(signal) self:read_stop() self._child_stdin:close() self._child_stdout:close() + self._child_stderr:close() if type(signal) == 'string' then self._proc:kill('sig' .. signal) end @@ -189,6 +258,6 @@ end return { StdioStream = StdioStream, - ChildProcessStream = ChildProcessStream, + ProcStream = ProcStream, SocketStream = SocketStream, } diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua index a16c6a88e3..fabd9be6d6 100644 --- a/test/functional/api/command_spec.lua +++ b/test/functional/api/command_spec.lua @@ -651,6 +651,11 @@ describe('nvim_create_user_command', function() api.nvim_set_current_buf(bufnr) command('Hello') assert_alive() + eq( + 'Invalid buffer id: 1234', + pcall_err(api.nvim_buf_create_user_command, 1234, 'Hello', '', {}) + ) + assert_alive() end) it('can use a Lua complete function', function() @@ -771,5 +776,9 @@ describe('nvim_del_user_command', function() command('Hello') api.nvim_buf_del_user_command(0, 'Hello') matches('Not an editor command: Hello', pcall_err(command, 'Hello')) + eq('Invalid command (not found): Hello', pcall_err(api.nvim_buf_del_user_command, 0, 'Hello')) + eq('Invalid command (not found): Bye', pcall_err(api.nvim_buf_del_user_command, 0, 'Bye')) + eq('Invalid buffer id: 1234', pcall_err(api.nvim_buf_del_user_command, 1234, 'Hello')) + assert_alive() end) end) diff --git a/test/functional/api/server_requests_spec.lua b/test/functional/api/server_requests_spec.lua index bdd340f6c6..c022ba28de 100644 --- a/test/functional/api/server_requests_spec.lua +++ b/test/functional/api/server_requests_spec.lua @@ -9,7 +9,6 @@ local nvim_prog, command, fn = n.nvim_prog, n.command, n.fn local source, next_msg = n.source, n.next_msg local ok = t.ok local api = n.api -local spawn, merge_args = n.spawn, n.merge_args local set_session = n.set_session local pcall_err = t.pcall_err local assert_alive = n.assert_alive @@ -281,10 +280,9 @@ describe('server -> client', function() end) describe('connecting to another (peer) nvim', function() - local nvim_argv = merge_args(n.nvim_argv, { '--headless' }) local function connect_test(server, mode, address) local serverpid = fn.getpid() - local client = spawn(nvim_argv, false, nil, true) + local client = n.new_session(true) set_session(client) local clientpid = fn.getpid() @@ -312,7 +310,7 @@ describe('server -> client', function() end it('via named pipe', function() - local server = spawn(nvim_argv) + local server = n.new_session(false) set_session(server) local address = fn.serverlist()[1] local first = string.sub(address, 1, 1) @@ -321,7 +319,7 @@ describe('server -> client', function() end) it('via ipv4 address', function() - local server = spawn(nvim_argv) + local server = n.new_session(false) set_session(server) local status, address = pcall(fn.serverstart, '127.0.0.1:') if not status then @@ -332,7 +330,7 @@ describe('server -> client', function() end) it('via ipv6 address', function() - local server = spawn(nvim_argv) + local server = n.new_session(false) set_session(server) local status, address = pcall(fn.serverstart, '::1:') if not status then @@ -343,7 +341,7 @@ describe('server -> client', function() end) it('via hostname', function() - local server = spawn(nvim_argv) + local server = n.new_session(false) set_session(server) local address = fn.serverstart('localhost:') eq('localhost:', string.sub(address, 1, 10)) @@ -351,10 +349,10 @@ describe('server -> client', function() end) it('does not crash on receiving UI events', function() - local server = spawn(nvim_argv) + local server = n.new_session(false) set_session(server) local address = fn.serverlist()[1] - local client = spawn(nvim_argv, false, nil, true) + local client = n.new_session(true) set_session(client) local id = fn.sockconnect('pipe', address, { rpc = true }) diff --git a/test/functional/core/channels_spec.lua b/test/functional/core/channels_spec.lua index dee13d19ae..7b10eb05ef 100644 --- a/test/functional/core/channels_spec.lua +++ b/test/functional/core/channels_spec.lua @@ -5,7 +5,6 @@ local clear, eq, eval, next_msg, ok, source = n.clear, t.eq, n.eval, n.next_msg, local command, fn, api = n.command, n.fn, n.api local matches = t.matches local sleep = vim.uv.sleep -local spawn, nvim_argv = n.spawn, n.nvim_argv local get_session, set_session = n.get_session, n.set_session local nvim_prog = n.nvim_prog local is_os = t.is_os @@ -33,10 +32,10 @@ describe('channels', function() end) pending('can connect to socket', function() - local server = spawn(nvim_argv, nil, nil, true) + local server = n.new_session(true) set_session(server) local address = fn.serverlist()[1] - local client = spawn(nvim_argv, nil, nil, true) + local client = n.new_session(true) set_session(client) source(init) @@ -63,7 +62,7 @@ describe('channels', function() it('dont crash due to garbage in rpc #23781', function() local client = get_session() - local server = spawn(nvim_argv, nil, nil, true) + local server = n.new_session(true) set_session(server) local address = fn.serverlist()[1] set_session(client) diff --git a/test/functional/core/exit_spec.lua b/test/functional/core/exit_spec.lua index 34c3eedbd2..65f6bc28a6 100644 --- a/test/functional/core/exit_spec.lua +++ b/test/functional/core/exit_spec.lua @@ -8,8 +8,6 @@ local feed = n.feed local eval = n.eval local eq = t.eq local run = n.run -local fn = n.fn -local nvim_prog = n.nvim_prog local pcall_err = t.pcall_err local exec_capture = n.exec_capture local poke_eventloop = n.poke_eventloop @@ -69,8 +67,8 @@ describe(':cquit', function() poke_eventloop() assert_alive() else - fn.system({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--headless', '--cmd', cmdline }) - eq(exit_code, eval('v:shell_error')) + local p = n.spawn_wait('--cmd', cmdline) + eq(exit_code, p.status) end end diff --git a/test/functional/core/fileio_spec.lua b/test/functional/core/fileio_spec.lua index cf9715f848..b1a8e21762 100644 --- a/test/functional/core/fileio_spec.lua +++ b/test/functional/core/fileio_spec.lua @@ -31,7 +31,6 @@ local feed_command = n.feed_command local skip = t.skip local is_os = t.is_os local is_ci = t.is_ci -local spawn = n.spawn local set_session = n.set_session describe('fileio', function() @@ -51,12 +50,11 @@ describe('fileio', function() rmdir('Xtest_backupdir with spaces') end) - local args = { nvim_prog, '--clean', '--cmd', 'set nofsync directory=Xtest_startup_swapdir' } + local args = { '--clean', '--cmd', 'set nofsync directory=Xtest_startup_swapdir' } --- Starts a new nvim session and returns an attached screen. - local function startup(extra_args) - extra_args = extra_args or {} - local argv = vim.iter({ args, '--embed', extra_args }):flatten():totable() - local screen_nvim = spawn(argv) + local function startup() + local argv = vim.iter({ args, '--embed' }):flatten():totable() + local screen_nvim = n.new_session(false, { args = argv, merge = false }) set_session(screen_nvim) local screen = Screen.new(70, 10) screen:set_default_attr_ids({ @@ -100,7 +98,8 @@ describe('fileio', function() eq('foozubbaz', trim(read_file('Xtest_startup_file1'))) -- 4. Exit caused by deadly signal (+ 'swapfile'). - local j = fn.jobstart(vim.iter({ args, '--embed' }):flatten():totable(), { rpc = true }) + local j = + fn.jobstart(vim.iter({ nvim_prog, args, '--embed' }):flatten():totable(), { rpc = true }) fn.rpcrequest( j, 'nvim_exec2', diff --git a/test/functional/core/main_spec.lua b/test/functional/core/main_spec.lua index ce4ba1905f..6add49ceae 100644 --- a/test/functional/core/main_spec.lua +++ b/test/functional/core/main_spec.lua @@ -9,7 +9,6 @@ local feed = n.feed local eval = n.eval local clear = n.clear local fn = n.fn -local nvim_prog_abs = n.nvim_prog_abs local write_file = t.write_file local is_os = t.is_os local skip = t.skip @@ -35,7 +34,7 @@ describe('command-line option', function() it('treats - as stdin', function() eq(nil, uv.fs_stat(fname)) fn.system({ - nvim_prog_abs(), + n.nvim_prog, '-u', 'NONE', '-i', @@ -56,41 +55,29 @@ describe('command-line option', function() eq(nil, uv.fs_stat(fname)) eq(true, not not dollar_fname:find('%$%w+')) write_file(dollar_fname, ':call setline(1, "100500")\n:wqall!\n') - fn.system({ - nvim_prog_abs(), - '-u', - 'NONE', - '-i', - 'NONE', - '--headless', + local p = n.spawn_wait( '--cmd', 'set noswapfile shortmess+=IFW fileformats=unix', '-s', dollar_fname, - fname, - }) - eq(0, eval('v:shell_error')) + fname + ) + eq(0, p.status) local attrs = uv.fs_stat(fname) eq(#'100500\n', attrs.size) end) - it('does not crash when run completion in ex mode', function() - fn.system({ - nvim_prog_abs(), - '--clean', - '-e', - '-s', - '--cmd', - 'exe "norm! i\\<C-X>\\<C-V>"', - }) - eq(0, eval('v:shell_error')) + it('does not crash when run completion in Ex mode', function() + local p = + n.spawn_wait('--clean', '-e', '-s', '--cmd', 'exe "norm! i\\<C-X>\\<C-V>"', '--cmd', 'qa!') + eq(0, p.status) end) it('does not crash after reading from stdin in non-headless mode', function() skip(is_os('win')) local screen = Screen.new(40, 8) local args = { - nvim_prog_abs(), + n.nvim_prog, '-u', 'NONE', '-i', @@ -138,51 +125,40 @@ describe('command-line option', function() ]=] end) - it('errors out when trying to use nonexistent file with -s', function() + it('fails when trying to use nonexistent file with -s', function() + local p = n.spawn_wait( + '--cmd', + 'set noswapfile shortmess+=IFW fileformats=unix', + '--cmd', + 'language C', + '-s', + nonexistent_fname + ) eq( 'Cannot open for reading: "' .. nonexistent_fname .. '": no such file or directory\n', - fn.system({ - nvim_prog_abs(), - '-u', - 'NONE', - '-i', - 'NONE', - '--headless', - '--cmd', - 'set noswapfile shortmess+=IFW fileformats=unix', - '--cmd', - 'language C', - '-s', - nonexistent_fname, - }) + --- TODO(justinmk): using `p.output` because Nvim emits CRLF even on non-Win. Emit LF instead? + p:output() ) - eq(2, eval('v:shell_error')) + eq(2, p.status) end) it('errors out when trying to use -s twice', function() write_file(fname, ':call setline(1, "1")\n:wqall!\n') write_file(dollar_fname, ':call setline(1, "2")\n:wqall!\n') - eq( - 'Attempt to open script file again: "-s ' .. dollar_fname .. '"\n', - fn.system({ - nvim_prog_abs(), - '-u', - 'NONE', - '-i', - 'NONE', - '--headless', - '--cmd', - 'set noswapfile shortmess+=IFW fileformats=unix', - '--cmd', - 'language C', - '-s', - fname, - '-s', - dollar_fname, - fname_2, - }) + local p = n.spawn_wait( + '--cmd', + 'set noswapfile shortmess+=IFW fileformats=unix', + '--cmd', + 'language C', + '-s', + fname, + '-s', + dollar_fname, + fname_2 ) - eq(2, eval('v:shell_error')) + --- TODO(justinmk): using `p.output` because Nvim emits CRLF even on non-Win. Emit LF instead? + eq('Attempt to open script file again: "-s ' .. dollar_fname .. '"\n', p:output()) + eq(2, p.status) eq(nil, uv.fs_stat(fname_2)) end) end) @@ -190,8 +166,8 @@ describe('command-line option', function() it('nvim -v, :version', function() matches('Run ":verbose version"', fn.execute(':version')) matches('fall%-back for %$VIM: .*Run :checkhealth', fn.execute(':verbose version')) - matches('Run "nvim %-V1 %-v"', fn.system({ nvim_prog_abs(), '-v' })) - matches('fall%-back for %$VIM: .*Run :checkhealth', fn.system({ nvim_prog_abs(), '-V1', '-v' })) + matches('Run "nvim %-V1 %-v"', n.spawn_wait('-v').stdout) + matches('fall%-back for %$VIM: .*Run :checkhealth', n.spawn_wait('-V1', '-v').stdout) end) if is_os('win') then @@ -205,7 +181,7 @@ describe('command-line option', function() eq( 'some text', fn.system({ - nvim_prog_abs(), + n.nvim_prog, '-es', '+%print', '+q', diff --git a/test/functional/core/remote_spec.lua b/test/functional/core/remote_spec.lua index 6cc28ddeef..1cfa0535f6 100644 --- a/test/functional/core/remote_spec.lua +++ b/test/functional/core/remote_spec.lua @@ -10,10 +10,8 @@ local expect = n.expect local fn = n.fn local insert = n.insert local nvim_prog = n.nvim_prog -local new_argv = n.new_argv local neq = t.neq local set_session = n.set_session -local spawn = n.spawn local tmpname = t.tmpname local write_file = t.write_file @@ -32,8 +30,7 @@ describe('Remote', function() describe('connect to server and', function() local server before_each(function() - server = spawn(new_argv(), true) - set_session(server) + server = n.clear() end) after_each(function() @@ -49,7 +46,7 @@ describe('Remote', function() -- to wait for the remote instance to exit and calling jobwait blocks -- the event loop. If the server event loop is blocked, it can't process -- our incoming --remote calls. - local client_starter = spawn(new_argv(), false, nil, true) + local client_starter = n.new_session(true) set_session(client_starter) -- Call jobstart() and jobwait() in the same RPC request to reduce flakiness. eq( @@ -144,15 +141,8 @@ describe('Remote', function() describe('exits with error on', function() local function run_and_check_exit_code(...) - local bogus_argv = new_argv(...) - - -- Create an nvim instance just to run the remote-invoking nvim. We want - -- to wait for the remote instance to exit and calling jobwait blocks - -- the event loop. If the server event loop is blocked, it can't process - -- our incoming --remote calls. - clear() - -- Call jobstart() and jobwait() in the same RPC request to reduce flakiness. - eq({ 2 }, exec_lua([[return vim.fn.jobwait({ vim.fn.jobstart(...) })]], bogus_argv)) + local p = n.spawn_wait { args = { ... } } + eq(2, p.status) end it('bogus subcommand', function() run_and_check_exit_code('--remote-bogus') diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index 76b0755441..8ecd3dca97 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -77,22 +77,9 @@ describe('startup', function() end) it('--startuptime does not crash on error #31125', function() - eq( - "E484: Can't open file .", - fn.system({ - nvim_prog, - '-u', - 'NONE', - '-i', - 'NONE', - '--headless', - '--startuptime', - '.', - '-c', - '42cquit', - }) - ) - eq(42, api.nvim_get_vvar('shell_error')) + local p = n.spawn_wait('--startuptime', '.', '-c', '42cquit') + eq("E484: Can't open file .", p.stderr) + eq(42, p.status) end) it('-D does not hang #12647', function() @@ -149,13 +136,18 @@ describe('startup', function() vim.list_extend(args, { '-l', (script or 'test/functional/fixtures/startup.lua') }) vim.list_extend(args, lua_args or {}) local out = fn.system(args, input):gsub('\r\n', '\n') - return eq(dedent(expected), out) + if type(expected) == 'function' then + return expected(out) + else + return eq(dedent(expected), out) + end end it('failure modes', function() -- nvim -l <empty> - matches('nvim%.?e?x?e?: Argument missing after: "%-l"', fn.system({ nvim_prog, '-l' })) - eq(1, eval('v:shell_error')) + local proc = n.spawn_wait('-l') + matches('nvim%.?e?x?e?: Argument missing after: "%-l"', proc.stderr) + eq(1, proc.status) end) it('os.exit() sets Nvim exitcode', function() @@ -182,13 +174,13 @@ describe('startup', function() end) it('Lua-error sets Nvim exitcode', function() + local proc = n.spawn_wait('-l', 'test/functional/fixtures/startup-fail.lua') + matches('E5113: .* my pearls!!', proc:output()) + eq(0, proc.signal) + eq(1, proc.status) + eq(0, eval('v:shell_error')) matches( - 'E5113: .* my pearls!!', - fn.system({ nvim_prog, '-l', 'test/functional/fixtures/startup-fail.lua' }) - ) - eq(1, eval('v:shell_error')) - matches( 'E5113: .* %[string "error%("whoa"%)"%]:1: whoa', fn.system({ nvim_prog, '-l', '-' }, 'error("whoa")') ) @@ -295,14 +287,30 @@ describe('startup', function() eq(0, eval('v:shell_error')) end) - it('disables swapfile/shada/config/plugins', function() + it('disables swapfile/shada/config/plugins unless overridden', function() + local script = [[print(('updatecount=%d shadafile=%s loadplugins=%s scripts=%d'):format( + vim.o.updatecount, vim.o.shadafile, tostring(vim.o.loadplugins), math.max(1, #vim.fn.getscriptinfo())))]] + finally(function() + os.remove('Xtest_shada') + end) + assert_l_out( 'updatecount=0 shadafile=NONE loadplugins=false scripts=1\n', nil, nil, '-', - [[print(('updatecount=%d shadafile=%s loadplugins=%s scripts=%d'):format( - vim.o.updatecount, vim.o.shadafile, tostring(vim.o.loadplugins), math.max(1, #vim.fn.getscriptinfo())))]] + script + ) + + -- User can override. + assert_l_out( + function(out) + return matches('updatecount=99 shadafile=Xtest_shada loadplugins=true scripts=2%d\n', out) + end, + { '+set updatecount=99', '-i', 'Xtest_shada', '+set loadplugins', '-u', 'NORC' }, + nil, + '-', + script ) end) end) @@ -585,19 +593,21 @@ describe('startup', function() eq(' encoding=utf-8\n', fn.system({ nvim_prog, '-n', '-es' }, { 'set encoding', '' })) end) - it('-es/-Es disables swapfile, user config #8540', function() + it('-es/-Es disables swapfile/shada/config #8540', function() for _, arg in ipairs({ '-es', '-Es' }) do local out = fn.system({ nvim_prog, arg, - '+set swapfile? updatecount? shadafile?', + '+set updatecount? shadafile? loadplugins?', '+put =map(getscriptinfo(), {-> v:val.name})', '+%print', }) local line1 = string.match(out, '^.-\n') -- updatecount=0 means swapfile was disabled. - eq(' swapfile updatecount=0 shadafile=\n', line1) - -- Standard plugins were loaded, but not user config. + eq(' updatecount=0 shadafile=NONE loadplugins\n', line1) + -- Standard plugins were loaded, but not user config. #31878 + local nrlines = #vim.split(out, '\n') + ok(nrlines > 20, '>20', nrlines) ok(string.find(out, 'man.lua') ~= nil) ok(string.find(out, 'init.vim') == nil) end @@ -606,15 +616,15 @@ describe('startup', function() it('fails on --embed with -es/-Es/-l', function() matches( 'nvim[.exe]*: %-%-embed conflicts with %-es/%-Es/%-l', - fn.system({ nvim_prog, '--embed', '-es' }) + n.spawn_wait('--embed', '-es').stderr ) matches( 'nvim[.exe]*: %-%-embed conflicts with %-es/%-Es/%-l', - fn.system({ nvim_prog, '--embed', '-Es' }) + n.spawn_wait('--embed', '-Es').stderr ) matches( 'nvim[.exe]*: %-%-embed conflicts with %-es/%-Es/%-l', - fn.system({ nvim_prog, '--embed', '-l', 'foo.lua' }) + n.spawn_wait('--embed', '-l', 'foo.lua').stderr ) end) @@ -698,20 +708,8 @@ describe('startup', function() end) it('get command line arguments from v:argv', function() - local out = fn.system({ - nvim_prog, - '-u', - 'NONE', - '-i', - 'NONE', - '--headless', - '--cmd', - nvim_set, - '-c', - [[echo v:argv[-1:] len(v:argv) > 1]], - '+q', - }) - eq("['+q'] 1", out) + local p = n.spawn_wait('--cmd', nvim_set, '-c', [[echo v:argv[-1:] len(v:argv) > 1]], '+q') + eq("['+q'] 1", p.stderr) end) end) diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index c20d925713..7c68de272b 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -10,6 +10,7 @@ local fn = n.fn local command = n.command local api = n.api local poke_eventloop = n.poke_eventloop +local exec_lua = n.exec_lua describe('completion', function() local screen @@ -1326,4 +1327,29 @@ describe('completion', function() ]], }) end) + + describe('nvim__complete_set', function() + it("fails when 'completeopt' does not include popup", function() + exec_lua([[ + function _G.omni_test(findstart, base) + if findstart == 1 then + return vim.fn.col('.') - 1 + end + return { { word = 'one' } } + end + vim.api.nvim_create_autocmd('CompleteChanged', { + callback = function() + local ok, err = pcall(vim.api.nvim__complete_set, 0, { info = '1info' }) + if not ok then + vim.g.err_msg = err + end + end, + }) + vim.opt.completeopt = 'menu,menuone' + vim.opt.omnifunc = 'v:lua.omni_test' + ]]) + feed('S<C-X><C-O>') + eq('completeopt option does not include popup', api.nvim_get_var('err_msg')) + end) + end) end) diff --git a/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua b/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua index 08f7663075..d1f598a9d8 100644 --- a/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua +++ b/test/functional/ex_cmds/swapfile_preserve_recover_spec.lua @@ -12,12 +12,10 @@ local fn = n.fn local nvim_prog = n.nvim_prog local ok = t.ok local rmdir = n.rmdir -local new_argv = n.new_argv local new_pipename = n.new_pipename local pesc = vim.pesc local os_kill = n.os_kill local set_session = n.set_session -local spawn = n.spawn local async_meths = n.async_meths local expect_msg_seq = n.expect_msg_seq local pcall_err = t.pcall_err @@ -56,7 +54,7 @@ describe("preserve and (R)ecover with custom 'directory'", function() local nvim0 before_each(function() - nvim0 = spawn(new_argv()) + nvim0 = n.new_session(false) set_session(nvim0) rmdir(swapdir) mkdir(swapdir) @@ -76,7 +74,8 @@ describe("preserve and (R)ecover with custom 'directory'", function() local function test_recover(swappath1) -- Start another Nvim instance. - local nvim2 = spawn({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed' }, true) + local nvim2 = + n.new_session(false, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false }) set_session(nvim2) exec(init) @@ -141,7 +140,7 @@ describe('swapfile detection', function() set swapfile fileformat=unix nomodified undolevels=-1 nohidden ]] before_each(function() - nvim0 = spawn(new_argv()) + nvim0 = n.new_session(false) set_session(nvim0) rmdir(swapdir) mkdir(swapdir) @@ -168,7 +167,8 @@ describe('swapfile detection', function() command('preserve') -- Start another Nvim instance. - local nvim2 = spawn({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed' }, true, nil, true) + local nvim2 = + n.new_session(true, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false }) set_session(nvim2) local screen2 = Screen.new(256, 40) screen2._default_attr_ids = nil @@ -251,7 +251,7 @@ describe('swapfile detection', function() command('preserve') -- Make sure the swap file exists. local nvimpid = fn.getpid() - local nvim1 = spawn(new_argv(), true, nil, true) + local nvim1 = n.new_session(true) set_session(nvim1) local screen = Screen.new(75, 18) exec(init) @@ -273,7 +273,7 @@ describe('swapfile detection', function() [1] = { bold = true, foreground = Screen.colors.SeaGreen }, -- MoreMsg }) - local nvim1 = spawn(new_argv(), true, nil, true) + local nvim1 = n.new_session(true) set_session(nvim1) screen:attach() exec(init) @@ -292,7 +292,7 @@ describe('swapfile detection', function() ]]) nvim1:close() - local nvim2 = spawn(new_argv(), true, nil, true) + local nvim2 = n.new_session(true) set_session(nvim2) screen:attach() exec(init) diff --git a/test/functional/ex_cmds/wundo_spec.lua b/test/functional/ex_cmds/wundo_spec.lua index 2299f33f06..9b81c6d06d 100644 --- a/test/functional/ex_cmds/wundo_spec.lua +++ b/test/functional/ex_cmds/wundo_spec.lua @@ -5,8 +5,6 @@ local n = require('test.functional.testnvim')() local command = n.command local clear = n.clear local eval = n.eval -local spawn = n.spawn -local nvim_prog = n.nvim_prog local set_session = n.set_session describe(':wundo', function() @@ -24,15 +22,11 @@ end) describe('u_* functions', function() it('safely fail on new, non-empty buffer', function() - local session = spawn({ - nvim_prog, - '-u', - 'NONE', - '-i', - 'NONE', - '--embed', - '-c', - 'set undodir=. undofile', + local session = n.new_session(false, { + args = { + '-c', + 'set undodir=. undofile', + }, }) set_session(session) command('echo "True"') -- Should not error out due to crashed Neovim diff --git a/test/functional/func/memoize_spec.lua b/test/functional/func/memoize_spec.lua new file mode 100644 index 0000000000..ca518ab88d --- /dev/null +++ b/test/functional/func/memoize_spec.lua @@ -0,0 +1,142 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local clear = n.clear +local exec_lua = n.exec_lua +local eq = t.eq + +describe('vim.func._memoize', function() + before_each(clear) + + it('caches function results based on their parameters', function() + exec_lua([[ + _G.count = 0 + + local adder = vim.func._memoize('concat', function(arg1, arg2) + _G.count = _G.count + 1 + return arg1 + arg2 + end) + + collectgarbage('stop') + adder(3, -4) + adder(3, -4) + adder(3, -4) + adder(3, -4) + adder(3, -4) + collectgarbage('restart') + ]]) + + eq(1, exec_lua([[return _G.count]])) + end) + + it('caches function results using a weak table by default', function() + exec_lua([[ + _G.count = 0 + + local adder = vim.func._memoize('concat-2', function(arg1, arg2) + _G.count = _G.count + 1 + return arg1 + arg2 + end) + + adder(3, -4) + collectgarbage() + adder(3, -4) + collectgarbage() + adder(3, -4) + ]]) + + eq(3, exec_lua([[return _G.count]])) + end) + + it('can cache using a strong table', function() + exec_lua([[ + _G.count = 0 + + local adder = vim.func._memoize('concat-2', function(arg1, arg2) + _G.count = _G.count + 1 + return arg1 + arg2 + end, false) + + adder(3, -4) + collectgarbage() + adder(3, -4) + collectgarbage() + adder(3, -4) + ]]) + + eq(1, exec_lua([[return _G.count]])) + end) + + it('can clear a single cache entry', function() + exec_lua([[ + _G.count = 0 + + local adder = vim.func._memoize(function(arg1, arg2) + return tostring(arg1) .. '%%' .. tostring(arg2) + end, function(arg1, arg2) + _G.count = _G.count + 1 + return arg1 + arg2 + end) + + collectgarbage('stop') + adder(3, -4) + adder(3, -4) + adder(3, -4) + adder(3, -4) + adder(3, -4) + adder:clear(3, -4) + adder(3, -4) + collectgarbage('restart') + ]]) + + eq(2, exec_lua([[return _G.count]])) + end) + + it('can clear the entire cache', function() + exec_lua([[ + _G.count = 0 + + local adder = vim.func._memoize(function(arg1, arg2) + return tostring(arg1) .. '%%' .. tostring(arg2) + end, function(arg1, arg2) + _G.count = _G.count + 1 + return arg1 + arg2 + end) + + collectgarbage('stop') + adder(1, 2) + adder(3, -4) + adder(1, 2) + adder(3, -4) + adder(1, 2) + adder(3, -4) + adder:clear() + adder(1, 2) + adder(3, -4) + collectgarbage('restart') + ]]) + + eq(4, exec_lua([[return _G.count]])) + end) + + it('can cache functions that return nil', function() + exec_lua([[ + _G.count = 0 + + local adder = vim.func._memoize('concat', function(arg1, arg2) + _G.count = _G.count + 1 + return nil + end) + + collectgarbage('stop') + adder(1, 2) + adder(1, 2) + adder(1, 2) + adder(1, 2) + adder:clear() + adder(1, 2) + collectgarbage('restart') + ]]) + + eq(2, exec_lua([[return _G.count]])) + end) +end) diff --git a/test/functional/legacy/cmdline_spec.lua b/test/functional/legacy/cmdline_spec.lua index 3addcb957c..819b40323a 100644 --- a/test/functional/legacy/cmdline_spec.lua +++ b/test/functional/legacy/cmdline_spec.lua @@ -159,6 +159,50 @@ describe('cmdline', function() endfunc ]]) + feed(':resize -3<CR>') + screen:expect([[ + ^ | + {1:~ }|*2 + {3:[No Name] }| + |*4 + ]]) + + -- :resize now also changes 'cmdheight' accordingly + feed(':set cmdheight+=1<CR>') + screen:expect([[ + ^ | + {1:~ }| + {3:[No Name] }| + |*5 + ]]) + + -- using more space moves the status line up + feed(':set cmdheight+=1<CR>') + screen:expect([[ + ^ | + {3:[No Name] }| + |*6 + ]]) + + -- reducing cmdheight moves status line down + feed(':set cmdheight-=3<CR>') + screen:expect([[ + ^ | + {1:~ }|*3 + {3:[No Name] }| + |*3 + ]]) + + -- reducing window size and then setting cmdheight + feed(':resize -1<CR>') + feed(':set cmdheight=1<CR>') + screen:expect([[ + ^ | + {1:~ }|*5 + {3:[No Name] }| + | + ]]) + -- setting 'cmdheight' works after outputting two messages feed(':call EchoTwo()') screen:expect([[ @@ -185,6 +229,16 @@ describe('cmdline', function() bar | {6:Press ENTER or type command to continue}^ | ]]) + + -- window commands do not reduce 'cmdheight' to value lower than :set by user + feed('<CR>:wincmd _<CR>') + screen:expect([[ + ^ | + {1:~ }|*4 + {3:[No Name] }| + :wincmd _ | + | + ]]) end) -- oldtest: Test_cmdheight_tabline() diff --git a/test/functional/legacy/messages_spec.lua b/test/functional/legacy/messages_spec.lua index adf75c2836..db5e3f6857 100644 --- a/test/functional/legacy/messages_spec.lua +++ b/test/functional/legacy/messages_spec.lua @@ -82,7 +82,7 @@ describe('messages', function() NoSuchFil^e | three | {1:~ }|*5 - from DebugSilent visual | + | {9:E447: Can't find file "NoSuchFile" in path} | ]]) end) diff --git a/test/functional/legacy/window_cmd_spec.lua b/test/functional/legacy/window_cmd_spec.lua index b58bf0bf43..fac982354c 100644 --- a/test/functional/legacy/window_cmd_spec.lua +++ b/test/functional/legacy/window_cmd_spec.lua @@ -299,7 +299,7 @@ describe('splitkeep', function() c | {1:~ }| {3:[No Name] }| - | + :call win_move_statusline(win, 1) | ]]) end) diff --git a/test/functional/lua/fs_spec.lua b/test/functional/lua/fs_spec.lua index 1c6ff5ac6d..218f9bbc46 100644 --- a/test/functional/lua/fs_spec.lua +++ b/test/functional/lua/fs_spec.lua @@ -340,6 +340,9 @@ describe('vim.fs', function() end) describe('normalize()', function() + -- local function vim.fs.normalize(path, opts) + -- return exec_lua([[return vim.fs.vim.fs.normalize(...)]], path, opts) + -- end it('removes trailing /', function() eq('/home/user', vim.fs.normalize('/home/user/')) end) @@ -373,6 +376,29 @@ describe('vim.fs', function() eq('/foo/bar', vim.fs.normalize('/foo//bar////', posix_opts)) end) + it('normalizes drive letter', function() + eq('C:/', vim.fs.normalize('C:/', win_opts)) + eq('C:/', vim.fs.normalize('c:/', win_opts)) + eq('D:/', vim.fs.normalize('d:/', win_opts)) + eq('C:', vim.fs.normalize('C:', win_opts)) + eq('C:', vim.fs.normalize('c:', win_opts)) + eq('D:', vim.fs.normalize('d:', win_opts)) + eq('C:/foo/test', vim.fs.normalize('C:/foo/test/', win_opts)) + eq('C:/foo/test', vim.fs.normalize('c:/foo/test/', win_opts)) + eq('D:foo/test', vim.fs.normalize('D:foo/test/', win_opts)) + eq('D:foo/test', vim.fs.normalize('d:foo/test/', win_opts)) + end) + + it('does not change case on paths, see #31833', function() + eq('TEST', vim.fs.normalize('TEST', win_opts)) + eq('test', vim.fs.normalize('test', win_opts)) + eq('C:/FOO/test', vim.fs.normalize('C:/FOO/test', win_opts)) + eq('C:/foo/test', vim.fs.normalize('C:/foo/test', win_opts)) + eq('//SERVER/SHARE/FOO/BAR', vim.fs.normalize('//SERVER/SHARE/FOO/BAR', win_opts)) + eq('//server/share/foo/bar', vim.fs.normalize('//server/share/foo/bar', win_opts)) + eq('C:/FOO/test', vim.fs.normalize('c:/FOO/test', win_opts)) + end) + it('allows backslashes on unix-based os', function() eq('/home/user/hello\\world', vim.fs.normalize('/home/user/hello\\world', posix_opts)) end) diff --git a/test/functional/lua/ui_event_spec.lua b/test/functional/lua/ui_event_spec.lua index 27640d6066..af6b2ceac3 100644 --- a/test/functional/lua/ui_event_spec.lua +++ b/test/functional/lua/ui_event_spec.lua @@ -106,20 +106,15 @@ describe('vim.ui_attach', function() end) it('does not crash on exit', function() - fn.system({ - n.nvim_prog, - '-u', - 'NONE', - '-i', - 'NONE', + local p = n.spawn_wait( '--cmd', [[ lua ns = vim.api.nvim_create_namespace 'testspace' ]], '--cmd', [[ lua vim.ui_attach(ns, {ext_popupmenu=true}, function() end) ]], '--cmd', - 'quitall!', - }) - eq(0, n.eval('v:shell_error')) + 'quitall!' + ) + eq(0, p.status) end) it('can receive accurate message kinds even if they are history', function() diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua index e7f47ef4e9..a82279e775 100644 --- a/test/functional/options/defaults_spec.lua +++ b/test/functional/options/defaults_spec.lua @@ -14,7 +14,6 @@ local api = n.api local command = n.command local clear = n.clear local exc_exec = n.exc_exec -local exec_lua = n.exec_lua local eval = n.eval local eq = t.eq local ok = t.ok @@ -929,17 +928,12 @@ describe('stdpath()', function() assert_alive() -- Check for crash. #8393 -- Check that Nvim rejects invalid APPNAMEs - -- Call jobstart() and jobwait() in the same RPC request to reduce flakiness. local function test_appname(testAppname, expected_exitcode) - local lua_code = string.format( - [[ - local child = vim.fn.jobstart({ vim.v.progpath, '--clean', '--headless', '--listen', 'x', '+qall!' }, { env = { NVIM_APPNAME = %q } }) - return vim.fn.jobwait({ child }, %d)[1] - ]], - testAppname, - 3000 - ) - eq(expected_exitcode, exec_lua(lua_code)) + local p = n.spawn_wait({ + args = { '--listen', 'x', '+qall!' }, + env = { NVIM_APPNAME = testAppname }, + }) + eq(expected_exitcode, p.status) end -- Invalid appnames: test_appname('a/../b', 1) diff --git a/test/functional/plugin/health_spec.lua b/test/functional/plugin/health_spec.lua index 753da64522..406b5c3c16 100644 --- a/test/functional/plugin/health_spec.lua +++ b/test/functional/plugin/health_spec.lua @@ -66,6 +66,18 @@ describe(':checkhealth', function() eq({}, getcompletion('', 'checkhealth')) assert_alive() end) + + it('vim.g.health', function() + clear() + command("let g:health = {'style':'float'}") + command('checkhealth lsp') + eq( + 'editor', + exec_lua([[ + return vim.api.nvim_win_get_config(0).relative + ]]) + ) + end) end) describe('vim.health', function() diff --git a/test/functional/plugin/lsp/utils_spec.lua b/test/functional/plugin/lsp/utils_spec.lua index ce6e6b2535..1e3e759e0b 100644 --- a/test/functional/plugin/lsp/utils_spec.lua +++ b/test/functional/plugin/lsp/utils_spec.lua @@ -301,4 +301,32 @@ describe('vim.lsp.util', function() end) end) end) + + it('open_floating_preview zindex greater than current window', function() + local screen = Screen.new() + exec_lua(function() + vim.api.nvim_open_win(0, true, { + relative = 'editor', + border = 'single', + height = 11, + width = 51, + row = 2, + col = 2, + }) + vim.keymap.set('n', 'K', function() + vim.lsp.util.open_floating_preview({ 'foo' }, '', { border = 'single' }) + end, {}) + end) + feed('K') + screen:expect([[ + ┌───────────────────────────────────────────────────┐| + │{4:^ }│| + │┌───┐{11: }│| + ││{4:foo}│{11: }│| + │└───┘{11: }│| + │{11:~ }│|*7 + └───────────────────────────────────────────────────┘| + | + ]]) + end) end) diff --git a/test/functional/shada/marks_spec.lua b/test/functional/shada/marks_spec.lua index d680f0b83b..837978dee1 100644 --- a/test/functional/shada/marks_spec.lua +++ b/test/functional/shada/marks_spec.lua @@ -218,7 +218,7 @@ describe('ShaDa support code', function() -- -c temporary sets lnum to zero to make `+/pat` work, so calling setpcmark() -- during -c used to add item with zero lnum to jump list. it('does not create incorrect file for non-existent buffers when writing from -c', function() - local argv = n.new_argv { + local p = n.spawn_wait { args_rm = { '-i', '--embed', -- no --embed @@ -232,12 +232,13 @@ describe('ShaDa support code', function() 'qall', }, } - eq('', fn.system(argv)) + eq('', p:output()) + eq(0, p.status) eq(0, exc_exec('rshada')) end) it('does not create incorrect file for non-existent buffers opened from -c', function() - local argv = n.new_argv { + local p = n.spawn_wait { args_rm = { '-i', '--embed', -- no --embed @@ -251,7 +252,8 @@ describe('ShaDa support code', function() 'autocmd VimEnter * qall', }, } - eq('', fn.system(argv)) + eq('', p:output()) + eq(0, p.status) eq(0, exc_exec('rshada')) end) diff --git a/test/functional/shada/shada_spec.lua b/test/functional/shada/shada_spec.lua index 5debdc6c77..78fe19b984 100644 --- a/test/functional/shada/shada_spec.lua +++ b/test/functional/shada/shada_spec.lua @@ -6,8 +6,7 @@ local uv = vim.uv local paths = t.paths local api, nvim_command, fn, eq = n.api, n.command, n.fn, t.eq -local write_file, spawn, set_session, nvim_prog, exc_exec = - t.write_file, n.spawn, n.set_session, n.nvim_prog, n.exc_exec +local write_file, set_session, exc_exec = t.write_file, n.set_session, n.exc_exec local is_os = t.is_os local skip = t.skip @@ -254,7 +253,7 @@ describe('ShaDa support code', function() it('does not crash when ShaDa file directory is not writable', function() skip(is_os('win')) - fn.mkdir(dirname, '', 0) + fn.mkdir(dirname, '', '0') eq(0, fn.filewritable(dirname)) reset { shadafile = dirshada, args = { '--cmd', 'set shada=' } } api.nvim_set_option_value('shada', "'10", {}) @@ -270,10 +269,10 @@ end) describe('ShaDa support code', function() it('does not write NONE file', function() - local session = spawn( - { nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed', '--headless', '--cmd', 'qall' }, - true - ) + local session = n.new_session(false, { + merge = false, + args = { '-u', 'NONE', '-i', 'NONE', '--embed', '--headless', '--cmd', 'qall' }, + }) session:close() eq(nil, uv.fs_stat('NONE')) eq(nil, uv.fs_stat('NONE.tmp.a')) @@ -281,7 +280,10 @@ describe('ShaDa support code', function() it('does not read NONE file', function() write_file('NONE', '\005\001\015\131\161na\162rX\194\162rc\145\196\001-') - local session = spawn({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed', '--headless' }, true) + local session = n.new_session( + false, + { merge = false, args = { '-u', 'NONE', '-i', 'NONE', '--embed', '--headless' } } + ) set_session(session) eq('', fn.getreg('a')) session:close() diff --git a/test/functional/terminal/buffer_spec.lua b/test/functional/terminal/buffer_spec.lua index b6de687af9..cc807ba555 100644 --- a/test/functional/terminal/buffer_spec.lua +++ b/test/functional/terminal/buffer_spec.lua @@ -435,6 +435,19 @@ describe(':terminal buffer', function() ]]) end) + it('handles unprintable chars', function() + local screen = Screen.new(50, 7) + feed 'i' + local chan = api.nvim_open_term(0, {}) + api.nvim_chan_send(chan, '\239\187\191') -- '\xef\xbb\xbf' + screen:expect([[ + {18:<feff>}^ | + |*5 + {5:-- TERMINAL --} | + ]]) + eq('\239\187\191', api.nvim_get_current_line()) + end) + it("handles bell respecting 'belloff' and 'visualbell'", function() local screen = Screen.new(50, 7) local chan = api.nvim_open_term(0, {}) diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua index c43d139f70..0afbd010f7 100644 --- a/test/functional/terminal/highlight_spec.lua +++ b/test/functional/terminal/highlight_spec.lua @@ -6,7 +6,6 @@ local tt = require('test.functional.testterm') local feed, clear = n.feed, n.clear local api = n.api local testprg, command = n.testprg, n.command -local nvim_prog_abs = n.nvim_prog_abs local fn = n.fn local nvim_set = n.nvim_set local is_os = t.is_os @@ -151,7 +150,7 @@ it(':terminal highlight has lower precedence than editor #9964', function() }) -- Child nvim process in :terminal (with cterm colors). fn.jobstart({ - nvim_prog_abs(), + n.nvim_prog, '-n', '-u', 'NORC', diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index f9145f9b63..3624a7bc2b 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -26,7 +26,6 @@ local api = n.api local is_ci = t.is_ci local is_os = t.is_os local new_pipename = n.new_pipename -local spawn_argv = n.spawn_argv local set_session = n.set_session local write_file = t.write_file local eval = n.eval @@ -3320,8 +3319,8 @@ describe('TUI as a client', function() end) it('connects to remote instance (with its own TUI)', function() - local server_super = spawn_argv(false) -- equivalent to clear() - local client_super = spawn_argv(true) + local server_super = n.new_session(false) + local client_super = n.new_session(true) set_session(server_super) local server_pipe = new_pipename() @@ -3395,8 +3394,8 @@ describe('TUI as a client', function() end) it('connects to remote instance (--headless)', function() - local server = spawn_argv(false) -- equivalent to clear() - local client_super = spawn_argv(true, { env = { NVIM_LOG_FILE = testlog } }) + local server = n.new_session(false) + local client_super = n.new_session(true, { env = { NVIM_LOG_FILE = testlog } }) set_session(server) local server_pipe = api.nvim_get_vvar('servername') @@ -3462,8 +3461,8 @@ describe('TUI as a client', function() end) local function test_remote_tui_quit(status) - local server_super = spawn_argv(false) -- equivalent to clear() - local client_super = spawn_argv(true) + local server_super = n.new_session(false) + local client_super = n.new_session(true) set_session(server_super) local server_pipe = new_pipename() diff --git a/test/functional/testnvim.lua b/test/functional/testnvim.lua index 675ad9e3d7..59cb593cf7 100644 --- a/test/functional/testnvim.lua +++ b/test/functional/testnvim.lua @@ -4,7 +4,7 @@ local t = require('test.testutil') local Session = require('test.client.session') local uv_stream = require('test.client.uv_stream') local SocketStream = uv_stream.SocketStream -local ChildProcessStream = uv_stream.ChildProcessStream +local ProcStream = uv_stream.ProcStream local check_cores = t.check_cores local check_logs = t.check_logs @@ -318,24 +318,14 @@ function M.stop() assert(session):stop() end -function M.nvim_prog_abs() - -- system(['build/bin/nvim']) does not work for whatever reason. It must - -- be executable searched in $PATH or something starting with / or ./. - if M.nvim_prog:match('[/\\]') then - return M.request('nvim_call_function', 'fnamemodify', { M.nvim_prog, ':p' }) - else - return M.nvim_prog - end -end - -- Use for commands which expect nvim to quit. -- The first argument can also be a timeout. function M.expect_exit(fn_or_timeout, ...) local eof_err_msg = 'EOF was received from Nvim. Likely the Nvim process crashed.' if type(fn_or_timeout) == 'function' then - eq(eof_err_msg, t.pcall_err(fn_or_timeout, ...)) + t.matches(eof_err_msg, t.pcall_err(fn_or_timeout, ...)) else - eq( + t.matches( eof_err_msg, t.pcall_err(function(timeout, fn, ...) fn(...) @@ -465,22 +455,6 @@ function M.check_close() session = nil end ---- @param argv string[] ---- @param merge boolean? ---- @param env string[]? ---- @param keep boolean? ---- @param io_extra uv.uv_pipe_t? used for stdin_fd, see :help ui-option ---- @return test.Session -function M.spawn(argv, merge, env, keep, io_extra) - if not keep then - M.check_close() - end - - local child_stream = - ChildProcessStream.spawn(merge and M.merge_args(prepend_argv, argv) or argv, env, io_extra) - return Session.new(child_stream) -end - -- Creates a new Session connected by domain socket (named pipe) or TCP. function M.connect(file_or_address) local addr, port = string.match(file_or_address, '(.*):(%d+)') @@ -489,61 +463,112 @@ function M.connect(file_or_address) return Session.new(stream) end --- Starts (and returns) a new global Nvim session. --- --- Parameters are interpreted as startup args, OR a map with these keys: --- args: List: Args appended to the default `nvim_argv` set. --- args_rm: List: Args removed from the default set. All cases are --- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd" --- (and its value) from the default set. --- env: Map: Defines the environment of the new session. --- --- Example: --- clear('-e') --- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} +--- Starts a new, global Nvim session and clears the current one. +--- +--- Note: Use `new_session()` to start a session without replacing the current one. +--- +--- Parameters are interpreted as startup args, OR a map with these keys: +--- - args: List: Args appended to the default `nvim_argv` set. +--- - args_rm: List: Args removed from the default set. All cases are +--- removed, e.g. args_rm={'--cmd'} removes all cases of "--cmd" +--- (and its value) from the default set. +--- - env: Map: Defines the environment of the new session. +--- +--- Example: +--- ``` +--- clear('-e') +--- clear{args={'-e'}, args_rm={'-i'}, env={TERM=term}} +--- ``` +--- +--- @param ... string Nvim CLI args +--- @return test.Session +--- @overload fun(opts: test.session.Opts): test.Session function M.clear(...) - M.set_session(M.spawn_argv(false, ...)) + M.set_session(M.new_session(false, ...)) return M.get_session() end ---- same params as clear, but does returns the session instead ---- of replacing the default session +--- Starts a new Nvim process with the given args and returns a msgpack-RPC session. +--- +--- Does not replace the current global session, unlike `clear()`. +--- +--- @param keep boolean (default: false) Don't close the current global session. +--- @param ... string Nvim CLI args (or see overload) --- @return test.Session -function M.spawn_argv(keep, ...) - local argv, env, io_extra = M.new_argv(...) - return M.spawn(argv, nil, env, keep, io_extra) +--- @overload fun(keep: boolean, opts: test.session.Opts): test.Session +function M.new_session(keep, ...) + if not keep then + M.check_close() + end + + local argv, env, io_extra = M._new_argv(...) + + local proc = ProcStream.spawn(argv, env, io_extra) + return Session.new(proc) end ---- @class test.new_argv.Opts +--- Starts a (non-RPC, `--headless --listen "Tx"`) Nvim process, waits for exit, and returns result. +--- +--- @param ... string Nvim CLI args, or `test.session.Opts` table. +--- @return test.ProcStream +--- @overload fun(opts: test.session.Opts): test.ProcStream +function M.spawn_wait(...) + local opts = type(...) == 'string' and { args = { ... } } or ... + opts.args_rm = opts.args_rm and opts.args_rm or {} + table.insert(opts.args_rm, '--embed') + local argv, env, io_extra = M._new_argv(opts) + local proc = ProcStream.spawn(argv, env, io_extra) + proc.collect_text = true + proc:read_start() + proc:wait() + proc:close() + return proc +end + +--- @class test.session.Opts +--- Nvim CLI args --- @field args? string[] +--- Remove these args from the default `nvim_argv` args set. Ignored if `merge=false`. --- @field args_rm? string[] +--- (default: true) Merge `args` with the default set. Else use only the provided `args`. +--- @field merge? boolean +--- Environment variables --- @field env? table<string,string> +--- Used for stdin_fd, see `:help ui-option` --- @field io_extra? uv.uv_pipe_t ---- Builds an argument list for use in clear(). +--- @private --- ---- @see clear() for parameters. ---- @param ... string +--- Builds an argument list for use in `new_session()`, `clear()`, and `spawn_wait()`. +--- +--- @param ... string Nvim CLI args, or `test.session.Opts` table. --- @return string[] --- @return string[]? --- @return uv.uv_pipe_t? -function M.new_argv(...) - local args = { unpack(M.nvim_argv) } - table.insert(args, '--headless') - if _G._nvim_test_id then - -- Set the server name to the test-id for logging. #8519 - table.insert(args, '--listen') - table.insert(args, _G._nvim_test_id) +--- @overload fun(opts: test.session.Opts): string[], string[]?, uv.uv_pipe_t? +function M._new_argv(...) + --- @type test.session.Opts|string + local opts = select(1, ...) + local merge = type(opts) ~= 'table' and true or opts.merge ~= false + + local args = merge and { unpack(M.nvim_argv) } or { M.nvim_prog } + if merge then + table.insert(args, '--headless') + if _G._nvim_test_id then + -- Set the server name to the test-id for logging. #8519 + table.insert(args, '--listen') + table.insert(args, _G._nvim_test_id) + end end + local new_args --- @type string[] local io_extra --- @type uv.uv_pipe_t? - local env --- @type string[]? - --- @type test.new_argv.Opts|string - local opts = select(1, ...) + local env --- @type string[]? List of "key=value" env vars. + if type(opts) ~= 'table' then new_args = { ... } else - args = remove_args(args, opts.args_rm) + args = merge and remove_args(args, opts.args_rm) or args if opts.env then local env_opt = {} --- @type table<string,string> for k, v in pairs(opts.env) do diff --git a/test/functional/treesitter/fold_spec.lua b/test/functional/treesitter/fold_spec.lua index e38e58ff92..9f7fdf529f 100644 --- a/test/functional/treesitter/fold_spec.lua +++ b/test/functional/treesitter/fold_spec.lua @@ -5,6 +5,7 @@ local Screen = require('test.functional.ui.screen') local clear = n.clear local eq = t.eq local insert = n.insert +local write_file = t.write_file local exec_lua = n.exec_lua local command = n.command local feed = n.feed @@ -767,4 +768,78 @@ t2]]) ]], } end) + + it("doesn't call get_parser too often when parser is not available", function() + -- spy on vim.treesitter.get_parser() to keep track of how many times it is called + exec_lua(function() + _G.count = 0 + vim.treesitter.get_parser = (function(wrapped) + return function(...) + _G.count = _G.count + 1 + return wrapped(...) + end + end)(vim.treesitter.get_parser) + end) + + insert(test_text) + command [[ + set filetype=some_filetype_without_treesitter_parser + set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1 foldlevel=0 + ]] + + -- foldexpr will return '0' for all lines + local levels = get_fold_levels() ---@type integer[] + eq(19, #levels) + for lnum, level in ipairs(levels) do + eq('0', level, string.format("foldlevel[%d] == %s; expected '0'", lnum, level)) + end + + eq( + 1, + exec_lua [[ return _G.count ]], + 'count should not be as high as the # of lines; actually only once for the buffer.' + ) + end) + + it('can detect a new parser and refresh folds accordingly', function() + write_file('test_fold_file.txt', test_text) + command [[ + e test_fold_file.txt + set filetype=some_filetype_without_treesitter_parser + set foldmethod=expr foldexpr=v:lua.vim.treesitter.foldexpr() foldcolumn=1 foldlevel=0 + ]] + + -- foldexpr will return '0' for all lines + local levels = get_fold_levels() ---@type integer[] + eq(19, #levels) + for lnum, level in ipairs(levels) do + eq('0', level, string.format("foldlevel[%d] == %s; expected '0'", lnum, level)) + end + + -- reload buffer as c filetype to simulate new parser being found + feed('GA// vim: ft=c<Esc>') + command([[w | e]]) + + eq({ + [1] = '>1', + [2] = '1', + [3] = '1', + [4] = '1', + [5] = '>2', + [6] = '2', + [7] = '2', + [8] = '1', + [9] = '1', + [10] = '>2', + [11] = '2', + [12] = '2', + [13] = '2', + [14] = '2', + [15] = '>3', + [16] = '3', + [17] = '3', + [18] = '2', + [19] = '1', + }, get_fold_levels()) + end) end) diff --git a/test/functional/treesitter/inspect_tree_spec.lua b/test/functional/treesitter/inspect_tree_spec.lua index 1f7d15cc96..47f3421cfe 100644 --- a/test/functional/treesitter/inspect_tree_spec.lua +++ b/test/functional/treesitter/inspect_tree_spec.lua @@ -120,14 +120,17 @@ describe('vim.treesitter.inspect_tree', function() end) it('updates source and tree buffer windows and closes them correctly', function() + local name = t.tmpname() + n.command('edit ' .. name) insert([[ print() ]]) + n.command('set filetype=lua | write') -- setup two windows for the source buffer exec_lua(function() _G.source_win = vim.api.nvim_get_current_win() - vim.api.nvim_open_win(0, false, { + _G.source_win2 = vim.api.nvim_open_win(0, false, { win = 0, split = 'left', }) @@ -135,40 +138,44 @@ describe('vim.treesitter.inspect_tree', function() -- setup three windows for the tree buffer exec_lua(function() - vim.treesitter.start(0, 'lua') vim.treesitter.inspect_tree() _G.tree_win = vim.api.nvim_get_current_win() - _G.tree_win_copy_1 = vim.api.nvim_open_win(0, false, { + _G.tree_win2 = vim.api.nvim_open_win(0, false, { win = 0, split = 'left', }) - _G.tree_win_copy_2 = vim.api.nvim_open_win(0, false, { + _G.tree_win3 = vim.api.nvim_open_win(0, false, { win = 0, split = 'left', }) end) - -- close original source window - exec_lua('vim.api.nvim_win_close(source_win, false)') + -- close original source window without closing tree views + exec_lua('vim.api.nvim_set_current_win(source_win)') + feed(':quit<CR>') + eq('', n.api.nvim_get_vvar('errmsg')) + eq(true, exec_lua('return vim.api.nvim_win_is_valid(tree_win)')) + eq(true, exec_lua('return vim.api.nvim_win_is_valid(tree_win2)')) + eq(true, exec_lua('return vim.api.nvim_win_is_valid(tree_win3)')) -- navigates correctly to the remaining source buffer window + exec_lua('vim.api.nvim_set_current_win(tree_win)') feed('<CR>') eq('', n.api.nvim_get_vvar('errmsg')) + eq(true, exec_lua('return vim.api.nvim_get_current_win() == source_win2')) -- close original tree window exec_lua(function() - vim.api.nvim_set_current_win(_G.tree_win_copy_1) + vim.api.nvim_set_current_win(_G.tree_win2) vim.api.nvim_win_close(_G.tree_win, false) end) -- navigates correctly to the remaining source buffer window feed('<CR>') eq('', n.api.nvim_get_vvar('errmsg')) + eq(true, exec_lua('return vim.api.nvim_get_current_win() == source_win2')) -- close source buffer window and all remaining tree windows - t.pcall_err(exec_lua, 'vim.api.nvim_win_close(0, false)') - - eq(false, exec_lua('return vim.api.nvim_win_is_valid(tree_win_copy_1)')) - eq(false, exec_lua('return vim.api.nvim_win_is_valid(tree_win_copy_2)')) + n.expect_exit(n.command, 'quit') end) end) diff --git a/test/functional/treesitter/query_spec.lua b/test/functional/treesitter/query_spec.lua index 634f8af83d..6e21ed1d99 100644 --- a/test/functional/treesitter/query_spec.lua +++ b/test/functional/treesitter/query_spec.lua @@ -835,9 +835,9 @@ void ui_refresh(void) local result = exec_lua(function() local query0 = vim.treesitter.query.parse('c', query) - local match_preds = query0.match_preds + local match_preds = query0._match_predicates local called = 0 - function query0:match_preds(...) + function query0:_match_predicates(...) called = called + 1 return match_preds(self, ...) end diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua index 93ea2b9186..a2722a4139 100644 --- a/test/functional/ui/cmdline_spec.lua +++ b/test/functional/ui/cmdline_spec.lua @@ -1038,6 +1038,18 @@ describe('cmdline redraw', function() ]], } end) + + it('silent prompt', function() + command([[nmap <silent> T :call confirm("Save changes?", "&Yes\n&No\n&Cancel")<CR>]]) + feed('T') + screen:expect([[ + | + {3: }| + | + {6:Save changes?} | + {6:[Y]es, (N)o, (C)ancel: }^ | + ]]) + end) end) describe('statusline is redrawn on entering cmdline', function() diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua index 1e51652c4f..77ffc475b0 100644 --- a/test/functional/ui/messages_spec.lua +++ b/test/functional/ui/messages_spec.lua @@ -1121,7 +1121,7 @@ stack traceback: ]], messages = { { - content = { { 'wildmenu wildmode' } }, + content = { { 'wildmenu wildmode\n' } }, history = false, kind = 'wildlist', }, @@ -1335,6 +1335,21 @@ stack traceback: feed('i') n.assert_alive() end) + + it(':digraph contains newlines', function() + command('digraph') + screen:expect({ + condition = function() + local nl = 0 + eq('list_cmd', screen.messages[1].kind) + for _, chunk in ipairs(screen.messages[1].content) do + nl = nl + (chunk[2]:find('\n') and 1 or 0) + end + eq(682, nl) + screen.messages = {} + end, + }) + end) end) describe('ui/builtin messages', function() diff --git a/test/functional/ui/popupmenu_spec.lua b/test/functional/ui/popupmenu_spec.lua index d1228d3607..60d59190ce 100644 --- a/test/functional/ui/popupmenu_spec.lua +++ b/test/functional/ui/popupmenu_spec.lua @@ -1680,7 +1680,7 @@ describe('builtin popupmenu', function() end) end - describe('floating window preview #popup', function() + describe('floating window preview popup', function() it('pum popup preview', function() --row must > 10 screen:try_resize(40, 11) @@ -1693,14 +1693,29 @@ describe('builtin popupmenu', function() endfunc set omnifunc=Omni_test set completeopt=menu,popup - funct Set_info() let comp_info = complete_info() if comp_info['selected'] == 2 call nvim__complete_set(comp_info['selected'], {"info": "3info"}) endif endfunc - autocmd CompleteChanged * call Set_info() + funct TsHl() + let comp_info = complete_info() + if get(comp_info, 'previewbufnr', 0) > 0 + call v:lua.vim.treesitter.start(comp_info['preview_bufnr'], 'markdown') + endif + if comp_info['selected'] == 0 + call nvim__complete_set(comp_info['selected'], {"info": "```lua\nfunction test()\n print('foo')\nend\n```"}) + endif + endfunc + augroup Group + au! + autocmd CompleteChanged * :call Set_info() + augroup END + funct TestTs() + autocmd! Group + autocmd CompleteChanged * call TsHl() + endfunc ]]) feed('Gi<C-x><C-o>') --floating preview in right @@ -2004,6 +2019,90 @@ describe('builtin popupmenu', function() ]], } end + feed('<C-E><Esc>') + + -- works when scroll with treesitter highlight + command('call TestTs()') + feed('S<C-x><C-o>') + if multigrid then + screen:expect({ + grid = [[ + ## grid 1 + [2:----------------------------------------]|*10 + [3:----------------------------------------]| + ## grid 2 + one^ | + {1:~ }|*9 + ## grid 3 + {2:-- }{5:match 1 of 3} | + ## grid 5 + {s:one }| + {n:two }| + {n:looooooooooooooong }| + ## grid 9 + {n:```lua }| + {n:function test()}| + {n: print('foo') }| + {n:end }| + {n:``` }| + {n: }| + ]], + float_pos = { + [5] = { -1, 'NW', 2, 1, 0, false, 100 }, + [9] = { 1005, 'NW', 1, 1, 19, false, 50 }, + }, + win_viewport = { + [2] = { + win = 1000, + topline = 0, + botline = 2, + curline = 0, + curcol = 3, + linecount = 1, + sum_scroll_delta = 0, + }, + [9] = { + win = 1005, + topline = 0, + botline = 6, + curline = 0, + curcol = 0, + linecount = 5, + sum_scroll_delta = 0, + }, + }, + win_viewport_margins = { + [2] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1000, + }, + [9] = { + bottom = 0, + left = 0, + right = 0, + top = 0, + win = 1005, + }, + }, + }) + else + screen:expect({ + grid = [[ + one^ | + {s:one }{n:```lua }{1: }| + {n:two function test()}{1: }| + {n:looooooooooooooong print('foo') }{1: }| + {1:~ }{n:end }{1: }| + {1:~ }{n:``` }{1: }| + {1:~ }{n: }{1: }| + {1:~ }|*3 + {2:-- }{5:match 1 of 3} | + ]], + }) + end end) end) diff --git a/test/functional/ui/screen_basic_spec.lua b/test/functional/ui/screen_basic_spec.lua index 666d98c3b2..295c40b9b6 100644 --- a/test/functional/ui/screen_basic_spec.lua +++ b/test/functional/ui/screen_basic_spec.lua @@ -2,7 +2,7 @@ local t = require('test.testutil') local n = require('test.functional.testnvim')() local Screen = require('test.functional.ui.screen') -local spawn, set_session, clear = n.spawn, n.set_session, n.clear +local set_session, clear = n.set_session, n.clear local feed, command = n.feed, n.command local exec = n.exec local insert = n.insert @@ -26,7 +26,7 @@ describe('screen', function() } before_each(function() - local screen_nvim = spawn(nvim_argv) + local screen_nvim = n.new_session(false, { args = nvim_argv, merge = false }) set_session(screen_nvim) screen = Screen.new() end) @@ -766,7 +766,7 @@ describe('Screen default colors', function() 'colorscheme vim', '--embed', } - local screen_nvim = spawn(nvim_argv) + local screen_nvim = n.new_session(false, { args = nvim_argv, merge = false }) set_session(screen_nvim) screen = Screen.new(53, 14, { rgb = true, ext_termcolors = termcolors or nil }) end diff --git a/test/old/testdir/test_autocmd.vim b/test/old/testdir/test_autocmd.vim index 40c09e61ac..d5f7c928de 100644 --- a/test/old/testdir/test_autocmd.vim +++ b/test/old/testdir/test_autocmd.vim @@ -4181,4 +4181,28 @@ func Test_autocmd_BufWinLeave_with_vsp() exe "bw! " .. dummy endfunc +func Test_OptionSet_cmdheight() + set mouse=a laststatus=2 + au OptionSet cmdheight :let &l:ch = v:option_new + + resize -1 + call assert_equal(2, &l:ch) + resize +1 + call assert_equal(1, &l:ch) + + call Ntest_setmouse(&lines - 1, 1) + call feedkeys("\<LeftMouse>", 'xt') + call Ntest_setmouse(&lines - 2, 1) + call feedkeys("\<LeftDrag>", 'xt') + call assert_equal(2, &l:ch) + + tabnew | resize +1 + call assert_equal(1, &l:ch) + tabfirst + call assert_equal(2, &l:ch) + + tabonly + set cmdheight& mouse& laststatus& +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index cbce0e908d..d4ad63d43e 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -291,8 +291,8 @@ func Test_changing_cmdheight() call term_sendkeys(buf, ":resize -3\<CR>") call VerifyScreenDump(buf, 'Test_changing_cmdheight_1', {}) - " using the space available doesn't change the status line - call term_sendkeys(buf, ":set cmdheight+=3\<CR>") + " :resize now also changes 'cmdheight' accordingly + call term_sendkeys(buf, ":set cmdheight+=1\<CR>") call VerifyScreenDump(buf, 'Test_changing_cmdheight_2', {}) " using more space moves the status line up @@ -300,10 +300,10 @@ func Test_changing_cmdheight() call VerifyScreenDump(buf, 'Test_changing_cmdheight_3', {}) " reducing cmdheight moves status line down - call term_sendkeys(buf, ":set cmdheight-=2\<CR>") + call term_sendkeys(buf, ":set cmdheight-=3\<CR>") call VerifyScreenDump(buf, 'Test_changing_cmdheight_4', {}) - " reducing window size and then setting cmdheight + " reducing window size and then setting cmdheight call term_sendkeys(buf, ":resize -1\<CR>") call term_sendkeys(buf, ":set cmdheight=1\<CR>") call VerifyScreenDump(buf, 'Test_changing_cmdheight_5', {}) @@ -316,6 +316,10 @@ func Test_changing_cmdheight() call term_sendkeys(buf, ":call EchoOne()\<CR>") call VerifyScreenDump(buf, 'Test_changing_cmdheight_7', {}) + " window commands do not reduce 'cmdheight' to value lower than :set by user + call term_sendkeys(buf, "\<CR>:wincmd _\<CR>") + call VerifyScreenDump(buf, 'Test_changing_cmdheight_8', {}) + " clean up call StopVimInTerminal(buf) endfunc diff --git a/test/old/testdir/test_filetype.vim b/test/old/testdir/test_filetype.vim index 685f700130..d890884eb5 100644 --- a/test/old/testdir/test_filetype.vim +++ b/test/old/testdir/test_filetype.vim @@ -672,7 +672,6 @@ func s:GetFilenameChecks() abort \ 'samba': ['smb.conf'], \ 'sas': ['file.sas'], \ 'sass': ['file.sass'], - \ 'sather': ['file.sa'], \ 'sbt': ['file.sbt'], \ 'scala': ['file.scala'], \ 'scheme': ['file.scm', 'file.ss', 'file.sld', 'file.stsg', 'any/local/share/supertux2/config', '.lips_repl_history'], @@ -691,6 +690,7 @@ func s:GetFilenameChecks() abort \ '.ash_history', 'any/etc/neofetch/config.conf', '.xprofile', 'user-dirs.defaults', 'user-dirs.dirs', \ 'makepkg.conf', '.makepkg.conf', 'file.mdd', '.env', '.envrc', 'devscripts.conf', '.devscripts', 'file.lo', \ 'file.la', 'file.lai'], + \ 'shaderslang': ['file.slang'], \ 'sieve': ['file.siv', 'file.sieve'], \ 'sil': ['file.sil'], \ 'simula': ['file.sim'], @@ -2321,6 +2321,22 @@ func Test_cmd_file() filetype off endfunc +func Test_sa_file() + filetype on + + call writefile([';* XXX-a.sa: XXX for TI C6000 DSP *;', '.no_mdep'], 'Xfile.sa') + split Xfile.sa + call assert_equal('tiasm', &filetype) + bwipe! + + call writefile(['-- comment'], 'Xfile.sa') + split Xfile.sa + call assert_equal('sather', &filetype) + bwipe! + + filetype off +endfunc + func Test_sig_file() filetype on diff --git a/test/old/testdir/test_stacktrace.vim b/test/old/testdir/test_stacktrace.vim new file mode 100644 index 0000000000..1e47deefdd --- /dev/null +++ b/test/old/testdir/test_stacktrace.vim @@ -0,0 +1,132 @@ +" Test for getstacktrace() and v:stacktrace + +source vim9.vim + +let s:thisfile = expand('%:p') +let s:testdir = s:thisfile->fnamemodify(':h') + +func Filepath(name) + return s:testdir .. '/' .. a:name +endfunc + +func AssertStacktrace(expect, actual) + call assert_equal(#{lnum: 581, filepath: Filepath('runtest.vim')}, a:actual[0]) + call assert_equal(a:expect, a:actual[-len(a:expect):]) +endfunc + +func Test_getstacktrace() + let g:stacktrace = [] + let lines1 =<< trim [SCRIPT] + " Xscript1 + source Xscript2 + func Xfunc1() + " Xfunc1 + call Xfunc2() + endfunc + [SCRIPT] + let lines2 =<< trim [SCRIPT] + " Xscript2 + func Xfunc2() + " Xfunc2 + let g:stacktrace = getstacktrace() + endfunc + [SCRIPT] + call writefile(lines1, 'Xscript1', 'D') + call writefile(lines2, 'Xscript2', 'D') + source Xscript1 + call Xfunc1() + call AssertStacktrace([ + \ #{funcref: funcref('Test_getstacktrace'), lnum: 37, filepath: s:thisfile}, + \ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')}, + \ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')}, + \ ], g:stacktrace) + unlet g:stacktrace +endfunc + +func Test_getstacktrace_event() + let g:stacktrace = [] + let lines1 =<< trim [SCRIPT] + " Xscript1 + func Xfunc() + " Xfunc + let g:stacktrace = getstacktrace() + endfunc + augroup test_stacktrace + autocmd SourcePre * call Xfunc() + augroup END + [SCRIPT] + let lines2 =<< trim [SCRIPT] + " Xscript2 + [SCRIPT] + call writefile(lines1, 'Xscript1', 'D') + call writefile(lines2, 'Xscript2', 'D') + source Xscript1 + source Xscript2 + call AssertStacktrace([ + \ #{funcref: funcref('Test_getstacktrace_event'), lnum: 64, filepath: s:thisfile}, + \ #{event: 'SourcePre Autocommands for "*"', lnum: 7, filepath: Filepath('Xscript1')}, + \ #{funcref: funcref('Xfunc'), lnum: 4, filepath: Filepath('Xscript1')}, + \ ], g:stacktrace) + augroup test_stacktrace + autocmd! + augroup END + unlet g:stacktrace +endfunc + +func Test_vstacktrace() + let lines1 =<< trim [SCRIPT] + " Xscript1 + source Xscript2 + func Xfunc1() + " Xfunc1 + call Xfunc2() + endfunc + [SCRIPT] + let lines2 =<< trim [SCRIPT] + " Xscript2 + func Xfunc2() + " Xfunc2 + throw 'Exception from Xfunc2' + endfunc + [SCRIPT] + call writefile(lines1, 'Xscript1', 'D') + call writefile(lines2, 'Xscript2', 'D') + source Xscript1 + call assert_equal([], v:stacktrace) + try + call Xfunc1() + catch + let stacktrace = v:stacktrace + endtry + call assert_equal([], v:stacktrace) + call AssertStacktrace([ + \ #{funcref: funcref('Test_vstacktrace'), lnum: 97, filepath: s:thisfile}, + \ #{funcref: funcref('Xfunc1'), lnum: 5, filepath: Filepath('Xscript1')}, + \ #{funcref: funcref('Xfunc2'), lnum: 4, filepath: Filepath('Xscript2')}, + \ ], stacktrace) +endfunc + +func Test_zzz_stacktrace_vim9() + let lines =<< trim [SCRIPT] + var stacktrace = getstacktrace() + assert_notequal([], stacktrace) + for d in stacktrace + assert_true(has_key(d, 'lnum')) + endfor + try + throw 'Exception from s:Func' + catch + assert_notequal([], v:stacktrace) + assert_equal(len(stacktrace), len(v:stacktrace)) + for d in v:stacktrace + assert_true(has_key(d, 'lnum')) + endfor + endtry + [SCRIPT] + call CheckDefSuccess(lines) + " FIXME: v:stacktrace is not cleared after the exception handling, and this + " test has to be run as the last one because of this. + " call assert_equal([], v:stacktrace) +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/test/old/testdir/test_window_cmd.vim b/test/old/testdir/test_window_cmd.vim index 343bc9fd83..24517f90cb 100644 --- a/test/old/testdir/test_window_cmd.vim +++ b/test/old/testdir/test_window_cmd.vim @@ -56,7 +56,6 @@ func Test_window_cmd_cmdwin_with_vsp() endfunc func Test_cmdheight_not_changed() - throw 'Skipped: N/A' set cmdheight=2 set winminheight=0 augroup Maximize diff --git a/test/unit/fixtures/vterm_test.c b/test/unit/fixtures/vterm_test.c index 8755e32e7a..7522962a05 100644 --- a/test/unit/fixtures/vterm_test.c +++ b/test/unit/fixtures/vterm_test.c @@ -1,7 +1,11 @@ #include <stdio.h> +#include <string.h> + #include "nvim/grid.h" #include "nvim/mbyte.h" - +#include "nvim/vterm/pen.h" +#include "nvim/vterm/screen.h" +#include "nvim/vterm/vterm_internal_defs.h" #include "vterm_test.h" int parser_text(const char bytes[], size_t len, void *user) @@ -204,7 +208,8 @@ int selection_query(VTermSelectionMask mask, void *user) return 1; } -static void print_schar(FILE *f, schar_T schar) { +static void print_schar(FILE *f, schar_T schar) +{ char buf[MAX_SCHAR_SIZE]; schar_get(buf, schar); StrCharInfo ci = utf_ptr2StrCharInfo(buf); @@ -319,6 +324,34 @@ void print_color(const VTermColor *col) fclose(f); } +static VTermValueType vterm_get_prop_type(VTermProp prop) +{ + switch (prop) { + case VTERM_PROP_CURSORVISIBLE: + return VTERM_VALUETYPE_BOOL; + case VTERM_PROP_CURSORBLINK: + return VTERM_VALUETYPE_BOOL; + case VTERM_PROP_ALTSCREEN: + return VTERM_VALUETYPE_BOOL; + case VTERM_PROP_TITLE: + return VTERM_VALUETYPE_STRING; + case VTERM_PROP_ICONNAME: + return VTERM_VALUETYPE_STRING; + case VTERM_PROP_REVERSE: + return VTERM_VALUETYPE_BOOL; + case VTERM_PROP_CURSORSHAPE: + return VTERM_VALUETYPE_INT; + case VTERM_PROP_MOUSE: + return VTERM_VALUETYPE_INT; + case VTERM_PROP_FOCUSREPORT: + return VTERM_VALUETYPE_BOOL; + + case VTERM_N_PROPS: + return 0; + } + return 0; // UNREACHABLE +} + bool want_state_settermprop; int state_settermprop(VTermProp prop, VTermValue *val, void *user) { @@ -463,14 +496,14 @@ int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user) } int eol = cols; - while (eol && !cells[eol-1].schar) { + while (eol && !cells[eol - 1].schar) { eol--; } FILE *f = fopen(VTERM_TEST_FILE, "a"); fprintf(f, "sb_pushline %d =", cols); for (int c = 0; c < eol; c++) { - fprintf(f, " "); + fprintf(f, " "); print_schar(f, cells[c].schar); } fprintf(f, "\n"); @@ -488,7 +521,7 @@ int screen_sb_popline(int cols, VTermScreenCell *cells, void *user) // All lines of scrollback contain "ABCDE" for (int col = 0; col < cols; col++) { - if(col < 5) { + if (col < 5) { cells[col].schar = schar_from_ascii((uint32_t)('A' + col)); } else { cells[col].schar = 0; @@ -524,3 +557,231 @@ void term_output(const char *s, size_t len, void *user) } fclose(f); } + +int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val) +{ + switch (attr) { + case VTERM_ATTR_BOLD: + val->boolean = state->pen.bold; + return 1; + + case VTERM_ATTR_UNDERLINE: + val->number = state->pen.underline; + return 1; + + case VTERM_ATTR_ITALIC: + val->boolean = state->pen.italic; + return 1; + + case VTERM_ATTR_BLINK: + val->boolean = state->pen.blink; + return 1; + + case VTERM_ATTR_REVERSE: + val->boolean = state->pen.reverse; + return 1; + + case VTERM_ATTR_CONCEAL: + val->boolean = state->pen.conceal; + return 1; + + case VTERM_ATTR_STRIKE: + val->boolean = state->pen.strike; + return 1; + + case VTERM_ATTR_FONT: + val->number = state->pen.font; + return 1; + + case VTERM_ATTR_FOREGROUND: + val->color = state->pen.fg; + return 1; + + case VTERM_ATTR_BACKGROUND: + val->color = state->pen.bg; + return 1; + + case VTERM_ATTR_SMALL: + val->boolean = state->pen.small; + return 1; + + case VTERM_ATTR_BASELINE: + val->number = state->pen.baseline; + return 1; + + case VTERM_ATTR_URI: + val->number = state->pen.uri; + return 1; + + case VTERM_N_ATTRS: + return 0; + } + + return 0; +} + +static int attrs_differ(VTermAttrMask attrs, ScreenCell *a, ScreenCell *b) +{ + if ((attrs & VTERM_ATTR_BOLD_MASK) && (a->pen.bold != b->pen.bold)) { + return 1; + } + if ((attrs & VTERM_ATTR_UNDERLINE_MASK) && (a->pen.underline != b->pen.underline)) { + return 1; + } + if ((attrs & VTERM_ATTR_ITALIC_MASK) && (a->pen.italic != b->pen.italic)) { + return 1; + } + if ((attrs & VTERM_ATTR_BLINK_MASK) && (a->pen.blink != b->pen.blink)) { + return 1; + } + if ((attrs & VTERM_ATTR_REVERSE_MASK) && (a->pen.reverse != b->pen.reverse)) { + return 1; + } + if ((attrs & VTERM_ATTR_CONCEAL_MASK) && (a->pen.conceal != b->pen.conceal)) { + return 1; + } + if ((attrs & VTERM_ATTR_STRIKE_MASK) && (a->pen.strike != b->pen.strike)) { + return 1; + } + if ((attrs & VTERM_ATTR_FONT_MASK) && (a->pen.font != b->pen.font)) { + return 1; + } + if ((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_is_equal(&a->pen.fg, &b->pen.fg)) { + return 1; + } + if ((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg)) { + return 1; + } + if ((attrs & VTERM_ATTR_SMALL_MASK) && (a->pen.small != b->pen.small)) { + return 1; + } + if ((attrs & VTERM_ATTR_BASELINE_MASK) && (a->pen.baseline != b->pen.baseline)) { + return 1; + } + if ((attrs & VTERM_ATTR_URI_MASK) && (a->pen.uri != b->pen.uri)) { + return 1; + } + + return 0; +} + +int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, + VTermAttrMask attrs) +{ + ScreenCell *target = getcell(screen, pos.row, pos.col); + + // TODO(vterm): bounds check + extent->start_row = pos.row; + extent->end_row = pos.row + 1; + + if (extent->start_col < 0) { + extent->start_col = 0; + } + if (extent->end_col < 0) { + extent->end_col = screen->cols; + } + + int col; + + for (col = pos.col - 1; col >= extent->start_col; col--) { + if (attrs_differ(attrs, target, getcell(screen, pos.row, col))) { + break; + } + } + extent->start_col = col + 1; + + for (col = pos.col + 1; col < extent->end_col; col++) { + if (attrs_differ(attrs, target, getcell(screen, pos.row, col))) { + break; + } + } + extent->end_col = col - 1; + + return 1; +} + +/// Does not NUL-terminate the buffer +size_t vterm_screen_get_text(const VTermScreen *screen, char *buffer, size_t len, + const VTermRect rect) +{ + size_t outpos = 0; + int padding = 0; + +#define PUT(bytes, thislen) \ + if (true) { \ + if (buffer && outpos + thislen <= len) \ + memcpy((char *)buffer + outpos, bytes, thislen); \ + outpos += thislen; \ + } \ + + for (int row = rect.start_row; row < rect.end_row; row++) { + for (int col = rect.start_col; col < rect.end_col; col++) { + ScreenCell *cell = getcell(screen, row, col); + + if (cell->schar == 0) { + // Erased cell, might need a space + padding++; + } else if (cell->schar == (uint32_t)-1) { + // Gap behind a double-width char, do nothing + } else { + while (padding) { + PUT(" ", 1); + padding--; + } + char buf[MAX_SCHAR_SIZE + 1]; + size_t thislen = schar_get(buf, cell->schar); + PUT(buf, thislen); + } + } + + if (row < rect.end_row - 1) { + PUT("\n", 1); + padding = 0; + } + } + + return outpos; +} + +int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos) +{ + // This cell is EOL if this and every cell to the right is black + for (; pos.col < screen->cols; pos.col++) { + ScreenCell *cell = getcell(screen, pos.row, pos.col); + if (cell->schar != 0) { + return 0; + } + } + + return 1; +} + +void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos) +{ + *cursorpos = state->pos; +} + +void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright) +{ + state->bold_is_highbright = bold_is_highbright; +} + +/// Compares two colours. Returns true if the colors are equal, false otherwise. +int vterm_color_is_equal(const VTermColor *a, const VTermColor *b) +{ + // First make sure that the two colours are of the same type (RGB/Indexed) + if (a->type != b->type) { + return false; + } + + // Depending on the type inspect the corresponding members + if (VTERM_COLOR_IS_INDEXED(a)) { + return a->indexed.idx == b->indexed.idx; + } else if (VTERM_COLOR_IS_RGB(a)) { + return (a->rgb.red == b->rgb.red) + && (a->rgb.green == b->rgb.green) + && (a->rgb.blue == b->rgb.blue); + } + + return 0; +} diff --git a/test/unit/fixtures/vterm_test.h b/test/unit/fixtures/vterm_test.h index a05e7d499e..ef6463af6d 100644 --- a/test/unit/fixtures/vterm_test.h +++ b/test/unit/fixtures/vterm_test.h @@ -2,8 +2,17 @@ #include <stdint.h> #include "nvim/macros_defs.h" -#include "vterm/vterm.h" +#include "nvim/vterm/vterm.h" +EXTERN VTermPos state_pos; +EXTERN bool want_state_putglyph INIT (=false); +EXTERN bool want_state_movecursor INIT(= false); +EXTERN bool want_state_erase INIT(= false); +EXTERN bool want_state_scrollrect INIT(= false); +EXTERN bool want_state_moverect INIT(= false); +EXTERN bool want_state_settermprop INIT(= false); +EXTERN bool want_state_scrollback INIT(= false); +EXTERN bool want_screen_scrollback INIT(= false); int parser_text(const char bytes[], size_t len, void *user); int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user); @@ -27,12 +36,10 @@ int screen_sb_pushline(int cols, const VTermScreenCell *cells, void *user); int screen_sb_popline(int cols, VTermScreenCell *cells, void *user); int screen_sb_clear(void *user); void term_output(const char *s, size_t len, void *user); -EXTERN VTermPos state_pos; -EXTERN bool want_state_putglyph INIT (=false); -EXTERN bool want_state_movecursor INIT(= false); -EXTERN bool want_state_erase INIT(= false); -EXTERN bool want_state_scrollrect INIT(= false); -EXTERN bool want_state_moverect INIT(= false); -EXTERN bool want_state_settermprop INIT(= false); -EXTERN bool want_state_scrollback INIT(= false); -EXTERN bool want_screen_scrollback INIT(= false); +int vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val); +int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs); +size_t vterm_screen_get_text(const VTermScreen *screen, char *buffer, size_t len, VTermRect rect); +int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos); +void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos); +void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright); +int vterm_color_is_equal(const VTermColor *a, const VTermColor *b); diff --git a/test/unit/vterm_spec.lua b/test/unit/vterm_spec.lua index 2457525fb7..db0aa3c575 100644 --- a/test/unit/vterm_spec.lua +++ b/test/unit/vterm_spec.lua @@ -61,7 +61,6 @@ local bit = require('bit') --- @field vterm_screen_enable_reflow function --- @field vterm_screen_get_attrs_extent function --- @field vterm_screen_get_cell function ---- @field vterm_screen_get_chars fun(any, any, any, any):any --- @field vterm_screen_get_text fun(any, any, any, any):any --- @field vterm_screen_is_eol fun(any, any):any --- @field vterm_screen_reset function @@ -79,10 +78,17 @@ local bit = require('bit') --- @field vterm_state_set_selection_callbacks function --- @field vterm_state_set_unrecognised_fallbacks function local vterm = t.cimport( - './src/nvim/mbyte.h', './src/nvim/grid.h', - './src/vterm/vterm.h', - './src/vterm/vterm_internal.h', + './src/nvim/mbyte.h', + './src/nvim/vterm/encoding.h', + './src/nvim/vterm/keyboard.h', + './src/nvim/vterm/mouse.h', + './src/nvim/vterm/parser.h', + './src/nvim/vterm/pen.h', + './src/nvim/vterm/screen.h', + './src/nvim/vterm/state.h', + './src/nvim/vterm/vterm.h', + './src/nvim/vterm/vterm_internal.h', './test/unit/fixtures/vterm_test.h' ) |