2019-12-28 12:16:32 +01:00
|
|
|
local Object = require "core.object"
|
2024-10-22 18:31:45 +02:00
|
|
|
local Highlighter = require ".highlighter"
|
|
|
|
local translate = require ".translate"
|
2021-06-20 06:13:07 +02:00
|
|
|
local core = require "core"
|
2020-05-08 21:29:22 +02:00
|
|
|
local syntax = require "core.syntax"
|
2019-12-28 12:16:32 +01:00
|
|
|
local config = require "core.config"
|
|
|
|
local common = require "core.common"
|
|
|
|
|
2022-06-08 02:09:48 +02:00
|
|
|
---@class core.doc : core.object
|
2019-12-28 12:16:32 +01:00
|
|
|
local Doc = Object:extend()
|
|
|
|
|
|
|
|
|
|
|
|
local function split_lines(text)
|
|
|
|
local res = {}
|
|
|
|
for line in (text .. "\n"):gmatch("(.-)\n") do
|
|
|
|
table.insert(res, line)
|
|
|
|
end
|
|
|
|
return res
|
|
|
|
end
|
|
|
|
|
2021-07-20 20:39:50 +02:00
|
|
|
|
|
|
|
function Doc:new(filename, abs_filename, new_file)
|
|
|
|
self.new_file = new_file
|
2019-12-28 12:16:32 +01:00
|
|
|
self:reset()
|
|
|
|
if filename then
|
2021-07-20 20:39:50 +02:00
|
|
|
self:set_filename(filename, abs_filename)
|
|
|
|
if not new_file then
|
|
|
|
self:load(filename)
|
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
2023-11-30 16:51:10 +01:00
|
|
|
if new_file then
|
|
|
|
self.crlf = config.line_endings == "crlf"
|
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:reset()
|
|
|
|
self.lines = { "\n" }
|
2021-06-05 21:22:56 +02:00
|
|
|
self.selections = { 1, 1, 1, 1 }
|
2022-11-01 23:16:39 +01:00
|
|
|
self.last_selection = 1
|
2019-12-28 12:16:32 +01:00
|
|
|
self.undo_stack = { idx = 1 }
|
|
|
|
self.redo_stack = { idx = 1 }
|
|
|
|
self.clean_change_id = 1
|
2020-05-07 22:14:46 +02:00
|
|
|
self.highlighter = Highlighter(self)
|
2023-11-29 20:12:43 +01:00
|
|
|
self.overwrite = false
|
2020-05-08 21:29:22 +02:00
|
|
|
self:reset_syntax()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:reset_syntax()
|
2020-06-08 10:44:51 +02:00
|
|
|
local header = self:get_text(1, 1, self:position_offset(1, 1, 128))
|
2023-06-09 15:31:59 +02:00
|
|
|
local path = self.abs_filename
|
|
|
|
if not path and self.filename then
|
|
|
|
path = core.project_dir .. PATHSEP .. self.filename
|
|
|
|
end
|
|
|
|
if path then path = common.normalize_path(path) end
|
2023-07-03 22:39:54 +02:00
|
|
|
local syn = syntax.get(path, header)
|
2020-05-08 21:29:22 +02:00
|
|
|
if self.syntax ~= syn then
|
|
|
|
self.syntax = syn
|
2021-11-20 01:15:13 +01:00
|
|
|
self.highlighter:soft_reset()
|
2020-05-08 21:29:22 +02:00
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2021-07-20 20:39:50 +02:00
|
|
|
function Doc:set_filename(filename, abs_filename)
|
2021-02-18 00:26:19 +01:00
|
|
|
self.filename = filename
|
2021-07-20 20:39:50 +02:00
|
|
|
self.abs_filename = abs_filename
|
2021-10-10 03:28:06 +02:00
|
|
|
self:reset_syntax()
|
2021-02-18 00:26:19 +01:00
|
|
|
end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
function Doc:load(filename)
|
2023-11-29 20:12:43 +01:00
|
|
|
local fp = assert(io.open(filename, "rb"))
|
2019-12-28 12:16:32 +01:00
|
|
|
self:reset()
|
|
|
|
self.lines = {}
|
2021-11-20 01:18:37 +01:00
|
|
|
local i = 1
|
2019-12-28 12:16:32 +01:00
|
|
|
for line in fp:lines() do
|
|
|
|
if line:byte(-1) == 13 then
|
|
|
|
line = line:sub(1, -2)
|
|
|
|
self.crlf = true
|
|
|
|
end
|
|
|
|
table.insert(self.lines, line .. "\n")
|
2021-11-20 01:18:37 +01:00
|
|
|
self.highlighter.lines[i] = false
|
|
|
|
i = i + 1
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
if #self.lines == 0 then
|
|
|
|
table.insert(self.lines, "\n")
|
|
|
|
end
|
|
|
|
fp:close()
|
2020-05-08 21:29:22 +02:00
|
|
|
self:reset_syntax()
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2022-04-29 03:30:55 +02:00
|
|
|
function Doc:reload()
|
|
|
|
if self.filename then
|
|
|
|
local sel = { self:get_selection() }
|
|
|
|
self:load(self.filename)
|
|
|
|
self:clean()
|
|
|
|
self:set_selection(table.unpack(sel))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-07-20 20:39:50 +02:00
|
|
|
function Doc:save(filename, abs_filename)
|
|
|
|
if not filename then
|
|
|
|
assert(self.filename, "no filename set to default to")
|
|
|
|
filename = self.filename
|
|
|
|
abs_filename = self.abs_filename
|
2021-12-30 12:11:01 +01:00
|
|
|
else
|
|
|
|
assert(self.filename or abs_filename, "calling save on unnamed doc without absolute path")
|
2021-07-20 20:39:50 +02:00
|
|
|
end
|
2024-10-22 18:14:30 +02:00
|
|
|
|
|
|
|
local fp
|
|
|
|
if PLATFORM == "Windows" then
|
|
|
|
-- On Windows, opening a hidden file with wb fails with a permission error.
|
|
|
|
-- To get around this, we must open the file as r+b and truncate.
|
|
|
|
-- Since r+b fails if file doesn't exist, fall back to wb.
|
|
|
|
fp = io.open(filename, "r+b")
|
|
|
|
if fp then
|
|
|
|
system.ftruncate(fp)
|
|
|
|
else
|
|
|
|
-- file probably doesn't exist, create one
|
|
|
|
fp = assert ( io.open(filename, "wb") )
|
|
|
|
end
|
|
|
|
else
|
|
|
|
fp = assert ( io.open(filename, "wb") )
|
|
|
|
end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
for _, line in ipairs(self.lines) do
|
|
|
|
if self.crlf then line = line:gsub("\n", "\r\n") end
|
|
|
|
fp:write(line)
|
|
|
|
end
|
|
|
|
fp:close()
|
2021-07-20 20:39:50 +02:00
|
|
|
self:set_filename(filename, abs_filename)
|
|
|
|
self.new_file = false
|
2019-12-28 12:16:32 +01:00
|
|
|
self:clean()
|
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:get_name()
|
|
|
|
return self.filename or "unsaved"
|
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:is_dirty()
|
2021-08-31 23:16:02 +02:00
|
|
|
if self.new_file then
|
2023-10-18 06:45:31 +02:00
|
|
|
if self.filename then return true end
|
2021-08-31 23:16:02 +02:00
|
|
|
return #self.lines > 1 or #self.lines[1] > 1
|
|
|
|
else
|
|
|
|
return self.clean_change_id ~= self:get_change_id()
|
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:clean()
|
|
|
|
self.clean_change_id = self:get_change_id()
|
|
|
|
end
|
|
|
|
|
2021-11-20 03:15:08 +01:00
|
|
|
function Doc:get_indent_info()
|
|
|
|
if not self.indent_info then return config.tab_type, config.indent_size, false end
|
|
|
|
return self.indent_info.type or config.tab_type,
|
2023-11-29 20:12:43 +01:00
|
|
|
self.indent_info.size or config.indent_size,
|
|
|
|
self.indent_info.confirmed
|
2021-11-20 03:15:08 +01:00
|
|
|
end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
function Doc:get_change_id()
|
|
|
|
return self.undo_stack.idx
|
|
|
|
end
|
|
|
|
|
2022-11-01 23:16:39 +01:00
|
|
|
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
|
|
|
|
|
2021-06-10 18:29:55 +02:00
|
|
|
-- 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)
|
2022-11-01 23:16:39 +01:00
|
|
|
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
|
2022-07-11 05:39:55 +02:00
|
|
|
return line1, col1, line2, col2, swap
|
2021-06-10 18:29:55 +02:00
|
|
|
end
|
|
|
|
|
2022-11-01 23:16:39 +01:00
|
|
|
---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)
|
2023-11-29 20:12:43 +01:00
|
|
|
local line1, col1, line2, col2 = self.selections[idx * 4 - 3], self.selections[idx * 4 - 2],
|
|
|
|
self.selections[idx * 4 - 1],
|
|
|
|
self.selections[idx * 4]
|
2022-11-02 21:11:41 +01:00
|
|
|
if line1 and sort then
|
2022-11-01 23:16:39 +01:00
|
|
|
return sort_positions(line1, col1, line2, col2)
|
|
|
|
else
|
|
|
|
return line1, col1, line2, col2
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-08-02 05:37:00 +02:00
|
|
|
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
|
|
|
|
|
2021-06-10 18:29:55 +02:00
|
|
|
function Doc:has_selection()
|
2021-08-17 14:16:56 +02:00
|
|
|
local line1, col1, line2, col2 = self:get_selection(false)
|
|
|
|
return line1 ~= line2 or col1 ~= col2
|
2021-06-10 18:29:55 +02:00
|
|
|
end
|
|
|
|
|
2021-11-24 03:03:38 +01:00
|
|
|
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
|
|
|
|
|
2021-06-10 18:29:55 +02:00
|
|
|
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)
|
2021-06-05 21:22:56 +02:00
|
|
|
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)
|
2019-12-28 12:16:32 +01:00
|
|
|
line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
|
2023-11-29 20:12:43 +01:00
|
|
|
common.splice(self.selections, (idx - 1) * 4 + 1, rm == nil and 4 or rm, { line1, col1, line2, col2 })
|
2021-06-10 18:29:55 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:add_selection(line1, col1, line2, col2, swap)
|
2021-06-10 18:32:33 +02:00
|
|
|
local l1, c1 = sort_positions(line1, col1, line2 or line1, col2 or col1)
|
2021-06-10 18:29:55 +02:00
|
|
|
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)
|
2022-11-01 23:16:39 +01:00
|
|
|
self.last_selection = target
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2022-03-17 21:55:52 +01:00
|
|
|
function Doc:remove_selection(idx)
|
2022-11-01 23:16:39 +01:00
|
|
|
if self.last_selection >= idx then
|
|
|
|
self.last_selection = self.last_selection - 1
|
|
|
|
end
|
2022-03-17 21:55:52 +01:00
|
|
|
common.splice(self.selections, (idx - 1) * 4 + 1, 4)
|
|
|
|
end
|
|
|
|
|
2021-06-07 02:30:09 +02:00
|
|
|
function Doc:set_selection(line1, col1, line2, col2, swap)
|
2021-12-08 13:34:10 +01:00
|
|
|
self.selections = {}
|
2021-06-07 02:30:09 +02:00
|
|
|
self:set_selections(1, line1, col1, line2, col2, swap)
|
2022-11-01 23:16:39 +01:00
|
|
|
self.last_selection = 1
|
2021-06-07 02:30:09 +02:00
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
|
2021-06-09 23:59:19 +02:00
|
|
|
function Doc:merge_cursors(idx)
|
2024-06-24 16:21:57 +02:00
|
|
|
local table_index = idx and (idx - 1) * 4 + 1
|
2024-06-24 05:17:49 +02:00
|
|
|
for i = (table_index or (#self.selections - 3)), (table_index or 5), -4 do
|
2021-06-09 23:59:19 +02:00
|
|
|
for j = 1, i - 4, 4 do
|
|
|
|
if self.selections[i] == self.selections[j] and
|
2023-11-29 20:12:43 +01:00
|
|
|
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
|
2021-06-09 23:59:19 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-10 18:29:55 +02:00
|
|
|
local function selection_iterator(invariant, idx)
|
2023-11-29 20:12:43 +01:00
|
|
|
local target = invariant[3] and (idx * 4 - 7) or (idx * 4 + 1)
|
2021-06-12 00:51:14 +02:00
|
|
|
if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end
|
2021-06-10 18:29:55 +02:00
|
|
|
if invariant[2] then
|
2023-11-29 20:12:43 +01:00
|
|
|
return idx + (invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target + 4))
|
2021-06-10 18:29:55 +02:00
|
|
|
else
|
2023-11-29 20:12:43 +01:00
|
|
|
return idx + (invariant[3] and -1 or 1), table.unpack(invariant[1], target, target + 4)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-20 06:13:07 +02:00
|
|
|
-- If idx_reverse is true, it'll reverse iterate. If nil, or false, regular iterate.
|
2021-06-12 00:51:14 +02:00
|
|
|
-- If a number, runs for exactly that iteration.
|
|
|
|
function Doc:get_selections(sort_intra, idx_reverse)
|
2021-06-20 06:13:07 +02:00
|
|
|
return selection_iterator, { self.selections, sort_intra, idx_reverse },
|
2023-11-29 20:12:43 +01:00
|
|
|
idx_reverse == true and ((#self.selections / 4) + 1) or ((idx_reverse or -1) + 1)
|
2021-06-05 21:22:56 +02:00
|
|
|
end
|
2023-11-29 20:12:43 +01:00
|
|
|
|
2021-06-10 18:29:55 +02:00
|
|
|
-- End of cursor seciton.
|
2019-12-28 12:16:32 +01:00
|
|
|
|
|
|
|
function Doc:sanitize_position(line, col)
|
2023-06-09 15:34:34 +02:00
|
|
|
local nlines = #self.lines
|
|
|
|
if line > nlines then
|
|
|
|
return nlines, #self.lines[nlines]
|
|
|
|
elseif line < 1 then
|
|
|
|
return 1, 1
|
|
|
|
end
|
|
|
|
return line, common.clamp(col, 1, #self.lines[line])
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
local function position_offset_func(self, 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)
|
|
|
|
line, col = self:sanitize_position(line, col)
|
|
|
|
col = col + offset
|
|
|
|
while line > 1 and col < 1 do
|
|
|
|
line = line - 1
|
|
|
|
col = col + #self.lines[line]
|
|
|
|
end
|
|
|
|
while line < #self.lines and col > #self.lines[line] do
|
|
|
|
col = col - #self.lines[line]
|
|
|
|
line = line + 1
|
|
|
|
end
|
|
|
|
return self:sanitize_position(line, col)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
local function position_offset_linecol(self, 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, ...)
|
|
|
|
elseif select("#", ...) == 1 then
|
|
|
|
return position_offset_byte(self, line, col, ...)
|
|
|
|
elseif select("#", ...) == 2 then
|
|
|
|
return position_offset_linecol(self, line, col, ...)
|
|
|
|
else
|
|
|
|
error("bad number of arguments")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-10-22 18:59:55 +02:00
|
|
|
---Returns the content of the doc between two positions. </br>
|
|
|
|
---The positions will be sanitized and sorted. </br>
|
|
|
|
---The character at the "end" position is not included by default.
|
|
|
|
---@see core.doc.sanitize_position
|
|
|
|
---@param line1 integer
|
|
|
|
---@param col1 integer
|
|
|
|
---@param line2 integer
|
|
|
|
---@param col2 integer
|
|
|
|
---@param inclusive boolean? Whether or not to return the character at the last position
|
|
|
|
---@return string
|
|
|
|
function Doc:get_text(line1, col1, line2, col2, inclusive)
|
2019-12-28 12:16:32 +01:00
|
|
|
line1, col1 = self:sanitize_position(line1, col1)
|
|
|
|
line2, col2 = self:sanitize_position(line2, col2)
|
|
|
|
line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
|
2024-10-22 18:59:55 +02:00
|
|
|
local col2_offset = inclusive and 0 or 1
|
2019-12-28 12:16:32 +01:00
|
|
|
if line1 == line2 then
|
2024-10-22 18:59:55 +02:00
|
|
|
return self.lines[line1]:sub(col1, col2 - col2_offset)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
local lines = { self.lines[line1]:sub(col1) }
|
|
|
|
for i = line1 + 1, line2 - 1 do
|
|
|
|
table.insert(lines, self.lines[i])
|
|
|
|
end
|
2024-10-22 18:59:55 +02:00
|
|
|
table.insert(lines, self.lines[line2]:sub(1, col2 - col2_offset))
|
2019-12-28 12:16:32 +01:00
|
|
|
return table.concat(lines)
|
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:get_char(line, col)
|
|
|
|
line, col = self:sanitize_position(line, col)
|
|
|
|
return self.lines[line]:sub(col, col)
|
|
|
|
end
|
|
|
|
|
2020-05-14 17:10:48 +02:00
|
|
|
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
|
|
|
|
undo_stack.idx = undo_stack.idx + 1
|
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
|
|
|
|
|
2021-04-05 00:11:56 +02:00
|
|
|
local function pop_undo(self, undo_stack, redo_stack, modified)
|
2020-05-14 17:10:48 +02:00
|
|
|
-- pop command
|
|
|
|
local cmd = undo_stack[undo_stack.idx - 1]
|
|
|
|
if not cmd then return end
|
|
|
|
undo_stack.idx = undo_stack.idx - 1
|
|
|
|
|
|
|
|
-- handle command
|
|
|
|
if cmd.type == "insert" then
|
|
|
|
local line, col, text = table.unpack(cmd)
|
2021-04-06 08:18:35 +02:00
|
|
|
self:raw_insert(line, col, text, redo_stack, cmd.time)
|
2020-05-14 17:10:48 +02:00
|
|
|
elseif cmd.type == "remove" then
|
|
|
|
local line1, col1, line2, col2 = table.unpack(cmd)
|
2021-04-06 08:18:35 +02:00
|
|
|
self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time)
|
2020-05-14 17:10:48 +02:00
|
|
|
elseif cmd.type == "selection" then
|
2021-08-29 03:14:12 +02:00
|
|
|
self.selections = { table.unpack(cmd) }
|
2021-10-02 20:01:23 +02:00
|
|
|
self:sanitize_selection()
|
2020-05-14 17:10:48 +02:00
|
|
|
end
|
|
|
|
|
2021-04-06 08:26:53 +02:00
|
|
|
modified = modified or (cmd.type ~= "selection")
|
|
|
|
|
2020-05-14 17:10:48 +02:00
|
|
|
-- if next undo command is within the merge timeout then treat as a single
|
|
|
|
-- command and continue to execute it
|
|
|
|
local next = undo_stack[undo_stack.idx - 1]
|
|
|
|
if next and math.abs(cmd.time - next.time) < config.undo_merge_timeout then
|
2021-04-06 08:26:53 +02:00
|
|
|
return pop_undo(self, undo_stack, redo_stack, modified)
|
2021-04-05 00:11:56 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
if modified then
|
|
|
|
self:on_text_change("undo")
|
2020-05-14 17:10:48 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2021-04-06 08:18:35 +02:00
|
|
|
function Doc:raw_insert(line, col, text, undo_stack, time)
|
2019-12-28 12:16:32 +01:00
|
|
|
-- split text into lines and merge with line at insertion point
|
|
|
|
local lines = split_lines(text)
|
2021-08-29 23:54:57 +02:00
|
|
|
local len = #lines[#lines]
|
2019-12-28 12:16:32 +01:00
|
|
|
local before = self.lines[line]:sub(1, col - 1)
|
|
|
|
local after = self.lines[line]:sub(col)
|
|
|
|
for i = 1, #lines - 1 do
|
|
|
|
lines[i] = lines[i] .. "\n"
|
|
|
|
end
|
|
|
|
lines[1] = before .. lines[1]
|
|
|
|
lines[#lines] = lines[#lines] .. after
|
|
|
|
|
|
|
|
-- splice lines into line array
|
2021-06-07 02:30:09 +02:00
|
|
|
common.splice(self.lines, line, 1, lines)
|
2021-12-06 13:48:30 +01:00
|
|
|
|
2021-08-30 02:05:58 +02:00
|
|
|
-- 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
|
2023-11-29 20:12:43 +01:00
|
|
|
self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition,
|
|
|
|
ccol2 + column_addition)
|
2021-08-30 02:05:58 +02:00
|
|
|
end
|
2019-12-28 12:16:32 +01:00
|
|
|
|
|
|
|
-- push undo
|
|
|
|
local line2, col2 = self:position_offset(line, col, #text)
|
2021-08-29 03:14:12 +02:00
|
|
|
push_undo(undo_stack, time, "selection", table.unpack(self.selections))
|
2020-05-14 17:10:48 +02:00
|
|
|
push_undo(undo_stack, time, "remove", line, col, line2, col2)
|
2020-05-07 22:14:46 +02:00
|
|
|
|
2020-05-14 17:10:48 +02:00
|
|
|
-- update highlighter and assure selection is in bounds
|
2021-10-07 19:19:03 +02:00
|
|
|
self.highlighter:insert_notify(line, #lines - 1)
|
2020-05-14 17:10:48 +02:00
|
|
|
self:sanitize_selection()
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2021-04-06 08:18:35 +02:00
|
|
|
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
|
2019-12-28 12:16:32 +01:00
|
|
|
-- push undo
|
|
|
|
local text = self:get_text(line1, col1, line2, col2)
|
2021-08-29 03:14:12 +02:00
|
|
|
push_undo(undo_stack, time, "selection", table.unpack(self.selections))
|
2020-05-14 17:10:48 +02:00
|
|
|
push_undo(undo_stack, time, "insert", line1, col1, text)
|
2019-12-28 12:16:32 +01:00
|
|
|
|
|
|
|
-- get line content before/after removed text
|
|
|
|
local before = self.lines[line1]:sub(1, col1 - 1)
|
|
|
|
local after = self.lines[line2]:sub(col2)
|
|
|
|
|
2023-04-07 18:54:00 +02:00
|
|
|
local line_removal = line2 - line1
|
|
|
|
local col_removal = col2 - col1
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
-- splice line into line array
|
2023-04-07 18:54:00 +02:00
|
|
|
common.splice(self.lines, line1, line_removal + 1, { before .. after })
|
|
|
|
|
|
|
|
local merge = false
|
2021-12-06 13:48:30 +01:00
|
|
|
|
2023-04-07 18:54:00 +02:00
|
|
|
-- 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
|
2021-08-30 02:05:58 +02:00
|
|
|
for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do
|
2023-04-07 18:54:00 +02:00
|
|
|
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
|
|
|
|
|
2023-04-10 21:57:11 +02:00
|
|
|
if cline2 > line1 or (cline2 == line1 and ccol2 > col1) then
|
2023-04-07 18:54:00 +02:00
|
|
|
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()
|
2021-08-29 23:54:57 +02:00
|
|
|
end
|
2020-05-07 22:14:46 +02:00
|
|
|
|
2020-05-14 17:10:48 +02:00
|
|
|
-- update highlighter and assure selection is in bounds
|
2023-04-07 18:54:00 +02:00
|
|
|
self.highlighter:remove_notify(line1, line_removal)
|
2019-12-28 12:16:32 +01:00
|
|
|
self:sanitize_selection()
|
|
|
|
end
|
|
|
|
|
2020-05-14 17:10:48 +02:00
|
|
|
function Doc:insert(line, col, text)
|
2019-12-28 12:16:32 +01:00
|
|
|
self.redo_stack = { idx = 1 }
|
2023-11-29 16:45:53 +01:00
|
|
|
-- Reset the clean id when we're pushing something new before it
|
|
|
|
if self:get_change_id() < self.clean_change_id then
|
|
|
|
self.clean_change_id = -1
|
|
|
|
end
|
2020-05-14 17:10:48 +02:00
|
|
|
line, col = self:sanitize_position(line, col)
|
2021-04-06 08:18:35 +02:00
|
|
|
self:raw_insert(line, col, text, self.undo_stack, system.get_time())
|
|
|
|
self:on_text_change("insert")
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2020-05-14 17:10:48 +02:00
|
|
|
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)
|
2021-04-06 08:18:35 +02:00
|
|
|
self:raw_remove(line1, col1, line2, col2, self.undo_stack, system.get_time())
|
|
|
|
self:on_text_change("remove")
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:undo()
|
2021-04-06 08:18:35 +02:00
|
|
|
pop_undo(self, self.undo_stack, self.redo_stack, false)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
function Doc:redo()
|
2021-04-06 08:18:35 +02:00
|
|
|
pop_undo(self, self.redo_stack, self.undo_stack, false)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2021-06-05 21:22:56 +02:00
|
|
|
function Doc:text_input(text, idx)
|
2021-08-30 02:05:58 +02:00
|
|
|
for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do
|
2023-11-29 20:12:43 +01:00
|
|
|
local had_selection = false
|
2021-06-12 00:51:14 +02:00
|
|
|
if line1 ~= line2 or col1 ~= col2 then
|
|
|
|
self:delete_to_cursor(sidx)
|
2023-11-29 20:12:43 +01:00
|
|
|
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))
|
2021-06-05 21:22:56 +02:00
|
|
|
end
|
2023-11-29 20:12:43 +01:00
|
|
|
|
2021-06-12 00:51:14 +02:00
|
|
|
self:insert(line1, col1, text)
|
|
|
|
self:move_to_cursor(sidx, #text)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-10-16 01:58:51 +02:00
|
|
|
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
|
|
|
|
|
2021-08-31 04:56:33 +02:00
|
|
|
function Doc:replace_cursor(idx, line1, col1, line2, col2, fn)
|
2019-12-28 12:16:32 +01:00
|
|
|
local old_text = self:get_text(line1, col1, line2, col2)
|
2022-04-27 21:53:35 +02:00
|
|
|
local new_text, res = fn(old_text)
|
2019-12-28 12:16:32 +01:00
|
|
|
if old_text ~= new_text then
|
|
|
|
self:insert(line2, col2, new_text)
|
|
|
|
self:remove(line1, col1, line2, col2)
|
2021-06-07 14:25:10 +02:00
|
|
|
if line1 == line2 and col1 == col2 then
|
2019-12-28 12:16:32 +01:00
|
|
|
line2, col2 = self:position_offset(line1, col1, #new_text)
|
2021-08-31 04:56:33 +02:00
|
|
|
self:set_selections(idx, line1, col1, line2, col2)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
end
|
2022-04-27 21:53:35 +02:00
|
|
|
return res
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2021-08-31 04:56:33 +02:00
|
|
|
function Doc:replace(fn)
|
2023-11-29 20:12:43 +01:00
|
|
|
local has_selection, results = false, {}
|
2021-08-31 04:56:33 +02:00
|
|
|
for idx, line1, col1, line2, col2 in self:get_selections(true) do
|
2021-12-06 13:48:30 +01:00
|
|
|
if line1 ~= line2 or col1 ~= col2 then
|
2022-04-27 21:53:35 +02:00
|
|
|
results[idx] = self:replace_cursor(idx, line1, col1, line2, col2, fn)
|
2021-08-31 04:56:33 +02:00
|
|
|
has_selection = true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if not has_selection then
|
|
|
|
self:set_selection(table.unpack(self.selections))
|
2022-04-27 21:53:35 +02:00
|
|
|
results[1] = self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn)
|
2021-08-31 04:56:33 +02:00
|
|
|
end
|
2022-04-27 21:53:35 +02:00
|
|
|
return results
|
2021-08-31 04:56:33 +02:00
|
|
|
end
|
|
|
|
|
2021-06-09 05:09:09 +02:00
|
|
|
function Doc:delete_to_cursor(idx, ...)
|
2021-06-12 00:51:14 +02:00
|
|
|
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)
|
2021-06-05 21:22:56 +02:00
|
|
|
end
|
2021-06-12 00:51:14 +02:00
|
|
|
self:set_selections(sidx, line1, col1)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
2021-06-09 23:59:19 +02:00
|
|
|
self:merge_cursors(idx)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
2023-11-29 20:12:43 +01:00
|
|
|
|
2021-06-15 02:11:40 +02:00
|
|
|
function Doc:delete_to(...) return self:delete_to_cursor(nil, ...) end
|
2019-12-28 12:16:32 +01:00
|
|
|
|
2021-06-09 05:09:09 +02:00
|
|
|
function Doc:move_to_cursor(idx, ...)
|
2021-06-12 00:51:14 +02:00
|
|
|
for sidx, line, col in self:get_selections(false, idx) do
|
|
|
|
self:set_selections(sidx, self:position_offset(line, col, ...))
|
2021-06-05 21:22:56 +02:00
|
|
|
end
|
2021-06-09 23:59:19 +02:00
|
|
|
self:merge_cursors(idx)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2023-11-29 20:12:43 +01:00
|
|
|
function Doc:move_to(...) return self:move_to_cursor(nil, ...) end
|
2019-12-28 12:16:32 +01:00
|
|
|
|
2021-06-09 05:09:09 +02:00
|
|
|
function Doc:select_to_cursor(idx, ...)
|
2021-06-12 00:51:14 +02:00
|
|
|
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)
|
2021-06-05 21:22:56 +02:00
|
|
|
end
|
2021-06-09 23:59:19 +02:00
|
|
|
self:merge_cursors(idx)
|
2019-12-28 12:16:32 +01:00
|
|
|
end
|
|
|
|
|
2023-11-29 20:12:43 +01:00
|
|
|
function Doc:select_to(...) return self:select_to_cursor(nil, ...) end
|
2021-06-05 06:44:35 +02:00
|
|
|
|
2021-11-20 03:20:49 +01:00
|
|
|
function Doc:get_indent_string()
|
|
|
|
local indent_type, indent_size = self:get_indent_info()
|
|
|
|
if indent_type == "hard" then
|
2021-06-05 06:50:25 +02:00
|
|
|
return "\t"
|
|
|
|
end
|
2021-11-20 03:20:49 +01:00
|
|
|
return string.rep(" ", indent_size)
|
2021-06-05 06:50:25 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
-- returns the size of the original indent, and the indent
|
|
|
|
-- in your config format, rounded either up or down
|
2021-11-20 03:20:49 +01:00
|
|
|
function Doc:get_line_indent(line, rnd_up)
|
2021-06-05 06:50:25 +02:00
|
|
|
local _, e = line:find("^[ \t]+")
|
2021-11-20 03:20:49 +01:00
|
|
|
local indent_type, indent_size = self:get_indent_info()
|
|
|
|
local soft_tab = string.rep(" ", indent_size)
|
|
|
|
if indent_type == "hard" then
|
2021-06-05 06:50:25 +02:00
|
|
|
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,
|
2023-11-29 20:12:43 +01:00
|
|
|
(rnd_up and math.ceil(number) or math.floor(number)) * #soft_tab)
|
2021-06-05 06:50:25 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-06-05 06:44:35 +02:00
|
|
|
-- 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).
|
2021-06-05 21:22:56 +02:00
|
|
|
function Doc:indent_text(unindent, line1, col1, line2, col2)
|
2021-11-20 03:20:49 +01:00
|
|
|
local text = self:get_indent_string()
|
2021-06-05 06:44:35 +02:00
|
|
|
local _, se = self.lines[line1]:find("^[ \t]+")
|
|
|
|
local in_beginning_whitespace = col1 == 1 or (se and col1 <= se + 1)
|
2021-06-05 06:50:25 +02:00
|
|
|
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]
|
2021-06-05 06:44:35 +02:00
|
|
|
for line = line1, line2 do
|
2022-06-25 03:32:47 +02:00
|
|
|
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
|
2021-06-05 06:44:35 +02:00
|
|
|
end
|
|
|
|
l1d, l2d = #self.lines[line1] - l1d, #self.lines[line2] - l2d
|
2021-06-06 03:18:05 +02:00
|
|
|
if (unindent or in_beginning_whitespace) and not has_selection then
|
2021-06-05 06:44:35 +02:00
|
|
|
local start_cursor = (se and se + 1 or 1) + l1d or #(self.lines[line1])
|
2021-06-05 21:22:56 +02:00
|
|
|
return line1, start_cursor, line2, start_cursor
|
2021-06-05 06:44:35 +02:00
|
|
|
end
|
2021-06-07 14:26:49 +02:00
|
|
|
return line1, col1 + l1d, line2, col2 + l2d
|
2021-06-05 06:44:35 +02:00
|
|
|
end
|
2021-06-07 14:26:49 +02:00
|
|
|
self:insert(line1, col1, text)
|
|
|
|
return line1, col1 + #text, line1, col1 + #text
|
2021-06-05 06:44:35 +02:00
|
|
|
end
|
|
|
|
|
2021-04-05 00:11:56 +02:00
|
|
|
-- For plugins to add custom actions of document change
|
|
|
|
function Doc:on_text_change(type)
|
|
|
|
end
|
|
|
|
|
2021-06-20 06:13:07 +02:00
|
|
|
-- For plugins to get notified when a document is closed
|
|
|
|
function Doc:on_close()
|
|
|
|
core.log_quiet("Closed doc \"%s\"", self:get_name())
|
|
|
|
end
|
|
|
|
|
2019-12-28 12:16:32 +01:00
|
|
|
return Doc
|