docview.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. local core = require "core"
  2. local common = require "core.common"
  3. local config = require "core.config"
  4. local style = require "core.style"
  5. local syntax = require "core.syntax"
  6. local translate = require "core.doc.translate"
  7. local View = require "core.view"
  8. local highlighter = require "core.highlighter"
  9. local DocView = View:extend()
  10. local function move_to_line_offset(dv, line, col, offset)
  11. local xo = dv.last_x_offset
  12. if xo.line ~= line or xo.col ~= col then
  13. xo.offset = dv:get_col_x_offset(line, col)
  14. end
  15. xo.line = line + offset
  16. xo.col = dv:get_x_offset_col(line + offset, xo.offset)
  17. return xo.line, xo.col
  18. end
  19. DocView.translate = {
  20. ["previous_page"] = function(doc, line, col, dv)
  21. local min, max = dv:get_visible_line_range()
  22. return line - (max - min), 1
  23. end,
  24. ["next_page"] = function(doc, line, col, dv)
  25. local min, max = dv:get_visible_line_range()
  26. return line + (max - min), 1
  27. end,
  28. ["previous_line"] = function(doc, line, col, dv)
  29. if line == 1 then
  30. return 1, 1
  31. end
  32. return move_to_line_offset(dv, line, col, -1)
  33. end,
  34. ["next_line"] = function(doc, line, col, dv)
  35. if line == #doc.lines then
  36. return #doc.lines, math.huge
  37. end
  38. return move_to_line_offset(dv, line, col, 1)
  39. end,
  40. }
  41. local blink_period = 0.8
  42. local function reset_syntax(self)
  43. local syn = syntax.get(self.doc.filename or "")
  44. if self.syntax ~= syn then
  45. self.syntax = syn
  46. self.cache = { last_valid = 1 }
  47. end
  48. end
  49. function DocView:new(doc)
  50. DocView.super.new(self)
  51. self.cursor = "ibeam"
  52. self.scrollable = true
  53. self.doc = assert(doc)
  54. self.font = "code_font"
  55. self.last_x_offset = {}
  56. self.blink_timer = 0
  57. reset_syntax(self)
  58. -- init thread for incremental highlighting
  59. self.updated_highlighting = false
  60. core.add_thread(function()
  61. while true do
  62. local _, max = self:get_visible_line_range()
  63. if self.cache.last_valid > max then
  64. coroutine.yield(1 / config.fps)
  65. else
  66. max = math.min(self.cache.last_valid + 20, max)
  67. for i = self.cache.last_valid, max do
  68. local state = (i > 1) and self.cache[i - 1].state
  69. local cl = self.cache[i]
  70. if not (cl and cl.init_state == state) then
  71. self.cache[i] = self:tokenize_line(i, state)
  72. end
  73. end
  74. self.cache.last_valid = max + 1
  75. self.updated_highlighting = true
  76. coroutine.yield()
  77. end
  78. end
  79. end, self)
  80. end
  81. function DocView:try_close(do_close)
  82. if self.doc:is_dirty()
  83. and #core.get_views_referencing_doc(self.doc) == 1 then
  84. core.command_view:enter("Unsaved Changes; Confirm Close", function(_, item)
  85. if item.text:match("^[cC]") then
  86. do_close()
  87. elseif item.text:match("^[sS]") then
  88. self.doc:save()
  89. do_close()
  90. end
  91. end, function(text)
  92. local items = {}
  93. if not text:find("^[^cC]") then table.insert(items, "Close Without Saving") end
  94. if not text:find("^[^sS]") then table.insert(items, "Save And Close") end
  95. return items
  96. end)
  97. else
  98. do_close()
  99. end
  100. end
  101. function DocView:get_name()
  102. local post = self.doc:is_dirty() and "*" or ""
  103. local name = self.doc:get_name()
  104. return name:match("[^/%\\]*$") .. post
  105. end
  106. function DocView:get_scrollable_size()
  107. return self:get_line_height() * #self.doc.lines + style.padding.y * 2
  108. end
  109. function DocView:tokenize_line(idx, state)
  110. local cl = {}
  111. cl.init_state = state
  112. cl.text = self.doc.lines[idx]
  113. cl.tokens, cl.state = highlighter.tokenize(self.syntax, cl.text, state)
  114. local t = cl.tokens
  115. t[#t] = t[#t]:sub(1, -2) -- strip '\n'
  116. return cl
  117. end
  118. function DocView:get_cached_line(idx)
  119. local cl = self.cache[idx]
  120. if not cl or cl.text ~= self.doc.lines[idx] then
  121. local prev = self.cache[idx-1]
  122. cl = self:tokenize_line(idx, prev and prev.state)
  123. self.cache[idx] = cl
  124. self.cache.last_valid = math.min(self.cache.last_valid, idx)
  125. end
  126. return cl
  127. end
  128. function DocView:get_font()
  129. return style[self.font]
  130. end
  131. function DocView:get_line_height()
  132. return math.floor(self:get_font():get_height() * config.line_height)
  133. end
  134. function DocView:get_gutter_width()
  135. return self:get_font():get_width(#self.doc.lines) + style.padding.x * 2
  136. end
  137. function DocView:get_line_screen_position(idx)
  138. local x, y = self:get_content_offset()
  139. local lh = self:get_line_height()
  140. local gw = self:get_gutter_width()
  141. return x + gw, y + (idx-1) * lh + style.padding.y
  142. end
  143. function DocView:get_line_text_y_offset()
  144. local lh = self:get_line_height()
  145. local th = self:get_font():get_height()
  146. return (lh - th) / 2
  147. end
  148. function DocView:get_visible_line_range()
  149. local x, y, x2, y2 = self:get_content_bounds()
  150. local lh = self:get_line_height()
  151. local minline = math.max(1, math.floor(y / lh))
  152. local maxline = math.min(#self.doc.lines, math.floor(y2 / lh) + 1)
  153. return minline, maxline
  154. end
  155. function DocView:get_col_x_offset(line, col)
  156. local text = self.doc.lines[line]
  157. if not text then return 0 end
  158. return self:get_font():get_width(text:sub(1, col - 1))
  159. end
  160. function DocView:get_x_offset_col(line, x)
  161. local text = self.doc.lines[line]
  162. local xoffset, last_i, i = 0, 1, 1
  163. for char in common.utf8_chars(text) do
  164. local w = self:get_font():get_width(char)
  165. if xoffset >= x then
  166. return (xoffset - x > w / 2) and last_i or i
  167. end
  168. xoffset = xoffset + w
  169. last_i = i
  170. i = i + #char
  171. end
  172. return #text
  173. end
  174. function DocView:resolve_screen_position(x, y)
  175. local ox, oy = self:get_line_screen_position(1)
  176. local line = math.floor((y - oy) / self:get_line_height()) + 1
  177. line = common.clamp(line, 1, #self.doc.lines)
  178. local col = self:get_x_offset_col(line, x - ox)
  179. return line, col
  180. end
  181. function DocView:scroll_to_line(line, ignore_if_visible, instant)
  182. local min, max = self:get_visible_line_range()
  183. if not (ignore_if_visible and line > min and line < max) then
  184. local lh = self:get_line_height()
  185. self.scroll.to.y = math.max(0, lh * (line - 1) - self.size.y / 2)
  186. if instant then
  187. self.scroll.y = self.scroll.to.y
  188. end
  189. end
  190. end
  191. function DocView:scroll_to_make_visible(line, col)
  192. local min = self:get_line_height() * (line - 1)
  193. local max = self:get_line_height() * (line + 2) - self.size.y
  194. self.scroll.to.y = math.min(self.scroll.to.y, min)
  195. self.scroll.to.y = math.max(self.scroll.to.y, max)
  196. local gw = self:get_gutter_width()
  197. local xoffset = self:get_col_x_offset(line, col)
  198. local max = xoffset - self.size.x + gw + self.size.x / 5
  199. self.scroll.to.x = math.max(0, max)
  200. end
  201. function DocView:on_mouse_pressed(button, x, y, clicks)
  202. local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks)
  203. if caught then
  204. return
  205. end
  206. local line, col = self:resolve_screen_position(x, y)
  207. if clicks == 2 then
  208. local line1, col1 = translate.start_of_word(self.doc, line, col)
  209. local line2, col2 = translate.end_of_word(self.doc, line, col)
  210. self.doc:set_selection(line2, col2, line1, col1)
  211. elseif clicks == 3 then
  212. self.doc:set_selection(line + 1, 1, line, 1)
  213. else
  214. self.doc:set_selection(line, col)
  215. self.mouse_selecting = true
  216. end
  217. self.blink_timer = 0
  218. end
  219. function DocView:on_mouse_moved(x, y, ...)
  220. DocView.super.on_mouse_moved(self, x, y, ...)
  221. if self:scrollbar_overlaps_point(x, y) or self.dragging_scrollbar then
  222. self.cursor = "arrow"
  223. else
  224. self.cursor = "ibeam"
  225. end
  226. if self.mouse_selecting then
  227. local _, _, line2, col2 = self.doc:get_selection()
  228. local line1, col1 = self:resolve_screen_position(x, y)
  229. self.doc:set_selection(line1, col1, line2, col2)
  230. end
  231. end
  232. function DocView:on_mouse_released(button)
  233. DocView.super.on_mouse_released(self, button)
  234. self.mouse_selecting = false
  235. end
  236. function DocView:on_text_input(text)
  237. self.doc:text_input(text)
  238. end
  239. function DocView:update()
  240. -- scroll to make caret visible and reset blink timer if it moved
  241. local line, col = self.doc:get_selection()
  242. if (line ~= self.last_line or col ~= self.last_col) and self.size.x > 0 then
  243. if core.active_view == self then
  244. self:scroll_to_make_visible(line, col)
  245. end
  246. self.blink_timer = 0
  247. self.last_line, self.last_col = line, col
  248. end
  249. if self.updated_highlighting then
  250. self.updated_highlighting = false
  251. core.redraw = true
  252. end
  253. if self.doc.filename ~= self.last_filename then
  254. reset_syntax(self)
  255. self.last_filename = self.doc.filename
  256. end
  257. -- update blink timer
  258. if self == core.active_view and not self.mouse_selecting then
  259. local n = blink_period / 2
  260. local prev = self.blink_timer
  261. self.blink_timer = (self.blink_timer + 1 / config.fps) % blink_period
  262. if (self.blink_timer > n) ~= (prev > n) then
  263. core.redraw = true
  264. end
  265. end
  266. DocView.super.update(self)
  267. end
  268. function DocView:draw_line_highlight(x, y)
  269. local lh = self:get_line_height()
  270. renderer.draw_rect(x, y, self.size.x, lh, style.line_highlight)
  271. end
  272. function DocView:draw_line_text(idx, x, y)
  273. local cl = self:get_cached_line(idx)
  274. local line, col = self.doc:get_selection()
  275. -- draw selection if it overlaps this line
  276. local line1, col1, line2, col2 = self.doc:get_selection(true)
  277. if idx >= line1 and idx <= line2 then
  278. if line1 ~= idx then col1 = 1 end
  279. if line2 ~= idx then col2 = #cl.text + 1 end
  280. local x1 = x + self:get_col_x_offset(idx, col1)
  281. local x2 = x + self:get_col_x_offset(idx, col2)
  282. local lh = self:get_line_height()
  283. renderer.draw_rect(x1, y, x2 - x1, lh, style.selection)
  284. end
  285. -- draw line highlight if caret is on this line
  286. if config.highlight_current_line and not self.doc:has_selection()
  287. and line == idx and core.active_view == self then
  288. self:draw_line_highlight(x + self.scroll.x, y)
  289. end
  290. -- draw line's text
  291. local tx, ty = x, y + self:get_line_text_y_offset()
  292. local font = self:get_font()
  293. for _, type, text in highlighter.each_token(cl.tokens) do
  294. local color = style.syntax[type]
  295. tx = renderer.draw_text(font, text, tx, ty, color)
  296. end
  297. -- draw caret if it overlaps this line
  298. if line == idx and core.active_view == self
  299. and self.blink_timer < blink_period / 2
  300. and system.window_has_focus() then
  301. local lh = self:get_line_height()
  302. local x1 = x + self:get_col_x_offset(line, col)
  303. renderer.draw_rect(x1, y, style.caret_width, lh, style.caret)
  304. end
  305. end
  306. function DocView:draw_gutter_text(idx, x, y)
  307. local color = style.line_number
  308. local line1, _, line2, _ = self.doc:get_selection(true)
  309. if idx >= line1 and idx <= line2 then
  310. color = style.line_number2
  311. end
  312. local yoffset = self:get_line_text_y_offset()
  313. x = x + self.scroll.x
  314. renderer.draw_text(self:get_font(), idx, x, y + yoffset, color)
  315. end
  316. function DocView:draw()
  317. self:draw_background(style.background)
  318. local font = self:get_font()
  319. font:set_tab_width(font:get_width(" ") * config.indent_size)
  320. local minline, maxline = self:get_visible_line_range()
  321. local lh = self:get_line_height()
  322. local _, y = self:get_line_screen_position(minline)
  323. local x = self:get_content_offset() + style.padding.x
  324. for i = minline, maxline do
  325. self:draw_gutter_text(i, x, y)
  326. y = y + lh
  327. end
  328. local x, y = self:get_line_screen_position(minline)
  329. local gw = self:get_gutter_width()
  330. local pos = self.position
  331. core.push_clip_rect(pos.x + gw, pos.y, self.size.x, self.size.y)
  332. for i = minline, maxline do
  333. self:draw_line_text(i, x, y)
  334. y = y + lh
  335. end
  336. core.pop_clip_rect()
  337. self:draw_scrollbar()
  338. end
  339. return DocView