From b346e47738495947ca0aa06282f66e27f0110b71 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 6 Dec 2023 08:58:10 -0500 Subject: [PATCH 01/24] Virtual lines begins. --- data/core/doc/init.lua | 326 +-------------------------------------- data/core/docview.lua | 335 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+), 324 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index e44610e58..6f6fb272f 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -21,6 +21,7 @@ end function Doc:new(filename, abs_filename, new_file) self.new_file = new_file + self.listeners = {} self:reset() if filename then self:set_filename(filename, abs_filename) @@ -142,138 +143,6 @@ function Doc:get_change_id() return self.undo_stack.idx end -local function sort_positions(line1, col1, line2, col2) - if line1 > line2 or line1 == line2 and col1 > col2 then - return line2, col2, line1, col1, true - end - return line1, col1, line2, col2, false -end - --- Cursor section. Cursor indices are *only* valid during a get_selections() call. --- Cursors will always be iterated in order from top to bottom. Through normal operation --- curors can never swap positions; only merge or split, or change their position in cursor --- order. -function Doc:get_selection(sort) - local line1, col1, line2, col2, swap = self:get_selection_idx(self.last_selection, sort) - if not line1 then - line1, col1, line2, col2, swap = self:get_selection_idx(1, sort) - end - return line1, col1, line2, col2, swap -end - ----Get the selection specified by `idx` ----@param idx integer @the index of the selection to retrieve ----@param sort? boolean @whether to sort the selection returned ----@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted -function Doc:get_selection_idx(idx, sort) - local line1, col1, line2, col2 = self.selections[idx * 4 - 3], self.selections[idx * 4 - 2], - self.selections[idx * 4 - 1], - self.selections[idx * 4] - if line1 and sort then - return sort_positions(line1, col1, line2, col2) - else - return line1, col1, line2, col2 - end -end - -function Doc:get_selection_text(limit) - limit = limit or math.huge - local result = {} - for idx, line1, col1, line2, col2 in self:get_selections() do - if idx > limit then break end - if line1 ~= line2 or col1 ~= col2 then - local text = self:get_text(line1, col1, line2, col2) - if text ~= "" then result[#result + 1] = text end - end - end - return table.concat(result, "\n") -end - -function Doc:has_selection() - local line1, col1, line2, col2 = self:get_selection(false) - return line1 ~= line2 or col1 ~= col2 -end - -function Doc:has_any_selection() - for idx, line1, col1, line2, col2 in self:get_selections() do - if line1 ~= line2 or col1 ~= col2 then return true end - end - return false -end - -function Doc:sanitize_selection() - for idx, line1, col1, line2, col2 in self:get_selections() do - self:set_selections(idx, line1, col1, line2, col2) - end -end - -function Doc:set_selections(idx, line1, col1, line2, col2, swap, rm) - assert(not line2 == not col2, "expected 3 or 5 arguments") - if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end - line1, col1 = self:sanitize_position(line1, col1) - line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) - common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) -end - -function Doc:add_selection(line1, col1, line2, col2, swap) - local l1, c1 = sort_positions(line1, col1, line2 or line1, col2 or col1) - local target = #self.selections / 4 + 1 - for idx, tl1, tc1 in self:get_selections(true) do - if l1 < tl1 or l1 == tl1 and c1 < tc1 then - target = idx - break - end - end - self:set_selections(target, line1, col1, line2, col2, swap, 0) - self.last_selection = target -end - -function Doc:remove_selection(idx) - if self.last_selection >= idx then - self.last_selection = self.last_selection - 1 - end - common.splice(self.selections, (idx - 1) * 4 + 1, 4) -end - -function Doc:set_selection(line1, col1, line2, col2, swap) - self.selections = {} - self:set_selections(1, line1, col1, line2, col2, swap) - self.last_selection = 1 -end - -function Doc:merge_cursors(idx) - for i = (idx or (#self.selections - 3)), (idx or 5), -4 do - for j = 1, i - 4, 4 do - if self.selections[i] == self.selections[j] and - self.selections[i + 1] == self.selections[j + 1] then - common.splice(self.selections, i, 4) - if self.last_selection >= (i + 3) / 4 then - self.last_selection = self.last_selection - 1 - end - break - end - end - end -end - -local function selection_iterator(invariant, idx) - local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1) - if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end - if invariant[2] then - return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4)) - else - return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4) - end -end - --- If idx_reverse is true, it'll reverse iterate. If nil, or false, regular iterate. --- If a number, runs for exactly that iteration. -function Doc:get_selections(sort_intra, idx_reverse) - return selection_iterator, { self.selections, sort_intra, idx_reverse }, - idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1) -end - --- End of cursor seciton. function Doc:sanitize_position(line, col) local nlines = #self.lines @@ -365,7 +234,6 @@ local function pop_undo(self, undo_stack, redo_stack, modified) self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) elseif cmd.type == "selection" then self.selections = { table.unpack(cmd) } - self:sanitize_selection() end modified = modified or (cmd.type ~= "selection") @@ -398,29 +266,19 @@ function Doc:raw_insert(line, col, text, undo_stack, time) -- splice lines into line array common.splice(self.lines, line, 1, lines) - -- keep cursors where they should be - for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do - if cline1 < line then break end - local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 - local column_addition = line == cline1 and ccol1 > col and len or 0 - self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, - ccol2 + column_addition) - end -- push undo local line2, col2 = self:position_offset(line, col, #text) - push_undo(undo_stack, time, "selection", table.unpack(self.selections)) push_undo(undo_stack, time, "remove", line, col, line2, col2) -- update highlighter and assure selection is in bounds self.highlighter:insert_notify(line, #lines - 1) - self:sanitize_selection() + for i,v in ipairs(listeners) do v(text, line, col, line, col) end end function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- push undo local text = self:get_text(line1, col1, line2, col2) - push_undo(undo_stack, time, "selection", table.unpack(self.selections)) push_undo(undo_stack, time, "insert", line1, col1, text) -- get line content before/after removed text @@ -435,44 +293,8 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) local merge = false - -- keep selections in correct positions: each pair (line, col) - -- * remains unchanged if before the deleted text - -- * is set to (line1, col1) if in the deleted text - -- * is set to (line1, col - col_removal) if on line2 but out of the deleted text - -- * is set to (line - line_removal, col) if after line2 - for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do - if cline2 < line1 then break end - local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 - - if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then - if cline1 > line2 then - l1 = l1 - line_removal - else - l1 = line1 - c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1 - end - end - - if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then - if cline2 > line2 then - l2 = l2 - line_removal - else - l2 = line1 - c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1 - end - end - - if l1 == line1 and c1 == col1 then merge = true end - self:set_selections(idx, l1, c1, l2, c2) - end - - if merge then - self:merge_cursors() - end - -- update highlighter and assure selection is in bounds self.highlighter:remove_notify(line1, line_removal) - self:sanitize_selection() end function Doc:insert(line, col, text) @@ -503,99 +325,6 @@ function Doc:redo() pop_undo(self, self.redo_stack, self.undo_stack, false) end -function Doc:text_input(text, idx) - for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do - local had_selection = false - if line1 ~= line2 or col1 ~= col2 then - self:delete_to_cursor(sidx) - had_selection = true - end - - if self.overwrite - and not had_selection - and col1 < #self.lines[line1] - and text:ulen() == 1 then - self:remove(line1, col1, translate.next_char(self, line1, col1)) - end - - self:insert(line1, col1, text) - self:move_to_cursor(sidx, #text) - end -end - -function Doc:ime_text_editing(text, start, length, idx) - for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do - if line1 ~= line2 or col1 ~= col2 then - self:delete_to_cursor(sidx) - end - self:insert(line1, col1, text) - self:set_selections(sidx, line1, col1 + #text, line1, col1) - end -end - -function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) - local old_text = self:get_text(line1, col1, line2, col2) - local new_text, res = fn(old_text) - if old_text ~= new_text then - self:insert(line2, col2, new_text) - self:remove(line1, col1, line2, col2) - if line1 == line2 and col1 == col2 then - line2, col2 = self:position_offset(line1, col1, #new_text) - self:set_selections(idx, line1, col1, line2, col2) - end - end - return res -end - -function Doc:replace(fn) - local has_selection, results = false, {} - for idx, line1, col1, line2, col2 in self:get_selections(true) do - if line1 ~= line2 or col1 ~= col2 then - results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) - has_selection = true - end - end - if not has_selection then - self:set_selection(table.unpack(self.selections)) - results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) - end - return results -end - -function Doc:delete_to_cursor(idx, ...) - for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do - if line1 ~= line2 or col1 ~= col2 then - self:remove(line1, col1, line2, col2) - else - local l2, c2 = self:position_offset(line1, col1, ...) - self:remove(line1, col1, l2, c2) - line1, col1 = sort_positions(line1, col1, l2, c2) - end - self:set_selections(sidx, line1, col1) - end - self:merge_cursors(idx) -end - -function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end - -function Doc:move_to_cursor(idx, ...) - for sidx, line, col in self:get_selections(false, idx) do - self:set_selections(sidx, self:position_offset(line, col, ...)) - end - self:merge_cursors(idx) -end - -function Doc:move_to(...) return self:move_to_cursor(nil, ...) end - -function Doc:select_to_cursor(idx, ...) - for sidx, line, col, line2, col2 in self:get_selections(false, idx) do - line, col = self:position_offset(line, col, ...) - self:set_selections(sidx, line, col, line2, col2) - end - self:merge_cursors(idx) -end - -function Doc:select_to(...) return self:select_to_cursor(nil, ...) end function Doc:get_indent_string() local indent_type, indent_size = self:get_indent_info() @@ -605,58 +334,7 @@ function Doc:get_indent_string() return string.rep(" ", indent_size) end --- returns the size of the original indent, and the indent --- in your config format, rounded either up or down -function Doc:get_line_indent(line, rnd_up) - local _, e = line:find("^[ \t]+") - local indent_type, indent_size = self:get_indent_info() - local soft_tab = string.rep(" ", indent_size) - if indent_type == "hard" then - local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" - return e, indent:gsub(" +", rnd_up and "\t" or "") - else - local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" - local number = #indent / #soft_tab - return e, indent:sub(1, - (rnd_up and math.ceil(number) or math.floor(number)) * #soft_tab) - end -end --- un/indents text; behaviour varies based on selection and un/indent. --- * if there's a selection, it will stay static around the --- text for both indenting and unindenting. --- * if you are in the beginning whitespace of a line, and are indenting, the --- cursor will insert the exactly appropriate amount of spaces, and jump the --- cursor to the beginning of first non whitespace characters --- * if you are not in the beginning whitespace of a line, and you indent, it --- inserts the appropriate whitespace, as if you typed them normally. --- * if you are unindenting, the cursor will jump to the start of the line, --- and remove the appropriate amount of spaces (or a tab). -function Doc:indent_text(unindent, line1, col1, line2, col2) - local text = self:get_indent_string() - local _, se = self.lines[line1]:find("^[ \t]+") - local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) - local has_selection = line1 ~= line2 or col1 ~= col2 - if unindent or has_selection or in_beginning_whitespace then - local l1d, l2d = #self.lines[line1], #self.lines[line2] - for line = line1, line2 do - if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection - local e, rnded = self:get_line_indent(self.lines[line], unindent) - self:remove(line, 1, line, (e or 0) + 1) - self:insert(line, 1, - unindent and rnded:sub(1, #rnded - #text) or rnded .. text) - end - end - l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d - if (unindent or in_beginning_whitespace) and not has_selection then - local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1]) - return line1, start_cursor, line2, start_cursor - end - return line1, col1 + l1d, line2, col2 + l2d - end - self:insert(line1, col1, text) - return line1, col1 + #text, line1, col1 + #text -end -- For plugins to add custom actions of document change function Doc:on_text_change(type) diff --git a/data/core/docview.lua b/data/core/docview.lua index b679eb6f5..6239c0215 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -64,10 +64,215 @@ function DocView:new(doc) self.ime_selection = { from = 0, size = 0 } self.ime_status = false self.hovering_gutter = false + table.insert(doc.listeners, function(...) self:listener(...) end) + self.lines = {} + self.selections = {} self.v_scrollbar:set_forced_status(config.force_scrollbar_status) self.h_scrollbar:set_forced_status(config.force_scrollbar_status) end +local function sort_positions(line1, col1, line2, col2) + if line1 > line2 or line1 == line2 and col1 > col2 then + return line2, col2, line1, col1, true + end + return line1, col1, line2, col2, false +end + + +-- Cursor section. Cursor indices are *only* valid during a get_selections() call. +-- Cursors will always be iterated in order from top to bottom. Through normal operation +-- curors can never swap positions; only merge or split, or change their position in cursor +-- order. +function DocView:get_selection(sort) + local line1, col1, line2, col2, swap = self:get_selection_idx(self.last_selection, sort) + if not line1 then + line1, col1, line2, col2, swap = self:get_selection_idx(1, sort) + end + return line1, col1, line2, col2, swap +end + +---Get the selection specified by `idx` +---@param idx integer @the index of the selection to retrieve +---@param sort? boolean @whether to sort the selection returned +---@return integer,integer,integer,integer,boolean? @line1, col1, line2, col2, was the selection sorted +function DocView:get_selection_idx(idx, sort) + local line1, col1, line2, col2 = self.selections[idx * 4 - 3], self.selections[idx * 4 - 2], + self.selections[idx * 4 - 1], + self.selections[idx * 4] + if line1 and sort then + return sort_positions(line1, col1, line2, col2) + else + return line1, col1, line2, col2 + end +end + +function DocView:get_selection_text(limit) + limit = limit or math.huge + local result = {} + for idx, line1, col1, line2, col2 in self:get_selections() do + if idx > limit then break end + if line1 ~= line2 or col1 ~= col2 then + local text = self:get_text(line1, col1, line2, col2) + if text ~= "" then result[#result + 1] = text end + end + end + return table.concat(result, "\n") +end + +function DocView:has_selection() + local line1, col1, line2, col2 = self:get_selection(false) + return line1 ~= line2 or col1 ~= col2 +end + +function DocView:has_any_selection() + for idx, line1, col1, line2, col2 in self:get_selections() do + if line1 ~= line2 or col1 ~= col2 then return true end + end + return false +end + + +function DocView:set_selections(idx, line1, col1, line2, col2, swap, rm) + assert(not line2 == not col2, "expected 3 or 5 arguments") + if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end + line1, col1 = self:sanitize_position(line1, col1) + line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) + common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) +end + +function DocView:add_selection(line1, col1, line2, col2, swap) + local l1, c1 = sort_positions(line1, col1, line2 or line1, col2 or col1) + local target = #self.selections / 4 + 1 + for idx, tl1, tc1 in self:get_selections(true) do + if l1 < tl1 or l1 == tl1 and c1 < tc1 then + target = idx + break + end + end + self:set_selections(target, line1, col1, line2, col2, swap, 0) + self.last_selection = target +end + +function DocView:remove_selection(idx) + if self.last_selection >= idx then + self.last_selection = self.last_selection - 1 + end + common.splice(self.selections, (idx - 1) * 4 + 1, 4) +end + +function DocView:set_selection(line1, col1, line2, col2, swap) + self.selections = {} + self:set_selections(1, line1, col1, line2, col2, swap) + self.last_selection = 1 +end + +function DocView:merge_cursors(idx) + for i = (idx or (#self.selections - 3)), (idx or 5), -4 do + for j = 1, i - 4, 4 do + if self.selections[i] == self.selections[j] and + self.selections[i + 1] == self.selections[j + 1] then + common.splice(self.selections, i, 4) + if self.last_selection >= (i + 3) / 4 then + self.last_selection = self.last_selection - 1 + end + break + end + end + end +end + +local function selection_iterator(invariant, idx) + local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1) + if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end + if invariant[2] then + return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4)) + else + return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4) + end +end + +-- If idx_reverse is true, it'll reverse iterate. If nil, or false, regular iterate. +-- If a number, runs for exactly that iteration. +function DocView:get_selections(sort_intra, idx_reverse) + return selection_iterator, { self.selections, sort_intra, idx_reverse }, + idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1) +end + +-- End of cursor seciton. + +function DocView:sanitize_selection() + for idx, line1, col1, line2, col2 in self:get_selections() do + self:set_selections(idx, line1, col1, line2, col2) + end +end + +function DocView:text_input(text, idx) + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do + local had_selection = false + if line1 ~= line2 or col1 ~= col2 then + self:delete_to_cursor(sidx) + had_selection = true + end + + if self.overwrite + and not had_selection + and col1 < #self.lines[line1] + and text:ulen() == 1 then + self:remove(line1, col1, translate.next_char(self, line1, col1)) + end + + self:insert(line1, col1, text) + self:move_to_cursor(sidx, #text) + end +end + +function DocView:listener(text, line1, col1, line2, col2) + + -- keep cursors where they should be on insertion + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline1 < line then break end + local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 + local column_addition = line == cline1 and ccol1 > col and len or 0 + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, + ccol2 + column_addition) + end + + + -- keep selections in correct positions on removal: each pair (line, col) + -- * remains unchanged if before the deleted text + -- * is set to (line1, col1) if in the deleted text + -- * is set to (line1, col - col_removal) if on line2 but out of the deleted text + -- * is set to (line - line_removal, col) if after line2 + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline2 < line1 then break end + local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 + + if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then + if cline1 > line2 then + l1 = l1 - line_removal + else + l1 = line1 + c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1 + end + end + + if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then + if cline2 > line2 then + l2 = l2 - line_removal + else + l2 = line1 + c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1 + end + end + + if l1 == line1 and c1 == col1 then merge = true end + self:set_selections(idx, l1, c1, l2, c2) + end + + if merge then + self:merge_cursors() + end +end function DocView:try_close(do_close) if self.doc:is_dirty() @@ -94,6 +299,136 @@ function DocView:try_close(do_close) end + +function DocView:ime_text_editing(text, start, length, idx) + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do + if line1 ~= line2 or col1 ~= col2 then + self:delete_to_cursor(sidx) + end + self:insert(line1, col1, text) + self:set_selections(sidx, line1, col1 + #text, line1, col1) + end +end + +function DocView:replace_cursor(idx, line1, col1, line2, col2, fn) + local old_text = self:get_text(line1, col1, line2, col2) + local new_text, res = fn(old_text) + if old_text ~= new_text then + self:insert(line2, col2, new_text) + self:remove(line1, col1, line2, col2) + if line1 == line2 and col1 == col2 then + line2, col2 = self:position_offset(line1, col1, #new_text) + self:set_selections(idx, line1, col1, line2, col2) + end + end + return res +end + +function DocView:replace(fn) + local has_selection, results = false, {} + for idx, line1, col1, line2, col2 in self:get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn) + has_selection = true + end + end + if not has_selection then + self:set_selection(table.unpack(self.selections)) + results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) + end + return results +end + +function DocView:delete_to_cursor(idx, ...) + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do + if line1 ~= line2 or col1 ~= col2 then + self:remove(line1, col1, line2, col2) + else + local l2, c2 = self:position_offset(line1, col1, ...) + self:remove(line1, col1, l2, c2) + line1, col1 = sort_positions(line1, col1, l2, c2) + end + self:set_selections(sidx, line1, col1) + end + self:merge_cursors(idx) +end + +function DocView:delete_to(...) return self:delete_to_cursor(nil, ...) end + +function DocView:move_to_cursor(idx, ...) + for sidx, line, col in self:get_selections(false, idx) do + self:set_selections(sidx, self:position_offset(line, col, ...)) + end + self:merge_cursors(idx) +end + +function DocView:move_to(...) return self:move_to_cursor(nil, ...) end + +function DocView:select_to_cursor(idx, ...) + for sidx, line, col, line2, col2 in self:get_selections(false, idx) do + line, col = self:position_offset(line, col, ...) + self:set_selections(sidx, line, col, line2, col2) + end + self:merge_cursors(idx) +end + +function DocView:select_to(...) return self:select_to_cursor(nil, ...) end + +-- returns the size of the original indent, and the indent +-- in your config format, rounded either up or down +function DocView:get_line_indent(line, rnd_up) + local _, e = line:find("^[ \t]+") + local indent_type, indent_size = self:get_indent_info() + local soft_tab = string.rep(" ", indent_size) + if indent_type == "hard" then + local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" + return e, indent:gsub(" +", rnd_up and "\t" or "") + else + local indent = e and line:sub(1, e):gsub("\t", soft_tab) or "" + local number = #indent / #soft_tab + return e, indent:sub(1, + (rnd_up and math.ceil(number) or math.floor(number)) * #soft_tab) + end +end + + +-- un/indents text; behaviour varies based on selection and un/indent. +-- * if there's a selection, it will stay static around the +-- text for both indenting and unindenting. +-- * if you are in the beginning whitespace of a line, and are indenting, the +-- cursor will insert the exactly appropriate amount of spaces, and jump the +-- cursor to the beginning of first non whitespace characters +-- * if you are not in the beginning whitespace of a line, and you indent, it +-- inserts the appropriate whitespace, as if you typed them normally. +-- * if you are unindenting, the cursor will jump to the start of the line, +-- and remove the appropriate amount of spaces (or a tab). +function DocView:indent_text(unindent, line1, col1, line2, col2) + local text = self:get_indent_string() + local _, se = self.lines[line1]:find("^[ \t]+") + local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) + local has_selection = line1 ~= line2 or col1 ~= col2 + if unindent or has_selection or in_beginning_whitespace then + local l1d, l2d = #self.lines[line1], #self.lines[line2] + for line = line1, line2 do + if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection + local e, rnded = self:get_line_indent(self.lines[line], unindent) + self:remove(line, 1, line, (e or 0) + 1) + self:insert(line, 1, + unindent and rnded:sub(1, #rnded - #text) or rnded .. text) + end + end + l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d + if (unindent or in_beginning_whitespace) and not has_selection then + local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1]) + return line1, start_cursor, line2, start_cursor + end + return line1, col1 + l1d, line2, col2 + l2d + end + self:insert(line1, col1, text) + return line1, col1 + #text, line1, col1 + #text +end + + function DocView:get_name() local post = self.doc:is_dirty() and "*" or "" local name = self.doc:get_name() From 86246a6b34c1a98ca92aab94a7f1cff1bb40e356 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Fri, 29 Dec 2023 14:34:15 -0500 Subject: [PATCH 02/24] Initial commit of virtual lines PR. --- data/core/command.lua | 2 +- data/core/commands/doc.lua | 600 ----------------------------- data/core/commands/docview.lua | 545 ++++++++++++++++++++++++++ data/core/commands/findreplace.lua | 23 +- data/core/common.lua | 7 + data/core/doc/init.lua | 121 ++++-- data/core/docview.lua | 304 +++++++++------ data/core/keymap-macos.lua | 22 +- data/core/keymap.lua | 144 +++---- data/core/rootview.lua | 2 +- data/core/statusview.lua | 6 +- data/plugins/autocomplete.lua | 12 +- data/plugins/codefolding.lua | 132 +++++++ 13 files changed, 1067 insertions(+), 853 deletions(-) create mode 100644 data/core/commands/docview.lua create mode 100644 data/plugins/codefolding.lua diff --git a/data/core/command.lua b/data/core/command.lua index 2b28f63b1..fd12dbd86 100644 --- a/data/core/command.lua +++ b/data/core/command.lua @@ -174,7 +174,7 @@ end ---Inserts the default commands for Lite XL into the map. function command.add_defaults() local reg = { - "core", "root", "command", "doc", "findreplace", + "core", "root", "command", "doc", "docview", "findreplace", "files", "dialog", "log", "statusbar" } for _, name in ipairs(reg) do diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index 3d4426c3c..ce215ef38 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -12,19 +12,6 @@ local function doc() return core.active_view.doc end - -local function doc_multiline_selections(sort) - local iter, state, idx, line1, col1, line2, col2 = doc():get_selections(sort) - return function() - idx, line1, col1, line2, col2 = iter(state, idx) - if idx and line2 > line1 and col2 == 1 then - line2 = line2 - 1 - col2 = #doc().lines[line2] - end - return idx, line1, col1, line2, col2 - end -end - local function append_line_if_last_line(line) if line >= #doc().lines then doc():insert(line, math.huge, "\n") @@ -57,499 +44,12 @@ local function save(filename) end end -local function cut_or_copy(delete) - local full_text = "" - local text = "" - core.cursor_clipboard = {} - core.cursor_clipboard_whole_line = {} - for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do - if line1 ~= line2 or col1 ~= col2 then - text = doc():get_text(line1, col1, line2, col2) - full_text = full_text == "" and text or (text .. " " .. full_text) - core.cursor_clipboard_whole_line[idx] = false - if delete then - doc():delete_to_cursor(idx, 0) - end - else -- Cut/copy whole line - -- Remove newline from the text. It will be added as needed on paste. - text = string.sub(doc().lines[line1], 1, -2) - full_text = full_text == "" and text .. "\n" or (text .. "\n" .. full_text) - core.cursor_clipboard_whole_line[idx] = true - if delete then - if line1 < #doc().lines then - doc():remove(line1, 1, line1 + 1, 1) - elseif #doc().lines == 1 then - doc():remove(line1, 1, line1, math.huge) - else - doc():remove(line1 - 1, math.huge, line1, math.huge) - end - doc():set_selections(idx, line1, col1, line2, col2) - end - end - core.cursor_clipboard[idx] = text - end - if delete then doc():merge_cursors() end - core.cursor_clipboard["full"] = full_text - system.set_clipboard(full_text) -end - -local function split_cursor(direction) - local new_cursors = {} - for _, line1, col1 in doc():get_selections() do - if line1 + direction >= 1 and line1 + direction <= #doc().lines then - table.insert(new_cursors, { line1 + direction, col1 }) - end - end - -- add selections in the order that will leave the "last" added one as doc.last_selection - local start, stop = 1, #new_cursors - if direction < 0 then - start, stop = #new_cursors, 1 - end - for i = start, stop, direction do - local v = new_cursors[i] - doc():add_selection(v[1], v[2]) - end - core.blink_reset() -end - -local function set_cursor(dv, x, y, snap_type) - local line, col = dv:resolve_screen_position(x, y) - dv.doc:set_selection(line, col, line, col) - if snap_type == "word" or snap_type == "lines" then - command.perform("doc:select-" .. snap_type) - end - dv.mouse_selecting = { line, col, snap_type } - core.blink_reset() -end - -local function line_comment(comment, line1, col1, line2, col2) - local start_comment = (type(comment) == 'table' and comment[1] or comment) .. " " - local end_comment = (type(comment) == 'table' and " " .. comment[2]) - local uncomment = true - local start_offset = math.huge - for line = line1, line2 do - local text = doc().lines[line] - local s = text:find("%S") - if s then - local cs, ce = text:find(start_comment, s, true) - if cs ~= s then - uncomment = false - end - start_offset = math.min(start_offset, s) - end - end - - local end_line = col2 == #doc().lines[line2] - for line = line1, line2 do - local text = doc().lines[line] - local s = text:find("%S") - if s and uncomment then - if end_comment and text:sub(#text - #end_comment, #text - 1) == end_comment then - doc():remove(line, #text - #end_comment, line, #text) - end - local cs, ce = text:find(start_comment, s, true) - if ce then - doc():remove(line, cs, line, ce + 1) - end - elseif s then - doc():insert(line, start_offset, start_comment) - if end_comment then - doc():insert(line, #doc().lines[line], " " .. comment[2]) - end - end - end - col1 = col1 + (col1 > start_offset and #start_comment or 0) * (uncomment and -1 or 1) - col2 = col2 + (col2 > start_offset and #start_comment or 0) * (uncomment and -1 or 1) - if end_comment and end_line then - col2 = col2 + #end_comment * (uncomment and -1 or 1) - end - return line1, col1, line2, col2 -end - -local function block_comment(comment, line1, col1, line2, col2) - -- automatically skip spaces - local word_start = doc():get_text(line1, col1, line1, math.huge):find("%S") - local word_end = doc():get_text(line2, 1, line2, col2):find("%s*$") - col1 = col1 + (word_start and (word_start - 1) or 0) - col2 = word_end and word_end or col2 - - local block_start = doc():get_text(line1, col1, line1, col1 + #comment[1]) - local block_end = doc():get_text(line2, col2 - #comment[2], line2, col2) - - if block_start == comment[1] and block_end == comment[2] then - -- remove up to 1 whitespace after the comment - local start_len, stop_len = #comment[1], #comment[2] - if doc():get_text(line1, col1 + #comment[1], line1, col1 + #comment[1] + 1):find("%s$") then - start_len = start_len + 1 - end - if doc():get_text(line2, col2 - #comment[2] - 1, line2, col2):find("^%s") then - stop_len = stop_len + 1 - end - - doc():remove(line1, col1, line1, col1 + start_len) - col2 = col2 - (line1 == line2 and start_len or 0) - doc():remove(line2, col2 - stop_len, line2, col2) - - return line1, col1, line2, col2 - stop_len - else - doc():insert(line1, col1, comment[1] .. " ") - col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0) - doc():insert(line2, col2, " " .. comment[2]) - - return line1, col1, line2, col2 + #comment[2] + 1 - end -end - -local function insert_paste(doc, value, whole_line, idx) - if whole_line then - local line1, col1 = doc:get_selection_idx(idx) - doc:insert(line1, 1, value:gsub("\r", "").."\n") - -- Because we're inserting at the start of the line, - -- if the cursor is in the middle of the line - -- it gets carried to the next line along with the old text. - -- If it's at the start of the line it doesn't get carried, - -- so we move it of as many characters as we're adding. - if col1 == 1 then - doc:move_to_cursor(idx, #value+1) - end - else - doc:text_input(value:gsub("\r", ""), idx) - end -end - local commands = { - ["doc:select-none"] = function(dv) - local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection) - if not l1 then - l1, c1 = dv.doc:get_selection_idx(1) - end - dv.doc:set_selection(l1, c1) - end, - - ["doc:cut"] = function() - cut_or_copy(true) - end, - - ["doc:copy"] = function() - cut_or_copy(false) - end, - - ["doc:undo"] = function(dv) - dv.doc:undo() - end, - - ["doc:redo"] = function(dv) - dv.doc:redo() - end, - - ["doc:paste"] = function(dv) - local clipboard = system.get_clipboard() - -- If the clipboard has changed since our last look, use that instead - if core.cursor_clipboard["full"] ~= clipboard then - core.cursor_clipboard = {} - core.cursor_clipboard_whole_line = {} - for idx in dv.doc:get_selections() do - insert_paste(dv.doc, clipboard, false, idx) - end - return - end - -- Use internal clipboard(s) - -- If there are mixed whole lines and normal lines, consider them all as normal - local only_whole_lines = true - for _,whole_line in pairs(core.cursor_clipboard_whole_line) do - if not whole_line then - only_whole_lines = false - break - end - end - if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then - -- If we have the same number of clipboards and selections, - -- paste each clipboard into its corresponding selection - for idx in dv.doc:get_selections() do - insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx) - end - else - -- Paste every clipboard and add a selection at the end of each one - local new_selections = {} - for idx in dv.doc:get_selections() do - for cb_idx in ipairs(core.cursor_clipboard_whole_line) do - insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx) - if not only_whole_lines then - table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) - end - end - if only_whole_lines then - table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) - end - end - local first = true - for _,selection in pairs(new_selections) do - if first then - dv.doc:set_selection(table.unpack(selection)) - first = false - else - dv.doc:add_selection(table.unpack(selection)) - end - end - end - end, - - ["doc:newline"] = function(dv) - for idx, line, col in dv.doc:get_selections(false, true) do - local indent = dv.doc.lines[line]:match("^[\t ]*") - if col <= #indent then - indent = indent:sub(#indent + 2 - col) - end - -- Remove current line if it contains only whitespace - if not config.keep_newline_whitespace and dv.doc.lines[line]:match("^%s+$") then - dv.doc:remove(line, 1, line, math.huge) - end - dv.doc:text_input("\n" .. indent, idx) - end - end, - - ["doc:newline-below"] = function(dv) - for idx, line in dv.doc:get_selections(false, true) do - local indent = dv.doc.lines[line]:match("^[\t ]*") - dv.doc:insert(line, math.huge, "\n" .. indent) - dv.doc:set_selections(idx, line + 1, math.huge) - end - end, - - ["doc:newline-above"] = function(dv) - for idx, line in dv.doc:get_selections(false, true) do - local indent = dv.doc.lines[line]:match("^[\t ]*") - dv.doc:insert(line, 1, indent .. "\n") - dv.doc:set_selections(idx, line, math.huge) - end - end, - - ["doc:delete"] = function(dv) - for idx, line1, col1, line2, col2 in dv.doc:get_selections(true, true) do - if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then - dv.doc:remove(line1, col1, line1, math.huge) - end - dv.doc:delete_to_cursor(idx, translate.next_char) - end - end, - - ["doc:backspace"] = function(dv) - local _, indent_size = dv.doc:get_indent_info() - for idx, line1, col1, line2, col2 in dv.doc:get_selections(true, true) do - if line1 == line2 and col1 == col2 then - local text = dv.doc:get_text(line1, 1, line1, col1) - if #text >= indent_size and text:find("^ *$") then - dv.doc:delete_to_cursor(idx, 0, -indent_size) - goto continue - end - end - dv.doc:delete_to_cursor(idx, translate.previous_char) - ::continue:: - end - end, - - ["doc:select-all"] = function(dv) - dv.doc:set_selection(1, 1, math.huge, math.huge) - -- avoid triggering DocView:scroll_to_make_visible - dv.last_line1 = 1 - dv.last_col1 = 1 - dv.last_line2 = #dv.doc.lines - dv.last_col2 = #dv.doc.lines[#dv.doc.lines] - end, - - ["doc:select-lines"] = function(dv) - for idx, line1, _, line2 in dv.doc:get_selections(true) do - append_line_if_last_line(line2) - dv.doc:set_selections(idx, line2 + 1, 1, line1, 1) - end - end, - - ["doc:select-word"] = function(dv) - for idx, line1, col1 in dv.doc:get_selections(true) do - local line1, col1 = translate.start_of_word(dv.doc, line1, col1) - local line2, col2 = translate.end_of_word(dv.doc, line1, col1) - dv.doc:set_selections(idx, line2, col2, line1, col1) - end - end, - - ["doc:join-lines"] = function(dv) - for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do - if line1 == line2 then line2 = line2 + 1 end - local text = dv.doc:get_text(line1, 1, line2, math.huge) - text = text:gsub("(.-)\n[\t ]*", function(x) - return x:find("^%s*$") and x or x .. " " - end) - dv.doc:insert(line1, 1, text) - dv.doc:remove(line1, #text + 1, line2, math.huge) - if line1 ~= line2 or col1 ~= col2 then - dv.doc:set_selections(idx, line1, math.huge) - end - end - end, - - ["doc:indent"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local l1, c1, l2, c2 = dv.doc:indent_text(false, line1, col1, line2, col2) - if l1 then - dv.doc:set_selections(idx, l1, c1, l2, c2) - end - end - end, - - ["doc:unindent"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2) - if l1 then - dv.doc:set_selections(idx, l1, c1, l2, c2) - end - end - end, - - ["doc:duplicate-lines"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - append_line_if_last_line(line2) - local text = doc():get_text(line1, 1, line2 + 1, 1) - dv.doc:insert(line2 + 1, 1, text) - local n = line2 - line1 + 1 - dv.doc:set_selections(idx, line1 + n, col1, line2 + n, col2) - end - end, - - ["doc:delete-lines"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - append_line_if_last_line(line2) - dv.doc:remove(line1, 1, line2 + 1, 1) - dv.doc:set_selections(idx, line1, col1) - end - end, - - ["doc:move-lines-up"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - append_line_if_last_line(line2) - if line1 > 1 then - local text = doc().lines[line1 - 1] - dv.doc:insert(line2 + 1, 1, text) - dv.doc:remove(line1 - 1, 1, line1, 1) - dv.doc:set_selections(idx, line1 - 1, col1, line2 - 1, col2) - end - end - end, - - ["doc:move-lines-down"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - append_line_if_last_line(line2 + 1) - if line2 < #dv.doc.lines then - local text = dv.doc.lines[line2 + 1] - dv.doc:remove(line2 + 1, 1, line2 + 2, 1) - dv.doc:insert(line1, 1, text) - dv.doc:set_selections(idx, line1 + 1, col1, line2 + 1, col2) - end - end - end, - - ["doc:toggle-block-comments"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local current_syntax = dv.doc.syntax - if line1 > 1 then - -- Use the previous line state, as it will be the state - -- of the beginning of the current line - local state = dv.doc.highlighter:get_line(line1 - 1).state - local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) - -- Go through all the syntaxes until the first with `block_comment` defined - for _, s in pairs(syntaxes) do - if s.block_comment then - current_syntax = s - break - end - end - end - local comment = current_syntax.block_comment - if not comment then - if dv.doc.syntax.comment then - command.perform "doc:toggle-line-comments" - end - return - end - -- if nothing is selected, toggle the whole line - if line1 == line2 and col1 == col2 then - col1 = 1 - col2 = #dv.doc.lines[line2] - end - dv.doc:set_selections(idx, block_comment(comment, line1, col1, line2, col2)) - end - end, - - ["doc:toggle-line-comments"] = function(dv) - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local current_syntax = dv.doc.syntax - if line1 > 1 then - -- Use the previous line state, as it will be the state - -- of the beginning of the current line - local state = dv.doc.highlighter:get_line(line1 - 1).state - local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) - -- Go through all the syntaxes until the first with comments defined - for _, s in pairs(syntaxes) do - if s.comment or s.block_comment then - current_syntax = s - break - end - end - end - local comment = current_syntax.comment or current_syntax.block_comment - if comment then - dv.doc:set_selections(idx, line_comment(comment, line1, col1, line2, col2)) - end - end - end, - - ["doc:upper-case"] = function(dv) - dv.doc:replace(string.uupper) - end, - - ["doc:lower-case"] = function(dv) - dv.doc:replace(string.ulower) - end, - - ["doc:go-to-line"] = function(dv) - local items - local function init_items() - if items then return end - items = {} - local mt = { __tostring = function(x) return x.text end } - for i, line in ipairs(dv.doc.lines) do - local item = { text = line:sub(1, -2), line = i, info = "line: " .. i } - table.insert(items, setmetatable(item, mt)) - end - end - - core.command_view:enter("Go To Line", { - submit = function(text, item) - local line = item and item.line or tonumber(text) - if not line then - core.error("Invalid line number or unmatched string") - return - end - dv.doc:set_selection(line, 1 ) - dv:scroll_to_line(line, true) - end, - suggest = function(text) - if not text:find("^%d*$") then - init_items() - return common.fuzzy_match(items, text) - end - end - }) - end, ["doc:toggle-line-ending"] = function(dv) dv.doc.crlf = not dv.doc.crlf end, - ["doc:toggle-overwrite"] = function(dv) - dv.doc.overwrite = not dv.doc.overwrite - core.blink_reset() -- to show the cursor has changed edit modes - end, - ["doc:save-as"] = function(dv) local last_doc = core.last_active_view and core.last_active_view.doc local text @@ -618,106 +118,6 @@ local commands = { core.log("Removed \"%s\"", filename) end, - ["doc:select-to-cursor"] = function(dv, x, y, clicks) - local line1, col1 = select(3, doc():get_selection()) - local line2, col2 = dv:resolve_screen_position(x, y) - dv.mouse_selecting = { line1, col1, nil } - dv.doc:set_selection(line2, col2, line1, col1) - end, - - ["doc:create-cursor-previous-line"] = function(dv) - split_cursor(-1) - dv.doc:merge_cursors() - end, - - ["doc:create-cursor-next-line"] = function(dv) - split_cursor(1) - dv.doc:merge_cursors() - end - } -command.add(function(x, y) - if x == nil or y == nil or not core.active_view:extends(DocView) then return false end - local dv = core.active_view - local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y - return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y -end, { - ["doc:set-cursor"] = function(dv, x, y) - set_cursor(dv, x, y, "set") - end, - - ["doc:set-cursor-word"] = function(dv, x, y) - set_cursor(dv, x, y, "word") - end, - - ["doc:set-cursor-line"] = function(dv, x, y, clicks) - set_cursor(dv, x, y, "lines") - end, - - ["doc:split-cursor"] = function(dv, x, y, clicks) - local line, col = dv:resolve_screen_position(x, y) - local removal_target = nil - for idx, line1, col1 in dv.doc:get_selections(true) do - if line1 == line and col1 == col and #doc().selections > 4 then - removal_target = idx - end - end - if removal_target then - dv.doc:remove_selection(removal_target) - else - dv.doc:add_selection(line, col, line, col) - end - dv.mouse_selecting = { line, col, "set" } - end -}) - -local translations = { - ["previous-char"] = translate, - ["next-char"] = translate, - ["previous-word-start"] = translate, - ["next-word-end"] = translate, - ["previous-block-start"] = translate, - ["next-block-end"] = translate, - ["start-of-doc"] = translate, - ["end-of-doc"] = translate, - ["start-of-line"] = translate, - ["end-of-line"] = translate, - ["start-of-word"] = translate, - ["start-of-indentation"] = translate, - ["end-of-word"] = translate, - ["previous-line"] = DocView.translate, - ["next-line"] = DocView.translate, - ["previous-page"] = DocView.translate, - ["next-page"] = DocView.translate, -} - -for name, obj in pairs(translations) do - commands["doc:move-to-" .. name] = function(dv) dv.doc:move_to(obj[name:gsub("-", "_")], dv) end - commands["doc:select-to-" .. name] = function(dv) dv.doc:select_to(obj[name:gsub("-", "_")], dv) end - commands["doc:delete-to-" .. name] = function(dv) dv.doc:delete_to(obj[name:gsub("-", "_")], dv) end -end - -commands["doc:move-to-previous-char"] = function(dv) - for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do - if line1 ~= line2 or col1 ~= col2 then - dv.doc:set_selections(idx, line1, col1) - else - dv.doc:move_to_cursor(idx, translate.previous_char) - end - end - dv.doc:merge_cursors() -end - -commands["doc:move-to-next-char"] = function(dv) - for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do - if line1 ~= line2 or col1 ~= col2 then - dv.doc:set_selections(idx, line2, col2) - else - dv.doc:move_to_cursor(idx, translate.next_char) - end - end - dv.doc:merge_cursors() -end - command.add("core.docview", commands) diff --git a/data/core/commands/docview.lua b/data/core/commands/docview.lua new file mode 100644 index 000000000..dcaabbee0 --- /dev/null +++ b/data/core/commands/docview.lua @@ -0,0 +1,545 @@ +local core = require "core" +local command = require "core.command" +local common = require "core.common" +local config = require "core.config" +local translate = require "core.doc.translate" +local style = require "core.style" +local DocView = require "core.docview" +local tokenizer = require "core.tokenizer" + + +local function doc() + return core.active_view.doc +end + +local function docview() + return core.active_view +end + + +local function doc_multiline_selections(sort) + local iter, state, idx, line1, col1, line2, col2 = docview():get_selections(sort) + return function() + idx, line1, col1, line2, col2 = iter(state, idx) + if idx and line2 > line1 and col2 == 1 then + line2 = line2 - 1 + col2 = #docview().doc.lines[line2] + end + return idx, line1, col1, line2, col2 + end +end + +local function cut_or_copy(delete) + local full_text = "" + local text = "" + core.cursor_clipboard = {} + core.cursor_clipboard_whole_line = {} + for idx, line1, col1, line2, col2 in docview():get_selections(true, true) do + if line1 ~= line2 or col1 ~= col2 then + text = doc():get_text(line1, col1, line2, col2) + full_text = full_text == "" and text or (text .. " " .. full_text) + core.cursor_clipboard_whole_line[idx] = false + if delete then + docview():delete_to_cursor(idx, 0) + end + else -- Cut/copy whole line + -- Remove newline from the text. It will be added as needed on paste. + text = string.sub(doc().lines[line1], 1, -2) + full_text = full_text == "" and text .. "\n" or (text .. "\n" .. full_text) + core.cursor_clipboard_whole_line[idx] = true + if delete then + if line1 < #doc().lines then + doc():remove(line1, 1, line1 + 1, 1) + elseif #doc().lines == 1 then + doc():remove(line1, 1, line1, math.huge) + else + doc():remove(line1 - 1, math.huge, line1, math.huge) + end + docview():set_selections(idx, line1, col1, line2, col2) + end + end + core.cursor_clipboard[idx] = text + end + if delete then docview():merge_cursors() end + core.cursor_clipboard["full"] = full_text + system.set_clipboard(full_text) +end + +local function split_cursor(direction) + local new_cursors = {} + for _, line1, col1 in docview():get_selections() do + if line1 + direction >= 1 and line1 + direction <= #doc().lines then + table.insert(new_cursors, { line1 + direction, col1 }) + end + end + -- add selections in the order that will leave the "last" added one as doc.last_selection + local start, stop = 1, #new_cursors + if direction < 0 then + start, stop = #new_cursors, 1 + end + for i = start, stop, direction do + local v = new_cursors[i] + docview():add_selection(v[1], v[2]) + end + core.blink_reset() +end + +local function set_cursor(dv, x, y, snap_type) + local line, col = dv:resolve_screen_position(x, y) + dv:set_selection(line, col, line, col) + if snap_type == "word" or snap_type == "lines" then + command.perform("doc:select-" .. snap_type) + end + dv.mouse_selecting = { line, col, snap_type } + core.blink_reset() +end + +local function insert_paste(doc, value, whole_line, idx) + if whole_line then + local line1, col1 = doc:get_selection_idx(idx) + doc:insert(line1, 1, value:gsub("\r", "").."\n") + -- Because we're inserting at the start of the line, + -- if the cursor is in the middle of the line + -- it gets carried to the next line along with the old text. + -- If it's at the start of the line it doesn't get carried, + -- so we move it of as many characters as we're adding. + if col1 == 1 then + doc:move_to_cursor(idx, #value+1) + end + else + doc:text_input(value:gsub("\r", ""), idx) + end +end + +local commands = { + ["docview:select-none"] = function(dv) + local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection) + if not l1 then + l1, c1 = dv.doc:get_selection_idx(1) + end + dv:set_selection(l1, c1) + end, + + ["docview:cut"] = function() + cut_or_copy(true) + end, + + ["docview:copy"] = function() + cut_or_copy(false) + end, + + ["docview:undo"] = function(dv) + dv.doc:undo() + end, + + ["docview:redo"] = function(dv) + dv.doc:redo() + end, + + ["docview:paste"] = function(dv) + local clipboard = system.get_clipboard() + -- If the clipboard has changed since our last look, use that instead + if core.cursor_clipboard["full"] ~= clipboard then + core.cursor_clipboard = {} + core.cursor_clipboard_whole_line = {} + for idx in dv.doc:get_selections() do + insert_paste(dv.doc, clipboard, false, idx) + end + return + end + -- Use internal clipboard(s) + -- If there are mixed whole lines and normal lines, consider them all as normal + local only_whole_lines = true + for _,whole_line in pairs(core.cursor_clipboard_whole_line) do + if not whole_line then + only_whole_lines = false + break + end + end + if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then + -- If we have the same number of clipboards and selections, + -- paste each clipboard into its corresponding selection + for idx in dv.doc:get_selections() do + insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx) + end + else + -- Paste every clipboard and add a selection at the end of each one + local new_selections = {} + for idx in dv.doc:get_selections() do + for cb_idx in ipairs(core.cursor_clipboard_whole_line) do + insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx) + if not only_whole_lines then + table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) + end + end + if only_whole_lines then + table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) + end + end + local first = true + for _,selection in pairs(new_selections) do + if first then + dv:set_selection(table.unpack(selection)) + first = false + else + dv:add_selection(table.unpack(selection)) + end + end + end + end, + + ["docview:newline"] = function(dv) + for idx, line, col in dv:get_selections(false, true) do + local indent = dv.doc.lines[line]:match("^[\t ]*") + if col <= #indent then + indent = indent:sub(#indent + 2 - col) + end + -- Remove current line if it contains only whitespace + if not config.keep_newline_whitespace and dv.doc.lines[line]:match("^%s+$") then + dv.doc:remove(line, 1, line, math.huge) + end + dv:text_input("\n" .. indent, idx) + end + end, + + ["docview:newline-below"] = function(dv) + for idx, line in dv:get_selections(false, true) do + local indent = dv.doc.lines[line]:match("^[\t ]*") + dv.doc:insert(line, math.huge, "\n" .. indent) + dv:set_selections(idx, line + 1, math.huge) + end + end, + + ["docview:newline-above"] = function(dv) + for idx, line in dv:get_selections(false, true) do + local indent = dv.lines[line]:match("^[\t ]*") + dv.doc:insert(line, 1, indent .. "\n") + dv:set_selections(idx, line, math.huge) + end + end, + + ["docview:delete"] = function(dv) + for idx, line1, col1, line2, col2 in dv:get_selections(true, true) do + if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then + dv.doc:remove(line1, col1, line1, math.huge) + end + dv.doc:delete_to_cursor(idx, translate.next_char) + end + end, + + ["docview:backspace"] = function(dv) + local _, indent_size = dv:get_indent_info() + for idx, line1, col1, line2, col2 in dv:get_selections(true, true) do + if line1 == line2 and col1 == col2 then + local text = dv.doc:get_text(line1, 1, line1, col1) + if #text >= indent_size and text:find("^ *$") then + dv.doc:delete_to_cursor(idx, 0, -indent_size) + goto continue + end + end + dv:delete_to_cursor(idx, translate.previous_char) + ::continue:: + end + end, + + ["docview:select-all"] = function(dv) + dv:set_selection(1, 1, math.huge, math.huge) + -- avoid triggering DocView:scroll_to_make_visible + dv.last_line1 = 1 + dv.last_col1 = 1 + dv.last_line2 = #dv.doc.lines + dv.last_col2 = #dv.doc.lines[#dv.doc.lines] + end, + + ["docview:select-lines"] = function(dv) + for idx, line1, _, line2 in dv.doc:get_selections(true) do + append_line_if_last_line(line2) + dv:set_selections(idx, line2 + 1, 1, line1, 1) + end + end, + + ["docview:select-word"] = function(dv) + for idx, line1, col1 in dv.doc:get_selections(true) do + local line1, col1 = translate.start_of_word(dv.doc, line1, col1) + local line2, col2 = translate.end_of_word(dv.doc, line1, col1) + dv:set_selections(idx, line2, col2, line1, col1) + end + end, + + ["docview:join-lines"] = function(dv) + for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do + if line1 == line2 then line2 = line2 + 1 end + local text = dv.doc:get_text(line1, 1, line2, math.huge) + text = text:gsub("(.-)\n[\t ]*", function(x) + return x:find("^%s*$") and x or x .. " " + end) + dv.doc:insert(line1, 1, text) + dv.doc:remove(line1, #text + 1, line2, math.huge) + if line1 ~= line2 or col1 ~= col2 then + dv:set_selections(idx, line1, math.huge) + end + end + end, + + ["docview:indent"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local l1, c1, l2, c2 = dv:indent_text(false, line1, col1, line2, col2) + if l1 then + dv:set_selections(idx, l1, c1, l2, c2) + end + end + end, + + ["docview:unindent"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local l1, c1, l2, c2 = dv.doc:indent_text(true, line1, col1, line2, col2) + if l1 then + dv:set_selections(idx, l1, c1, l2, c2) + end + end + end, + + ["docview:duplicate-lines"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2) + local text = doc():get_text(line1, 1, line2 + 1, 1) + dv.doc:insert(line2 + 1, 1, text) + local n = line2 - line1 + 1 + dv:set_selections(idx, line1 + n, col1, line2 + n, col2) + end + end, + + ["docview:delete-lines"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2) + dv.doc:remove(line1, 1, line2 + 1, 1) + dv:set_selections(idx, line1, col1) + end + end, + + ["docview:move-lines-up"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2) + if line1 > 1 then + local text = doc().lines[line1 - 1] + dv.doc:insert(line2 + 1, 1, text) + dv.doc:remove(line1 - 1, 1, line1, 1) + dv:set_selections(idx, line1 - 1, col1, line2 - 1, col2) + end + end + end, + + ["docview:move-lines-down"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + append_line_if_last_line(line2 + 1) + if line2 < #dv.doc.lines then + local text = dv.doc.lines[line2 + 1] + dv.doc:remove(line2 + 1, 1, line2 + 2, 1) + dv.doc:insert(line1, 1, text) + dv:set_selections(idx, line1 + 1, col1, line2 + 1, col2) + end + end + end, + + ["docview:toggle-block-comments"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local current_syntax = dv.doc.syntax + if line1 > 1 then + -- Use the previous line state, as it will be the state + -- of the beginning of the current line + local state = dv.doc.highlighter:get_line(line1 - 1).state + local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) + -- Go through all the syntaxes until the first with `block_comment` defined + for _, s in pairs(syntaxes) do + if s.block_comment then + current_syntax = s + break + end + end + end + local comment = current_syntax.block_comment + if not comment then + if dv.doc.syntax.comment then + command.perform "doc:toggle-line-comments" + end + return + end + -- if nothing is selected, toggle the whole line + if line1 == line2 and col1 == col2 then + col1 = 1 + col2 = #dv.doc.lines[line2] + end + dv:set_selections(idx, block_comment(comment, line1, col1, line2, col2)) + end + end, + + ["docview:toggle-line-comments"] = function(dv) + for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do + local current_syntax = dv.doc.syntax + if line1 > 1 then + -- Use the previous line state, as it will be the state + -- of the beginning of the current line + local state = dv.doc.highlighter:get_line(line1 - 1).state + local syntaxes = tokenizer.extract_subsyntaxes(dv.doc.syntax, state) + -- Go through all the syntaxes until the first with comments defined + for _, s in pairs(syntaxes) do + if s.comment or s.block_comment then + current_syntax = s + break + end + end + end + local comment = current_syntax.comment or current_syntax.block_comment + if comment then + dv:set_selections(idx, line_comment(comment, line1, col1, line2, col2)) + end + end + end, + + ["docview:upper-case"] = function(dv) + dv:replace(string.uupper) + end, + + ["docview:lower-case"] = function(dv) + dv:replace(string.ulower) + end, + + ["docview:go-to-line"] = function(dv) + local items + local function init_items() + if items then return end + items = {} + local mt = { __tostring = function(x) return x.text end } + for i, line in ipairs(dv.doc.lines) do + local item = { text = line:sub(1, -2), line = i, info = "line: " .. i } + table.insert(items, setmetatable(item, mt)) + end + end + + core.command_view:enter("Go To Line", { + submit = function(text, item) + local line = item and item.line or tonumber(text) + if not line then + core.error("Invalid line number or unmatched string") + return + end + dv:set_selection(line, 1 ) + dv:scroll_to_line(line, true) + end, + suggest = function(text) + if not text:find("^%d*$") then + init_items() + return common.fuzzy_match(items, text) + end + end + }) + end, + + ["docview:toggle-overwrite"] = function(dv) + dv.overwrite = not dv.overwrite + core.blink_reset() -- to show the cursor has changed edit modes + end, + + ["docview:select-to-cursor"] = function(dv, x, y, clicks) + local line1, col1 = select(3, doc():get_selection()) + local line2, col2 = dv:resolve_screen_position(x, y) + dv.mouse_selecting = { line1, col1, nil } + dv:set_selection(line2, col2, line1, col1) + end, + + ["docview:create-cursor-previous-line"] = function(dv) + split_cursor(-1) + dv:merge_cursors() + end, + + ["docview:create-cursor-next-line"] = function(dv) + split_cursor(1) + dv:merge_cursors() + end + +} + +command.add(function(x, y) + if x == nil or y == nil or not core.active_view:extends(DocView) then return false end + local dv = core.active_view + local x1,y1,x2,y2 = dv.position.x, dv.position.y, dv.position.x + dv.size.x, dv.position.y + dv.size.y + return x >= x1 + dv:get_gutter_width() and x < x2 and y >= y1 and y < y2, dv, x, y +end, { + ["docview:set-cursor"] = function(dv, x, y) + set_cursor(dv, x, y, "set") + end, + + ["docview:set-cursor-word"] = function(dv, x, y) + set_cursor(dv, x, y, "word") + end, + + ["docview:set-cursor-line"] = function(dv, x, y, clicks) + set_cursor(dv, x, y, "lines") + end, + + ["docview:split-cursor"] = function(dv, x, y, clicks) + local line, col = dv:resolve_screen_position(x, y) + local removal_target = nil + for idx, line1, col1 in dv.doc:get_selections(true) do + if line1 == line and col1 == col and #doc().selections > 4 then + removal_target = idx + end + end + if removal_target then + dv.doc:remove_selection(removal_target) + else + dv.doc:add_selection(line, col, line, col) + end + dv.mouse_selecting = { line, col, "set" } + end +}) + +local translations = { + ["previous-char"] = translate, + ["next-char"] = translate, + ["previous-word-start"] = translate, + ["next-word-end"] = translate, + ["previous-block-start"] = translate, + ["next-block-end"] = translate, + ["start-of-doc"] = translate, + ["end-of-doc"] = translate, + ["start-of-line"] = translate, + ["end-of-line"] = translate, + ["start-of-word"] = translate, + ["start-of-indentation"] = translate, + ["end-of-word"] = translate, + ["previous-line"] = DocView.translate, + ["next-line"] = DocView.translate, + ["previous-page"] = DocView.translate, + ["next-page"] = DocView.translate, +} + +for name, obj in pairs(translations) do + commands["docview:move-to-" .. name] = function(dv) dv:move_to(obj[name:gsub("-", "_")], dv) end + commands["docview:select-to-" .. name] = function(dv) dv:select_to(obj[name:gsub("-", "_")], dv) end + commands["docview:delete-to-" .. name] = function(dv) dv:delete_to(obj[name:gsub("-", "_")], dv) end +end + +commands["docview:move-to-previous-char"] = function(dv) + for idx, line1, col1, line2, col2 in dv:get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + dv:set_selections(idx, line1, col1) + else + dv:move_to_cursor(idx, translate.previous_char) + end + end + dv:merge_cursors() +end + +commands["docview:move-to-next-char"] = function(dv) + for idx, line1, col1, line2, col2 in dv:get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + dv:set_selections(idx, line2, col2) + else + dv:move_to_cursor(idx, translate.next_char) + end + end + dv:merge_cursors() +end + +command.add("core.docview", commands) diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index b77bbcf22..05a93ca61 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -13,6 +13,11 @@ local case_sensitive = config.find_case_sensitive or false local find_regex = config.find_regex or false local found_expression +local function dv() + local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView) + return is_DocView and core.active_view or last_view +end + local function doc() local is_DocView = core.active_view:is(DocView) and not core.active_view:is(CommandView) return is_DocView and core.active_view.doc or (last_view and last_view.doc) @@ -57,7 +62,7 @@ end local function find(label, search_fn) last_view, last_sel = core.active_view, - { core.active_view.doc:get_selection() } + { core.active_view:get_selection() } local text = last_view.doc:get_text(table.unpack(last_sel)) found_expression = false @@ -140,7 +145,7 @@ end local function has_unique_selection() if not doc() then return false end local text = nil - for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do + for idx, line1, col1, line2, col2 in dv():get_selections(true, true) do if line1 == line2 and col1 == col2 then return false end local selection = doc():get_text(line1, col1, line2, col2) if text ~= nil and text ~= selection then return false end @@ -157,7 +162,7 @@ local function is_in_selection(line, col, l1, c1, l2, c2) end local function is_in_any_selection(line, col) - for idx, l1, c1, l2, c2 in doc():get_selections(true, false) do + for idx, l1, c1, l2, c2 in dv():get_selections(true, false) do if is_in_selection(line, col, l1, c1, l2, c2) then return true end end return false @@ -165,7 +170,7 @@ end local function select_add_next(all) local il1, ic1 - for _, l1, c1, l2, c2 in doc():get_selections(true, true) do + for _, l1, c1, l2, c2 in dv():get_selections(true, true) do if not il1 then il1, ic1 = l1, c1 end @@ -186,7 +191,7 @@ local function select_add_next(all) end local function select_next(reverse) - local l1, c1, l2, c2 = doc():get_selection(true) + local l1, c1, l2, c2 = dv():get_selection(true) local text = doc():get_text(l1, c1, l2, c2) if reverse then l1, c1, l2, c2 = search.find(doc(), l1, c1, text, { wrap = true, reverse = true }) @@ -198,7 +203,7 @@ end ---@param in_selection? boolean whether to replace in the selections only, or in the whole file. local function find_replace(in_selection) - local l1, c1, l2, c2 = doc():get_selection() + local l1, c1, l2, c2 = dv():get_selection() local selected_text = "" if not in_selection then selected_text = doc():get_text(l1, c1, l2, c2) @@ -239,7 +244,7 @@ command.add("core.docview!", { ["find-replace:replace-symbol"] = function() local first = "" if doc():has_selection() then - local text = doc():get_text(doc():get_selection()) + local text = doc():get_text(dv():get_selection()) first = text:match(config.symbol_pattern) or "" end replace("Symbol", first, function(text, old, new) @@ -268,7 +273,7 @@ command.add(valid_for_finding, { if not last_fn then core.error("No find to continue from") else - local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true) + local sl1, sc1, sl2, sc2 = dv:get_selection(true) local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false) if line1 then dv.doc:set_selection(line2, col2, line1, col1) @@ -283,7 +288,7 @@ command.add(valid_for_finding, { if not last_fn then core.error("No find to continue from") else - local sl1, sc1, sl2, sc2 = dv.doc:get_selection(true) + local sl1, sc1, sl2, sc2 = dv:get_selection(true) local line1, col1, line2, col2 = last_fn(dv.doc, sl1, sc1, last_text, case_sensitive, find_regex, true) if line1 then dv.doc:set_selection(line2, col2, line1, col1) diff --git a/data/core/common.lua b/data/core/common.lua index 82f1ab6fc..23f1fd033 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -716,5 +716,12 @@ function common.rm(path, recursively) return true end +function common.sort_positions(line1, col1, line2, col2) + if line1 > line2 or line1 == line2 and col1 > col2 then + return line2, col2, line1, col1, true + end + return line1, col1, line2, col2, false +end + return common diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 6f6fb272f..9c5878ff1 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -36,8 +36,6 @@ end function Doc:reset() self.lines = { "\n" } - self.selections = { 1, 1, 1, 1 } - self.last_selection = 1 self.undo_stack = { idx = 1 } self.redo_stack = { idx = 1 } self.clean_change_id = 1 @@ -195,7 +193,7 @@ end function Doc:get_text(line1, col1, line2, col2) line1, col1 = self:sanitize_position(line1, col1) line2, col2 = self:sanitize_position(line2, col2) - line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2) + line1, col1, line2, col2 = common.sort_positions(line1, col1, line2, col2) if line1 == line2 then return self.lines[line1]:sub(col1, col2 - 1) end @@ -212,6 +210,7 @@ function Doc:get_char(line, col) return self.lines[line]:sub(col, col) end + local function push_undo(undo_stack, time, type, ...) undo_stack[undo_stack.idx] = { type = type, time = time, ... } undo_stack[undo_stack.idx - config.max_undos] = nil @@ -232,8 +231,6 @@ local function pop_undo(self, undo_stack, redo_stack, modified) elseif cmd.type == "remove" then local line1, col1, line2, col2 = table.unpack(cmd) self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) - elseif cmd.type == "selection" then - self.selections = { table.unpack(cmd) } end modified = modified or (cmd.type ~= "selection") @@ -245,12 +242,11 @@ local function pop_undo(self, undo_stack, redo_stack, modified) return pop_undo(self, undo_stack, redo_stack, modified) end - if modified then - self:on_text_change("undo") - end + if modified then for i,v in ipairs(self.listeners) do v("undo") end end end + function Doc:raw_insert(line, col, text, undo_stack, time) -- split text into lines and merge with line at insertion point local lines = split_lines(text) @@ -272,8 +268,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time) push_undo(undo_stack, time, "remove", line, col, line2, col2) -- update highlighter and assure selection is in bounds - self.highlighter:insert_notify(line, #lines - 1) - for i,v in ipairs(listeners) do v(text, line, col, line, col) end + for i,v in ipairs(self.listeners) do v("insert", text, line, col, line, col) end end function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) @@ -293,10 +288,10 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) local merge = false - -- update highlighter and assure selection is in bounds - self.highlighter:remove_notify(line1, line_removal) + for i,v in ipairs(self.listeners) do v("remove", "", line1, col1, line2, col2) end end + function Doc:insert(line, col, text) self.redo_stack = { idx = 1 } -- Reset the clean id when we're pushing something new before it @@ -304,26 +299,18 @@ function Doc:insert(line, col, text) self.clean_change_id = -1 end line, col = self:sanitize_position(line, col) - self:raw_insert(line, col, text, self.undo_stack, system.get_time()) - self:on_text_change("insert") + self:insert(line, col, text, self.undo_stack, system.get_time()) end function Doc:remove(line1, col1, line2, col2) self.redo_stack = { idx = 1 } line1, col1 = self:sanitize_position(line1, col1) line2, col2 = self:sanitize_position(line2, col2) - line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2) - self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time()) - self:on_text_change("remove") + line1, col1, line2, col2 = common.sort_positions(line1, col1, line2, col2) + self:remove(line1, col1, line2, col2, self.undo_stack, system.get_time()) end -function Doc:undo() - pop_undo(self, self.undo_stack, self.redo_stack, false) -end -function Doc:redo() - pop_undo(self, self.redo_stack, self.undo_stack, false) -end function Doc:get_indent_string() @@ -336,8 +323,92 @@ end --- For plugins to add custom actions of document change -function Doc:on_text_change(type) +function Doc:line_comment(comment, line1, col1, line2, col2) + local start_comment = (type(comment) == 'table' and comment[1] or comment) .. " " + local end_comment = (type(comment) == 'table' and " " .. comment[2]) + local uncomment = true + local start_offset = math.huge + for line = line1, line2 do + local text = self.lines[line] + local s = text:find("%S") + if s then + local cs, ce = text:find(start_comment, s, true) + if cs ~= s then + uncomment = false + end + start_offset = math.min(start_offset, s) + end + end + + local end_line = col2 == #self.lines[line2] + for line = line1, line2 do + local text = self.lines[line] + local s = text:find("%S") + if s and uncomment then + if end_comment and text:sub(#text - #end_comment, #text - 1) == end_comment then + self:remove(line, #text - #end_comment, line, #text) + end + local cs, ce = text:find(start_comment, s, true) + if ce then + self:remove(line, cs, line, ce + 1) + end + elseif s then + self:insert(line, start_offset, start_comment) + if end_comment then + self:insert(line, #doc().lines[line], " " .. comment[2]) + end + end + end + col1 = col1 + (col1 > start_offset and #start_comment or 0) * (uncomment and -1 or 1) + col2 = col2 + (col2 > start_offset and #start_comment or 0) * (uncomment and -1 or 1) + if end_comment and end_line then + col2 = col2 + #end_comment * (uncomment and -1 or 1) + end + return line1, col1, line2, col2 +end + + +function Doc:block_comment(comment, line1, col1, line2, col2) + -- automatically skip spaces + local word_start = self:get_text(line1, col1, line1, math.huge):find("%S") + local word_end = self:get_text(line2, 1, line2, col2):find("%s*$") + col1 = col1 + (word_start and (word_start - 1) or 0) + col2 = word_end and word_end or col2 + + local block_start = self:get_text(line1, col1, line1, col1 + #comment[1]) + local block_end = self:get_text(line2, col2 - #comment[2], line2, col2) + + if block_start == comment[1] and block_end == comment[2] then + -- remove up to 1 whitespace after the comment + local start_len, stop_len = #comment[1], #comment[2] + if self:get_text(line1, col1 + #comment[1], line1, col1 + #comment[1] + 1):find("%s$") then + start_len = start_len + 1 + end + if self:get_text(line2, col2 - #comment[2] - 1, line2, col2):find("^%s") then + stop_len = stop_len + 1 + end + + self:remove(line1, col1, line1, col1 + start_len) + col2 = col2 - (line1 == line2 and start_len or 0) + self:remove(line2, col2 - stop_len, line2, col2) + + return line1, col1, line2, col2 - stop_len + else + self:insert(line1, col1, comment[1] .. " ") + col2 = col2 + (line1 == line2 and (#comment[1] + 1) or 0) + self:insert(line2, col2, " " .. comment[2]) + + return line1, col1, line2, col2 + #comment[2] + 1 + end +end + + +function Doc:undo() + pop_undo(self, self.undo_stack, self.redo_stack, false) +end + +function Doc:redo() + pop_undo(self, self.redo_stack, self.undo_stack, false) end -- For plugins to get notified when a document is closed diff --git a/data/core/docview.lua b/data/core/docview.lua index 6239c0215..5e1fe8406 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -26,7 +26,7 @@ end DocView.translate = { ["previous_page"] = function(doc, line, col, dv) - local min, max = dv:get_visible_line_range() + local min, max = dv:get_visible_virutal_line_range() return line - (max - min), 1 end, @@ -34,7 +34,7 @@ DocView.translate = { if line == #doc.lines then return #doc.lines, #doc.lines[line] end - local min, max = dv:get_visible_line_range() + local min, max = dv:get_visible_virutal_line_range() return line + (max - min), 1 end, @@ -64,21 +64,17 @@ function DocView:new(doc) self.ime_selection = { from = 0, size = 0 } self.ime_status = false self.hovering_gutter = false - table.insert(doc.listeners, function(...) self:listener(...) end) - self.lines = {} - self.selections = {} + self.tokens = {} + self.vcache = {} + self.dcache = {} + self.vtodcache = {} + self.selections = { 1, 1, 1, 1 } + self.last_selection = 1 self.v_scrollbar:set_forced_status(config.force_scrollbar_status) self.h_scrollbar:set_forced_status(config.force_scrollbar_status) + table.insert(doc.listeners, function(...) self:listener(...) end) end -local function sort_positions(line1, col1, line2, col2) - if line1 > line2 or line1 == line2 and col1 > col2 then - return line2, col2, line1, col1, true - end - return line1, col1, line2, col2, false -end - - -- Cursor section. Cursor indices are *only* valid during a get_selections() call. -- Cursors will always be iterated in order from top to bottom. Through normal operation -- curors can never swap positions; only merge or split, or change their position in cursor @@ -100,7 +96,7 @@ function DocView:get_selection_idx(idx, sort) self.selections[idx * 4 - 1], self.selections[idx * 4] if line1 and sort then - return sort_positions(line1, col1, line2, col2) + return common.sort_positions(line1, col1, line2, col2) else return line1, col1, line2, col2 end @@ -135,13 +131,13 @@ end function DocView:set_selections(idx, line1, col1, line2, col2, swap, rm) assert(not line2 == not col2, "expected 3 or 5 arguments") if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end - line1, col1 = self:sanitize_position(line1, col1) - line2, col2 = self:sanitize_position(line2 or line1, col2 or col1) + line1, col1 = self.doc:sanitize_position(line1, col1) + line2, col2 = self.doc:sanitize_position(line2 or line1, col2 or col1) common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 }) end function DocView:add_selection(line1, col1, line2, col2, swap) - local l1, c1 = sort_positions(line1, col1, line2 or line1, col2 or col1) + local l1, c1 = common.sort_positions(line1, col1, line2 or line1, col2 or col1) local target = #self.selections / 4 + 1 for idx, tl1, tc1 in self:get_selections(true) do if l1 < tl1 or l1 == tl1 and c1 < tc1 then @@ -185,7 +181,7 @@ local function selection_iterator(invariant, idx) local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1) if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if invariant[2] then - return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4)) + return idx + (invariant[3] and -1 or 1), common.sort_positions(table.unpack(invariant[1], target, target + 4)) else return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4) end @@ -218,24 +214,23 @@ function DocView:text_input(text, idx) and not had_selection and col1 < #self.lines[line1] and text:ulen() == 1 then - self:remove(line1, col1, translate.next_char(self, line1, col1)) + self.doc:remove(line1, col1, translate.next_char(self.doc, line1, col1)) end - self:insert(line1, col1, text) + self.doc:insert(line1, col1, text) self:move_to_cursor(sidx, #text) end end -function DocView:listener(text, line1, col1, line2, col2) - +function DocView:listener(type, text, line1, col1, line2, col2) -- keep cursors where they should be on insertion - for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do - if cline1 < line then break end - local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 - local column_addition = line == cline1 and ccol1 > col and len or 0 - self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, - ccol2 + column_addition) - end + -- for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + -- if cline1 < line then break end + -- local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 + -- local column_addition = line == cline1 and ccol1 > col and len or 0 + -- self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, + -- ccol2 + column_addition) + -- end -- keep selections in correct positions on removal: each pair (line, col) @@ -243,35 +238,35 @@ function DocView:listener(text, line1, col1, line2, col2) -- * is set to (line1, col1) if in the deleted text -- * is set to (line1, col - col_removal) if on line2 but out of the deleted text -- * is set to (line - line_removal, col) if after line2 - for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do - if cline2 < line1 then break end - local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 - - if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then - if cline1 > line2 then - l1 = l1 - line_removal - else - l1 = line1 - c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1 - end - end - - if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then - if cline2 > line2 then - l2 = l2 - line_removal - else - l2 = line1 - c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1 - end - end - - if l1 == line1 and c1 == col1 then merge = true end - self:set_selections(idx, l1, c1, l2, c2) - end - - if merge then - self:merge_cursors() - end + -- for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + -- if cline2 < line1 then break end + -- local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 + + -- if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then + -- if cline1 > line2 then + -- l1 = l1 - line_removal + -- else + -- l1 = line1 + -- c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1 + -- end + -- end + + -- if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then + -- if cline2 > line2 then + -- l2 = l2 - line_removal + -- else + -- l2 = line1 + -- c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1 + -- end + -- end + + -- if l1 == line1 and c1 == col1 then merge = true end + -- self:set_selections(idx, l1, c1, l2, c2) + -- end + + -- if merge then + -- self:merge_cursors() + -- end end function DocView:try_close(do_close) @@ -311,13 +306,13 @@ function DocView:ime_text_editing(text, start, length, idx) end function DocView:replace_cursor(idx, line1, col1, line2, col2, fn) - local old_text = self:get_text(line1, col1, line2, col2) + local old_text = self.doc:get_text(line1, col1, line2, col2) local new_text, res = fn(old_text) if old_text ~= new_text then self:insert(line2, col2, new_text) self:remove(line1, col1, line2, col2) if line1 == line2 and col1 == col2 then - line2, col2 = self:position_offset(line1, col1, #new_text) + line2, col2 = self.doc:position_offset(line1, col1, #new_text) self:set_selections(idx, line1, col1, line2, col2) end end @@ -342,11 +337,11 @@ end function DocView:delete_to_cursor(idx, ...) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do if line1 ~= line2 or col1 ~= col2 then - self:remove(line1, col1, line2, col2) + self.doc:remove(line1, col1, line2, col2) else - local l2, c2 = self:position_offset(line1, col1, ...) - self:remove(line1, col1, l2, c2) - line1, col1 = sort_positions(line1, col1, l2, c2) + local l2, c2 = self.doc:position_offset(line1, col1, ...) + self.doc:remove(line1, col1, l2, c2) + line1, col1 = common.sort_positions(line1, col1, l2, c2) end self:set_selections(sidx, line1, col1) end @@ -357,7 +352,7 @@ function DocView:delete_to(...) return self:delete_to_cursor(nil, ...) end function DocView:move_to_cursor(idx, ...) for sidx, line, col in self:get_selections(false, idx) do - self:set_selections(sidx, self:position_offset(line, col, ...)) + self:set_selections(sidx, self.doc:position_offset(line, col, ...)) end self:merge_cursors(idx) end @@ -366,7 +361,7 @@ function DocView:move_to(...) return self:move_to_cursor(nil, ...) end function DocView:select_to_cursor(idx, ...) for sidx, line, col, line2, col2 in self:get_selections(false, idx) do - line, col = self:position_offset(line, col, ...) + line, col = self.doc:position_offset(line, col, ...) self:set_selections(sidx, line, col, line2, col2) end self:merge_cursors(idx) @@ -378,7 +373,7 @@ function DocView:select_to(...) return self:select_to_cursor(nil, ...) end -- in your config format, rounded either up or down function DocView:get_line_indent(line, rnd_up) local _, e = line:find("^[ \t]+") - local indent_type, indent_size = self:get_indent_info() + local indent_type, indent_size = self.doc:get_indent_info() local soft_tab = string.rep(" ", indent_size) if indent_type == "hard" then local indent = e and line:sub(1, e):gsub(soft_tab, "\t") or "" @@ -403,28 +398,28 @@ end -- * if you are unindenting, the cursor will jump to the start of the line, -- and remove the appropriate amount of spaces (or a tab). function DocView:indent_text(unindent, line1, col1, line2, col2) - local text = self:get_indent_string() - local _, se = self.lines[line1]:find("^[ \t]+") + local text = self.doc:get_indent_string() + local _, se = self.doc.lines[line1]:find("^[ \t]+") local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1) local has_selection = line1 ~= line2 or col1 ~= col2 if unindent or has_selection or in_beginning_whitespace then - local l1d, l2d = #self.lines[line1], #self.lines[line2] + local l1d, l2d = #self.doc.lines[line1], #self.doc.lines[line2] for line = line1, line2 do - if not has_selection or #self.lines[line] > 1 then -- don't indent empty lines in a selection - local e, rnded = self:get_line_indent(self.lines[line], unindent) - self:remove(line, 1, line, (e or 0) + 1) - self:insert(line, 1, + if not has_selection or #self.doc.lines[line] > 1 then -- don't indent empty lines in a selection + local e, rnded = self:get_line_indent(self.doc.lines[line], unindent) + self.doc:remove(line, 1, line, (e or 0) + 1) + self.doc:insert(line, 1, unindent and rnded:sub(1, #rnded - #text) or rnded .. text) end end - l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d + l1d, l2d = #self.doc.lines[line1] - l1d, #self.doc.lines[line2] - l2d if (unindent or in_beginning_whitespace) and not has_selection then local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1]) return line1, start_cursor, line2, start_cursor end return line1, col1 + l1d, line2, col2 + l2d end - self:insert(line1, col1, text) + self.doc:insert(line1, col1, text) return line1, col1 + #text, line1, col1 + #text end @@ -493,7 +488,7 @@ function DocView:get_line_text_y_offset() end -function DocView:get_visible_line_range() +function DocView:get_visible_virutal_line_range() local x, y, x2, y2 = self:get_content_bounds() local lh = self:get_line_height() local minline = math.max(1, math.floor((y - style.padding.y) / lh) + 1) @@ -508,8 +503,8 @@ function DocView:get_col_x_offset(line, col) default_font:set_tab_size(indent_size) local column = 1 local xoffset = 0 - for _, type, text in self.doc.highlighter:each_token(line) do - local font = style.syntax_fonts[type] or default_font + for _, text, style in self:each_text_token(line) do + local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end local length = #text if column + length <= col then @@ -540,8 +535,8 @@ function DocView:get_x_offset_col(line, x) local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) - for _, type, text in self.doc.highlighter:each_token(line) do - local font = style.syntax_fonts[type] or default_font + for _, text, style in self:each_text_token(line) do + local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end local width = font:get_width(text) -- Don't take the shortcut if the width matches x, @@ -576,7 +571,7 @@ end function DocView:scroll_to_line(line, ignore_if_visible, instant) - local min, max = self:get_visible_line_range() + local min, max = self:get_visible_virutal_line_range() if not (ignore_if_visible and line > min and line < max) then local x, y = self:get_line_screen_position(line) local ox, oy = self:get_content_offset() @@ -636,13 +631,13 @@ function DocView:on_mouse_moved(x, y, ...) if l1 > l2 then l1, l2 = l2, l1 end self.doc.selections = { } for i = l1, l2 do - self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i])) + self:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i])) end else if snap_type then l1, c1, l2, c2 = self:mouse_selection(self.doc, snap_type, l1, c1, l2, c2) end - self.doc:set_selection(l1, c1, l2, c2) + self:set_selection(l1, c1, l2, c2) end end end @@ -672,17 +667,17 @@ function DocView:on_mouse_pressed(button, x, y, clicks) end local line = self:resolve_screen_position(x, y) if keymap.modkeys["shift"] then - local sline, scol, sline2, scol2 = self.doc:get_selection(true) + local sline, scol, sline2, scol2 = self:get_selection(true) if line > sline then - self.doc:set_selection(sline, 1, line, #self.doc.lines[line]) + self:set_selection(sline, 1, line, #self.doc.lines[line]) else - self.doc:set_selection(line, 1, sline2, #self.doc.lines[sline2]) + self:set_selection(line, 1, sline2, #self.doc.lines[sline2]) end else if clicks == 1 then - self.doc:set_selection(line, 1, line, 1) + self:set_selection(line, 1, line, 1) elseif clicks == 2 then - self.doc:set_selection(line, 1, line, #self.doc.lines[line]) + self:set_selection(line, 1, line, #self.doc.lines[line]) end end return true @@ -696,7 +691,7 @@ end function DocView:on_text_input(text) - self.doc:text_input(text) + self:text_input(text) end function DocView:on_ime_text_editing(text, start, length) @@ -707,7 +702,7 @@ function DocView:on_ime_text_editing(text, start, length) -- Set the composition bounding box that the system IME -- will consider when drawing its interface - local line1, col1, line2, col2 = self.doc:get_selection(true) + local line1, col1, line2, col2 = self:get_selection(true) local col = math.min(col1, col2) self:update_ime_location() self:scroll_to_make_visible(line1, col + start) @@ -718,7 +713,7 @@ end function DocView:update_ime_location() if not self.ime_status then return end - local line1, col1, line2, col2 = self.doc:get_selection(true) + local line1, col1, line2, col2 = self:get_selection(true) local x, y = self:get_line_screen_position(line1) local h = self:get_line_height() local col = math.min(col1, col2) @@ -742,7 +737,7 @@ end function DocView:update() -- scroll to make caret visible and reset blink timer if it moved - local line1, col1, line2, col2 = self.doc:get_selection() + local line1, col1, line2, col2 = self:get_selection() if (line1 ~= self.last_line1 or col1 ~= self.last_col1 or line2 ~= self.last_line2 or col2 ~= self.last_col2) and self.size.x > 0 then if core.active_view == self and not ime.editing then @@ -775,21 +770,13 @@ function DocView:draw_line_highlight(x, y) end -function DocView:draw_line_text(line, x, y) +local default_color = { common.color "#FFFFFF" } +function DocView:draw_line_text(vline, x, y) local default_font = self:get_font() local tx, ty = x, y + self:get_line_text_y_offset() - local last_token = nil - local tokens = self.doc.highlighter:get_line(line).tokens - local tokens_count = #tokens - if string.sub(tokens[tokens_count], -1) == "\n" then - last_token = tokens_count - 1 - end - for tidx, type, text in self.doc.highlighter:each_token(line) do - local color = style.syntax[type] - local font = style.syntax_fonts[type] or default_font - -- do not render newline, fixes issue #1164 - if tidx == last_token then text = text:sub(1, -2) end - tx = renderer.draw_text(font, text, tx, ty, color) + for tidx, text, style in self:each_text_token(vline) do + local font = style.font or default_font + tx = renderer.draw_text(font, text, tx, ty, style.color or default_color) if tx > self.position.x + self.size.x then break end end return self:get_line_height() @@ -807,13 +794,13 @@ function DocView:draw_caret(x, y) renderer.draw_rect(x, y, style.caret_width, lh, style.caret) end -function DocView:draw_line_body(line, x, y) +function DocView:draw_line_body(vline, x, y) -- draw highlight if any selection ends on this line local draw_highlight = false local hcl = config.highlight_current_line if hcl ~= false then - for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do - if line1 == line then + for lidx, line1, col1, line2, col2 in self:get_selections(false) do + if line1 == vline then if hcl == "no_selection" then if (line1 ~= line2) or (col1 ~= col2) then draw_highlight = false @@ -831,13 +818,13 @@ function DocView:draw_line_body(line, x, y) -- draw selection if it overlaps this line local lh = self:get_line_height() - for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do - if line >= line1 and line <= line2 then - local text = self.doc.lines[line] - if line1 ~= line then col1 = 1 end - if line2 ~= line then col2 = #text + 1 end - local x1 = x + self:get_col_x_offset(line, col1) - local x2 = x + self:get_col_x_offset(line, col2) + for lidx, line1, col1, line2, col2 in self:get_selections(true) do + if vline >= line1 and vline <= line2 then + local text = self.doc.lines[vline] + if line1 ~= vline then col1 = 1 end + if line2 ~= vline then col2 = #text + 1 end + local x1 = x + self:get_col_x_offset(vline, col1) + local x2 = x + self:get_col_x_offset(vline, col2) if x1 ~= x2 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end @@ -845,21 +832,21 @@ function DocView:draw_line_body(line, x, y) end -- draw line's text - return self:draw_line_text(line, x, y) + return self:draw_line_text(vline, x, y) end -function DocView:draw_line_gutter(line, x, y, width) +function DocView:draw_line_gutter(vline, x, y, width) local color = style.line_number - for _, line1, _, line2 in self.doc:get_selections(true) do - if line >= line1 and line <= line2 then + for _, line1, _, line2 in self:get_selections(true) do + if vline >= line1 and vline <= line2 then color = style.line_number2 break end end x = x + style.padding.x local lh = self:get_line_height() - common.draw_text(self:get_font(), color, line, "right", x, y, width, lh) + common.draw_text(self:get_font(), color, self.vtodcache[vline] or vline, "right", x, y, width, lh) return lh end @@ -890,10 +877,10 @@ end function DocView:draw_overlay() if core.active_view == self then - local minline, maxline = self:get_visible_line_range() + local minline, maxline = self:get_visible_virutal_line_range() -- draw caret if it overlaps this line local T = config.blink_period - for _, line1, col1, line2, col2 in self.doc:get_selections() do + for _, line1, col1, line2, col2 in self:get_selections() do if line1 >= minline and line1 <= maxline and system.window_has_focus() then if ime.editing then @@ -919,13 +906,15 @@ function DocView:draw() local _, indent_size = self.doc:get_indent_info() self:get_font():set_tab_size(indent_size) - local minline, maxline = self:get_visible_line_range() + local minline, maxline = self:get_visible_virutal_line_range() local lh = self:get_line_height() local x, y = self:get_line_screen_position(minline) local gw, gpad = self:get_gutter_width() for i = minline, maxline do - y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh) + if self:has_tokens(i) then + y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh) + end end local pos = self.position @@ -943,4 +932,69 @@ function DocView:draw() end +-- Transform function for lines from the doc. +-- Plugins hook this to return a line/col list from `doc`, or provide a virtual line. +-- `{ "doc", doc_line, 1, #self.doc.lines[doc_line], style }` +-- `{ "virtual", text, false, false, style } +function DocView:transform(doc_line) + return { "doc", doc_line, 1, #self.doc.lines[doc_line] - 1, {} } +end + +--[[ +self.vcache maps virtual line numbers to the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. +self.dcache maps doc line number sto the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. +self.vtodcache maps the virtual line number to the relevant doc line number; not foundational to the algorithm; purely cosmetic. +self.tokens contains the stream of transformed tokens. +Each token can contain *at most* one new line, at the end of it. +]] + +function DocView:invalidate_cache(start_doc_line) + while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end + while #self.dcache >= start_doc_line do table.remove(self.dcache) end + while self.vcache[#self.vcache] > #self.tokens do table.remove(self.vcache) end +end + +function DocView:get_token_text(type, doc_line, col_start, col_end) + return type == "doc" and self.doc.lines[doc_line]:sub(col_start, col_end) or doc_line +end + + +local function retrieve_tokens(self, vline) + while vline > #self.vcache and #self.dcache < #self.doc.lines do + local tokens = self:transform(#self.dcache + 1) + local bundles = #tokens / 5 + table.insert(self.dcache, #self.tokens + 1) + if #tokens > 0 then + table.insert(self.vcache, #self.tokens + 1) + self.vtodcache[#self.vcache] = #self.dcache + end + for j = 1, bundles do + local token_idx = (j - 1) * 5 + 1 + local text = self:get_token_text(tokens[token_idx], tokens[token_idx+1], tokens[token_idx+2], tokens[token_idx+3]) + table.move(tokens, token_idx, token_idx+4, #self.tokens + 1, self.tokens) + if text:find("\n$") and j < bundles then + table.insert(self.vcache, #self.tokens + 1) + self.vtodcache[#self.vcache] = #self.dcache + end + end + end + return self.vcache[vline] +end + +local function text_iter(state, idx) + local self, line = table.unpack(state) + if not idx or not self.tokens[idx] or (self.vcache[line + 1] and idx >= self.vcache[line + 1]) then return nil end + local text = self:get_token_text(self.tokens[idx], self.tokens[idx+1], self.tokens[idx+2], self.tokens[idx+3]) + return idx + 5, text, self.tokens[idx+4] +end + +function DocView:each_text_token(vline) + return text_iter, { self, vline }, retrieve_tokens(self, vline) +end + +function DocView:has_tokens(vline) + local token_idx = retrieve_tokens(self, vline) + return token_idx and token_idx < #self.tokens and token_idx ~= retrieve_tokens(self, vline + 1) +end + return DocView diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index 965191ef0..1309f691a 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -47,17 +47,17 @@ local function keymap_macos(keymap) ["cmd+s"] = "doc:save", ["cmd+shift+s"] = "doc:save-as", - ["cmd+z"] = "doc:undo", - ["cmd+y"] = "doc:redo", - ["cmd+x"] = "doc:cut", - ["cmd+c"] = "doc:copy", - ["cmd+v"] = "doc:paste", - ["ctrl+insert"] = "doc:copy", - ["shift+insert"] = "doc:paste", - ["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" }, - ["tab"] = { "command:complete", "doc:indent" }, - ["shift+tab"] = "doc:unindent", - ["backspace"] = "doc:backspace", + ["cmd+z"] = "docview:undo", + ["cmd+y"] = "docview:redo", + ["cmd+x"] = "docview:cut", + ["cmd+c"] = "docview:copy", + ["cmd+v"] = "docview:paste", + ["ctrl+insert"] = "docview:copy", + ["shift+insert"] = "docview:paste", + ["escape"] = { "command:escape", "docview:select-none", "dialog:select-no" }, + ["tab"] = { "command:complete", "docview:indent" }, + ["shift+tab"] = "docview:unindent", + ["backspace"] = "docview:backspace", ["shift+backspace"] = "doc:backspace", ["option+backspace"] = "doc:delete-to-previous-word-start", ["cmd+shift+backspace"] = "doc:delete-to-previous-word-start", diff --git a/data/core/keymap.lua b/data/core/keymap.lua index 9f19cfb73..4db8dd549 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -332,83 +332,83 @@ keymap.add_direct { ["shift+f3"] = "find-replace:previous-find", ["ctrl+i"] = "find-replace:toggle-sensitivity", ["ctrl+shift+i"] = "find-replace:toggle-regex", - ["ctrl+g"] = "doc:go-to-line", + ["ctrl+g"] = "docview:go-to-line", ["ctrl+s"] = "doc:save", ["ctrl+shift+s"] = "doc:save-as", - ["ctrl+z"] = "doc:undo", - ["ctrl+y"] = "doc:redo", - ["ctrl+x"] = "doc:cut", - ["ctrl+c"] = "doc:copy", - ["ctrl+v"] = "doc:paste", - ["insert"] = "doc:toggle-overwrite", - ["ctrl+insert"] = "doc:copy", - ["shift+insert"] = "doc:paste", - ["escape"] = { "command:escape", "doc:select-none", "dialog:select-no" }, - ["tab"] = { "command:complete", "doc:indent" }, - ["shift+tab"] = "doc:unindent", - ["backspace"] = "doc:backspace", - ["shift+backspace"] = "doc:backspace", - ["ctrl+backspace"] = "doc:delete-to-previous-word-start", - ["ctrl+shift+backspace"] = "doc:delete-to-previous-word-start", - ["delete"] = "doc:delete", - ["shift+delete"] = "doc:delete", - ["ctrl+delete"] = "doc:delete-to-next-word-end", - ["ctrl+shift+delete"] = "doc:delete-to-next-word-end", - ["return"] = { "command:submit", "doc:newline", "dialog:select" }, - ["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" }, - ["ctrl+return"] = "doc:newline-below", - ["ctrl+shift+return"] = "doc:newline-above", - ["ctrl+j"] = "doc:join-lines", - ["ctrl+a"] = "doc:select-all", - ["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["ctrl+z"] = "docview:undo", + ["ctrl+y"] = "docview:redo", + ["ctrl+x"] = "docview:cut", + ["ctrl+c"] = "docview:copy", + ["ctrl+v"] = "docview:paste", + ["insert"] = "docview:toggle-overwrite", + ["ctrl+insert"] = "docview:copy", + ["shift+insert"] = "docview:paste", + ["escape"] = { "command:escape", "docview:select-none", "dialog:select-no" }, + ["tab"] = { "command:complete", "docview:indent" }, + ["shift+tab"] = "docview:unindent", + ["backspace"] = "docview:backspace", + ["shift+backspace"] = "docview:backspace", + ["ctrl+backspace"] = "docview:delete-to-previous-word-start", + ["ctrl+shift+backspace"] = "docview:delete-to-previous-word-start", + ["delete"] = "docview:delete", + ["shift+delete"] = "docview:delete", + ["ctrl+delete"] = "docview:delete-to-next-word-end", + ["ctrl+shift+delete"] = "docview:delete-to-next-word-end", + ["return"] = { "command:submit", "docview:newline", "dialog:select" }, + ["keypad enter"] = { "command:submit", "docview:newline", "dialog:select" }, + ["ctrl+return"] = "docview:newline-below", + ["ctrl+shift+return"] = "docview:newline-above", + ["ctrl+j"] = "docview:join-lines", + ["ctrl+a"] = "docview:select-all", + ["ctrl+d"] = { "find-replace:select-add-next", "docview:select-word" }, ["ctrl+f3"] = "find-replace:select-next", ["ctrl+shift+f3"] = "find-replace:select-previous", - ["ctrl+l"] = "doc:select-lines", - ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, - ["ctrl+/"] = "doc:toggle-line-comments", - ["ctrl+shift+/"] = "doc:toggle-block-comments", - ["ctrl+up"] = "doc:move-lines-up", - ["ctrl+down"] = "doc:move-lines-down", - ["ctrl+shift+d"] = "doc:duplicate-lines", - ["ctrl+shift+k"] = "doc:delete-lines", - - ["left"] = { "doc:move-to-previous-char", "dialog:previous-entry" }, - ["right"] = { "doc:move-to-next-char", "dialog:next-entry"}, - ["up"] = { "command:select-previous", "doc:move-to-previous-line" }, - ["down"] = { "command:select-next", "doc:move-to-next-line" }, - ["ctrl+left"] = "doc:move-to-previous-word-start", - ["ctrl+right"] = "doc:move-to-next-word-end", - ["ctrl+["] = "doc:move-to-previous-block-start", - ["ctrl+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-indentation", - ["end"] = "doc:move-to-end-of-line", - ["ctrl+home"] = "doc:move-to-start-of-doc", - ["ctrl+end"] = "doc:move-to-end-of-doc", - ["pageup"] = "doc:move-to-previous-page", - ["pagedown"] = "doc:move-to-next-page", - - ["shift+1lclick"] = "doc:select-to-cursor", - ["ctrl+1lclick"] = "doc:split-cursor", - ["1lclick"] = "doc:set-cursor", - ["2lclick"] = { "doc:set-cursor-word", "emptyview:new-doc", "tabbar:new-doc" }, - ["3lclick"] = "doc:set-cursor-line", - ["shift+left"] = "doc:select-to-previous-char", - ["shift+right"] = "doc:select-to-next-char", - ["shift+up"] = "doc:select-to-previous-line", - ["shift+down"] = "doc:select-to-next-line", - ["ctrl+shift+left"] = "doc:select-to-previous-word-start", - ["ctrl+shift+right"] = "doc:select-to-next-word-end", - ["ctrl+shift+["] = "doc:select-to-previous-block-start", - ["ctrl+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-indentation", - ["shift+end"] = "doc:select-to-end-of-line", - ["ctrl+shift+home"] = "doc:select-to-start-of-doc", - ["ctrl+shift+end"] = "doc:select-to-end-of-doc", - ["shift+pageup"] = "doc:select-to-previous-page", - ["shift+pagedown"] = "doc:select-to-next-page", - ["ctrl+shift+up"] = "doc:create-cursor-previous-line", - ["ctrl+shift+down"] = "doc:create-cursor-next-line" + ["ctrl+l"] = "docview:select-lines", + ["ctrl+shift+l"] = { "find-replace:select-add-all", "docview:select-word" }, + ["ctrl+/"] = "docview:toggle-line-comments", + ["ctrl+shift+/"] = "docview:toggle-block-comments", + ["ctrl+up"] = "docview:move-lines-up", + ["ctrl+down"] = "docview:move-lines-down", + ["ctrl+shift+d"] = "docview:duplicate-lines", + ["ctrl+shift+k"] = "docview:delete-lines", + + ["left"] = { "docview:move-to-previous-char", "dialog:previous-entry" }, + ["right"] = { "docview:move-to-next-char", "dialog:next-entry"}, + ["up"] = { "command:select-previous", "docview:move-to-previous-line" }, + ["down"] = { "command:select-next", "docview:move-to-next-line" }, + ["ctrl+left"] = "docview:move-to-previous-word-start", + ["ctrl+right"] = "docview:move-to-next-word-end", + ["ctrl+["] = "docview:move-to-previous-block-start", + ["ctrl+]"] = "docview:move-to-next-block-end", + ["home"] = "docview:move-to-start-of-indentation", + ["end"] = "docview:move-to-end-of-line", + ["ctrl+home"] = "docview:move-to-start-of-doc", + ["ctrl+end"] = "docview:move-to-end-of-doc", + ["pageup"] = "docview:move-to-previous-page", + ["pagedown"] = "docview:move-to-next-page", + + ["shift+1lclick"] = "docview:select-to-cursor", + ["ctrl+1lclick"] = "docview:split-cursor", + ["1lclick"] = "docview:set-cursor", + ["2lclick"] = { "docview:set-cursor-word", "emptyview:new-doc", "tabbar:new-doc" }, + ["3lclick"] = "docview:set-cursor-line", + ["shift+left"] = "docview:select-to-previous-char", + ["shift+right"] = "docview:select-to-next-char", + ["shift+up"] = "docview:select-to-previous-line", + ["shift+down"] = "docview:select-to-next-line", + ["ctrl+shift+left"] = "docview:select-to-previous-word-start", + ["ctrl+shift+right"] = "docview:select-to-next-word-end", + ["ctrl+shift+["] = "docview:select-to-previous-block-start", + ["ctrl+shift+]"] = "docview:select-to-next-block-end", + ["shift+home"] = "docview:select-to-start-of-indentation", + ["shift+end"] = "docview:select-to-end-of-line", + ["ctrl+shift+home"] = "docview:select-to-start-of-doc", + ["ctrl+shift+end"] = "docview:select-to-end-of-doc", + ["shift+pageup"] = "docview:select-to-previous-page", + ["shift+pagedown"] = "docview:select-to-next-page", + ["ctrl+shift+up"] = "docview:create-cursor-previous-line", + ["ctrl+shift+down"] = "docview:create-cursor-next-line" } return keymap diff --git a/data/core/rootview.lua b/data/core/rootview.lua index ec9d5e0e4..c2ccaa69c 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -108,7 +108,7 @@ function RootView:open_doc(doc) local view = DocView(doc) node:add_view(view) self.root_node:update_layout() - view:scroll_to_line(view.doc:get_selection(), true, true) + view:scroll_to_line(view:get_selection(), true, true) return view end diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 741247680..87e2b7359 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -214,7 +214,7 @@ function StatusView:register_docview_items() alignment = StatusView.Item.LEFT, get_item = function() local dv = core.active_view - local line, col = dv.doc:get_selection() + local line, col = dv:get_selection() local _, indent_size = dv.doc:get_indent_info() -- Calculating tabs when the doc is using the "hard" indent type. local ntabs = 0 @@ -245,7 +245,7 @@ function StatusView:register_docview_items() alignment = StatusView.Item.LEFT, get_item = function() local dv = core.active_view - local line = dv.doc:get_selection() + local line = dv:get_selection() return { string.format("%.f%%", line / #dv.doc.lines * 100) } @@ -259,7 +259,7 @@ function StatusView:register_docview_items() alignment = StatusView.Item.LEFT, get_item = function() local dv = core.active_view - local nsel = math.floor(#dv.doc.selections / 4) + local nsel = math.floor(#dv.selections / 4) if nsel > 1 then return { style.text, nsel, " selections" } end diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index 98b2dcd04..bb38a9c09 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -373,7 +373,7 @@ end local function get_partial_symbol() local doc = core.active_view.doc - local line2, col2 = doc:get_selection() + local line2, col2 = core.active_view:get_selection() local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word) return doc:get_text(line1, col1, line2, col2) end @@ -389,7 +389,7 @@ local function get_suggestions_rect(av) return 0, 0, 0, 0 end - local line, col = av.doc:get_selection() + local line, col = av:get_selection() local x, y = av:get_line_screen_position(line, col - #partial) y = y + av:get_line_height() + style.padding.y local font = av:get_font() @@ -654,9 +654,9 @@ local function show_autocomplete() update_suggestions() if not triggered_manually then - last_line, last_col = av.doc:get_selection() + last_line, last_col = av:get_selection() else - local line, col = av.doc:get_selection() + local line, col = av:get_selection() local char = av.doc:get_char(line, col-1, line, col-1) if char:match("%s") or (char:match("%p") and col ~= last_col) then @@ -707,7 +707,7 @@ RootView.update = function(...) local av = get_active_view() if av then -- reset suggestions if caret was moved - local line, col = av.doc:get_selection() + local line, col = av:get_selection() if not triggered_manually then if line ~= last_line or col ~= last_col then @@ -819,7 +819,7 @@ command.add(predicate, { local current_partial = get_partial_symbol() local sz = #current_partial - for idx, line1, col1, line2, col2 in doc:get_selections(true) do + for idx, line1, col1, line2, col2 in dv:get_selections(true) do local n = col1 - 1 local line = doc.lines[line1] for i = 1, sz + 1 do diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua new file mode 100644 index 000000000..38d5cae9c --- /dev/null +++ b/data/plugins/codefolding.lua @@ -0,0 +1,132 @@ +-- mod-version:3 +local core = require "core" +local config = require "core.config" +local style = require "core.style" +local Doc = require "core.doc" +local DocView = require "core.docview" +local Node = require "core.node" +local common = require "core.common" + +local old_transform = DocView.transform + +function DocView:is_folded(doc_line) + return self.folded[doc_line+1] +end + +local old_docview_new = DocView.new +function DocView:new(...) + self.folded = {} + self.foldable = {} + return old_docview_new(self, ...) +end + +function DocView:compute_fold(doc_line) + local start_of_computation = doc_line + for i = doc_line - 1, 1, -1 do + if self.foldable[i] then break end + start_of_computation = i + end + for i = start_of_computation, doc_line do + if i > 1 then + local origin = self.foldable[i - 1] + if self.doc.lines[i-1]:find("{%s*$") then + origin = origin + 1 + elseif self.doc.lines[i-1]:find("}%s*$") and not self.doc.lines[i-1]:find("^%s* }%s*$") then + origin = origin - 1 + end + if self.doc.lines[i]:find("^%s*}") then + origin = origin - 1 + end + self.foldable[i] = origin + else + self.foldable[i] = 0 + end + end +end + +function DocView:transform(doc_line) + local results = old_transform(self, doc_line) + self:compute_fold(doc_line) + if self.folded[doc_line] then return {} end + if self:is_foldable(doc_line) and self.folded[doc_line+1] then + -- remove the newline from the end of the tokens + table.insert(results, "virtual") + table.insert(results, " ... ") + table.insert(results, false) + table.insert(results, false) + table.insert(results, { color = style.dim }) + table.insert(results, "virtual") + table.insert(results, "}") + table.insert(results, false) + table.insert(results, false) + table.insert(results, { }) + end + return results +end + +function DocView:is_foldable(doc_line) + if doc_line < #self.doc.lines then + if not self.foldable[doc_line] or not self.foldable[doc_line+1] then self:compute_fold(doc_line+1) end + return self.foldable[doc_line] and self.foldable[doc_line+1] > self.foldable[doc_line] + end + return false +end + +function DocView:toggle_fold(doc_line, value) + if self:is_foldable(doc_line) then + if value == nil then value = not self:is_folded(doc_line) end + local starting_fold = self.foldable[doc_line] + local line = doc_line + 1 + self:invalidate_cache(doc_line) + while line <= #self.doc.lines do + if self.foldable[line] <= starting_fold then + if self.doc.lines[line]:find("}%s*$") then self.folded[line] = value end + break + end + self.folded[line] = value + line = line + 1 + end + end +end + + +local old_get_gutter_width = DocView.get_gutter_width +function DocView:get_gutter_width() + local x,y = old_get_gutter_width(self) + return x + style.padding.x, y +end + +local old_draw_line_gutter = DocView.draw_line_gutter +function DocView:draw_line_gutter(line, x, y, width) + local lh = old_draw_line_gutter(self, line, x, y, width) + local start = x + 4 + if self:is_foldable(line) then + renderer.draw_rect(start, y, lh, lh, style.accent) + renderer.draw_rect(start + 1, y + 1, lh - 2, lh - 2, self.hovering_foldable == line and style.dim or style.background) + common.draw_text(self:get_font(), style.accent, self:is_folded(line) and "+" or "-", "left", start + 6, y, width, lh) + end + -- common.draw_text(self:get_font(), style.accent, self.foldable[line] or "nil", "left", start + 6, y, width, lh) + return lh +end + +local old_mouse_moved = DocView.on_mouse_moved +function DocView:on_mouse_moved(x, y, ...) + old_mouse_moved(self, x, y, ...) + self.hovering_foldable = false + if self.hovering_gutter then + local line = self:resolve_screen_position(x, y) + if self:is_foldable(line) then + self.hovering_foldable = line + self.cursor = "hand" + end + end +end + +local old_mouse_pressed = DocView.on_mouse_pressed +function DocView:on_mouse_pressed(button, x, y, clicks) + if button == "left" and self.hovering_foldable then + self:toggle_fold(self.hovering_foldable) + end + return old_mouse_pressed(button, x, y, clicks) +end + From 60546ba5809824645878ee65e132be8c83973f83 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Fri, 29 Dec 2023 18:02:07 -0500 Subject: [PATCH 03/24] Making it so that linewrapping also works. --- data/core/doc/init.lua | 5 - data/core/docview.lua | 34 +- data/plugins/codefolding.lua | 38 +- data/plugins/drawwhitespace.lua | 42 -- data/{core/doc => plugins}/highlighter.lua | 77 ++- data/plugins/linewrapping.lua | 563 +++------------------ 6 files changed, 155 insertions(+), 604 deletions(-) rename data/{core/doc => plugins}/highlighter.lua (60%) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 9c5878ff1..2e7d753f3 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -1,5 +1,4 @@ local Object = require "core.object" -local Highlighter = require "core.doc.highlighter" local translate = require "core.doc.translate" local core = require "core" local syntax = require "core.syntax" @@ -39,7 +38,6 @@ function Doc:reset() self.undo_stack = { idx = 1 } self.redo_stack = { idx = 1 } self.clean_change_id = 1 - self.highlighter = Highlighter(self) self.overwrite = false self:reset_syntax() end @@ -54,7 +52,6 @@ function Doc:reset_syntax() local syn = syntax.get(path, header) if self.syntax ~= syn then self.syntax = syn - self.highlighter:soft_reset() end end @@ -75,7 +72,6 @@ function Doc:load(filename) self.crlf = true end table.insert(self.lines, line .. "\n") - self.highlighter.lines[i] = false i = i + 1 end if #self.lines == 0 then @@ -267,7 +263,6 @@ function Doc:raw_insert(line, col, text, undo_stack, time) local line2, col2 = self:position_offset(line, col, #text) push_undo(undo_stack, time, "remove", line, col, line2, col2) - -- update highlighter and assure selection is in bounds for i,v in ipairs(self.listeners) do v("insert", text, line, col, line, col) end end diff --git a/data/core/docview.lua b/data/core/docview.lua index 5e1fe8406..017f62e0a 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -492,7 +492,7 @@ function DocView:get_visible_virutal_line_range() local x, y, x2, y2 = self:get_content_bounds() local lh = self:get_line_height() local minline = math.max(1, math.floor((y - style.padding.y) / lh) + 1) - local maxline = math.min(#self.doc.lines, math.floor((y2 - style.padding.y) / lh) + 1) + local maxline = math.floor((y2 - style.padding.y) / lh) + 1 return minline, maxline end @@ -503,7 +503,7 @@ function DocView:get_col_x_offset(line, col) default_font:set_tab_size(indent_size) local column = 1 local xoffset = 0 - for _, text, style in self:each_text_token(line) do + for _, text, style in self:each_vline_token(line) do local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end local length = #text @@ -535,7 +535,7 @@ function DocView:get_x_offset_col(line, x) local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) - for _, text, style in self:each_text_token(line) do + for _, text, style in self:each_vline_token(line) do local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end local width = font:get_width(text) @@ -774,7 +774,7 @@ local default_color = { common.color "#FFFFFF" } function DocView:draw_line_text(vline, x, y) local default_font = self:get_font() local tx, ty = x, y + self:get_line_text_y_offset() - for tidx, text, style in self:each_text_token(vline) do + for tidx, text, style in self:each_vline_token(vline) do local font = style.font or default_font tx = renderer.draw_text(font, text, tx, ty, style.color or default_color) if tx > self.position.x + self.size.x then break end @@ -937,7 +937,7 @@ end -- `{ "doc", doc_line, 1, #self.doc.lines[doc_line], style }` -- `{ "virtual", text, false, false, style } function DocView:transform(doc_line) - return { "doc", doc_line, 1, #self.doc.lines[doc_line] - 1, {} } + return { "doc", doc_line, 1, #self.doc.lines[doc_line] - 1, { } } end --[[ @@ -949,9 +949,10 @@ Each token can contain *at most* one new line, at the end of it. ]] function DocView:invalidate_cache(start_doc_line) + if not start_doc_line then start_doc_line = 1 end while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end while #self.dcache >= start_doc_line do table.remove(self.dcache) end - while self.vcache[#self.vcache] > #self.tokens do table.remove(self.vcache) end + while (self.vcache[#self.vcache] or 0) > #self.tokens do table.remove(self.vcache) end end function DocView:get_token_text(type, doc_line, col_start, col_end) @@ -968,11 +969,10 @@ local function retrieve_tokens(self, vline) table.insert(self.vcache, #self.tokens + 1) self.vtodcache[#self.vcache] = #self.dcache end - for j = 1, bundles do - local token_idx = (j - 1) * 5 + 1 - local text = self:get_token_text(tokens[token_idx], tokens[token_idx+1], tokens[token_idx+2], tokens[token_idx+3]) - table.move(tokens, token_idx, token_idx+4, #self.tokens + 1, self.tokens) - if text:find("\n$") and j < bundles then + for j = 1, #tokens, 5 do + local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) + table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) + if text:find("\n$") and j < #tokens - 5 then table.insert(self.vcache, #self.tokens + 1) self.vtodcache[#self.vcache] = #self.dcache end @@ -988,7 +988,7 @@ local function text_iter(state, idx) return idx + 5, text, self.tokens[idx+4] end -function DocView:each_text_token(vline) +function DocView:each_vline_token(vline) return text_iter, { self, vline }, retrieve_tokens(self, vline) end @@ -997,4 +997,14 @@ function DocView:has_tokens(vline) return token_idx and token_idx < #self.tokens and token_idx ~= retrieve_tokens(self, vline + 1) end +local function dline_iter(state, idx) + local self, tokens = table.unpack(state) + if idx > #tokens then return nil end + return idx + 5, tokens[idx], tokens[idx+1], tokens[idx+2], tokens[idx+3], tokens[idx+4] +end + +function DocView:each_dline_token(tokens) + return dline_iter, { self, tokens }, 1 +end + return DocView diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index 38d5cae9c..430dc53c2 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -7,7 +7,6 @@ local DocView = require "core.docview" local Node = require "core.node" local common = require "core.common" -local old_transform = DocView.transform function DocView:is_folded(doc_line) return self.folded[doc_line+1] @@ -44,24 +43,25 @@ function DocView:compute_fold(doc_line) end end +local old_transform = DocView.transform function DocView:transform(doc_line) - local results = old_transform(self, doc_line) + local tokens = old_transform(self, doc_line) self:compute_fold(doc_line) if self.folded[doc_line] then return {} end if self:is_foldable(doc_line) and self.folded[doc_line+1] then -- remove the newline from the end of the tokens - table.insert(results, "virtual") - table.insert(results, " ... ") - table.insert(results, false) - table.insert(results, false) - table.insert(results, { color = style.dim }) - table.insert(results, "virtual") - table.insert(results, "}") - table.insert(results, false) - table.insert(results, false) - table.insert(results, { }) + table.insert(tokens, "virtual") + table.insert(tokens, " ... ") + table.insert(tokens, false) + table.insert(tokens, false) + table.insert(tokens, { color = style.dim }) + table.insert(tokens, "virtual") + table.insert(tokens, "}") + table.insert(tokens, false) + table.insert(tokens, false) + table.insert(tokens, { }) end - return results + return tokens end function DocView:is_foldable(doc_line) @@ -99,13 +99,15 @@ end local old_draw_line_gutter = DocView.draw_line_gutter function DocView:draw_line_gutter(line, x, y, width) local lh = old_draw_line_gutter(self, line, x, y, width) - local start = x + 4 + local size = lh - 4 + local startx = x + 4 + local starty = y + (lh - size) / 2 if self:is_foldable(line) then - renderer.draw_rect(start, y, lh, lh, style.accent) - renderer.draw_rect(start + 1, y + 1, lh - 2, lh - 2, self.hovering_foldable == line and style.dim or style.background) - common.draw_text(self:get_font(), style.accent, self:is_folded(line) and "+" or "-", "left", start + 6, y, width, lh) + renderer.draw_rect(startx, starty, size, size, style.accent) + renderer.draw_rect(startx + 1, starty + 1, size - 2, size - 2, self.hovering_foldable == line and style.dim or style.background) + common.draw_text(self:get_font(), style.accent, self:is_folded(line) and "+" or "-", "center", startx, starty, size, size) end - -- common.draw_text(self:get_font(), style.accent, self.foldable[line] or "nil", "left", start + 6, y, width, lh) + -- common.draw_text(self:get_font(), style.accent, self.foldable[line] or "nil", "center", startx, starty, size, size) return lh end diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 753b3bb45..36e29aa84 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -6,7 +6,6 @@ local DocView = require "core.docview" local common = require "core.common" local command = require "core.command" local config = require "core.config" -local Highlighter = require "core.doc.highlighter" config.plugins.drawwhitespace = common.merge({ enabled = false, @@ -145,47 +144,6 @@ local function reset_cache_if_needed() end end --- Move cache to make space for new lines -local prev_insert_notify = Highlighter.insert_notify -function Highlighter:insert_notify(line, n, ...) - prev_insert_notify(self, line, n, ...) - if not ws_cache[self] then - ws_cache[self] = {} - end - local to = math.min(line + n, #self.doc.lines) - for i=#self.doc.lines+n,to,-1 do - ws_cache[self][i] = ws_cache[self][i - n] - end - for i=line,to do - ws_cache[self][i] = nil - end -end - --- Close the cache gap created by removed lines -local prev_remove_notify = Highlighter.remove_notify -function Highlighter:remove_notify(line, n, ...) - prev_remove_notify(self, line, n, ...) - if not ws_cache[self] then - ws_cache[self] = {} - end - local to = math.max(line + n, #self.doc.lines) - for i=line,to do - ws_cache[self][i] = ws_cache[self][i + n] - end -end - --- Remove changed lines from the cache -local prev_update_notify = Highlighter.update_notify -function Highlighter:update_notify(line, n, ...) - prev_update_notify(self, line, n, ...) - if not ws_cache[self] then - ws_cache[self] = {} - end - for i=line,line+n do - ws_cache[self][i] = nil - end -end - local function get_option(substitution, option) if substitution[option] == nil then diff --git a/data/core/doc/highlighter.lua b/data/plugins/highlighter.lua similarity index 60% rename from data/core/doc/highlighter.lua rename to data/plugins/highlighter.lua index 14868674f..eb627566f 100644 --- a/data/core/doc/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -1,3 +1,12 @@ +-- mod-version:3 +local core = require "core" +local config = require "core.config" +local style = require "core.style" +local Doc = require "core.doc" +local DocView = require "core.docview" +local Node = require "core.node" +local common = require "core.common" + local core = require "core" local common = require "core.common" local config = require "core.config" @@ -7,13 +16,13 @@ local Object = require "core.object" local Highlighter = Object:extend() - function Highlighter:new(doc) self.doc = doc self.running = false self:reset() end + -- init incremental syntax highlighting function Highlighter:start() if self.running then return end @@ -37,16 +46,12 @@ function Highlighter:start() goto yield end elseif retokenized_from then - self:update_notify(retokenized_from, i - retokenized_from - 1) retokenized_from = nil end end - self.first_invalid_line = max + 1 ::yield:: - if retokenized_from then - self:update_notify(retokenized_from, max - retokenized_from) - end + self.first_invalid_line = max + 1 core.redraw = true coroutine.yield(0) end @@ -68,6 +73,7 @@ function Highlighter:reset() self:soft_reset() end + function Highlighter:soft_reset() for i=1,#self.lines do self.lines[i] = false @@ -76,29 +82,12 @@ function Highlighter:soft_reset() self.max_wanted_line = 0 end + function Highlighter:invalidate(idx) self.first_invalid_line = math.min(self.first_invalid_line, idx) set_max_wanted_lines(self, math.min(self.max_wanted_line, #self.doc.lines)) end -function Highlighter:insert_notify(line, n) - self:invalidate(line) - local blanks = { } - for i = 1, n do - blanks[i] = false - end - common.splice(self.lines, line, 0, blanks) -end - -function Highlighter:remove_notify(line, n) - self:invalidate(line) - common.splice(self.lines, line, n) -end - -function Highlighter:update_notify(line, n) - -- plugins can hook here to be notified that lines have been retokenized -end - function Highlighter:tokenize_line(idx, state, resume) local res = {} @@ -115,16 +104,50 @@ function Highlighter:get_line(idx) local prev = self.lines[idx - 1] line = self:tokenize_line(idx, prev and prev.state) self.lines[idx] = line - self:update_notify(idx, 0) end set_max_wanted_lines(self, math.max(self.max_wanted_line, idx)) return line end -function Highlighter:each_token(idx) - return tokenizer.each_token(self:get_line(idx).tokens) +local function get_tokens(highlighted_tokens, doc_line, start_offset, end_offset, token_style) + local tokens = {} + local offset = 1 + for i = 1, #highlighted_tokens, 2 do + local type, text = highlighted_tokens[i], highlighted_tokens[i+1] + if offset <= end_offset and offset + #text >= start_offset then + table.insert(tokens, "doc") + table.insert(tokens, doc_line) + table.insert(tokens, math.max(start_offset, offset)) + table.insert(tokens, math.min(end_offset, offset + #text - 1)) + table.insert(tokens, common.merge(token_style, { color = style.syntax[type], font = style.syntax_fonts[type] })) + end + if offset > end_offset then break end + offset = offset + #text + end + return tokens end +local old_transform = DocView.transform +function DocView:transform(doc_line) + if not self.doc.highighter then self.doc.highlighter = Highlighter(self.doc) end + + local tokens = old_transform(self, doc_line) + if #tokens == 0 then return tokens end + local highlighted_tokens = self.doc.highlighter:get_line(doc_line).tokens + -- Walk through all doc tokens, and then map them onto what we've tokenized. + local colorized = {} + local start_offset = 1 + local start_highlighted = 1 + for i = 1, #tokens, 5 do + if tokens[i] == "doc" then + local t = get_tokens(highlighted_tokens, tokens[i+1], tokens[i+2], tokens[i+3], tokens[i+4]) + table.move(t, 1, #t, #colorized + 1, colorized) + else + table.move(tokens, i, i + 4, #colorized + 1, colorized) + end + end + return #colorized > 0 and colorized or { "doc", doc_line, 1, 0, {} } +end return Highlighter diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index 0b9f0b76b..a156349ac 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -68,524 +68,89 @@ config.plugins.linewrapping = common.merge({ } }, config.plugins.linewrapping) -local LineWrapping = {} - --- Optimzation function. The tokenizer is relatively slow (at present), and --- so if we don't need to run it, should be run sparingly. -local function spew_tokens(doc, line) if line < math.huge then return math.huge, "normal", doc.lines[line] end end -local function get_tokens(doc, line) - if config.plugins.linewrapping.require_tokenization then - return doc.highlighter:each_token(line) - end - return spew_tokens, doc, line -end - --- Computes the breaks for a given line, width and mode. Returns a list of columns --- at which the line should be broken. -function LineWrapping.compute_line_breaks(doc, default_font, line, width, mode) - local xoffset, last_i, i, last_space, last_width, begin_width = 0, 1, 1, nil, 0, 0 - local splits = { 1 } - for idx, type, text in get_tokens(doc, line) do - local font = style.syntax_fonts[type] or default_font - if idx == 1 or idx == math.huge and config.plugins.linewrapping.indent then - local _, indent_end = text:find("^%s+") - if indent_end then begin_width = font:get_width(text:sub(1, indent_end)) end - end - local w = font:get_width(text) - if xoffset + w > width then - for char in common.utf8_chars(text) do - w = font:get_width(char) - xoffset = xoffset + w - if xoffset > width then - if mode == "word" and last_space then - table.insert(splits, last_space + 1) - xoffset = w + begin_width + (xoffset - last_width) - else - table.insert(splits, i) - xoffset = w + begin_width - end - last_space = nil - elseif char == ' ' then - last_space = i - last_width = xoffset - end - i = i + #char - end - else - xoffset = xoffset + w - i = i + #text - end - end - return splits, begin_width -end - --- breaks are held in a single table that contains n*2 elements, where n is the amount of line breaks. --- each element represents line and column of the break. line_offset will check from the specified line --- if the first line has not changed breaks, it will stop there. -function LineWrapping.reconstruct_breaks(docview, default_font, width, line_offset) - if width ~= math.huge then - local doc = docview.doc - -- two elements per wrapped line; first maps to original line number, second to column number. - docview.wrapped_lines = { } - -- one element per actual line; maps to the first index of in wrapped_lines for this line - docview.wrapped_line_to_idx = { } - -- one element per actual line; gives the indent width for the acutal line - docview.wrapped_line_offsets = { } - docview.wrapped_settings = { ["width"] = width, ["font"] = default_font } - for i = line_offset or 1, #doc.lines do - local breaks, offset = LineWrapping.compute_line_breaks(doc, default_font, i, width, config.plugins.linewrapping.mode) - table.insert(docview.wrapped_line_offsets, offset) - for k, col in ipairs(breaks) do - table.insert(docview.wrapped_lines, i) - table.insert(docview.wrapped_lines, col) - end - end - -- list of indices for wrapped_lines, that are based on original line number - -- holds the index to the first in the wrapped_lines list. - local last_wrap = nil - for i = 1, #docview.wrapped_lines, 2 do - if not last_wrap or last_wrap ~= docview.wrapped_lines[i] then - table.insert(docview.wrapped_line_to_idx, (i + 1) / 2) - last_wrap = docview.wrapped_lines[i] - end - end - else - docview.wrapped_lines = nil - docview.wrapped_line_to_idx = nil - docview.wrapped_line_offsets = nil - docview.wrapped_settings = nil - end -end - --- When we have an insertion or deletion, we have four sections of text. --- 1. The unaffected section, located prior to the cursor. This is completely ignored. --- 2. The beginning of the affected line prior to the insertion or deletion. Begins on column 1 of the selection. --- 3. The removed/pasted lines. --- 4. Every line after the modification, begins one line after the selection in the initial document. -function LineWrapping.update_breaks(docview, old_line1, old_line2, net_lines) - -- Step 1: Determine the index for the line for #2. - local old_idx1 = docview.wrapped_line_to_idx[old_line1] or 1 - -- Step 2: Determine the index of the line for #4. - local old_idx2 = (docview.wrapped_line_to_idx[old_line2 + 1] or ((#docview.wrapped_lines / 2) + 1)) - 1 - -- Step 3: Remove all old breaks for the old lines from the table, and all old widths from wrapped_line_offsets. - local offset = (old_idx1 - 1) * 2 + 1 - for i = old_idx1, old_idx2 do - table.remove(docview.wrapped_lines, offset) - table.remove(docview.wrapped_lines, offset) - end - for i = old_line1, old_line2 do - table.remove(docview.wrapped_line_offsets, old_line1) - end - -- Step 4: Shift the line number of wrapped_lines past #4 by the amount of inserted/deleted lines. - if net_lines ~= 0 then - for i = offset, #docview.wrapped_lines, 2 do - docview.wrapped_lines[i] = docview.wrapped_lines[i] + net_lines - end - end - -- Step 5: Compute the breaks and offsets for the lines for #2 and #3. Insert them into the table. - local new_line1 = old_line1 - local new_line2 = old_line2 + net_lines - for line = new_line1, new_line2 do - local breaks, begin_width = LineWrapping.compute_line_breaks(docview.doc, docview.wrapped_settings.font, line, docview.wrapped_settings.width, config.plugins.linewrapping.mode) - table.insert(docview.wrapped_line_offsets, line, begin_width) - for i,b in ipairs(breaks) do - table.insert(docview.wrapped_lines, offset, b) - table.insert(docview.wrapped_lines, offset, line) - offset = offset + 2 - end - end - -- Step 6: Recompute the wrapped_line_to_idx cache from #2. - local line = old_line1 - offset = (old_idx1 - 1) * 2 + 1 - while offset < #docview.wrapped_lines do - if docview.wrapped_lines[offset + 1] == 1 then - docview.wrapped_line_to_idx[line] = ((offset - 1) / 2) + 1 - line = line + 1 - end - offset = offset + 2 - end - while line <= #docview.wrapped_line_to_idx do - table.remove(docview.wrapped_line_to_idx) - end -end - --- Draws a guide if applicable to show where wrapping is occurring. -function LineWrapping.draw_guide(docview) - if config.plugins.linewrapping.guide and docview.wrapped_settings.width ~= math.huge then - local x, y = docview:get_content_offset() - local gw = docview:get_gutter_width() - renderer.draw_rect(x + gw + docview.wrapped_settings.width, y, 1, core.root_view.size.y, style.selection) - end -end - -function LineWrapping.update_docview_breaks(docview) - local w = docview.v_scrollbar.expanded_size or style.expanded_scrollbar_size - local width = (type(config.plugins.linewrapping.width_override) == "function" and config.plugins.linewrapping.width_override(docview)) - or config.plugins.linewrapping.width_override or (docview.size.x - docview:get_gutter_width() - w) - if (not docview.wrapped_settings or docview.wrapped_settings.width == nil or width ~= docview.wrapped_settings.width) then - docview.scroll.to.x = 0 - LineWrapping.reconstruct_breaks(docview, docview:get_font(), width) - end -end - -local function get_idx_line_col(docview, idx) - local doc = docview.doc - if not docview.wrapped_settings then - if idx > #doc.lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end - return idx, 1 - end - if idx < 1 then return 1, 1 end - local offset = (idx - 1) * 2 + 1 - if offset > #docview.wrapped_lines then return #doc.lines, #doc.lines[#doc.lines] + 1 end - return docview.wrapped_lines[offset], docview.wrapped_lines[offset + 1] -end - -local function get_idx_line_length(docview, idx) - local doc = docview.doc - if not docview.wrapped_settings then - if idx > #doc.lines then return #doc.lines[#doc.lines] + 1 end - return #doc.lines[idx] - end - local offset = (idx - 1) * 2 + 1 - local start = docview.wrapped_lines[offset + 1] - if docview.wrapped_lines[offset + 2] and docview.wrapped_lines[offset + 2] == docview.wrapped_lines[offset] then - return docview.wrapped_lines[offset + 3] - docview.wrapped_lines[offset + 1] - else - return #doc.lines[docview.wrapped_lines[offset]] - docview.wrapped_lines[offset + 1] + 1 - end -end - -local function get_total_wrapped_lines(docview) - if not docview.wrapped_settings then return docview.doc and #docview.doc.lines end - return #docview.wrapped_lines / 2 -end - --- If line end, gives the end of an index line, rather than the first character of the next line. -local function get_line_idx_col_count(docview, line, col, line_end, ndoc) - local doc = docview.doc - if not docview.wrapped_settings then return common.clamp(line, 1, #doc.lines), col, 1, 1 end - if line > #doc.lines then return get_line_idx_col_count(docview, #doc.lines, #doc.lines[#doc.lines] + 1) end - line = math.max(line, 1) - local idx = docview.wrapped_line_to_idx[line] or 1 - local ncol, scol = 1, 1 - if col then - local i = idx + 1 - while line == docview.wrapped_lines[(i - 1) * 2 + 1] and col >= docview.wrapped_lines[(i - 1) * 2 + 2] do - local nscol = docview.wrapped_lines[(i - 1) * 2 + 2] - if line_end and col == nscol then - break - end - scol = nscol - i = i + 1 - idx = idx + 1 - end - ncol = (col - scol) + 1 - end - local count = (docview.wrapped_line_to_idx[line + 1] or (get_total_wrapped_lines(docview) + 1)) - (docview.wrapped_line_to_idx[line] or get_total_wrapped_lines(docview)) - return idx, ncol, count, scol -end - -local function get_line_col_from_index_and_x(docview, idx, x) - local doc = docview.doc - local line, col = get_idx_line_col(docview, idx) - if idx < 1 then return 1, 1 end - local xoffset, last_i, i = (col ~= 1 and docview.wrapped_line_offsets[line] or 0), col, 1 - if x < xoffset then return line, col end - local default_font = docview:get_font() - for _, type, text in doc.highlighter:each_token(line) do - local font, w = style.syntax_fonts[type] or default_font, 0 - for char in common.utf8_chars(text) do - if i >= col then - if xoffset >= x then - return line, (xoffset - x > (w / 2) and last_i or i) - end - w = font:get_width(char) - xoffset = xoffset + w - end - last_i = i - i = i + #char - end - end - return line, #doc.lines[line] -end - - -local open_files = setmetatable({ }, { __mode = "k" }) - -local old_doc_insert = Doc.raw_insert -function Doc:raw_insert(line, col, text, undo_stack, time) - local old_lines = #self.lines - old_doc_insert(self, line, col, text, undo_stack, time) - if open_files[self] then - for i,docview in ipairs(open_files[self]) do - if docview.wrapped_settings then - local lines = #self.lines - old_lines - LineWrapping.update_breaks(docview, line, line, lines) - end - end +local function split_token(font, text, mode, width) + local offset = 0 + local last_break = nil + for i = 1, #text do + offset = offset + font:get_width(text:sub(i, i)) + if offset >= width then return last_break end + if mode == "word" or text:sub(i, i) == " " then last_break = i end end + return nil end -local old_doc_remove = Doc.raw_remove -function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) - local old_lines = #self.lines - old_doc_remove(self, line1, col1, line2, col2, undo_stack, time) - if open_files[self] then - for i,docview in ipairs(open_files[self]) do - if docview.wrapped_settings then - local lines = #self.lines - old_lines - LineWrapping.update_breaks(docview, line1, line2, lines) - end - end - end -end - -local old_doc_update = DocView.update -function DocView:update() - old_doc_update(self) - if self.wrapped_settings and self.size.x > 0 then - LineWrapping.update_docview_breaks(self) - end -end - -function DocView:get_scrollable_size() - if not config.scroll_past_end then - return self:get_line_height() * get_total_wrapped_lines(self) + style.padding.y * 2 - end - return self:get_line_height() * (get_total_wrapped_lines(self) - 1) + self.size.y -end - -local old_get_h_scrollable_size = DocView.get_h_scrollable_size -function DocView:get_h_scrollable_size(...) - if self.wrapping_enabled then return 0 end - return old_get_h_scrollable_size(self, ...) -end - -local old_new = DocView.new -function DocView:new(doc) - old_new(self, doc) - if not open_files[doc] then open_files[doc] = {} end - table.insert(open_files[doc], self) - if config.plugins.linewrapping.enable_by_default then - self.wrapping_enabled = true - LineWrapping.update_docview_breaks(self) - else - self.wrapping_enabled = false - end -end - -local old_scroll_to_line = DocView.scroll_to_line -function DocView:scroll_to_line(...) - if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end - old_scroll_to_line(self, ...) -end - -local old_scroll_to_make_visible = DocView.scroll_to_make_visible -function DocView:scroll_to_make_visible(line, col) - if self.wrapping_enabled then LineWrapping.update_docview_breaks(self) end - old_scroll_to_make_visible(self, line, col) - if self.wrapped_settings then self.scroll.to.x = 0 end -end - -local old_get_visible_line_range = DocView.get_visible_line_range -function DocView:get_visible_line_range() - if not self.wrapped_settings then return old_get_visible_line_range(self) end - local x, y, x2, y2 = self:get_content_bounds() - local lh = self:get_line_height() - local minline = get_idx_line_col(self, math.max(1, math.floor(y / lh))) - local maxline = get_idx_line_col(self, math.min(get_total_wrapped_lines(self), math.floor(y2 / lh) + 1)) - return minline, maxline -end - -local old_get_x_offset_col = DocView.get_x_offset_col -function DocView:get_x_offset_col(line, x) - if not self.wrapped_settings then return old_get_x_offset_col(self, line, x) end - local idx = get_line_idx_col_count(self, line) - return get_line_col_from_index_and_x(self, idx, x) -end - --- If line end is true, returns the end of the previous line, in a multi-line break. -local old_get_col_x_offset = DocView.get_col_x_offset -function DocView:get_col_x_offset(line, col, line_end) - if not self.wrapped_settings then return old_get_col_x_offset(self, line, col) end - local idx, ncol, count, scol = get_line_idx_col_count(self, line, col, line_end) - local xoffset, i = (scol ~= 1 and self.wrapped_line_offsets[line] or 0), 1 - local default_font = self:get_font() - for _, type, text in self.doc.highlighter:each_token(line) do - if i + #text >= scol then - if i < scol then - text = text:sub(scol - i + 1) - i = scol - end - local font = style.syntax_fonts[type] or default_font - for char in common.utf8_chars(text) do - if i >= col then - return xoffset - end - xoffset = xoffset + font:get_width(char) - i = i + #char - end - else - i = i + #text - end - end - return xoffset -end - -local old_get_line_screen_position = DocView.get_line_screen_position -function DocView:get_line_screen_position(line, col) - if not self.wrapped_settings then return old_get_line_screen_position(self, line, col) end - local idx, ncol, count = get_line_idx_col_count(self, line, col) - local x, y = self:get_content_offset() - local lh = self:get_line_height() +local old_transform = DocView.transform +function DocView:transform(doc_line) + if not self.wrapping then return old_transform(self, doc_line) end + local tokens = {} local gw = self:get_gutter_width() - return x + gw + (col and self:get_col_x_offset(line, col) or 0), y + (idx-1) * lh + style.padding.y -end - -local old_resolve_screen_position = DocView.resolve_screen_position -function DocView:resolve_screen_position(x, y) - if not self.wrapped_settings then return old_resolve_screen_position(self, x, y) end - local ox, oy = self:get_line_screen_position(1) - local idx = common.clamp(math.floor((y - oy) / self:get_line_height()) + 1, 1, get_total_wrapped_lines(self)) - return get_line_col_from_index_and_x(self, idx, x - ox) -end - -local old_draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(line, x, y) - if not self.wrapped_settings then return old_draw_line_text(self, line, x, y) end - local default_font = self:get_font() - local tx, ty, begin_width = x, y + self:get_line_text_y_offset(), self.wrapped_line_offsets[line] - local lh = self:get_line_height() - local idx, _, count = get_line_idx_col_count(self, line) - local total_offset = 1 - for _, type, text in self.doc.highlighter:each_token(line) do - local color = style.syntax[type] - local font = style.syntax_fonts[type] or default_font - local token_offset = 1 - -- Split tokens if we're at the end of the document. - while text ~= nil and token_offset <= #text do - local next_line, next_line_start_col = get_idx_line_col(self, idx + 1) - if next_line ~= line then - next_line_start_col = #self.doc.lines[line] - end - local max_length = next_line_start_col - total_offset - local rendered_text = text:sub(token_offset, token_offset + max_length - 1) - tx = renderer.draw_text(font, rendered_text, tx, ty, color) - total_offset = total_offset + #rendered_text - if total_offset ~= next_line_start_col or max_length == 0 then break end - token_offset = token_offset + #rendered_text - idx = idx + 1 - tx, ty = x + begin_width, ty + lh - end - end - return lh * count -end - -local old_draw_line_body = DocView.draw_line_body -function DocView:draw_line_body(line, x, y) - if not self.wrapped_settings then return old_draw_line_body(self, line, x, y) end - local lh = self:get_line_height() - local idx0, _, count = get_line_idx_col_count(self, line) - for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do - if line >= line1 and line <= line2 then - if line1 ~= line then col1 = 1 end - if line2 ~= line then col2 = #self.doc.lines[line] + 1 end - if col1 ~= col2 then - local idx1, ncol1 = get_line_idx_col_count(self, line, col1) - local idx2, ncol2 = get_line_idx_col_count(self, line, col2) - local start = 0 - for i = idx1, idx2 do - local x1, x2 = x + (idx1 == i and self:get_col_x_offset(line1, col1) or 0) - if idx2 == i then - x2 = x + self:get_col_x_offset(line, col2) - else - start = start + get_idx_line_length(self, i, line) - x2 = x + self:get_col_x_offset(line, start + 1, true) - end - renderer.draw_rect(x1, y + (i - idx0) * lh, x2 - x1, lh, style.selection) + local width = self.size.x + if config.plugins.linewrapping.width_override then + width = config.plugins.linewrapping.width_override + if type(width) == "function" then width = width(self) end + end + + local docstart = self.position.x + gw + style.padding.x + local offset = docstart + local docend = self.position.x + width + + for _, type, l, s, e, style in self:each_dline_token(old_transform(self, doc_line)) do + local font = self:get_font() or style.font + while true do + local text = self:get_token_text(type, l, s, e) + local w = font:get_width(text) + if offset + w >= docend then + local i = split_token(font, text, config.plugins.linewrapping.mode, docend - offset) + assert(i) + table.insert(tokens, type) + if type == "doc" then + table.insert(tokens, l) + table.insert(tokens, s) + table.insert(tokens, i and (i - 1) or e) + s = i + else + table.insert(tokens, i and l:sub(1, i) or l) + table.insert(tokens, false) + table.insert(tokens, false) + l = l:sub(i+1) end + table.insert(tokens, style) + + table.insert(tokens, "virtual") + table.insert(tokens, "\n") + table.insert(tokens, false) + table.insert(tokens, false) + table.insert(tokens, style) + + offset = docstart + else + table.insert(tokens, type) + table.insert(tokens, l) + table.insert(tokens, s) + table.insert(tokens, e) + table.insert(tokens, style) + break end end end - local draw_highlight = nil - for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do - -- draw line highlight if caret is on this line - if draw_highlight ~= false and config.highlight_current_line - and line1 == line and core.active_view == self then - draw_highlight = (line1 == line2 and col1 == col2) - end - end - if draw_highlight then - for i=1,count do - self:draw_line_highlight(x + self.scroll.x, y + lh * (i - 1)) - end - end - -- draw line's text - return self:draw_line_text(line, x, y) -end - -local old_draw = DocView.draw -function DocView:draw() - old_draw(self) - if self.wrapped_settings then - LineWrapping.draw_guide(self) - end -end - -local old_draw_line_gutter = DocView.draw_line_gutter -function DocView:draw_line_gutter(line, x, y, width) - local lh = self:get_line_height() - local _, _, count = get_line_idx_col_count(self, line) - return (old_draw_line_gutter(self, line, x, y, width) or lh) * count -end - -local old_translate_end_of_line = translate.end_of_line -function translate.end_of_line(doc, line, col) - if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_end_of_line(doc, line, col) end - local idx, ncol = get_line_idx_col_count(core.active_view, line, col) - local nline, ncol2 = get_idx_line_col(core.active_view, idx + 1) - if nline ~= line then return line, math.huge end - return line, ncol2 - 1 -end - -local old_translate_start_of_line = translate.start_of_line -function translate.start_of_line(doc, line, col) - if not core.active_view or core.active_view.doc ~= doc or not core.active_view.wrapped_settings then old_translate_start_of_line(doc, line, col) end - local idx, ncol = get_line_idx_col_count(core.active_view, line, col) - local nline, ncol2 = get_idx_line_col(core.active_view, idx - 1) - if nline ~= line then return line, 1 end - return line, ncol2 + 1 -end - -local old_previous_line = DocView.translate.previous_line -function DocView.translate.previous_line(doc, line, col, dv) - if not dv.wrapped_settings then return old_previous_line(doc, line, col, dv) end - local idx, ncol = get_line_idx_col_count(dv, line, col) - return get_line_col_from_index_and_x(dv, idx - 1, dv:get_col_x_offset(line, col)) -end - -local old_next_line = DocView.translate.next_line -function DocView.translate.next_line(doc, line, col, dv) - if not dv.wrapped_settings then return old_next_line(doc, line, col, dv) end - local idx, ncol = get_line_idx_col_count(dv, line, col) - return get_line_col_from_index_and_x(dv, idx + 1, dv:get_col_x_offset(line, col)) + return tokens end command.add(nil, { ["line-wrapping:enable"] = function() if core.active_view and core.active_view.doc then - core.active_view.wrapping_enabled = true - LineWrapping.update_docview_breaks(core.active_view) + core.active_view.wrapping = true + core.active_view:invalidate_cache() end end, ["line-wrapping:disable"] = function() if core.active_view and core.active_view.doc then - core.active_view.wrapping_enabled = false - LineWrapping.reconstruct_breaks(core.active_view, core.active_view:get_font(), math.huge) + core.active_view.wrapping = false + core.active_view:invalidate_cache() end end, ["line-wrapping:toggle"] = function() - if core.active_view and core.active_view.doc and core.active_view.wrapped_settings then + if core.active_view and core.active_view.doc and core.active_view.wrapping then command.perform("line-wrapping:disable") else command.perform("line-wrapping:enable") @@ -596,5 +161,3 @@ command.add(nil, { keymap.add { ["f10"] = "line-wrapping:toggle", } - -return LineWrapping From 771da34edef35d5da06cd4b4106945d914f4db7e Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sat, 30 Dec 2023 16:33:52 -0500 Subject: [PATCH 04/24] Things proceed apace. --- data/core/commands/docview.lua | 8 +- data/core/commands/findreplace.lua | 6 +- data/core/doc/init.lua | 16 +- data/core/doc/translate.lua | 1 + data/core/docview.lua | 338 ++++++++++++++++++----------- data/plugins/codefolding.lua | 31 +-- data/plugins/linewrapping.lua | 20 +- data/plugins/projectsearch.lua | 2 +- 8 files changed, 259 insertions(+), 163 deletions(-) diff --git a/data/core/commands/docview.lua b/data/core/commands/docview.lua index dcaabbee0..f9223bb08 100644 --- a/data/core/commands/docview.lua +++ b/data/core/commands/docview.lua @@ -223,12 +223,12 @@ local commands = { if line1 == line2 and col1 == col2 and dv.doc.lines[line1]:find("^%s*$", col1) then dv.doc:remove(line1, col1, line1, math.huge) end - dv.doc:delete_to_cursor(idx, translate.next_char) + dv:delete_to_cursor(idx, translate.next_char) end end, ["docview:backspace"] = function(dv) - local _, indent_size = dv:get_indent_info() + local _, indent_size = dv.doc:get_indent_info() for idx, line1, col1, line2, col2 in dv:get_selections(true, true) do if line1 == line2 and col1 == col2 then local text = dv.doc:get_text(line1, 1, line1, col1) @@ -260,8 +260,8 @@ local commands = { ["docview:select-word"] = function(dv) for idx, line1, col1 in dv.doc:get_selections(true) do - local line1, col1 = translate.start_of_word(dv.doc, line1, col1) - local line2, col2 = translate.end_of_word(dv.doc, line1, col1) + local line1, col1 = translate.start_of_word(dv, line1, col1) + local line2, col2 = translate.end_of_word(dv, line1, col1) dv:set_selections(idx, line2, col2, line1, col1) end end, diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 05a93ca61..e626c4121 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -39,7 +39,7 @@ local function update_preview(sel, search_fn, text) sel[1], sel[2], text, case_sensitive, find_regex) if ok and line1 and text ~= "" then last_view.doc:set_selection(line2, col2, line1, col1) - last_view:scroll_to_line(line2, true) + last_view:scroll_to_line(line2, true, nil, col2) found_expression = true else last_view.doc:set_selection(table.unpack(sel)) @@ -277,7 +277,7 @@ command.add(valid_for_finding, { local line1, col1, line2, col2 = last_fn(dv.doc, sl2, sc2, last_text, case_sensitive, find_regex, false) if line1 then dv.doc:set_selection(line2, col2, line1, col1) - dv:scroll_to_line(line2, true) + dv:scroll_to_line(line2, true, nil, col2) else core.error("Couldn't find %q", last_text) end @@ -292,7 +292,7 @@ command.add(valid_for_finding, { local line1, col1, line2, col2 = last_fn(dv.doc, sl1, sc1, last_text, case_sensitive, find_regex, true) if line1 then dv.doc:set_selection(line2, col2, line1, col1) - dv:scroll_to_line(line2, true) + dv:scroll_to_line(line2, true, nil, col2) else core.error("Couldn't find %q", last_text) end diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 2e7d753f3..152faa90e 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -148,13 +148,13 @@ function Doc:sanitize_position(line, col) return line, common.clamp(col, 1, #self.lines[line]) end -local function position_offset_func(self, line, col, fn, ...) +function Doc:position_offset_func(line, col, fn, ...) line, col = self:sanitize_position(line, col) return fn(self, line, col, ...) end -local function position_offset_byte(self, line, col, offset) +function Doc:position_offset_byte(line, col, offset) line, col = self:sanitize_position(line, col) col = col + offset while line > 1 and col < 1 do @@ -169,18 +169,18 @@ local function position_offset_byte(self, line, col, offset) end -local function position_offset_linecol(self, line, col, lineoffset, coloffset) +function Doc:position_offset_linecol(line, col, lineoffset, coloffset) return self:sanitize_position(line + lineoffset, col + coloffset) end function Doc:position_offset(line, col, ...) if type(...) ~= "number" then - return position_offset_func(self, line, col, ...) + return self:position_offset_func(line, col, ...) elseif select("#", ...) == 1 then - return position_offset_byte(self, line, col, ...) + return self:position_offset_byte(line, col, ...) elseif select("#", ...) == 2 then - return position_offset_linecol(self, line, col, ...) + return self:position_offset_linecol(line, col, ...) else error("bad number of arguments") end @@ -294,7 +294,7 @@ function Doc:insert(line, col, text) self.clean_change_id = -1 end line, col = self:sanitize_position(line, col) - self:insert(line, col, text, self.undo_stack, system.get_time()) + self:raw_insert(line, col, text, self.undo_stack, system.get_time()) end function Doc:remove(line1, col1, line2, col2) @@ -302,7 +302,7 @@ function Doc:remove(line1, col1, line2, col2) line1, col1 = self:sanitize_position(line1, col1) line2, col2 = self:sanitize_position(line2, col2) line1, col1, line2, col2 = common.sort_positions(line1, col1, line2, col2) - self:remove(line1, col1, line2, col2, self.undo_stack, system.get_time()) + self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time()) end diff --git a/data/core/doc/translate.lua b/data/core/doc/translate.lua index d1bde5f0f..651a78cad 100644 --- a/data/core/doc/translate.lua +++ b/data/core/doc/translate.lua @@ -3,6 +3,7 @@ local config = require "core.config" -- functions for translating a Doc position to another position these functions -- can be passed to Doc:move_to|select_to|delete_to() +-- The doc variable can also be passed a Docview in order to account for virtual lines. local translate = {} diff --git a/data/core/docview.lua b/data/core/docview.lua index 017f62e0a..2bf0d50db 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -4,6 +4,7 @@ local config = require "core.config" local style = require "core.style" local keymap = require "core.keymap" local translate = require "core.doc.translate" +local Doc = require "core.doc" local ime = require "core.ime" local View = require "core.view" @@ -13,43 +14,43 @@ local DocView = View:extend() DocView.context = "session" -local function move_to_line_offset(dv, line, col, offset) +local function move_to_line_offset(dv, dline, dcol, offset) + local vline, vcol = dv:get_closest_vline(dline, dcol) local xo = dv.last_x_offset - if xo.line ~= line or xo.col ~= col then - xo.offset = dv:get_col_x_offset(line, col) + if xo.line ~= vline or xo.col ~= vcol then + xo.x, xo.y = dv:get_line_screen_position(vline, vcol) end - xo.line = line + offset - xo.col = dv:get_x_offset_col(line + offset, xo.offset) - return xo.line, xo.col + xo.line = dv:get_dline(vline + offset, dcol) + return xo.line, vcol end DocView.translate = { - ["previous_page"] = function(doc, line, col, dv) - local min, max = dv:get_visible_virutal_line_range() - return line - (max - min), 1 + ["previous_page"] = function(self, dline, col) + local min, max = self:get_visible_virutal_line_range() + return dline - (max - min), 1 end, - ["next_page"] = function(doc, line, col, dv) - if line == #doc.lines then - return #doc.lines, #doc.lines[line] + ["next_page"] = function(self, dline, col) + if dline == #doc.lines then + return #self.doc.lines, #self.doc.lines[dline] end - local min, max = dv:get_visible_virutal_line_range() - return line + (max - min), 1 + local min, max = self:get_visible_virutal_line_range() + return dline + (max - min), 1 end, - ["previous_line"] = function(doc, line, col, dv) - if line == 1 then + ["previous_line"] = function(self, dline, col) + if dline == 1 then return 1, 1 end - return move_to_line_offset(dv, line, col, -1) + return move_to_line_offset(self, dline, col, -1) end, - ["next_line"] = function(doc, line, col, dv) - if line == #doc.lines then - return #doc.lines, math.huge + ["next_line"] = function(self, dline, col) + if dline == #self.doc.lines then + return #self.doc.lines, math.huge end - return move_to_line_offset(dv, line, col, 1) + return move_to_line_offset(self, dline, col, 1) end, } @@ -68,6 +69,7 @@ function DocView:new(doc) self.vcache = {} self.dcache = {} self.vtodcache = {} + self.dtovcache = {} self.selections = { 1, 1, 1, 1 } self.last_selection = 1 self.v_scrollbar:set_forced_status(config.force_scrollbar_status) @@ -75,6 +77,135 @@ function DocView:new(doc) table.insert(doc.listeners, function(...) self:listener(...) end) end +function DocView:get_char(line, col) + line, col = self.doc:sanitize_position(line, col) + return self.doc.lines[line]:sub(col, col) +end + +function DocView:position_offset_byte(line, col, offset) + local token_idx = self:get_dline_token_idx(line, col) + if offset > 0 then + if self.tokens[token_idx+1] ~= line or col < self.tokens[token_idx+2] then + line, col = self.tokens[token_idx+1], self.tokens[token_idx+3] + end + local total_offset = (col + offset) - self.tokens[token_idx+2] + for i = token_idx, #self.tokens, 5 do + if self.tokens[i] == "doc" then + local width = (self.tokens[i+3] - self.tokens[i+2]) + 1 + if total_offset < width then + return self.tokens[i+1], self.tokens[i+2] + total_offset + end + total_offset = total_offset - width + end + end + return #self.doc.lines, #self.doc.lines[#self.doc.lines] + else + if self.tokens[token_idx+1] ~= line or col < self.tokens[token_idx+2] then + line, col = self.tokens[token_idx+1], self.tokens[token_idx+3] + end + local total_offset = self.tokens[token_idx+3] - (col + offset) + for i = token_idx, 1, -5 do + if self.tokens[i] == "doc" then + local width = (self.tokens[i+3] - self.tokens[i+2]) + 1 + if total_offset < width then + return self.tokens[i+1], self.tokens[i+3] - total_offset + end + total_offset = total_offset - width + end + end + end + return 1,1 +end + +function DocView:sanitize_position(line, col) + return self.doc:sanitize_position(line, col) +end + +DocView.position_offset_func = Doc.position_offset_func +DocView.position_offset = Doc.position_offset + +function DocView:get_closest_vline(dline, dcol) + return self.dtovcache[dline], dcol +end + +function DocView:get_dline(vline, vcol) + return self.vtodcache[vline], vcol +end + +-- Takes position in virtual space. +function DocView:get_line_screen_position(vline, col) + local x, y = self:get_content_offset() + local lh = self:get_line_height() + local gw = self:get_gutter_width() + y = y + (vline-1) * lh + style.padding.y + if col then + local default_font = self:get_font() + local _, indent_size = self.doc:get_indent_info() + default_font:set_tab_size(indent_size) + local column = 1 + local xoffset = 0 + for _, text, style in self:each_vline_token(vline) do + local font = style.font or default_font + if font ~= default_font then font:set_tab_size(indent_size) end + local length = #text + if column + length <= col then + xoffset = xoffset + font:get_width(text) + column = column + length + if column >= col then + return x + gw + xoffset, y + end + else + for char in common.utf8_chars(text) do + if column >= col then + return x + gw + xoffset, y + end + xoffset = xoffset + font:get_width(char) + column = column + #char + end + end + end + return x + gw + xoffset, y + else + return x + gw, y + end +end + +-- Returns cursor position in virtual space. +function DocView:resolve_screen_position(x, y) + local ox, oy = self:get_line_screen_position(1) + local line = math.floor((y - oy) / self:get_line_height()) + 1 + line = common.clamp(line, 1, #self.doc.lines) + local line_text = self.doc.lines[line] + + local xoffset, last_i, i = 0, 1, 1 + local default_font = self:get_font() + local _, indent_size = self.doc:get_indent_info() + default_font:set_tab_size(indent_size) + for _, text, style in self:each_vline_token(line) do + local font = style.font or default_font + if font ~= default_font then font:set_tab_size(indent_size) end + local width = font:get_width(text) + -- Don't take the shortcut if the width matches x, + -- because we need last_i which should be calculated using utf-8. + if xoffset + width < x then + xoffset = xoffset + width + i = i + #text + else + for char in common.utf8_chars(text) do + local w = font:get_width(char) + if xoffset >= x then + return line, (xoffset - x > w / 2) and last_i or i + end + xoffset = xoffset + w + last_i = i + i = i + #char + end + end + end + return line, #line_text +end + + -- Cursor section. Cursor indices are *only* valid during a get_selections() call. -- Cursors will always be iterated in order from top to bottom. Through normal operation -- curors can never swap positions; only merge or split, or change their position in cursor @@ -190,10 +321,30 @@ end -- If idx_reverse is true, it'll reverse iterate. If nil, or false, regular iterate. -- If a number, runs for exactly that iteration. function DocView:get_selections(sort_intra, idx_reverse) - return selection_iterator, { self.selections, sort_intra, idx_reverse }, + return selection_iterator, { self.selections, sort_intra, idx_reverse, self }, idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1) end + +local function vselection_iterator(invariant, idx) + local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1) + if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end + local line1, col1, line2, col2 + if invariant[2] then + line1, col1, line2, col2 = common.sort_positions(table.unpack(invariant[1], target, target + 4)) + else + line1, col1, line2, col2 = table.unpack(invariant[1], target, target + 4) + end + line1, col1 = invariant[4]:get_closest_vline(line1, col1) + line2, col2 = invariant[4]:get_closest_vline(line2, col2) + return idx + (invariant[3] and -1 or 1), line1, col1, line2, col2 +end + + +function DocView:get_vselections(...) + return vselection_iterator, select(2, self:get_selections(...)) +end + -- End of cursor seciton. function DocView:sanitize_selection() @@ -214,7 +365,7 @@ function DocView:text_input(text, idx) and not had_selection and col1 < #self.lines[line1] and text:ulen() == 1 then - self.doc:remove(line1, col1, translate.next_char(self.doc, line1, col1)) + self.doc:remove(line1, col1, translate.next_char(self, line1, col1)) end self.doc:insert(line1, col1, text) @@ -223,6 +374,7 @@ function DocView:text_input(text, idx) end function DocView:listener(type, text, line1, col1, line2, col2) + self:invalidate_cache(line1, line2) -- keep cursors where they should be on insertion -- for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do -- if cline1 < line then break end @@ -339,7 +491,7 @@ function DocView:delete_to_cursor(idx, ...) if line1 ~= line2 or col1 ~= col2 then self.doc:remove(line1, col1, line2, col2) else - local l2, c2 = self.doc:position_offset(line1, col1, ...) + local l2, c2 = self:position_offset(line1, col1, ...) self.doc:remove(line1, col1, l2, c2) line1, col1 = common.sort_positions(line1, col1, l2, c2) end @@ -352,7 +504,7 @@ function DocView:delete_to(...) return self:delete_to_cursor(nil, ...) end function DocView:move_to_cursor(idx, ...) for sidx, line, col in self:get_selections(false, idx) do - self:set_selections(sidx, self.doc:position_offset(line, col, ...)) + self:set_selections(sidx, self:position_offset(line, col, ...)) end self:merge_cursors(idx) end @@ -361,7 +513,7 @@ function DocView:move_to(...) return self:move_to_cursor(nil, ...) end function DocView:select_to_cursor(idx, ...) for sidx, line, col, line2, col2 in self:get_selections(false, idx) do - line, col = self.doc:position_offset(line, col, ...) + line, col = self:position_offset(line, col, ...) self:set_selections(sidx, line, col, line2, col2) end self:merge_cursors(idx) @@ -468,19 +620,6 @@ function DocView:get_gutter_width() return self:get_font():get_width(#self.doc.lines) + padding, padding end - -function DocView:get_line_screen_position(line, col) - local x, y = self:get_content_offset() - local lh = self:get_line_height() - local gw = self:get_gutter_width() - y = y + (line-1) * lh + style.padding.y - if col then - return x + gw + self:get_col_x_offset(line, col), y - else - return x + gw, y - end -end - function DocView:get_line_text_y_offset() local lh = self:get_line_height() local th = self:get_font():get_height() @@ -497,83 +636,12 @@ function DocView:get_visible_virutal_line_range() end -function DocView:get_col_x_offset(line, col) - local default_font = self:get_font() - local _, indent_size = self.doc:get_indent_info() - default_font:set_tab_size(indent_size) - local column = 1 - local xoffset = 0 - for _, text, style in self:each_vline_token(line) do - local font = style.font or default_font - if font ~= default_font then font:set_tab_size(indent_size) end - local length = #text - if column + length <= col then - xoffset = xoffset + font:get_width(text) - column = column + length - if column >= col then - return xoffset - end - else - for char in common.utf8_chars(text) do - if column >= col then - return xoffset - end - xoffset = xoffset + font:get_width(char) - column = column + #char - end - end - end - - return xoffset -end - - -function DocView:get_x_offset_col(line, x) - local line_text = self.doc.lines[line] - - local xoffset, last_i, i = 0, 1, 1 - local default_font = self:get_font() - local _, indent_size = self.doc:get_indent_info() - default_font:set_tab_size(indent_size) - for _, text, style in self:each_vline_token(line) do - local font = style.font or default_font - if font ~= default_font then font:set_tab_size(indent_size) end - local width = font:get_width(text) - -- Don't take the shortcut if the width matches x, - -- because we need last_i which should be calculated using utf-8. - if xoffset + width < x then - xoffset = xoffset + width - i = i + #text - else - for char in common.utf8_chars(text) do - local w = font:get_width(char) - if xoffset >= x then - return (xoffset - x > w / 2) and last_i or i - end - xoffset = xoffset + w - last_i = i - i = i + #char - end - end - end - - return #line_text -end - - -function DocView:resolve_screen_position(x, y) - local ox, oy = self:get_line_screen_position(1) - local line = math.floor((y - oy) / self:get_line_height()) + 1 - line = common.clamp(line, 1, #self.doc.lines) - local col = self:get_x_offset_col(line, x - ox) - return line, col -end -function DocView:scroll_to_line(line, ignore_if_visible, instant) +function DocView:scroll_to_line(line, ignore_if_visible, instant, col) local min, max = self:get_visible_virutal_line_range() if not (ignore_if_visible and line > min and line < max) then - local x, y = self:get_line_screen_position(line) + local x, y = self:get_line_screen_position(line, col) local ox, oy = self:get_content_offset() local _, _, _, scroll_h = self.h_scrollbar:get_track_rect() self.scroll.to.y = math.max(0, y - oy - (self.size.y - scroll_h) / 2) @@ -591,15 +659,13 @@ end function DocView:scroll_to_make_visible(line, col) local _, oy = self:get_content_offset() - local _, ly = self:get_line_screen_position(line, col) + local lx, ly = self:get_line_screen_position(line, col) local lh = self:get_line_height() local _, _, _, scroll_h = self.h_scrollbar:get_track_rect() self.scroll.to.y = common.clamp(self.scroll.to.y, ly - oy - self.size.y + scroll_h + lh * 2, ly - oy - lh) - local gw = self:get_gutter_width() - local xoffset = self:get_col_x_offset(line, col) local xmargin = 3 * self:get_font():get_width(' ') - local xsup = xoffset + gw + xmargin - local xinf = xoffset - xmargin + local xsup = lx + self:get_gutter_width() + xmargin + local xinf = lx - xmargin local _, _, scroll_w = self.v_scrollbar:get_track_rect() local size_x = math.max(0, self.size.x - scroll_w) if xsup > self.scroll.x + size_x then @@ -649,8 +715,8 @@ function DocView:mouse_selection(doc, snap_type, line1, col1, line2, col2) line1, col1, line2, col2 = line2, col2, line1, col1 end if snap_type == "word" then - line1, col1 = translate.start_of_word(doc, line1, col1) - line2, col2 = translate.end_of_word(doc, line2, col2) + line1, col1 = translate.start_of_word(self, line1, col1) + line2, col2 = translate.end_of_word(self, line2, col2) elseif snap_type == "lines" then col1, col2, line2 = 1, 1, line2 + 1 end @@ -799,7 +865,7 @@ function DocView:draw_line_body(vline, x, y) local draw_highlight = false local hcl = config.highlight_current_line if hcl ~= false then - for lidx, line1, col1, line2, col2 in self:get_selections(false) do + for lidx, line1, col1, line2, col2 in self:get_vselections(false) do if line1 == vline then if hcl == "no_selection" then if (line1 ~= line2) or (col1 ~= col2) then @@ -818,13 +884,13 @@ function DocView:draw_line_body(vline, x, y) -- draw selection if it overlaps this line local lh = self:get_line_height() - for lidx, line1, col1, line2, col2 in self:get_selections(true) do + for lidx, line1, col1, line2, col2 in self:get_vselections(true) do if vline >= line1 and vline <= line2 then local text = self.doc.lines[vline] if line1 ~= vline then col1 = 1 end if line2 ~= vline then col2 = #text + 1 end - local x1 = x + self:get_col_x_offset(vline, col1) - local x2 = x + self:get_col_x_offset(vline, col2) + local _, x1 = x + self:get_line_screen_position(vline, col1) + local _, x2 = x + self:get_line_screen_position(vline, col2) if x1 ~= x2 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end @@ -838,7 +904,7 @@ end function DocView:draw_line_gutter(vline, x, y, width) local color = style.line_number - for _, line1, _, line2 in self:get_selections(true) do + for _, line1, _, line2 in self:get_vselections(true) do if vline >= line1 and vline <= line2 then color = style.line_number2 break @@ -880,7 +946,7 @@ function DocView:draw_overlay() local minline, maxline = self:get_visible_virutal_line_range() -- draw caret if it overlaps this line local T = config.blink_period - for _, line1, col1, line2, col2 in self:get_selections() do + for _, line1, col1, line2, col2 in self:get_vselections() do if line1 >= minline and line1 <= maxline and system.window_has_focus() then if ime.editing then @@ -932,18 +998,20 @@ function DocView:draw() end +-- Selections are in document space. -- Transform function for lines from the doc. -- Plugins hook this to return a line/col list from `doc`, or provide a virtual line. -- `{ "doc", doc_line, 1, #self.doc.lines[doc_line], style }` --- `{ "virtual", text, false, false, style } +-- `{ "virtual", doc_line, text, false, style } function DocView:transform(doc_line) - return { "doc", doc_line, 1, #self.doc.lines[doc_line] - 1, { } } + return { "doc", doc_line, 1, #self.doc.lines[doc_line], { } } end --[[ self.vcache maps virtual line numbers to the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. self.dcache maps doc line number sto the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. self.vtodcache maps the virtual line number to the relevant doc line number; not foundational to the algorithm; purely cosmetic. +self.dtovcache maps the doc line number onto the earliest relevant virtual line number. self.tokens contains the stream of transformed tokens. Each token can contain *at most* one new line, at the end of it. ]] @@ -956,7 +1024,7 @@ function DocView:invalidate_cache(start_doc_line) end function DocView:get_token_text(type, doc_line, col_start, col_end) - return type == "doc" and self.doc.lines[doc_line]:sub(col_start, col_end) or doc_line + return type == "doc" and self.doc.lines[doc_line]:sub(col_start, col_end) or col_start end @@ -969,6 +1037,7 @@ local function retrieve_tokens(self, vline) table.insert(self.vcache, #self.tokens + 1) self.vtodcache[#self.vcache] = #self.dcache end + self.dtovcache[#self.dcache] = #self.vcache for j = 1, #tokens, 5 do local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) @@ -1003,8 +1072,17 @@ local function dline_iter(state, idx) return idx + 5, tokens[idx], tokens[idx+1], tokens[idx+2], tokens[idx+3], tokens[idx+4] end -function DocView:each_dline_token(tokens) - return dline_iter, { self, tokens }, 1 +function DocView:each_dline_token(tokens, idx) + return dline_iter, { self, tokens }, idx or 1 +end + +function DocView:get_dline_token_idx(dline, dcol) + for i = self.dcache[dline], #self.tokens, 5 do + if self.tokens[i] == "doc" and (self.tokens[i+1] ~= dline or dcol >= self.tokens[i+2]) then + return i + end + end + return 1 end return DocView diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index 430dc53c2..7a1e218ad 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -46,18 +46,21 @@ end local old_transform = DocView.transform function DocView:transform(doc_line) local tokens = old_transform(self, doc_line) + if not self.foldable then return tokens end self:compute_fold(doc_line) if self.folded[doc_line] then return {} end if self:is_foldable(doc_line) and self.folded[doc_line+1] then -- remove the newline from the end of the tokens + local type, line, e = tokens[#tokens - 4], tokens[#tokens - 3], tokens[#tokens - 1] + if type == "doc" and self.doc.lines[line]:sub(e, e) == "\n" then tokens[#tokens - 1] = tokens[#tokens - 1] - 1 end table.insert(tokens, "virtual") + table.insert(tokens, doc_line) table.insert(tokens, " ... ") table.insert(tokens, false) - table.insert(tokens, false) table.insert(tokens, { color = style.dim }) table.insert(tokens, "virtual") - table.insert(tokens, "}") - table.insert(tokens, false) + table.insert(tokens, doc_line) + table.insert(tokens, "}\n") table.insert(tokens, false) table.insert(tokens, { }) end @@ -72,20 +75,20 @@ function DocView:is_foldable(doc_line) return false end -function DocView:toggle_fold(doc_line, value) - if self:is_foldable(doc_line) then - if value == nil then value = not self:is_folded(doc_line) end - local starting_fold = self.foldable[doc_line] - local line = doc_line + 1 - self:invalidate_cache(doc_line) - while line <= #self.doc.lines do - if self.foldable[line] <= starting_fold then - if self.doc.lines[line]:find("}%s*$") then self.folded[line] = value end +function DocView:toggle_fold(start_doc_line, value) + if self:is_foldable(start_doc_line) then + if value == nil then value = not self:is_folded(start_doc_line) end + local starting_fold = self.foldable[start_doc_line] + local end_doc_line = start_doc_line + 1 + while end_doc_line <= #self.doc.lines do + if self.foldable[end_doc_line] <= starting_fold then + if self.doc.lines[end_doc_line]:find("}%s*$") then self.folded[end_doc_line] = value end break end - self.folded[line] = value - line = line + 1 + self.folded[end_doc_line] = value + end_doc_line = end_doc_line + 1 end + self:invalidate_cache(start_doc_line, end_doc_line) end end diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index a156349ac..ea51fe74a 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -79,20 +79,34 @@ local function split_token(font, text, mode, width) return nil end +local old_draw = DocView.draw +function DocView:draw() + old_draw(self) + if self.wrapping and config.plugins.linewrapping.guide and config.plugins.linewrapping.width_override then + local width = config.plugins.linewrapping.width_override + if type(width) == "function" then width = width(self) end + + local x, y = docview:get_content_offset() + local gw = docview:get_gutter_width() + renderer.draw_rect(x + gw + width, y, 1, core.root_view.size.y, style.selection) + end +end + local old_transform = DocView.transform function DocView:transform(doc_line) if not self.wrapping then return old_transform(self, doc_line) end local tokens = {} + local x, y = self:get_content_offset() local gw = self:get_gutter_width() - local width = self.size.x + local width = self.size.x - gw if config.plugins.linewrapping.width_override then width = config.plugins.linewrapping.width_override if type(width) == "function" then width = width(self) end end - local docstart = self.position.x + gw + style.padding.x + local docstart = x + gw + style.padding.x local offset = docstart - local docend = self.position.x + width + local docend = docstart + width for _, type, l, s, e, style in self:each_dline_token(old_transform(self, doc_line)) do local font = self:get_font() or style.font diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 8c8dfd951..552e11103 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -106,7 +106,7 @@ function ResultsView:open_selected_result() local dv = core.root_view:open_doc(core.open_doc(res.file)) core.root_view.root_node:update_layout() dv.doc:set_selection(res.line, res.col) - dv:scroll_to_line(res.line, false, true) + dv:scroll_to_line(res.line, false, true, res.col) end) return true end From 4792949c2e58bb5589695dc47fed3f1a333a76a6 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Mon, 1 Jan 2024 17:06:56 -0500 Subject: [PATCH 05/24] We proceed apace. --- data/core/docview.lua | 207 ++++++++++++++++++++++------------ data/plugins/linewrapping.lua | 11 +- 2 files changed, 136 insertions(+), 82 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 2bf0d50db..4737c3bb5 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -18,25 +18,25 @@ local function move_to_line_offset(dv, dline, dcol, offset) local vline, vcol = dv:get_closest_vline(dline, dcol) local xo = dv.last_x_offset if xo.line ~= vline or xo.col ~= vcol then - xo.x, xo.y = dv:get_line_screen_position(vline, vcol) + xo.x, xo.y = dv:get_line_screen_position(dline, dcol) end - xo.line = dv:get_dline(vline + offset, dcol) - return xo.line, vcol + xo.line, xo.col = dv:get_dline(math.max(vline + offset, 1), vcol) + return xo.line, xo.col end DocView.translate = { ["previous_page"] = function(self, dline, col) local min, max = self:get_visible_virutal_line_range() - return dline - (max - min), 1 + return move_to_line_offset(self, dline, col, (min - max)) end, ["next_page"] = function(self, dline, col) - if dline == #doc.lines then + if dline == #self.doc.lines then return #self.doc.lines, #self.doc.lines[dline] end local min, max = self:get_visible_virutal_line_range() - return dline + (max - min), 1 + return move_to_line_offset(self, dline, col, (max - min)) end, ["previous_line"] = function(self, dline, col) @@ -68,8 +68,8 @@ function DocView:new(doc) self.tokens = {} self.vcache = {} self.dcache = {} - self.vtodcache = {} self.dtovcache = {} + self.lines = doc.lines self.selections = { 1, 1, 1, 1 } self.last_selection = 1 self.v_scrollbar:set_forced_status(config.force_scrollbar_status) @@ -124,46 +124,115 @@ end DocView.position_offset_func = Doc.position_offset_func DocView.position_offset = Doc.position_offset + + +local function retrieve_tokens(self, vline, dline) + while ((vline and vline > #self.vcache) or (dline and dline > #self.dcache)) and #self.dcache < #self.doc.lines do + local tokens = self:transform(#self.dcache + 1) + local bundles = #tokens / 5 + table.insert(self.dcache, #self.tokens + 1) + if #tokens > 0 then + table.insert(self.vcache, #self.tokens + 1) + end + self.dtovcache[#self.dcache] = #self.vcache + for j = 1, #tokens, 5 do + local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) + table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) + if text:find("\n$") and j < #tokens - 5 then + table.insert(self.vcache, #self.tokens + 1) + end + end + end + if vline then return self.vcache[vline] end + return self.dcache[dline] +end + + function DocView:get_closest_vline(dline, dcol) - return self.dtovcache[dline], dcol + local token_idx = retrieve_tokens(self, nil, dline) + if not token_idx then + return #self.vcache + 1, 1 + end + if self.tokens[token_idx+1] ~= dline then return self.dtovcache[dline] end + local total_line_length = 0 + local total_token_length = 0 + local vline = self.dtovcache[dline] + if dcol and dcol > 1 then + for _, text, _, type in self:each_dline_token(dline) do + if type == "doc" then + local length = text:ulen() or #text + if dcol <= total_token_length + total_line_length + length then + return vline, dcol - total_line_length + end + total_token_length = total_token_length + length + end + if text:find("\n$") then + total_line_length = total_line_length + total_token_length + vline = vline + 1 + end + end + end + return vline, 1 end function DocView:get_dline(vline, vcol) - return self.vtodcache[vline], vcol + local dline = self.tokens[retrieve_tokens(self, vline) + 1] + local total_line_length = 0 + local current_line = self.dtovcache[dline] + for _, text, _, type in self:each_dline_token(dline) do + if current_line == vline then + return dline, vcol + total_line_length + end + if type == "doc" then total_line_length = total_line_length + text:ulen() end + if text:find("\n$") then + current_line = current_line + 1 + end + end + return dline, vcol +end + +function DocView:get_virtual_line_offset(vline) + local x, y = self:get_content_offset() + local gw = self:get_gutter_width() + return x + gw, y end --- Takes position in virtual space. -function DocView:get_line_screen_position(vline, col) +function DocView:get_line_screen_position(line, col) local x, y = self:get_content_offset() local lh = self:get_line_height() local gw = self:get_gutter_width() - y = y + (vline-1) * lh + style.padding.y - if col then + local vline, vcol = self:get_closest_vline(line, col) + y = y + (vline-1) * lh + if col and self.vcache[vline] and self.tokens[self.vcache[vline]+1] == line then local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) local column = 1 local xoffset = 0 - for _, text, style in self:each_vline_token(vline) do + for _, text, style, type in self:each_vline_token(vline) do local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end - local length = #text - if column + length <= col then - xoffset = xoffset + font:get_width(text) - column = column + length - if column >= col then - return x + gw + xoffset, y - end - else - for char in common.utf8_chars(text) do - if column >= col then - return x + gw + xoffset, y + local length = text:ulen() + if type == "doc" then + if column + length < vcol then + xoffset = xoffset + font:get_width(text) + column = column + length + else + for char in common.utf8_chars(text) do + if column >= vcol then + return x + gw + xoffset, y + end + xoffset = xoffset + font:get_width(char) + column = column + 1 end - xoffset = xoffset + font:get_width(char) - column = column + #char end + else + xoffset = xoffset + font:get_width(text) end end + if line == 8 then + print("VLINE", line, col, vline, vcol) + end return x + gw + xoffset, y else return x + gw, y @@ -172,7 +241,7 @@ end -- Returns cursor position in virtual space. function DocView:resolve_screen_position(x, y) - local ox, oy = self:get_line_screen_position(1) + local ox, oy = self:get_virtual_line_offset(1) local line = math.floor((y - oy) / self:get_line_height()) + 1 line = common.clamp(line, 1, #self.doc.lines) local line_text = self.doc.lines[line] @@ -593,6 +662,7 @@ end function DocView:get_scrollable_size() + local max_lines = math.max(#self.doc.lines, #self.vcache) if not config.scroll_past_end then local _, _, _, h_scroll = self.h_scrollbar:get_track_rect() return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2 + h_scroll @@ -658,14 +728,14 @@ end function DocView:scroll_to_make_visible(line, col) - local _, oy = self:get_content_offset() + local ox, oy = self:get_content_offset() local lx, ly = self:get_line_screen_position(line, col) local lh = self:get_line_height() local _, _, _, scroll_h = self.h_scrollbar:get_track_rect() self.scroll.to.y = common.clamp(self.scroll.to.y, ly - oy - self.size.y + scroll_h + lh * 2, ly - oy - lh) local xmargin = 3 * self:get_font():get_width(' ') - local xsup = lx + self:get_gutter_width() + xmargin - local xinf = lx - xmargin + local xsup = lx + self:get_gutter_width() + xmargin - ox + local xinf = lx - self:get_gutter_width() - xmargin - ox local _, _, scroll_w = self.v_scrollbar:get_track_rect() local size_x = math.max(0, self.size.x - scroll_w) if xsup > self.scroll.x + size_x then @@ -889,8 +959,8 @@ function DocView:draw_line_body(vline, x, y) local text = self.doc.lines[vline] if line1 ~= vline then col1 = 1 end if line2 ~= vline then col2 = #text + 1 end - local _, x1 = x + self:get_line_screen_position(vline, col1) - local _, x2 = x + self:get_line_screen_position(vline, col2) + local _, x1 = x + self:get_line_screen_position(line1, col1) + local _, x2 = x + self:get_line_screen_position(line2, col2) if x1 ~= x2 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end @@ -912,7 +982,9 @@ function DocView:draw_line_gutter(vline, x, y, width) end x = x + style.padding.x local lh = self:get_line_height() - common.draw_text(self:get_font(), color, self.vtodcache[vline] or vline, "right", x, y, width, lh) + if self:is_first_line_of_block(vline) then + common.draw_text(self:get_font(), color, self.tokens[self.vcache[vline] + 1], "right", x, y, width, lh) + end return lh end @@ -943,12 +1015,10 @@ end function DocView:draw_overlay() if core.active_view == self then - local minline, maxline = self:get_visible_virutal_line_range() -- draw caret if it overlaps this line local T = config.blink_period - for _, line1, col1, line2, col2 in self:get_vselections() do - if line1 >= minline and line1 <= maxline - and system.window_has_focus() then + for _, line1, col1, line2, col2 in self:get_selections() do + if system.window_has_focus() then if ime.editing then self:draw_ime_decoration(line1, col1, line2, col2) else @@ -975,7 +1045,7 @@ function DocView:draw() local minline, maxline = self:get_visible_virutal_line_range() local lh = self:get_line_height() - local x, y = self:get_line_screen_position(minline) + local x, y = self:get_virtual_line_offset(minline) local gw, gpad = self:get_gutter_width() for i = minline, maxline do if self:has_tokens(i) then @@ -984,7 +1054,7 @@ function DocView:draw() end local pos = self.position - x, y = self:get_line_screen_position(minline) + x, y = self:get_virtual_line_offset(minline) -- the clip below ensure we don't write on the gutter region. On the -- right side it is redundant with the Node's clip. core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y) @@ -1010,12 +1080,16 @@ end --[[ self.vcache maps virtual line numbers to the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. self.dcache maps doc line number sto the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. -self.vtodcache maps the virtual line number to the relevant doc line number; not foundational to the algorithm; purely cosmetic. self.dtovcache maps the doc line number onto the earliest relevant virtual line number. self.tokens contains the stream of transformed tokens. Each token can contain *at most* one new line, at the end of it. ]] +function DocView:is_first_line_of_block(vline) + local token_idx = self.vcache[vline] + return token_idx == self.dcache[self.tokens[token_idx + 1]] +end + function DocView:invalidate_cache(start_doc_line) if not start_doc_line then start_doc_line = 1 end while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end @@ -1028,56 +1102,39 @@ function DocView:get_token_text(type, doc_line, col_start, col_end) end -local function retrieve_tokens(self, vline) - while vline > #self.vcache and #self.dcache < #self.doc.lines do - local tokens = self:transform(#self.dcache + 1) - local bundles = #tokens / 5 - table.insert(self.dcache, #self.tokens + 1) - if #tokens > 0 then - table.insert(self.vcache, #self.tokens + 1) - self.vtodcache[#self.vcache] = #self.dcache - end - self.dtovcache[#self.dcache] = #self.vcache - for j = 1, #tokens, 5 do - local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) - table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) - if text:find("\n$") and j < #tokens - 5 then - table.insert(self.vcache, #self.tokens + 1) - self.vtodcache[#self.vcache] = #self.dcache - end - end - end - return self.vcache[vline] +function DocView:has_tokens(vline) + local token_idx = retrieve_tokens(self, vline) + return token_idx and token_idx < #self.tokens and token_idx ~= retrieve_tokens(self, vline + 1) end -local function text_iter(state, idx) + +local function vline_iter(state, idx) local self, line = table.unpack(state) if not idx or not self.tokens[idx] or (self.vcache[line + 1] and idx >= self.vcache[line + 1]) then return nil end local text = self:get_token_text(self.tokens[idx], self.tokens[idx+1], self.tokens[idx+2], self.tokens[idx+3]) - return idx + 5, text, self.tokens[idx+4] -end - -function DocView:each_vline_token(vline) - return text_iter, { self, vline }, retrieve_tokens(self, vline) + return idx + 5, text, self.tokens[idx+4], self.tokens[idx] end -function DocView:has_tokens(vline) - local token_idx = retrieve_tokens(self, vline) - return token_idx and token_idx < #self.tokens and token_idx ~= retrieve_tokens(self, vline + 1) -end -local function dline_iter(state, idx) +local function token_iter(state, idx) local self, tokens = table.unpack(state) if idx > #tokens then return nil end return idx + 5, tokens[idx], tokens[idx+1], tokens[idx+2], tokens[idx+3], tokens[idx+4] end -function DocView:each_dline_token(tokens, idx) - return dline_iter, { self, tokens }, idx or 1 +local function dline_iter(state, idx) + local self, line = table.unpack(state) + if not idx or not self.tokens[idx] or self.tokens[idx+1] ~= line then return nil end + local text = self:get_token_text(self.tokens[idx], self.tokens[idx+1], self.tokens[idx+2], self.tokens[idx+3]) + return idx + 5, text, self.tokens[idx+4], self.tokens[idx] end +function DocView:each_vline_token(vline) return vline_iter, { self, vline }, retrieve_tokens(self, vline) end +function DocView:each_token(tokens, line) return token_iter, { self, tokens }, (((line or 1) - 1) * 5) + 1 end +function DocView:each_dline_token(line) return dline_iter, { self, line }, retrieve_tokens(self, nil, line) end + function DocView:get_dline_token_idx(dline, dcol) - for i = self.dcache[dline], #self.tokens, 5 do + for i = retrieve_tokens(self, nil, dline), #self.tokens, 5 do if self.tokens[i] == "doc" and (self.tokens[i+1] ~= dline or dcol >= self.tokens[i+2]) then return i end diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index ea51fe74a..1be2973bd 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -108,38 +108,35 @@ function DocView:transform(doc_line) local offset = docstart local docend = docstart + width - for _, type, l, s, e, style in self:each_dline_token(old_transform(self, doc_line)) do + for _, type, l, s, e, style in self:each_token(old_transform(self, doc_line)) do local font = self:get_font() or style.font while true do local text = self:get_token_text(type, l, s, e) local w = font:get_width(text) + table.insert(tokens, type) + table.insert(tokens, l) if offset + w >= docend then local i = split_token(font, text, config.plugins.linewrapping.mode, docend - offset) assert(i) - table.insert(tokens, type) if type == "doc" then - table.insert(tokens, l) table.insert(tokens, s) table.insert(tokens, i and (i - 1) or e) s = i else table.insert(tokens, i and l:sub(1, i) or l) table.insert(tokens, false) - table.insert(tokens, false) l = l:sub(i+1) end table.insert(tokens, style) table.insert(tokens, "virtual") + table.insert(tokens, doc_line) table.insert(tokens, "\n") table.insert(tokens, false) - table.insert(tokens, false) table.insert(tokens, style) offset = docstart else - table.insert(tokens, type) - table.insert(tokens, l) table.insert(tokens, s) table.insert(tokens, e) table.insert(tokens, style) From c5c5095394f647abba8723817db1b282db0e6125 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Tue, 2 Jan 2024 12:44:08 -0500 Subject: [PATCH 06/24] Fixed many things. --- data/core/commands/docview.lua | 2 +- data/core/docview.lua | 166 ++++++++++++++++----------------- data/plugins/codefolding.lua | 6 +- data/plugins/highlighter.lua | 6 +- data/plugins/linewrapping.lua | 8 +- 5 files changed, 94 insertions(+), 94 deletions(-) diff --git a/data/core/commands/docview.lua b/data/core/commands/docview.lua index f9223bb08..bd5f2f680 100644 --- a/data/core/commands/docview.lua +++ b/data/core/commands/docview.lua @@ -441,7 +441,7 @@ local commands = { end, ["docview:select-to-cursor"] = function(dv, x, y, clicks) - local line1, col1 = select(3, doc():get_selection()) + local line1, col1 = select(3, dv:get_selection()) local line2, col2 = dv:resolve_screen_position(x, y) dv.mouse_selecting = { line1, col1, nil } dv:set_selection(line2, col2, line1, col1) diff --git a/data/core/docview.lua b/data/core/docview.lua index 4737c3bb5..00dc5aedd 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -27,7 +27,7 @@ end DocView.translate = { ["previous_page"] = function(self, dline, col) - local min, max = self:get_visible_virutal_line_range() + local min, max = self:get_visible_virtual_line_range() return move_to_line_offset(self, dline, col, (min - max)) end, @@ -35,7 +35,7 @@ DocView.translate = { if dline == #self.doc.lines then return #self.doc.lines, #self.doc.lines[dline] end - local min, max = self:get_visible_virutal_line_range() + local min, max = self:get_visible_virtual_line_range() return move_to_line_offset(self, dline, col, (max - min)) end, @@ -125,31 +125,8 @@ DocView.position_offset_func = Doc.position_offset_func DocView.position_offset = Doc.position_offset - -local function retrieve_tokens(self, vline, dline) - while ((vline and vline > #self.vcache) or (dline and dline > #self.dcache)) and #self.dcache < #self.doc.lines do - local tokens = self:transform(#self.dcache + 1) - local bundles = #tokens / 5 - table.insert(self.dcache, #self.tokens + 1) - if #tokens > 0 then - table.insert(self.vcache, #self.tokens + 1) - end - self.dtovcache[#self.dcache] = #self.vcache - for j = 1, #tokens, 5 do - local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) - table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) - if text:find("\n$") and j < #tokens - 5 then - table.insert(self.vcache, #self.tokens + 1) - end - end - end - if vline then return self.vcache[vline] end - return self.dcache[dline] -end - - function DocView:get_closest_vline(dline, dcol) - local token_idx = retrieve_tokens(self, nil, dline) + local token_idx = self:retrieve_tokens(nil, dline) if not token_idx then return #self.vcache + 1, 1 end @@ -176,7 +153,7 @@ function DocView:get_closest_vline(dline, dcol) end function DocView:get_dline(vline, vcol) - local dline = self.tokens[retrieve_tokens(self, vline) + 1] + local dline = self.tokens[self:retrieve_tokens(vline) + 1] local total_line_length = 0 local current_line = self.dtovcache[dline] for _, text, _, type in self:each_dline_token(dline) do @@ -192,9 +169,10 @@ function DocView:get_dline(vline, vcol) end function DocView:get_virtual_line_offset(vline) + local lh = self:get_line_height() local x, y = self:get_content_offset() local gw = self:get_gutter_width() - return x + gw, y + return x + gw, y + lh * (vline - 1) end function DocView:get_line_screen_position(line, col) @@ -230,27 +208,23 @@ function DocView:get_line_screen_position(line, col) xoffset = xoffset + font:get_width(text) end end - if line == 8 then - print("VLINE", line, col, vline, vcol) - end return x + gw + xoffset, y else return x + gw, y end end --- Returns cursor position in virtual space. + function DocView:resolve_screen_position(x, y) local ox, oy = self:get_virtual_line_offset(1) - local line = math.floor((y - oy) / self:get_line_height()) + 1 - line = common.clamp(line, 1, #self.doc.lines) - local line_text = self.doc.lines[line] + local vline = math.floor((y - oy) / self:get_line_height()) + 1 - local xoffset, last_i, i = 0, 1, 1 + local xoffset, last_i, i = ox, 1, 1 local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) - for _, text, style in self:each_vline_token(line) do + local line = self.tokens[self.vcache[vline]+1] + for _, text, style in self:each_vline_token(vline) do local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end local width = font:get_width(text) @@ -258,7 +232,7 @@ function DocView:resolve_screen_position(x, y) -- because we need last_i which should be calculated using utf-8. if xoffset + width < x then xoffset = xoffset + width - i = i + #text + i = i + text:ulen() else for char in common.utf8_chars(text) do local w = font:get_width(char) @@ -267,11 +241,11 @@ function DocView:resolve_screen_position(x, y) end xoffset = xoffset + w last_i = i - i = i + #char + i = i + 1 end end end - return line, #line_text + return line, i end @@ -697,19 +671,23 @@ function DocView:get_line_text_y_offset() end -function DocView:get_visible_virutal_line_range() - local x, y, x2, y2 = self:get_content_bounds() +function DocView:get_visible_virtual_line_range() + local x1, y1, x2, y2 = self:get_content_bounds() local lh = self:get_line_height() - local minline = math.max(1, math.floor((y - style.padding.y) / lh) + 1) + local minline = math.max(1, math.floor((y1 - style.padding.y) / lh) + 1) local maxline = math.floor((y2 - style.padding.y) / lh) + 1 return minline, maxline end +function DocView:get_visible_line_range() + local minline, maxline = self:get_visible_virtual_line_range() + return self.vcache[self:retrieve_tokens(minline) + 1], self.vcache[self:retrieve_tokens(maxline) + 1] +end function DocView:scroll_to_line(line, ignore_if_visible, instant, col) - local min, max = self:get_visible_virutal_line_range() + local min, max = self:get_visible_virtual_line_range() if not (ignore_if_visible and line > min and line < max) then local x, y = self:get_line_screen_position(line, col) local ox, oy = self:get_content_offset() @@ -907,15 +885,21 @@ end local default_color = { common.color "#FFFFFF" } -function DocView:draw_line_text(vline, x, y) +function DocView:draw_line_text(line, x, y) local default_font = self:get_font() local tx, ty = x, y + self:get_line_text_y_offset() - for tidx, text, style in self:each_vline_token(vline) do + local otx = tx + local lines = 0 + for tidx, text, style in self:each_dline_token(line) do local font = style.font or default_font tx = renderer.draw_text(font, text, tx, ty, style.color or default_color) if tx > self.position.x + self.size.x then break end + if text:find("\n$") then + tx = otx + lines = lines + 1 + end end - return self:get_line_height() + return self:get_line_height() * lines end @@ -930,13 +914,22 @@ function DocView:draw_caret(x, y) renderer.draw_rect(x, y, style.caret_width, lh, style.caret) end -function DocView:draw_line_body(vline, x, y) +function DocView:draw_line(line, x, y) + local gw, gpad = self:get_gutter_width() + self:draw_line_gutter(line, x, y, gpad and gw - gpad or gw) + core.push_clip_rect(self.position.x + gw, self.position.y, self.size.x - gw, self.size.y) + local lh = self:draw_line_body(line, x + gw, y) + core.pop_clip_rect() + return lh +end + +function DocView:draw_line_body(line, x, y) -- draw highlight if any selection ends on this line local draw_highlight = false local hcl = config.highlight_current_line if hcl ~= false then for lidx, line1, col1, line2, col2 in self:get_vselections(false) do - if line1 == vline then + if line1 == line then if hcl == "no_selection" then if (line1 ~= line2) or (col1 ~= col2) then draw_highlight = false @@ -954,11 +947,11 @@ function DocView:draw_line_body(vline, x, y) -- draw selection if it overlaps this line local lh = self:get_line_height() - for lidx, line1, col1, line2, col2 in self:get_vselections(true) do - if vline >= line1 and vline <= line2 then - local text = self.doc.lines[vline] - if line1 ~= vline then col1 = 1 end - if line2 ~= vline then col2 = #text + 1 end + for lidx, line1, col1, line2, col2 in self:get_selections(true) do + if line >= line1 and line <= line2 then + local text = self.doc.lines[line] + if line1 ~= line then col1 = 1 end + if line2 ~= line then col2 = #text + 1 end local _, x1 = x + self:get_line_screen_position(line1, col1) local _, x2 = x + self:get_line_screen_position(line2, col2) if x1 ~= x2 then @@ -968,23 +961,21 @@ function DocView:draw_line_body(vline, x, y) end -- draw line's text - return self:draw_line_text(vline, x, y) + return self:draw_line_text(line, x, y) end -function DocView:draw_line_gutter(vline, x, y, width) +function DocView:draw_line_gutter(line, x, y, width) local color = style.line_number for _, line1, _, line2 in self:get_vselections(true) do - if vline >= line1 and vline <= line2 then + if line >= line1 and line <= line2 then color = style.line_number2 break end end x = x + style.padding.x local lh = self:get_line_height() - if self:is_first_line_of_block(vline) then - common.draw_text(self:get_font(), color, self.tokens[self.vcache[vline] + 1], "right", x, y, width, lh) - end + common.draw_text(self:get_font(), color, line, "right", x, y, width, lh) return lh end @@ -1042,38 +1033,25 @@ function DocView:draw() local _, indent_size = self.doc:get_indent_info() self:get_font():set_tab_size(indent_size) - local minline, maxline = self:get_visible_virutal_line_range() + local minline, maxline = self:get_visible_virtual_line_range() + local gw, gpad = self:get_gutter_width() local lh = self:get_line_height() - local x, y = self:get_virtual_line_offset(minline) - local gw, gpad = self:get_gutter_width() for i = minline, maxline do - if self:has_tokens(i) then - y = y + (self:draw_line_gutter(i, self.position.x, y, gpad and gw - gpad or gw) or lh) - end - end - - local pos = self.position - x, y = self:get_virtual_line_offset(minline) - -- the clip below ensure we don't write on the gutter region. On the - -- right side it is redundant with the Node's clip. - core.push_clip_rect(pos.x + gw, pos.y, self.size.x - gw, self.size.y) - for i = minline, maxline do - y = y + (self:draw_line_body(i, x, y) or lh) + y = y + self:draw_line(i, x - gw, y) or lh end self:draw_overlay() - core.pop_clip_rect() self:draw_scrollbar() end -- Selections are in document space. --- Transform function for lines from the doc. +-- Tokenize function for lines from the doc. -- Plugins hook this to return a line/col list from `doc`, or provide a virtual line. -- `{ "doc", doc_line, 1, #self.doc.lines[doc_line], style }` -- `{ "virtual", doc_line, text, false, style } -function DocView:transform(doc_line) +function DocView:tokenize(doc_line) return { "doc", doc_line, 1, #self.doc.lines[doc_line], { } } end @@ -1103,8 +1081,8 @@ end function DocView:has_tokens(vline) - local token_idx = retrieve_tokens(self, vline) - return token_idx and token_idx < #self.tokens and token_idx ~= retrieve_tokens(self, vline + 1) + local token_idx = self:retrieve_tokens(vline) + return token_idx and token_idx < #self.tokens and token_idx ~= self:retrieve_tokens(vline + 1) end @@ -1129,12 +1107,34 @@ local function dline_iter(state, idx) return idx + 5, text, self.tokens[idx+4], self.tokens[idx] end -function DocView:each_vline_token(vline) return vline_iter, { self, vline }, retrieve_tokens(self, vline) end + +function DocView:retrieve_tokens(vline, dline) + while ((vline and vline > #self.vcache) or (dline and dline > #self.dcache)) and #self.dcache < #self.doc.lines do + local tokens = self:tokenize(#self.dcache + 1) + local bundles = #tokens / 5 + table.insert(self.dcache, #self.tokens + 1) + if #tokens > 0 then + table.insert(self.vcache, #self.tokens + 1) + end + self.dtovcache[#self.dcache] = #self.vcache + for j = 1, #tokens, 5 do + local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) + table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) + if text:find("\n$") and j < #tokens - 5 then + table.insert(self.vcache, #self.tokens + 1) + end + end + end + if vline then return self.vcache[vline] end + return self.dcache[dline] +end + +function DocView:each_vline_token(vline) return vline_iter, { self, vline }, self:retrieve_tokens(vline) end function DocView:each_token(tokens, line) return token_iter, { self, tokens }, (((line or 1) - 1) * 5) + 1 end -function DocView:each_dline_token(line) return dline_iter, { self, line }, retrieve_tokens(self, nil, line) end +function DocView:each_dline_token(line) return dline_iter, { self, line }, self:retrieve_tokens(nil, line) end function DocView:get_dline_token_idx(dline, dcol) - for i = retrieve_tokens(self, nil, dline), #self.tokens, 5 do + for i = self:retrieve_tokens(nil, dline), #self.tokens, 5 do if self.tokens[i] == "doc" and (self.tokens[i+1] ~= dline or dcol >= self.tokens[i+2]) then return i end diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index 7a1e218ad..fb558468d 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -43,9 +43,9 @@ function DocView:compute_fold(doc_line) end end -local old_transform = DocView.transform -function DocView:transform(doc_line) - local tokens = old_transform(self, doc_line) +local old_tokenize = DocView.tokenize +function DocView:tokenize(doc_line) + local tokens = old_tokenize(self, doc_line) if not self.foldable then return tokens end self:compute_fold(doc_line) if self.folded[doc_line] then return {} end diff --git a/data/plugins/highlighter.lua b/data/plugins/highlighter.lua index eb627566f..fc50620cc 100644 --- a/data/plugins/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -128,11 +128,11 @@ local function get_tokens(highlighted_tokens, doc_line, start_offset, end_offset return tokens end -local old_transform = DocView.transform -function DocView:transform(doc_line) +local old_tokenize = DocView.tokenize +function DocView:tokenize(doc_line) if not self.doc.highighter then self.doc.highlighter = Highlighter(self.doc) end - local tokens = old_transform(self, doc_line) + local tokens = old_tokenize(self, doc_line) if #tokens == 0 then return tokens end local highlighted_tokens = self.doc.highlighter:get_line(doc_line).tokens -- Walk through all doc tokens, and then map them onto what we've tokenized. diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index 1be2973bd..30f54355e 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -92,9 +92,9 @@ function DocView:draw() end end -local old_transform = DocView.transform -function DocView:transform(doc_line) - if not self.wrapping then return old_transform(self, doc_line) end +local old_tokenize = DocView.tokenize +function DocView:tokenize(doc_line) + if not self.wrapping then return old_tokenize(self, doc_line) end local tokens = {} local x, y = self:get_content_offset() local gw = self:get_gutter_width() @@ -108,7 +108,7 @@ function DocView:transform(doc_line) local offset = docstart local docend = docstart + width - for _, type, l, s, e, style in self:each_token(old_transform(self, doc_line)) do + for _, type, l, s, e, style in self:each_token(old_tokenize(self, doc_line)) do local font = self:get_font() or style.font while true do local text = self:get_token_text(type, l, s, e) From 086e47f7190bd6fe0803c1f1b7e1784765320160 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 11:55:19 -0500 Subject: [PATCH 07/24] Fixed a number of bugs. --- data/core/docview.lua | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 00dc5aedd..52fb416e0 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -219,11 +219,14 @@ function DocView:resolve_screen_position(x, y) local ox, oy = self:get_virtual_line_offset(1) local vline = math.floor((y - oy) / self:get_line_height()) + 1 + local xoffset, last_i, i = ox, 1, 1 local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) - local line = self.tokens[self.vcache[vline]+1] + local token_idx = self:retrieve_tokens(nil, vline) + if not token_idx then return #self.doc.lines, #self.doc.lines[#self.doc.lines] end + local line = self.tokens[token_idx+1] for _, text, style in self:each_vline_token(vline) do local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end @@ -682,7 +685,9 @@ end function DocView:get_visible_line_range() local minline, maxline = self:get_visible_virtual_line_range() - return self.vcache[self:retrieve_tokens(minline) + 1], self.vcache[self:retrieve_tokens(maxline) + 1] + local min_token_idx = self:retrieve_tokens(minline) or self.vcache[#self.vcache] + local max_token_idx = self:retrieve_tokens(maxline) or self.vcache[#self.vcache] + return self.tokens[min_token_idx + 1], self.tokens[max_token_idx + 1] end @@ -893,9 +898,9 @@ function DocView:draw_line_text(line, x, y) for tidx, text, style in self:each_dline_token(line) do local font = style.font or default_font tx = renderer.draw_text(font, text, tx, ty, style.color or default_color) - if tx > self.position.x + self.size.x then break end if text:find("\n$") then tx = otx + ty = ty + self:get_line_height() lines = lines + 1 end end @@ -916,10 +921,12 @@ end function DocView:draw_line(line, x, y) local gw, gpad = self:get_gutter_width() - self:draw_line_gutter(line, x, y, gpad and gw - gpad or gw) core.push_clip_rect(self.position.x + gw, self.position.y, self.size.x - gw, self.size.y) local lh = self:draw_line_body(line, x + gw, y) core.pop_clip_rect() + if lh > 0 then + self:draw_line_gutter(line, x, y, gpad and gw - gpad or gw) + end return lh end @@ -928,7 +935,7 @@ function DocView:draw_line_body(line, x, y) local draw_highlight = false local hcl = config.highlight_current_line if hcl ~= false then - for lidx, line1, col1, line2, col2 in self:get_vselections(false) do + for lidx, line1, col1, line2, col2 in self:get_selections(false) do if line1 == line then if hcl == "no_selection" then if (line1 ~= line2) or (col1 ~= col2) then @@ -1033,12 +1040,12 @@ function DocView:draw() local _, indent_size = self.doc:get_indent_info() self:get_font():set_tab_size(indent_size) - local minline, maxline = self:get_visible_virtual_line_range() + local minline, maxline = self:get_visible_line_range() local gw, gpad = self:get_gutter_width() local lh = self:get_line_height() - local x, y = self:get_virtual_line_offset(minline) + local _, y = self:get_line_screen_position(minline, 1) for i = minline, maxline do - y = y + self:draw_line(i, x - gw, y) or lh + y = y + self:draw_line(i, self.position.x, y) or lh end self:draw_overlay() From 5dd02d0bad5b48e523d17d997e77d04f6c1222d9 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 12:27:23 -0500 Subject: [PATCH 08/24] Standardizing naming conventions. --- data/core/docview.lua | 10 +++++----- data/plugins/codefolding.lua | 22 +++++++++++----------- data/plugins/highlighter.lua | 14 ++++---------- data/plugins/linewrapping.lua | 8 ++++---- 4 files changed, 24 insertions(+), 30 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 52fb416e0..5573e438c 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -224,7 +224,7 @@ function DocView:resolve_screen_position(x, y) local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) - local token_idx = self:retrieve_tokens(nil, vline) + local token_idx = self:retrieve_tokens(vline) if not token_idx then return #self.doc.lines, #self.doc.lines[#self.doc.lines] end local line = self.tokens[token_idx+1] for _, text, style in self:each_vline_token(vline) do @@ -1056,10 +1056,10 @@ end -- Selections are in document space. -- Tokenize function for lines from the doc. -- Plugins hook this to return a line/col list from `doc`, or provide a virtual line. --- `{ "doc", doc_line, 1, #self.doc.lines[doc_line], style }` --- `{ "virtual", doc_line, text, false, style } -function DocView:tokenize(doc_line) - return { "doc", doc_line, 1, #self.doc.lines[doc_line], { } } +-- `{ "doc", line, 1, #self.doc.lines[line], style }` +-- `{ "virtual", line, text, false, style } +function DocView:tokenize(line) + return { "doc", line, 1, #self.doc.lines[line], { } } end --[[ diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index fb558468d..5b7867de4 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -44,22 +44,22 @@ function DocView:compute_fold(doc_line) end local old_tokenize = DocView.tokenize -function DocView:tokenize(doc_line) - local tokens = old_tokenize(self, doc_line) +function DocView:tokenize(line) + local tokens = old_tokenize(self, line) if not self.foldable then return tokens end - self:compute_fold(doc_line) - if self.folded[doc_line] then return {} end - if self:is_foldable(doc_line) and self.folded[doc_line+1] then + self:compute_fold(line) + if self.folded[line] then return {} end + if self:is_foldable(line) and self.folded[line+1] then -- remove the newline from the end of the tokens local type, line, e = tokens[#tokens - 4], tokens[#tokens - 3], tokens[#tokens - 1] if type == "doc" and self.doc.lines[line]:sub(e, e) == "\n" then tokens[#tokens - 1] = tokens[#tokens - 1] - 1 end table.insert(tokens, "virtual") - table.insert(tokens, doc_line) + table.insert(tokens, line) table.insert(tokens, " ... ") table.insert(tokens, false) table.insert(tokens, { color = style.dim }) table.insert(tokens, "virtual") - table.insert(tokens, doc_line) + table.insert(tokens, line) table.insert(tokens, "}\n") table.insert(tokens, false) table.insert(tokens, { }) @@ -67,10 +67,10 @@ function DocView:tokenize(doc_line) return tokens end -function DocView:is_foldable(doc_line) - if doc_line < #self.doc.lines then - if not self.foldable[doc_line] or not self.foldable[doc_line+1] then self:compute_fold(doc_line+1) end - return self.foldable[doc_line] and self.foldable[doc_line+1] > self.foldable[doc_line] +function DocView:is_foldable(line) + if line < #self.doc.lines then + if not self.foldable[line] or not self.foldable[line+1] then self:compute_fold(line+1) end + return self.foldable[line] and self.foldable[line+1] > self.foldable[line] end return false end diff --git a/data/plugins/highlighter.lua b/data/plugins/highlighter.lua index fc50620cc..af0a08190 100644 --- a/data/plugins/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -2,14 +2,8 @@ local core = require "core" local config = require "core.config" local style = require "core.style" -local Doc = require "core.doc" local DocView = require "core.docview" -local Node = require "core.node" local common = require "core.common" - -local core = require "core" -local common = require "core.common" -local config = require "core.config" local tokenizer = require "core.tokenizer" local Object = require "core.object" @@ -129,12 +123,12 @@ local function get_tokens(highlighted_tokens, doc_line, start_offset, end_offset end local old_tokenize = DocView.tokenize -function DocView:tokenize(doc_line) +function DocView:tokenize(line) if not self.doc.highighter then self.doc.highlighter = Highlighter(self.doc) end - local tokens = old_tokenize(self, doc_line) + local tokens = old_tokenize(self, line) if #tokens == 0 then return tokens end - local highlighted_tokens = self.doc.highlighter:get_line(doc_line).tokens + local highlighted_tokens = self.doc.highlighter:get_line(line).tokens -- Walk through all doc tokens, and then map them onto what we've tokenized. local colorized = {} local start_offset = 1 @@ -147,7 +141,7 @@ function DocView:tokenize(doc_line) table.move(tokens, i, i + 4, #colorized + 1, colorized) end end - return #colorized > 0 and colorized or { "doc", doc_line, 1, 0, {} } + return #colorized > 0 and colorized or { "doc", line, 1, 1, {} } end return Highlighter diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index 30f54355e..f00b4913c 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -93,8 +93,8 @@ function DocView:draw() end local old_tokenize = DocView.tokenize -function DocView:tokenize(doc_line) - if not self.wrapping then return old_tokenize(self, doc_line) end +function DocView:tokenize(line) + if not self.wrapping then return old_tokenize(self, line) end local tokens = {} local x, y = self:get_content_offset() local gw = self:get_gutter_width() @@ -108,7 +108,7 @@ function DocView:tokenize(doc_line) local offset = docstart local docend = docstart + width - for _, type, l, s, e, style in self:each_token(old_tokenize(self, doc_line)) do + for _, type, l, s, e, style in self:each_token(old_tokenize(self, line)) do local font = self:get_font() or style.font while true do local text = self:get_token_text(type, l, s, e) @@ -130,7 +130,7 @@ function DocView:tokenize(doc_line) table.insert(tokens, style) table.insert(tokens, "virtual") - table.insert(tokens, doc_line) + table.insert(tokens, line) table.insert(tokens, "\n") table.insert(tokens, false) table.insert(tokens, style) From b82cdd8cd942fc51d62af24c9dc3ca822e642637 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 12:42:42 -0500 Subject: [PATCH 09/24] Fixed issue with resolving screen position on multi-lines. --- data/core/docview.lua | 116 ++++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 5573e438c..6be5fd620 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -14,11 +14,11 @@ local DocView = View:extend() DocView.context = "session" -local function move_to_line_offset(dv, dline, dcol, offset) - local vline, vcol = dv:get_closest_vline(dline, dcol) +local function move_to_line_offset(dv, line, col, offset) + local vline, vcol = dv:get_closest_vline(line, col) local xo = dv.last_x_offset if xo.line ~= vline or xo.col ~= vcol then - xo.x, xo.y = dv:get_line_screen_position(dline, dcol) + xo.x, xo.y = dv:get_line_screen_position(line, col) end xo.line, xo.col = dv:get_dline(math.max(vline + offset, 1), vcol) return xo.line, xo.col @@ -26,31 +26,31 @@ end DocView.translate = { - ["previous_page"] = function(self, dline, col) + ["previous_page"] = function(self, line, col) local min, max = self:get_visible_virtual_line_range() - return move_to_line_offset(self, dline, col, (min - max)) + return move_to_line_offset(self, line, col, (min - max)) end, - ["next_page"] = function(self, dline, col) - if dline == #self.doc.lines then - return #self.doc.lines, #self.doc.lines[dline] + ["next_page"] = function(self, line, col) + if line == #self.doc.lines then + return #self.doc.lines, #self.doc.lines[line] end local min, max = self:get_visible_virtual_line_range() - return move_to_line_offset(self, dline, col, (max - min)) + return move_to_line_offset(self, line, col, (max - min)) end, - ["previous_line"] = function(self, dline, col) - if dline == 1 then + ["previous_line"] = function(self, line, col) + if line == 1 then return 1, 1 end - return move_to_line_offset(self, dline, col, -1) + return move_to_line_offset(self, line, col, -1) end, - ["next_line"] = function(self, dline, col) - if dline == #self.doc.lines then + ["next_line"] = function(self, line, col) + if line == #self.doc.lines then return #self.doc.lines, math.huge end - return move_to_line_offset(self, dline, col, 1) + return move_to_line_offset(self, line, col, 1) end, } @@ -83,7 +83,7 @@ function DocView:get_char(line, col) end function DocView:position_offset_byte(line, col, offset) - local token_idx = self:get_dline_token_idx(line, col) + local token_idx = self:get_line_token_idx(line, col) if offset > 0 then if self.tokens[token_idx+1] ~= line or col < self.tokens[token_idx+2] then line, col = self.tokens[token_idx+1], self.tokens[token_idx+3] @@ -125,21 +125,21 @@ DocView.position_offset_func = Doc.position_offset_func DocView.position_offset = Doc.position_offset -function DocView:get_closest_vline(dline, dcol) - local token_idx = self:retrieve_tokens(nil, dline) +function DocView:get_closest_vline(line, col) + local token_idx = self:retrieve_tokens(nil, line) if not token_idx then return #self.vcache + 1, 1 end - if self.tokens[token_idx+1] ~= dline then return self.dtovcache[dline] end + if self.tokens[token_idx+1] ~= line then return self.dtovcache[line] end local total_line_length = 0 local total_token_length = 0 - local vline = self.dtovcache[dline] - if dcol and dcol > 1 then - for _, text, _, type in self:each_dline_token(dline) do + local vline = self.dtovcache[line] + if col and col > 1 then + for _, text, _, type in self:each_line_token(line) do if type == "doc" then local length = text:ulen() or #text - if dcol <= total_token_length + total_line_length + length then - return vline, dcol - total_line_length + if col <= total_token_length + total_line_length + length then + return vline, col - total_line_length end total_token_length = total_token_length + length end @@ -153,19 +153,19 @@ function DocView:get_closest_vline(dline, dcol) end function DocView:get_dline(vline, vcol) - local dline = self.tokens[self:retrieve_tokens(vline) + 1] + local line = self.tokens[self:retrieve_tokens(vline) + 1] local total_line_length = 0 - local current_line = self.dtovcache[dline] - for _, text, _, type in self:each_dline_token(dline) do + local current_line = self.dtovcache[line] + for _, text, _, type in self:each_line_token(line) do if current_line == vline then - return dline, vcol + total_line_length + return line, vcol + total_line_length end if type == "doc" then total_line_length = total_line_length + text:ulen() end if text:find("\n$") then current_line = current_line + 1 end end - return dline, vcol + return line, vcol end function DocView:get_virtual_line_offset(vline) @@ -227,24 +227,30 @@ function DocView:resolve_screen_position(x, y) local token_idx = self:retrieve_tokens(vline) if not token_idx then return #self.doc.lines, #self.doc.lines[#self.doc.lines] end local line = self.tokens[token_idx+1] - for _, text, style in self:each_vline_token(vline) do - local font = style.font or default_font - if font ~= default_font then font:set_tab_size(indent_size) end - local width = font:get_width(text) - -- Don't take the shortcut if the width matches x, - -- because we need last_i which should be calculated using utf-8. - if xoffset + width < x then - xoffset = xoffset + width - i = i + text:ulen() + for idx, text, style, type in self:each_line_token(line) do + if idx < token_idx then + if type == "doc" then + i = i + text:ulen() + end else - for char in common.utf8_chars(text) do - local w = font:get_width(char) - if xoffset >= x then - return line, (xoffset - x > w / 2) and last_i or i + local font = style.font or default_font + if font ~= default_font then font:set_tab_size(indent_size) end + local width = font:get_width(text) + -- Don't take the shortcut if the width matches x, + -- because we need last_i which should be calculated using utf-8. + if xoffset + width < x then + xoffset = xoffset + width + i = i + text:ulen() + else + for char in common.utf8_chars(text) do + local w = font:get_width(char) + if xoffset >= x then + return line, (xoffset - x > w / 2) and last_i or i + end + xoffset = xoffset + w + last_i = i + i = i + 1 end - xoffset = xoffset + w - last_i = i - i = i + 1 end end end @@ -895,7 +901,7 @@ function DocView:draw_line_text(line, x, y) local tx, ty = x, y + self:get_line_text_y_offset() local otx = tx local lines = 0 - for tidx, text, style in self:each_dline_token(line) do + for tidx, text, style in self:each_line_token(line) do local font = style.font or default_font tx = renderer.draw_text(font, text, tx, ty, style.color or default_color) if text:find("\n$") then @@ -959,8 +965,8 @@ function DocView:draw_line_body(line, x, y) local text = self.doc.lines[line] if line1 ~= line then col1 = 1 end if line2 ~= line then col2 = #text + 1 end - local _, x1 = x + self:get_line_screen_position(line1, col1) - local _, x2 = x + self:get_line_screen_position(line2, col2) + local x1, y1 = self:get_line_screen_position(line1, col1) + local x2, y2 = self:get_line_screen_position(line2, col2) if x1 ~= x2 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end @@ -1115,8 +1121,8 @@ local function dline_iter(state, idx) end -function DocView:retrieve_tokens(vline, dline) - while ((vline and vline > #self.vcache) or (dline and dline > #self.dcache)) and #self.dcache < #self.doc.lines do +function DocView:retrieve_tokens(vline, line) + while ((vline and vline > #self.vcache) or (line and line > #self.dcache)) and #self.dcache < #self.doc.lines do local tokens = self:tokenize(#self.dcache + 1) local bundles = #tokens / 5 table.insert(self.dcache, #self.tokens + 1) @@ -1133,16 +1139,16 @@ function DocView:retrieve_tokens(vline, dline) end end if vline then return self.vcache[vline] end - return self.dcache[dline] + return self.dcache[line] end function DocView:each_vline_token(vline) return vline_iter, { self, vline }, self:retrieve_tokens(vline) end function DocView:each_token(tokens, line) return token_iter, { self, tokens }, (((line or 1) - 1) * 5) + 1 end -function DocView:each_dline_token(line) return dline_iter, { self, line }, self:retrieve_tokens(nil, line) end +function DocView:each_line_token(line) return dline_iter, { self, line }, self:retrieve_tokens(nil, line) end -function DocView:get_dline_token_idx(dline, dcol) - for i = self:retrieve_tokens(nil, dline), #self.tokens, 5 do - if self.tokens[i] == "doc" and (self.tokens[i+1] ~= dline or dcol >= self.tokens[i+2]) then +function DocView:get_line_token_idx(line, col) + for i = self:retrieve_tokens(nil, line), #self.tokens, 5 do + if self.tokens[i] == "doc" and (self.tokens[i+1] ~= line or dcol >= self.tokens[i+2]) then return i end end From 197c1aaf0d84730505e3e1b5b809a601276fe83c Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 12:50:57 -0500 Subject: [PATCH 10/24] Added in read-only mode. --- data/core/commands/docview.lua | 91 ++++++++++++++++++---------------- data/core/docview.lua | 3 +- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/data/core/commands/docview.lua b/data/core/commands/docview.lua index bd5f2f680..e43e536e8 100644 --- a/data/core/commands/docview.lua +++ b/data/core/commands/docview.lua @@ -111,23 +111,11 @@ local function insert_paste(doc, value, whole_line, idx) end end -local commands = { - ["docview:select-none"] = function(dv) - local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection) - if not l1 then - l1, c1 = dv.doc:get_selection_idx(1) - end - dv:set_selection(l1, c1) - end, - +local write_commands = { ["docview:cut"] = function() cut_or_copy(true) end, - ["docview:copy"] = function() - cut_or_copy(false) - end, - ["docview:undo"] = function(dv) dv.doc:undo() end, @@ -242,30 +230,6 @@ local commands = { end end, - ["docview:select-all"] = function(dv) - dv:set_selection(1, 1, math.huge, math.huge) - -- avoid triggering DocView:scroll_to_make_visible - dv.last_line1 = 1 - dv.last_col1 = 1 - dv.last_line2 = #dv.doc.lines - dv.last_col2 = #dv.doc.lines[#dv.doc.lines] - end, - - ["docview:select-lines"] = function(dv) - for idx, line1, _, line2 in dv.doc:get_selections(true) do - append_line_if_last_line(line2) - dv:set_selections(idx, line2 + 1, 1, line1, 1) - end - end, - - ["docview:select-word"] = function(dv) - for idx, line1, col1 in dv.doc:get_selections(true) do - local line1, col1 = translate.start_of_word(dv, line1, col1) - local line2, col2 = translate.end_of_word(dv, line1, col1) - dv:set_selections(idx, line2, col2, line1, col1) - end - end, - ["docview:join-lines"] = function(dv) for idx, line1, col1, line2, col2 in dv.doc:get_selections(true) do if line1 == line2 then line2 = line2 + 1 end @@ -402,6 +366,44 @@ local commands = { ["docview:lower-case"] = function(dv) dv:replace(string.ulower) + end +} + +local read_commands = { + ["docview:select-none"] = function(dv) + local l1, c1 = dv.doc:get_selection_idx(dv.doc.last_selection) + if not l1 then + l1, c1 = dv.doc:get_selection_idx(1) + end + dv:set_selection(l1, c1) + end, + + ["docview:copy"] = function() + cut_or_copy(false) + end, + + ["docview:select-all"] = function(dv) + dv:set_selection(1, 1, math.huge, math.huge) + -- avoid triggering DocView:scroll_to_make_visible + dv.last_line1 = 1 + dv.last_col1 = 1 + dv.last_line2 = #dv.doc.lines + dv.last_col2 = #dv.doc.lines[#dv.doc.lines] + end, + + ["docview:select-lines"] = function(dv) + for idx, line1, _, line2 in dv.doc:get_selections(true) do + append_line_if_last_line(line2) + dv:set_selections(idx, line2 + 1, 1, line1, 1) + end + end, + + ["docview:select-word"] = function(dv) + for idx, line1, col1 in dv.doc:get_selections(true) do + local line1, col1 = translate.start_of_word(dv, line1, col1) + local line2, col2 = translate.end_of_word(dv, line1, col1) + dv:set_selections(idx, line2, col2, line1, col1) + end end, ["docview:go-to-line"] = function(dv) @@ -515,12 +517,12 @@ local translations = { } for name, obj in pairs(translations) do - commands["docview:move-to-" .. name] = function(dv) dv:move_to(obj[name:gsub("-", "_")], dv) end - commands["docview:select-to-" .. name] = function(dv) dv:select_to(obj[name:gsub("-", "_")], dv) end - commands["docview:delete-to-" .. name] = function(dv) dv:delete_to(obj[name:gsub("-", "_")], dv) end + read_commands["docview:move-to-" .. name] = function(dv) dv:move_to(obj[name:gsub("-", "_")], dv) end + read_commands["docview:select-to-" .. name] = function(dv) dv:select_to(obj[name:gsub("-", "_")], dv) end + write_commands["docview:delete-to-" .. name] = function(dv) dv:delete_to(obj[name:gsub("-", "_")], dv) end end -commands["docview:move-to-previous-char"] = function(dv) +read_commands["docview:move-to-previous-char"] = function(dv) for idx, line1, col1, line2, col2 in dv:get_selections(true) do if line1 ~= line2 or col1 ~= col2 then dv:set_selections(idx, line1, col1) @@ -531,7 +533,7 @@ commands["docview:move-to-previous-char"] = function(dv) dv:merge_cursors() end -commands["docview:move-to-next-char"] = function(dv) +read_commands["docview:move-to-next-char"] = function(dv) for idx, line1, col1, line2, col2 in dv:get_selections(true) do if line1 ~= line2 or col1 ~= col2 then dv:set_selections(idx, line2, col2) @@ -542,4 +544,7 @@ commands["docview:move-to-next-char"] = function(dv) dv:merge_cursors() end -command.add("core.docview", commands) +command.add(function() + return core.active_view:is(DocView) and not core.active_view.read_only +end, read_commands) +command.add("core.docview", write_commands) diff --git a/data/core/docview.lua b/data/core/docview.lua index 6be5fd620..5651ecbf4 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -65,6 +65,7 @@ function DocView:new(doc) self.ime_selection = { from = 0, size = 0 } self.ime_status = false self.hovering_gutter = false + self.read_only = false self.tokens = {} self.vcache = {} self.dcache = {} @@ -712,7 +713,7 @@ end function DocView:supports_text_input() - return true + return not self.read_only end From 0657e2aa50aac9b48c5c9261a3f62b6f4cb4f0e4 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 22:04:41 -0500 Subject: [PATCH 11/24] Fixed selections. --- data/core/commands/docview.lua | 6 +-- data/core/doc/translate.lua | 10 +--- data/core/docview.lua | 93 +++++++++++++++------------------- data/plugins/codefolding.lua | 1 + 4 files changed, 45 insertions(+), 65 deletions(-) diff --git a/data/core/commands/docview.lua b/data/core/commands/docview.lua index e43e536e8..5a865f7d8 100644 --- a/data/core/commands/docview.lua +++ b/data/core/commands/docview.lua @@ -544,7 +544,5 @@ read_commands["docview:move-to-next-char"] = function(dv) dv:merge_cursors() end -command.add(function() - return core.active_view:is(DocView) and not core.active_view.read_only -end, read_commands) -command.add("core.docview", write_commands) +command.add(function() return core.active_view:is(DocView) and not core.active_view.read_only, core.active_view end, write_commands) +command.add("core.docview", read_commands) diff --git a/data/core/doc/translate.lua b/data/core/doc/translate.lua index 651a78cad..98d385da0 100644 --- a/data/core/doc/translate.lua +++ b/data/core/doc/translate.lua @@ -14,18 +14,12 @@ end function translate.previous_char(doc, line, col) - repeat - line, col = doc:position_offset(line, col, -1) - until not common.is_utf8_cont(doc:get_char(line, col)) - return line, col + return doc:position_offset(line, col, -1) end function translate.next_char(doc, line, col) - repeat - line, col = doc:position_offset(line, col, 1) - until not common.is_utf8_cont(doc:get_char(line, col)) - return line, col + return doc:position_offset(line, col, 1) end diff --git a/data/core/docview.lua b/data/core/docview.lua index 5651ecbf4..85ab335af 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -84,38 +84,37 @@ function DocView:get_char(line, col) end function DocView:position_offset_byte(line, col, offset) - local token_idx = self:get_line_token_idx(line, col) + local token_idx = self:retrieve_tokens(nil, line) + for idx = token_idx, #self.tokens, 5 do + if self.tokens[idx] == "doc" then token_idx = idx if self.tokens[idx+1] ~= line or col >= self.tokens[idx+2] then break end end + end if offset > 0 then - if self.tokens[token_idx+1] ~= line or col < self.tokens[token_idx+2] then - line, col = self.tokens[token_idx+1], self.tokens[token_idx+3] - end local total_offset = (col + offset) - self.tokens[token_idx+2] - for i = token_idx, #self.tokens, 5 do - if self.tokens[i] == "doc" then - local width = (self.tokens[i+3] - self.tokens[i+2]) + 1 + while token_idx do + if self.tokens[token_idx] == "doc" then + local width = (self.tokens[token_idx+3] - self.tokens[token_idx+2]) + 1 if total_offset < width then - return self.tokens[i+1], self.tokens[i+2] + total_offset + return self.tokens[token_idx+1], self.tokens[token_idx+2] + total_offset end total_offset = total_offset - width end + token_idx = self:next_token(token_idx) end return #self.doc.lines, #self.doc.lines[#self.doc.lines] else - if self.tokens[token_idx+1] ~= line or col < self.tokens[token_idx+2] then - line, col = self.tokens[token_idx+1], self.tokens[token_idx+3] - end local total_offset = self.tokens[token_idx+3] - (col + offset) - for i = token_idx, 1, -5 do - if self.tokens[i] == "doc" then - local width = (self.tokens[i+3] - self.tokens[i+2]) + 1 + while token_idx > 0 do + if self.tokens[token_idx] == "doc" then + local width = (self.tokens[token_idx+3] - self.tokens[token_idx+2]) + 1 if total_offset < width then - return self.tokens[i+1], self.tokens[i+3] - total_offset + return self.tokens[token_idx+1], self.tokens[token_idx+3] - total_offset end total_offset = total_offset - width end + token_idx = token_idx - 5 end + return 1,1 end - return 1,1 end function DocView:sanitize_position(line, col) @@ -378,26 +377,6 @@ function DocView:get_selections(sort_intra, idx_reverse) idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1) end - -local function vselection_iterator(invariant, idx) - local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1) - if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end - local line1, col1, line2, col2 - if invariant[2] then - line1, col1, line2, col2 = common.sort_positions(table.unpack(invariant[1], target, target + 4)) - else - line1, col1, line2, col2 = table.unpack(invariant[1], target, target + 4) - end - line1, col1 = invariant[4]:get_closest_vline(line1, col1) - line2, col2 = invariant[4]:get_closest_vline(line2, col2) - return idx + (invariant[3] and -1 or 1), line1, col1, line2, col2 -end - - -function DocView:get_vselections(...) - return vselection_iterator, select(2, self:get_selections(...)) -end - -- End of cursor seciton. function DocView:sanitize_selection() @@ -929,7 +908,7 @@ end function DocView:draw_line(line, x, y) local gw, gpad = self:get_gutter_width() core.push_clip_rect(self.position.x + gw, self.position.y, self.size.x - gw, self.size.y) - local lh = self:draw_line_body(line, x + gw, y) + local lh = self:draw_line_body(line, x + gw - self.scroll.x, y) core.pop_clip_rect() if lh > 0 then self:draw_line_gutter(line, x, y, gpad and gw - gpad or gw) @@ -963,13 +942,21 @@ function DocView:draw_line_body(line, x, y) local lh = self:get_line_height() for lidx, line1, col1, line2, col2 in self:get_selections(true) do if line >= line1 and line <= line2 then - local text = self.doc.lines[line] + local length = self.doc.lines[line]:ulen() if line1 ~= line then col1 = 1 end - if line2 ~= line then col2 = #text + 1 end - local x1, y1 = self:get_line_screen_position(line1, col1) - local x2, y2 = self:get_line_screen_position(line2, col2) - if x1 ~= x2 then - renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + if line2 ~= line then col2 = length + 1 end + local x1, y1 = self:get_line_screen_position(line, col1) + local x2, y2 = self:get_line_screen_position(line, col2) + if y1 ~= y2 or x1 ~= x2 then + if y1 == y2 then + renderer.draw_rect(x1, y1, x2 - x1, (y2 - y1) + lh, style.selection) + else + local vline = self:get_closest_vline(line1, col1) + -- we need at most three rects here; one for the initial selection, one for the middle large block, one for the end + renderer.draw_rect(x1, y1, self:get_vline_width(vline) + x - x1, lh, style.selection) + if y2 > y1 + lh then renderer.draw_rect(x, y1 + lh, self:get_vline_width(vline), (y2 - y1), style.selection) end + renderer.draw_rect(x, y1 + lh, x2, lh, style.selection) + end end end end @@ -981,7 +968,7 @@ end function DocView:draw_line_gutter(line, x, y, width) local color = style.line_number - for _, line1, _, line2 in self:get_vselections(true) do + for _, line1, _, line2 in self:get_selections(true) do if line >= line1 and line <= line2 then color = style.line_number2 break @@ -1050,7 +1037,7 @@ function DocView:draw() local minline, maxline = self:get_visible_line_range() local gw, gpad = self:get_gutter_width() local lh = self:get_line_height() - local _, y = self:get_line_screen_position(minline, 1) + local x, y = self:get_line_screen_position(minline, 1) for i = minline, maxline do y = y + self:draw_line(i, self.position.x, y) or lh end @@ -1083,6 +1070,7 @@ function DocView:is_first_line_of_block(vline) end function DocView:invalidate_cache(start_doc_line) + if #self.tokens == 0 then return end if not start_doc_line then start_doc_line = 1 end while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end while #self.dcache >= start_doc_line do table.remove(self.dcache) end @@ -1144,16 +1132,15 @@ function DocView:retrieve_tokens(vline, line) end function DocView:each_vline_token(vline) return vline_iter, { self, vline }, self:retrieve_tokens(vline) end -function DocView:each_token(tokens, line) return token_iter, { self, tokens }, (((line or 1) - 1) * 5) + 1 end +function DocView:each_token(tokens, idx) return token_iter, { self, tokens }, (((idx or 1) - 1) * 5) + 1 end function DocView:each_line_token(line) return dline_iter, { self, line }, self:retrieve_tokens(nil, line) end - -function DocView:get_line_token_idx(line, col) - for i = self:retrieve_tokens(nil, line), #self.tokens, 5 do - if self.tokens[i] == "doc" and (self.tokens[i+1] ~= line or dcol >= self.tokens[i+2]) then - return i - end +function DocView:next_token(idx) return idx < #self.tokens and idx + 5 or self:retrieve_tokens(#self.dcache + 1) end +function DocView:get_vline_width(vline) + local width = 0 + for _, text, style, type in self:each_vline_token(vline) do + width = width + (style.font or self:get_font()):get_width(text) end - return 1 + return width end return DocView diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index 5b7867de4..cc44ee4c6 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -81,6 +81,7 @@ function DocView:toggle_fold(start_doc_line, value) local starting_fold = self.foldable[start_doc_line] local end_doc_line = start_doc_line + 1 while end_doc_line <= #self.doc.lines do + self:compute_fold(end_doc_line+1) if self.foldable[end_doc_line] <= starting_fold then if self.doc.lines[end_doc_line]:find("}%s*$") then self.folded[end_doc_line] = value end break From 08e63a0c52d835cf74b62940a9726e677365fc8f Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 23:16:35 -0500 Subject: [PATCH 12/24] Gloriously beautiful. --- data/core/commands/docview.lua | 26 ++++++++--------- data/core/docview.lua | 52 +++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 33 deletions(-) diff --git a/data/core/commands/docview.lua b/data/core/commands/docview.lua index 5a865f7d8..5721bd7ca 100644 --- a/data/core/commands/docview.lua +++ b/data/core/commands/docview.lua @@ -94,20 +94,20 @@ local function set_cursor(dv, x, y, snap_type) core.blink_reset() end -local function insert_paste(doc, value, whole_line, idx) +local function insert_paste(dv, value, whole_line, idx) if whole_line then - local line1, col1 = doc:get_selection_idx(idx) - doc:insert(line1, 1, value:gsub("\r", "").."\n") + local line1, col1 = dv:get_selection_idx(idx) + dv:insert(line1, 1, value:gsub("\r", "").."\n") -- Because we're inserting at the start of the line, -- if the cursor is in the middle of the line -- it gets carried to the next line along with the old text. -- If it's at the start of the line it doesn't get carried, -- so we move it of as many characters as we're adding. if col1 == 1 then - doc:move_to_cursor(idx, #value+1) + dv:move_to_cursor(idx, #value+1) end else - doc:text_input(value:gsub("\r", ""), idx) + dv:text_input(value:gsub("\r", ""), idx) end end @@ -131,7 +131,7 @@ local write_commands = { core.cursor_clipboard = {} core.cursor_clipboard_whole_line = {} for idx in dv.doc:get_selections() do - insert_paste(dv.doc, clipboard, false, idx) + insert_paste(dv, clipboard, false, idx) end return end @@ -144,24 +144,24 @@ local write_commands = { break end end - if #core.cursor_clipboard_whole_line == (#dv.doc.selections/4) then + if #core.cursor_clipboard_whole_line == (#dv.selections/4) then -- If we have the same number of clipboards and selections, -- paste each clipboard into its corresponding selection - for idx in dv.doc:get_selections() do - insert_paste(dv.doc, core.cursor_clipboard[idx], only_whole_lines, idx) + for idx in dv:get_selections() do + insert_paste(dv, core.cursor_clipboard[idx], only_whole_lines, idx) end else -- Paste every clipboard and add a selection at the end of each one local new_selections = {} - for idx in dv.doc:get_selections() do + for idx in dv:get_selections() do for cb_idx in ipairs(core.cursor_clipboard_whole_line) do - insert_paste(dv.doc, core.cursor_clipboard[cb_idx], only_whole_lines, idx) + insert_paste(dv, core.cursor_clipboard[cb_idx], only_whole_lines, idx) if not only_whole_lines then - table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) + table.insert(new_selections, {dv:get_selection_idx(idx)}) end end if only_whole_lines then - table.insert(new_selections, {dv.doc:get_selection_idx(idx)}) + table.insert(new_selections, {dv:get_selection_idx(idx)}) end end local first = true diff --git a/data/core/docview.lua b/data/core/docview.lua index 85ab335af..6fa741b94 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -127,14 +127,12 @@ DocView.position_offset = Doc.position_offset function DocView:get_closest_vline(line, col) local token_idx = self:retrieve_tokens(nil, line) - if not token_idx then - return #self.vcache + 1, 1 - end + if not token_idx then return #self.vcache + 1, 1 end if self.tokens[token_idx+1] ~= line then return self.dtovcache[line] end - local total_line_length = 0 - local total_token_length = 0 local vline = self.dtovcache[line] if col and col > 1 then + local total_line_length = 0 + local total_token_length = 0 for _, text, _, type in self:each_line_token(line) do if type == "doc" then local length = text:ulen() or #text @@ -152,6 +150,12 @@ function DocView:get_closest_vline(line, col) return vline, 1 end +function DocView:get_last_vline(line) + local vline = self:get_closest_vline(line) + while vline <= #self.vcache and self.tokens[self.vcache[vline + 1]+1] == line do vline = vline + 1 end + return vline +end + function DocView:get_dline(vline, vcol) local line = self.tokens[self:retrieve_tokens(vline) + 1] local total_line_length = 0 @@ -917,6 +921,7 @@ function DocView:draw_line(line, x, y) end function DocView:draw_line_body(line, x, y) + if not self:has_tokens(line) then return 0 end -- draw highlight if any selection ends on this line local draw_highlight = false local hcl = config.highlight_current_line @@ -943,20 +948,27 @@ function DocView:draw_line_body(line, x, y) for lidx, line1, col1, line2, col2 in self:get_selections(true) do if line >= line1 and line <= line2 then local length = self.doc.lines[line]:ulen() + local x1, x2, y1, y2 if line1 ~= line then col1 = 1 end - if line2 ~= line then col2 = length + 1 end - local x1, y1 = self:get_line_screen_position(line, col1) - local x2, y2 = self:get_line_screen_position(line, col2) - if y1 ~= y2 or x1 ~= x2 then - if y1 == y2 then - renderer.draw_rect(x1, y1, x2 - x1, (y2 - y1) + lh, style.selection) + if line2 ~= line then col2 = self.doc.lines[line]:ulen() end + + local vline1, vcol1 = self:get_closest_vline(line, col1) + local vline2, vcol2 = self:get_closest_vline(line, col2) + for i = vline1, vline2 do + local offset, width, vy + if i > vline1 or (i == vline1 and line1 ~= line) then + vy = select(2, self:get_virtual_line_offset(i)) + offset = x else - local vline = self:get_closest_vline(line1, col1) - -- we need at most three rects here; one for the initial selection, one for the middle large block, one for the end - renderer.draw_rect(x1, y1, self:get_vline_width(vline) + x - x1, lh, style.selection) - if y2 > y1 + lh then renderer.draw_rect(x, y1 + lh, self:get_vline_width(vline), (y2 - y1), style.selection) end - renderer.draw_rect(x, y1 + lh, x2, lh, style.selection) + offset, vy = self:get_line_screen_position(line, col1) end + if i < vline2 or (i == vline2 and line2 ~= line) then + width = self:get_vline_width(i) + else + local x2, y2 = self:get_line_screen_position(line, col2) + width = x2 - offset + end + renderer.draw_rect(offset, vy, width, lh, style.selection) end end end @@ -1069,7 +1081,7 @@ function DocView:is_first_line_of_block(vline) return token_idx == self.dcache[self.tokens[token_idx + 1]] end -function DocView:invalidate_cache(start_doc_line) +function DocView:invalidate_cache(start_doc_line, end_doc_line) if #self.tokens == 0 then return end if not start_doc_line then start_doc_line = 1 end while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end @@ -1082,9 +1094,9 @@ function DocView:get_token_text(type, doc_line, col_start, col_end) end -function DocView:has_tokens(vline) - local token_idx = self:retrieve_tokens(vline) - return token_idx and token_idx < #self.tokens and token_idx ~= self:retrieve_tokens(vline + 1) +function DocView:has_tokens(line) + local token_idx = self:retrieve_tokens(nil, line) + return token_idx and token_idx < #self.tokens and self.tokens[token_idx+1] == line end From e1d81cb3eee8195456514a7d295bf4359ca77668 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Wed, 3 Jan 2024 23:48:04 -0500 Subject: [PATCH 13/24] Uncommented modified selcetions. --- data/core/docview.lua | 80 +++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 6fa741b94..d704d58a1 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -412,49 +412,50 @@ end function DocView:listener(type, text, line1, col1, line2, col2) self:invalidate_cache(line1, line2) -- keep cursors where they should be on insertion - -- for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do - -- if cline1 < line then break end - -- local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 - -- local column_addition = line == cline1 and ccol1 > col and len or 0 - -- self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, - -- ccol2 + column_addition) - -- end - - + if type == "insert" then + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline1 < line1 then break end + local line_addition = (line1 < cline1 or col1 < ccol1) and #lines - 1 or 0 + local column_addition = line1 == cline1 and ccol1 > col1 and len or 0 + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) + end + end -- keep selections in correct positions on removal: each pair (line, col) -- * remains unchanged if before the deleted text -- * is set to (line1, col1) if in the deleted text -- * is set to (line1, col - col_removal) if on line2 but out of the deleted text -- * is set to (line - line_removal, col) if after line2 - -- for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do - -- if cline2 < line1 then break end - -- local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 - - -- if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then - -- if cline1 > line2 then - -- l1 = l1 - line_removal - -- else - -- l1 = line1 - -- c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1 - -- end - -- end - - -- if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then - -- if cline2 > line2 then - -- l2 = l2 - line_removal - -- else - -- l2 = line1 - -- c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1 - -- end - -- end - - -- if l1 == line1 and c1 == col1 then merge = true end - -- self:set_selections(idx, l1, c1, l2, c2) - -- end - - -- if merge then - -- self:merge_cursors() - -- end + if type == "remove" then + local merge = false + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline2 < line1 then break end + local l1, c1, l2, c2 = cline1, ccol1, cline2, ccol2 + + if cline1 > line1 or (cline1 == line1 and ccol1 > col1) then + if cline1 > line2 then + l1 = l1 - line_removal + else + l1 = line1 + c1 = (cline1 == line2 and ccol1 > col2) and c1 - col_removal or col1 + end + end + + if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then + if cline2 > line2 then + l2 = l2 - line_removal + else + l2 = line1 + c2 = (cline2 == line2 and ccol2 > col2) and c2 - col_removal or col1 + end + end + + if l1 == line1 and c1 == col1 then merge = true end + self:set_selections(idx, l1, c1, l2, c2) + end + if merge then + self:merge_cursors() + end + end end function DocView:try_close(do_close) @@ -1058,9 +1059,6 @@ function DocView:draw() self:draw_scrollbar() end - --- Selections are in document space. --- Tokenize function for lines from the doc. -- Plugins hook this to return a line/col list from `doc`, or provide a virtual line. -- `{ "doc", line, 1, #self.doc.lines[line], style }` -- `{ "virtual", line, text, false, style } From 6efe819cfedb608db89c9a500bc4076176079f26 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Jan 2024 14:16:30 -0500 Subject: [PATCH 14/24] Rearranged things to be more abstracted. --- data/core/docview.lua | 135 ++++++++++++++++++++--------------- data/plugins/highlighter.lua | 12 ++-- 2 files changed, 85 insertions(+), 62 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index d704d58a1..bb7b7c51f 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -124,54 +124,6 @@ end DocView.position_offset_func = Doc.position_offset_func DocView.position_offset = Doc.position_offset - -function DocView:get_closest_vline(line, col) - local token_idx = self:retrieve_tokens(nil, line) - if not token_idx then return #self.vcache + 1, 1 end - if self.tokens[token_idx+1] ~= line then return self.dtovcache[line] end - local vline = self.dtovcache[line] - if col and col > 1 then - local total_line_length = 0 - local total_token_length = 0 - for _, text, _, type in self:each_line_token(line) do - if type == "doc" then - local length = text:ulen() or #text - if col <= total_token_length + total_line_length + length then - return vline, col - total_line_length - end - total_token_length = total_token_length + length - end - if text:find("\n$") then - total_line_length = total_line_length + total_token_length - vline = vline + 1 - end - end - end - return vline, 1 -end - -function DocView:get_last_vline(line) - local vline = self:get_closest_vline(line) - while vline <= #self.vcache and self.tokens[self.vcache[vline + 1]+1] == line do vline = vline + 1 end - return vline -end - -function DocView:get_dline(vline, vcol) - local line = self.tokens[self:retrieve_tokens(vline) + 1] - local total_line_length = 0 - local current_line = self.dtovcache[line] - for _, text, _, type in self:each_line_token(line) do - if current_line == vline then - return line, vcol + total_line_length - end - if type == "doc" then total_line_length = total_line_length + text:ulen() end - if text:find("\n$") then - current_line = current_line + 1 - end - end - return line, vcol -end - function DocView:get_virtual_line_offset(vline) local lh = self:get_line_height() local x, y = self:get_content_offset() @@ -185,7 +137,7 @@ function DocView:get_line_screen_position(line, col) local gw = self:get_gutter_width() local vline, vcol = self:get_closest_vline(line, col) y = y + (vline-1) * lh - if col and self.vcache[vline] and self.tokens[self.vcache[vline]+1] == line then + if col and self:get_vline_line(vline) == line then local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) @@ -228,9 +180,9 @@ function DocView:resolve_screen_position(x, y) local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) + local line = self:get_dline(vline) local token_idx = self:retrieve_tokens(vline) - if not token_idx then return #self.doc.lines, #self.doc.lines[#self.doc.lines] end - local line = self.tokens[token_idx+1] + if not token_idx then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end for idx, text, style, type in self:each_line_token(line) do if idx < token_idx then if type == "doc" then @@ -630,7 +582,7 @@ end function DocView:get_scrollable_size() - local max_lines = math.max(#self.doc.lines, #self.vcache) + local max_lines = math.max(#self.doc.lines, self:get_total_vlines()) if not config.scroll_past_end then local _, _, _, h_scroll = self.h_scrollbar:get_track_rect() return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2 + h_scroll @@ -676,9 +628,9 @@ end function DocView:get_visible_line_range() local minline, maxline = self:get_visible_virtual_line_range() - local min_token_idx = self:retrieve_tokens(minline) or self.vcache[#self.vcache] - local max_token_idx = self:retrieve_tokens(maxline) or self.vcache[#self.vcache] - return self.tokens[min_token_idx + 1], self.tokens[max_token_idx + 1] + local line1 = self:get_dline(minline) + local line2 = self:get_dline(maxline) + return line1, line2 end @@ -1067,6 +1019,8 @@ function DocView:tokenize(line) end --[[ +Virtual Lines Section + self.vcache maps virtual line numbers to the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. self.dcache maps doc line number sto the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. self.dtovcache maps the doc line number onto the earliest relevant virtual line number. @@ -1082,9 +1036,18 @@ end function DocView:invalidate_cache(start_doc_line, end_doc_line) if #self.tokens == 0 then return end if not start_doc_line then start_doc_line = 1 end - while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end - while #self.dcache >= start_doc_line do table.remove(self.dcache) end - while (self.vcache[#self.vcache] or 0) > #self.tokens do table.remove(self.vcache) end + --if not end_doc_line then + while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end + while #self.dcache >= start_doc_line do table.remove(self.dcache) end + while (self.vcache[#self.vcache] or 0) > #self.tokens do table.remove(self.vcache) end + -- else + -- for i = start_doc_line, end_doc_line do self.dcache[i] = false end + -- local i = self.dtovcache[start_doc_line] + -- while self.vcache[i] and self.tokens[self.vcache[i]+1] <= end_doc_line do + -- self.vcache[i] = false + -- i = i + 1 + -- end + -- end end function DocView:get_token_text(type, doc_line, col_start, col_end) @@ -1152,5 +1115,61 @@ function DocView:get_vline_width(vline) end return width end +function DocView:get_total_vlines() return #self.vcache end +function DocView:get_vline_line(vline) + local token_idx = self:retrieve_tokens(vline) + return token_idx and self.tokens[token_idx + 1] or #self.doc.lines +end + +function DocView:get_last_vline(line) + local vline = self:get_closest_vline(line) + while vline <= #self.vcache and self:get_vline_line(vline) == line do vline = vline + 1 end + return vline +end + +function DocView:get_closest_vline(line, col) + local token_idx = self:retrieve_tokens(nil, line) + if not token_idx then return #self.vcache + 1, 1 end + if self.tokens[token_idx+1] ~= line then return self.dtovcache[line] end + local vline = self.dtovcache[line] + if col and col > 1 then + local total_line_length = 0 + local total_token_length = 0 + for _, text, _, type in self:each_line_token(line) do + if type == "doc" then + local length = text:ulen() or #text + if col <= total_token_length + total_line_length + length then + return vline, col - total_line_length + end + total_token_length = total_token_length + length + end + if text:find("\n$") then + total_line_length = total_line_length + total_token_length + vline = vline + 1 + end + end + end + return vline, 1 +end + + +function DocView:get_dline(vline, vcol) + local token_idx = self:retrieve_tokens(vline) + if not token_idx then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end + local line = self.tokens[token_idx + 1] + vcol = vcol or 1 + local total_line_length = 0 + local current_line = self.dtovcache[line] + for _, text, _, type in self:each_line_token(line) do + if current_line == vline then + return line, vcol + total_line_length + end + if type == "doc" then total_line_length = total_line_length + text:ulen() end + if text:find("\n$") then + current_line = current_line + 1 + end + end + return line, vcol +end return DocView diff --git a/data/plugins/highlighter.lua b/data/plugins/highlighter.lua index af0a08190..373df7815 100644 --- a/data/plugins/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -14,6 +14,7 @@ function Highlighter:new(doc) self.doc = doc self.running = false self:reset() + self.state = {} end @@ -128,14 +129,17 @@ function DocView:tokenize(line) local tokens = old_tokenize(self, line) if #tokens == 0 then return tokens end - local highlighted_tokens = self.doc.highlighter:get_line(line).tokens + local tokenized = self.doc.highlighter:get_line(line) + -- Ensure we tokenize the next line if our state is different. + if self.doc.highlighter.state[line] and self.doc.highlighter.state[line] ~= tokenized.state then + self:invalidate_cache(line + 1, line + 1) + end + self.doc.highlighter.state[line] = tokenized.state -- Walk through all doc tokens, and then map them onto what we've tokenized. local colorized = {} - local start_offset = 1 - local start_highlighted = 1 for i = 1, #tokens, 5 do if tokens[i] == "doc" then - local t = get_tokens(highlighted_tokens, tokens[i+1], tokens[i+2], tokens[i+3], tokens[i+4]) + local t = get_tokens(tokenized.tokens, tokens[i+1], tokens[i+2], tokens[i+3], tokens[i+4]) table.move(t, 1, #t, #colorized + 1, colorized) else table.move(tokens, i, i + 4, #colorized + 1, colorized) From 50fac204c9b2180cd4cb2017fceac1102d2059ec Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Jan 2024 14:25:36 -0500 Subject: [PATCH 15/24] Fixed line length issue. --- data/core/docview.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index bb7b7c51f..815becd8c 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -916,7 +916,7 @@ function DocView:draw_line_body(line, x, y) offset, vy = self:get_line_screen_position(line, col1) end if i < vline2 or (i == vline2 and line2 ~= line) then - width = self:get_vline_width(i) + width = self:get_vline_width(i) - (offset - x) else local x2, y2 = self:get_line_screen_position(line, col2) width = x2 - offset @@ -1109,7 +1109,7 @@ function DocView:each_token(tokens, idx) return token_iter, { self, tokens }, (( function DocView:each_line_token(line) return dline_iter, { self, line }, self:retrieve_tokens(nil, line) end function DocView:next_token(idx) return idx < #self.tokens and idx + 5 or self:retrieve_tokens(#self.dcache + 1) end function DocView:get_vline_width(vline) - local width = 0 + local width = -self:get_font():get_width("\n") for _, text, style, type in self:each_vline_token(vline) do width = width + (style.font or self:get_font()):get_width(text) end From 9c694ae9bbfd9b5d3a686319a3d2bd37b1470f3e Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Jan 2024 17:36:50 -0500 Subject: [PATCH 16/24] Rearchitected token storage. --- data/core/docview.lua | 317 ++++++++++++++++++++++++++++-------------- 1 file changed, 216 insertions(+), 101 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 815becd8c..7533533c8 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -66,7 +66,6 @@ function DocView:new(doc) self.ime_status = false self.hovering_gutter = false self.read_only = false - self.tokens = {} self.vcache = {} self.dcache = {} self.dtovcache = {} @@ -83,47 +82,10 @@ function DocView:get_char(line, col) return self.doc.lines[line]:sub(col, col) end -function DocView:position_offset_byte(line, col, offset) - local token_idx = self:retrieve_tokens(nil, line) - for idx = token_idx, #self.tokens, 5 do - if self.tokens[idx] == "doc" then token_idx = idx if self.tokens[idx+1] ~= line or col >= self.tokens[idx+2] then break end end - end - if offset > 0 then - local total_offset = (col + offset) - self.tokens[token_idx+2] - while token_idx do - if self.tokens[token_idx] == "doc" then - local width = (self.tokens[token_idx+3] - self.tokens[token_idx+2]) + 1 - if total_offset < width then - return self.tokens[token_idx+1], self.tokens[token_idx+2] + total_offset - end - total_offset = total_offset - width - end - token_idx = self:next_token(token_idx) - end - return #self.doc.lines, #self.doc.lines[#self.doc.lines] - else - local total_offset = self.tokens[token_idx+3] - (col + offset) - while token_idx > 0 do - if self.tokens[token_idx] == "doc" then - local width = (self.tokens[token_idx+3] - self.tokens[token_idx+2]) + 1 - if total_offset < width then - return self.tokens[token_idx+1], self.tokens[token_idx+3] - total_offset - end - total_offset = total_offset - width - end - token_idx = token_idx - 5 - end - return 1,1 - end -end - function DocView:sanitize_position(line, col) return self.doc:sanitize_position(line, col) end -DocView.position_offset_func = Doc.position_offset_func -DocView.position_offset = Doc.position_offset - function DocView:get_virtual_line_offset(vline) local lh = self:get_line_height() local x, y = self:get_content_offset() @@ -621,15 +583,15 @@ function DocView:get_visible_virtual_line_range() local x1, y1, x2, y2 = self:get_content_bounds() local lh = self:get_line_height() local minline = math.max(1, math.floor((y1 - style.padding.y) / lh) + 1) - local maxline = math.floor((y2 - style.padding.y) / lh) + 1 + local maxline = math.max(math.floor((y2 - style.padding.y) / lh), 0) + 1 return minline, maxline end function DocView:get_visible_line_range() local minline, maxline = self:get_visible_virtual_line_range() - local line1 = self:get_dline(minline) local line2 = self:get_dline(maxline) + local line1 = self:get_dline(minline) return line1, line2 end @@ -1021,33 +983,130 @@ end --[[ Virtual Lines Section -self.vcache maps virtual line numbers to the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. -self.dcache maps doc line number sto the point in the self.tokens array where that line starts. It is always guaranteed to be correct, up until the point where it's invalid. +self.dcache holds a table of tokens for the relevant line. +self.vcache holds a reference to the doc line that's relevant for this vline, and offset into this table for this number. self.dtovcache maps the doc line number onto the earliest relevant virtual line number. -self.tokens contains the stream of transformed tokens. Each token can contain *at most* one new line, at the end of it. ]] -function DocView:is_first_line_of_block(vline) - local token_idx = self.vcache[vline] - return token_idx == self.dcache[self.tokens[token_idx + 1]] +local function mkvoffset(line, offset) return ((line << 32) | offset) end +local function getvoffset(number) if not number then return nil end return number >> 32, number & 0xFFFFFFFF end + +local function tokenize_line(self, vline, line) + local tokens = self:tokenize(line) + local vlines = {} + if #tokens > 0 then + table.insert(vlines, mkvoffset(line, 1)) + end + for j = 1, #tokens, 5 do + local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) + if text:find("\n$") and j < #tokens - 5 then + vline = vline + 1 + table.insert(vlines, mkvoffset(line, j)) + end + end + return tokens, vlines end +function DocView:retrieve_tokens(vline, line) + while ((vline and vline > #self.vcache) or (line and line > #self.dcache)) and #self.dcache < #self.doc.lines do + local tokens, vlines = tokenize_line(self, #self.vcache + 1, #self.dcache + 1) + table.insert(self.dcache, tokens) + self.dtovcache[#self.dcache] = #self.vcache + 1 + table.move(vlines, 1, #vlines, #self.vcache + 1, self.vcache) + end + if vline and self.vcache[vline] then return getvoffset(self.vcache[vline]) end + if line and self.dcache[line] then return line, 1 end + -- If we're here, it means we need to tokenize a block of lines in the middle of the document. Start from the beginning up until the relevant line. + if (vline and vline < #self.vcache) or (line and line < #self.dcache) then + local start_line, end_line + if vline then + local invalid_line = vline + while self.vcache[invalid_line - 1] == false and invalid_line > 1 do + invalid_line = invalid_line - 1 + end + if invalid_line > 1 then + local l, offset = getvoffset(self.vcache[invalid_line - 1]) + start_line = l + 1 + while self.dcache[start_line] do + start_line = start_line + 1 + end + else + start_line = 1 + end + while self.vcache[invalid_line + 1] == false do + invalid_line = invalid_line + 1 + end + local l, offset = getvoffset(self.vcache[invalid_line + 1]) + end_line = l - 1 + else + end_line = line + start_line = line + while self.dcache[start_line - 1] == false and start_line > 1 do + start_line = start_line - 1 + end + if start_line > 1 then + vline = self.dtovcache[start_line - 1] + self:get_vlines(start_line - 1) + else + vline = 1 + end + end + + -- From start to end of the block, compute all new tokens, and insert them appropriately. + local total_vlines = {} + local start_vline = vline + local total_free_vlines = 0 + for i = start_vline, #self.vcache do + if self.vcache[i] then break end + total_free_vlines = total_free_vlines + 1 + end + for i = start_line, end_line do + local tokens, vlines = tokenize_line(self, i, vline) + self.dcache[i] = tokens + self.dtovcache[i] = vline + table.move(vlines, 1, #vlines, #total_vlines + 1, total_vlines) + vline = vline + #vlines + end + -- Adjust the vcache as necessary to ensure that we have enough space. + local differential = #total_vlines - total_free_vlines + if differential ~= 0 then + table.move(self.vcache, start_vline + total_free_vlines, #self.vcache, start_vline + #total_vlines) + for i = end_line + 1, #self.dtovcache do + self.dtovcache[i] = self.dtovcache[i] + differential + end + end + table.move(total_vlines, 1, #total_vlines, start_vline, self.vcache) + return end_line, 1 + end + return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() +end + + +-- This function is very important, and is always potentially more destructive +-- than indicated with its parameters. It will invalidate *at least* as much as +-- you specify, but may invalidate more. function DocView:invalidate_cache(start_doc_line, end_doc_line) - if #self.tokens == 0 then return end if not start_doc_line then start_doc_line = 1 end - --if not end_doc_line then - while #self.tokens >= self.dcache[start_doc_line] do table.remove(self.tokens) end + if not end_doc_line then end_doc_line = #self.dcache end + if end_doc_line >= #self.dcache then while #self.dcache >= start_doc_line do table.remove(self.dcache) end - while (self.vcache[#self.vcache] or 0) > #self.tokens do table.remove(self.vcache) end - -- else - -- for i = start_doc_line, end_doc_line do self.dcache[i] = false end - -- local i = self.dtovcache[start_doc_line] - -- while self.vcache[i] and self.tokens[self.vcache[i]+1] <= end_doc_line do - -- self.vcache[i] = false - -- i = i + 1 - -- end - -- end + while self.vcache[#self.vcache] do + local line, offset = getvoffset(self.vcache[#self.vcache]) + if line < start_doc_line then break end + table.remove(self.vcache) + end + else + for line = start_doc_line, end_doc_line do + self.dcache[line] = false + local vline = self.dtovcache[line] + while true do + local l, offset = getvoffset(self.vcache[vline]) + if l ~= line then break end + self.vcache[vline] = false + vline = vline + 1 + end + end + end end function DocView:get_token_text(type, doc_line, col_start, col_end) @@ -1056,16 +1115,16 @@ end function DocView:has_tokens(line) - local token_idx = self:retrieve_tokens(nil, line) - return token_idx and token_idx < #self.tokens and self.tokens[token_idx+1] == line + local line, offset = self:retrieve_tokens(nil, line) + return line <= #self.dcache and #self.dcache[line] > 0 end -local function vline_iter(state, idx) - local self, line = table.unpack(state) - if not idx or not self.tokens[idx] or (self.vcache[line + 1] and idx >= self.vcache[line + 1]) then return nil end - local text = self:get_token_text(self.tokens[idx], self.tokens[idx+1], self.tokens[idx+2], self.tokens[idx+3]) - return idx + 5, text, self.tokens[idx+4], self.tokens[idx] +local function vline_iter(state, offset1) + local self, line, offset2 = table.unpack(state) + if not line or offset1 > offset2 then return nil end + local text = self:get_token_text(self.dcache[line][offset1], self.dcache[line][offset1+1], self.dcache[line][offset1+2], self.dcache[line][offset1+3]) + return offset1 + 5, text, self.dcache[line][offset1+4], self.dcache[line][offset1] end @@ -1077,37 +1136,24 @@ end local function dline_iter(state, idx) local self, line = table.unpack(state) - if not idx or not self.tokens[idx] or self.tokens[idx+1] ~= line then return nil end - local text = self:get_token_text(self.tokens[idx], self.tokens[idx+1], self.tokens[idx+2], self.tokens[idx+3]) - return idx + 5, text, self.tokens[idx+4], self.tokens[idx] + local tokens = self.dcache[line] + if not idx or idx > #tokens then return nil end + local text = self:get_token_text(tokens[idx], tokens[idx+1], tokens[idx+2], tokens[idx+3]) + return idx + 5, text, tokens[idx+4], tokens[idx] end -function DocView:retrieve_tokens(vline, line) - while ((vline and vline > #self.vcache) or (line and line > #self.dcache)) and #self.dcache < #self.doc.lines do - local tokens = self:tokenize(#self.dcache + 1) - local bundles = #tokens / 5 - table.insert(self.dcache, #self.tokens + 1) - if #tokens > 0 then - table.insert(self.vcache, #self.tokens + 1) - end - self.dtovcache[#self.dcache] = #self.vcache - for j = 1, #tokens, 5 do - local text = self:get_token_text(tokens[j], tokens[j+1], tokens[j+2], tokens[j+3]) - table.move(tokens, j, j+4, #self.tokens + 1, self.tokens) - if text:find("\n$") and j < #tokens - 5 then - table.insert(self.vcache, #self.tokens + 1) - end - end - end - if vline then return self.vcache[vline] end - return self.dcache[line] +function DocView:each_vline_token(vline) + local line1, offset1 = self:retrieve_tokens(vline) + local line2, offset2 = self:retrieve_tokens(vline + 1) + if line1 ~= line2 then offset2 = #self.dcache[line1] end + return vline_iter, { self, line1, offset2 }, offset1 +end +function DocView:each_line_token(line) + local l = self:retrieve_tokens(nil, line) + return dline_iter, { self, line }, l and 1 end - -function DocView:each_vline_token(vline) return vline_iter, { self, vline }, self:retrieve_tokens(vline) end function DocView:each_token(tokens, idx) return token_iter, { self, tokens }, (((idx or 1) - 1) * 5) + 1 end -function DocView:each_line_token(line) return dline_iter, { self, line }, self:retrieve_tokens(nil, line) end -function DocView:next_token(idx) return idx < #self.tokens and idx + 5 or self:retrieve_tokens(#self.dcache + 1) end function DocView:get_vline_width(vline) local width = -self:get_font():get_width("\n") for _, text, style, type in self:each_vline_token(vline) do @@ -1116,9 +1162,18 @@ function DocView:get_vline_width(vline) return width end function DocView:get_total_vlines() return #self.vcache end +function DocView:get_vlines(line) + local vlines = 0 + for i = self.dtovcache[line], #self.vcache do + local l, offset = getvoffset(self.vcache[i]) + if l ~= line then break end + vlines = vlines + 1 + end + return vlines +end function DocView:get_vline_line(vline) - local token_idx = self:retrieve_tokens(vline) - return token_idx and self.tokens[token_idx + 1] or #self.doc.lines + local line = self:retrieve_tokens(vline) + return line or #self.doc.lines end function DocView:get_last_vline(line) @@ -1127,10 +1182,14 @@ function DocView:get_last_vline(line) return vline end +function DocView:is_first_line_of_block(vline) + local _, offset = getvoffset(self.vcache[vline]) + return offset == 1 +end + function DocView:get_closest_vline(line, col) - local token_idx = self:retrieve_tokens(nil, line) - if not token_idx then return #self.vcache + 1, 1 end - if self.tokens[token_idx+1] ~= line then return self.dtovcache[line] end + line = self:retrieve_tokens(nil, line) + if not line then return #self.vcache + 1, 1 end local vline = self.dtovcache[line] if col and col > 1 then local total_line_length = 0 @@ -1152,12 +1211,10 @@ function DocView:get_closest_vline(line, col) return vline, 1 end - function DocView:get_dline(vline, vcol) - local token_idx = self:retrieve_tokens(vline) - if not token_idx then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end - local line = self.tokens[token_idx + 1] - vcol = vcol or 1 + local line, col = self:retrieve_tokens(vline) + if not line then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end + if not vcol then vcol = 1 end local total_line_length = 0 local current_line = self.dtovcache[line] for _, text, _, type in self:each_line_token(line) do @@ -1172,4 +1229,62 @@ function DocView:get_dline(vline, vcol) return line, vcol end + +DocView.position_offset_func = Doc.position_offset_func +DocView.position_offset = Doc.position_offset +function DocView:position_offset_byte(line, col, offset) + local pos + line, pos = self:retrieve_tokens(nil, line) + if offset > 0 then + local from_token_start + while line do + local tokens = self.dcache[line] + for i = pos, #tokens, 5 do + if tokens[i] == "doc" then + local width = tokens[i+3] - tokens[i+2] + 1 + if not from_token_start then + if col >= tokens[i+2] then + from_token_start = col - tokens[i+2] + offset + end + end + if from_token_start then + if from_token_start < width then + return line, tokens[i+2] + from_token_start + end + from_token_start = from_token_start - width + end + end + end + line = self:retrieve_tokens(nil, line + 1) + pos = 1 + end + return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() + else + local from_token_end + while line and line > 0 do + local tokens = self.dcache[line] + for i = pos, 1, -5 do + if tokens[i] == "doc" then + local width = tokens[i+3] - tokens[i+2] + 1 + if not from_token_end then + if col >= tokens[i+2] then + from_token_end = tokens[i+3] - col - offset + end + end + if from_token_end then + if from_token_end < width then + return line, tokens[i+3] - from_token_end + end + from_token_end = from_token_end - width + end + end + end + line = self:retrieve_tokens(nil, line - 1) + pos = line and (#self.dcache[line] - 4) + end + return 1,1 + end +end + + return DocView From 40b1d25925ddc4d89ffb2aa2a14f607471cd4249 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 7 Jan 2024 17:49:42 -0500 Subject: [PATCH 17/24] Fixed invalidations. --- data/core/docview.lua | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 7533533c8..31e43865a 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -324,9 +324,12 @@ function DocView:text_input(text, idx) end function DocView:listener(type, text, line1, col1, line2, col2) - self:invalidate_cache(line1, line2) + local invalid_line1, invalid_line2 = line1, line2 + if type == "insert" and text:find("\n") then invalid_line2 = nil end + if type == "remove" and line2 ~= line1 then invalid_line2 = nil end -- keep cursors where they should be on insertion if type == "insert" then + self:invalidate_cache(invalid_line1, invalid_line2) for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do if cline1 < line1 then break end local line_addition = (line1 < cline1 or col1 < ccol1) and #lines - 1 or 0 @@ -340,6 +343,7 @@ function DocView:listener(type, text, line1, col1, line2, col2) -- * is set to (line1, col - col_removal) if on line2 but out of the deleted text -- * is set to (line - line_removal, col) if after line2 if type == "remove" then + self:invalidate_cache(invalid_line1, invalid_line2) local merge = false for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do if cline2 < line1 then break end @@ -1090,20 +1094,18 @@ function DocView:invalidate_cache(start_doc_line, end_doc_line) if not end_doc_line then end_doc_line = #self.dcache end if end_doc_line >= #self.dcache then while #self.dcache >= start_doc_line do table.remove(self.dcache) end - while self.vcache[#self.vcache] do + while self.vcache[#self.vcache] ~= nil do local line, offset = getvoffset(self.vcache[#self.vcache]) - if line < start_doc_line then break end + if line and line < start_doc_line then break end table.remove(self.vcache) end else for line = start_doc_line, end_doc_line do self.dcache[line] = false - local vline = self.dtovcache[line] - while true do + for vline = self.dtovcache[line], #self.vcache do local l, offset = getvoffset(self.vcache[vline]) if l ~= line then break end self.vcache[vline] = false - vline = vline + 1 end end end From 58fb11d7ff09da93c7a4beafa33d1fac07130dfa Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Mon, 8 Jan 2024 00:13:37 -0500 Subject: [PATCH 18/24] Hooked up highlighter to syntax highlighting. --- data/core/doc/init.lua | 13 ++-- data/core/docview.lua | 17 ++--- data/plugins/highlighter.lua | 135 ++++++++++++++--------------------- 3 files changed, 63 insertions(+), 102 deletions(-) diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index 152faa90e..aeee7bbff 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -20,7 +20,7 @@ end function Doc:new(filename, abs_filename, new_file) self.new_file = new_file - self.listeners = {} + self.listeners = setmetatable({}, { __mode = "v" }) self:reset() if filename then self:set_filename(filename, abs_filename) @@ -49,10 +49,7 @@ function Doc:reset_syntax() path = core.project_dir .. PATHSEP .. self.filename end if path then path = common.normalize_path(path) end - local syn = syntax.get(path, header) - if self.syntax ~= syn then - self.syntax = syn - end + self.syntax = syntax.get(path, header) end function Doc:set_filename(filename, abs_filename) @@ -238,7 +235,7 @@ local function pop_undo(self, undo_stack, redo_stack, modified) return pop_undo(self, undo_stack, redo_stack, modified) end - if modified then for i,v in ipairs(self.listeners) do v("undo") end end + if modified then for i,v in ipairs(self.listeners) do v:on_doc_change("undo") end end end @@ -263,7 +260,7 @@ function Doc:raw_insert(line, col, text, undo_stack, time) local line2, col2 = self:position_offset(line, col, #text) push_undo(undo_stack, time, "remove", line, col, line2, col2) - for i,v in ipairs(self.listeners) do v("insert", text, line, col, line, col) end + for i,v in ipairs(self.listeners) do v:on_doc_change("insert", text, line, col, line, col) end end function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) @@ -283,7 +280,7 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) local merge = false - for i,v in ipairs(self.listeners) do v("remove", "", line1, col1, line2, col2) end + for i,v in ipairs(self.listeners) do v:on_doc_change("remove", "", line1, col1, line2, col2) end end diff --git a/data/core/docview.lua b/data/core/docview.lua index 31e43865a..62ab4f444 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -74,7 +74,7 @@ function DocView:new(doc) self.last_selection = 1 self.v_scrollbar:set_forced_status(config.force_scrollbar_status) self.h_scrollbar:set_forced_status(config.force_scrollbar_status) - table.insert(doc.listeners, function(...) self:listener(...) end) + table.insert(doc.listeners, self) end function DocView:get_char(line, col) @@ -323,7 +323,7 @@ function DocView:text_input(text, idx) end end -function DocView:listener(type, text, line1, col1, line2, col2) +function DocView:on_doc_change(type, text, line1, col1, line2, col2) local invalid_line1, invalid_line2 = line1, line2 if type == "insert" and text:find("\n") then invalid_line2 = nil end if type == "remove" and line2 ~= line1 then invalid_line2 = nil end @@ -1157,7 +1157,7 @@ function DocView:each_line_token(line) end function DocView:each_token(tokens, idx) return token_iter, { self, tokens }, (((idx or 1) - 1) * 5) + 1 end function DocView:get_vline_width(vline) - local width = -self:get_font():get_width("\n") + local width = 0 for _, text, style, type in self:each_vline_token(vline) do width = width + (style.font or self:get_font()):get_width(text) end @@ -1173,21 +1173,12 @@ function DocView:get_vlines(line) end return vlines end + function DocView:get_vline_line(vline) local line = self:retrieve_tokens(vline) return line or #self.doc.lines end -function DocView:get_last_vline(line) - local vline = self:get_closest_vline(line) - while vline <= #self.vcache and self:get_vline_line(vline) == line do vline = vline + 1 end - return vline -end - -function DocView:is_first_line_of_block(vline) - local _, offset = getvoffset(self.vcache[vline]) - return offset == 1 -end function DocView:get_closest_vline(line, col) line = self:retrieve_tokens(nil, line) diff --git a/data/plugins/highlighter.lua b/data/plugins/highlighter.lua index 373df7815..1d60627fb 100644 --- a/data/plugins/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -2,6 +2,7 @@ local core = require "core" local config = require "core.config" local style = require "core.style" +local Doc = require "core.doc" local DocView = require "core.docview" local common = require "core.common" local tokenizer = require "core.tokenizer" @@ -10,84 +11,63 @@ local Object = require "core.object" local Highlighter = Object:extend() +config.plugins.highlighter = common.merge({ + lines_per_step = 40 +}, config.plugins.highlighter) + + function Highlighter:new(doc) self.doc = doc self.running = false - self:reset() - self.state = {} + self.lines = {} + self.syntax = self.doc.syntax + self.views = setmetatable({}, { __mode = "k" }) + table.insert(doc.listeners, self) end +local old_doc_reset_syntax = Doc.reset_syntax +function Doc:reset_syntax() + old_doc_reset_syntax(self) + if self.highlighter and self.syntax ~= self.highlighter.syntax then + self:start(1) + end +end + -- init incremental syntax highlighting -function Highlighter:start() +function Highlighter:start(s, e) + if s then self.first_invalid_line = math.min(self.first_invalid_line, s) end + if e then self.max_wanted_line = math.max(self.max_wanted_line or 0, e) end if self.running then return end self.running = true core.add_thread(function() - while self.first_invalid_line <= self.max_wanted_line do - local max = math.min(self.first_invalid_line + 40, self.max_wanted_line) - local retokenized_from + while self.first_invalid_line and self.max_wanted_line and self.first_invalid_line <= self.max_wanted_line do + local max = math.min(self.first_invalid_line + config.plugins.highlighter.step_line_tokenization, self.max_wanted_line) for i = self.first_invalid_line, max do local state = (i > 1) and self.lines[i - 1].state local line = self.lines[i] - if line and line.resume and (line.init_state ~= state or line.text ~= self.doc.lines[i]) then - -- Reset the progress if no longer valid - line.resume = nil - end - if not (line and line.init_state == state and line.text == self.doc.lines[i] and not line.resume) then - retokenized_from = retokenized_from or i - self.lines[i] = self:tokenize_line(i, state, line and line.resume) - if self.lines[i].resume then - self.first_invalid_line = i - goto yield - end - elseif retokenized_from then - retokenized_from = nil + if not (line and line.init_state == state and line.text == self.doc.lines[i]) then + self.lines[i] = self:tokenize_line(i, state) end end - - ::yield:: + for view in pairs(self.views) do view:invalidate_cache(self.first_invalid_line, max) end self.first_invalid_line = max + 1 core.redraw = true coroutine.yield(0) end self.max_wanted_line = 0 self.running = false - end, self) -end - -local function set_max_wanted_lines(self, amount) - self.max_wanted_line = amount - if self.first_invalid_line <= self.max_wanted_line then - self:start() - end + end) end -function Highlighter:reset() - self.lines = {} - self:soft_reset() -end - - -function Highlighter:soft_reset() - for i=1,#self.lines do - self.lines[i] = false - end - self.first_invalid_line = 1 - self.max_wanted_line = 0 -end - - -function Highlighter:invalidate(idx) - self.first_invalid_line = math.min(self.first_invalid_line, idx) - set_max_wanted_lines(self, math.min(self.max_wanted_line, #self.doc.lines)) +function Highlighter:on_doc_change(type, text, line1, col1, line2, col2) + if type == "insert" or type == "remove" then self:start(math.min(line1, line2)) end end function Highlighter:tokenize_line(idx, state, resume) - local res = {} - res.init_state = state - res.text = self.doc.lines[idx] + local res = { init_state = state, text = self.doc.lines[idx] } res.tokens, res.state, res.resume = tokenizer.tokenize(self.doc.syntax, res.text, state, resume) return res end @@ -98,51 +78,44 @@ function Highlighter:get_line(idx) if not line or line.text ~= self.doc.lines[idx] then local prev = self.lines[idx - 1] line = self:tokenize_line(idx, prev and prev.state) + if self.lines[idx] and line.state ~= self.lines[idx].state then + for view in pairs(self.views) do view:invalidate_cache(idx, idx + 1) end + end self.lines[idx] = line end - set_max_wanted_lines(self, math.max(self.max_wanted_line, idx)) + self:start(nil, idx) return line end -local function get_tokens(highlighted_tokens, doc_line, start_offset, end_offset, token_style) - local tokens = {} - local offset = 1 - for i = 1, #highlighted_tokens, 2 do - local type, text = highlighted_tokens[i], highlighted_tokens[i+1] - if offset <= end_offset and offset + #text >= start_offset then - table.insert(tokens, "doc") - table.insert(tokens, doc_line) - table.insert(tokens, math.max(start_offset, offset)) - table.insert(tokens, math.min(end_offset, offset + #text - 1)) - table.insert(tokens, common.merge(token_style, { color = style.syntax[type], font = style.syntax_fonts[type] })) - end - if offset > end_offset then break end - offset = offset + #text - end - return tokens -end - local old_tokenize = DocView.tokenize function DocView:tokenize(line) - if not self.doc.highighter then self.doc.highlighter = Highlighter(self.doc) end + if not self.doc.highlighter then self.doc.highlighter = Highlighter(self.doc) end + local highlighter = self.doc.highlighter + if not highlighter.views[self] then highlighter.views[self] = true end local tokens = old_tokenize(self, line) if #tokens == 0 then return tokens end - local tokenized = self.doc.highlighter:get_line(line) - -- Ensure we tokenize the next line if our state is different. - if self.doc.highlighter.state[line] and self.doc.highlighter.state[line] ~= tokenized.state then - self:invalidate_cache(line + 1, line + 1) - end - self.doc.highlighter.state[line] = tokenized.state + local tokenized = highlighter:get_line(line) -- Walk through all doc tokens, and then map them onto what we've tokenized. local colorized = {} - for i = 1, #tokens, 5 do - if tokens[i] == "doc" then - local t = get_tokens(tokenized.tokens, tokens[i+1], tokens[i+2], tokens[i+3], tokens[i+4]) - table.move(t, 1, #t, #colorized + 1, colorized) + for idx, type, doc_line, start_offset, end_offset, token_style in self:each_token(tokens) do + if type == "doc" then + local offset = 1 + for i = 1, #tokenized.tokens, 2 do + local type, text = tokenized.tokens[i], tokenized.tokens[i+1] + if offset <= end_offset and offset + #text >= start_offset then + table.insert(colorized, "doc") + table.insert(colorized, doc_line) + table.insert(colorized, math.max(start_offset, offset)) + table.insert(colorized, math.min(end_offset, offset + #text - 1)) + table.insert(colorized, common.merge(token_style, { color = style.syntax[type], font = style.syntax_fonts[type] })) + end + if offset > end_offset then break end + offset = offset + #text + end else - table.move(tokens, i, i + 4, #colorized + 1, colorized) + table.move(tokens, idx - 5, idx - 1, #colorized + 1, colorized) end end return #colorized > 0 and colorized or { "doc", line, 1, 1, {} } From 95403c5c4370cae4bfa2ed2af26672bfae3105fc Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Mon, 8 Jan 2024 00:52:41 -0500 Subject: [PATCH 19/24] Added in rqeuest to tokenize extra lines. --- data/plugins/highlighter.lua | 51 ++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/data/plugins/highlighter.lua b/data/plugins/highlighter.lua index 1d60627fb..8dc621d7f 100644 --- a/data/plugins/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -21,6 +21,8 @@ function Highlighter:new(doc) self.running = false self.lines = {} self.syntax = self.doc.syntax + self.first_invalid_line = 1 + self.max_wanted_line = 0 self.views = setmetatable({}, { __mode = "k" }) table.insert(doc.listeners, self) end @@ -34,15 +36,26 @@ function Doc:reset_syntax() end end --- init incremental syntax highlighting -function Highlighter:start(s, e) - if s then self.first_invalid_line = math.min(self.first_invalid_line, s) end - if e then self.max_wanted_line = math.max(self.max_wanted_line or 0, e) end - if self.running then return end - self.running = true - core.add_thread(function() - while self.first_invalid_line and self.max_wanted_line and self.first_invalid_line <= self.max_wanted_line do - local max = math.min(self.first_invalid_line + config.plugins.highlighter.step_line_tokenization, self.max_wanted_line) + +function Highlighter:on_doc_change(type, text, line1, col1, line2, col2) + if type == "insert" or type == "remove" then self:start(math.min(line1, line2)) end +end + + +function Highlighter:tokenize_line(idx, state, resume) + local res = { init_state = state, text = self.doc.lines[idx] } + res.tokens, res.state, res.resume = tokenizer.tokenize(self.doc.syntax, res.text, state, resume) + return res +end + + +function Highlighter:start(first_invalid_line, max_wanted_line) + if first_invalid_line then self.first_invalid_line = common.clamp(self.first_invalid_line, 1, first_invalid_line) end + if max_wanted_line then self.max_wanted_line = common.clamp(self.max_wanted_line, #self.doc.lines, max_wanted_line) end + + self.running = self.running or core.add_thread(function() + while self.first_invalid_line <= self.max_wanted_line do + local max = math.min(self.first_invalid_line + config.plugins.highlighter.lines_per_step, self.max_wanted_line) for i = self.first_invalid_line, max do local state = (i > 1) and self.lines[i - 1].state local line = self.lines[i] @@ -56,30 +69,18 @@ function Highlighter:start(s, e) coroutine.yield(0) end self.max_wanted_line = 0 - self.running = false + self.running = nil end) end -function Highlighter:on_doc_change(type, text, line1, col1, line2, col2) - if type == "insert" or type == "remove" then self:start(math.min(line1, line2)) end -end - - -function Highlighter:tokenize_line(idx, state, resume) - local res = { init_state = state, text = self.doc.lines[idx] } - res.tokens, res.state, res.resume = tokenizer.tokenize(self.doc.syntax, res.text, state, resume) - return res -end - - function Highlighter:get_line(idx) local line = self.lines[idx] - if not line or line.text ~= self.doc.lines[idx] then - local prev = self.lines[idx - 1] + local prev = self.lines[idx - 1] + if not line or line.text ~= self.doc.lines[idx] or (prev and line.init_state ~= prev.state) then line = self:tokenize_line(idx, prev and prev.state) if self.lines[idx] and line.state ~= self.lines[idx].state then - for view in pairs(self.views) do view:invalidate_cache(idx, idx + 1) end + self:start(idx + 1, idx + config.plugins.highlighter.lines_per_step) end self.lines[idx] = line end From 9567ae80fbdc76e621cd465259327590960ce861 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 21 Jan 2024 14:10:40 -0500 Subject: [PATCH 20/24] Reduced size. --- data/plugins/codefolding.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index cc44ee4c6..abe8704d7 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -7,7 +7,6 @@ local DocView = require "core.docview" local Node = require "core.node" local common = require "core.common" - function DocView:is_folded(doc_line) return self.folded[doc_line+1] end @@ -81,7 +80,7 @@ function DocView:toggle_fold(start_doc_line, value) local starting_fold = self.foldable[start_doc_line] local end_doc_line = start_doc_line + 1 while end_doc_line <= #self.doc.lines do - self:compute_fold(end_doc_line+1) + self:compute_fold(end_doc_line+1) if self.foldable[end_doc_line] <= starting_fold then if self.doc.lines[end_doc_line]:find("}%s*$") then self.folded[end_doc_line] = value end break @@ -103,7 +102,7 @@ end local old_draw_line_gutter = DocView.draw_line_gutter function DocView:draw_line_gutter(line, x, y, width) local lh = old_draw_line_gutter(self, line, x, y, width) - local size = lh - 4 + local size = lh - 8 local startx = x + 4 local starty = y + (lh - size) / 2 if self:is_foldable(line) then From 3c9b3c700cb671dbd928d04e12ac293a6a5c6a5a Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 21 Jan 2024 14:15:42 -0500 Subject: [PATCH 21/24] Bumped modversion. --- data/core/start.lua | 2 +- data/plugins/autocomplete.lua | 2 +- data/plugins/autoreload.lua | 2 +- data/plugins/codefolding.lua | 2 +- data/plugins/contextmenu.lua | 2 +- data/plugins/detectindent.lua | 2 +- data/plugins/drawwhitespace.lua | 2 +- data/plugins/highlighter.lua | 2 +- data/plugins/language_c.lua | 2 +- data/plugins/language_cpp.lua | 2 +- data/plugins/language_css.lua | 2 +- data/plugins/language_html.lua | 2 +- data/plugins/language_js.lua | 2 +- data/plugins/language_lua.lua | 2 +- data/plugins/language_md.lua | 2 +- data/plugins/language_python.lua | 2 +- data/plugins/language_xml.lua | 2 +- data/plugins/lineguide.lua | 2 +- data/plugins/linewrapping.lua | 2 +- data/plugins/macro.lua | 2 +- data/plugins/projectsearch.lua | 2 +- data/plugins/quote.lua | 2 +- data/plugins/reflow.lua | 2 +- data/plugins/scale.lua | 2 +- data/plugins/tabularize.lua | 2 +- data/plugins/toolbarview.lua | 2 +- data/plugins/treeview.lua | 2 +- data/plugins/trimwhitespace.lua | 2 +- data/plugins/workspace.lua | 2 +- 29 files changed, 29 insertions(+), 29 deletions(-) diff --git a/data/core/start.lua b/data/core/start.lua index 07e1e3857..558a5af8b 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,6 @@ -- this file is used by lite-xl to setup the Lua environment when starting VERSION = "@PROJECT_VERSION@" -MOD_VERSION_MAJOR = 3 +MOD_VERSION_MAJOR = 4 MOD_VERSION_MINOR = 0 MOD_VERSION_PATCH = 0 MOD_VERSION_STRING = string.format("%d.%d.%d", MOD_VERSION_MAJOR, MOD_VERSION_MINOR, MOD_VERSION_PATCH) diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index bb38a9c09..668a0851c 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local config = require "core.config" diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index 8286d62fd..461b2dab0 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local config = require "core.config" local style = require "core.style" diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index abe8704d7..8aab2d0ec 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local config = require "core.config" local style = require "core.style" diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index 26afbb574..8b1fbd829 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 9f22c66bd..74d1fd119 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local common = require "core.common" diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua index 36e29aa84..5ee10e29d 100644 --- a/data/plugins/drawwhitespace.lua +++ b/data/plugins/drawwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local style = require "core.style" diff --git a/data/plugins/highlighter.lua b/data/plugins/highlighter.lua index 8dc621d7f..36b0220a6 100644 --- a/data/plugins/highlighter.lua +++ b/data/plugins/highlighter.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local config = require "core.config" local style = require "core.style" diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index c15466be4..f69f93188 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index 70489713c..35479e1f9 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua index 9e78cde58..07ceb26cf 100644 --- a/data/plugins/language_css.lua +++ b/data/plugins/language_css.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua index 6da1b45f8..b553a2280 100644 --- a/data/plugins/language_html.lua +++ b/data/plugins/language_html.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 307aeecfb..cb11978ee 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" -- Regex pattern explanation: diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 55cc8adc9..84a3c8cea 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index 544bff151..2d9a47f11 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" local style = require "core.style" local core = require "core" diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 743e990a8..fd1bb0c92 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua index 125c260eb..6042893de 100644 --- a/data/plugins/language_xml.lua +++ b/data/plugins/language_xml.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index 5c4a92044..61989816e 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local common = require "core.common" local command = require "core.command" local config = require "core.config" diff --git a/data/plugins/linewrapping.lua b/data/plugins/linewrapping.lua index f00b4913c..febd0ef18 100644 --- a/data/plugins/linewrapping.lua +++ b/data/plugins/linewrapping.lua @@ -1,4 +1,4 @@ --- mod-version:3 --priority:10 +-- mod-version:4 --priority:10 local core = require "core" local common = require "core.common" local DocView = require "core.docview" diff --git a/data/plugins/macro.lua b/data/plugins/macro.lua index 9f3b84820..c95facfa6 100644 --- a/data/plugins/macro.lua +++ b/data/plugins/macro.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 552e11103..7fba25242 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local keymap = require "core.keymap" diff --git a/data/plugins/quote.lua b/data/plugins/quote.lua index 60f0cf1eb..1e05cc713 100644 --- a/data/plugins/quote.lua +++ b/data/plugins/quote.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/reflow.lua b/data/plugins/reflow.lua index e70b06f6e..dbb234744 100644 --- a/data/plugins/reflow.lua +++ b/data/plugins/reflow.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 89a016b71..00f01395e 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/tabularize.lua b/data/plugins/tabularize.lua index 5185fbf60..2d09e7bcd 100644 --- a/data/plugins/tabularize.lua +++ b/data/plugins/tabularize.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index ddc0f39d9..c75f505f9 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 9037d51a9..bfc5cc4c7 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index 8daa77e01..168e75527 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local common = require "core.common" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 3b3bd0445..b7a4d1a07 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -1,4 +1,4 @@ --- mod-version:3 +-- mod-version:4 local core = require "core" local common = require "core.common" local DocView = require "core.docview" From 5a6880a62baf4bbe51b66e935efce374df202bda Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 21 Jan 2024 14:59:50 -0500 Subject: [PATCH 22/24] Fixed issue with cache not resizing appropriately. --- data/core/docview.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/data/core/docview.lua b/data/core/docview.lua index 62ab4f444..c11162f7d 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -969,6 +969,7 @@ function DocView:draw() local gw, gpad = self:get_gutter_width() local lh = self:get_line_height() local x, y = self:get_line_screen_position(minline, 1) + for i = minline, maxline do y = y + self:draw_line(i, self.position.x, y) or lh end @@ -1075,6 +1076,9 @@ function DocView:retrieve_tokens(vline, line) local differential = #total_vlines - total_free_vlines if differential ~= 0 then table.move(self.vcache, start_vline + total_free_vlines, #self.vcache, start_vline + #total_vlines) + if differential < 0 then + for i = 1, -differential do table.remove(self.vcache) end + end for i = end_line + 1, #self.dtovcache do self.dtovcache[i] = self.dtovcache[i] + differential end From 08fd5f775aaaaebb1e1e9a6d5bc946316a56cc23 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 21 Jan 2024 15:53:46 -0500 Subject: [PATCH 23/24] Changed over resolve_screen_position to new system. --- data/core/docview.lua | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index c11162f7d..1058068c8 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -142,14 +142,12 @@ function DocView:resolve_screen_position(x, y) local default_font = self:get_font() local _, indent_size = self.doc:get_indent_info() default_font:set_tab_size(indent_size) - local line = self:get_dline(vline) - local token_idx = self:retrieve_tokens(vline) - if not token_idx then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end - for idx, text, style, type in self:each_line_token(line) do - if idx < token_idx then - if type == "doc" then - i = i + text:ulen() - end + local line, col = self:retrieve_tokens(vline) + if not line then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end + for _, text, style, type in self:each_line_token(line) do + local len = type == "doc" and text:ulen() + if i + len < col then + i = i + len else local font = style.font or default_font if font ~= default_font then font:set_tab_size(indent_size) end @@ -158,7 +156,7 @@ function DocView:resolve_screen_position(x, y) -- because we need last_i which should be calculated using utf-8. if xoffset + width < x then xoffset = xoffset + width - i = i + text:ulen() + i = i + len else for char in common.utf8_chars(text) do local w = font:get_width(char) From 85103ae2c84f6dcae87a200e39d4977855000a44 Mon Sep 17 00:00:00 2001 From: Adam Harrison Date: Sun, 21 Jan 2024 16:32:54 -0500 Subject: [PATCH 24/24] Mixed up order of parameters. --- data/core/docview.lua | 41 +++++++++++++++++++----------------- data/plugins/codefolding.lua | 1 - 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/data/core/docview.lua b/data/core/docview.lua index 1058068c8..6693a6977 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -145,27 +145,29 @@ function DocView:resolve_screen_position(x, y) local line, col = self:retrieve_tokens(vline) if not line then return #self.doc.lines, self.doc.lines[#self.doc.lines]:ulen() end for _, text, style, type in self:each_line_token(line) do - local len = type == "doc" and text:ulen() - if i + len < col then - i = i + len - else - local font = style.font or default_font - if font ~= default_font then font:set_tab_size(indent_size) end - local width = font:get_width(text) - -- Don't take the shortcut if the width matches x, - -- because we need last_i which should be calculated using utf-8. - if xoffset + width < x then - xoffset = xoffset + width + local len = type == "doc" and (text:ulen() or #text) + if len then + if i + len < col then i = i + len else - for char in common.utf8_chars(text) do - local w = font:get_width(char) - if xoffset >= x then - return line, (xoffset - x > w / 2) and last_i or i + local font = style.font or default_font + if font ~= default_font then font:set_tab_size(indent_size) end + local width = font:get_width(text) + -- Don't take the shortcut if the width matches x, + -- because we need last_i which should be calculated using utf-8. + if xoffset + width < x then + xoffset = xoffset + width + i = i + len + else + for char in common.utf8_chars(text) do + local w = font:get_width(char) + if xoffset >= x then + return line, (xoffset - x > w / 2) and last_i or i + end + xoffset = xoffset + w + last_i = i + i = i + 1 end - xoffset = xoffset + w - last_i = i - i = i + 1 end end end @@ -1022,6 +1024,7 @@ function DocView:retrieve_tokens(vline, line) if line and self.dcache[line] then return line, 1 end -- If we're here, it means we need to tokenize a block of lines in the middle of the document. Start from the beginning up until the relevant line. if (vline and vline < #self.vcache) or (line and line < #self.dcache) then + local start_line, end_line if vline then local invalid_line = vline @@ -1064,7 +1067,7 @@ function DocView:retrieve_tokens(vline, line) total_free_vlines = total_free_vlines + 1 end for i = start_line, end_line do - local tokens, vlines = tokenize_line(self, i, vline) + local tokens, vlines = tokenize_line(self, vline, i) self.dcache[i] = tokens self.dtovcache[i] = vline table.move(vlines, 1, #vlines, #total_vlines + 1, total_vlines) diff --git a/data/plugins/codefolding.lua b/data/plugins/codefolding.lua index 8aab2d0ec..604809f5d 100644 --- a/data/plugins/codefolding.lua +++ b/data/plugins/codefolding.lua @@ -110,7 +110,6 @@ function DocView:draw_line_gutter(line, x, y, width) renderer.draw_rect(startx + 1, starty + 1, size - 2, size - 2, self.hovering_foldable == line and style.dim or style.background) common.draw_text(self:get_font(), style.accent, self:is_folded(line) and "+" or "-", "center", startx, starty, size, size) end - -- common.draw_text(self:get_font(), style.accent, self.foldable[line] or "nil", "center", startx, starty, size, size) return lh end