Initial commit
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
local Object = require "core.object"
|
||||
local config = require "core.config"
|
||||
local common = require "core.common"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
local function splice(t, at, remove, insert)
|
||||
insert = insert or {}
|
||||
local offset = #insert - remove
|
||||
local old_len = #t
|
||||
local new_len = old_len + offset
|
||||
if offset < 0 then
|
||||
for i = at - offset, old_len - offset do
|
||||
t[i + offset] = t[i]
|
||||
end
|
||||
elseif offset > 0 then
|
||||
for i = old_len, at, -1 do
|
||||
t[i + offset] = t[i]
|
||||
end
|
||||
end
|
||||
for i, item in ipairs(insert) do
|
||||
t[at + i - 1] = item
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Doc:new(filename)
|
||||
self:reset()
|
||||
if filename then
|
||||
self:load(filename)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Doc:reset()
|
||||
self.lines = { "\n" }
|
||||
self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } }
|
||||
self.undo_stack = { idx = 1 }
|
||||
self.redo_stack = { idx = 1 }
|
||||
self.clean_change_id = 1
|
||||
end
|
||||
|
||||
|
||||
function Doc:load(filename)
|
||||
local fp = assert( io.open(filename, "rb") )
|
||||
self:reset()
|
||||
self.filename = filename
|
||||
self.lines = {}
|
||||
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")
|
||||
end
|
||||
if #self.lines == 0 then
|
||||
table.insert(self.lines, "\n")
|
||||
end
|
||||
fp:close()
|
||||
end
|
||||
|
||||
|
||||
function Doc:save(filename)
|
||||
filename = filename or assert(self.filename, "no filename set to default to")
|
||||
local fp = assert( io.open(filename, "wb") )
|
||||
for _, line in ipairs(self.lines) do
|
||||
if self.crlf then line = line:gsub("\n", "\r\n") end
|
||||
fp:write(line)
|
||||
end
|
||||
fp:close()
|
||||
self.filename = filename or self.filename
|
||||
self:clean()
|
||||
end
|
||||
|
||||
|
||||
function Doc:get_name()
|
||||
return self.filename or "unsaved"
|
||||
end
|
||||
|
||||
|
||||
function Doc:is_dirty()
|
||||
return self.clean_change_id ~= self:get_change_id()
|
||||
end
|
||||
|
||||
|
||||
function Doc:clean()
|
||||
self.clean_change_id = self:get_change_id()
|
||||
end
|
||||
|
||||
|
||||
function Doc:get_change_id()
|
||||
return self.undo_stack.idx
|
||||
end
|
||||
|
||||
|
||||
function Doc:set_selection(line1, col1, line2, col2, swap)
|
||||
assert(not line2 == not col2, "expected 2 or 4 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)
|
||||
self.selection.a.line, self.selection.a.col = line1, col1
|
||||
self.selection.b.line, self.selection.b.col = line2, col2
|
||||
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
|
||||
|
||||
|
||||
function Doc:get_selection(sort)
|
||||
local a, b = self.selection.a, self.selection.b
|
||||
if sort then
|
||||
return sort_positions(a.line, a.col, b.line, b.col)
|
||||
end
|
||||
return a.line, a.col, b.line, b.col
|
||||
end
|
||||
|
||||
|
||||
function Doc:has_selection()
|
||||
local a, b = self.selection.a, self.selection.b
|
||||
return not (a.line == b.line and a.col == b.col)
|
||||
end
|
||||
|
||||
|
||||
function Doc:sanitize_selection()
|
||||
self:set_selection(self:get_selection())
|
||||
end
|
||||
|
||||
|
||||
function Doc:sanitize_position(line, col)
|
||||
line = common.clamp(line, 1, #self.lines)
|
||||
col = common.clamp(col, 1, #self.lines[line])
|
||||
return line, col
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
if line1 == line2 then
|
||||
return self.lines[line1]:sub(col1, col2 - 1)
|
||||
end
|
||||
local lines = { self.lines[line1]:sub(col1) }
|
||||
for i = line1 + 1, line2 - 1 do
|
||||
table.insert(lines, self.lines[i])
|
||||
end
|
||||
table.insert(lines, self.lines[line2]:sub(1, col2 - 1))
|
||||
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
|
||||
|
||||
|
||||
local push_undo
|
||||
|
||||
local function insert(self, undo_stack, time, line, col, text)
|
||||
line, col = self:sanitize_position(line, col)
|
||||
|
||||
-- split text into lines and merge with line at insertion point
|
||||
local lines = split_lines(text)
|
||||
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
|
||||
splice(self.lines, line, 1, lines)
|
||||
|
||||
-- push undo
|
||||
local line2, col2 = self:position_offset(line, col, #text)
|
||||
push_undo(self, undo_stack, time, "selection", self:get_selection())
|
||||
push_undo(self, undo_stack, time, "remove", line, col, line2, col2)
|
||||
end
|
||||
|
||||
|
||||
local function remove(self, undo_stack, time, 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)
|
||||
|
||||
-- push undo
|
||||
local text = self:get_text(line1, col1, line2, col2)
|
||||
push_undo(self, undo_stack, time, "selection", self:get_selection())
|
||||
push_undo(self, undo_stack, time, "insert", line1, col1, text)
|
||||
|
||||
-- get line content before/after removed text
|
||||
local before = self.lines[line1]:sub(1, col1 - 1)
|
||||
local after = self.lines[line2]:sub(col2)
|
||||
|
||||
-- splice line into line array
|
||||
splice(self.lines, line1, line2 - line1 + 1, { before .. after })
|
||||
end
|
||||
|
||||
|
||||
function Doc:insert(...)
|
||||
insert(self, self.undo_stack, system.get_time(), ...)
|
||||
self:sanitize_selection()
|
||||
self.redo_stack = { idx = 1 }
|
||||
end
|
||||
|
||||
|
||||
function Doc:remove(...)
|
||||
remove(self, self.undo_stack, system.get_time(), ...)
|
||||
self:sanitize_selection()
|
||||
self.redo_stack = { idx = 1 }
|
||||
end
|
||||
|
||||
|
||||
function push_undo(self, 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
|
||||
|
||||
|
||||
local function pop_undo(self, undo_stack, redo_stack)
|
||||
-- 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
|
||||
insert(self, redo_stack, cmd.time, table.unpack(cmd))
|
||||
|
||||
elseif cmd.type == "remove" then
|
||||
remove(self, redo_stack, cmd.time, table.unpack(cmd))
|
||||
|
||||
elseif cmd.type == "selection" then
|
||||
self.selection.a.line, self.selection.a.col = cmd[1], cmd[2]
|
||||
self.selection.b.line, self.selection.b.col = cmd[3], cmd[4]
|
||||
end
|
||||
|
||||
-- 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
|
||||
return pop_undo(self, undo_stack, redo_stack)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Doc:undo()
|
||||
pop_undo(self, self.undo_stack, self.redo_stack)
|
||||
end
|
||||
|
||||
|
||||
function Doc:redo()
|
||||
pop_undo(self, self.redo_stack, self.undo_stack)
|
||||
end
|
||||
|
||||
|
||||
function Doc:text_input(text)
|
||||
if self:has_selection() then
|
||||
self:delete_to()
|
||||
end
|
||||
local line, col = self:get_selection()
|
||||
self:insert(line, col, text)
|
||||
self:move_to(#text)
|
||||
end
|
||||
|
||||
|
||||
function Doc:replace(fn)
|
||||
local line1, col1, line2, col2, swap
|
||||
local had_selection = self:has_selection()
|
||||
if had_selection then
|
||||
line1, col1, line2, col2, swap = self:get_selection(true)
|
||||
else
|
||||
line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines]
|
||||
end
|
||||
local old_text = self:get_text(line1, col1, line2, col2)
|
||||
local new_text, n = fn(old_text)
|
||||
if old_text ~= new_text then
|
||||
self:insert(line2, col2, new_text)
|
||||
self:remove(line1, col1, line2, col2)
|
||||
if had_selection then
|
||||
line2, col2 = self:position_offset(line1, col1, #new_text)
|
||||
self:set_selection(line1, col1, line2, col2, swap)
|
||||
end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
|
||||
function Doc:delete_to(...)
|
||||
local line, col = self:get_selection(true)
|
||||
if self:has_selection() then
|
||||
self:remove(self:get_selection())
|
||||
else
|
||||
local line2, col2 = self:position_offset(line, col, ...)
|
||||
self:remove(line, col, line2, col2)
|
||||
line, col = sort_positions(line, col, line2, col2)
|
||||
end
|
||||
self:set_selection(line, col)
|
||||
end
|
||||
|
||||
|
||||
function Doc:move_to(...)
|
||||
local line, col = self:get_selection()
|
||||
self:set_selection(self:position_offset(line, col, ...))
|
||||
end
|
||||
|
||||
|
||||
function Doc:select_to(...)
|
||||
local line, col, line2, col2 = self:get_selection()
|
||||
line, col = self:position_offset(line, col, ...)
|
||||
self:set_selection(line, col, line2, col2)
|
||||
end
|
||||
|
||||
|
||||
return Doc
|
||||
@@ -0,0 +1,52 @@
|
||||
local search = {}
|
||||
|
||||
local default_opt = {}
|
||||
|
||||
|
||||
local function pattern_lower(str)
|
||||
if str:sub(1, 1) == "%" then
|
||||
return str
|
||||
end
|
||||
return str:lower()
|
||||
end
|
||||
|
||||
|
||||
local function init_args(doc, line, col, text, opt)
|
||||
opt = opt or default_opt
|
||||
line, col = doc:sanitize_position(line, col)
|
||||
|
||||
if opt.no_case then
|
||||
if opt.pattern then
|
||||
text = text:gsub("%%?.", pattern_lower)
|
||||
else
|
||||
text = text:lower()
|
||||
end
|
||||
end
|
||||
|
||||
return doc, line, col, text, opt
|
||||
end
|
||||
|
||||
|
||||
function search.find(doc, line, col, text, opt)
|
||||
doc, line, col, text, opt = init_args(doc, line, col, text, opt)
|
||||
|
||||
for line = line, #doc.lines do
|
||||
local line_text = doc.lines[line]
|
||||
if opt.no_case then
|
||||
line_text = line_text:lower()
|
||||
end
|
||||
local s, e = line_text:find(text, col, not opt.pattern)
|
||||
if s then
|
||||
return line, s, line, e + 1
|
||||
end
|
||||
col = 1
|
||||
end
|
||||
|
||||
if opt.wrap then
|
||||
opt = { no_case = opt.no_case, pattern = opt.pattern }
|
||||
return search.find(doc, 1, 1, text, opt)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return search
|
||||
@@ -0,0 +1,137 @@
|
||||
local common = require "core.common"
|
||||
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()
|
||||
|
||||
local translate = {}
|
||||
|
||||
|
||||
local function is_non_word(char)
|
||||
return config.non_word_chars:find(char, nil, true)
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
function translate.previous_word_boundary(doc, line, col)
|
||||
local char = doc:get_char(doc:position_offset(line, col, -1))
|
||||
local inword = not is_non_word(char)
|
||||
repeat
|
||||
local line2, col2 = line, col
|
||||
line, col = doc:position_offset(line, col, -1)
|
||||
if line == line2 and col == col2 then
|
||||
break
|
||||
end
|
||||
local c = doc:get_char(doc:position_offset(line, col, -1))
|
||||
until inword and is_non_word(c) or not inword and c ~= char
|
||||
return line, col
|
||||
end
|
||||
|
||||
|
||||
function translate.next_word_boundary(doc, line, col)
|
||||
local char = doc:get_char(line, col)
|
||||
local inword = not is_non_word(char)
|
||||
repeat
|
||||
local line2, col2 = line, col
|
||||
line, col = doc:position_offset(line, col, 1)
|
||||
if line == line2 and col == col2 then
|
||||
break
|
||||
end
|
||||
local c = doc:get_char(line, col)
|
||||
until inword and is_non_word(c) or not inword and c ~= char
|
||||
return line, col
|
||||
end
|
||||
|
||||
|
||||
function translate.start_of_word(doc, line, col)
|
||||
while true do
|
||||
local line2, col2 = doc:position_offset(line, col, -1)
|
||||
local char = doc:get_char(line2, col2)
|
||||
if is_non_word(char)
|
||||
or line == line2 and col == col2 then
|
||||
break
|
||||
end
|
||||
line, col = line2, col2
|
||||
end
|
||||
return line, col
|
||||
end
|
||||
|
||||
|
||||
function translate.end_of_word(doc, line, col)
|
||||
while true do
|
||||
local line2, col2 = doc:position_offset(line, col, 1)
|
||||
local char = doc:get_char(line, col)
|
||||
if is_non_word(char)
|
||||
or line == line2 and col == col2 then
|
||||
break
|
||||
end
|
||||
line, col = line2, col2
|
||||
end
|
||||
return line, col
|
||||
end
|
||||
|
||||
|
||||
function translate.previous_start_of_block(doc, line, col)
|
||||
while true do
|
||||
line = line - 1
|
||||
if line <= 1 then
|
||||
return 1, 1
|
||||
end
|
||||
if doc.lines[line-1]:match("^%s*$")
|
||||
and not doc.lines[line]:match("^%s*$") then
|
||||
return line, (doc.lines[line]:find("%S"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function translate.next_start_of_block(doc, line, col)
|
||||
while true do
|
||||
line = line + 1
|
||||
if line >= #doc.lines then
|
||||
return #doc.lines, 1
|
||||
end
|
||||
if doc.lines[line-1]:match("^%s*$")
|
||||
and not doc.lines[line]:match("^%s*$") then
|
||||
return line, (doc.lines[line]:find("%S"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function translate.start_of_line(doc, line, col)
|
||||
return line, 1
|
||||
end
|
||||
|
||||
|
||||
function translate.end_of_line(doc, line, col)
|
||||
return line, math.huge
|
||||
end
|
||||
|
||||
|
||||
function translate.start_of_doc(doc, line, col)
|
||||
return 1, 1
|
||||
end
|
||||
|
||||
|
||||
function translate.end_of_doc(doc, line, col)
|
||||
return #doc.lines, #doc.lines[#doc.lines]
|
||||
end
|
||||
|
||||
|
||||
return translate
|
||||
Reference in New Issue
Block a user