From 103270a24141e565d0d8b6c523caa364cd9ea19f Mon Sep 17 00:00:00 2001 From: Jonathan Apodaca Date: Tue, 17 Jun 2025 23:37:16 -0600 Subject: [PATCH] better extmark inclusivity --- examples/form.lua | 14 +++++--------- lua/u/range.lua | 2 ++ lua/u/renderer.lua | 35 ++++++++++++++++++++++++++++------- spec/renderer_spec.lua | 18 +++++++++++++++++- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/examples/form.lua b/examples/form.lua index bfd55eb..875e128 100644 --- a/examples/form.lua +++ b/examples/form.lua @@ -10,9 +10,6 @@ local Renderer = require('u.renderer').Renderer local h = require('u.renderer').h local tracker = require 'u.tracker' --- Utility to trim brackets from strings: -local function trimb(s) return (s:gsub('^%[(.*)%]$', '%1')) end - -- Create a new, temporary, buffer to the side: vim.cmd.vnew() vim.bo.buftype = 'nofile' @@ -21,13 +18,13 @@ vim.bo.buflisted = false local renderer = Renderer.new() -- Create two signals: -local s_name = tracker.create_signal '[whoever-you-are]' -local s_age = tracker.create_signal '[ideally-a-number]' +local s_name = tracker.create_signal 'whoever-you-are' +local s_age = tracker.create_signal 'ideally-a-number' -- We can create derived information from the signals above. Say we want to do -- some validation on the input for `age`: we can do that with a memo: local s_age_info = tracker.create_memo(function() - local age_raw = trimb(s_age:get()) + local age_raw = s_age:get() local age_digits = age_raw:match '^%s*(%d+)%s*$' local age_n = age_digits and tonumber(age_digits) or nil return { @@ -59,9 +56,8 @@ tracker.create_effect(function() on_change = function(text) s_name:set(text) end, }, name), }, - '\n', { - 'Age: ', + '\nAge: ', h.Structure({ on_change = function(text) s_age:set(text) end, }, age), @@ -72,7 +68,7 @@ tracker.create_effect(function() -- Show the values of the signals here, too, so that we can see the -- reactivity in action. If you change the values in the tags above, you -- can see the changes reflected here immediately. - { 'Hello, "', trimb(name), '"!' }, + { 'Hello, "', name, '"!' }, -- -- A more complex example: we can do much more complex rendering, based on diff --git a/lua/u/range.lua b/lua/u/range.lua index a40312c..04d5eb7 100644 --- a/lua/u/range.lua +++ b/lua/u/range.lua @@ -382,6 +382,8 @@ function Range:save_to_extmark() end_col = 0 end local id = vim.api.nvim_buf_set_extmark(r.start.bufnr, NS, r.start.lnum - 1, r.start.col - 1, { + right_gravity = false, + end_right_gravity = true, end_row = end_row, end_col = end_col, }) diff --git a/lua/u/renderer.lua b/lua/u/renderer.lua index 8aebf77..f1e7b7b 100644 --- a/lua/u/renderer.lua +++ b/lua/u/renderer.lua @@ -28,7 +28,6 @@ M.h = setmetatable({}, { } end, __index = function(_, name) - -- vim.print('dynamic hl ' .. name) return function(attributes, children) return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children) end @@ -324,6 +323,13 @@ function Renderer:_reconcile() -- {{{ id = extmark.id, end_row = extmark.stop[1], end_col = extmark.stop[2], + -- If we change the text starting from the beginning (where the extmark + -- is), we don't want the extmark to move to the right. + right_gravity = false, + -- If we change the text starting from the end (where the end extmark + -- is), we don't want the extmark to move to stay stationary: we want + -- it to move to the right. + end_right_gravity = true, }, extmark.opts) ) end @@ -385,7 +391,7 @@ function Renderer:_on_text_changed() local end_row0, end_col0 = details.end_row, details.end_col if start_row0 == end_row0 and start_col0 == end_col0 then - -- Invalid extmark + on_change '' else local lines = vim.fn.getregion( { self.bufnr, start_row0 + 1, start_col0 + 1 }, @@ -405,9 +411,11 @@ end --- --- @private (private for now) --- @param pos0 [number; number] +--- @param mode string? --- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[] -function Renderer:get_pos_infos(pos0) -- {{{ +function Renderer:get_pos_infos(pos0, mode) -- {{{ local cursor_line0, cursor_col0 = pos0[1], pos0[2] + if not mode then mode = vim.api.nvim_get_mode().mode end -- The cursor (block) occupies **two** extmark spaces: one for it's left -- edge, and one for it's right. We need to do our own intersection test, @@ -437,10 +445,23 @@ function Renderer:get_pos_infos(pos0) -- {{{ --- @param ext u.renderer.RendererExtmark :filter(function(ext) if ext.stop[1] ~= nil and ext.stop[2] ~= nil then - return cursor_line0 >= ext.start[1] - and cursor_col0 >= ext.start[2] - and cursor_line0 <= ext.stop[1] - and cursor_col0 < ext.stop[2] + -- If we've "ciw" and "collapsed" an extmark onto the cursor, + -- the cursor pos will equal the exmark's start AND end. In this + -- case, we want to include the extmark. + return ( + cursor_line0 == ext.start[1] + and cursor_col0 == ext.start[2] + and cursor_line0 == ext.stop[1] + and cursor_col0 == ext.stop[2] + ) + or ( + cursor_line0 >= ext.start[1] + and cursor_col0 >= ext.start[2] + and cursor_line0 <= ext.stop[1] + -- In insert mode, the cursor is "thin", so <= to compensate: + -- In normal mode, the cursor is "wide", so < to compensate: + and (mode == 'i' and cursor_col0 <= ext.stop[2] or cursor_col0 < ext.stop[2]) + ) else return true end diff --git a/spec/renderer_spec.lua b/spec/renderer_spec.lua index 8e38a2d..dee5da6 100644 --- a/spec/renderer_spec.lua +++ b/spec/renderer_spec.lua @@ -103,11 +103,27 @@ describe('Renderer', function() } 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 }) + pos_infos = r:get_pos_infos { 0, 4 } + 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 }) + + pos_infos = r:get_pos_infos { 0, 5 } + assert.are.same(#pos_infos, 1) + assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2') + assert.are.same(pos_infos[1].extmark.start, { 0, 5 }) + assert.are.same(pos_infos[1].extmark.stop, { 0, 11 }) + + -- In insert mode, bounds are eagerly included: + pos_infos = r:get_pos_infos({ 0, 5 }, 'i') + assert.are.same(#pos_infos, 2) + assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1') + assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup2') end) end)