ICUW7F3XQLURK4LSNPH5E3NDEFSRHKATEUHH2UPFJTMHYR3ZJF3QC UCSEVQFJJUGB64R4SHLJW4AXQA2VYPJBLV6KLOMTFLOXZ7CC72RAC F5X3ARQXJS5MZNVOEPS2CSQGUY5UXK2NVHXVAK7PTTRSM22Q6ICQC 4QCR4O2V63WVOTGBWRP6AQVAF4QQAL5VQMEBA26GJMB6LJXXDLCQC O36QMO7JHZZCAU7SAMXXW77NV4D7X2BXUXL56C6WXVHUOU7C5PGQC CHEHZV5JUFZD4WMHFAXNKFMVFSAA4Y4MMWLRBFDJBHAZUFRKOD5AC Z6ITQSTTHQZMTZDDP3SDJCTGIGY4DXTEGYTYN6JCRAPMB7KDHJZAC D76IPVZBVKA53JJVNGTA2AQSRLIJY6W5RBU4PTUPR7IGL7BYNZCQC ZLJYLPOTXIVBVWJ4NTRM2YCQPT2FCSN7446P56MJFEFY45QTB7IAC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC 2R6UORLNH722O3OOQMXEMZNRSRW2JCN5OBVYJZMIA6AIJCKLJ24QC VIGITGW5FAYYF4PDK6XAPI4S43X4FMAMBH77VLGMKLNUY7JJHR4QC GG2553RBUKMCWIAD3EWJV7C2QH2BIS5V2E3BDTFSTYRHAPXGBF7QC VBU5YHLRO5ZSKFWBJRX7DWQGWPEHEWZMRRVV2WMWDJ54PKUNYCNQC RPGTBMMMEVBSUEGLWKVHNCBBNK24RNLOZVAVUCNI46VFBRVAZB6QC 3VHUIIATPOF7FXB7NTL5MESCV5BCQACII2D7QZ4UIUCBX3CWXMMAC 73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC LLQC2M2IMEJBJQXZTKC3OAKG5WKHSERXKAKCYHQRUZZD6CVRIHAQC LXTTOB33N2HCUZFIUDRQGGBVHK2HODRG4NBLH6RXRQZDCHF27BSAC LF7BWEG4DKQI7NMXMZC4LC2BE5PB42HK5PD6OYBNIDMAZBJASOKQC I64IPGJXWRTGHHVAYJUBUIWFR4BY6NM5P7TLTV4JOD7K4BVYDECQC BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC H2DPLWMVRFYTO2CQTG54FMT2LF3B6UKLXH32CUA22DNQJVP5XBNQC XPXYD64Y76UT24GENTC6IWYNYWRCTZZE55YCBGWJT7YAYHIFZLXQC V5SYDHPQ7IKNLZZ3NJ24FDW3IG4O23AASLP2DTKOBPWUUZ5KUPOAC XUGDTYW2OALZNGX52BJXFYW2IJ6YSXA62ANG2NX2KDWULYAPZYOAC HALS7E5UGKCP3DFY456F7Z3Y6WNGIABOCV2SHT34D5ZAGNCPV5PQC S2QMLRXLULVA6M73YVC5VQJ2SYUBHG2DTGXEMOBKEPR4OU7ZKG3AC MXA3RZYKUI4UF2ISY7JEF6VKX6NOPZMZH5SLLCZHRJKFIXXXDPSAC 6PCE7VUWEE5UVSE3ZZSQAMGUJDDHOBNO2MVD6POUWFP7UY7WOZHAC DHI6IJCNSTHGED67T6H5X6Y636C7PIDGIJD32HBEKLT5WIMRS5MAC HTZ3WRQHWJHMRR354RCPYK3L7OB5KRDJPJEL3Q4Y7IVNCYN2TSJQC MUJTM6REGQAK3LZTIFWGJRXE2UPCM4HSLXQYSF5ITLXLS6JCVPMQC XNVOMQKM6KBP26VNJQOWW3ZQC4VYXQFWMBKYUX77YXC5PTWDPYCAC JYVO76NCD5B2R3JL5MDOI4OQZFPVAU6FGMMINWYR7J4BZKHR33GAC TVZWTOE3T54QML767ZPFXBAFACGQ7CI4NIU63YZP5STFCWSAJ7CAC XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC GSIYZ7K25HG2BYKUF3G5JKI4M3MBWC2LRC5KJIHKG2JES6GYX5DAC Y36LOGR5X6S6AEJJ3EBVDC4I3RRFRCZ6GXPXSEIOMVXY2B7UCCGQC OYVFFWBK5IL7IPAF5HGFONJ2NEBRR3GTISPFROG7HJDEZYJAM7VQC PFT5Y2ZYGQA6XXOZ5HH75WVUGA4B3KTDRHSFOZRAUKTPSFOPMNRAC QYIFOHW3WDDQMK4ATY6IOSQRFHJOQ5QCPDKRC4GVGWLQEH4HGWVQC ISOFHXB2DX6IRN4HVBYWLADZM7QXQKRNAAS577G542KS4L6G5H3QC UPCIYZEUIFO2UJ3WPAFOD7VLNZEIIYYGJQGEMJOP5TSSE5PM4ZWAC IWYLK45KJSPRXKW55OD4GEPMLTYMMTXNFJJU26JTZN3RE35DWSCQC GL4Q5WCVMOBEKW7SMBKRSL3DRG2NSTXRI7VQFK77OXAWLBDKWTNQC VSBSWTE4IVQDRXLPQ7VTDIIEBEF7GMGRBHZ2IA73ZR6B2KZWI5JAC R5OKMVVCPAKL2IUMIY7A7ZMTJQZS6UWKW4EVLAVCPLPVNI5DCEYQC 3PSFWAILGRA4OYXWS2DX7VF332AIBPYBXHEA4GIQY2XEJVD65UMAC 3IGFCRNEH3FIUWUFVRDDTGP3SRNPEJCWB3ELVEK6R45PRYXDKNSAC F52PSTYSIIUD7UPEVLEJPVSBQU7IKVHM4GN4YASNSKFP4MRE6S6AC VHUNJHXB4BQISKAXTMVB66C4QKF2NUZQRPRB37Z7OADC7PW5IMRAC ATQO62TFDZ7J4RCOSB3K2QCCB5R6PNYQIIGNXTLZMEFG5UG5PUJQC 2TQUKHBC2EB3WDBD5UL62DQYV7CV6B7OJYK7CHOEDNOZENSOG42AC SAATXLBLJS4VRZAFGDCKO45AKVFLS7TNXN4SZLBNOSPE7SKGKB2AC FXI74QCLOZ4BS7UVZ3U2PE3LOL7MX3FWGHZCTGH3DYFXGTXVVIRAC HIKLULFQG7Q7L4C5KXR3DV3TBZ2RGWXBJJXIGSE5YQWF37AJOYZAC 4KC7I3E2DIKLIP7LQRKB5WFA2Z5XZXAU46RFHNFQU5BVEJPDX6UQC INVLLFPN46D7CXCPILG76LQMPALJHAPFJY3WJSL6IT3AMKJSNYWQC KEFZWDCOCLPTLSZJKRV4VYAHRITV5T33YKG2VGT332YAUCOBS3EAC JBS77AXN7VRCG7EWWKCL52Q7GAYIZ4TZDI5HD4DL7BJL7FXX2PFQC 4I2LMNEXDHZTOTDKBJXHSZRQDHIBDXQOYJDNGQSZYXRNHZGP7YPAC 2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC ILOA5BYFTQKBSHLFMMZUVPQ2JXBFJD62ERQFBTDK2WSRXUN525VQC PNHUONCTGKL3FJIGCGRKTJ532MAK7JXWIFAITS3M7C4YDHCAMTGAC GRSHSSV5ZRYOXD3SGMCOXKVFXLNZYLAN3YVBDUE6IVZBIK5S3UXQC 5E7BBR7HWGOEJJ6YWPL3NJKZVBTUZWET7OQNCRU2MMNVEGCEIAWQC O3HWKYBS66MT6JAPHNSNCFIXQAFZOIKPMLDQ4LXKO2W3M3IEF3YQC QSPYRABWJDUVTRV5HTCOC4FY3DGYPLQU4DJ2UDJLPIIJWMTPOA3AC 3SMSYTKNRPZXV5TFOVAKW7FUPPPDSTG6BWVT3ZAXB23YCH36MXCAC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC 4FL2EDGBMB7ORKXCHLSZTSMNDZELNV65M4OYHMFMU6FCSQFM3BMQC A4BSGS2CX4JK7IELL655EC6HAY6ILCWTGIHWZXHRGQOKU3HSUPLAC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC VDXOEDS757ISV5KM5AUFZDFH223CHQB4FK3TQOQ75CDDJAPLCFQQC R3KXFRZNL4CAT5OSKIIGWR3CHL2YJ5S4TKQDIPEHIJ2HW2WS46BQC source.initialize_window_geometry(App.width('m'))Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)
love.graphics.print(line.section_stack[i].name, x, App.screen.height-App.width(line.section_stack[i].name)-5, --[[vertically]] math.pi/2)endendreturn log_browser.right_margin(State, line)endfunction should_show(line)-- Show a line if every single section it's in is expanded.for i=1,#line.section_stack dolocal section = line.section_stack[i]if not section.expanded thenreturn falseendendreturn trueendfunction log_browser.left_margin(State, line)return State.left + #line.section_stack*Section_border_padding_horizontalendfunction log_browser.right_margin(State, line)return State.right - #line.section_stack*Section_border_padding_horizontalendfunction log_browser.update(State, dt)endfunction log_browser.quit(State)endlove.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-App.width(line.section_stack[i].name)-5, --[[vertically]] math.pi/2)endendreturn log_browser.left_margin(State, line)endfunction render_stack_right_margin(State, line_index, line, y)App.color(Section_border_color)for i=1,#line.section_stack dolocal x = State.right - (i-1)*Section_border_padding_horizontallove.graphics.line(x,y, x,y+log_browser.height(State, line_index))if y < 30 thenlove.graphics.print(line.section_stack[i].name, x, y+5, --[[vertically]] math.pi/2)endif y > App.screen.height-log_browser.height(State, line_index) thenlove.graphics.print(line.section_name, xleft+50,y)love.graphics.line(xleft+50+App.width(line.section_name)+2,sectiony, xright,sectiony)endelseif type(line.data) == 'string' thenlocal old_left, old_right = State.left,State.rightState.left,State.right = xleft,xrighty = Text.draw(State, line_index, y, --[[startpos]] 1)State.left,State.right = old_left,old_rightelseheight = log_render[line.data.name](line.data, xleft, y, xright-xleft)endendif App.mouse_x() > Log_browser_state.left and line_index == mouse_line_index thenApp.color(Cursor_line_background_color)love.graphics.rectangle('fill', xleft,y, xright-xleft, height)endy = y + heightendendendfunction render_stack_left_margin(State, line_index, line, y)if line.section_stack == nil then-- assertion messagefor k,v in pairs(line) doprint(k)endendApp.color(Section_border_color)for i=1,#line.section_stack dolocal x = State.left + (i-1)*Section_border_padding_horizontallove.graphics.line(x,y, x,y+log_browser.height(State, line_index))if y < 30 thenlove.graphics.print(line.section_stack[i].name, x+State.font_height+5, y+5, --[[vertically]] math.pi/2)endif y > App.screen.height-log_browser.height(State, line_index) thenlove.graphics.print(line.section_name, xleft+50,y)love.graphics.line(xleft+50+App.width(line.section_name)+2,sectiony, xright,sectiony)else assert(line.section_end)local sectiony = y+State.line_height-Section_border_padding_verticallove.graphics.line(xleft,y, xleft,sectiony)love.graphics.line(xright,y, xright,sectiony)love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)
local width = App.width(filename)if x + width > App.screen.width - 5 theny = y + Editor_state.line_heightx = 5endif y == fy thenlog(2, ('%d: correct row; considering %d %s %d %d'):format(y, i, filename, x, width))if best_guess == nil thenlog(2, 'nil')best_guess = ibest_guess_x = xbest_guess_width = widthelseif math.abs(fx + fwidth/2 - x - width/2) < math.abs(fx + fwidth/2 - best_guess_x - best_guess_width/2) thenbest_guess = ibest_guess_x = xbest_guess_width = widthendlog(2, ('best guess now %d %s %d %d'):format(best_guess, File_navigation.candidates[best_guess], best_guess_x, best_guess_width))endx = x + width + 30endlog_end('file index')return best_guessendlocal width = App.width(filename)if x + width > App.screen.width - 5 theny = y + Editor_state.line_heightx = 5endif i == index thenreturn y, x, widthendx = x + width + 30endendfunction file_index(fy, fx, fwidth)log_start('file index')log(2, ('for %d %d %d'):format(fy, fx, fwidth))local y,x = Menu_status_bar_height, 5local best_guess, best_guess_x, best_guess_widthfor i,filename in ipairs(File_navigation.candidates) doApp.screen.print(filename, x + x3-menu_xmin, y3)endx3 = x3 + width + 30end--return h+20endfunction file_navigator_up()local y, x, width = file_coord(File_navigation.index)local index = file_index(y-Editor_state.line_height, x, width)if index thenFile_navigation.index = indexendendfunction file_navigator_down()local y, x, width = file_coord(File_navigation.index)local index = file_index(y+Editor_state.line_height, x, width)if index thenFile_navigation.index = indexendendfunction file_coord(index)local y,x = Menu_status_bar_height, 5for i,filename in ipairs(File_navigation.candidates) dowidth = App.width(filename)if x3 + width > App.screen.width - 5 theny3 = y3 + Editor_state.line_heightx3 = 0endif i == o.index thenApp.color(Menu_highlight_color)love.graphics.rectangle('fill', x + x3-menu_xmin - 5, y3-2, width+5*2, Editor_state.line_height+2*2)endif x3 >= menu_xmin and x3 + width < menu_xmax thenApp.color(Menu_command_color)width = App.width(filename)if x2 + width > App.screen.width - 5 theny2 = y2 + Editor_state.line_heightx2 = 0endif i == o.index thenbreakendx2 = x2 + width + 30end-- figure out how much of the menu to displaylocal menu_xmin = math.max(0, x2-w/2)local menu_xmax = math.min(App.screen.width, x2+w/2)-- now selectively print out entrieslocal x3,y3 = 0,y -- x3 is relative, y3 is absolutelocal width = 0for i,filename in ipairs(o.files) doApp.screen.print(s, x,y)x = x + width + 30return x,ylocal width = App.width(s)if x + width > App.screen.width - 5 theny = y + Editor_state.line_heightx = 5local width = App.width(filename)if x + width > App.screen.width - 5 thenresult = result+1x = 5 + widthelsex = x + width + 30endendreturn resultendfunction add_file_to_menu(x,y, s, cursor_highlight)App.screen.print(File_navigation.filter, 5, 5)draw_cursor(5 + App.width(File_navigation.filter), 5)if File_navigation.num_lines == nil thenFile_navigation.num_lines = source.num_lines_for_file_navigator(File_navigation.candidates)endApp.color(Menu_background_color)love.graphics.rectangle('fill', 0,Menu_status_bar_height, App.screen.width, File_navigation.num_lines * Editor_state.line_height + --[[highlight padding]] 2)local x,y = 5, Menu_status_bar_heightfor i,filename in ipairs(File_navigation.candidates) doApp.screen.print(s, Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction source.draw_file_navigator()local width = App.width(s)if Menu_cursor > App.screen.width - 30 then
-- undo/redo by managing the sequence of events in the current session-- based on https://github.com/akkartik/mu1/blob/master/edit/012-editor-undo.mu-- Incredibly inefficient; we make a copy of lines on every single keystroke.-- The hope here is that we're either editing small files or just reading large files.-- TODO: highlight stuff inserted by any undo/redo operation-- TODO: coalesce multiple similar operationsfunction record_undo_event(State, data)State.history[State.next_history] = dataState.next_history = State.next_history+1for i=State.next_history,#State.history doState.history[i] = nilendendfunction undo_event(State)if State.next_history > 1 then--? print('moving to history', State.next_history-1)State.next_history = State.next_history-1local result = State.history[State.next_history]return resultendendfunction redo_event(State)if State.next_history <= #State.history then--? print('restoring history', State.next_history+1)local result = State.history[State.next_history]State.next_history = State.next_history+1return resultendend-- Copy all relevant global state.-- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.function snapshot(State, s,e)-- Snapshot everything by default, but subset if requested.assert(s)if e == nil thene = sendassert(#State.lines > 0)if s < 1 then s = 1 endif s > #State.lines then s = #State.lines endif e < 1 then e = 1 endif e > #State.lines then e = #State.lines end-- compare with App.initialize_globalslocal event = {screen_top=deepcopy(State.screen_top1),selection=deepcopy(State.selection1),cursor=deepcopy(State.cursor1),lines={},start_line=s,end_line=e,-- no filename; undo history is cleared when filename changes}-- deep copy lines without cached stuff like text fragmentsfor i=s,e dolocal line = State.lines[i]
endreturn eventendfunction patch(lines, from, to)--? if #from.lines == 1 and #to.lines == 1 then--? assert(from.start_line == from.end_line)--? assert(to.start_line == to.end_line)--? assert(from.start_line == to.start_line)--? lines[from.start_line] = to.lines[1]--? return--? endassert(from.start_line == to.start_line)for i=from.end_line,from.start_line,-1 dotable.remove(lines, i)endassert(#to.lines == to.end_line-to.start_line+1)for i=1,#to.lines dotable.insert(lines, to.start_line+i-1, to.lines[i])endendfunction patch_placeholders(line_cache, from, to)assert(from.start_line == to.start_line)for i=from.end_line,from.start_line,-1 dotable.remove(line_cache, i)endassert(#to.lines == to.end_line-to.start_line+1)for i=1,#to.lines dotable.insert(line_cache, to.start_line+i-1, {})endend-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080function deepcopy(obj, seen)if type(obj) ~= 'table' then return obj endif seen and seen[obj] then return seen[obj] endlocal s = seen or {}local result = setmetatable({}, getmetatable(obj))s[obj] = resultfor k,v in pairs(obj) doresult[deepcopy(k, s)] = deepcopy(v, s)endreturn resultendfunction minmax(a, b)return math.min(a,b), math.max(a,b)end
-- major tests for text editing flowsfunction test_initial_state()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{}Text.redraw_all(Editor_state)edit.draw(Editor_state)check_eq(#Editor_state.lines, 1, '#lines')check_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')check_eq(Editor_state.screen_top1.line, 1, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')endfunction test_backspace_from_start_of_final_line()-- display final line of text with cursor at start of itApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.cursor1 = {line=2, pos=1}Text.redraw_all(Editor_state)-- backspace scrolls upedit.run_after_keychord(Editor_state, 'backspace')check_eq(#Editor_state.lines, 1, '#lines')check_eq(Editor_state.cursor1.line, 1, 'cursor')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')endfunction test_insert_first_character()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{}Text.redraw_all(Editor_state)edit.draw(Editor_state)edit.run_after_text_input(Editor_state, 'a')local y = Editor_state.topApp.screen.check(y, 'a', 'screen:1')endfunction test_press_ctrl()-- press ctrl while the cursor is on textApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{''}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.run_after_keychord(Editor_state, 'C-m')endfunction test_move_left()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'a'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'left')check_eq(Editor_state.cursor1.pos, 1, 'check')endfunction test_move_right()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'a'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.cursor1.pos, 2, 'check')endfunction test_move_left_to_previous_line()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'left')check_eq(Editor_state.cursor1.line, 1, 'line')check_eq(Editor_state.cursor1.pos, 4, 'pos') -- past end of lineendfunction test_move_right_to_next_line()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- past end of lineedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.cursor1.line, 2, 'line')check_eq(Editor_state.cursor1.pos, 1, 'pos')endfunction test_move_to_start_of_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=3}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'check')endfunction test_move_to_start_of_previous_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'check')endfunction test_skip_to_previous_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=5} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'check')endfunction test_skip_past_tab_to_previous_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def\tghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=10} -- within third wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 9, 'check')endfunction test_skip_multiple_spaces_to_previous_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=6} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'check')endfunction test_move_to_start_of_word_on_previous_line()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.line, 1, 'line')check_eq(Editor_state.cursor1.pos, 5, 'pos')endfunction test_move_past_end_of_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 4, 'check')endfunction test_skip_to_next_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 8, 'check')endfunction test_skip_past_tab_to_next_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc\tdef'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 4, 'check')endfunction test_skip_multiple_spaces_to_next_word()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 9, 'check')endfunction test_move_past_end_of_word_on_next_line()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=8}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.line, 2, 'line')check_eq(Editor_state.cursor1.pos, 4, 'pos')endfunction test_click_moves_cursor()App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cacheedit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)check_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')-- selection is empty to avoid perturbing future editscheck_nil(Editor_state.selection1.line, 'selection:line')check_nil(Editor_state.selection1.pos, 'selection:pos')endfunction test_click_to_left_of_line()-- display a line with the cursor in the middleApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=3}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click to the left of the lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left-4,Editor_state.top+5, 1)-- cursor moves to start of linecheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')endfunction test_click_takes_margins_into_account()-- display two lines with cursor on one of themApp.screen.init{width=100, height=80}Editor_state = edit.initialize_test_state()Editor_state.left = 50 -- occupy only right side of screenEditor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')endfunction test_click_on_empty_line()-- display two lines with the first one emptyApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the empty lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'cursor')endfunction test_draw_text()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:3')endfunction test_draw_wrapping_text()App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'fgh', 'screen:3')endfunction test_draw_word_wrapping_text()App.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:3')endfunction test_click_on_wrapping_line()-- display two screen lines with cursor on one of themApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=20}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')endfunction test_click_on_wrapping_line_takes_margins_into_account()-- display two screen lines with cursor on one of themApp.screen.init{width=100, height=80}Editor_state = edit.initialize_test_state()Editor_state.left = 50 -- occupy only right side of screenEditor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=20}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')check_nil(Editor_state.selection1.line, 'selection is empty to avoid perturbing future edits')endfunction test_draw_text_wrapping_within_word()-- arrange a screen line that needs to be split within a wordApp.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abcd e fghijk', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abcd ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'e fgh', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ijk', 'screen:3')endfunction test_draw_wrapping_text_containing_non_ascii()-- draw a long line containing non-ASCIIApp.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'madam I’m adam', 'xyz'} -- notice the non-ASCII apostropheText.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'mad', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'am I', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, '’m a', 'screen:3')endfunction test_click_past_end_of_screen_line()-- display a wrapping lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'baseline/screen:2')y = y + Editor_state.line_height-- click past end of second screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 12, 'cursor:pos')endfunction test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen()-- display a wrapping line from its second screen lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=8}Editor_state.screen_top1 = {line=1, pos=7}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, "I'm ad", 'baseline/screen:2')y = y + Editor_state.line_height-- click past end of second screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 12, 'cursor:pos')endfunction test_click_past_end_of_wrapping_line()-- display a wrapping lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'am', 'baseline/screen:3')y = y + Editor_state.line_height-- click past the end of itedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of linecheck_eq(Editor_state.cursor1.pos, 15, 'cursor') -- one more than the number of UTF-8 code-pointsendfunction test_click_past_end_of_wrapping_line_containing_non_ascii()-- display a wrapping line containing non-ASCIIApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostropheText.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'I’m ad', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'am', 'baseline/screen:3')y = y + Editor_state.line_height-- click past the end of itedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of linecheck_eq(Editor_state.cursor1.pos, 15, 'cursor') -- one more than the number of UTF-8 code-pointsendfunction test_click_past_end_of_word_wrapping_line()-- display a long line wrapping at a word boundary on a screen of more realistic lengthApp.screen.init{width=160, height=80}Editor_state = edit.initialize_test_state()-- 0 1 2-- 123456789012345678901Editor_state.lines = load_array{'the quick brown fox jumped over the lazy dog'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'the quick brown fox ', 'baseline/screen:1')y = y + Editor_state.line_height-- click past the end of the screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.pos, 20, 'cursor')endfunction test_select_text()-- display a line of textApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- select a letterApp.fake_key_press('lshift')edit.run_after_keychord(Editor_state, 'S-right')App.fake_key_release('lshift')edit.key_release(Editor_state, 'lshift')-- selection persists even after shift is releasedcheck_eq(Editor_state.selection1.line, 1, 'selection:line')check_eq(Editor_state.selection1.pos, 1, 'selection:pos')check_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')endfunction test_cursor_movement_without_shift_resets_selection()-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- press an arrow key without shiftedit.run_after_keychord(Editor_state, 'right')-- no change to data, selection is resetcheck_nil(Editor_state.selection1.line, 'check')check_eq(Editor_state.lines[1].data, 'abc', 'data')endfunction test_edit_deletes_selection()-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- press a keyedit.run_after_text_input(Editor_state, 'x')-- selected text is deleted and replaced with the keycheck_eq(Editor_state.lines[1].data, 'xbc', 'check')endfunction test_edit_with_shift_key_deletes_selection()-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- mimic precise keypresses for a capital letterApp.fake_key_press('lshift')edit.keychord_press(Editor_state, 'd', 'd')edit.text_input(Editor_state, 'D')edit.key_release(Editor_state, 'd')App.fake_key_release('lshift')-- selected text is deleted and replaced with the keycheck_nil(Editor_state.selection1.line, 'check')check_eq(Editor_state.lines[1].data, 'Dbc', 'data')endfunction test_copy_does_not_reset_selection()-- display a line of text with a selectionApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- copy selectionedit.run_after_keychord(Editor_state, 'C-c')check_eq(App.clipboard, 'a', 'clipboard')-- selection is reset since shift key is not pressedcheck(Editor_state.selection1.line, 'check')endfunction test_cut()-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- press a keyedit.run_after_keychord(Editor_state, 'C-x')check_eq(App.clipboard, 'a', 'clipboard')-- selected text is deletedcheck_eq(Editor_state.lines[1].data, 'bc', 'data')endfunction test_paste_replaces_selection()-- display a line of text with a selectionApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.selection1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- set clipboardApp.clipboard = 'xyz'-- paste selectionedit.run_after_keychord(Editor_state, 'C-v')-- selection is reset since shift key is not pressed-- selection includes the newline, so it's also deletedcheck_eq(Editor_state.lines[1].data, 'xyzdef', 'check')endfunction test_deleting_selection_may_scroll()-- display lines 2/3/4App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=2}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'baseline/screen:3')-- set up a selection starting above the currently displayed pageEditor_state.selection1 = {line=1, pos=2}-- delete selectionedit.run_after_keychord(Editor_state, 'backspace')-- page scrolls upcheck_eq(Editor_state.screen_top1.line, 1, 'check')check_eq(Editor_state.lines[1].data, 'ahi', 'data')endfunction test_edit_wrapping_text()App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)edit.run_after_text_input(Editor_state, 'g')local y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'fg', 'screen:3')endfunction test_insert_newline()-- display a few linesApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- hitting the enter key splits the lineedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'a', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'bc', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:3')endfunction test_insert_newline_at_start_of_line()-- display a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- hitting the enter key splits the lineedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')check_eq(Editor_state.lines[1].data, '', 'data:1')check_eq(Editor_state.lines[2].data, 'abc', 'data:2')endfunction test_insert_from_clipboard()-- display a few linesApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- paste some text including a newline, check that new line is createdApp.clipboard = 'xy\nz'edit.run_after_keychord(Editor_state, 'C-v')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'axy', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'zbc', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:3')endfunction test_select_text_using_mouse()App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache-- press and hold on first locationedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- drag and release somewhere elseedit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)check_eq(Editor_state.selection1.line, 1, 'selection:line')check_eq(Editor_state.selection1.pos, 2, 'selection:pos')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')endfunction test_select_text_using_mouse_and_shift()App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache-- click on first locationedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- hold down shift and click somewhere elseApp.fake_key_press('lshift')edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)App.fake_key_release('lshift')check_eq(Editor_state.selection1.line, 1, 'selection:line')check_eq(Editor_state.selection1.pos, 2, 'selection:pos')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')endfunction test_select_text_repeatedly_using_mouse_and_shift()App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cache-- click on first locationedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- hold down shift and click on a second locationApp.fake_key_press('lshift')edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+20,Editor_state.top+Editor_state.line_height+5, 1)-- hold down shift and click at a third locationApp.fake_key_press('lshift')edit.run_after_mouse_press(Editor_state, Editor_state.left+20,Editor_state.top+5, 1)edit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height+5, 1)App.fake_key_release('lshift')-- selection is between first and third location. forget the second location, not the first.check_eq(Editor_state.selection1.line, 1, 'selection:line')check_eq(Editor_state.selection1.pos, 2, 'selection:pos')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')endfunction test_cut_without_selection()-- display a few linesApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Editor_state.selection1 = {}edit.draw(Editor_state)-- try to cut without selecting textedit.run_after_keychord(Editor_state, 'C-x')-- no crashcheck_nil(Editor_state.selection1.line, 'check')endfunction test_pagedown()App.screen.init{width=120, height=45}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- initially the first two lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')-- after pagedown the bottom line becomes the topedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 2, 'cursor')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:2')endfunction test_pagedown_often_shows_start_of_wrapping_line()-- draw a few lines ending in part of a wrapping lineApp.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'baseline/screen:3')-- after pagedown we start drawing from the bottom _line_ (multiple screen lines)edit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 2, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')endfunction test_pagedown_can_start_from_middle_of_long_wrapping_line()-- draw a few lines starting from a very long wrapping lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu vwx yza bcd efg hij', 'XYZ'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc ', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'baseline/screen:3')-- after pagedown we scroll down the very long wrapping lineedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 1, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 9, 'screen_top:pos')y = Editor_state.topApp.screen.check(y, 'ghi ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl ', 'screen:2')y = y + Editor_state.line_height
endfunction test_pagedown_never_moves_up()-- draw the final screen line of a wrapping lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=9}Editor_state.screen_top1 = {line=1, pos=9}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- pagedown makes no changeedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 1, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 9, 'screen_top:pos')endfunction test_down_arrow_moves_cursor()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- initially the first three lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- after hitting the down arrow, the cursor moves down by 1 lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 2, 'cursor')-- the screen is unchangedy = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:3')endfunction test_down_arrow_scrolls_down_by_one_line()-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 4, 'cursor')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')endfunction test_down_arrow_scrolls_down_by_one_screen_line()-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')endfunction test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word()-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'baseline/screen:3')-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'screen:3')endfunction test_pagedown_followed_by_down_arrow_does_not_scroll_screen_up()App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'baseline/screen:3')-- after hitting pagedown the screen scrolls down to start of a long lineedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 3, 'baseline2/screen_top')check_eq(Editor_state.cursor1.line, 3, 'baseline2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'baseline2/cursor:pos')-- after hitting down arrow the screen doesn't scroll down further, and certainly doesn't scroll upedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 3, 'screen_top')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'ghij', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'screen:3')endfunction test_up_arrow_moves_cursor()-- display the first 3 lines with the cursor on the bottom lineApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- after hitting the up arrow the cursor moves up by 1 lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 2, 'cursor')-- the screen is unchangedy = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:3')endfunction test_up_arrow_scrolls_up_by_one_line()-- display the lines 2/3/4 with the cursor on line 2App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'baseline/screen:3')-- after hitting the up arrow the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 1, 'cursor')y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:3')endfunction test_up_arrow_scrolls_up_by_one_screen_line()-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=6}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:2')-- after hitting the up arrow the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topApp.screen.check(y, 'ghi ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'screen:3')check_eq(Editor_state.screen_top1.line, 3, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')endfunction test_up_arrow_scrolls_up_to_final_screen_line()-- display lines starting just after a long lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'ghi', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:3')-- after hitting the up arrow the screen scrolls up to final screen line of previous lineedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')check_eq(Editor_state.screen_top1.line, 1, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 5, 'screen_top:pos')check_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos')endfunction test_up_arrow_scrolls_up_to_empty_line()-- display a screenful of text with an empty line just above it outside the screenApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'', 'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- after hitting the up arrow the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 1, 'cursor')y = Editor_state.top-- empty first liney = y + Editor_state.line_heightApp.screen.check(y, 'abc', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:3')endfunction test_pageup()App.screen.init{width=120, height=45}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}-- initially the last two lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:2')-- after pageup the cursor goes to first lineedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 1, 'cursor')y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')endfunction test_pageup_scrolls_up_by_screen_line()-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'ghi', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the page-up key the screen scrolls up to topedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:3')endfunction test_pageup_scrolls_up_from_middle_screen_line()-- display a few lines starting from the middle of a line (Editor_state.cursor1.pos > 1)App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=5}Editor_state.screen_top1 = {line=2, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the page-up key the screen scrolls up to topedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'screen:3')endfunction test_enter_on_bottom_line_scrolls_down()-- display a few lines with cursor on bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- after hitting the enter key the screen scrolls downedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 4, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'g', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'hi', 'screen:3')endfunction test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom()-- display just the bottom line on screenApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=4, pos=2}Editor_state.screen_top1 = {line=4, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'baseline/screen:1')-- after hitting the enter key the screen does not scroll downedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.screen_top1.line, 4, 'screen_top')check_eq(Editor_state.cursor1.line, 5, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'j', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'screen:2')endfunction test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom()-- display just an empty bottom line on screenApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', ''}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- after hitting the inserting_text key the screen does not scroll downedit.run_after_text_input(Editor_state, 'a')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')local y = Editor_state.topApp.screen.check(y, 'a', 'screen:1')endfunction test_typing_on_bottom_line_scrolls_down()-- display a few lines with cursor on bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:3')-- after typing something the line wraps and the screen scrolls downedit.run_after_text_input(Editor_state, 'j')edit.run_after_text_input(Editor_state, 'k')edit.run_after_text_input(Editor_state, 'l')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 7, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'screen:3')endfunction test_left_arrow_scrolls_up_in_wrapped_line()-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}-- cursor is at top of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:2')-- after hitting the left arrow the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'left')y = Editor_state.topApp.screen.check(y, 'ghi ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'screen:3')check_eq(Editor_state.screen_top1.line, 3, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')endfunction test_right_arrow_scrolls_down_in_wrapped_line()-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- cursor is at bottom right of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the right arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 6, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')endfunction test_home_scrolls_up_in_wrapped_line()-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}-- cursor is at top of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:2')-- after hitting home the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'home')y = Editor_state.topApp.screen.check(y, 'ghi ', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'screen:3')check_eq(Editor_state.screen_top1.line, 3, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')endfunction test_end_scrolls_down_in_wrapped_line()-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- cursor is at bottom right of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting end the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'end')check_eq(Editor_state.screen_top1.line, 2, 'screen_top')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 8, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')endfunction test_position_cursor_on_recently_edited_wrapping_line()-- draw a line wrapping over 2 screen linesApp.screen.init{width=100, height=200}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=25}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc def ghi ', 'baseline1/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl mno pqr ', 'baseline1/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'baseline1/screen:3')-- add to the line until it's wrapping over 3 screen linesedit.run_after_text_input(Editor_state, 's')edit.run_after_text_input(Editor_state, 't')edit.run_after_text_input(Editor_state, 'u')check_eq(Editor_state.cursor1.pos, 28, 'cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc def ghi ', 'baseline2/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl mno pqr ', 'baseline2/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'stu', 'baseline2/screen:3')-- try to move the cursor earlier in the third screen line by clicking the mouseedit.run_after_mouse_release(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)-- cursor should movecheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 26, 'cursor:pos')endfunction test_backspace_can_scroll_up()-- display the lines 2/3/4 with the cursor on line 2App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'baseline/screen:3')-- after hitting backspace the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.screen_top1.line, 1, 'screen_top')check_eq(Editor_state.cursor1.line, 1, 'cursor')y = Editor_state.topApp.screen.check(y, 'abcdef', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'screen:3')endfunction test_backspace_can_scroll_up_screen_line()-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=5}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'baseline/screen:2')-- after hitting backspace the screen scrolls up by one screen lineedit.run_after_keychord(Editor_state, 'backspace')y = Editor_state.topApp.screen.check(y, 'ghij', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'screen:3')check_eq(Editor_state.screen_top1.line, 3, 'screen_top:line')check_eq(Editor_state.screen_top1.pos, 1, 'screen_top:pos')check_eq(Editor_state.cursor1.line, 3, 'cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')endfunction test_backspace_past_line_boundary()-- position cursor at start of a (non-first) lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}-- backspace joins with previous lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'abcdef', 'check')end-- some tests for operating over selections created using Shift- chords-- we're just testing delete_selection, and it works the same for all keysfunction test_backspace_over_selection()-- select just one character within a line with cursor before selectionApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}-- backspace deletes the selected character, even though it's after the cursoredit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'bc', 'data')-- cursor (remains) at start of selectioncheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')-- selection is clearedcheck_nil(Editor_state.selection1.line, 'selection')endfunction test_backspace_over_selection_reverse()-- select just one character within a line with cursor after selectionApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.selection1 = {line=1, pos=1}-- backspace deletes the selected characteredit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'bc', 'data')-- cursor moves to start of selectioncheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')-- selection is clearedcheck_nil(Editor_state.selection1.line, 'selection')endfunction test_backspace_over_multiple_lines()-- select just one character within a line with cursor after selectionApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.selection1 = {line=4, pos=2}-- backspace deletes the region and joins the remaining portions of lines on either sideedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'akl', 'data:1')check_eq(Editor_state.lines[2].data, 'mno', 'data:2')-- cursor remains at start of selectioncheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')-- selection is clearedcheck_nil(Editor_state.selection1.line, 'selection')endfunction test_backspace_to_end_of_line()-- select region from cursor to end of lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.selection1 = {line=1, pos=4}-- backspace deletes rest of line without joining to any other lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'a', 'data:1')check_eq(Editor_state.lines[2].data, 'def', 'data:2')-- cursor remains at start of selectioncheck_eq(Editor_state.cursor1.line, 1, 'cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'cursor:pos')-- selection is clearedcheck_nil(Editor_state.selection1.line, 'selection')endfunction test_backspace_to_start_of_line()-- select region from cursor to start of lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.selection1 = {line=2, pos=3}-- backspace deletes beginning of line without joining to any other lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'abc', 'data:1')check_eq(Editor_state.lines[2].data, 'f', 'data:2')-- cursor remains at start of selectioncheck_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'cursor:pos')-- selection is clearedcheck_nil(Editor_state.selection1.line, 'selection')endfunction test_undo_insert_text()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- insert a characteredit.draw(Editor_state)edit.run_after_text_input(Editor_state, 'g')check_eq(Editor_state.cursor1.line, 2, 'baseline/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'baseline/cursor:pos')check_nil(Editor_state.selection1.line, 'baseline/selection:line')check_nil(Editor_state.selection1.pos, 'baseline/selection:pos')local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'defg', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'baseline/screen:3')-- undoedit.run_after_keychord(Editor_state, 'C-z')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'cursor:pos')check_nil(Editor_state.selection1.line, 'selection:line')check_nil(Editor_state.selection1.pos, 'selection:pos')y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'screen:3')endfunction test_undo_delete_text()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'defg', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=5}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- delete a characteredit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.cursor1.line, 2, 'baseline/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'baseline/cursor:pos')check_nil(Editor_state.selection1.line, 'baseline/selection:line')check_nil(Editor_state.selection1.pos, 'baseline/selection:pos')local y = Editor_state.topApp.screen.check(y, 'abc', 'baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'baseline/screen:3')-- undo--? -- after undo, the backspaced key is selectededit.run_after_keychord(Editor_state, 'C-z')check_eq(Editor_state.cursor1.line, 2, 'cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'cursor:pos')check_nil(Editor_state.selection1.line, 'selection:line')check_nil(Editor_state.selection1.pos, 'selection:pos')--? check_eq(Editor_state.selection1.line, 2, 'selection:line')--? check_eq(Editor_state.selection1.pos, 4, 'selection:pos')y = Editor_state.topApp.screen.check(y, 'abc', 'screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'defg', 'screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'screen:3')endfunction test_undo_restores_selection()-- display a line of text with some part selectedApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.selection1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- delete selected textedit.run_after_text_input(Editor_state, 'x')check_eq(Editor_state.lines[1].data, 'xbc', 'baseline')check_nil(Editor_state.selection1.line, 'baseline:selection')-- undoedit.run_after_keychord(Editor_state, 'C-z')edit.run_after_keychord(Editor_state, 'C-z')-- selection is restoredcheck_eq(Editor_state.selection1.line, 1, 'line')check_eq(Editor_state.selection1.pos, 2, 'pos')endfunction test_search()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_text_input(Editor_state, 'd')edit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 2, '1/cursor:line')check_eq(Editor_state.cursor1.pos, 1, '1/cursor:pos')-- reset cursorEditor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}-- search for second occurrenceedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_text_input(Editor_state, 'de')edit.run_after_keychord(Editor_state, 'down')edit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 4, '2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, '2/cursor:pos')endfunction test_search_upwards()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc abd'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_text_input(Editor_state, 'a')-- search for previous occurrenceedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.cursor1.line, 1, '2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, '2/cursor:pos')endfunction test_search_wrap()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=3}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_text_input(Editor_state, 'a')edit.run_after_keychord(Editor_state, 'return')-- cursor wrapscheck_eq(Editor_state.cursor1.line, 1, '1/cursor:line')check_eq(Editor_state.cursor1.pos, 1, '1/cursor:pos')endfunction test_search_wrap_upwards()App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc abd'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search upwards for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_text_input(Editor_state, 'a')edit.run_after_keychord(Editor_state, 'up')-- cursor wrapscheck_eq(Editor_state.cursor1.line, 1, '1/cursor:line')check_eq(Editor_state.cursor1.pos, 5, '1/cursor:pos')end
-- text editor, particularly text drawing, horizontal wrap, vertical scrollingText = {}-- draw a line starting from startpos to screen at y between State.left and State.right-- return the final y, and position of start of final screen line drawnfunction Text.draw(State, line_index, y, startpos)--? print('text.draw', line_index, y)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]line_cache.starty = yline_cache.startpos = startpos-- wrap long lines
final_screen_line_starting_pos = poslocal f = Text.screen_line(line, line_cache, i)--? print('text.draw:', f, 'at', line_index,pos, 'after', x,y)local frag_len = utf8.len(f)Text.draw_highlight(State, line, State.left,y, pos, lo,hi)App.color(Text_color)App.screen.print(f, State.left,y)if pos <= State.cursor1.pos and pos + frag_len >= State.cursor1.pos thenif State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term thenlocal lo_px = Text.draw_highlight(State, line, State.left,y, pos, State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term))love.graphics.print(State.search_term, State.left+lo_px,y)Text.draw_cursor(State, State.left+Text.x(f, State.cursor1.pos-pos+1), y)y = y + State.line_heightif y >= App.screen.height thenbreakendreturn y - State.line_height, final_screen_line_starting_posendfunction Text.screen_line(line, line_cache, i)local pos = line_cache.screen_line_starting_pos[i]local offset = Text.offset(line.data, pos)if i >= #line_cache.screen_line_starting_pos thenreturn line.data:sub(offset)local endpos = line_cache.screen_line_starting_pos[i+1]-1local end_offset = Text.offset(line.data, endpos)return line.data:sub(offset, end_offset)local x = 0
endfunction Text.draw_cursor(State, x, y)-- blink every 0.5sif math.floor(Cursor_time*2)%2 == 0 thenApp.color(Cursor_color)love.graphics.rectangle('fill', x,y, 3,State.line_height)App.color(Text_color)endState.cursor_x = xState.cursor_y = y+State.line_heightendfunction Text.populate_screen_line_starting_pos(State, line_index)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.screen_line_starting_pos thenreturnendline_cache.screen_line_starting_pos = {1}
local frag_width = App.width(frag)--? print('-- frag:', frag, pos, x, frag_width, State.width)while x + frag_width > State.width do--? print('frag:', frag, pos, x, frag_width, State.width)if x < 0.8 * State.width thenlocal bpos = Text.nearest_pos_less_than(frag, State.width - x)-- everything works if bpos == 0, but is a little inefficientpos = pos + bpos--? if bpos > 0 then--? print('after chop:', frag)--? endfrag_width = App.width(frag)
endendfunction Text.text_input(State, t)if App.mouse_down(1) then return endif App.ctrl_down() or App.alt_down() or App.cmd_down() then return endlocal before = snapshot(State, State.cursor1.line)--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)Text.insert_at_cursor(State, t)if State.cursor_y > App.screen.height - State.line_height thenText.populate_screen_line_starting_pos(State, State.cursor1.line)Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endrecord_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})endfunction Text.insert_at_cursor(State, t)local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)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)Text.clear_screen_line_cache(State, State.cursor1.line)State.cursor1.pos = State.cursor1.pos+1end-- Don't handle any keys here that would trigger text_input above.function Text.keychord_press(State, chord)--? print('chord', chord, State.selection1.line, State.selection1.pos)--== shortcuts that mutate textif chord == 'return' thenlocal before_line = State.cursor1.linelocal before = snapshot(State, before_line)Text.insert_return(State)State.selection1 = {}if State.cursor_y > App.screen.height - State.line_height thenText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})elseif chord == 'tab' thenlocal before = snapshot(State, State.cursor1.line)--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)Text.insert_at_cursor(State, '\t')if State.cursor_y > App.screen.height - State.line_height thenText.populate_screen_line_starting_pos(State, State.cursor1.line)Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)--? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})elseif chord == 'backspace' thenif State.selection1.line thenText.delete_selection(State, State.left, State.right)schedule_save(State)returnendlocal beforeif State.cursor1.pos > 1 thenbefore = snapshot(State, State.cursor1.line)local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1)local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)if byte_start thenif byte_end thenState.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)elseState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)endState.cursor1.pos = State.cursor1.pos-1endelseif State.cursor1.line > 1 thenbefore = snapshot(State, State.cursor1.line-1, State.cursor1.line)-- join linesState.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].datatable.remove(State.lines, State.cursor1.line)table.remove(State.line_cache, State.cursor1.line)State.cursor1.line = State.cursor1.line-1endif State.screen_top1.line > #State.lines thenText.populate_screen_line_starting_pos(State, #State.lines)local line_cache = State.line_cache[#State.line_cache]State.screen_top1 = {line=#State.lines, pos=line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]}elseif Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = {line=State.cursor1.line,pos=Text.pos_at_start_of_screen_line(State, State.cursor1),}Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaksendText.clear_screen_line_cache(State, State.cursor1.line)assert(Text.le1(State.screen_top1, State.cursor1))schedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})elseif chord == 'delete' thenif State.selection1.line thenText.delete_selection(State, State.left, State.right)schedule_save(State)returnendlocal beforeif State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenbefore = snapshot(State, State.cursor1.line)elsebefore = snapshot(State, State.cursor1.line, State.cursor1.line+1)endif State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenlocal byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos+1)if byte_start thenif byte_end thenState.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)elseState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)end-- no change to State.cursor1.posendelseif State.cursor1.line < #State.lines then-- join linesState.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].datatable.remove(State.lines, State.cursor1.line+1)table.remove(State.line_cache, State.cursor1.line+1)endText.clear_screen_line_cache(State, State.cursor1.line)schedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})--== shortcuts that move the cursorelseif chord == 'left' thenText.left(State)State.selection1 = {}elseif chord == 'right' thenText.right(State)State.selection1 = {}elseif chord == 'S-left' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.left(State)elseif chord == 'S-right' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.right(State)-- C- hotkeys reserved for drawings, so we'll use M-elseif chord == 'M-left' thenText.word_left(State)State.selection1 = {}elseif chord == 'M-right' thenText.word_right(State)State.selection1 = {}elseif chord == 'M-S-left' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.word_left(State)elseif chord == 'M-S-right' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.word_right(State)elseif chord == 'home' thenText.start_of_line(State)State.selection1 = {}elseif chord == 'end' thenText.end_of_line(State)State.selection1 = {}elseif chord == 'S-home' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.start_of_line(State)elseif chord == 'S-end' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.end_of_line(State)elseif chord == 'up' thenText.up(State)State.selection1 = {}elseif chord == 'down' thenText.down(State)State.selection1 = {}elseif chord == 'S-up' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.up(State)elseif chord == 'S-down' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.down(State)elseif chord == 'pageup' thenText.pageup(State)State.selection1 = {}elseif chord == 'pagedown' thenText.pagedown(State)State.selection1 = {}elseif chord == 'S-pageup' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.pageup(State)elseif chord == 'S-pagedown' thenif State.selection1.line == nil thenState.selection1 = {line=State.cursor1.line, pos=State.cursor1.pos}endText.pagedown(State)endendfunction Text.insert_return(State)local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset)})table.insert(State.line_cache, State.cursor1.line+1, {})State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)Text.clear_screen_line_cache(State, State.cursor1.line)State.cursor1 = {line=State.cursor1.line+1, pos=1}endfunction Text.pageup(State)--? print('pageup')-- duplicate some logic from love.drawlocal top2 = Text.to2(State, State.screen_top1)--? print(App.screen.height)local y = App.screen.height - State.line_heightwhile y >= State.top do--? print(y, top2.line, top2.screen_line, top2.screen_pos)if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break endy = y - State.line_heighttop2 = Text.previous_screen_line(State, top2)endState.screen_top1 = Text.to1(State, top2)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)--? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)--? print('pageup end')endfunction Text.pagedown(State)--? print('pagedown')-- If a line/paragraph gets to a page boundary, I often want to scroll-- before I get to the bottom.-- However, only do this if it makes forward progress.local bot2 = Text.to2(State, State.screen_bottom1)if bot2.screen_line > 1 thenbot2.screen_line = math.max(bot2.screen_line-10, 1)endlocal new_top1 = Text.to1(State, bot2)if Text.lt1(State.screen_top1, new_top1) thenState.screen_top1 = new_top1elseState.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}end--? print('setting top to', State.screen_top1.line, State.screen_top1.pos)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)--? print('top now', State.screen_top1.line)Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks--? print('pagedown end')endfunction Text.up(State)--? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)if screen_line_starting_pos == 1 then--? print('cursor is at first screen line of its line')-- line is done; skip to previous text lineif State.cursor1.line > 1 thenlocal new_cursor_line = State.cursor1.line-1--? print('found previous text line')State.cursor1 = {line=new_cursor_line, pos=nil}Text.populate_screen_line_starting_pos(State, State.cursor1.line)-- previous text line found, pick its final screen line--? print('has multiple screen lines')local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos--? print(#screen_line_starting_pos)screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos]local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset)State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1endelse-- move up one screen line in current lineassert(screen_line_index > 1)local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1--? print('cursor pos is now '..tostring(State.cursor1.pos))endif Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = {line=State.cursor1.line,pos=Text.pos_at_start_of_screen_line(State, State.cursor1),}Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaksendendfunction Text.down(State)--? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)if Text.cursor_at_final_screen_line(State) then-- line is done, skip to next text line--? print('cursor at final screen line of its line')if State.cursor1.line < #State.lines thenlocal new_cursor_line = State.cursor1.line+1State.cursor1.line = new_cursor_lineState.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left)--? print(State.cursor1.pos)endif State.cursor1.line > State.screen_bottom1.line then--? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)--? print('scroll up preserving cursor')Text.snap_cursor_to_bottom_of_screen(State)--? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)endelse-- move down one screen line in current linelocal scroll_down = Text.le1(State.screen_bottom1, State.cursor1)--? print('cursor is NOT at final screen line of its line')local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)Text.populate_screen_line_starting_pos(State, State.cursor1.line)local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1]--? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1--? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)if scroll_down then--? print('scroll up preserving cursor')Text.snap_cursor_to_bottom_of_screen(State)--? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)endend--? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)endfunction Text.start_of_line(State)State.cursor1.pos = 1if Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copyendendfunction Text.end_of_line(State)State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1if Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.word_left(State)-- skip some whitespacewhile true doif State.cursor1.pos == 1 thenbreakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') thenbreakendText.left(State)end-- skip some non-whitespacewhile true doText.left(State)if State.cursor1.pos == 1 thenbreakendassert(State.cursor1.pos > 1)if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') thenbreakendendendfunction Text.word_right(State)-- skip some whitespacewhile true doif State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) thenbreakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') thenbreakendText.right_without_scroll(State)endwhile true doText.right_without_scroll(State)if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) thenbreakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') thenbreakendendif Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.match(s, pos, pat)local start_offset = Text.offset(s, pos)assert(start_offset)local end_offset = Text.offset(s, pos+1)assert(end_offset > start_offset)local curr = s:sub(start_offset, end_offset-1)return curr:match(pat)endfunction Text.left(State)if State.cursor1.pos > 1 thenState.cursor1.pos = State.cursor1.pos-1elseif State.cursor1.line > 1 thenState.cursor1.line = State.cursor1.line-1State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1endif Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = {line=State.cursor1.line,pos=Text.pos_at_start_of_screen_line(State, State.cursor1),}Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaksendendfunction Text.right(State)Text.right_without_scroll(State)if Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.right_without_scroll(State)if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenState.cursor1.pos = State.cursor1.pos+1elseif State.cursor1.line <= #State.lines-1 thenState.cursor1.line = State.cursor1.line+1State.cursor1.pos = 1endendfunction Text.pos_at_start_of_screen_line(State, loc1)Text.populate_screen_line_starting_pos(State, loc1.line)local line_cache = State.line_cache[loc1.line]for i=#line_cache.screen_line_starting_pos,1,-1 dolocal spos = line_cache.screen_line_starting_pos[i]if spos <= loc1.pos thenreturn spos,iendendassert(false)endfunction Text.cursor_at_final_screen_line(State)Text.populate_screen_line_starting_pos(State, State.cursor1.line)local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_pos--? print(screen_lines[#screen_lines], State.cursor1.pos)return screen_lines[#screen_lines] <= State.cursor1.posendfunction Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)if State.top > App.screen.height - State.line_height then--? print('scroll up')Text.snap_cursor_to_bottom_of_screen(State)endend-- should never modify State.cursor1function Text.snap_cursor_to_bottom_of_screen(State)--? print('to2:', State.cursor1.line, State.cursor1.pos)local top2 = Text.to2(State, State.cursor1)--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)-- slide to start of screen linetop2.screen_pos = 1 -- start of screen line--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)--? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')local y = App.screen.height - State.line_height-- duplicate some logic from love.drawwhile true do--? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)if top2.line == 1 and top2.screen_line == 1 then break endlocal h = State.line_heightif y - h < State.top thenbreakendy = y - htop2 = Text.previous_screen_line(State, top2)end--? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos)State.screen_top1 = Text.to1(State, top2)--? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos)--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaksendfunction Text.in_line(State, line_index, x,y)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.starty == nil then return false end -- outside current pageif y < line_cache.starty then return false endText.populate_screen_line_starting_pos(State, line_index)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)end-- convert mx,my in pixels to schema-1 coordinatesfunction Text.to_pos_on_line(State, line_index, mx, my)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]assert(my >= line_cache.starty)-- duplicate some logic from Text.drawlocal y = line_cache.startylocal start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos dolocal screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))local nexty = y + State.line_heightif my < nexty then-- On all wrapped screen lines but the final one, clicks past end of-- line position cursor on final character of screen line.-- (The final screen line positions past end of screen line as always.)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--? print('past end of non-final line; return')return line_cache.screen_line_starting_pos[screen_line_index+1]-1endlocal s = string.sub(line.data, screen_line_starting_byte_offset)--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)return screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1endy = nextyendassert(false)endfunction Text.screen_line_width(State, line_index, i)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]local start_pos = line_cache.screen_line_starting_pos[i]local start_offset = Text.offset(line.data, start_pos)local screen_lineif i < #line_cache.screen_line_starting_pos thenlocal past_end_pos = line_cache.screen_line_starting_pos[i+1]local past_end_offset = Text.offset(line.data, past_end_pos)screen_line = string.sub(line.data, start_offset, past_end_offset-1)elsescreen_line = string.sub(line.data, start_pos)endreturn App.width(screen_line)endfunction Text.screen_line_index(screen_line_starting_pos, pos)for i = #screen_line_starting_pos,1,-1 doif screen_line_starting_pos[i] <= pos thenreturn iendendend-- convert x pixel coordinate to pos-- oblivious to wrapping-- result: 1 to len+1function Text.nearest_cursor_pos(line, x, left)if x < left thenreturn 1endlocal len = utf8.len(line)local max_x = left+Text.x(line, len+1)if x > max_x thenreturn len+1endlocal leftpos, rightpos = 1, len+1--? print('-- nearest', x)while true do--? print('nearest', x, '^'..line..'$', leftpos, rightpos)if leftpos == rightpos thenreturn leftposendlocal curr = math.floor((leftpos+rightpos)/2)local currxmin = left+Text.x(line, curr)local currxmax = left+Text.x(line, curr+1)--? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)if currxmin <= x and x < currxmax thenif x-currxmin < currxmax-x thenreturn currelsereturn curr+1endendif leftpos >= rightpos-1 thenreturn rightposendif currxmin > x thenrightpos = currelseleftpos = currendendassert(false)end-- return the nearest index of line (in utf8 code points) which lies entirely-- within x pixels of the left margin-- result: 0 to len+1function Text.nearest_pos_less_than(line, x)--? print('', '-- nearest_pos_less_than', line, x)local len = utf8.len(line)local max_x = Text.x_after(line, len)if x > max_x thenreturn len+1endlocal left, right = 0, len+1while true dolocal curr = math.floor((left+right)/2)local currxmin = Text.x_after(line, curr+1)local currxmax = Text.x_after(line, curr+2)--? print('', x, left, right, curr, currxmin, currxmax)if currxmin <= x and x < currxmax thenreturn currendif left >= right-1 thenreturn leftendif currxmin > x thenright = currelseleft = currendendassert(false)endfunction Text.x_after(s, pos)local offset = Text.offset(s, math.min(pos+1, #s+1))local s_before = s:sub(1, offset-1)--? print('^'..s_before..'$')return App.width(s_before)endfunction Text.x(s, pos)local offset = Text.offset(s, pos)local s_before = s:sub(1, offset-1)return App.width(s_before)endfunction Text.to2(State, loc1)local result = {line=loc1.line}local line_cache = State.line_cache[loc1.line]Text.populate_screen_line_starting_pos(State, loc1.line)for i=#line_cache.screen_line_starting_pos,1,-1 dolocal spos = line_cache.screen_line_starting_pos[i]if spos <= loc1.pos thenresult.screen_line = iresult.screen_pos = loc1.pos - spos + 1breakendendassert(result.screen_pos)return resultendfunction Text.to1(State, loc2)local result = {line=loc2.line, pos=loc2.screen_pos}if loc2.screen_line > 1 thenresult.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1endreturn resultendfunction Text.eq1(a, b)return a.line == b.line and a.pos == b.posendfunction Text.lt1(a, b)if a.line < b.line thenreturn trueendif a.line > b.line thenreturn falseendreturn a.pos < b.posendfunction Text.le1(a, b)if a.line < b.line thenreturn trueendif a.line > b.line thenreturn falseendreturn a.pos <= b.posendfunction Text.offset(s, pos1)if pos1 == 1 then return 1 endlocal result = utf8.offset(s, pos1)if result == nil thenprint(pos1, #s, s)endassert(result)return resultendfunction Text.previous_screen_line(State, loc2)if loc2.screen_line > 1 thenreturn {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}elseif loc2.line == 1 thenreturn loc2elselocal l = State.lines[loc2.line-1]Text.populate_screen_line_starting_pos(State, loc2.line-1)return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}endend-- resize helperfunction Text.tweak_screen_top_and_cursor(State)if State.screen_top1.pos == 1 then return endText.populate_screen_line_starting_pos(State, State.screen_top1.line)local line = State.lines[State.screen_top1.line]local line_cache = State.line_cache[State.screen_top1.line]for i=2,#line_cache.screen_line_starting_pos dolocal pos = line_cache.screen_line_starting_pos[i]if pos == State.screen_top1.pos thenbreakendif pos > State.screen_top1.pos then-- make sure screen top is at start of a screen linelocal prev = line_cache.screen_line_starting_pos[i-1]if State.screen_top1.pos - prev < pos - State.screen_top1.pos thenState.screen_top1.pos = prevelseState.screen_top1.pos = posendbreakendend-- make sure cursor is on screenif Text.lt1(State.cursor1, State.screen_top1) thenState.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}elseif State.cursor1.line >= State.screen_bottom1.line then--? print('too low')if Text.cursor_out_of_screen(State) then--? print('tweak')State.cursor1 = {line=State.screen_bottom1.line,pos=Text.to_pos_on_line(State, State.screen_bottom1.line, State.right-5, App.screen.height-5),}endendend-- slightly expensive since it redraws the screenfunction Text.cursor_out_of_screen(State)App.draw()return State.cursor_y == nil-- this approach is cheaper and almost works, except on the final screen-- where file ends above bottom of screen--? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)--? local botline1 = {line=State.cursor1.line, pos=botpos}--? return Text.lt1(State.screen_bottom1, botline1)endfunction Text.redraw_all(State)--? print('clearing fragments')State.line_cache = {}for i=1,#State.lines doState.line_cache[i] = {}endendfunction Text.clear_screen_line_cache(State, line_index)State.line_cache[line_index].screen_line_starting_pos = nilendfunction trim(s)return s:gsub('^%s+', ''):gsub('%s+$', '')endfunction ltrim(s)return s:gsub('^%s+', '')endfunction rtrim(s)return s:gsub('%s+$', '')endfunction starts_with(s, prefix)if #s < #prefix thenreturn falseendfor i=1,#prefix doif s:sub(i,i) ~= prefix:sub(i,i) thenreturn falseendendreturn trueendfunction ends_with(s, suffix)if #s < #suffix thenreturn falseendfor i=0,#suffix-1 doif s:sub(#s-i,#s-i) ~= suffix:sub(#suffix-i,#suffix-i) thenreturn falseendendreturn trueend
local points=deepcopy(line.points)--? print('copying', line.points, 'with', #line.points, 'points into', points)local shapes=deepcopy(line.shapes)--? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes)table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}})--? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}})
local frag, frag_text = f.data, f.textselect_color(frag)local frag_len = utf8.len(frag)--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
select_color(f)local frag_len = utf8.len(f)--? print('text.draw:', f, 'at', line_index,pos, 'after', x,y)
local filename_text = App.newText(love.graphics.getFont(), filename)button(State, 'link', {x=x+App.width(to_text('[[')), y=y, w=App.width(filename_text), h=State.line_height, color={1,1,1},
button(State, 'link', {x=x+App.width('[['), y=y, w=App.width(filename), h=State.line_height, color={1,1,1},
Text.populate_screen_line_starting_pos(State, line_index)select_color(f)local frag_len = utf8.len(f)--? print('text.draw:', f, 'at', line_index,pos, 'after', x,y)--? print('skipping', f)local frag_width = App.width(f)local trimmed_word = rtrim(f) -- compute_fragments puts whitespace at the endbutton(State, 'link', {x=x+App.width('[['), y=y, w=App.width(filename), h=State.line_height, color={1,1,1},App.screen.print(f, x,y)Text.draw_cursor(State, x+Text.x(f, State.cursor1.pos-pos+1), y)
local frag_text = App.newText(love.graphics.getFont(), frag)local frag_width = App.width(frag_text)--? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')
local frag_width = App.width(frag)
local frag_width = App.width(frag)local frag1_width = App.width(frag1)table.insert(line_cache.fragments, frag1)frag_width = App.width(frag)table.insert(line_cache.fragments, frag)
return App.width(screen_line)return App.width(s_before)return App.width(s_before)
local line_info = {mode='text'}if line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')elseline_info.data = lineendtable.insert(result, line_info)
table.insert(result, {mode='text', data=line})
-- helpers for selecting portions of text-- Return any intersection of the region from State.selection1 to State.cursor1 (or-- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and-- currently over a drawing) with the region between {line=line_index, pos=apos}-- and {line=line_index, pos=bpos}.-- apos must be less than bpos. However State.selection1 and State.cursor1 can be in any order.-- Result: positions spos,epos between apos,bpos.function Text.clip_selection(State, line_index, apos, bpos)if State.selection1.line == nil then return nil,nil end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxpif App.mouse_down(1) thenmaxl,maxp = Text.mouse_pos(State)elsemaxl,maxp = State.cursor1.line,State.cursor1.posendif Text.lt1({line=maxl, pos=maxp},{line=minl, pos=minp}) thenminl,maxl = maxl,minlminp,maxp = maxp,minpend-- check if intervals are disjointif line_index < minl then return nil,nil endif line_index > maxl then return nil,nil endif line_index == minl and bpos <= minp then return nil,nil endif line_index == maxl and apos >= maxp then return nil,nil end-- compare bounds more carefully (start inclusive, end exclusive)local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos})local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp})--? print(minl,line_index,maxl, '--', minp,apos,bpos,maxp, '--', a_ge,b_lt)if a_ge and b_lt then-- fully containedreturn apos,bposelseif a_ge thenassert(maxl == line_index)return apos,maxpelseif b_lt thenassert(minl == line_index)return minp,bposelseassert(minl == maxl and minl == line_index)return minp,maxpendend-- draw highlight for line corresponding to (lo,hi) given an approximate x,y and pos on the same screen line-- Creates text objects every time, so use this sparingly.-- Returns some intermediate computation useful elsewhere.function Text.draw_highlight(State, line, x,y, pos, lo,hi)if lo thenlocal lo_offset = Text.offset(line.data, lo)local hi_offset = Text.offset(line.data, hi)local pos_offset = Text.offset(line.data, pos)local lo_pxif pos == lo thenlo_px = 0else
App.color(Highlight_color)
App.color(Text_color)return lo_pxendend-- inefficient for some reason, so don't do it on every framefunction Text.mouse_pos(State)local time = love.timer.getTime()if State.recent_mouse.time and State.recent_mouse.time > time-0.1 thenreturn State.recent_mouse.line, State.recent_mouse.posendState.recent_mouse.time = timelocal line,pos = Text.to_pos(State, App.mouse_x(), App.mouse_y())if line thenState.recent_mouse.line = lineState.recent_mouse.pos = posendreturn State.recent_mouse.line, State.recent_mouse.posendfunction Text.to_pos(State, x,y)for line_index,line in ipairs(State.lines) doif Text.in_line(State, line_index, x,y) thenreturn line_index, Text.to_pos_on_line(State, line_index, x,y)endendendfunction Text.cut_selection(State)if State.selection1.line == nil then return endlocal result = Text.selection(State)Text.delete_selection(State)return resultendfunction Text.delete_selection(State)if State.selection1.line == nil then return endlocal minl,maxl = minmax(State.selection1.line, State.cursor1.line)local before = snapshot(State, minl, maxl)Text.delete_selection_without_undo(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})endfunction Text.delete_selection_without_undo(State)if State.selection1.line == nil then return end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxp = State.cursor1.line,State.cursor1.posif minl > maxl thenminl,maxl = maxl,minlminp,maxp = maxp,minpelseif minl == maxl thenif minp > maxp thenminp,maxp = maxp,minpendend-- update State.cursor1 and State.selection1State.cursor1.line = minlState.cursor1.pos = minpif Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1.line = State.cursor1.lineState.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)endState.selection1 = {}-- delete everything between min (inclusive) and max (exclusive)Text.clear_screen_line_cache(State, minl)local min_offset = Text.offset(State.lines[minl].data, minp)local max_offset = Text.offset(State.lines[maxl].data, maxp)if minl == maxl then--? print('minl == maxl')State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)returnendassert(minl < maxl)local rhs = State.lines[maxl].data:sub(max_offset)for i=maxl,minl+1,-1 dotable.remove(State.lines, i)table.remove(State.line_cache, i)endState.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhsendfunction Text.selection(State)if State.selection1.line == nil then return end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxp = State.cursor1.line,State.cursor1.posif minl > maxl thenminl,maxl = maxl,minlminp,maxp = maxp,minpelseif minl == maxl thenif minp > maxp thenminp,maxp = maxp,minpendendlocal min_offset = Text.offset(State.lines[minl].data, minp)local max_offset = Text.offset(State.lines[maxl].data, maxp)if minl == maxl thenreturn State.lines[minl].data:sub(min_offset, max_offset-1)endassert(minl < maxl)local result = {State.lines[minl].data:sub(min_offset)}for i=minl+1,maxl-1 dotable.insert(result, State.lines[i].data)endtable.insert(result, State.lines[maxl].data:sub(1, max_offset-1))return table.concat(result, '\n')end
run = {}Editor_state = {}-- called both in tests and real runfunction run.initialize_globals()-- tests currently mostly clear their own state-- a few text objects we can avoid recomputing unless the font changesText_cache = {}-- blinking cursorCursor_time = 0end-- called only for real runfunction run.initialize(arg)log_new('run')if Settings thenrun.load_settings()elserun.initialize_default_settings()endif #arg > 0 thenEditor_state.filename = arg[1]load_from_disk(Editor_state)Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}elseload_from_disk(Editor_state)Text.redraw_all(Editor_state)endedit.check_locs(Editor_state)-- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708love.window.setTitle('text.love - '..Editor_state.filename)if #arg > 1 thenprint('ignoring commandline args after '..arg[1])endif rawget(_G, 'jit') thenjit.off()jit.flush()endendfunction run.load_settings()love.graphics.setFont(love.graphics.newFont(Settings.font_height))-- determine default dimensions and flagsApp.screen.width, App.screen.height, App.screen.flags = App.screen.size()-- set up desired window dimensionsApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(App.screen.width, 200)App.screen.flags.minheight = math.min(App.screen.height, 200)App.screen.width, App.screen.height = Settings.width, Settings.heightApp.screen.resize(App.screen.width, App.screen.height, App.screen.flags)App.screen.move(Settings.x, Settings.y, Settings.displayindex)Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, Settings.font_height, math.floor(Settings.font_height*1.3))Editor_state.filename = Settings.filenameEditor_state.screen_top1 = Settings.screen_topEditor_state.cursor1 = Settings.cursorendfunction run.initialize_default_settings()local font_height = 20love.graphics.setFont(love.graphics.newFont(font_height))
Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)Editor_state.font_height = font_heightEditor_state.line_height = math.floor(font_height*1.3)Settings = run.settings()endfunction run.initialize_window_geometry(em_width)local os = love.system.getOS()if os == 'Android' or os == 'iOS' then-- maximizing on iOS breaks text rendering: https://github.com/deltadaedalus/vudu/issues/7-- no point second-guessing window dimensions on mobileApp.screen.width, App.screen.height, App.screen.flags = App.screen.size()returnend-- maximize windowApp.screen.resize(0, 0) -- maximizeApp.screen.width, App.screen.height, App.screen.flags = App.screen.size()-- shrink height slightly to account for window decorationApp.screen.height = App.screen.height-100App.screen.width = 40*em_widthApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(App.screen.width, 200)App.screen.flags.minheight = math.min(App.screen.height, 200)App.screen.resize(App.screen.width, App.screen.height, App.screen.flags)endfunction run.resize(w, h)--? print(("Window resized to width: %d and height: %d."):format(w, h))App.screen.width, App.screen.height = w, hText.redraw_all(Editor_state)Editor_state.selection1 = {} -- no support for shift drag while we're resizingEditor_state.right = App.screen.width-Margin_rightEditor_state.width = Editor_state.right-Editor_state.leftText.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)endfunction run.file_drop(file)-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)end-- clear the slate for the new fileApp.initialize_globals()Editor_state.filename = file:getFilename()file:open('r')Editor_state.lines = load_from_file(file)file:close()Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}-- keep a few blank lines around: https://merveilles.town/@akkartik/110084833821965708love.window.setTitle('text.love - '..Editor_state.filename)endfunction run.draw()edit.draw(Editor_state)endfunction run.update(dt)Cursor_time = Cursor_time + dtedit.update(Editor_state, dt)endfunction run.quit()edit.quit(Editor_state)endfunction run.settings()if Settings == nil thenSettings = {}endif Current_app == 'run' thenSettings.x, Settings.y, Settings.displayindex = App.screen.position()endlocal filename = Editor_state.filenameif is_relative_path(filename) thenfilename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windowsendreturn {x=Settings.x, y=Settings.y, displayindex=Settings.displayindex,width=App.screen.width, height=App.screen.height,font_height=Editor_state.font_height,filename=filename,screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1}endfunction run.mouse_press(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_press(Editor_state, x,y, mouse_button)endfunction run.mouse_release(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_release(Editor_state, x,y, mouse_button)endfunction run.mouse_wheel_move(dx,dy)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.mouse_wheel_move(Editor_state, dx,dy)endfunction run.text_input(t)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.text_input(Editor_state, t)endfunction run.keychord_press(chord, key)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.keychord_press(Editor_state, chord, key)endfunction run.key_release(key, scancode)Cursor_time = 0 -- ensure cursor is visible immediately after it movesreturn edit.key_release(Editor_state, key, scancode)end
end
love.graphics.draw(section_text, xleft+50,y)love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)
love.graphics.print(line.section_name, xleft+50,y)love.graphics.line(xleft+50+App.width(line.section_name)+2,sectiony, xright,sectiony)
love.graphics.draw(section_text, xleft+50,y)love.graphics.line(xleft+50+App.width(section_text)+2,sectiony, xright,sectiony)
love.graphics.print(line.section_name, xleft+50,y)love.graphics.line(xleft+50+App.width(line.section_name)+2,sectiony, xright,sectiony)
love.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)
love.graphics.print(line.section_stack[i].name, x+State.font_height+5, App.screen.height-App.width(line.section_stack[i].name)-5, --[[vertically]] math.pi/2)
love.graphics.print(line.section_stack[i].name, x, App.screen.height-App.width(to_text(line.section_stack[i].name))-5, --[[vertically]] math.pi/2)
love.graphics.print(line.section_stack[i].name, x, App.screen.height-App.width(line.section_stack[i].name)-5, --[[vertically]] math.pi/2)
-- some constants people might like to tweakText_color = {r=0, g=0, b=0}Cursor_color = {r=1, g=0, b=0}Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering overHighlight_color = {r=0.7, g=0.7, b=0.9} -- selected textMargin_top = 15Margin_left = 25Margin_right = 25edit = {}-- run in both tests and a real runfunction edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screenlocal result = {lines = {{data=''}}, -- array of strings-- Lines can be too long to fit on screen, in which case they _wrap_ into-- multiple _screen lines_.-- rendering wrapped text lines needs some additional short-lived data per line:-- startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen-- starty, the y coord in pixels the line starts rendering from
-- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen lineline_cache = {},-- Given wrapping, any potential location for the text cursor can be described in two ways:-- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)-- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.---- Most of the time we'll only persist positions in schema 1, translating to-- schema 2 when that's convenient.---- Make sure these coordinates are never aliased, so that changing one causes-- action at a distance.screen_top1 = {line=1, pos=1}, -- position of start of screen line at top of screencursor1 = {line=1, pos=1}, -- position of cursorscreen_bottom1 = {line=1, pos=1}, -- position of start of screen line at bottom of screenselection1 = {},-- some extra state to compute selection between mouse press and releaseold_cursor1 = nil,old_selection1 = nil,mousepress_shift = nil,-- when selecting text, avoid recomputing some state on every single framerecent_mouse = {},-- cursor coordinates in pixelscursor_x = 0,cursor_y = 0,font_height = font_height,line_height = line_height,top = top,left = math.floor(left),right = math.floor(right),width = right-left,filename = love.filesystem.getSourceBaseDirectory()..'/lines.txt', -- '/' should work even on Windowsnext_save = nil,-- undohistory = {},next_history = 1,-- searchsearch_term = nil,search_backup = nil, -- stuff to restore when cancelling search}return resultend -- App.initialize_statefunction edit.check_locs(State)-- if State is inconsistent (i.e. file changed by some other program),-- throw away all cursor state entirelyif edit.invalid1(State, State.screen_top1)or edit.invalid1(State, State.cursor1)or not Text.le1(State.screen_top1, State.cursor1) thenState.screen_top1 = {line=1, pos=1}State.cursor1 = {line=1, pos=1}endendfunction edit.invalid1(State, loc1)if loc1.line > #State.lines then return true endlocal l = State.lines[loc1.line]if l.mode ~= 'text' then return false end -- pos is irrelevant to validity for a drawing linereturn loc1.pos > #State.lines[loc1.line].dataendfunction edit.draw(State)App.color(Text_color)if #State.lines ~= #State.line_cache thenprint(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))assert(false)endif not Text.le1(State.screen_top1, State.cursor1) thenprint(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)assert(false)endState.cursor_x = nilState.cursor_y = nillocal y = State.top--? print('== draw')for line_index = State.screen_top1.line,#State.lines dolocal line = State.lines[line_index]--? print('draw:', y, line_index, line)if y + State.line_height > App.screen.height then break endState.screen_bottom1 = {line=line_index, pos=nil}--? print('text.draw', y, line_index)local startpos = 1if line_index == State.screen_top1.line thenstartpos = State.screen_top1.posendy, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos)y = y + State.line_height--? print('=> y', y)endif State.search_term thenText.draw_search_bar(State)endendfunction edit.update(State, dt)if State.next_save and State.next_save < Current_time thensave_to_disk(State)State.next_save = nilendendfunction schedule_save(State)if State.next_save == nil thenState.next_save = Current_time + 3 -- short enough that you're likely to still remember what you didendendfunction edit.quit(State)-- make sure to save before quittingif State.next_save thensave_to_disk(State)-- give some time for the OS to flush everything to disklove.timer.sleep(0.1)endendfunction edit.mouse_press(State, x,y, mouse_button)if State.search_term then return end--? print('press', State.cursor1.line)for line_index,line in ipairs(State.lines) doif Text.in_line(State, line_index, x,y) then-- delicate dance between cursor, selection and old cursor/selection-- scenarios:-- regular press+release: sets cursor, clears selection-- shift press+release:-- sets selection to old cursor if not set otherwise leaves it untouched-- sets cursor-- press and hold to start a selection: sets selection on press, cursor on release-- press and hold, then press shift: ignore shift-- i.e. mouse_released should never look at shift stateState.old_cursor1 = State.cursor1State.old_selection1 = State.selection1State.mousepress_shift = App.shift_down()State.selection1 = {line=line_index,pos=Text.to_pos_on_line(State, line_index, x, y),}--? print('selection', State.selection1.line, State.selection1.pos)breakendendendfunction edit.mouse_release(State, x,y, mouse_button)if State.search_term then return end--? print('release', State.cursor1.line)for line_index,line in ipairs(State.lines) doif Text.in_line(State, line_index, x,y) then--? print('reset selection')State.cursor1 = {line=line_index,pos=Text.to_pos_on_line(State, line_index, x, y),}--? print('cursor', State.cursor1.line, State.cursor1.pos)if State.mousepress_shift thenif State.old_selection1.line == nil thenState.selection1 = State.old_cursor1elseState.selection1 = State.old_selection1endendState.old_cursor1, State.old_selection1, State.mousepress_shift = nilif eq(State.cursor1, State.selection1) thenState.selection1 = {}endbreakendend--? print('selection:', State.selection1.line, State.selection1.pos)endfunction edit.mouse_wheel_move(State, dx,dy)if dy > 0 thenState.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}for i=1,math.floor(dy) doText.up(State)endelseif dy < 0 thenState.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}for i=1,math.floor(-dy) doText.down(State)endendendfunction edit.text_input(State, t)--? print('text input', t)if State.search_term thenState.search_term = State.search_term..tText.search_next(State)elsefor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollText.text_input(State, t)endschedule_save(State)endfunction edit.keychord_press(State, chord, key)if State.selection1.line and-- printable character created using shift key => delete selection-- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys)(not App.shift_down() or utf8.len(key) == 1) andchord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and not App.is_cursor_movement(chord) thenText.delete_selection(State, State.left, State.right)endif State.search_term thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollif chord == 'escape' thenState.search_term = nilState.cursor1 = State.search_backup.cursorState.screen_top1 = State.search_backup.screen_topState.search_backup = nilText.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leakselseif chord == 'return' thenState.search_term = nilState.search_backup = nilelseif chord == 'backspace' thenlocal len = utf8.len(State.search_term)local byte_offset = Text.offset(State.search_term, len)State.search_term = string.sub(State.search_term, 1, byte_offset-1)elseif chord == 'down' thenState.cursor1.pos = State.cursor1.pos+1Text.search_next(State)elseif chord == 'up' thenText.search_previous(State)endreturnelseif chord == 'C-f' thenState.search_term = ''State.search_backup = {cursor={line=State.cursor1.line, pos=State.cursor1.pos},screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},}-- zoomelseif chord == 'C-=' thenedit.update_font_settings(State, State.font_height+2)Text.redraw_all(State)elseif chord == 'C--' thenif State.font_height > 2 thenedit.update_font_settings(State, State.font_height-2)Text.redraw_all(State)endelseif chord == 'C-0' thenedit.update_font_settings(State, 20)Text.redraw_all(State)-- undoelseif chord == 'C-z' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal event = undo_event(State)if event thenlocal src = event.beforeState.screen_top1 = deepcopy(src.screen_top)State.cursor1 = deepcopy(src.cursor)State.selection1 = deepcopy(src.selection)patch(State.lines, event.after, event.before)patch_placeholders(State.line_cache, event.after, event.before)-- if we're scrolling, reclaim all fragments to avoid memory leaksText.redraw_all(State)schedule_save(State)endelseif chord == 'C-y' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal event = redo_event(State)if event thenlocal src = event.afterState.screen_top1 = deepcopy(src.screen_top)State.cursor1 = deepcopy(src.cursor)State.selection1 = deepcopy(src.selection)patch(State.lines, event.before, event.after)-- if we're scrolling, reclaim all fragments to avoid memory leaksText.redraw_all(State)schedule_save(State)end-- clipboardelseif chord == 'C-a' thenState.selection1 = {line=1, pos=1}State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}elseif chord == 'C-c' thenlocal s = Text.selection(State)if s thenApp.setClipboardText(s)endelseif chord == 'C-x' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal s = Text.cut_selection(State, State.left, State.right)if s thenApp.setClipboardText(s)endschedule_save(State)elseif chord == 'C-v' thenfor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll-- We don't have a good sense of when to scroll, so we'll be conservative-- and sometimes scroll when we didn't quite need to.local before_line = State.cursor1.linelocal before = snapshot(State, before_line)local clipboard_data = App.getClipboardText()for _,code in utf8.codes(clipboard_data) dolocal c = utf8.char(code)if c == '\n' thenText.insert_return(State)elseText.insert_at_cursor(State, c)endendif Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})-- dispatch to textelsefor _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollText.keychord_press(State, chord)endendfunction edit.key_release(State, key, scancode)endfunction edit.update_font_settings(State, font_height)State.font_height = font_heightlove.graphics.setFont(love.graphics.newFont(State.font_height))State.line_height = math.floor(font_height*1.3)Text_cache = {}end--== some methods for tests-- Insulate tests from some key globals so I don't have to change the vast-- majority of tests when they're modified for the real app.Test_margin_left = 25Test_margin_right = 0function edit.initialize_test_state()-- if you change these values, tests will start failingreturn edit.initialize_state(15, -- top marginTest_margin_left,App.screen.width - Test_margin_right,14, -- font height assuming default LÖVE font15) -- line heightend-- all text_input events are also keypresses-- TODO: handle chords of multiple keysfunction edit.run_after_text_input(State, t)edit.keychord_press(State, t)edit.text_input(State, t)edit.key_release(State, t)App.screen.contents = {}edit.update(State, 0)edit.draw(State)end-- not all keys are text_inputfunction edit.run_after_keychord(State, chord)edit.keychord_press(State, chord)edit.key_release(State, chord)App.screen.contents = {}edit.update(State, 0)edit.draw(State)endfunction edit.run_after_mouse_click(State, x,y, mouse_button)App.fake_mouse_press(x,y, mouse_button)edit.mouse_press(State, x,y, mouse_button)App.fake_mouse_release(x,y, mouse_button)edit.mouse_release(State, x,y, mouse_button)App.screen.contents = {}edit.update(State, 0)edit.draw(State)endfunction edit.run_after_mouse_press(State, x,y, mouse_button)App.fake_mouse_press(x,y, mouse_button)edit.mouse_press(State, x,y, mouse_button)App.screen.contents = {}edit.update(State, 0)edit.draw(State)endfunction edit.run_after_mouse_release(State, x,y, mouse_button)App.fake_mouse_release(x,y, mouse_button)edit.mouse_release(State, x,y, mouse_button)App.screen.contents = {}edit.update(State, 0)edit.draw(State)end
App.open_for_reading =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('r')if ok thenreturn resultelsereturn ok, errendendApp.open_for_writing =function(filename)local result = love.filesystem.newFile(filename)local ok, err = result:open('w')if ok thenreturn resultelsereturn ok, errendend