text.lua 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036
  1. -- text editor, particularly text drawing, horizontal wrap, vertical scrolling
  2. Text = {}
  3. -- draw a line starting from startpos to screen at y between State.left and State.right
  4. -- return y for the next line, and position of start of final screen line drawn
  5. function Text.draw(State, line_index, y, startpos)
  6. --? print('text.draw', line_index, y)
  7. local line = State.lines[line_index]
  8. local line_cache = State.line_cache[line_index]
  9. line_cache.starty = y
  10. line_cache.startpos = startpos
  11. -- wrap long lines
  12. local final_screen_line_starting_pos = startpos -- track value to return
  13. Text.populate_screen_line_starting_pos(State, line_index)
  14. assert(#line_cache.screen_line_starting_pos >= 1, 'line cache missing screen line info')
  15. for i=1,#line_cache.screen_line_starting_pos do
  16. local pos = line_cache.screen_line_starting_pos[i]
  17. if pos < startpos then
  18. -- render nothing
  19. else
  20. final_screen_line_starting_pos = pos
  21. local screen_line = Text.screen_line(line, line_cache, i)
  22. --? print('text.draw:', screen_line, 'at', line_index,pos, 'after', x,y)
  23. local frag_len = utf8.len(screen_line)
  24. -- render any highlights
  25. if State.selection1.line then
  26. local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)
  27. Text.draw_highlight(State, line, State.left,y, pos, lo,hi)
  28. end
  29. if line_index == State.cursor1.line then
  30. -- render search highlight or cursor
  31. if State.search_term then
  32. local data = State.lines[State.cursor1.line].data
  33. local cursor_offset = Text.offset(data, State.cursor1.pos)
  34. if data:sub(cursor_offset, cursor_offset+#State.search_term-1) == State.search_term then
  35. local save_selection = State.selection1
  36. State.selection1 = {line=line_index, pos=State.cursor1.pos+utf8.len(State.search_term)}
  37. local lo, hi = Text.clip_selection(State, line_index, pos, pos+frag_len)
  38. Text.draw_highlight(State, line, State.left,y, pos, lo,hi)
  39. State.selection1 = save_selection
  40. end
  41. else
  42. if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
  43. Text.draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y)
  44. elseif pos + frag_len == State.cursor1.pos then
  45. -- Show cursor at end of line.
  46. -- This place also catches end of wrapping screen lines. That doesn't seem worth distinguishing.
  47. -- It seems useful to see a cursor whether your eye is on the left or right margin.
  48. Text.draw_cursor(State, State.left+Text.x(State.font, screen_line, State.cursor1.pos-pos+1), y)
  49. end
  50. end
  51. end
  52. -- render fragment
  53. App.color(Text_color)
  54. App.screen.print(screen_line, State.left,y)
  55. y = y + State.line_height
  56. if y >= App.screen.height then
  57. break
  58. end
  59. end
  60. end
  61. return y, final_screen_line_starting_pos
  62. end
  63. function Text.screen_line(line, line_cache, i)
  64. local pos = line_cache.screen_line_starting_pos[i]
  65. local offset = Text.offset(line.data, pos)
  66. if i >= #line_cache.screen_line_starting_pos then
  67. return line.data:sub(offset)
  68. end
  69. local endpos = line_cache.screen_line_starting_pos[i+1]-1
  70. local end_offset = Text.offset(line.data, endpos)
  71. return line.data:sub(offset, end_offset)
  72. end
  73. function Text.draw_cursor(State, x, y)
  74. -- blink every 0.5s
  75. if math.floor(Cursor_time*2)%2 == 0 then
  76. App.color(Cursor_color)
  77. love.graphics.rectangle('fill', x,y, 3,State.line_height)
  78. end
  79. State.cursor_x = x
  80. State.cursor_y = y+State.line_height
  81. end
  82. function Text.populate_screen_line_starting_pos(State, line_index)
  83. local line = State.lines[line_index]
  84. if line.mode ~= 'text' then return end
  85. local line_cache = State.line_cache[line_index]
  86. if line_cache.screen_line_starting_pos then
  87. return
  88. end
  89. line_cache.screen_line_starting_pos = {1}
  90. local x = 0
  91. local pos = 1
  92. -- try to wrap at word boundaries
  93. for frag in line.data:gmatch('%S*%s*') do
  94. local frag_width = State.font:getWidth(frag)
  95. --? print('-- frag:', frag, pos, x, frag_width, State.width)
  96. while x + frag_width > State.width do
  97. --? print('frag:', frag, pos, x, frag_width, State.width)
  98. if x < 0.8 * State.width then
  99. -- long word; chop it at some letter
  100. -- We're not going to reimplement TeX here.
  101. local bpos = Text.nearest_pos_less_than(State.font, frag, State.width - x)
  102. if x == 0 and bpos == 0 then
  103. assert(false, ("Infinite loop while line-wrapping. Editor is %dpx wide; window is %dpx wide"):format(State.width, App.screen.width))
  104. end
  105. pos = pos + bpos
  106. local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
  107. frag = string.sub(frag, boffset)
  108. --? if bpos > 0 then
  109. --? print('after chop:', frag)
  110. --? end
  111. frag_width = State.font:getWidth(frag)
  112. end
  113. --? print('screen line:', pos)
  114. table.insert(line_cache.screen_line_starting_pos, pos)
  115. x = 0 -- new screen line
  116. end
  117. x = x + frag_width
  118. pos = pos + utf8.len(frag)
  119. end
  120. end
  121. function Text.text_input(State, t)
  122. if App.mouse_down(1) then return end
  123. if App.any_modifier_down() then
  124. if App.key_down(t) then
  125. -- The modifiers didn't change the key. Handle it in keychord_pressed.
  126. return
  127. else
  128. -- Key mutated by the keyboard layout. Continue below.
  129. end
  130. end
  131. local before = snapshot(State, State.cursor1.line)
  132. --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  133. Text.insert_at_cursor(State, t)
  134. if State.cursor_y > App.screen.height - State.line_height then
  135. Text.populate_screen_line_starting_pos(State, State.cursor1.line)
  136. Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
  137. end
  138. record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
  139. end
  140. function Text.insert_at_cursor(State, t)
  141. assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
  142. local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
  143. State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
  144. Text.clear_screen_line_cache(State, State.cursor1.line)
  145. State.cursor1.pos = State.cursor1.pos+1
  146. end
  147. -- Don't handle any keys here that would trigger text_input above.
  148. function Text.keychord_press(State, chord)
  149. --? print('chord', chord, State.selection1.line, State.selection1.pos)
  150. --== shortcuts that mutate text
  151. if chord == 'return' then
  152. local before_line = State.cursor1.line
  153. local before = snapshot(State, before_line)
  154. Text.insert_return(State)
  155. State.selection1 = {}
  156. if State.cursor_y > App.screen.height - State.line_height then
  157. Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
  158. end
  159. schedule_save(State)
  160. record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
  161. elseif chord == 'tab' then
  162. local before = snapshot(State, State.cursor1.line)
  163. --? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  164. Text.insert_at_cursor(State, '\t')
  165. if State.cursor_y > App.screen.height - State.line_height then
  166. Text.populate_screen_line_starting_pos(State, State.cursor1.line)
  167. Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
  168. --? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  169. end
  170. schedule_save(State)
  171. record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
  172. elseif chord == 'backspace' then
  173. if State.selection1.line then
  174. Text.delete_selection(State, State.left, State.right)
  175. schedule_save(State)
  176. return
  177. end
  178. local before
  179. if State.cursor1.pos > 1 then
  180. before = snapshot(State, State.cursor1.line)
  181. local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1)
  182. local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
  183. if byte_start then
  184. if byte_end then
  185. State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
  186. else
  187. State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
  188. end
  189. State.cursor1.pos = State.cursor1.pos-1
  190. end
  191. elseif State.cursor1.line > 1 then
  192. before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
  193. if State.lines[State.cursor1.line-1].mode == 'drawing' then
  194. table.remove(State.lines, State.cursor1.line-1)
  195. table.remove(State.line_cache, State.cursor1.line-1)
  196. else
  197. -- join lines
  198. State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
  199. State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
  200. table.remove(State.lines, State.cursor1.line)
  201. table.remove(State.line_cache, State.cursor1.line)
  202. end
  203. State.cursor1.line = State.cursor1.line-1
  204. end
  205. if State.screen_top1.line > #State.lines then
  206. Text.populate_screen_line_starting_pos(State, #State.lines)
  207. local line_cache = State.line_cache[#State.line_cache]
  208. State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}
  209. elseif Text.lt1(State.cursor1, State.screen_top1) then
  210. State.screen_top1 = {
  211. line=State.cursor1.line,
  212. pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
  213. }
  214. Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
  215. end
  216. Text.clear_screen_line_cache(State, State.cursor1.line)
  217. assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))
  218. schedule_save(State)
  219. record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
  220. elseif chord == 'delete' then
  221. if State.selection1.line then
  222. Text.delete_selection(State, State.left, State.right)
  223. schedule_save(State)
  224. return
  225. end
  226. local before
  227. if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
  228. before = snapshot(State, State.cursor1.line)
  229. else
  230. before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
  231. end
  232. if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
  233. local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
  234. local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)
  235. if byte_start then
  236. if byte_end then
  237. State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)
  238. else
  239. State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
  240. end
  241. -- no change to State.cursor1.pos
  242. end
  243. elseif State.cursor1.line < #State.lines then
  244. if State.lines[State.cursor1.line+1].mode == 'text' then
  245. -- join lines
  246. State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
  247. end
  248. table.remove(State.lines, State.cursor1.line+1)
  249. table.remove(State.line_cache, State.cursor1.line+1)
  250. end
  251. Text.clear_screen_line_cache(State, State.cursor1.line)
  252. schedule_save(State)
  253. record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
  254. --== shortcuts that move the cursor
  255. elseif chord == 'left' then
  256. Text.left(State)
  257. State.selection1 = {}
  258. elseif chord == 'right' then
  259. Text.right(State)
  260. State.selection1 = {}
  261. elseif chord == 'S-left' then
  262. if State.selection1.line == nil then
  263. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  264. end
  265. Text.left(State)
  266. elseif chord == 'S-right' then
  267. if State.selection1.line == nil then
  268. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  269. end
  270. Text.right(State)
  271. -- C- hotkeys reserved for drawings, so we'll use M-
  272. elseif chord == 'M-left' then
  273. Text.word_left(State)
  274. State.selection1 = {}
  275. elseif chord == 'M-right' then
  276. Text.word_right(State)
  277. State.selection1 = {}
  278. elseif chord == 'M-S-left' then
  279. if State.selection1.line == nil then
  280. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  281. end
  282. Text.word_left(State)
  283. elseif chord == 'M-S-right' then
  284. if State.selection1.line == nil then
  285. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  286. end
  287. Text.word_right(State)
  288. elseif chord == 'home' then
  289. Text.start_of_line(State)
  290. State.selection1 = {}
  291. elseif chord == 'end' then
  292. Text.end_of_line(State)
  293. State.selection1 = {}
  294. elseif chord == 'S-home' then
  295. if State.selection1.line == nil then
  296. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  297. end
  298. Text.start_of_line(State)
  299. elseif chord == 'S-end' then
  300. if State.selection1.line == nil then
  301. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  302. end
  303. Text.end_of_line(State)
  304. elseif chord == 'up' then
  305. Text.up(State)
  306. State.selection1 = {}
  307. elseif chord == 'down' then
  308. Text.down(State)
  309. State.selection1 = {}
  310. elseif chord == 'S-up' then
  311. if State.selection1.line == nil then
  312. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  313. end
  314. Text.up(State)
  315. elseif chord == 'S-down' then
  316. if State.selection1.line == nil then
  317. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  318. end
  319. Text.down(State)
  320. elseif chord == 'pageup' then
  321. Text.pageup(State)
  322. State.selection1 = {}
  323. elseif chord == 'pagedown' then
  324. Text.pagedown(State)
  325. State.selection1 = {}
  326. elseif chord == 'S-pageup' then
  327. if State.selection1.line == nil then
  328. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  329. end
  330. Text.pageup(State)
  331. elseif chord == 'S-pagedown' then
  332. if State.selection1.line == nil then
  333. State.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}
  334. end
  335. Text.pagedown(State)
  336. end
  337. end
  338. function Text.insert_return(State)
  339. local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
  340. table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})
  341. table.insert(State.line_cache, State.cursor1.line+1, {})
  342. State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
  343. Text.clear_screen_line_cache(State, State.cursor1.line)
  344. State.cursor1 = {line=State.cursor1.line+1, pos=1}
  345. end
  346. function Text.pageup(State)
  347. --? print('pageup')
  348. -- duplicate some logic from love.draw
  349. local top2 = Text.to2(State, State.screen_top1)
  350. --? print(App.screen.height)
  351. local y = App.screen.height - State.line_height
  352. while y >= State.top do
  353. --? print(y, top2.line, top2.screen_line, top2.screen_pos)
  354. if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end
  355. if State.lines[State.screen_top1.line].mode == 'text' then
  356. y = y - State.line_height
  357. elseif State.lines[State.screen_top1.line].mode == 'drawing' then
  358. y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
  359. end
  360. top2 = Text.previous_screen_line(State, top2)
  361. end
  362. State.screen_top1 = Text.to1(State, top2)
  363. State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
  364. Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
  365. --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
  366. --? print('pageup end')
  367. end
  368. function Text.pagedown(State)
  369. --? print('pagedown')
  370. -- If a line/paragraph gets to a page boundary, I often want to scroll
  371. -- before I get to the bottom.
  372. -- However, only do this if it makes forward progress.
  373. local bot2 = Text.to2(State, State.screen_bottom1)
  374. if bot2.screen_line > 1 then
  375. bot2.screen_line = math.max(bot2.screen_line-10, 1)
  376. end
  377. local new_top1 = Text.to1(State, bot2)
  378. if Text.lt1(State.screen_top1, new_top1) then
  379. State.screen_top1 = new_top1
  380. else
  381. State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
  382. end
  383. --? print('setting top to', State.screen_top1.line, State.screen_top1.pos)
  384. State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
  385. Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
  386. --? print('top now', State.screen_top1.line)
  387. Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
  388. --? print('pagedown end')
  389. end
  390. function Text.up(State)
  391. assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
  392. --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)
  393. local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
  394. if screen_line_starting_pos == 1 then
  395. --? print('cursor is at first screen line of its line')
  396. -- line is done; skip to previous text line
  397. local new_cursor_line = State.cursor1.line
  398. while new_cursor_line > 1 do
  399. new_cursor_line = new_cursor_line-1
  400. if State.lines[new_cursor_line].mode == 'text' then
  401. --? print('found previous text line')
  402. State.cursor1 = {line=new_cursor_line, pos=nil}
  403. Text.populate_screen_line_starting_pos(State, State.cursor1.line)
  404. -- previous text line found, pick its final screen line
  405. --? print('has multiple screen lines')
  406. local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos
  407. --? print(#screen_line_starting_pos)
  408. screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]
  409. local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)
  410. local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)
  411. State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
  412. break
  413. end
  414. end
  415. else
  416. -- move up one screen line in current line
  417. assert(screen_line_index > 1, 'bumped up against top screen line in line')
  418. local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]
  419. local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
  420. local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
  421. State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
  422. --? print('cursor pos is now '..tostring(State.cursor1.pos))
  423. end
  424. if Text.lt1(State.cursor1, State.screen_top1) then
  425. State.screen_top1 = {
  426. line=State.cursor1.line,
  427. pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
  428. }
  429. Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
  430. end
  431. end
  432. function Text.down(State)
  433. assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
  434. --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  435. assert(State.cursor1.pos, 'cursor has no pos')
  436. if Text.cursor_at_final_screen_line(State) then
  437. -- line is done, skip to next text line
  438. --? print('cursor at final screen line of its line')
  439. local new_cursor_line = State.cursor1.line
  440. while new_cursor_line < #State.lines do
  441. new_cursor_line = new_cursor_line+1
  442. if State.lines[new_cursor_line].mode == 'text' then
  443. State.cursor1 = {
  444. line = new_cursor_line,
  445. pos = Text.nearest_cursor_pos(State.font, State.lines[new_cursor_line].data, State.cursor_x, State.left),
  446. }
  447. --? print(State.cursor1.pos)
  448. break
  449. end
  450. end
  451. if State.cursor1.line > State.screen_bottom1.line then
  452. --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
  453. --? print('scroll up preserving cursor')
  454. Text.snap_cursor_to_bottom_of_screen(State)
  455. --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
  456. end
  457. else
  458. -- move down one screen line in current line
  459. local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
  460. --? print('cursor is NOT at final screen line of its line')
  461. local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
  462. Text.populate_screen_line_starting_pos(State, State.cursor1.line)
  463. local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1]
  464. --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
  465. local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
  466. local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
  467. State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
  468. --? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)
  469. if scroll_down then
  470. --? print('scroll up preserving cursor')
  471. Text.snap_cursor_to_bottom_of_screen(State)
  472. --? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
  473. end
  474. end
  475. --? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  476. end
  477. function Text.start_of_line(State)
  478. State.cursor1.pos = 1
  479. if Text.lt1(State.cursor1, State.screen_top1) then
  480. State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy
  481. end
  482. end
  483. function Text.end_of_line(State)
  484. State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
  485. if Text.cursor_out_of_screen(State) then
  486. Text.snap_cursor_to_bottom_of_screen(State)
  487. end
  488. end
  489. function Text.word_left(State)
  490. -- skip some whitespace
  491. while true do
  492. if State.cursor1.pos == 1 then
  493. break
  494. end
  495. if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
  496. break
  497. end
  498. Text.left(State)
  499. end
  500. -- skip some non-whitespace
  501. while true do
  502. Text.left(State)
  503. if State.cursor1.pos == 1 then
  504. break
  505. end
  506. assert(State.cursor1.pos > 1, 'bumped up against start of line')
  507. if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
  508. break
  509. end
  510. end
  511. end
  512. function Text.word_right(State)
  513. -- skip some whitespace
  514. while true do
  515. if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
  516. break
  517. end
  518. if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
  519. break
  520. end
  521. Text.right_without_scroll(State)
  522. end
  523. while true do
  524. Text.right_without_scroll(State)
  525. if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
  526. break
  527. end
  528. if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
  529. break
  530. end
  531. end
  532. if Text.cursor_out_of_screen(State) then
  533. Text.snap_cursor_to_bottom_of_screen(State)
  534. end
  535. end
  536. function Text.match(s, pos, pat)
  537. local start_offset = Text.offset(s, pos)
  538. local end_offset = Text.offset(s, pos+1)
  539. assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))
  540. local curr = s:sub(start_offset, end_offset-1)
  541. return curr:match(pat)
  542. end
  543. function Text.left(State)
  544. assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
  545. if State.cursor1.pos > 1 then
  546. State.cursor1.pos = State.cursor1.pos-1
  547. else
  548. local new_cursor_line = State.cursor1.line
  549. while new_cursor_line > 1 do
  550. new_cursor_line = new_cursor_line-1
  551. if State.lines[new_cursor_line].mode == 'text' then
  552. State.cursor1 = {
  553. line = new_cursor_line,
  554. pos = utf8.len(State.lines[new_cursor_line].data) + 1,
  555. }
  556. break
  557. end
  558. end
  559. end
  560. if Text.lt1(State.cursor1, State.screen_top1) then
  561. State.screen_top1 = {
  562. line=State.cursor1.line,
  563. pos=Text.pos_at_start_of_screen_line(State, State.cursor1),
  564. }
  565. Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
  566. end
  567. end
  568. function Text.right(State)
  569. Text.right_without_scroll(State)
  570. if Text.cursor_out_of_screen(State) then
  571. Text.snap_cursor_to_bottom_of_screen(State)
  572. end
  573. end
  574. function Text.right_without_scroll(State)
  575. assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
  576. if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
  577. State.cursor1.pos = State.cursor1.pos+1
  578. else
  579. local new_cursor_line = State.cursor1.line
  580. while new_cursor_line <= #State.lines-1 do
  581. new_cursor_line = new_cursor_line+1
  582. if State.lines[new_cursor_line].mode == 'text' then
  583. State.cursor1 = {line=new_cursor_line, pos=1}
  584. break
  585. end
  586. end
  587. end
  588. end
  589. -- result: pos, index of screen line
  590. function Text.pos_at_start_of_screen_line(State, loc1)
  591. Text.populate_screen_line_starting_pos(State, loc1.line)
  592. local line_cache = State.line_cache[loc1.line]
  593. for i=#line_cache.screen_line_starting_pos,1,-1 do
  594. local spos = line_cache.screen_line_starting_pos[i]
  595. if spos <= loc1.pos then
  596. return spos,i
  597. end
  598. end
  599. assert(false, ('invalid pos %d'):format(loc1.pos))
  600. end
  601. function Text.pos_at_end_of_screen_line(State, loc1)
  602. Text.populate_screen_line_starting_pos(State, loc1.line)
  603. local line_cache = State.line_cache[loc1.line]
  604. local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1
  605. for i=#line_cache.screen_line_starting_pos,1,-1 do
  606. local spos = line_cache.screen_line_starting_pos[i]
  607. if spos <= loc1.pos then
  608. return most_recent_final_pos
  609. end
  610. most_recent_final_pos = spos-1
  611. end
  612. assert(false, ('invalid pos %d'):format(loc1.pos))
  613. end
  614. function Text.cursor_at_final_screen_line(State)
  615. Text.populate_screen_line_starting_pos(State, State.cursor1.line)
  616. local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos
  617. --? print(screen_lines[#screen_lines], State.cursor1.pos)
  618. return screen_lines[#screen_lines] <= State.cursor1.pos
  619. end
  620. function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
  621. local y = State.top
  622. while State.cursor1.line <= #State.lines do
  623. if State.lines[State.cursor1.line].mode == 'text' then
  624. break
  625. end
  626. --? print('cursor skips', State.cursor1.line)
  627. y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width)
  628. State.cursor1.line = State.cursor1.line + 1
  629. end
  630. if State.cursor1.pos == nil then
  631. State.cursor1.pos = 1
  632. end
  633. -- hack: insert a text line at bottom of file if necessary
  634. if State.cursor1.line > #State.lines then
  635. assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')
  636. table.insert(State.lines, {mode='text', data=''})
  637. table.insert(State.line_cache, {})
  638. end
  639. --? print(y, App.screen.height, App.screen.height-State.line_height)
  640. if y > App.screen.height - State.line_height then
  641. --? print('scroll up')
  642. Text.snap_cursor_to_bottom_of_screen(State)
  643. end
  644. end
  645. -- should never modify State.cursor1
  646. function Text.snap_cursor_to_bottom_of_screen(State)
  647. --? print('to2:', State.cursor1.line, State.cursor1.pos)
  648. local top2 = Text.to2(State, State.cursor1)
  649. --? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
  650. -- slide to start of screen line
  651. top2.screen_pos = 1 -- start of screen line
  652. --? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  653. --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
  654. local y = App.screen.height - State.line_height
  655. -- duplicate some logic from love.draw
  656. while true do
  657. --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
  658. if top2.line == 1 and top2.screen_line == 1 then break end
  659. if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then
  660. local h = State.line_height
  661. if y - h < State.top then
  662. break
  663. end
  664. y = y - h
  665. else
  666. assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')
  667. assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")
  668. -- We currently can't draw partial drawings, so either skip it entirely
  669. -- or not at all.
  670. local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
  671. if y - h < State.top then
  672. break
  673. end
  674. --? print('skipping drawing of height', h)
  675. y = y - h
  676. end
  677. top2 = Text.previous_screen_line(State, top2)
  678. end
  679. --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)
  680. State.screen_top1 = Text.to1(State, top2)
  681. --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)
  682. --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
  683. Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
  684. end
  685. function Text.in_line(State, line_index, x,y)
  686. local line = State.lines[line_index]
  687. local line_cache = State.line_cache[line_index]
  688. if line_cache.starty == nil then return false end -- outside current page
  689. if y < line_cache.starty then return false end
  690. Text.populate_screen_line_starting_pos(State, line_index)
  691. return y < line_cache.starty + State.line_height*(#line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1)
  692. end
  693. -- convert mx,my in pixels to schema-1 coordinates
  694. function Text.to_pos_on_line(State, line_index, mx, my)
  695. local line = State.lines[line_index]
  696. local line_cache = State.line_cache[line_index]
  697. assert(my >= line_cache.starty, 'failed to map y pixel to line')
  698. -- duplicate some logic from Text.draw
  699. local y = line_cache.starty
  700. local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
  701. for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
  702. local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
  703. local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
  704. --? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
  705. local nexty = y + State.line_height
  706. if my < nexty then
  707. -- On all wrapped screen lines but the final one, clicks past end of
  708. -- line position cursor on final character of screen line.
  709. -- (The final screen line positions past end of screen line as always.)
  710. if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
  711. --? print('past end of non-final line; return')
  712. return line_cache.screen_line_starting_pos[screen_line_index+1]
  713. end
  714. local s = string.sub(line.data, screen_line_starting_byte_offset)
  715. --? print('return', mx, Text.nearest_cursor_pos(State.font, s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1)
  716. return screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1
  717. end
  718. y = nexty
  719. end
  720. assert(false, 'failed to map y pixel to line')
  721. end
  722. function Text.screen_line_width(State, line_index, i)
  723. local line = State.lines[line_index]
  724. local line_cache = State.line_cache[line_index]
  725. local start_pos = line_cache.screen_line_starting_pos[i]
  726. local start_offset = Text.offset(line.data, start_pos)
  727. local screen_line
  728. if i < #line_cache.screen_line_starting_pos then
  729. local past_end_pos = line_cache.screen_line_starting_pos[i+1]
  730. local past_end_offset = Text.offset(line.data, past_end_pos)
  731. screen_line = string.sub(line.data, start_offset, past_end_offset-1)
  732. else
  733. screen_line = string.sub(line.data, start_pos)
  734. end
  735. return State.font:getWidth(screen_line)
  736. end
  737. function Text.screen_line_index(screen_line_starting_pos, pos)
  738. for i = #screen_line_starting_pos,1,-1 do
  739. if screen_line_starting_pos[i] <= pos then
  740. return i
  741. end
  742. end
  743. end
  744. -- convert x pixel coordinate to pos
  745. -- oblivious to wrapping
  746. -- result: 1 to len+1
  747. function Text.nearest_cursor_pos(font, line, x, left)
  748. if x < left then
  749. return 1
  750. end
  751. local len = utf8.len(line)
  752. local max_x = left+Text.x(font, line, len+1)
  753. if x > max_x then
  754. return len+1
  755. end
  756. local leftpos, rightpos = 1, len+1
  757. --? print('-- nearest', x)
  758. while true do
  759. --? print('nearest', x, '^'..line..'$', leftpos, rightpos)
  760. if leftpos == rightpos then
  761. return leftpos
  762. end
  763. local curr = math.floor((leftpos+rightpos)/2)
  764. local currxmin = left+Text.x(font, line, curr)
  765. local currxmax = left+Text.x(font, line, curr+1)
  766. --? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
  767. if currxmin <= x and x < currxmax then
  768. if x-currxmin < currxmax-x then
  769. return curr
  770. else
  771. return curr+1
  772. end
  773. end
  774. if leftpos >= rightpos-1 then
  775. return rightpos
  776. end
  777. if currxmin > x then
  778. rightpos = curr
  779. else
  780. leftpos = curr
  781. end
  782. end
  783. assert(false, 'failed to map x pixel to pos')
  784. end
  785. -- return the nearest index of line (in utf8 code points) which lies entirely
  786. -- within x pixels of the left margin
  787. -- result: 0 to len+1
  788. function Text.nearest_pos_less_than(font, line, x)
  789. --? print('', '-- nearest_pos_less_than', line, x)
  790. local len = utf8.len(line)
  791. local max_x = Text.x_after(font, line, len)
  792. if x > max_x then
  793. return len+1
  794. end
  795. local left, right = 0, len+1
  796. while true do
  797. local curr = math.floor((left+right)/2)
  798. local currxmin = Text.x_after(font, line, curr+1)
  799. local currxmax = Text.x_after(font, line, curr+2)
  800. --? print('', x, left, right, curr, currxmin, currxmax)
  801. if currxmin <= x and x < currxmax then
  802. return curr
  803. end
  804. if left >= right-1 then
  805. return left
  806. end
  807. if currxmin > x then
  808. right = curr
  809. else
  810. left = curr
  811. end
  812. end
  813. assert(false, 'failed to map x pixel to pos')
  814. end
  815. function Text.x_after(font, s, pos)
  816. local len = utf8.len(s)
  817. local offset = Text.offset(s, math.min(pos+1, len+1))
  818. local s_before = s:sub(1, offset-1)
  819. --? print('^'..s_before..'$')
  820. return font:getWidth(s_before)
  821. end
  822. function Text.x(font, s, pos)
  823. local offset = Text.offset(s, pos)
  824. local s_before = s:sub(1, offset-1)
  825. return font:getWidth(s_before)
  826. end
  827. function Text.to2(State, loc1)
  828. if State.lines[loc1.line].mode == 'drawing' then
  829. return {line=loc1.line, screen_line=1, screen_pos=1}
  830. end
  831. local result = {line=loc1.line}
  832. local line_cache = State.line_cache[loc1.line]
  833. Text.populate_screen_line_starting_pos(State, loc1.line)
  834. for i=#line_cache.screen_line_starting_pos,1,-1 do
  835. local spos = line_cache.screen_line_starting_pos[i]
  836. if spos <= loc1.pos then
  837. result.screen_line = i
  838. result.screen_pos = loc1.pos - spos + 1
  839. break
  840. end
  841. end
  842. assert(result.screen_pos, 'failed to convert schema-1 coordinate to schema-2')
  843. return result
  844. end
  845. function Text.to1(State, loc2)
  846. local result = {line=loc2.line, pos=loc2.screen_pos}
  847. if loc2.screen_line > 1 then
  848. result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
  849. end
  850. return result
  851. end
  852. function Text.eq1(a, b)
  853. return a.line == b.line and a.pos == b.pos
  854. end
  855. function Text.lt1(a, b)
  856. if a.line < b.line then
  857. return true
  858. end
  859. if a.line > b.line then
  860. return false
  861. end
  862. return a.pos < b.pos
  863. end
  864. function Text.le1(a, b)
  865. if a.line < b.line then
  866. return true
  867. end
  868. if a.line > b.line then
  869. return false
  870. end
  871. return a.pos <= b.pos
  872. end
  873. function Text.offset(s, pos1)
  874. if pos1 == 1 then return 1 end
  875. local result = utf8.offset(s, pos1)
  876. if result == nil then
  877. assert(false, ('Text.offset(%d) called on a string of length %d (byte size %d); this is likely a failure to handle utf8\n\n^%s$\n'):format(pos1, utf8.len(s), #s, s))
  878. end
  879. return result
  880. end
  881. function Text.previous_screen_line(State, loc2)
  882. if loc2.screen_line > 1 then
  883. return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
  884. elseif loc2.line == 1 then
  885. return loc2
  886. elseif State.lines[loc2.line-1].mode == 'drawing' then
  887. return {line=loc2.line-1, screen_line=1, screen_pos=1}
  888. else
  889. local l = State.lines[loc2.line-1]
  890. Text.populate_screen_line_starting_pos(State, loc2.line-1)
  891. return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
  892. end
  893. end
  894. -- resize helper
  895. function Text.tweak_screen_top_and_cursor(State)
  896. if State.screen_top1.pos == 1 then return end
  897. Text.populate_screen_line_starting_pos(State, State.screen_top1.line)
  898. local line = State.lines[State.screen_top1.line]
  899. local line_cache = State.line_cache[State.screen_top1.line]
  900. for i=2,#line_cache.screen_line_starting_pos do
  901. local pos = line_cache.screen_line_starting_pos[i]
  902. if pos == State.screen_top1.pos then
  903. break
  904. end
  905. if pos > State.screen_top1.pos then
  906. -- make sure screen top is at start of a screen line
  907. local prev = line_cache.screen_line_starting_pos[i-1]
  908. if State.screen_top1.pos - prev < pos - State.screen_top1.pos then
  909. State.screen_top1.pos = prev
  910. else
  911. State.screen_top1.pos = pos
  912. end
  913. break
  914. end
  915. end
  916. -- make sure cursor is on screen
  917. if Text.lt1(State.cursor1, State.screen_top1) then
  918. State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
  919. elseif State.cursor1.line >= State.screen_bottom1.line then
  920. --? print('too low')
  921. if Text.cursor_out_of_screen(State) then
  922. --? print('tweak')
  923. State.cursor1 = {
  924. line=State.screen_bottom1.line,
  925. pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5),
  926. }
  927. end
  928. end
  929. end
  930. -- slightly expensive since it redraws the screen
  931. function Text.cursor_out_of_screen(State)
  932. edit.draw(State)
  933. return State.cursor_y == nil
  934. -- this approach is cheaper and almost works, except on the final screen
  935. -- where file ends above bottom of screen
  936. --? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)
  937. --? local botline1 = {line=State.cursor1.line, pos=botpos}
  938. --? return Text.lt1(State.screen_bottom1, botline1)
  939. end
  940. function Text.redraw_all(State)
  941. --? print('clearing fragments')
  942. -- Perform some early sanity checking here, in hopes that we correctly call
  943. -- this whenever we change editor state.
  944. if State.right <= State.left then
  945. assert(false, ('Right margin %d must be to the right of the left margin %d'):format(State.right, State.left))
  946. end
  947. State.line_cache = {}
  948. for i=1,#State.lines do
  949. State.line_cache[i] = {}
  950. end
  951. end
  952. function Text.clear_screen_line_cache(State, line_index)
  953. State.line_cache[line_index].screen_line_starting_pos = nil
  954. end
  955. function trim(s)
  956. return s:gsub('^%s+', ''):gsub('%s+$', '')
  957. end
  958. function ltrim(s)
  959. return s:gsub('^%s+', '')
  960. end
  961. function rtrim(s)
  962. return s:gsub('%s+$', '')
  963. end
  964. function starts_with(s, prefix)
  965. if #s < #prefix then
  966. return false
  967. end
  968. for i=1,#prefix do
  969. if s:sub(i,i) ~= prefix:sub(i,i) then
  970. return false
  971. end
  972. end
  973. return true
  974. end
  975. function ends_with(s, suffix)
  976. if #s < #suffix then
  977. return false
  978. end
  979. for i=0,#suffix-1 do
  980. if s:sub(#s-i,#s-i) ~= suffix:sub(#suffix-i,#suffix-i) then
  981. return false
  982. end
  983. end
  984. return true
  985. end