123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- 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
|