From 6e53eecca4c7f79bc77d66c57d5378ce1be77593 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 8 Mar 2023 13:17:20 +0000 Subject: [PATCH] feat!: (BREAKING PLEASE READ) use queries for determining context (#198) This plugin has been significantly rewritten to use Treesitter queries instead of patterns for determining context regions for languages. The main benefits of this change are: - it is a much simpler implementation since we can leverage core APIs. - it fits in more generally with the Treesitter eco-system. - it allows configuration of contexts to be provided from multiples sources. - it allows more sophisticated configuration of contexts since queries (with directives and predicates) are much more powerful than patterns. - the query format should be usable for other editors. The major downside of this new implementation is that it requires each language to provide it's own query as opposed to using the general purpose patterns. This means that some languages which had contexts before may not have them now. If this is the case then please raise an issue. Adding queries for a specific language is fairly simple but too much work to implement for all 170+ parsers that exist. This commits provides explicit support for: - bash - c - cpp - typescript - rust - json - lua - markdown - python - yaml - php - scala - teal - toml - vim Please see the README for instructions on how to add support for other languages. This commit also drops explicit support for Nvim 0.7. If you still need support for this version then you can use the `compat/0.7` release. --- .github/workflows/ci.yml | 2 +- Makefile | 17 +- README.md | 299 ++++++++++++------ lua/treesitter-context.lua | 503 +++++++++++++------------------ queries/bash/context.scm | 6 + queries/c/context.scm | 28 ++ queries/cpp/context.scm | 5 + queries/json/context.scm | 5 + queries/lua/context.scm | 27 ++ queries/markdown/context.scm | 2 + queries/php/context.scm | 28 ++ queries/python/context.scm | 47 +++ queries/rust/context.scm | 29 ++ queries/scala/context.scm | 28 ++ queries/teal/context.scm | 28 ++ queries/toml/context.scm | 5 + queries/typescript/context.scm | 23 ++ queries/vim/context.scm | 36 +++ queries/yaml/context.scm | 5 + test/test.c | 76 +++++ test/test.php | 83 +++++ test/{nested_file.rs => test.rs} | 24 ++ test/test.ts | 47 +++ test/ts_context_spec.lua | 294 ++++++++++++------ 24 files changed, 1141 insertions(+), 506 deletions(-) create mode 100644 queries/bash/context.scm create mode 100644 queries/c/context.scm create mode 100644 queries/cpp/context.scm create mode 100644 queries/json/context.scm create mode 100644 queries/lua/context.scm create mode 100644 queries/markdown/context.scm create mode 100644 queries/php/context.scm create mode 100644 queries/python/context.scm create mode 100644 queries/rust/context.scm create mode 100644 queries/scala/context.scm create mode 100644 queries/teal/context.scm create mode 100644 queries/toml/context.scm create mode 100644 queries/typescript/context.scm create mode 100644 queries/vim/context.scm create mode 100644 queries/yaml/context.scm create mode 100644 test/test.c create mode 100644 test/test.php rename test/{nested_file.rs => test.rs} (51%) create mode 100644 test/test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bc8c4af..8e1a8f0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: true matrix: - neovim_branch: ['v0.7.0'] + neovim_branch: ['v0.8.2'] runs-on: ubuntu-latest env: NEOVIM_BRANCH: ${{ matrix.neovim_branch }} diff --git a/Makefile b/Makefile index 3cef9349..a4cbbf12 100644 --- a/Makefile +++ b/Makefile @@ -16,26 +16,21 @@ $(NEOVIM): nvim-treesitter: git clone --depth 1 https://github.com/nvim-treesitter/nvim-treesitter -nvim-treesitter/parser/lua.so: nvim-treesitter $(NEOVIM) +nvim-treesitter/parser/%.so: nvim-treesitter $(NEOVIM) VIMRUNTIME=$(NEOVIM)/runtime $(NEOVIM)/build/bin/nvim \ --headless \ --clean \ --cmd 'set rtp+=./nvim-treesitter' \ - -c "TSInstallSync lua" \ - -c "q" - -nvim-treesitter/parser/rust.so: nvim-treesitter $(NEOVIM) - VIMRUNTIME=$(NEOVIM)/runtime $(NEOVIM)/build/bin/nvim \ - --headless \ - --clean \ - --cmd 'set rtp+=./nvim-treesitter' \ - -c "TSInstallSync rust" \ + -c "TSInstallSync $*" \ -c "q" export VIMRUNTIME=$(PWD)/$(NEOVIM)/runtime .PHONY: test -test: $(NEOVIM) nvim-treesitter nvim-treesitter/parser/lua.so nvim-treesitter/parser/rust.so +test: $(NEOVIM) nvim-treesitter \ + nvim-treesitter/parser/lua.so \ + nvim-treesitter/parser/rust.so \ + nvim-treesitter/parser/typescript.so $(NEOVIM)/.deps/usr/bin/busted \ -v \ --lazy \ diff --git a/README.md b/README.md index f7bbe1b9..7605372f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ implemented with [nvim-treesitter](https://github.com/nvim-treesitter/nvim-trees ## Requirements -Neovim >= v0.7.x +Neovim >= v0.8.2 Note: if you need support for Neovim 0.6.x please use the tag `compat/0.6`. @@ -29,11 +29,166 @@ use 'nvim-treesitter/nvim-treesitter-context' ![theme](./static/demo.gif) -### Notes +## Supported languages -This plugins uses the new neovim `WinScrolled` event when available to update its -context window. Make sure to have a recent neovim build to get this behavior. The fallback -behavior is to update its content on `CursorMoved`. + - [x] `bash` + - [x] `c` + - [x] `cpp` + - [x] `typescript` + - [x] `rust` + - [x] `json` + - [x] `lua` + - [x] `markdown` + - [x] `python` + - [x] `yaml` + - [x] `php` + - [x] `scala` + - [x] `teal` + - [x] `toml` + - [x] `vim` + - [ ] `ada` + - [ ] `agda` + - [ ] `arduino` + - [ ] `astro` + - [ ] `beancount` + - [ ] `bibtex` + - [ ] `bicep` + - [ ] `blueprint` + - [ ] `c_sharp` + - [ ] `capnp` + - [ ] `chatito` + - [ ] `clojure` + - [ ] `cmake` + - [ ] `commonlisp` + - [ ] `cooklang` + - [ ] `cpon` + - [ ] `css` + - [ ] `cuda` + - [ ] `d` + - [ ] `dart` + - [ ] `devicetree` + - [ ] `dhall` + - [ ] `dockerfile` + - [ ] `dot` + - [ ] `ebnf` + - [ ] `ecma` + - [ ] `eex` + - [ ] `elixir` + - [ ] `elm` + - [ ] `elsa` + - [ ] `elvish` + - [ ] `embedded_template` + - [ ] `erlang` + - [ ] `fennel` + - [ ] `fish` + - [ ] `foam` + - [ ] `fsh` + - [ ] `func` + - [ ] `fusion` + - [ ] `gdscript` + - [ ] `git_rebase` + - [ ] `gleam` + - [ ] `glimmer` + - [ ] `glsl` + - [ ] `go` + - [ ] `godot_resource` + - [ ] `gomod` + - [ ] `gosum` + - [ ] `gowork` + - [ ] `graphql` + - [ ] `hack` + - [ ] `haskell` + - [ ] `hcl` + - [ ] `heex` + - [ ] `hjson` + - [ ] `hlsl` + - [ ] `hocon` + - [ ] `html` + - [ ] `html_tags` + - [ ] `htmldjango` + - [ ] `http` + - [ ] `ini` + - [ ] `java` + - [ ] `javascript` + - [ ] `jq` + - [ ] `jsdoc` + - [ ] `json5` + - [ ] `jsonc` + - [ ] `jsonnet` + - [ ] `jsx` + - [ ] `julia` + - [ ] `kdl` + - [ ] `kotlin` + - [ ] `lalrpop` + - [ ] `latex` + - [ ] `ledger` + - [ ] `llvm` + - [ ] `m68k` + - [ ] `matlab` + - [ ] `menhir` + - [ ] `mermaid` + - [ ] `meson` + - [ ] `nickel` + - [ ] `nix` + - [ ] `ocaml` + - [ ] `ocaml_interface` + - [ ] `ocamllex` + - [ ] `pascal` + - [ ] `perl` + - [ ] `phpdoc` + - [ ] `pioasm` + - [ ] `po` + - [ ] `poe_filter` + - [ ] `prisma` + - [ ] `proto` + - [ ] `prql` + - [ ] `pug` + - [ ] `ql` + - [ ] `qmldir` + - [ ] `qmljs` + - [ ] `query` + - [ ] `r` + - [ ] `racket` + - [ ] `rasi` + - [ ] `rego` + - [ ] `rnoweb` + - [ ] `ron` + - [ ] `rst` + - [ ] `ruby` + - [ ] `scheme` + - [ ] `scss` + - [ ] `slint` + - [ ] `smali` + - [ ] `smithy` + - [ ] `solidity` + - [ ] `sparql` + - [ ] `sql` + - [ ] `starlark` + - [ ] `supercollider` + - [ ] `surface` + - [ ] `svelte` + - [ ] `swift` + - [ ] `sxhkdrc` + - [ ] `t32` + - [ ] `terraform` + - [ ] `thrift` + - [ ] `tiger` + - [ ] `tlaplus` + - [ ] `todotxt` + - [ ] `tsx` + - [ ] `turtle` + - [ ] `twig` + - [ ] `ungrammar` + - [ ] `v` + - [ ] `vala` + - [ ] `verilog` + - [ ] `vhs` + - [ ] `vue` + - [ ] `wgsl` + - [ ] `wgsl_bevy` + - [ ] `yang` + - [ ] `yuck` + - [ ] `zig` ## Configuration @@ -41,94 +196,17 @@ behavior is to update its content on `CursorMoved`. ```lua require'treesitter-context'.setup{ - enable = true, -- Enable this plugin (Can be enabled/disabled later via commands) - max_lines = 0, -- How many lines the window should span. Values <= 0 mean no limit. - trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer' - min_window_height = 0, -- Minimum editor window height to enable context. Values <= 0 mean no limit. - patterns = { -- Match patterns for TS nodes. These get wrapped to match at word boundaries. - -- For all filetypes - -- Note that setting an entry here replaces all other patterns for this entry. - -- By setting the 'default' entry below, you can control which nodes you want to - -- appear in the context window. - default = { - 'class', - 'function', - 'method', - 'for', - 'while', - 'if', - 'switch', - 'case', - 'interface', - 'struct', - 'enum', - }, - -- Patterns for specific filetypes - -- If a pattern is missing, *open a PR* so everyone can benefit. - tex = { - 'chapter', - 'section', - 'subsection', - 'subsubsection', - }, - haskell = { - 'adt' - }, - rust = { - 'impl_item', - - }, - terraform = { - 'block', - 'object_elem', - 'attribute', - }, - scala = { - 'object_definition', - }, - vhdl = { - 'process_statement', - 'architecture_body', - 'entity_declaration', - }, - markdown = { - 'section', - }, - elixir = { - 'anonymous_function', - 'arguments', - 'block', - 'do_block', - 'list', - 'map', - 'tuple', - 'quoted_content', - }, - json = { - 'pair', - }, - typescript = { - 'export_statement', - }, - yaml = { - 'block_mapping_pair', - }, - }, - exact_patterns = { - -- Example for a specific filetype with Lua patterns - -- Treat patterns.rust as a Lua pattern (i.e "^impl_item$" will - -- exactly match "impl_item" only) - -- rust = true, - }, - - -- [!] The options below are exposed but shouldn't require your attention, - -- you can safely ignore them. - - zindex = 20, -- The Z-index of the context window - mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline' - -- Separator between context and content. Should be a single character string, like '-'. - -- When separator is set, the context will only show up when there are at least 2 lines above cursorline. - separator = nil, + enable = true, -- Enable this plugin (Can be enabled/disabled later via commands) + max_lines = 0, -- How many lines the window should span. Values <= 0 mean no limit. + min_window_height = 0, -- Minimum editor window height to enable context. Values <= 0 mean no limit. + line_numbers = true, + multiline_threshold = 20, -- Maximum number of lines to collapse for a single context line + trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer' + mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline' + -- Separator between context and content. Should be a single character string, like '-'. + -- When separator is set, the context will only show up when there are at least 2 lines above cursorline. + separator = nil, + zindex = 20, -- The Z-index of the context window } ``` @@ -151,3 +229,38 @@ However, you can use this to create a border by applying an underline highlight, ```vim hi TreesitterContextBottom gui=underline guisp=Grey ``` + +## Adding support for other languages + +To add support for another language, simply add a `context.scm` file under +`queries/[LANG]`. + +Queries specify the `@context` capture which specifies the first line of a node +will be used for the context. + +Here is a basic example for C: + +```query +(function_definition) @context +(for_statement) @context +(if_statement) @context +(while_statement) @context +(do_statement) @context +``` + +You can easily look at a node names of a tree using `InspectTree` in Nvim 0.9. + +Additionally an optional `@context.end` capture can also be specified. When +provided, the text from the start of the `@context` capture to the start of +`@context.end` capture (exclusive) will be used for the context and joined into +a single line. + +Here's what that looks like for C: + +```query +(if_statement consequence: (_ (_) @context.end)) @context +``` + +This query specifies that everything from the `if` keyword up-to the first +statement (exclusive) should be used for the context. This is useful when an +if-statement spans multiple lines. diff --git a/lua/treesitter-context.lua b/lua/treesitter-context.lua index 4915d0d0..cae25999 100644 --- a/lua/treesitter-context.lua +++ b/lua/treesitter-context.lua @@ -1,15 +1,25 @@ local api = vim.api -local ts_utils = require'nvim-treesitter.ts_utils' local highlighter = vim.treesitter.highlighter + local parsers = require'nvim-treesitter.parsers' local augroup = api.nvim_create_augroup local command = api.nvim_create_user_command -local function word_pattern(p) - return '%f[%w]' .. p .. '%f[^%w]' -end +---@diagnostic disable:invisible +--- @class Config +--- @field enable boolean +--- @field max_lines integer +--- @field min_window_height integer +--- @field line_numbers boolean +--- @field multiline_threshold integer +--- @field trim_scope 'outer'|'inner' +--- @field zindex integer +--- @field mode 'cursor'|'topline' +--- @field separator string? + +--- @type Config local defaultConfig = { enable = true, max_lines = 0, -- no limit @@ -22,275 +32,118 @@ local defaultConfig = { separator = nil, } +--- @type Config local config = {} -- Constants --- Tells us at which node type to stop when highlighting a multi-line --- node. If not specified, the highlighting stops after the first line. -local last_nodes -local QUERY_FIELD_NAME = 1 -local QUERY_NODE_TYPE = 2 -do - local function f(name) - return { - name = name, - kind = QUERY_FIELD_NAME, - } - end - - local function t(name) - return { - name = name, - kind = QUERY_NODE_TYPE, - } - end - - last_nodes = { - [word_pattern('function')] = { - c = { f'declarator' }, - cpp = { f'declarator' }, - lua = { f'parameters' }, - teal = { f'signature' }, - python = { f'return_type', f'parameters' }, - rust = { f'return_type', f'parameters' }, - javascript = { f'parameters' }, - typescript = { f'return_type', f'parameters' }, - }, - [word_pattern('method')] = { - lua = { f'parameters' }, - javascript = { f'parameters' }, - typescript = { f'return_type', f'parameters' }, - }, - [word_pattern('class')] = { - cpp = { t'base_class_clause', f'name' }, - python = { f'superclasses' }, - } - } -end - --- Tells us which leading child node type to skip when highlighting a --- multi-line node. -local skip_leading_types = { - [word_pattern('class')] = { - php = 'attribute_list', - }, - [word_pattern('method')] = { - php = 'attribute_list', - }, -} - --- There are language-specific -local DEFAULT_TYPE_PATTERNS = { - -- These catch most generic groups, eg "function_declaration" or "function_block" - default = { - 'class', - 'function', - 'method', - 'for', - 'while', - 'if', - 'switch', - 'case', - 'interface', - 'struct', - 'enum', - }, - elixir = { - 'anonymous_function', - 'arguments', - 'block', - 'do_block', - 'list', - 'map', - 'tuple', - 'quoted_content', - }, - haskell = { - 'adt' - }, - json = { - 'pair', - }, - markdown = { - 'section', - }, - python = { - 'with_statement', - }, - rust = { - 'impl_item', - }, - scala = { - 'object_definition', - }, - terraform = { - 'block', - 'object_elem', - 'attribute', - }, - tex = { - 'chapter', - 'section', - 'subsection', - 'subsubsection', - }, - typescript = { - 'export_statement', - }, - verilog = { - 'always_construct', - 'statement_or_null', - }, - vhdl = { - 'process_statement', - 'architecture_body', - 'entity_declaration', - }, - yaml = { - 'block_mapping_pair', - }, - exact_patterns = {}, -} - -local DEFAULT_TYPE_EXCLUDE_PATTERNS = { - default = {}, - teal = { - 'function_body', - }, -} - local INDENT_PATTERN = '^%s+' -- Script variables local did_setup = false local enabled = false -local gutter_winid, context_winid -local gutter_bufnr, context_bufnr -- Don't access directly, use get_bufs() + +-- Don't access directly, use get_bufs() +--- @type integer? +local gutter_winid + +--- @type integer? +local context_winid + +--- @type integer? +local gutter_bufnr + +--- @type integer? +local context_bufnr + local ns = api.nvim_create_namespace('nvim-treesitter-context') + +--- @type TSNode[]? local previous_nodes +--- @return TSNode local function get_root_node() + ---@diagnostic disable-next-line local tree = parsers.get_parser():parse()[1] return tree:root() end -local function is_excluded(node, filetype) - local node_type = node:type() - for _, rgx in ipairs(config.exclude_patterns.default) do - if node_type:find(rgx) then - return true - end - end - local filetype_patterns = config.exclude_patterns[filetype] - for _, rgx in ipairs(filetype_patterns or {}) do - if node_type:find(rgx) then - return true - end - end - return false -end +--- @param node TSNode +--- @param query Query +--- @return Range4? +local function is_valid(node, query) + local bufnr = api.nvim_get_current_buf() + local range --[[@type Range4]] = {node:range()} + range[3] = range[1] + range[4] = -1 -local function is_valid(node, filetype) - if is_excluded(node, filetype) then - return false - end + -- Try and iterate on the parent node as iter_matches won't match on the top + -- level node + local iter_node = node:parent() or node - local node_type = node:type() - for _, rgx in ipairs(config.patterns.default) do - if node_type:find(rgx) then - return true - end - end - local filetype_patterns = config.patterns[filetype] - for _, rgx in ipairs(filetype_patterns or {}) do - if node_type:find(rgx) then - return true - end - end - return false -end + for _, match in query:iter_matches(iter_node, bufnr, 0, -1) do + local r = false -local function get_type_pattern(node, type_patterns) - local node_type = node:type() - for _, rgx in ipairs(type_patterns) do - if node_type:find(rgx) then - return rgx - end - end -end + for id, node0 in pairs(match --[[@as table]]) do + local srow, scol, erow, ecol = node0:range() -local function find_node(node, query) - if query.kind == QUERY_FIELD_NAME then - local fields = node:field(query.name) - if fields and fields[1] then - return fields[1] - end - elseif query.kind == QUERY_NODE_TYPE then - local children = ts_utils.get_named_children(node) - for _, c in ipairs(children) do - if c:type() == query.name then - return c + -- because iter_node != node we could match outside of node + if srow < range[1] then + break + end + + local name = query.captures[id] -- name of the capture in the query + if not r and name == 'context' then + r = node == node0 + elseif name == 'context.final' then + range[3] = erow + range[4] = ecol + elseif name == 'context.end' then + range[3] = srow + range[4] = scol end end + + if r then + return range + end end end -local function get_text_for_node(node) - local type = get_type_pattern(node, config.patterns.default) or node:type() - local filetype = vim.bo.filetype - - local start_row, start_col = node:start() - local end_row, end_col = node:end_() - - local node_text = vim.treesitter.query.get_node_text(node, 0) - if node_text == nil then return nil, nil end - - local lines = vim.split(node_text, '\n') - - if start_col ~= 0 then - lines[1] = api.nvim_buf_get_lines(0, start_row, start_row + 1, false)[1] +--- @param range Range4 +--- @return string[]?, Range4? +local function get_text_for_range(range) + if range[4] == 0 then + range[3] = range[3] - 1 + range[4] = -1 + end + local lines = api.nvim_buf_get_text(0, range[1], 0, range[3], range[4], {}) + if lines == nil then + return nil, nil end - start_col = 0 - local queries = (last_nodes[type] or {})[filetype] + local start_row = range[1] + local end_row = range[3] + local end_col = range[4] - local last_position + lines = vim.list_slice(lines, 1, end_row - start_row+1) + lines[#lines] = lines[#lines]:sub(1, end_col) - if queries then - local child - for _, q in ipairs(queries) do - local n = find_node(node, q) - if n then - child = n - break - end - end - - if child then - last_position = {child:end_()} - - end_row = last_position[1] - end_col = last_position[2] - local last_index = end_row - start_row - lines = vim.list_slice(lines, 1, last_index + 1) - lines[#lines] = lines[#lines]:sub(1, end_col) - end - end - - if not last_position or #lines > config.multiline_threshold then + if #lines > config.multiline_threshold then lines = vim.list_slice(lines, 1, 1) end_row = start_row end_col = #lines[1] end - local range = {start_row, start_col, end_row, end_col} + range = {start_row, 0, end_row, end_col} return lines, range end -- Merge lines, removing the indentation after 1st line +--- @param lines string[] +--- @return string local function merge_lines(lines) local text = { lines[1] } for i = 2, #lines do @@ -300,8 +153,13 @@ local function merge_lines(lines) end -- Get indentation for lines except first +--- @param lines string[] +--- @return integer[] local function get_indents(lines) + --- @type integer[] + --- @diagnostic disable-next-line local indents = vim.tbl_map(function(line) + --- @type string? local indent = line:match(INDENT_PATTERN) return indent and #indent or 0 end, lines) @@ -310,13 +168,14 @@ local function get_indents(lines) return indents end +--- @return integer local function get_gutter_width() return vim.fn.getwininfo(vim.api.nvim_get_current_win())[1].textoff end -local cursor_moved_vertical +local cursor_moved_vertical --[[@type fun(): boolean]] do - local line + local line --[[@type integer]] cursor_moved_vertical = function() local newline = vim.api.nvim_win_get_cursor(0)[1] if newline ~= line then @@ -327,6 +186,7 @@ do end end +--- @return integer, integer local function get_bufs() if not context_bufnr or not api.nvim_buf_is_valid(context_bufnr) then context_bufnr = api.nvim_create_buf(false, true) @@ -351,6 +211,14 @@ local function delete_bufs() gutter_bufnr = nil end +--- @param bufnr integer +--- @param winid integer? +--- @param width integer +--- @param height integer +--- @param col integer +--- @param ty string +--- @param hl string +--- @return integer local function display_window(bufnr, winid, width, height, col, ty, hl) if not winid or not api.nvim_win_is_valid(winid) then local sep = config.separator @@ -389,6 +257,34 @@ local M = { config = config, } +--- @param node TSNode? +--- @return TSNode[] +local function get_node_parents(node) + -- save nodes in a table to iterate from top to bottom + --- @type TSNode[] + local parents = {} + while node ~= nil do + parents[#parents+1] = node + node = node:parent() + end + return parents +end + +--- @return integer, integer +local function get_pos() + --- @type integer, integer + local lnum, col + if config.mode == 'topline' then + lnum, col = vim.fn.line('w0') --[[@as integer]], 0 + else -- default to 'cursor' + lnum, col = unpack(api.nvim_win_get_cursor(0)) --[[@as integer]] + end + + return lnum, col +end + +--- @param max_lines integer +--- @return Range4[]? local function get_parent_matches(max_lines) if max_lines == 0 then return @@ -399,14 +295,31 @@ local function get_parent_matches(max_lines) end local root_node = get_root_node() - local lnum, col - if config.mode == 'topline' then - lnum, col = vim.fn.line('w0'), 0 - else -- default to 'cursor' - lnum, col = unpack(api.nvim_win_get_cursor(0)) + + --- @type string + local lang = parsers.ft_to_lang(vim.bo.filetype) + + local ok, query = pcall(vim.treesitter.query.get_query, lang, 'context') + + if not ok then + vim.notify_once( + string.format('Unable to load context query for %s:\n%s', lang, query), + vim.log.levels.ERROR, + { title = 'nvim-treesitter-context' } + ) + return + end + + if not query then + return end + local lnum, col = get_pos() + + --- @type Range4[] local last_matches + + --- @type Range4[] local parent_matches = {} local line_offset = 0 @@ -423,25 +336,19 @@ local function get_parent_matches(max_lines) local topline = vim.fn.line('w0') -- save nodes in a table to iterate from top to bottom - local parents = {} - while node ~= nil do - parents[#parents+1] = node - node = node:parent() - end + local parents = get_node_parents(node) for i = #parents, 1, -1 do local parent = parents[i] local row = parent:start() local height = math.min(max_lines, #parent_matches) - if is_valid(parent, vim.bo.filetype) - and row >= 0 - and row < (topline + height - 1) then - + local range = is_valid(parent, query) + if range and row >= 0 and row < (topline + height - 1) then if row == last_row then - parent_matches[#parent_matches] = parent + parent_matches[#parent_matches] = range else - table.insert(parent_matches, parent) + parent_matches[#parent_matches+1] = range last_row = row local new_height = math.min(max_lines, #parent_matches) @@ -469,6 +376,9 @@ local function get_parent_matches(max_lines) end end +--- @generic F: function +--- @param fn F +--- @return F local function throttle_fn(fn) local recalc_after_cooldown = false local cooling_down = false @@ -495,7 +405,6 @@ local function throttle_fn(fn) return wrapped end - local function close() previous_nodes = nil -- Can't close other windows when the command-line window is open @@ -514,6 +423,9 @@ local function close() gutter_winid = nil end +--- @param bufnr integer +--- @param lines string[] +--- @return boolean local function set_lines(bufnr, lines) local clines = api.nvim_buf_get_lines(bufnr, 0, -1, false) local redraw = false @@ -536,10 +448,13 @@ local function set_lines(bufnr, lines) return redraw end +--- @param bufnr integer +--- @param ctx_bufnr integer +--- @param contexts Context[] local function highlight_contexts(bufnr, ctx_bufnr, contexts) api.nvim_buf_clear_namespace(ctx_bufnr, ns, 0, -1) - local buf_highlighter = highlighter.active[bufnr] + local buf_highlighter = highlighter.active[bufnr] --[[@as TSHighlighter]] if not buf_highlighter then -- Use standard highlighting when TS highlighting is not available @@ -558,26 +473,26 @@ local function highlight_contexts(bufnr, ctx_bufnr, contexts) local buf_query = buf_highlighter:get_query(parsers.ft_to_lang(vim.bo.filetype)) - local query = buf_query:query() + local query = assert(buf_query:query()) local root = get_root_node() for i, context in ipairs(contexts) do - local start_row, _, end_row, end_col = unpack(context.range) + local start_row = context.range[1] + local end_row = context.range[3] + local end_col = context.range[4] local indents = context.indents local lines = context.lines - local start_row_abs = context.node:start() - - for capture, node in query:iter_captures(root, bufnr, start_row, context.node:end_()) do + for capture, node in query:iter_captures(root, bufnr, start_row, end_row + 1) do local node_start_row, node_start_col, node_end_row, node_end_col = node:range() if node_end_row > end_row or - (node_end_row == end_row and node_end_col > end_col) then + (node_end_row == end_row and node_end_col > end_col and end_col ~= -1) then break end - if node_start_row >= start_row_abs then - local intended_start_row = node_start_row - start_row_abs + if node_start_row >= start_row then + local intended_start_row = node_start_row - start_row -- Add 1 for each space added between lines when -- we replace '\n' with ' ' @@ -600,10 +515,15 @@ local function highlight_contexts(bufnr, ctx_bufnr, contexts) end end +--- @param lnum integer +--- @param width integer +--- @return string local function build_lno_str(lnum, width) return string.format('%'..width..'d', lnum) end +--- @param ctx_node_line_num integer +--- @return integer local function get_relative_line_num(ctx_node_line_num) local cursor_line_num = vim.fn.line('.') local num_folded_lines = 0 @@ -635,30 +555,18 @@ local function horizontal_scroll_contexts() end end -local function normalize_node(node) - local type = get_type_pattern(node, config.patterns.default) or node:type() - local filetype = vim.bo.filetype - - local skip_leading_type = (skip_leading_types[type] or {})[filetype] - if skip_leading_type then - local children = ts_utils.get_named_children(node) - for _, child in ipairs(children) do - if child:type() ~= skip_leading_type then - node = child - break - end - end - end - - return node -end +--- @class Context +--- @field indents integer[] +--- @field lines string[] +--- @field range Range4 -local function open(ctx_nodes) +--- @param ctx_ranges Range4[] +local function open(ctx_ranges) local bufnr = api.nvim_get_current_buf() local gutter_width = get_gutter_width() local win_width = math.max(1, api.nvim_win_get_width(0) - gutter_width) - local win_height = math.max(1, #ctx_nodes) + local win_height = math.max(1, #ctx_ranges) local gbufnr, ctx_bufnr = get_bufs() @@ -674,19 +582,18 @@ local function open(ctx_nodes) -- Set text - local context_text = {} - local lno_text = {} - local contexts = {} - - for _, node in ipairs(ctx_nodes) do - node = normalize_node(node) + local context_text --[[@type string[] ]] = {} + local lno_text --[[@type string[] ]] = {} + local contexts --[[@type Context[] ]] = {} - local lines, range = get_text_for_node(node) - if lines == nil or range == nil or range[1] == nil then return end + for _, range0 in ipairs(ctx_ranges) do + local lines, range = get_text_for_range(range0) + if lines == nil or range == nil or range[1] == nil then + return + end local text = merge_lines(lines) contexts[#contexts+1] = { - node = node, lines = lines, range = range, indents = get_indents(lines), @@ -694,7 +601,7 @@ local function open(ctx_nodes) table.insert(context_text, text) - local line_num + local line_num --[[@type integer]] local ctx_line_num = range[1] + 1 if vim.o.relativenumber then line_num = get_relative_line_num(ctx_line_num) @@ -710,13 +617,14 @@ local function open(ctx_nodes) return end - highlight_contexts(bufnr, ctx_bufnr, contexts) api.nvim_buf_set_extmark(ctx_bufnr, ns, #lno_text-1, 0, {end_line=#lno_text, hl_group='TreesitterContextBottom', hl_eol=true}) api.nvim_buf_set_extmark(gbufnr, ns, #context_text-1, 0, {end_line=#context_text, hl_group='TreesitterContextBottom', hl_eol=true}) end +--- @param config_max integer +--- @return integer local function calc_max_lines(config_max) local max_lines = config_max max_lines = max_lines == 0 and -1 or max_lines @@ -765,9 +673,12 @@ local update = throttle_fn(function() end end) +--- @param group string +--- @return function local function autocmd_for_group(group) local gid = augroup(group, {}) return function(event, opts) + ---@diagnostic disable:no-unknown if opts then if type(opts) == 'function' then opts = { callback = opts } @@ -826,17 +737,7 @@ function M.setup(options) local userOptions = options or {} - config = vim.tbl_deep_extend('force', {}, defaultConfig, userOptions) - config.patterns = vim.tbl_deep_extend('force', {}, DEFAULT_TYPE_PATTERNS, userOptions.patterns or {}) - config.exclude_patterns = vim.tbl_deep_extend('force', {}, DEFAULT_TYPE_EXCLUDE_PATTERNS, userOptions.exclude_patterns or {}) - config.exact_patterns = vim.tbl_deep_extend('force', {}, userOptions.exact_patterns or {}) - - for filetype, patterns in pairs(config.patterns) do - -- Map with word_pattern only if users don't need exact pattern matching - if not config.exact_patterns[filetype] then - config.patterns[filetype] = vim.tbl_map(word_pattern, patterns) - end - end + config = vim.tbl_deep_extend('force', {}, defaultConfig, userOptions) if config.enable then M.enable() diff --git a/queries/bash/context.scm b/queries/bash/context.scm new file mode 100644 index 00000000..c8408159 --- /dev/null +++ b/queries/bash/context.scm @@ -0,0 +1,6 @@ + +([ + (for_statement) + (function_definition) + (if_statement) +] @context) diff --git a/queries/c/context.scm b/queries/c/context.scm new file mode 100644 index 00000000..b7465503 --- /dev/null +++ b/queries/c/context.scm @@ -0,0 +1,28 @@ + +(function_definition + body: (_ (_) @context.end) +) @context + +(for_statement + (compound_statement) @context.end +) @context + +(if_statement + consequence: (_ (_) @context.end) +) @context + +(while_statement + body: (_ (_) @context.end) +) @context + +(do_statement + body: (_ (_) @context.end) +) @context + +(struct_specifier + body: (_ (_) @context.end) +) @context + +(enum_specifier + body: (_ (_) @context.end) +) @context diff --git a/queries/cpp/context.scm b/queries/cpp/context.scm new file mode 100644 index 00000000..85263e7f --- /dev/null +++ b/queries/cpp/context.scm @@ -0,0 +1,5 @@ +; inherits: c + +(class_specifier + body: (_ (_) @context.end) +) @context diff --git a/queries/json/context.scm b/queries/json/context.scm new file mode 100644 index 00000000..1ca5078a --- /dev/null +++ b/queries/json/context.scm @@ -0,0 +1,5 @@ + +([ + (object) + (pair) +] @context) diff --git a/queries/lua/context.scm b/queries/lua/context.scm new file mode 100644 index 00000000..3bda9bb9 --- /dev/null +++ b/queries/lua/context.scm @@ -0,0 +1,27 @@ +(for_statement + body: (_) @context.end +) @context + +(while_statement + body: (_) @context.end +) @context + +(do_statement + body: (_) @context.end +) @context + +(function_definition + body: (_) @context.end +) @context + +(table_constructor + (_) @context.end +) @context + +(function_declaration + parameters: (_) @context.final +) @context + +(if_statement + consequence: (_) @context.end +) @context diff --git a/queries/markdown/context.scm b/queries/markdown/context.scm new file mode 100644 index 00000000..6006e40b --- /dev/null +++ b/queries/markdown/context.scm @@ -0,0 +1,2 @@ + +((section) @context) diff --git a/queries/php/context.scm b/queries/php/context.scm new file mode 100644 index 00000000..bb54e6c6 --- /dev/null +++ b/queries/php/context.scm @@ -0,0 +1,28 @@ + +(function_definition + body: (_ (_) @context.end) +) @context + +(while_statement + body: (_ (_) @context.end) +) @context + +(if_statement + body: (_ (_) @context.end) +) @context + +(do_statement + body: (_ (_) @context.end) +) @context + +(foreach_statement + body: (_ (_) @context.end) +) @context + +(class_declaration + body: (_ (_) @context.end) +) @context + +(for_statement + (compound_statement (_) @context.end) +) @context diff --git a/queries/python/context.scm b/queries/python/context.scm new file mode 100644 index 00000000..4678814b --- /dev/null +++ b/queries/python/context.scm @@ -0,0 +1,47 @@ +(class_definition + body: (_) @context.end +) @context + +(function_definition + body: (_) @context.end +) @context + +(try_statement + body: (_) @context.end +) @context + +(with_statement + body: (_) @context.end +) @context + +(if_statement + consequence: (_) @context.end +) @context + +(elif_clause + consequence: (_) @context.end +) @context + +(case_clause + consequence: (_) @context.end +) @context + +(while_statement + body: (_) @context.end +) @context + +(except_clause + (block) @context.end +) @context + +(match_statement + alternative: (_) @context.end +) @context + +([ + (for_statement) + (finally_clause) + (else_clause) + (pair) + (expression_statement) +] @context) diff --git a/queries/rust/context.scm b/queries/rust/context.scm new file mode 100644 index 00000000..ea5c46c3 --- /dev/null +++ b/queries/rust/context.scm @@ -0,0 +1,29 @@ + +(for_expression + body: (_ (_) @context.end) +) @context + +(if_expression + consequence: (_ (_) @context.end) +) @context + +(function_item + body: (_ (_) @context.end) +) @context + +(impl_item + type: (_) @context.final +) @context + +(struct_item + body: (_ (_) @context.end) +) @context + +([ + (mod_item) + (enum_item) + (closure_expression) + (expression_statement) + (loop_expression) + (match_expression) +] @context) diff --git a/queries/scala/context.scm b/queries/scala/context.scm new file mode 100644 index 00000000..5882e727 --- /dev/null +++ b/queries/scala/context.scm @@ -0,0 +1,28 @@ + +(function_definition + body: (_ (_) @context.end) +) @context + +(class_definition + body: (_ (_) @context.end) +) @context + +(object_definition + body: (_ (_) @context.end) +) @context + +(case_clause + body: (_ (_) @context.end) +) @context + +(match_expression + body: (_ (_) @context.end) +) @context + +(call_expression + arguments: (_ (_) @context.end) +) @context + +(if_expression + consequence: (_ (_) @context.end) +) @context diff --git a/queries/teal/context.scm b/queries/teal/context.scm new file mode 100644 index 00000000..1af6b47a --- /dev/null +++ b/queries/teal/context.scm @@ -0,0 +1,28 @@ + +(while_statement) @context + +(generic_for_statement + body: (_ (_) @context.end) +) @context + +(function_statement + body: (_) @context.end +) @context + +(anon_function + body: (_) @context.end +) @context + +(if_statement + condition: (_) + (_) @context.end +) @context + +(elseif_block + condition: (_) + (_) @context.end +) @context + +(record_declaration + record_body: (_) @context.end +) @context diff --git a/queries/toml/context.scm b/queries/toml/context.scm new file mode 100644 index 00000000..3309812f --- /dev/null +++ b/queries/toml/context.scm @@ -0,0 +1,5 @@ + +([ + (table) + (pair) +] @context) diff --git a/queries/typescript/context.scm b/queries/typescript/context.scm new file mode 100644 index 00000000..9891c9bc --- /dev/null +++ b/queries/typescript/context.scm @@ -0,0 +1,23 @@ +(interface_declaration + body: (_ (_) @context.end) +) @context + +(class_declaration + body: (_ (_) @context.end) +) @context + +(method_definition + body: (_ (_) @context.end) +) @context + +(for_statement + body: (_ (_) @context.end) +) @context + +(function_declaration + body: (_ (_) @context.end) +) @context + +(if_statement + consequence: (_ (_) @context.end) +) @context diff --git a/queries/vim/context.scm b/queries/vim/context.scm new file mode 100644 index 00000000..f5f14082 --- /dev/null +++ b/queries/vim/context.scm @@ -0,0 +1,36 @@ + +(if_statement + (body) @context.end +) @context + +(elseif_statement + (body) @context.end +) @context + +(else_statement + (body) @context.end +) @context + +(function_definition + (body) @context.end +) @context + +(while_loop + (body) @context.end +) @context + +(for_loop + (body) @context.end +) @context + +(try_statement + (body) @context.end +) @context + +(catch_statement + (body) @context.end +) @context + +(finally_statement + (body) @context.end +) @context diff --git a/queries/yaml/context.scm b/queries/yaml/context.scm new file mode 100644 index 00000000..5b9a6bd2 --- /dev/null +++ b/queries/yaml/context.scm @@ -0,0 +1,5 @@ +([ + (block_mapping) + (block_mapping_pair) + (block_sequence_item) +] @context) diff --git a/test/test.c b/test/test.c new file mode 100644 index 00000000..6d175905 --- /dev/null +++ b/test/test.c @@ -0,0 +1,76 @@ +struct Bert { + int *f1; + // comment + int *f2; + // comment + // comment + // comment + // comment + // comment +}; + +typedef enum { + E1, + E2, + E3 + // comment + // comment + // comment + // comment + // comment + // comment +} Myenum; + +int main(int arg1, + char **arg2, + char **arg3 + ) +{ + + if (arg1 == 4 + && arg2 == arg3) { + + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + // comment + for (int i = 0; i < arg1; i++) { + // comment + // comment + // comment + // comment + while (1) { + // comment + // comment + // comment + // comment + // comment + } + + do { + // comment + // comment + // comment + // comment + // comment + + } while (1); + // comment + // comment + // comment + // comment + // comment + } + + } +} diff --git a/test/test.php b/test/test.php new file mode 100644 index 00000000..94b5fd33 --- /dev/null +++ b/test/test.php @@ -0,0 +1,83 @@ + $key) { + // comment + do { + // comment + echo "The number is: $x
"; + $x++; + + + + + } while ($x <= 5); + + for ($x = 0; $x <= 10; $x++) { + echo "The number is: $x
"; + + + + + + + + + + + + } + + + + foreach ($colors as $value) { + echo "$value
"; + + + + + + + } + + $high = $index - 1; + } + } + + + + //when key not found in array or array not sorted + return null; +} + +class Fruit { + + + + + // comment + + + + +} diff --git a/test/nested_file.rs b/test/test.rs similarity index 51% rename from test/nested_file.rs rename to test/test.rs index 30d7144f..fa40a895 100644 --- a/test/nested_file.rs +++ b/test/test.rs @@ -1,12 +1,28 @@ impl Foo { + + + fn bar(&self) { + + + + + if condition { + + + + for i in 0..100 { + + // comment + + } } } @@ -14,4 +30,12 @@ impl Foo { struct Foo { + active: bool, + + username: String, + + email: String, + + sign_in_count: u64, + } diff --git a/test/test.ts b/test/test.ts new file mode 100644 index 00000000..daab8405 --- /dev/null +++ b/test/test.ts @@ -0,0 +1,47 @@ +interface User { + name: string; + + + + id: number; + + + + +} +  +class UserAccount { + name: string; + id: number; + + + +  + constructor(name: string, id: number) { + this.name = name; + this.id = id; + + for (let i = 0; i < 3; i++) { + console.log("hello"); + + + + } + + + + + } +} + + +function wrapInArray(obj: string | string[]) { + if (typeof obj === "string") { + return [obj]; + + + + + } + return obj; +} diff --git a/test/ts_context_spec.lua b/test/ts_context_spec.lua index 31d5e6e6..8dd3d298 100644 --- a/test/ts_context_spec.lua +++ b/test/ts_context_spec.lua @@ -3,15 +3,16 @@ local Screen = require('test.functional.ui.screen') local clear = helpers.clear local exec_lua = helpers.exec_lua -local eq = helpers.eq local cmd = helpers.command local feed = helpers.feed describe('ts_context', function() local screen - setup(function() + before_each(function() + clear() screen = Screen.new(30, 16) + screen:attach() screen:set_default_attr_ids({ [1] = {foreground = Screen.colors.Brown, background = Screen.colors.LightMagenta, bold = true}; [2] = {background = Screen.colors.LightMagenta}; @@ -19,12 +20,13 @@ describe('ts_context', function() [4] = {bold = true, foreground = Screen.colors.Brown}; [5] = {foreground = Screen.colors.DarkCyan}; [6] = {bold = true, foreground = Screen.colors.Blue}; + [7] = {foreground = Screen.colors.SeaGreen, background = Screen.colors.LightMagenta, bold = true}; + [8] = {foreground = Screen.colors.Blue}; + [9] = {bold = true, foreground = Screen.colors.SeaGreen}; + [10] = {foreground = Screen.colors.Fuchsia, background = Screen.colors.LightMagenta}; + [11] = {foreground = Screen.colors.Fuchsia}; + [12] = {foreground = tonumber('0x6a0dad'), background = Screen.colors.LightMagenta}; }) - end) - - before_each(function() - clear() - screen:attach() cmd [[set runtimepath+=.,./nvim-treesitter]] cmd [[let $XDG_CACHE_HOME='scratch/cache']] cmd [[set packpath=]] @@ -88,99 +90,191 @@ describe('ts_context', function() ]]} end) - it('edit a file in topline mode', function() - exec_lua[[require'treesitter-context'.setup{ - mode = 'topline', - max_lines = 2, - }]] - cmd('edit test/nested_file.rs') - feed'L' - feed'' - -- screen:snapshot_util() - screen:expect{grid=[[ - {1:impl}{2: Foo { }| - {4:fn} {5:bar}({7:&}{8:self}) { | - {4:if} condition { | - | - | - {4:for} i {4:in} {8:0}..{8:100} { | - | - | - } | - } | - } | - } | - | - {4:^struct} {5:Foo} { | - | - | - ]], attr_ids={ - [1] = {foreground = Screen.colors.Brown, background = Screen.colors.Plum1, bold = true}; - [2] = {background = Screen.colors.Plum1}; - [3] = {foreground = Screen.colors.Cyan4, background = Screen.colors.Plum1}; - [4] = {bold = true, foreground = Screen.colors.Brown}; - [5] = {foreground = Screen.colors.Cyan4}; - [6] = {bold = true, foreground = Screen.colors.Blue1}; - [7] = {bold = true, foreground = Screen.colors.SeaGreen4}; - [8] = {foreground = Screen.colors.Fuchsia}; - }} + describe('language:', function() + before_each(function() + exec_lua[[require'treesitter-context'.setup{ + mode = 'topline', + }]] + cmd'set scrolloff=5' + cmd'set nowrap' + end) + + it('rust', function() + cmd('edit test/test.rs') + feed'20' + + screen:expect{grid=[[ + {1:impl}{2: Foo }| + {2: }{1:fn}{2: }{3:bar}{2:(}{7:&}{10:self}{2:) { }| + {2: }{1:if}{2: condition { }| + {2: }{1:for}{2: i }{1:in}{2: }{10:0}{2:..}{10:100}{2: { }| + | + ^ } | + } | + } | + } | + | + {4:struct} {5:Foo} { | + | + active: {9:bool}, | + | + username: {9:String}, | + | + ]]} + + feed'14' + screen:expect{grid=[[ + {1:struct}{2: }{3:Foo}{2: { }| + | + email: {9:String}, | + | + sign_in_count: {9:u64}, | + ^ | + } | + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + | + ]]} + + end) + + it('c', function() + cmd('edit test/test.c') + feed'' + + -- Check the struct context + screen:expect{grid=[[ + {7:struct}{2: Bert { }| + {8:// comment} | + {9:int} *f2; | + {8:// comment} | + {8:// comment} | + ^ {8:// comment} | + {8:// comment} | + {8:// comment} | + }; | + | + {9:typedef} {9:enum} { | + E1, | + E2, | + E3 | + {8:// comment} | + | + ]]} + + feed'12' + + -- Check the enum context + screen:expect{grid=[[ + {7:typedef}{2: }{7:enum}{2: { }| + E3 | + {8:// comment} | + {8:// comment} | + {8:// comment} | + ^ {8:// comment} | + {8:// comment} | + {8:// comment} | + } Myenum; | + | + {9:int} main({9:int} arg1, | + {9:char} **arg2, | + {9:char} **arg3 | + ) | + { | + | + ]]} + + feed'40' + screen:expect{grid=[[ + {7:int}{2: main(}{7:int}{2: arg1, }{7:char}{2: **arg2}| + {2: }{1:if}{2: (arg1 == }{10:4}{2: && arg2 == arg}| + {2: }{1:for}{2: (}{7:int}{2: i = }{10:0}{2:; i < arg1; }| + {2: }{1:while}{2: (}{10:1}{2:) { }| + } | + ^ | + {4:do} { | + {8:// comment} | + {8:// comment} | + {8:// comment} | + {8:// comment} | + {8:// comment} | + | + } {4:while} ({11:1}); | + {8:// comment} | + | + ]]} + end) + + it('typescript', function() + cmd('edit test/test.ts') + feed'' + + screen:expect{grid=[[ + {1:interface}{2: }{3:User}{2: }{3:{}{2: }| + | + | + | + {5:id}: {9:number}{4:;} | + ^ | + | + | + | + {5:}} | +   | + {4:class} UserAccount {5:{} | + {5:name}: {9:string}; | + {5:id}: {9:number}; | + | + | + ]]} + + feed'21' + screen:expect{grid=[[ + {1:class}{2: UserAccount }{3:{}{2: }| + {2: }{3:constructor}{2:(}{12:name}{2::}{12: }{7:string}{1:,}{12: id}| + {2: }{1:for}{2: (}{3:let}{2: i = }{10:0}{1:;}{2: i < }{10:3}{1:;}{2: i++}| + | + | + ^ | + {5:}} | + | + | + | + | + {5:}} | + {5:}} | + | + | + | + ]]} + + feed'16' + screen:expect{grid=[[ + {1:function}{2: }{3:wrapInArray}{2:(}{12:obj}{2::}{12: }{7:stri}| + {2: }{1:if}{2: (}{3:typeof}{2: obj === }{10:"string"}{2:)}| + | + | + | + ^ | + {5:}} | + {4:return} obj; | + {5:}} | + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + {6:~ }| + | + ]]} + end) - feed'' - screen:expect{grid=[[ - {2: }{1:fn}{2: }{3:bar}{2:(}{7:&}{8:self}{2:) }| - {2: }{1:if}{2: condition { }| - | - | - {4:for} i {4:in} {9:0}..{9:100} { | - | - | - } | - } | - } | - } | - | - {4:^struct} {5:Foo} { | - | - } | - | - ]], attr_ids={ - [1] = {foreground = Screen.colors.Brown, bold = true, background = Screen.colors.LightMagenta}; - [2] = {background = Screen.colors.LightMagenta}; - [3] = {background = Screen.colors.LightMagenta, foreground = Screen.colors.Cyan4}; - [4] = {foreground = Screen.colors.Brown, bold = true}; - [5] = {foreground = Screen.colors.Cyan4}; - [6] = {foreground = Screen.colors.Blue1, bold = true}; - [7] = {foreground = Screen.colors.SeaGreen4, bold = true, background = Screen.colors.LightMagenta}; - [8] = {background = Screen.colors.LightMagenta, foreground = Screen.colors.Magenta1}; - [9] = {foreground = Screen.colors.Magenta1}; - }} - - feed'3' - screen:expect{grid=[[ - {2: }{1:if}{2: condition { }| - {2: }{1:for}{2: i }{1:in}{2: }{7:0}{2:..}{7:100}{2: { }| - | - | - } | - } | - } | - } | - | - {4:^struct} {5:Foo} { | - | - } | - {6:~ }| - {6:~ }| - {6:~ }| - | - ]], attr_ids={ - [1] = {background = Screen.colors.Plum1, bold = true, foreground = Screen.colors.Brown}; - [2] = {background = Screen.colors.Plum1}; - [3] = {background = Screen.colors.Plum1, foreground = Screen.colors.Cyan4}; - [4] = {foreground = Screen.colors.Brown, bold = true}; - [5] = {foreground = Screen.colors.Cyan4}; - [6] = {foreground = Screen.colors.Blue1, bold = true}; - [7] = {background = Screen.colors.Plum1, foreground = Screen.colors.Magenta}; - }} end) + end)