cleanup and add more tests
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				NeoVim tests / plenary-tests (push) Successful in 13s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	NeoVim tests / plenary-tests (push) Successful in 13s
				
			This commit is contained in:
		
							parent
							
								
									d941cbb2b7
								
							
						
					
					
						commit
						b458587c67
					
				| @ -1,7 +1,7 @@ | ||||
| local M = {} | ||||
| 
 | ||||
| --- @params name string | ||||
| function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'my.log', name .. '.log.jsonl') end | ||||
| function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log', name .. '.log.jsonl') end | ||||
| 
 | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Logger class | ||||
| @ -49,7 +49,15 @@ function M.setup() | ||||
|     local log_file_path = M.file_for_name(args.fargs[1]) | ||||
|     vim.fn.mkdir(vim.fs.dirname(log_file_path), 'p') | ||||
|     vim.system({ 'touch', log_file_path }):wait() | ||||
|     vim.cmd.Term('tail -f "' .. log_file_path .. '"') | ||||
| 
 | ||||
|     vim.cmd.new() | ||||
| 
 | ||||
|     local winnr = vim.api.nvim_get_current_win() | ||||
|     vim.wo[winnr][0].number = false | ||||
|     vim.wo[winnr][0].relativenumber = false | ||||
| 
 | ||||
|     vim.cmd.terminal('tail -f "' .. log_file_path .. '"') | ||||
|     vim.cmd.startinsert() | ||||
|   end, { nargs = '*' }) | ||||
| end | ||||
| 
 | ||||
|  | ||||
| @ -1,11 +1,9 @@ | ||||
| local utils = require 'u.utils' | ||||
| 
 | ||||
| local M = {} | ||||
| local H = {} | ||||
| 
 | ||||
| --- @alias Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: Tree } | ||||
| --- @alias Node nil | boolean | string | Tag | ||||
| --- @alias Tree Node | Node[] | ||||
| local TagMetaTable = {} | ||||
| 
 | ||||
| --- @param name string | ||||
| --- @param attributes? table<string, any> | ||||
| @ -20,9 +18,7 @@ function M.h(name, attributes, children) | ||||
|   } | ||||
| end | ||||
| 
 | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Renderer class | ||||
| -------------------------------------------------------------------------------- | ||||
| -- Renderer {{{ | ||||
| --- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any } | ||||
| 
 | ||||
| --- @class Renderer | ||||
| @ -35,10 +31,12 @@ local Renderer = {} | ||||
| Renderer.__index = Renderer | ||||
| M.Renderer = Renderer | ||||
| 
 | ||||
| --- @private | ||||
| --- @param x any | ||||
| --- @return boolean | ||||
| function Renderer.is_tag(x) return type(x) == 'table' and x.kind == 'tag' end | ||||
| 
 | ||||
| --- @private | ||||
| --- @param x any | ||||
| --- @return boolean | ||||
| function Renderer.is_tag_arr(x) | ||||
| @ -46,7 +44,7 @@ function Renderer.is_tag_arr(x) | ||||
|   return #x == 0 or not Renderer.is_tag(x) | ||||
| end | ||||
| --- @param bufnr number|nil | ||||
| function Renderer.new(bufnr) | ||||
| function Renderer.new(bufnr) -- {{{ | ||||
|   if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end | ||||
| 
 | ||||
|   if vim.b[bufnr]._renderer_ns == nil then | ||||
| @ -61,13 +59,13 @@ function Renderer.new(bufnr) | ||||
|     curr = { lines = {}, extmarks = {} }, | ||||
|   }, Renderer) | ||||
|   return self | ||||
| end | ||||
| end -- }}} | ||||
| 
 | ||||
| --- @param opts { | ||||
| ---   tree: Tree; | ||||
| ---   on_tag?: fun(tag: Tag, start0: [number, number], stop0: [number, number]): any; | ||||
| --- } | ||||
| function Renderer.markup_to_lines(opts) | ||||
| function Renderer.markup_to_lines(opts) -- {{{ | ||||
|   --- @type string[] | ||||
|   local lines = {} | ||||
| 
 | ||||
| @ -85,7 +83,7 @@ function Renderer.markup_to_lines(opts) | ||||
|   end | ||||
| 
 | ||||
|   --- @param node Node | ||||
|   local function visit(node) | ||||
|   local function visit(node) -- {{{ | ||||
|     if node == nil or type(node) == 'boolean' then return end | ||||
| 
 | ||||
|     if type(node) == 'string' then | ||||
| @ -99,7 +97,9 @@ function Renderer.markup_to_lines(opts) | ||||
| 
 | ||||
|       -- visit the children: | ||||
|       if Renderer.is_tag_arr(node.children) then | ||||
|         for _, child in ipairs(node.children) do | ||||
|         for _, child in | ||||
|           ipairs(node.children --[[@as Node[] ]]) | ||||
|         do | ||||
|           -- newlines are not controlled by array entries, do NOT output a line here: | ||||
|           visit(child) | ||||
|         end | ||||
| @ -115,11 +115,11 @@ function Renderer.markup_to_lines(opts) | ||||
|         visit(child) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|   end -- }}} | ||||
|   visit(opts.tree) | ||||
| 
 | ||||
|   return lines | ||||
| end | ||||
| end -- }}} | ||||
| 
 | ||||
| --- @param opts { | ||||
| ---   tree: string; | ||||
| @ -128,7 +128,7 @@ end | ||||
| function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end | ||||
| 
 | ||||
| --- @param tree Tree | ||||
| function Renderer:render(tree) | ||||
| function Renderer:render(tree) -- {{{ | ||||
|   local changedtick = vim.b[self.bufnr].changedtick | ||||
|   if changedtick ~= self.changedtick then | ||||
|     self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) } | ||||
| @ -142,7 +142,7 @@ function Renderer:render(tree) | ||||
|   local lines = Renderer.markup_to_lines { | ||||
|     tree = tree, | ||||
| 
 | ||||
|     on_tag = function(tag, start0, stop0) | ||||
|     on_tag = function(tag, start0, stop0) -- {{{ | ||||
|       if tag.name == 'text' then | ||||
|         local hl = tag.attributes.hl | ||||
|         if type(hl) == 'string' then | ||||
| @ -161,7 +161,7 @@ function Renderer:render(tree) | ||||
|             vim.keymap.set( | ||||
|               'n', | ||||
|               lhs, | ||||
|               function() return self:_on_expr_map('n', lhs) end, | ||||
|               function() return self:_expr_map_callback('n', lhs) end, | ||||
|               { buffer = self.bufnr, expr = true, replace_keycodes = true } | ||||
|             ) | ||||
|           end | ||||
| @ -176,79 +176,64 @@ function Renderer:render(tree) | ||||
|           }) | ||||
|         end | ||||
|       end | ||||
|     end, | ||||
|     end, -- }}} | ||||
|   } | ||||
| 
 | ||||
|   self.old = self.curr | ||||
|   self.curr = { lines = lines, extmarks = extmarks } | ||||
|   self:_reconcile() | ||||
| end | ||||
| end -- }}} | ||||
| 
 | ||||
| --- @private | ||||
| --- @param info string | ||||
| --- @param start integer | ||||
| --- @param end_ integer | ||||
| --- @param strict_indexing boolean | ||||
| --- @param replacement string[] | ||||
| function Renderer:_set_lines(info, start, end_, strict_indexing, replacement) | ||||
|   self:_log { 'set_lines', self.bufnr, start, end_, strict_indexing, replacement } | ||||
| function Renderer:_set_lines(start, end_, strict_indexing, replacement) | ||||
|   vim.api.nvim_buf_set_lines(self.bufnr, start, end_, strict_indexing, replacement) | ||||
|   self:_log { 'after(' .. info .. ')', vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) } | ||||
| end | ||||
| 
 | ||||
| --- @private | ||||
| --- @param info string | ||||
| --- @param start_row integer | ||||
| --- @param start_col integer | ||||
| --- @param end_row integer | ||||
| --- @param end_col integer | ||||
| --- @param replacement string[] | ||||
| function Renderer:_set_text(info, start_row, start_col, end_row, end_col, replacement) | ||||
|   self:_log { 'set_text', self.bufnr, start_row, start_col, end_row, end_col, replacement } | ||||
| function Renderer:_set_text(start_row, start_col, end_row, end_col, replacement) | ||||
|   vim.api.nvim_buf_set_text(self.bufnr, start_row, start_col, end_row, end_col, replacement) | ||||
|   self:_log { 'after(' .. info .. ')', vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) } | ||||
| end | ||||
| 
 | ||||
| --- @private | ||||
| function Renderer:_log(...) | ||||
|   -- | ||||
|   -- vim.print(...) | ||||
| end | ||||
| 
 | ||||
| --- @private | ||||
| function Renderer:_reconcile() | ||||
|   local line_changes = utils.levenshtein(self.old.lines, self.curr.lines) | ||||
| function Renderer:_reconcile() -- {{{ | ||||
|   local line_changes = H.levenshtein(self.old.lines, self.curr.lines) | ||||
|   self.old = self.curr | ||||
| 
 | ||||
|   -- | ||||
|   -- Step 1: morph the text to the desired state: | ||||
|   -- | ||||
|   self:_log { line_changes = line_changes } | ||||
|   for _, line_change in ipairs(line_changes) do | ||||
|     local lnum0 = line_change.index - 1 | ||||
| 
 | ||||
|     if line_change.kind == 'add' then | ||||
|       self:_set_lines('add-line', lnum0, lnum0, true, { line_change.item }) | ||||
|       self:_set_lines(lnum0, lnum0, true, { line_change.item }) | ||||
|     elseif line_change.kind == 'change' then | ||||
|       -- Compute inter-line diff, and apply: | ||||
|       self:_log '--------------------------------------------------------------------------------' | ||||
|       local col_changes = utils.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, '')) | ||||
|       local col_changes = H.levenshtein(vim.split(line_change.from, ''), vim.split(line_change.to, '')) | ||||
| 
 | ||||
|       for _, col_change in ipairs(col_changes) do | ||||
|         local cnum0 = col_change.index - 1 | ||||
|         self:_log { line_change = col_change, cnum = cnum0, lnum = lnum0 } | ||||
|         if col_change.kind == 'add' then | ||||
|           self:_set_text('add-char', lnum0, cnum0, lnum0, cnum0, { col_change.item }) | ||||
|           self:_set_text(lnum0, cnum0, lnum0, cnum0, { col_change.item }) | ||||
|         elseif col_change.kind == 'change' then | ||||
|           self:_set_text('change-char', lnum0, cnum0, lnum0, cnum0 + 1, { col_change.to }) | ||||
|           self:_set_text(lnum0, cnum0, lnum0, cnum0 + 1, { col_change.to }) | ||||
|         elseif col_change.kind == 'delete' then | ||||
|           self:_set_text('del-char', lnum0, cnum0, lnum0, cnum0 + 1, {}) | ||||
|           self:_set_text(lnum0, cnum0, lnum0, cnum0 + 1, {}) | ||||
|         else | ||||
|           -- No change | ||||
|         end | ||||
|       end | ||||
|     elseif line_change.kind == 'delete' then | ||||
|       self:_set_lines('del-line', lnum0, lnum0 + 1, true, {}) | ||||
|       self:_set_lines(lnum0, lnum0 + 1, true, {}) | ||||
|     else | ||||
|       -- No change | ||||
|     end | ||||
| @ -274,12 +259,12 @@ function Renderer:_reconcile() | ||||
|       }, extmark.opts) | ||||
|     ) | ||||
|   end | ||||
| end | ||||
| end -- }}} | ||||
| 
 | ||||
| --- @private | ||||
| --- @param mode string | ||||
| --- @param lhs string | ||||
| function Renderer:_on_expr_map(mode, lhs) | ||||
| function Renderer:_expr_map_callback(mode, lhs) -- {{{ | ||||
|   -- find the tag with the smallest intersection that contains the cursor: | ||||
|   local pos0 = vim.api.nvim_win_get_cursor(0) | ||||
|   pos0[1] = pos0[1] - 1 -- make it actually 0-based | ||||
| @ -308,7 +293,7 @@ function Renderer:_on_expr_map(mode, lhs) | ||||
| 
 | ||||
|   -- Resort to default behavior: | ||||
|   return cancel and '' or lhs | ||||
| end | ||||
| end -- }}} | ||||
| 
 | ||||
| --- Returns pairs of extmarks and tags associate with said extmarks. The | ||||
| --- returned tags/extmarks are sorted smallest (innermost) to largest | ||||
| @ -317,7 +302,7 @@ end | ||||
| --- @private (private for now) | ||||
| --- @param pos0 [number; number] | ||||
| --- @return { extmark: RendererExtmark; tag: Tag; }[] | ||||
| function Renderer:get_pos_infos(pos0) | ||||
| function Renderer:get_pos_infos(pos0) -- {{{ | ||||
|   local cursor_line0, cursor_col0 = pos0[1], pos0[2] | ||||
| 
 | ||||
|   -- The cursor (block) occupies **two** extmark spaces: one for it's left | ||||
| @ -388,12 +373,10 @@ function Renderer:get_pos_infos(pos0) | ||||
|     :totable() | ||||
| 
 | ||||
|   return matching_tags | ||||
| end | ||||
| 
 | ||||
| -------------------------------------------------------------------------------- | ||||
| -- TreeBuilder class | ||||
| -------------------------------------------------------------------------------- | ||||
| end -- }}} | ||||
| -- }}} | ||||
| 
 | ||||
| -- TreeBuilder {{{ | ||||
| --- @class TreeBuilder | ||||
| --- @field private nodes Node[] | ||||
| local TreeBuilder = {} | ||||
| @ -433,5 +416,108 @@ end | ||||
| 
 | ||||
| --- @return Tree | ||||
| function TreeBuilder:tree() return self.nodes end | ||||
| -- }}} | ||||
| 
 | ||||
| -- Levenshtein utility {{{ | ||||
| --- @alias LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; }) | ||||
| --- @private | ||||
| --- @generic T | ||||
| --- @param x `T`[] | ||||
| --- @param y T[] | ||||
| --- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; } | ||||
| --- @return LevenshteinChange<T>[] | ||||
| function H.levenshtein(x, y, cost) | ||||
|   -- At the moment, this whole `cost` plumbing is not used. Deletes have the | ||||
|   -- same cost as Adds or Changes. I can imagine a future, however, where | ||||
|   -- fudging with the costs of operations produces a more optimized change-set | ||||
|   -- that is tailored to working better with how NeoVim manipulates text. I've | ||||
|   -- done no further investigation in this area, however, so it's impossible to | ||||
|   -- tell if such tuning would produce real benefit. For now, I'm leaving this | ||||
|   -- in here even though it's not actively used. Hopefully having this | ||||
|   -- callback-based plumbing does not cause too much of a performance hit to | ||||
|   -- the renderer. | ||||
|   cost = cost or {} | ||||
|   local cost_of_delete_f = cost.of_delete or function() return 1 end | ||||
|   local cost_of_add_f = cost.of_add or function() return 1 end | ||||
|   local cost_of_change_f = cost.of_change or function() return 1 end | ||||
| 
 | ||||
|   local m, n = #x, #y | ||||
|   -- Initialize the distance matrix | ||||
|   local dp = {} | ||||
|   for i = 0, m do | ||||
|     dp[i] = {} | ||||
|     for j = 0, n do | ||||
|       dp[i][j] = 0 | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   -- Fill the base cases | ||||
|   for i = 0, m do | ||||
|     dp[i][0] = i | ||||
|   end | ||||
|   for j = 0, n do | ||||
|     dp[0][j] = j | ||||
|   end | ||||
| 
 | ||||
|   -- Compute the Levenshtein distance dynamically | ||||
|   for i = 1, m do | ||||
|     for j = 1, n do | ||||
|       if x[i] == y[j] then | ||||
|         dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same | ||||
|       else | ||||
|         local costDelete = dp[i - 1][j] + cost_of_delete_f(x[i]) | ||||
|         local costAdd = dp[i][j - 1] + cost_of_add_f(y[j]) | ||||
|         local costChange = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j]) | ||||
|         dp[i][j] = math.min(costDelete, costAdd, costChange) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   -- Backtrack to find the changes | ||||
|   local i = m | ||||
|   local j = n | ||||
|   --- @type LevenshteinChange[] | ||||
|   local changes = {} | ||||
| 
 | ||||
|   while i > 0 or j > 0 do | ||||
|     local default_cost = dp[i][j] | ||||
|     local cost_of_change = (i > 0 and j > 0) and dp[i - 1][j - 1] or default_cost | ||||
|     local cost_of_add = j > 0 and dp[i][j - 1] or default_cost | ||||
|     local cost_of_delete = i > 0 and dp[i - 1][j] or default_cost | ||||
| 
 | ||||
|     --- @param u number | ||||
|     --- @param v number | ||||
|     --- @param w number | ||||
|     local function is_first_min(u, v, w) return u <= v and u <= w end | ||||
| 
 | ||||
|     if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then | ||||
|       -- potential change | ||||
|       if x[i] ~= y[j] then | ||||
|         --- @type LevenshteinChange | ||||
|         local change = { kind = 'change', from = x[i], index = i, to = y[j] } | ||||
|         table.insert(changes, change) | ||||
|       end | ||||
|       i = i - 1 | ||||
|       j = j - 1 | ||||
|     elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then | ||||
|       -- addition | ||||
|       --- @type LevenshteinChange | ||||
|       local change = { kind = 'add', item = y[j], index = i + 1 } | ||||
|       table.insert(changes, change) | ||||
|       j = j - 1 | ||||
|     elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then | ||||
|       -- deletion | ||||
|       --- @type LevenshteinChange | ||||
|       local change = { kind = 'delete', item = x[i], index = i } | ||||
|       table.insert(changes, change) | ||||
|       i = i - 1 | ||||
|     else | ||||
|       error 'unreachable' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   return changes | ||||
| end | ||||
| -- }}} | ||||
| 
 | ||||
| return M | ||||
|  | ||||
| @ -121,95 +121,4 @@ end | ||||
| 
 | ||||
| function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end | ||||
| 
 | ||||
| --- @alias LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; }) | ||||
| --- @private | ||||
| --- @generic T | ||||
| --- @param x `T`[] | ||||
| --- @param y T[] | ||||
| --- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; } | ||||
| --- @return LevenshteinChange<T>[] | ||||
| function M.levenshtein(x, y, cost) | ||||
|   cost = cost or {} | ||||
|   local cost_of_delete_f = cost.of_delete or function() return 1 end | ||||
|   local cost_of_add_f = cost.of_add or function() return 1 end | ||||
|   local cost_of_change_f = cost.of_change or function() return 1 end | ||||
| 
 | ||||
|   local m, n = #x, #y | ||||
|   -- Initialize the distance matrix | ||||
|   local dp = {} | ||||
|   for i = 0, m do | ||||
|     dp[i] = {} | ||||
|     for j = 0, n do | ||||
|       dp[i][j] = 0 | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   -- Fill the base cases | ||||
|   for i = 0, m do | ||||
|     dp[i][0] = i | ||||
|   end | ||||
|   for j = 0, n do | ||||
|     dp[0][j] = j | ||||
|   end | ||||
| 
 | ||||
|   -- Compute the Levenshtein distance dynamically | ||||
|   for i = 1, m do | ||||
|     for j = 1, n do | ||||
|       if x[i] == y[j] then | ||||
|         dp[i][j] = dp[i - 1][j - 1] -- no cost if items are the same | ||||
|       else | ||||
|         local costDelete = dp[i - 1][j] + cost_of_delete_f(x[i]) | ||||
|         local costAdd = dp[i][j - 1] + cost_of_add_f(y[j]) | ||||
|         local costChange = dp[i - 1][j - 1] + cost_of_change_f(x[i], y[j]) | ||||
|         dp[i][j] = math.min(costDelete, costAdd, costChange) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   -- Backtrack to find the changes | ||||
|   local i = m | ||||
|   local j = n | ||||
|   --- @type LevenshteinChange[] | ||||
|   local changes = {} | ||||
| 
 | ||||
|   while i > 0 or j > 0 do | ||||
|     local default_cost = dp[i][j] | ||||
|     local cost_of_change = (i > 0 and j > 0) and dp[i - 1][j - 1] or default_cost | ||||
|     local cost_of_add = j > 0 and dp[i][j - 1] or default_cost | ||||
|     local cost_of_delete = i > 0 and dp[i - 1][j] or default_cost | ||||
| 
 | ||||
|     --- @param u number | ||||
|     --- @param v number | ||||
|     --- @param w number | ||||
|     local function is_first_min(u, v, w) return u <= v and u <= w end | ||||
| 
 | ||||
|     if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then | ||||
|       -- potential change | ||||
|       if x[i] ~= y[j] then | ||||
|         --- @type LevenshteinChange | ||||
|         local change = { kind = 'change', from = x[i], index = i, to = y[j] } | ||||
|         table.insert(changes, change) | ||||
|       end | ||||
|       i = i - 1 | ||||
|       j = j - 1 | ||||
|     elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then | ||||
|       -- addition | ||||
|       --- @type LevenshteinChange | ||||
|       local change = { kind = 'add', item = y[j], index = i + 1 } | ||||
|       table.insert(changes, change) | ||||
|       j = j - 1 | ||||
|     elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then | ||||
|       -- deletion | ||||
|       --- @type LevenshteinChange | ||||
|       local change = { kind = 'delete', item = x[i], index = i } | ||||
|       table.insert(changes, change) | ||||
|       i = i - 1 | ||||
|     else | ||||
|       error 'unreachable' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   return changes | ||||
| end | ||||
| 
 | ||||
| return M | ||||
|  | ||||
							
								
								
									
										131
									
								
								spec/renderer_spec.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								spec/renderer_spec.lua
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,131 @@ | ||||
| local R = require 'u.renderer' | ||||
| local withbuf = loadfile './spec/withbuf.lua'() | ||||
| 
 | ||||
| local function getlines() return vim.api.nvim_buf_get_lines(0, 0, -1, true) end | ||||
| 
 | ||||
| describe('Renderer', function() | ||||
|   it('should render text in an empty buffer', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { 'hello', ' ', 'world' } | ||||
|       assert.are.same(getlines(), { 'hello world' }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should result in the correct text after repeated renders', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { 'hello', ' ', 'world' } | ||||
|       assert.are.same(getlines(), { 'hello world' }) | ||||
| 
 | ||||
|       r:render { 'goodbye', ' ', 'world' } | ||||
|       assert.are.same(getlines(), { 'goodbye world' }) | ||||
| 
 | ||||
|       r:render { 'hello', ' ', 'universe' } | ||||
|       assert.are.same(getlines(), { 'hello universe' }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should handle tags correctly', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { | ||||
|         R.h('text', { hl = 'HighlightGroup' }, 'hello '), | ||||
|         R.h('text', { hl = 'HighlightGroup' }, 'world'), | ||||
|       } | ||||
|       assert.are.same(getlines(), { 'hello world' }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should reconcile added lines', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { 'line 1', '\n', 'line 2' } | ||||
|       assert.are.same(getlines(), { 'line 1', 'line 2' }) | ||||
| 
 | ||||
|       -- Add a new line: | ||||
|       r:render { 'line 1', '\n', 'line 2\n', 'line 3' } | ||||
|       assert.are.same(getlines(), { 'line 1', 'line 2', 'line 3' }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should reconcile deleted lines', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { 'line 1', '\nline 2', '\nline 3' } | ||||
|       assert.are.same(getlines(), { 'line 1', 'line 2', 'line 3' }) | ||||
| 
 | ||||
|       -- Remove a line: | ||||
|       r:render { 'line 1', '\nline 3' } | ||||
|       assert.are.same(getlines(), { 'line 1', 'line 3' }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should handle multiple nested elements', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { | ||||
|         R.h('text', {}, { | ||||
|           'first line', | ||||
|         }), | ||||
|         '\n', | ||||
|         R.h('text', {}, 'second line'), | ||||
|       } | ||||
|       assert.are.same(getlines(), { 'first line', 'second line' }) | ||||
| 
 | ||||
|       r:render { | ||||
|         R.h('text', {}, 'updated first line'), | ||||
|         '\n', | ||||
|         R.h('text', {}, 'third line'), | ||||
|       } | ||||
|       assert.are.same(getlines(), { 'updated first line', 'third line' }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   -- | ||||
|   -- get_pos_infos | ||||
|   -- | ||||
| 
 | ||||
|   it('should return no extmarks for an empty buffer', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       local pos_infos = r:get_pos_infos { 0, 0 } | ||||
|       assert.are.same(pos_infos, {}) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should return correct extmark for a given position', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { | ||||
|         R.h('text', { hl = 'HighlightGroup1' }, 'Hello'), | ||||
|         R.h('text', { hl = 'HighlightGroup2' }, ' World'), | ||||
|       } | ||||
| 
 | ||||
|       local pos_infos = r:get_pos_infos { 0, 2 } | ||||
| 
 | ||||
|       assert.are.same(#pos_infos, 1) | ||||
|       assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1') | ||||
|       assert.are.same(pos_infos[1].extmark.start, { 0, 0 }) | ||||
|       assert.are.same(pos_infos[1].extmark.stop, { 0, 5 }) | ||||
|     end) | ||||
|   end) | ||||
| 
 | ||||
|   it('should return multiple extmarks for overlapping text', function() | ||||
|     withbuf({}, function() | ||||
|       local r = R.Renderer.new(0) | ||||
|       r:render { | ||||
|         R.h('text', { hl = 'HighlightGroup1' }, { | ||||
|           'Hello', | ||||
|           R.h('text', { hl = 'HighlightGroup2', extmark = { hl_group = 'HighlightGroup2' } }, ' World'), | ||||
|         }), | ||||
|       } | ||||
| 
 | ||||
|       local pos_infos = r:get_pos_infos { 0, 5 } | ||||
| 
 | ||||
|       assert.are.same(#pos_infos, 2) | ||||
|       assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2') | ||||
|       assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1') | ||||
|     end) | ||||
|   end) | ||||
| end) | ||||
| @ -1,70 +0,0 @@ | ||||
| local utils = require 'u.utils' | ||||
| 
 | ||||
| --- @param s string | ||||
| local function split(s) return vim.split(s, '') end | ||||
| 
 | ||||
| --- @param original string | ||||
| --- @param changes LevenshteinChange[] | ||||
| local function morph(original, changes) | ||||
|   local t = split(original) | ||||
|   for _, change in ipairs(changes) do | ||||
|     if change.kind == 'add' then | ||||
|       table.insert(t, change.index, change.item) | ||||
|     elseif change.kind == 'delete' then | ||||
|       table.remove(t, change.index) | ||||
|     elseif change.kind == 'change' then | ||||
|       t[change.index] = change.to | ||||
|     end | ||||
|   end | ||||
|   return vim.iter(t):join '' | ||||
| end | ||||
| 
 | ||||
| describe('utils', function() | ||||
|   it('levenshtein', function() | ||||
|     local original = 'abc' | ||||
|     local result = 'absece' | ||||
|     local changes = utils.levenshtein(split(original), split(result)) | ||||
|     assert.are.same(changes, { | ||||
|       { | ||||
|         item = 'e', | ||||
|         kind = 'add', | ||||
|         index = 4, | ||||
|       }, | ||||
|       { | ||||
|         item = 'e', | ||||
|         kind = 'add', | ||||
|         index = 3, | ||||
|       }, | ||||
|       { | ||||
|         item = 's', | ||||
|         kind = 'add', | ||||
|         index = 3, | ||||
|       }, | ||||
|     }) | ||||
|     assert.are.same(morph(original, changes), result) | ||||
| 
 | ||||
|     original = 'jonathan' | ||||
|     result = 'ajoanthan' | ||||
|     changes = utils.levenshtein(split(original), split(result)) | ||||
|     assert.are.same(changes, { | ||||
|       { | ||||
|         from = 'a', | ||||
|         index = 4, | ||||
|         kind = 'change', | ||||
|         to = 'n', | ||||
|       }, | ||||
|       { | ||||
|         from = 'n', | ||||
|         index = 3, | ||||
|         kind = 'change', | ||||
|         to = 'a', | ||||
|       }, | ||||
|       { | ||||
|         index = 1, | ||||
|         item = 'a', | ||||
|         kind = 'add', | ||||
|       }, | ||||
|     }) | ||||
|     assert.are.same(morph(original, changes), result) | ||||
|   end) | ||||
| end) | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user