init.lua 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. local Object = require "core.object"
  2. local config = require "core.config"
  3. local common = require "core.common"
  4. local Doc = Object:extend()
  5. local function split_lines(text)
  6. local res = {}
  7. for line in (text .. "\n"):gmatch("(.-)\n") do
  8. table.insert(res, line)
  9. end
  10. return res
  11. end
  12. local function splice(t, at, remove, insert)
  13. insert = insert or {}
  14. local offset = #insert - remove
  15. local old_len = #t
  16. local new_len = old_len + offset
  17. if offset < 0 then
  18. for i = at - offset, old_len - offset do
  19. t[i + offset] = t[i]
  20. end
  21. elseif offset > 0 then
  22. for i = old_len, at, -1 do
  23. t[i + offset] = t[i]
  24. end
  25. end
  26. for i, item in ipairs(insert) do
  27. t[at + i - 1] = item
  28. end
  29. end
  30. function Doc:new(filename)
  31. self:reset()
  32. if filename then
  33. self:load(filename)
  34. end
  35. end
  36. function Doc:reset()
  37. self.lines = { "\n" }
  38. self.selection = { a = { line=1, col=1 }, b = { line=1, col=1 } }
  39. self.undo_stack = { idx = 1 }
  40. self.redo_stack = { idx = 1 }
  41. self.clean_change_id = 1
  42. end
  43. function Doc:load(filename)
  44. local fp = assert( io.open(filename, "rb") )
  45. self:reset()
  46. self.filename = filename
  47. self.lines = {}
  48. for line in fp:lines() do
  49. if line:byte(-1) == 13 then
  50. line = line:sub(1, -2)
  51. self.crlf = true
  52. end
  53. table.insert(self.lines, line .. "\n")
  54. end
  55. if #self.lines == 0 then
  56. table.insert(self.lines, "\n")
  57. end
  58. fp:close()
  59. end
  60. function Doc:save(filename)
  61. filename = filename or assert(self.filename, "no filename set to default to")
  62. local fp = assert( io.open(filename, "wb") )
  63. for _, line in ipairs(self.lines) do
  64. if self.crlf then line = line:gsub("\n", "\r\n") end
  65. fp:write(line)
  66. end
  67. fp:close()
  68. self.filename = filename or self.filename
  69. self:clean()
  70. end
  71. function Doc:get_name()
  72. return self.filename or "unsaved"
  73. end
  74. function Doc:is_dirty()
  75. return self.clean_change_id ~= self:get_change_id()
  76. end
  77. function Doc:clean()
  78. self.clean_change_id = self:get_change_id()
  79. end
  80. function Doc:get_change_id()
  81. return self.undo_stack.idx
  82. end
  83. function Doc:set_selection(line1, col1, line2, col2, swap)
  84. assert(not line2 == not col2, "expected 2 or 4 arguments")
  85. if swap then line1, col1, line2, col2 = line2, col2, line1, col1 end
  86. line1, col1 = self:sanitize_position(line1, col1)
  87. line2, col2 = self:sanitize_position(line2 or line1, col2 or col1)
  88. self.selection.a.line, self.selection.a.col = line1, col1
  89. self.selection.b.line, self.selection.b.col = line2, col2
  90. end
  91. local function sort_positions(line1, col1, line2, col2)
  92. if line1 > line2
  93. or line1 == line2 and col1 > col2 then
  94. return line2, col2, line1, col1, true
  95. end
  96. return line1, col1, line2, col2, false
  97. end
  98. function Doc:get_selection(sort)
  99. local a, b = self.selection.a, self.selection.b
  100. if sort then
  101. return sort_positions(a.line, a.col, b.line, b.col)
  102. end
  103. return a.line, a.col, b.line, b.col
  104. end
  105. function Doc:has_selection()
  106. local a, b = self.selection.a, self.selection.b
  107. return not (a.line == b.line and a.col == b.col)
  108. end
  109. function Doc:sanitize_selection()
  110. self:set_selection(self:get_selection())
  111. end
  112. function Doc:sanitize_position(line, col)
  113. line = common.clamp(line, 1, #self.lines)
  114. col = common.clamp(col, 1, #self.lines[line])
  115. return line, col
  116. end
  117. local function position_offset_func(self, line, col, fn, ...)
  118. line, col = self:sanitize_position(line, col)
  119. return fn(self, line, col, ...)
  120. end
  121. local function position_offset_byte(self, line, col, offset)
  122. line, col = self:sanitize_position(line, col)
  123. col = col + offset
  124. while line > 1 and col < 1 do
  125. line = line - 1
  126. col = col + #self.lines[line]
  127. end
  128. while line < #self.lines and col > #self.lines[line] do
  129. col = col - #self.lines[line]
  130. line = line + 1
  131. end
  132. return self:sanitize_position(line, col)
  133. end
  134. local function position_offset_linecol(self, line, col, lineoffset, coloffset)
  135. return self:sanitize_position(line + lineoffset, col + coloffset)
  136. end
  137. function Doc:position_offset(line, col, ...)
  138. if type(...) ~= "number" then
  139. return position_offset_func(self, line, col, ...)
  140. elseif select("#", ...) == 1 then
  141. return position_offset_byte(self, line, col, ...)
  142. elseif select("#", ...) == 2 then
  143. return position_offset_linecol(self, line, col, ...)
  144. else
  145. error("bad number of arguments")
  146. end
  147. end
  148. function Doc:get_text(line1, col1, line2, col2)
  149. line1, col1 = self:sanitize_position(line1, col1)
  150. line2, col2 = self:sanitize_position(line2, col2)
  151. line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
  152. if line1 == line2 then
  153. return self.lines[line1]:sub(col1, col2 - 1)
  154. end
  155. local lines = { self.lines[line1]:sub(col1) }
  156. for i = line1 + 1, line2 - 1 do
  157. table.insert(lines, self.lines[i])
  158. end
  159. table.insert(lines, self.lines[line2]:sub(1, col2 - 1))
  160. return table.concat(lines)
  161. end
  162. function Doc:get_char(line, col)
  163. line, col = self:sanitize_position(line, col)
  164. return self.lines[line]:sub(col, col)
  165. end
  166. local push_undo
  167. local function insert(self, undo_stack, time, line, col, text)
  168. line, col = self:sanitize_position(line, col)
  169. -- split text into lines and merge with line at insertion point
  170. local lines = split_lines(text)
  171. local before = self.lines[line]:sub(1, col - 1)
  172. local after = self.lines[line]:sub(col)
  173. for i = 1, #lines - 1 do
  174. lines[i] = lines[i] .. "\n"
  175. end
  176. lines[1] = before .. lines[1]
  177. lines[#lines] = lines[#lines] .. after
  178. -- splice lines into line array
  179. splice(self.lines, line, 1, lines)
  180. -- push undo
  181. local line2, col2 = self:position_offset(line, col, #text)
  182. push_undo(self, undo_stack, time, "selection", self:get_selection())
  183. push_undo(self, undo_stack, time, "remove", line, col, line2, col2)
  184. end
  185. local function remove(self, undo_stack, time, line1, col1, line2, col2)
  186. line1, col1 = self:sanitize_position(line1, col1)
  187. line2, col2 = self:sanitize_position(line2, col2)
  188. line1, col1, line2, col2 = sort_positions(line1, col1, line2, col2)
  189. -- push undo
  190. local text = self:get_text(line1, col1, line2, col2)
  191. push_undo(self, undo_stack, time, "selection", self:get_selection())
  192. push_undo(self, undo_stack, time, "insert", line1, col1, text)
  193. -- get line content before/after removed text
  194. local before = self.lines[line1]:sub(1, col1 - 1)
  195. local after = self.lines[line2]:sub(col2)
  196. -- splice line into line array
  197. splice(self.lines, line1, line2 - line1 + 1, { before .. after })
  198. end
  199. function Doc:insert(...)
  200. insert(self, self.undo_stack, system.get_time(), ...)
  201. self:sanitize_selection()
  202. self.redo_stack = { idx = 1 }
  203. end
  204. function Doc:remove(...)
  205. remove(self, self.undo_stack, system.get_time(), ...)
  206. self:sanitize_selection()
  207. self.redo_stack = { idx = 1 }
  208. end
  209. function push_undo(self, undo_stack, time, type, ...)
  210. undo_stack[undo_stack.idx] = { type = type, time = time, ... }
  211. undo_stack[undo_stack.idx - config.max_undos] = nil
  212. undo_stack.idx = undo_stack.idx + 1
  213. end
  214. local function pop_undo(self, undo_stack, redo_stack)
  215. -- pop command
  216. local cmd = undo_stack[undo_stack.idx - 1]
  217. if not cmd then return end
  218. undo_stack.idx = undo_stack.idx - 1
  219. -- handle command
  220. if cmd.type == "insert" then
  221. insert(self, redo_stack, cmd.time, table.unpack(cmd))
  222. elseif cmd.type == "remove" then
  223. remove(self, redo_stack, cmd.time, table.unpack(cmd))
  224. elseif cmd.type == "selection" then
  225. self.selection.a.line, self.selection.a.col = cmd[1], cmd[2]
  226. self.selection.b.line, self.selection.b.col = cmd[3], cmd[4]
  227. end
  228. -- if next undo command is within the merge timeout then treat as a single
  229. -- command and continue to execute it
  230. local next = undo_stack[undo_stack.idx - 1]
  231. if next and math.abs(cmd.time - next.time) < config.undo_merge_timeout then
  232. return pop_undo(self, undo_stack, redo_stack)
  233. end
  234. end
  235. function Doc:undo()
  236. pop_undo(self, self.undo_stack, self.redo_stack)
  237. end
  238. function Doc:redo()
  239. pop_undo(self, self.redo_stack, self.undo_stack)
  240. end
  241. function Doc:text_input(text)
  242. if self:has_selection() then
  243. self:delete_to()
  244. end
  245. local line, col = self:get_selection()
  246. self:insert(line, col, text)
  247. self:move_to(#text)
  248. end
  249. function Doc:replace(fn)
  250. local line1, col1, line2, col2, swap
  251. local had_selection = self:has_selection()
  252. if had_selection then
  253. line1, col1, line2, col2, swap = self:get_selection(true)
  254. else
  255. line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines]
  256. end
  257. local old_text = self:get_text(line1, col1, line2, col2)
  258. local new_text, n = fn(old_text)
  259. if old_text ~= new_text then
  260. self:insert(line2, col2, new_text)
  261. self:remove(line1, col1, line2, col2)
  262. if had_selection then
  263. line2, col2 = self:position_offset(line1, col1, #new_text)
  264. self:set_selection(line1, col1, line2, col2, swap)
  265. end
  266. end
  267. return n
  268. end
  269. function Doc:delete_to(...)
  270. local line, col = self:get_selection(true)
  271. if self:has_selection() then
  272. self:remove(self:get_selection())
  273. else
  274. local line2, col2 = self:position_offset(line, col, ...)
  275. self:remove(line, col, line2, col2)
  276. line, col = sort_positions(line, col, line2, col2)
  277. end
  278. self:set_selection(line, col)
  279. end
  280. function Doc:move_to(...)
  281. local line, col = self:get_selection()
  282. self:set_selection(self:position_offset(line, col, ...))
  283. end
  284. function Doc:select_to(...)
  285. local line, col, line2, col2 = self:get_selection()
  286. line, col = self:position_offset(line, col, ...)
  287. self:set_selection(line, col, line2, col2)
  288. end
  289. return Doc