buffer.lua 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. local m = {} -- platform independant text buffer
  2. local lexer = require'lua-lexer'
  3. local clipboard = '' -- shared between buffer instances
  4. --helper functions
  5. local function sanitize(text)
  6. text = text:gsub('[\192-\255][\128-\191]*', '?') -- remove non-ASCII chars which would cause crash
  7. text = text:gsub('\t', '') -- tabs are handled with buffer:insertTab()
  8. return text
  9. end
  10. local insertCharAt = function(text, c, pos)
  11. c = sanitize(c)
  12. local first = text:sub(1, pos)
  13. local last = text:sub(pos + 1)
  14. return first .. c .. last, #c
  15. end
  16. local repeatN = function(n, f, ...)
  17. for i=1,n do f(...) end
  18. end
  19. function m.new(cols, rows, drawToken, drawRectangle, initialText, undoStackSize)
  20. local buffer = {
  21. -- all coordinates in text space (integers)
  22. cols = cols,
  23. rows = rows,
  24. name = '',
  25. undoStackSize = undoStackSize or 0,
  26. undoStack = {},
  27. cursor = {x=0, y=1}, -- x is 0-indexed, y is 1-indexed
  28. selection = {x=0, y=1}, -- x is 0-indexed, y is 1-indexed
  29. -- selected text spans between cursor and selection marker
  30. scroll = {x=5, y=0}, -- both 0-indexed
  31. lines = {}, -- text broken in lines
  32. lexed = {}, -- text lines broken into tokens
  33. drawToken = drawToken,
  34. drawRectangle = drawRectangle,
  35. -- 'public' api
  36. getText = function(self)
  37. return table.concat(self.lines, '\n')
  38. end,
  39. getCursorLine = function(self)
  40. return self.lines[self.cursor.y]
  41. end,
  42. setName = function(self, name)
  43. self.name = name:gsub('%c', '')
  44. end,
  45. setText = function(self, text)
  46. text = sanitize(text)
  47. self.lines = {}
  48. self.lexed = lexer(text)
  49. for i, line in ipairs(self.lexed) do
  50. lineStrings = {}
  51. for l, token in ipairs(line) do
  52. table.insert(lineStrings, token.data)
  53. end
  54. table.insert(self.lines, table.concat(lineStrings, ''))
  55. end
  56. self:updateView()
  57. self:deselect()
  58. end,
  59. drawCode = function(self)
  60. local linesToDraw = math.min(self.rows, #(self.lexed)-self.scroll.y)
  61. local selectionFrom, selectionTo = self:selectionSpan()
  62. local selectionWidth = (selectionTo.x + selectionTo.y * self.cols) - (selectionFrom.x + selectionFrom.y * self.cols)
  63. -- highlight cursor line
  64. if selectionWidth == 0 then
  65. self.drawRectangle(-1, self.cursor.y - self.scroll.y, self.cols + 2, 'cursorline')
  66. end
  67. -- selection
  68. local x, y = selectionFrom.x, selectionFrom.y
  69. self.drawRectangle(x - self.scroll.x, y - self.scroll.y, selectionWidth, 'selection')
  70. selectionWidth = selectionWidth - (self.cols - selectionFrom.x)
  71. while selectionWidth > 0 do
  72. y = y + 1
  73. self.drawRectangle(0 - self.scroll.x, y - self.scroll.y, selectionWidth, 'selection')
  74. selectionWidth = selectionWidth - self.cols
  75. end
  76. -- file content
  77. for y = 1, linesToDraw do
  78. local x = -self.scroll.x
  79. local currentLine = y + self.scroll.y
  80. -- draw cursor line and caret
  81. if currentLine == self.cursor.y then
  82. self.drawToken('|', self.cursor.x - self.scroll.x - 0.5, y, 'caret')
  83. end
  84. -- draw single line of text
  85. local lineTokens = self.lexed[currentLine]
  86. for j, token in ipairs(lineTokens) do
  87. self.drawToken(token.data, x, y, token.type)
  88. --print('token',x,y)
  89. x = x + #token.data
  90. end
  91. end
  92. -- status line
  93. self.drawToken(self.statusLine, self.cols - #self.statusLine, 0, 'comment')
  94. end,
  95. -- cursor movement
  96. cursorUp = function(self)
  97. self.cursor.y = self.cursor.y - 1
  98. self:updateView()
  99. end,
  100. cursorDown = function(self)
  101. self.cursor.y = self.cursor.y + 1
  102. self:updateView()
  103. end,
  104. cursorJumpUp = function(self)
  105. repeatN(10, self.cursorUp, self)
  106. end,
  107. cursorJumpDown = function(self)
  108. repeatN(10, self.cursorDown, self)
  109. end,
  110. cursorLeft = function(self)
  111. if self.cursor.x == 0 then
  112. if self.cursor.y > 1 then
  113. self:cursorUp()
  114. self:cursorEnd()
  115. else
  116. return false;
  117. end
  118. else
  119. self.cursor.x = self.cursor.x - 1
  120. self:updateView()
  121. end
  122. return true
  123. end,
  124. cursorRight = function(self)
  125. local length = string.len(self.lines[self.cursor.y])
  126. if self.cursor.x == length then
  127. if self.cursor.y < #(self.lines) then
  128. self:cursorDown()
  129. self:cursorHome()
  130. else
  131. return false
  132. end
  133. else
  134. self.cursor.x = self.cursor.x + 1
  135. self:updateView()
  136. end
  137. return true
  138. end,
  139. cursorJumpLeft = function(self)
  140. self:cursorLeft()
  141. local pattern = self:charAtCursor():find('[%d%l%u]') and '[%d%l%u]' or '%s'
  142. self:repeatOverPattern(pattern, self.cursorLeft, self)
  143. end,
  144. cursorJumpRight = function(self)
  145. self:cursorRight()
  146. local pattern = self:charAtCursor():find('[%d%l%u]') and '[%d%l%u]' or '%s'
  147. self:repeatOverPattern(pattern, self.cursorRight, self)
  148. end,
  149. cursorHome = function(self)
  150. self.cursor.x = 0
  151. self:updateView()
  152. end,
  153. cursorEnd = function(self)
  154. self.cursor.x = string.len(self.lines[self.cursor.y])
  155. self:updateView()
  156. end,
  157. cursorPageUp = function(self)
  158. self.cursor.y = self.cursor.y - self.rows
  159. self:updateView()
  160. end,
  161. cursorPageDown = function(self)
  162. self.cursor.y = self.cursor.y + self.rows
  163. self:updateView()
  164. end,
  165. cursorJumpHome = function(self)
  166. self.cursor.x, self.cursor.y = 0, 1
  167. self:updateView()
  168. end,
  169. cursorJumpEnd = function(self)
  170. self.cursor.y = #self.lines
  171. self.cursor.x = #self.lines[self.cursor.y]
  172. self:updateView()
  173. end,
  174. -- inserting and removing characters
  175. insertCharacter = function(self, c)
  176. self:deleteSelection()
  177. self:processUndo('insert character')
  178. local length
  179. self.lines[self.cursor.y], length = insertCharAt(self.lines[self.cursor.y], c, self.cursor.x)
  180. self:lexLine(self.cursor.y)
  181. self.cursor.x = self.cursor.x + length
  182. self:updateView()
  183. self:deselect()
  184. end,
  185. insertTab = function(self)
  186. self:deleteSelection()
  187. self:insertString(' ') -- tab width is adjustable here
  188. self:deselect()
  189. end,
  190. breakLine = function(self, withoutIndent)
  191. self:deleteSelection()
  192. self:processUndo('break line')
  193. local nl = self.lines[self.cursor.y]
  194. local bef = nl:sub(1,self.cursor.x)
  195. local aft = nl:sub(self.cursor.x + 1, #nl)
  196. local indent = #(bef:match('^%s+') or '')
  197. self.lines[self.cursor.y] = bef
  198. self:lexLine(self.cursor.y)
  199. table.insert(self.lines, self.cursor.y + 1, aft)
  200. table.insert(self.lexed, self.cursor.y + 1, {})
  201. self:lexLine(self.cursor.y + 1)
  202. self:cursorHome()
  203. self:cursorDown()
  204. self:deselect()
  205. if not withoutIndent then
  206. repeatN(indent, self.insertCharacter, self, ' ')
  207. end
  208. end,
  209. deleteRight = function(self)
  210. if self:isSelected() then
  211. self:deleteSelection()
  212. else
  213. self:processUndo('delete character')
  214. local length = string.len(self.lines[self.cursor.y])
  215. if length == self.cursor.x then -- end of line
  216. if self.cursor.y < #self.lines then
  217. -- if we have another line, remove newline by joining lines
  218. local nl = self.lines[self.cursor.y] .. self.lines[self.cursor.y + 1]
  219. self.lines[self.cursor.y] = nl
  220. self:lexLine(self.cursor.y)
  221. table.remove(self.lines, self.cursor.y + 1)
  222. table.remove(self.lexed, self.cursor.y + 1)
  223. end
  224. else -- middle of line, remove char
  225. local nl = self.lines[self.cursor.y]
  226. local bef = nl:sub(1, self.cursor.x)
  227. local aft = nl:sub(self.cursor.x + 2, string.len(nl))
  228. self.lines[self.cursor.y] = bef..aft
  229. self:lexLine(self.cursor.y)
  230. end
  231. end
  232. end,
  233. deleteLeft = function(self)
  234. if self:isSelected() then
  235. self:deleteSelection()
  236. elseif self:cursorLeft() then
  237. self:deleteRight()
  238. end
  239. end,
  240. deleteWord = function(self)
  241. self:deleteLeft()
  242. local pattern = self:charAtCursor():find('[%d%l%u]') and '[%d%l%u]' or '%s'
  243. self:repeatOverPattern(pattern, self.deleteLeft, self)
  244. end,
  245. -- clipboard
  246. cutText = function(self)
  247. if self:isSelected() then
  248. self:copyText()
  249. self:deleteSelection()
  250. elseif #self.lines > 1 then
  251. clipboard = self.lines[self.cursor.y] .. '\n'
  252. table.remove(self.lines, self.cursor.y)
  253. table.remove(self.lexed, self.cursor.y)
  254. else
  255. self.lines[1] = ''
  256. self.lexed[1] = {}
  257. end
  258. self:updateView()
  259. end,
  260. copyText = function(self)
  261. self:processUndo('copy clipboard')
  262. if self:isSelected() then
  263. local selectionFrom, selectionTo = self:selectionSpan()
  264. local lines = {}
  265. for y = selectionFrom.y, selectionTo.y do
  266. local fromX = y == selectionFrom.y and selectionFrom.x or 0
  267. local toX = y == selectionTo.y and selectionTo.x or self.lines[y]:len()
  268. table.insert(lines, self.lines[y]:sub(fromX + 1, toX))
  269. end
  270. clipboard = table.concat(lines, '\n')
  271. else -- copy cursor line
  272. clipboard = self.lines[self.cursor.y] .. '\n'
  273. end
  274. end,
  275. pasteText = function(self)
  276. self:processUndo('paste clipboard')
  277. self:deleteSelection()
  278. self:insertString(clipboard)
  279. end,
  280. -- undo handling
  281. processUndo = function(self, action)
  282. if self.undoStackSize == 0 then
  283. return
  284. end
  285. if action == self.undoLastAction then
  286. if action == 'insert character' or action == 'delete character' then
  287. return
  288. end
  289. end
  290. table.insert(self.undoStack, {self:getText(), self.cursor.x, self.cursor.y, self.selection.x, self.selection.y})
  291. if #self.undoStack > self.undoStackSize then
  292. table.remove(self.undoStack, 1)
  293. end
  294. print('undo pushed:', action)
  295. print('"', self:getText(), '"')
  296. self.undoLastAction = action
  297. end,
  298. undo = function(self)
  299. if self.undoStack[#self.undoStack] then
  300. local text, cx, cy, sx, sy = unpack(self.undoStack[#self.undoStack])
  301. self.cursor.x, self.cursor.y = cx, cy
  302. self:setText(text)
  303. table.remove(self.undoStack)
  304. self.undoLastAction = "undo'ed"
  305. print('undo popped')
  306. end
  307. end,
  308. -- helper functions
  309. isSelected = function(self)
  310. return self.selection.x ~= self.cursor.x or self.selection.y ~= self.cursor.y
  311. end,
  312. selectionSpan = function(self)
  313. if self.selection.y * self.cols + self.selection.x < self.cursor.y * self.cols + self.cursor.x then
  314. return self.selection, self.cursor
  315. else
  316. return self.cursor, self.selection
  317. end
  318. end,
  319. deleteSelection = function(self)
  320. if not self:isSelected() then return end
  321. self:processUndo('delete string')
  322. local selectionFrom, selectionTo = self:selectionSpan()
  323. local singleLineChange = selectionFrom.y == selectionTo.y
  324. local lines = {}
  325. for y = selectionTo.y, selectionFrom.y, -1 do
  326. local fromX = y == selectionFrom.y and selectionFrom.x or 0
  327. local toX = y == selectionTo.y and selectionTo.x or self.lines[y]:len()
  328. if y > selectionFrom.y and y < selectionTo.y then
  329. table.remove(self.lines, y)
  330. table.remove(self.lexed, y)
  331. else
  332. local fromX = y == selectionFrom.y and selectionFrom.x or 0
  333. local toX = y == selectionTo.y and selectionTo.x or self.lines[y]:len()
  334. local bef = self.lines[y]:sub(0, fromX)
  335. local aft = self.lines[y]:sub(toX + 1, self.lines[y]:len())
  336. self.lines[y] = bef .. aft
  337. end
  338. end
  339. self.cursor.x, self.cursor.y = selectionFrom.x, selectionFrom.y
  340. self:deselect()
  341. if singleLineChange then
  342. self:lexLine(self.cursor.y)
  343. else
  344. self:deleteRight()
  345. self:lexAll()
  346. end
  347. self:updateView()
  348. end,
  349. deselect = function(self)
  350. self.selection.x, self.selection.y = self.cursor.x, self.cursor.y
  351. end,
  352. jumpToLine = function(self, lineNumber, columnNumber)
  353. lineNumber = math.min(lineNumber or 1, #self.lines)
  354. columnNumber = math.min(columnNumber or 0, #self.lines[lineNumber] - 1)
  355. self.cursor.x = columnNumber
  356. self.cursor.y = lineNumber
  357. self.scroll.y = math.max(lineNumber - math.floor(self.rows / 2), 0)
  358. self.scroll.x = math.max(columnNumber - math.floor(7 * self.cols / 8), 0)
  359. self:deselect()
  360. end,
  361. insertString = function(self, str)
  362. self:processUndo('insert string')
  363. local singleLineChange = true
  364. for c in str:gmatch('.') do
  365. if c == '\n' then
  366. singleLineChange = false
  367. self:deselect()
  368. self:breakLine(true)
  369. elseif c:match('%C') then
  370. local length
  371. self.lines[self.cursor.y], length = insertCharAt(self.lines[self.cursor.y], c, self.cursor.x)
  372. self.cursor.x = self.cursor.x + length
  373. end
  374. end
  375. if singleLineChange then
  376. self:lexLine(self.cursor.y)
  377. else
  378. self:lexAll()
  379. end
  380. self:deselect()
  381. end,
  382. charAtCursor = function(self)
  383. return self.lines[self.cursor.y]:sub(self.cursor.x, self.cursor.x)
  384. end,
  385. lexLine = function(self, lineNum)
  386. self.lexed[lineNum] = lexer(self.lines[lineNum])[1]
  387. end,
  388. lexAll = function(self) -- lexing single line cannot handle multiline comments and strings
  389. self.lexed = lexer(self:getText())
  390. end,
  391. updateView = function(self)
  392. -- constrain cursor to text limits
  393. self.cursor.y = math.max(self.cursor.y, 1)
  394. self.cursor.y = math.min(self.cursor.y, #(self.lines))
  395. local lineLength = string.len(self.lines[self.cursor.y] or '')
  396. self.cursor.x = math.max(self.cursor.x, 0)
  397. self.cursor.x = math.min(self.cursor.x, lineLength)
  398. -- constrain selection to text limits
  399. self.selection.y = math.max(self.selection.y, 1)
  400. self.selection.y = math.min(self.selection.y, #(self.lines))
  401. local lineLength = string.len(self.lines[self.selection.y] or "")
  402. self.selection.x = math.max(self.selection.x, 0)
  403. self.selection.x = math.min(self.selection.x, lineLength)
  404. -- keep cursor on screen
  405. if self.cursor.y <= self.scroll.y then
  406. self.scroll.y = self.cursor.y - 1
  407. elseif self.cursor.y > self.scroll.y + self.rows then
  408. self.scroll.y = self.cursor.y - self.rows
  409. end
  410. if self.cursor.x < self.scroll.x then
  411. self.scroll.x = math.max(self.cursor.x - 10, 0)
  412. elseif self.cursor.x > self.scroll.x + self.cols then
  413. self.scroll.x = self.cursor.x + 10 - self.cols
  414. end
  415. -- update status line
  416. self.statusLine = string.format('L%d C%d %s', self.cursor.y, self.cursor.x, self.name)
  417. end,
  418. repeatOverPattern = function(self, pattern, moveF, ...)
  419. -- execute moveF() over text as long as character matches pattern and cursor moves
  420. while self:charAtCursor():match(pattern) do
  421. local oldX, oldY = self.cursor.x, self.cursor.y
  422. moveF(...)
  423. if (oldX == self.cursor.x and oldY == self.cursor.y) then break end
  424. end
  425. end,
  426. }
  427. -- generate all select_ and move_ actions
  428. for _, functionName in ipairs({'Up', 'Down', 'Left', 'Right', 'JumpUp', 'JumpDown', 'JumpLeft', 'JumpRight',
  429. 'Home', 'End', 'PageUp', 'PageDown', 'JumpHome', 'JumpEnd'}) do
  430. buffer['select' .. functionName] = function(self)
  431. --self.selection = self.selection or {x= self.cursor.x, y= self.cursor.y}
  432. self['cursor' .. functionName](self)
  433. end
  434. buffer['move' .. functionName] = function(self)
  435. self['cursor' .. functionName](self)
  436. self:deselect()
  437. end
  438. end
  439. buffer:setText(initialText or '')
  440. return buffer
  441. end
  442. return m
  443. --[[ Lua pattern matching
  444. str:find(pattern) finds the first instance of pattern in string and returns its position
  445. str:gmatch(pattern) when called repeatedly, returns each successive instance of pattern in string
  446. str:gsub(pattern, repl) returns a string where all instances of pattern in string have been replaced with repl
  447. str:match(pattern) returns the first instance of pattern in string
  448. X represents the character X itself as long as it is not a magic character
  449. . represents any single character
  450. %a represents all letters A-Z and a-z
  451. %c represents all control characters such as Null, Tab, Carr.Return, Linefeed, Delete, etc
  452. %d represents all digits 0-9
  453. %l represents all lowercase letters a-z
  454. %p represents all punctuation characters or symbols such as . , ? ! : ; @ [ ] _ { } ~
  455. %s represents all white space characters such as Tab, Carr.Return, Linefeed, Space, etc
  456. %u represents all uppercase letters A-Z
  457. %w represents all alphanumeric characters A-Z and a-z and 0-9
  458. %x represents all hexadecimal digits 0-9 and A-F and a-f
  459. %z represents the character with code \000 because embedded zeroes in a pattern do not work
  460. The upper case letter versions of the above reverses their meaning
  461. i.e. %A represents all non-letters and %D represents all non-digits
  462. + one or more repetitions
  463. * or - zero or more repetitions
  464. ? optional (zero or one occurrence)
  465. %Y represents the character Y if it is any non-alphanumeric character
  466. This is the standard way to get a magic character to match itself
  467. Any punctuation character (even a non magic one) preceded by a % represents itself
  468. e.g. %% represents % percent and %+ represents + plus
  469. [set] represents the class which is the union of all characters in the set
  470. A range of characters is specified by separating first and last character of range with a - hyphen e.g. 1-5
  471. All classes described above may also be used as components in the set
  472. e.g. [%w~] (or [~%w]) represents all alphanumeric characters plus the ~ tilde
  473. [^set] represents the complement of set, where set is interpreted as above
  474. e.g. [^A-Z] represents any character except upper case letters --]]