From 1c462cbfc42a4412dd30320fd780704e312ae0cd Mon Sep 17 00:00:00 2001 From: Fredrik Holmberg Date: Sun, 3 Aug 2025 23:48:59 +0200 Subject: [PATCH 1/5] added handling for tabs and moved drawing of extmark to imitatate previous solutiuon --- lua/orgmode/ui/virtual_indent.lua | 156 +++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 13 deletions(-) diff --git a/lua/orgmode/ui/virtual_indent.lua b/lua/orgmode/ui/virtual_indent.lua index c1b29988e..f65ee0406 100644 --- a/lua/orgmode/ui/virtual_indent.lua +++ b/lua/orgmode/ui/virtual_indent.lua @@ -1,4 +1,5 @@ local tree_utils = require('orgmode.utils.treesitter') +local utils = require('orgmode.utils') ---@class OrgVirtualIndent ---@field private _ns_id number extmarks namespace id ---@field private _bufnr integer Buffer VirtualIndent is attached to @@ -89,6 +90,101 @@ function VirtualIndent:_get_indent_size(line, tree_has_errors) return 0 end +---@param line_nr number Current line that wrapping operation is done on. +---@param line_str string Lua-string representing the current line. +---@param indent number Current length of indentation. +---@param wrap_col number width of writable space in buffer, from utils.winwidth +function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, wrap_col) + local function update_exmarks(wrap_arr) + local function set_extmarks(curr_line, pos, nr_spaces) + pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, curr_line, pos, { + virt_text = { { string.rep(' ', nr_spaces), 'OrgIndent' } }, + virt_text_pos = 'inline', + right_gravity = false, + priority = 110, + }) + end + + for _, wrapped_line in ipairs(wrap_arr) do + set_extmarks(line_nr, wrapped_line.pos, wrapped_line.spaces) + end + end + + local temp_wrap_arr = {} + temp_wrap_arr[1] = { + pos = 0, + spaces = indent, + } + + local i = 2 + local wrap_pos = 0 + local last_break = 0 + local break_before_word = 0 + local idx = 1 + local ext_pos = 0 + local nr_spaces = indent + local cumsum_virt_cols = indent + + while idx < #line_str do + local char_start = vim.str_utf_start(line_str, idx) + local charclass = vim.fn.charclass(line_str:sub(idx + char_start, idx)) + local char_is_tab = (line_str:byte(idx) == 9) + + if vim.str_utf_end(line_str, idx) == 0 then + wrap_pos = wrap_pos + 1 + + if char_is_tab then + local shiftw = vim.fn.shiftwidth() + local tab_shift = (shiftw - (((idx + cumsum_virt_cols) - 1) % shiftw)) - 1 + + cumsum_virt_cols = cumsum_virt_cols + tab_shift + + wrap_pos = wrap_pos + tab_shift + if wrap_pos > wrap_col then + nr_spaces = tab_shift + end + end + end + + if wrap_pos >= wrap_col then + local cut_len = idx - break_before_word + + if cut_len >= wrap_col then + ext_pos = idx + nr_spaces = indent + -- Tabs are a big problem. This works as long as the tabs + -- are not on the breakpoint + elseif char_is_tab then + local overflow = wrap_pos - wrap_col + ext_pos = break_before_word + nr_spaces = cut_len + nr_spaces + indent - overflow + cumsum_virt_cols = cumsum_virt_cols - nr_spaces + else + ext_pos = break_before_word + nr_spaces = indent + cut_len + idx = break_before_word + end + + temp_wrap_arr[i] = { + pos = ext_pos, + spaces = nr_spaces, + } + cumsum_virt_cols = cumsum_virt_cols + nr_spaces + i = i + 1 + wrap_pos = 0 + end + + if charclass < 2 then + last_break = idx + end + if charclass > 1 then + break_before_word = last_break + end + idx = idx + 1 + end + return temp_wrap_arr +end + ---@param start_line number start line number to set the indentation, 0-based inclusive ---@param end_line number end line number to set the indentation, 0-based inclusive ---@param ignore_ts? boolean whether or not to skip the treesitter start & end lookup @@ -102,9 +198,9 @@ function VirtualIndent:set_indent(start_line, end_line, ignore_ts) end_line = math.max(parent:end_(), end_line) end end - if start_line > 0 then - start_line = start_line - 1 - end + -- if start_line > 0 then + -- start_line = start_line - 1 + -- end local node_at_cursor = tree_utils.get_node() local tree_has_errors = false @@ -113,17 +209,46 @@ function VirtualIndent:set_indent(start_line, end_line, ignore_ts) end self:_delete_old_extmarks(start_line, end_line) + + -- Put this in as preparation for making it an option. + local indent_longlines = true + local org_lines = {} + local win_width = 0 + if indent_longlines then + org_lines = vim.api.nvim_buf_get_lines(0, start_line, end_line, false) + win_width = utils.winwidth(0) + end + for line = start_line, end_line do local indent = self:_get_indent_size(line, tree_has_errors) if indent > 0 then -- NOTE: `ephemeral = true` is not implemented for `inline` virt_text_pos :( - pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, line, 0, { - virt_text = { { string.rep(' ', indent), 'OrgIndent' } }, - virt_text_pos = 'inline', - right_gravity = false, - priority = 110, - }) + if indent_longlines then + local wrap_col = win_width - indent + local arr_index = (line - start_line) + 1 + + if org_lines[arr_index] then + local wrap_arr = self:_set_wrappoints_of_luastring(line, org_lines[arr_index], indent, wrap_col) + + for _, wrap_point in ipairs(wrap_arr) do + pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, line, wrap_point.pos, { + virt_text = { { string.rep(' ', wrap_point.spaces), 'OrgIndent' } }, + virt_text_pos = 'inline', + right_gravity = false, + priority = 110, + }) + end + end + else + -- old behavior, no indent of longlines. + pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, line, 0, { + virt_text = { { string.rep(' ', indent), 'OrgIndent' } }, + virt_text_pos = 'inline', + right_gravity = false, + priority = 110, + }) + end end end end @@ -133,7 +258,7 @@ function VirtualIndent:attach() if self._attached then return end - self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr) - 1, true) + self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr)) vim.api.nvim_buf_attach(self._bufnr, false, { on_lines = function(_, _, _, start_line, _, end_line) @@ -141,12 +266,17 @@ function VirtualIndent:attach() return true end - vim.schedule(function() + local indent_longlines = true + if indent_longlines then self:set_indent(start_line, end_line) - end) + else + vim.schedule(function() + self:set_indent(start_line, end_line) + end) + end end, on_reload = function() - self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr) - 1, true) + self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr)) end, on_detach = function(_, bufnr) self:detach() From 77950429890a90cfcbf86256b52afae7bee20b73 Mon Sep 17 00:00:00 2001 From: fredrikh Date: Fri, 15 Aug 2025 17:04:41 +0200 Subject: [PATCH 2/5] have changed one line to make todos prepend todolists. Also working on virtual indent --- lua/orgmode/capture/init.lua | 3 ++- lua/orgmode/ui/virtual_indent.lua | 35 +++++++++++++------------------ test.lua | 12 +++++++++++ 3 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 test.lua diff --git a/lua/orgmode/capture/init.lua b/lua/orgmode/capture/init.lua index 85eb8dbac..be24eba8a 100644 --- a/lua/orgmode/capture/init.lua +++ b/lua/orgmode/capture/init.lua @@ -165,7 +165,8 @@ function Capture:_refile_from_capture_buffer(opts) local destination_headline = opts.destination_headline if destination_headline then - target_line = destination_headline:get_range().end_line + -- target_line = destination_headline:get_range().end_line + target_line = destination_headline:get_range().start_line end if opts.template.datetree then diff --git a/lua/orgmode/ui/virtual_indent.lua b/lua/orgmode/ui/virtual_indent.lua index f65ee0406..62cf61dd5 100644 --- a/lua/orgmode/ui/virtual_indent.lua +++ b/lua/orgmode/ui/virtual_indent.lua @@ -95,21 +95,6 @@ end ---@param indent number Current length of indentation. ---@param wrap_col number width of writable space in buffer, from utils.winwidth function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, wrap_col) - local function update_exmarks(wrap_arr) - local function set_extmarks(curr_line, pos, nr_spaces) - pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, curr_line, pos, { - virt_text = { { string.rep(' ', nr_spaces), 'OrgIndent' } }, - virt_text_pos = 'inline', - right_gravity = false, - priority = 110, - }) - end - - for _, wrapped_line in ipairs(wrap_arr) do - set_extmarks(line_nr, wrapped_line.pos, wrapped_line.spaces) - end - end - local temp_wrap_arr = {} temp_wrap_arr[1] = { pos = 0, @@ -124,10 +109,18 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w local ext_pos = 0 local nr_spaces = indent local cumsum_virt_cols = indent + local prefered_wrapwidth = 70 + + if wrap_col < prefered_wrapwidth then + prefered_wrapwidth = wrap_col + end + + local wrap_widthdiff = wrap_col - prefered_wrapwidth while idx < #line_str do local char_start = vim.str_utf_start(line_str, idx) - local charclass = vim.fn.charclass(line_str:sub(idx + char_start, idx)) + local char_end = vim.str_utf_end(line_str, idx) + local charclass = vim.fn.charclass(line_str:sub(idx + char_start, idx + char_end)) local char_is_tab = (line_str:byte(idx) == 9) if vim.str_utf_end(line_str, idx) == 0 then @@ -140,22 +133,22 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w cumsum_virt_cols = cumsum_virt_cols + tab_shift wrap_pos = wrap_pos + tab_shift - if wrap_pos > wrap_col then + if wrap_pos > prefered_wrapwidth then nr_spaces = tab_shift end end end - if wrap_pos >= wrap_col then + if wrap_pos >= prefered_wrapwidth then local cut_len = idx - break_before_word - if cut_len >= wrap_col then + if cut_len >= prefered_wrapwidth then ext_pos = idx nr_spaces = indent -- Tabs are a big problem. This works as long as the tabs -- are not on the breakpoint elseif char_is_tab then - local overflow = wrap_pos - wrap_col + local overflow = wrap_pos - prefered_wrapwidth ext_pos = break_before_word nr_spaces = cut_len + nr_spaces + indent - overflow cumsum_virt_cols = cumsum_virt_cols - nr_spaces @@ -167,7 +160,7 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w temp_wrap_arr[i] = { pos = ext_pos, - spaces = nr_spaces, + spaces = nr_spaces + wrap_widthdiff, } cumsum_virt_cols = cumsum_virt_cols + nr_spaces i = i + 1 diff --git a/test.lua b/test.lua new file mode 100644 index 000000000..bc32dc0d2 --- /dev/null +++ b/test.lua @@ -0,0 +1,12 @@ +a = 'aäi' + +for i = 1, #a, 1 do + local char_start = vim.str_utf_start(a, i) + local char_end = vim.str_utf_end(a, i) + local charclass = vim.fn.charclass(a:sub(i + char_start, i + char_end)) + + print('----') + print(vim.fn.charclass(a:sub(i))) + print(charclass) + print('----') +end From e8ee4e7cfd4db092639f18dc431e9068ca0e60d6 Mon Sep 17 00:00:00 2001 From: fredrikh Date: Fri, 15 Aug 2025 17:34:26 +0200 Subject: [PATCH 3/5] breaking with words with multibyte chars at EOL now works. Also add correct check of charclass when idx looks as startbyte of multichar character --- lua/orgmode/ui/virtual_indent.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/orgmode/ui/virtual_indent.lua b/lua/orgmode/ui/virtual_indent.lua index 62cf61dd5..e5bd978bb 100644 --- a/lua/orgmode/ui/virtual_indent.lua +++ b/lua/orgmode/ui/virtual_indent.lua @@ -140,7 +140,7 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w end if wrap_pos >= prefered_wrapwidth then - local cut_len = idx - break_before_word + local cut_len = vim.api.nvim_strwidth(line_str:sub(break_before_word, idx)) if cut_len >= prefered_wrapwidth then ext_pos = idx @@ -154,7 +154,7 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w cumsum_virt_cols = cumsum_virt_cols - nr_spaces else ext_pos = break_before_word - nr_spaces = indent + cut_len + nr_spaces = indent + cut_len - 1 idx = break_before_word end From 244144c67873542d9c65ef9b02f1e095cb816420 Mon Sep 17 00:00:00 2001 From: Fredrik Holmberg Date: Wed, 24 Dec 2025 14:17:48 +0100 Subject: [PATCH 4/5] debugging error where treesitter cant understand when a headline is joined with paragraph before it --- lua/orgmode/ui/virtual_indent.lua | 36 ++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/lua/orgmode/ui/virtual_indent.lua b/lua/orgmode/ui/virtual_indent.lua index e5bd978bb..ee7d09b17 100644 --- a/lua/orgmode/ui/virtual_indent.lua +++ b/lua/orgmode/ui/virtual_indent.lua @@ -78,7 +78,9 @@ function VirtualIndent:_get_indent_size(line, tree_has_errors) local headline = tree_utils.closest_headline_node({ line + 1, 1 }) + print('HL:', headline) if headline then + print('is headline') local headline_line = headline:start() if headline_line ~= line then @@ -183,22 +185,42 @@ end ---@param ignore_ts? boolean whether or not to skip the treesitter start & end lookup function VirtualIndent:set_indent(start_line, end_line, ignore_ts) ignore_ts = ignore_ts or false + local headline = tree_utils.closest_headline_node({ start_line + 1, 1 }) + print('--------------------------------------------------') + print('headline: ', headline) if headline and not ignore_ts then local parent = headline:parent() + print('parent: ', parent) if parent then start_line = math.min(parent:start(), start_line) end_line = math.max(parent:end_(), end_line) end end - -- if start_line > 0 then - -- start_line = start_line - 1 - -- end + print('startstart: ', start_line) + print(end_line) + print('endend', end_line) + if start_line > 0 then + start_line = start_line - 1 + + -- to handle ts-crash when headline stars are joined with + -- paragraph above. + local err_check = tree_utils.closest_headline_node({ end_line + 1, 1 }) + + local err_at_cursor = (err_check == nil) + + if err_at_cursor then + print('error close') + end + print('errcheck: ', err_check) + end local node_at_cursor = tree_utils.get_node() local tree_has_errors = false if node_at_cursor then tree_has_errors = node_at_cursor:tree():root():has_error() + print('node: ', node_at_cursor) + print('node: ', node_at_cursor:parent()) end self:_delete_old_extmarks(start_line, end_line) @@ -213,7 +235,15 @@ function VirtualIndent:set_indent(start_line, end_line, ignore_ts) end for line = start_line, end_line do + print('-------------- ', line) + print(tree_has_errors) + print('start: ', start_line) + local test = tree_utils.closest_headline_node({ line + 1, 1 }) + print('test ', test) + print('end: ', end_line) local indent = self:_get_indent_size(line, tree_has_errors) + print(indent) + print('ddd') if indent > 0 then -- NOTE: `ephemeral = true` is not implemented for `inline` virt_text_pos :( From d51ae641eb4f596ed546faef87024ce4b13067d2 Mon Sep 17 00:00:00 2001 From: Fredrik Holmberg Date: Wed, 31 Dec 2025 13:30:59 +0100 Subject: [PATCH 5/5] latest state, config init catches nil, no print debugging --- lua/orgmode/config/init.lua | 4 ++ lua/orgmode/ui/virtual_indent.lua | 82 ++++++++++++------------------- 2 files changed, 36 insertions(+), 50 deletions(-) diff --git a/lua/orgmode/config/init.lua b/lua/orgmode/config/init.lua index 0fbebcd89..4727a0d39 100644 --- a/lua/orgmode/config/init.lua +++ b/lua/orgmode/config/init.lua @@ -373,6 +373,7 @@ function Config:setup_ts_predicates() local capture_id = pred[2] local section_node = match[capture_id] section_node = section_node and section_node[#section_node] + if not capture_id or not section_node or section_node:type() ~= 'section' then return end @@ -386,6 +387,9 @@ function Config:setup_ts_predicates() local empty_lines = 0 while end_row > start_row do local line = vim.api.nvim_buf_get_lines(bufnr, end_row - 1, end_row, false)[1] + if not line then + return + end if vim.trim(line) ~= '' then break end diff --git a/lua/orgmode/ui/virtual_indent.lua b/lua/orgmode/ui/virtual_indent.lua index ee7d09b17..a91012351 100644 --- a/lua/orgmode/ui/virtual_indent.lua +++ b/lua/orgmode/ui/virtual_indent.lua @@ -78,9 +78,7 @@ function VirtualIndent:_get_indent_size(line, tree_has_errors) local headline = tree_utils.closest_headline_node({ line + 1, 1 }) - print('HL:', headline) if headline then - print('is headline') local headline_line = headline:start() if headline_line ~= line then @@ -88,15 +86,13 @@ function VirtualIndent:_get_indent_size(line, tree_has_errors) return level + 1 end end - return 0 end ----@param line_nr number Current line that wrapping operation is done on. ---@param line_str string Lua-string representing the current line. ---@param indent number Current length of indentation. ---@param wrap_col number width of writable space in buffer, from utils.winwidth -function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, wrap_col) +function VirtualIndent:_set_wrappoints_of_luastring(line_str, indent, wrap_col, conf_wrapwidth) local temp_wrap_arr = {} temp_wrap_arr[1] = { pos = 0, @@ -111,7 +107,7 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w local ext_pos = 0 local nr_spaces = indent local cumsum_virt_cols = indent - local prefered_wrapwidth = 70 + local prefered_wrapwidth = conf_wrapwidth if wrap_col < prefered_wrapwidth then prefered_wrapwidth = wrap_col @@ -180,53 +176,53 @@ function VirtualIndent:_set_wrappoints_of_luastring(line_nr, line_str, indent, w return temp_wrap_arr end +function VirtualIndent:_indent_and_break_longlines(start_line, line, win_width, org_lines, indent, conf_wrapwidth) + local wrap_col = win_width - indent + local arr_index = (line - start_line) + 1 + + if org_lines[arr_index] then + local wrap_arr = self:_set_wrappoints_of_luastring(org_lines[arr_index], indent, wrap_col, conf_wrapwidth) + + for _, wrap_point in ipairs(wrap_arr) do + pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, line, wrap_point.pos, { + virt_text = { { string.rep(' ', wrap_point.spaces), 'OrgIndent' } }, + virt_text_pos = 'inline', + right_gravity = false, + priority = 110, + }) + end + end +end + ---@param start_line number start line number to set the indentation, 0-based inclusive ---@param end_line number end line number to set the indentation, 0-based inclusive ---@param ignore_ts? boolean whether or not to skip the treesitter start & end lookup function VirtualIndent:set_indent(start_line, end_line, ignore_ts) ignore_ts = ignore_ts or false - local headline = tree_utils.closest_headline_node({ start_line + 1, 1 }) - print('--------------------------------------------------') - print('headline: ', headline) if headline and not ignore_ts then local parent = headline:parent() - print('parent: ', parent) if parent then start_line = math.min(parent:start(), start_line) end_line = math.max(parent:end_(), end_line) end end - print('startstart: ', start_line) - print(end_line) - print('endend', end_line) if start_line > 0 then start_line = start_line - 1 - - -- to handle ts-crash when headline stars are joined with - -- paragraph above. - local err_check = tree_utils.closest_headline_node({ end_line + 1, 1 }) - - local err_at_cursor = (err_check == nil) - - if err_at_cursor then - print('error close') - end - print('errcheck: ', err_check) end local node_at_cursor = tree_utils.get_node() local tree_has_errors = false if node_at_cursor then tree_has_errors = node_at_cursor:tree():root():has_error() - print('node: ', node_at_cursor) - print('node: ', node_at_cursor:parent()) end self:_delete_old_extmarks(start_line, end_line) - -- Put this in as preparation for making it an option. + -- Put this in as preparation for making it an config option. + -- wrapwidth adds virtual linebreaks to make paragraphs prettier. local indent_longlines = true + local conf_wrapwidth = 70 local org_lines = {} local win_width = 0 if indent_longlines then @@ -235,34 +231,12 @@ function VirtualIndent:set_indent(start_line, end_line, ignore_ts) end for line = start_line, end_line do - print('-------------- ', line) - print(tree_has_errors) - print('start: ', start_line) - local test = tree_utils.closest_headline_node({ line + 1, 1 }) - print('test ', test) - print('end: ', end_line) local indent = self:_get_indent_size(line, tree_has_errors) - print(indent) - print('ddd') if indent > 0 then -- NOTE: `ephemeral = true` is not implemented for `inline` virt_text_pos :( if indent_longlines then - local wrap_col = win_width - indent - local arr_index = (line - start_line) + 1 - - if org_lines[arr_index] then - local wrap_arr = self:_set_wrappoints_of_luastring(line, org_lines[arr_index], indent, wrap_col) - - for _, wrap_point in ipairs(wrap_arr) do - pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, line, wrap_point.pos, { - virt_text = { { string.rep(' ', wrap_point.spaces), 'OrgIndent' } }, - virt_text_pos = 'inline', - right_gravity = false, - priority = 110, - }) - end - end + self:_indent_and_break_longlines(start_line, line, win_width, org_lines, indent, conf_wrapwidth) else -- old behavior, no indent of longlines. pcall(vim.api.nvim_buf_set_extmark, self._bufnr, self._ns_id, line, 0, { @@ -283,6 +257,13 @@ function VirtualIndent:attach() end self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr)) + -- vim.api.nvim_create_autocmd({ 'TextChangedI', 'TextChanged' }, { + -- buffer = self._bufnr, + -- callback = function() + -- self:set_indent(0, vim.api.nvim_buf_line_count(self._bufnr)) + -- end, + -- }) + vim.api.nvim_buf_attach(self._bufnr, false, { on_lines = function(_, _, _, start_line, _, end_line) if not self._attached then @@ -291,6 +272,7 @@ function VirtualIndent:attach() local indent_longlines = true if indent_longlines then + indent_longlines = true self:set_indent(start_line, end_line) else vim.schedule(function()