fix a bad merge

[?]
Sep 16, 2023, 7:07 AM
R2ASHK5CEE3PTRLS37GP4PXJ7HIGJ6UD72KKBI57UDJI7VRROQGQC

Dependencies

  • [2] 6ECYOEHY bugfix: obsolete location for attribute
  • [3] PV2YA7KS subsection headings in a long switch
  • [4] RRDO6H7H bugfix
  • [5] 5ITAXPEP wait a little to flush disk before quitting
  • [6] 3KMHGUMH Merge lines.love
  • [7] 34BZ5ZKN Merge lines.love
  • [8] UD7HNQL7 purge unused button infrastructure
  • [9] QQBP3G6W return height of editor widget after drawing
  • [10] WLJCIXYM add state arg to a few functions
  • [11] 4EGQRXDA bugfix: naming points
  • [12] UH4YWHW5 button framework is at the app level
  • [13] HNZMFBMQ Merge lines.love
  • [14] PP2IIHL6 stop putting button state in a global
  • [15] BH7BT36L ctrl+a: select entire buffer
  • [16] GN3C6AGM bugfix in changing shape mid-stroke
  • [17] SW7BSBMJ several bugfixes in saving/loading cursor position
  • [18] 2MGBV7NP bugfix: crash when using mouse wheel
  • [19] ISOFHXB2 App.width can no longer take a Text
  • [20] U3MJNFUY Merge lines.love
  • [21] UHB4GARJ left/right margin -> left/right coordinates
  • [22] 4KC7I3E2 make colors easier to edit
  • [23] 3QQZ7W4E bring couple more globals back to the app level
  • [24] LF7BWEG4 group all editor globals
  • [25] O4RRXNOK bugfix: disallow font size of 0
  • [26] TXI6GSQD some minor cleanup
  • [27] 5ZA3BRNY add state arg to a few functions
  • [28] TGHAJBES use line cache for drawings as well
  • [29] ELJNEPW2 simplify cursor-on-screen check
  • [30] QJISOCHJ some temporary logging to catch a bug
  • [31] 2CK5QI7W make love event names consistent
  • [32] LDFXFRUO bring a few things in sync between run and source
  • [33] GFXWHTE6 mouse wheel support
  • [34] NVSC4N4K change a helper slightly
  • [35] X3F7ECSL add state arg to some functions
  • [36] WJBZZQE4 fold together two largely similar cases
  • [37] VZJHGWSP Merge lines.love
  • [38] LNUHQOGH start passing in Editor_state explicitly
  • [39] AF253GHL bugfix
  • [40] MD3W5IRA new fork: rip out drawing support
  • [41] 6D5MOJS4 allow buttons to interrupt events
  • [42] WLWNS6FB a bug I've never run into
  • [43] SPNMXTYR have file API operate on state object
  • [44] APX2PY6G stop tracking wallclock time
  • [45] MU2HIRR6 Merge lines.love
  • [46] VOU73AK6 Merge lines.love
  • [47] 3XNFQDDN Merge lines.love
  • [48] QCPXQ2E3 add state arg to a few functions
  • [49] PWDBOOWJ copying to clipboard can never scroll
  • [50] WK6UK5AJ enhance bugfix of commit a9aa3436f (Dec 2024)
  • [51] YFW4MNNP handle wrapping lines
  • [52] CNCYMM6A make test initializations a little more obvious
  • [53] YRJFJNUD bugfix
  • [54] KWIVKQQ7 Merge lines.love
  • [55] Z5HLXU4P add state arg to a few functions
  • [56] APYPFFS3 call edit rather than App callbacks in tests
  • [57] 5UKUADTW distinguish consistently between mouse buttons and other buttons
  • [58] SPSW74Y5 add state arg to Text.keychord_pressed
  • [59] H6QZ7GRR more precise name
  • [60] 5BMR5HRT click to the left of a line
  • [61] VP5KC4XZ Merge lines.love
  • [62] OGD5RAQK bugfix: naming points in drawings
  • [63] ORKN6EOB Merge lines.love
  • [64] 7FPELAZB ah, I see the problem
  • [65] FPY4LO2W make a few names consistent with snake_case
  • [66] 32V6ZHQB Merge lines.love
  • [67] PJEQCTBL add state arg to Drawing.update
  • [68] MUJTM6RE bring back a level of wrapping
  • [69] 3PSFWAIL Merge lines.love
  • [70] 2WGHUWE6 self-documenting 0 Test_right_margin
  • [71] 2Y7YH7UP infrastructure for caching LÖVE text objects
  • [72] ILOA5BYF separate data structure for each line's cache data
  • [73] KYNGDE2C consistent names in a few more places
  • [74] 3MAZEQK5 add state arg to Text.textinput
  • [75] S2MISTTM add state arg to a few functions
  • [76] Z3IQ6A4R bugfix
  • [77] OI4FPFIN support drawings in the source editor
  • [78] 7VGDIPLC more robust state validation
  • [79] IFTYOERM line.y -> line_cache.starty in a few more places
  • [80] GZ5WULJV switch source side to new screen-line-based render
  • [81] Z3BQO2RK typo
  • [82] 6XCJX4DZ bugfix: inscript's bug
  • [83] ONHKBLLC Merge lines.love
  • [84] MTJEVRJR add state arg to a few functions
  • [85] PTDO2SOT add state arg to schedule_save
  • [86] 2L5MEZV3 experiment: new edit namespace
  • [87] ERQKFTPV extract method
  • [88] 6RYLD5ON change how we handle clicks above top margin
  • [89] MOAEVTKJ clean up a few more loose ends

Change contents

  • replacement in edit.lua at line 98
    [8.22445][8.1951:1996](),[8.9658][8.6003:6003](),[8.470][5.26:113](),[8.11328][8.6653:6653](),[8.9616][8.11329:11507](),[8.1870][8.6654:6847](),[8.5593][8.170:199](),[8.6985][8.134:241](),[8.6870][3.26:36](),[8.11602][8.75:75](),[8.756][8.364:368](),[8.258][8.364:368](),[8.7283][8.756:756](),[8.258][8.7264:7283](),[8.7264][8.7264:7283](),[8.7264][8.234:258](),[8.567][8.7237:7264](),[8.6186][8.7237:7264](),[8.7237][8.7237:7264](),[8.519][8.6139:6186](),[8.7099][8.411:519](),[8.233][8.7075:7099](),[8.7075][8.7075:7099](),[8.7075][8.209:233](),[8.410][8.7048:7075](),[8.880][8.7048:7075](),[8.7048][8.7048:7075](),[8.363][8.835:880](),[8.6915][8.259:363](),[8.208][8.6891:6915](),[8.6891][8.6891:6915](),[8.6891][8.184:208](),[8.258][8.6864:6891](),[8.6138][8.6864:6891](),[8.6864][8.6864:6891](),[8.210][8.6091:6138](),[8.834][8.166:210](),[8.166][8.166:210](),[8.119][8.789:834](),[8.6651][8.15:119](),[8.183][8.6627:6651](),[8.6627][8.6627:6651](),[8.6627][8.159:183](),[8.6090][8.6600:6627](),[8.6600][8.6600:6627](),[8.6528][8.6021:6090](),[8.6020][8.6481:6528](),[8.6481][8.6481:6528](),[8.6451][8.5989:6020](),[8.16047][8.6450:6451](),[8.6449][8.16043:16047](),[8.9642][8.16043:16047](),[8.16043][8.16043:16047](),[8.158][8.6430:6449](),[8.6430][8.6430:6449](),[8.6430][8.134:158](),[8.5988][8.6403:6430](),[8.6403][8.6403:6430](),[8.6268][8.5854:5988](),[8.5853][8.6228:6268](),[8.6228][8.6228:6268](),[8.6184][8.5808:5853](),[8.5729][8.6183:6184](),[8.6183][8.6183:6184](),[8.1119][8.5645:5729](),[8.5645][8.5645:5729](),[8.5600][8.1075:1119](),[8.1074][8.5422:5600](),[8.5422][8.5422:5600](),[8.5422][8.1052:1074](),[8.1051][8.5400:5422](),[8.5400][8.5400:5422](),[8.5400][8.915:1051](),[8.447][8.5371:5400](),[8.6146][8.5371:5400](),[8.329][8.442:447](),[8.442][8.442:447](),[4.92][8.337:387](),[8.337][8.337:387](),[8.264][4.26:92](),[8.6146][8.175:264](),[8.5807][8.6141:6146](),[8.9642][8.6141:6146](),[8.16001][8.5759:5807](),[8.5758][8.15990:16001](),[8.7139][8.15990:16001](),[8.15990][8.15990:16001](),[8.1400][8.5720:5758](),[8.15878][8.1293:1400](),[8.75][8.15871:15878](),[8.631][8.15871:15878](),[8.8798][8.15871:15878](),[8.11602][8.15871:15878](),[8.15871][8.15871:15878](),[8.2812][8.11580:11602](),[8.577][8.2710:2812](),[8.14368][8.552:577](),[8.1438][8.14360:14368](),[8.8913][8.14360:14368](),[8.23116][8.14360:14368](),[8.105181][8.14360:14368](),[8.14360][8.14360:14368](),[8.360][8.23041:23116](),[8.1331][8.23041:23116](),[8.14227][8.315:360](),[8.8706][8.14209:14227](),[8.14209][8.14209:14227](),[8.14176][8.8666:8706](),[8.8665][8.14165:14176](),[8.14165][8.14165:14176](),[8.14136][8.8631:8665](),[8.454][8.14032:14136](),[8.14032][8.14032:14136](),[8.2709][8.407:454](),[8.8812][8.2661:2709](),[8.13904][8.8769:8812](),[2.230][8.13768:13904](),[8.1249][8.13768:13904](),[8.8768][8.13768:13904](),[8.105015][8.13768:13904](),[8.13768][8.13768:13904](),[8.13689][2.123:230](),[8.551][8.13660:13689](),[8.13660][8.13660:13689](),[8.13640][8.526:551](),[8.406][8.13632:13640](),[8.13632][8.13632:13640](),[8.13602][8.379:406](),[8.3962][8.13588:13602](),[8.8682][8.13588:13602](),[8.23040][8.13588:13602](),[8.104922][8.13588:13602](),[8.13588][8.13588:13602](),[8.1158][8.22975:23040](),[8.8593][8.22975:23040](),[8.13432][8.1051:1158](),[8.378][8.13395:13432](),[8.13395][8.13395:13432](),[8.13365][8.351:378](),[8.3865][8.13351:13365](),[8.13351][8.13351:13365](),[8.1050][8.3829:3865](),[8.8507][8.3829:3865](),[8.657][8.13212:13241](),[8.13212][8.13212:13241](),[8.13212][8.501:657](),[8.525][8.13189:13212](),[8.13189][8.13189:13212](),[8.398][8.498:525](),[8.7066][8.498:525](),[8.13167][8.498:525](),[8.296][8.296:398](),[8.12904][8.8226:8421](),[8.2660][8.12856:12904](),[8.12856][8.12856:12904](),[8.942][8.2624:2660](),[8.8225][8.2624:2660](),[8.12746][8.835:942](),[8.497][8.12709:12746](),[8.12709][8.12709:12746](),[8.206][8.470:497](),[8.6968][8.470:497](),[8.12687][8.470:497](),[8.104][8.104:206](),[8.8139][8.1967:2037](),[8.12424][8.7944:8139](),[8.2623][8.12375:12424](),[8.12375][8.12375:12424](),[2.122][8.2587:2623](),[8.834][8.2587:2623](),[8.7943][8.2587:2623](),[8.12265][2.15:122](),[3.36][8.12236:12265](),[8.6870][8.12236:12265](),[8.12236][8.12236:12265](),[8.174][8.6843:6870](),[8.12214][8.6843:6870](),[8.12181][8.133:174](),[8.289][8.12152:12181](),[8.6842][8.12152:12181](),[8.12152][8.12152:12181](),[8.12086][8.158:289](),[8.6814][8.12057:12086](),[8.12057][8.12057:12086](),[8.73][8.6787:6814](),[8.7806][8.6787:6814](),[8.11991][8.15:73](),[3.25][8.11962:11991](),[8.7755][8.11962:11991](),[8.103883][8.11962:11991](),[8.11962][8.11962:11991](),[8.7755][3.15:25](),[8.7565][8.25240:25412](),[8.11787][8.7538:7565](),[8.2183][8.11739:11787](),[8.11739][8.11739:11787](),[8.11710][8.2149:2183](),[8.2148][8.11680:11710](),[8.11680][8.11680:11710](),[8.7537][8.2118:2148](),[8.11621][8.7491:7537](),[8.7490][8.11589:11621](),[8.103555][8.11589:11621](),[8.11589][8.11589:11621](),[8.11407][8.7278:7460](),[8.7277][8.11370:11407](),[8.103307][8.11370:11407](),[8.11370][8.11370:11407](),[8.7245][8.7245:7277](),[8.11296][8.7185:7215](),[8.6786][8.11262:11296](),[8.11262][8.11262:11296](),[8.7184][8.6689:6786](),[8.7046][8.7046:7184](),[8.11014][8.6986:7016](),[8.241][8.10984:11014](),[8.6985][8.10984:11014](),[8.102945][8.10984:11014](),[8.10984][8.10984:11014](),[8.10962][8.6957:6985](),[8.3828][8.10956:10962](),[8.6956][8.10956:10962](),[8.22974][8.10956:10962](),[8.102909][8.10956:10962](),[8.10956][8.10956:10962](),[8.325][8.22916:22974](),[8.363][8.22916:22974](),[8.401][8.22916:22974](),[8.500][8.22916:22974](),[8.10886][8.22916:22974](),[8.10752][8.214:401](),[8.6873][8.10522:10752](),[8.102812][8.10522:10752](),[8.10522][8.10522:10752](),[8.5675][8.6800:6831](),[8.6800][8.6800:6831](),[8.10343][8.5627:5675](),[8.469][8.10338:10343](),[8.10338][8.10338:10343](),[8.10320][8.446:469](),[8.162][8.10314:10320](),[8.273][8.10314:10320](),[8.806][8.10314:10320](),[8.8796][8.10314:10320](),[8.10314][8.10314:10320](),[8.8765][8.8766:8796](),[8.133][8.8765:8765](),[8.10292][8.26:133](),[8.2586][8.10285:10292](),[8.6749][8.10285:10292](),[8.102724][8.10285:10292](),[8.10285][8.10285:10292](),[8.6495][8.2089:2117](),[8.199][8.6394:6467](),[8.743][8.6394:6467](),[8.5593][8.6394:6467](),[8.6394][8.6394:6467](),[8.9696][8.5558:5593](),[8.11579][8.9691:9696](),[8.9691][8.9691:9696](),[8.2225][8.9685:9691](),[8.6276][8.9685:9691](),[8.102222][8.9685:9691](),[8.9685][8.9685:9691](),[8.212][8.2163:2225](),[8.2163][8.2163:2225](),[8.2163][8.167:212](),[8.182][8.2080:2163](),[8.2080][8.2080:2163](),[8.2073][8.161:182](),[8.166][8.2014:2073](),[8.2014][8.2014:2073](),[8.2014][8.121:166](),[8.2028][8.1870:2014](),[8.6847][8.1870:2014](),[8.1870][8.1870:2014](),[8.936][8.1864:1870](),[8.1241][8.1864:1870](),[8.3165][8.1864:1870](),[8.6276][8.1864:1870](),[8.11507][8.9616:9624](),[8.9616][8.9616:9624](),[8.73][8.9594:9616](),[8.6653][8.9594:9616](),[8.8763][8.9594:9616](),[8.11328][8.9594:9616](),[8.9594][8.9594:9616](),[8.6651][8.11132:11328](),[8.11132][8.11132:11328](),[8.11067][8.6539:6651](),[8.6538][8.10948:11067](),[8.10948][8.10948:11067](),[8.10913][8.6459:6538](),[8.71][8.10816:10913](),[8.6458][8.10816:10913](),[8.8468][8.10816:10913](),[8.5338][8.6339:6458](),[8.5504][8.5299:5338](),[8.5299][8.5299:5338](),[8.8364][8.5450:5504](),[8.2595][8.8359:8364](),[8.8359][8.8359:8364](),[8.8359][8.2287:2595](),[8.6338][8.8345:8359](),[8.8761][8.8345:8359](),[8.10815][8.8345:8359](),[8.8345][8.8345:8359](),[8.6324][8.6325:6338](),[8.10729][8.6324:6324](),[8.6322][8.10473:10729](),[8.10473][8.10473:10729](),[8.10408][8.6181:6322](),[8.4696][8.9917:10408](),[8.1281][8.4648:4696](),[8.7017][8.4648:4696](),[8.865][8.1257:1281](),[8.1257][8.1257:1281](),[8.1226][8.793:865](),[8.6180][8.1049:1226](),[8.7017][8.1049:1226](),[8.4581][8.6075:6180](),[8.721][8.4542:4581](),[8.4542][8.4542:4581](),[8.6757][8.669:721](),[5.113][8.6746:6757](),[8.470][8.6746:6757](),[8.4487][8.6746:6757](),[8.100241][8.6746:6757](),[8.6746][8.6746:6757](),[8.4441][8.446:470](),[8.6692][8.4415:4441](),[8.4414][8.6653:6692](),[8.6653][8.6653:6692](),[8.6632][8.4388:4414](),[8.419][8.6621:6632](),[8.895][8.6621:6632](),[8.100147][8.6621:6632](),[8.6621][8.6621:6632](),[8.312][8.789:895](),[8.6468][8.249:312](),[8.4387][8.6457:6468](),[8.99992][8.6457:6468](),[8.6457][8.6457:6468](),[8.445][8.4361:4387](),[8.4361][8.4361:4387](),[8.544][8.421:445](),[8.788][8.421:445](),[8.4315][8.421:445](),[8.4252][8.483:544](),[8.6095][8.4220:4252](),[8.64][8.6090:6095](),[8.6090][8.6090:6095](),[8.6090][8.53:64](),[8.2101][8.6084:6090](),[8.6084][8.6084:6090](),[8.4219][8.2069:2101](),[8.2148][8.4191:4219](),[8.8245][8.4191:4219](),[8.9916][8.4191:4219](),[8.4191][8.4191:4219](),[8.5923][8.2108:2148](),[8.4060][8.5917:5923](),[8.99635][8.5917:5923](),[8.5917][8.5917:5923](),[8.6074][8.9765:9790](),[8.9765][8.9765:9790](),[8.5853][8.6004:6074](),[8.13][8.5845:5853](),[8.1110][8.5845:5853](),[8.6003][8.5845:5853](),[8.9658][8.5845:5853](),[8.37331][8.5845:5853](),[8.5845][8.5845:5853](),[8.6001][8.9505:9658](),[8.37329][8.9505:9658](),[8.3138][8.9505:9658](),[8.3095][8.5964:6001](),[8.4249][8.3031:3095](),[8.3030][8.4205:4249](),[8.98451][8.4205:4249](),[8.4205][8.4205:4249](),[8.4124][8.2931:3030](),[8.1996][8.4101:4124](),[8.2930][8.4101:4124](),[8.22445][8.4101:4124](),[8.98330][8.4101:4124](),[8.4101][8.4101:4124](),[8.314][8.22423:22445](),[8.2901][8.22423:22445](),[8.1165][8.268:314](),[8.816][8.988:1165](),[8.1895][8.988:1165](),[8.1023][8.642:816](),[8.44][8.999:1023](),[8.2727][8.999:1023](),[8.3913][8.999:1023](),[8.52][8.2701:2727](),[8.366][8.2701:2727](),[8.3868][8.2701:2727](),[8.366][8.28:52](),[8.754][8.790:795](),[8.790][8.790:795]()
    local screen_bottom1 = {line=nil, pos=nil}
    -- give some time for the OS to flush everything to disk
    love.timer.sleep(0.1)
    State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
    if eq(State.cursor1, State.selection1) then
    State.selection1 = {}
    end
    break
    --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
    --? print('text input', t)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    -- undo
    end
    edit.draw(State)
    edit.update(State, 0)
    App.screen.contents = {}
    edit.mouse_release(State, x,y, mouse_button)
    function edit.run_after_mouse_release(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.draw(State)
    end
    edit.update(State, 0)
    App.screen.contents = {}
    edit.mouse_press(State, x,y, mouse_button)
    function edit.run_after_mouse_press(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.draw(State)
    end
    edit.update(State, 0)
    App.screen.contents = {}
    edit.mouse_release(State, x,y, mouse_button)
    App.fake_mouse_release(x,y, mouse_button)
    edit.mouse_press(State, x,y, mouse_button)
    function edit.run_after_mouse_click(State, x,y, mouse_button)
    App.fake_mouse_press(x,y, mouse_button)
    edit.draw(State)
    end
    edit.update(State, 0)
    App.screen.contents = {}
    edit.keychord_press(State, chord)
    edit.key_release(State, chord)
    function edit.run_after_keychord(State, chord)
    -- not all keys are text_input
    end
    edit.draw(State)
    edit.update(State, 0)
    App.screen.contents = {}
    function edit.run_after_text_input(State, t)
    edit.keychord_press(State, t)
    edit.text_input(State, t)
    edit.key_release(State, t)
    -- TODO: handle chords of multiple keys
    -- all text_input events are also keypresses
    14, -- font height assuming default LÖVE font
    15) -- line height
    end
    App.screen.width - Test_margin_right,
    function edit.initialize_test_state()
    -- if you change these values, tests will start failing
    return edit.initialize_state(
    15, -- top margin
    Test_margin_left,
    Test_margin_right = 0
    Test_margin_left = 25
    -- 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.
    --== some methods for tests
    end
    State.line_height = math.floor(font_height*1.3)
    love.graphics.setFont(love.graphics.newFont(State.font_height))
    function edit.update_font_settings(State, font_height)
    State.font_height = font_height
    end
    function edit.key_release(State, key, scancode)
    end
    end
    Text.keychord_press(State, chord)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    else
    -- dispatch to text
    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
    schedule_save(State)
    end
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    if Text.cursor_out_of_screen(State) then
    end
    end
    Text.insert_at_cursor(State, c)
    else
    Text.insert_return(State)
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    local clipboard_data = App.get_clipboard()
    local before = snapshot(State, before_line)
    local before_line = State.cursor1.line
    -- 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.
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    elseif chord == 'C-v' then
    schedule_save(State)
    end
    App.set_clipboard(s)
    if s then
    local s = Text.cut_selection(State, State.left, State.right)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    end
    elseif chord == 'C-x' then
    App.set_clipboard(s)
    if s then
    local s = Text.selection(State)
    elseif chord == 'C-c' then
    elseif chord == 'C-a' then
    State.selection1 = {line=1, pos=1}
    State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}
    end
    -- clipboard
    schedule_save(State)
    -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Text.redraw_all(State)
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    State.selection1 = deepcopy(src.selection)
    patch(State.lines, event.before, event.after)
    if event then
    local src = event.after
    local event = redo_event(State)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    end
    elseif chord == 'C-y' then
    schedule_save(State)
    -- if we're scrolling, reclaim all fragments to avoid memory leaks
    Text.redraw_all(State)
    patch_placeholders(State.line_cache, event.after, event.before)
    State.screen_top1 = deepcopy(src.screen_top)
    State.cursor1 = deepcopy(src.cursor)
    State.selection1 = deepcopy(src.selection)
    patch(State.lines, event.after, event.before)
    if event then
    local src = event.before
    local event = undo_event(State)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    elseif chord == 'C-z' then
    Text.redraw_all(State)
    edit.update_font_settings(State, 20)
    elseif chord == 'C-0' then
    if State.font_height > 2 then
    edit.update_font_settings(State, State.font_height-2)
    Text.redraw_all(State)
    end
    elseif chord == 'C--' then
    Text.redraw_all(State)
    edit.update_font_settings(State, State.font_height+2)
    elseif chord == 'C-=' then
    -- zoom
    State.search_backup = {
    cursor={line=State.cursor1.line, pos=State.cursor1.pos},
    screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
    }
    State.search_term = ''
    end
    return
    elseif chord == 'C-f' then
    Text.search_previous(State)
    elseif chord == 'up' then
    Text.search_next(State)
    State.cursor1.pos = State.cursor1.pos+1
    elseif chord == 'down' then
    local 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 == 'backspace' then
    State.search_backup = nil
    State.search_term = nil
    elseif chord == 'return' then
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    State.cursor1 = State.search_backup.cursor
    State.screen_top1 = State.search_backup.screen_top
    State.search_backup = nil
    State.search_term = nil
    if chord == 'escape' then
    if State.search_term then
    end
    Text.delete_selection(State, State.left, State.right)
    chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(chord) then
    -- 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) and
    if State.selection1.line and
    function edit.keychord_press(State, chord, key)
    end
    schedule_save(State)
    end
    Text.text_input(State, t)
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    else
    Text.search_next(State)
    if State.search_term then
    State.search_term = State.search_term..t
    function edit.text_input(State, t)
    end
    end
    for i=1,math.floor(-dy) do
    Text.down(State)
    end
    edit.put_cursor_on_next_text_line(State)
    State.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
    elseif dy < 0 then
    for i=1,math.floor(dy) do
    Text.up(State)
    end
    edit.put_cursor_on_next_text_line(State)
    end
    function edit.mouse_wheel_move(State, dx,dy)
    if dy > 0 then
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
    end
    end
    end
    end
    if State.mousepress_shift then
    if State.old_selection1.line == nil then
    State.selection1 = State.old_cursor1
    else
    State.selection1 = State.old_selection1
    --? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    State.cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print_and_log(('edit.mouse_release: in line %d'):format(line_index))
    for line_index,line in ipairs(State.lines) do
    if Text.in_line(State, line_index, x,y) then
    --? print_and_log(('edit.mouse_release(%d,%d): cursor at %d,%d'):format(x,y, State.cursor1.line, State.cursor1.pos))
    if State.search_term then return end
    function edit.mouse_release(State, x,y, mouse_button)
    end
    -- still here? click is below all screen lines
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=State.screen_bottom1.line,
    pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
    }
    end
    end
    return
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    -- i.e. mouse_release should never look at shift state
    --? print_and_log(('edit.mouse_press: in line %d'):format(line_index))
    if 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
    for line_index,line in ipairs(State.lines) do
    }
    return
    end
    line=State.screen_top1.line,
    pos=State.screen_top1.pos,
    if y < State.top then
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if State.search_term then return end
    function edit.mouse_press(State, x,y, mouse_button)
    end
    end
    save_to_disk(State)
    if State.next_save then
    -- make sure to save before quitting
    function edit.quit(State)
    end
    end
    State.next_save = Current_time + 3 -- short enough that you're likely to still remember what you did
    function schedule_save(State)
    if State.next_save == nil then
    end
    end
    State.next_save = nil
    save_to_disk(State)
    if State.next_save and State.next_save < Current_time then
    function edit.update(State, dt)
    end
    return y
    end
    Text.draw_search_bar(State)
    if State.search_term then
    State.screen_bottom1 = screen_bottom1
    end
    --? print('=> y', y)
    y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
    end
    --? print('text.draw', y, line_index)
    local startpos = 1
    if line_index == State.screen_top1.line then
    startpos = State.screen_top1.pos
    screen_bottom1.line = line_index
    if y + State.line_height > App.screen.height then break end
    --? print('draw:', y, line_index, line)
    for line_index = State.screen_top1.line,#State.lines do
    local line = State.lines[line_index]
    --? print('== draw')
    local y = State.top
    State.cursor_x = nil
    State.cursor_y = nil
    if not Text.le1(State.screen_top1, State.cursor1) then
    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
    assert(false)
    end
    if #State.lines ~= #State.line_cache then
    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
    assert(false)
    end
    App.color(Text_color)
    function edit.draw(State)
    -- return y drawn until
    end
    [8.754]
    end
    -- return y drawn until
    function edit.draw(State)
    App.color(Text_color)
    if #State.lines ~= #State.line_cache then
    print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
    assert(false)
    end
    if not Text.le1(State.screen_top1, State.cursor1) then
    print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
    assert(false)
    end
    State.cursor_x = nil
    State.cursor_y = nil
    local y = State.top
    local screen_bottom1 = {line=nil, pos=nil}
    --? print('== draw')
    for line_index = State.screen_top1.line,#State.lines do
    local line = State.lines[line_index]
    --? print('draw:', y, line_index, line)
    if y + State.line_height > App.screen.height then break end
    screen_bottom1.line = line_index
    --? print('text.draw', y, line_index)
    local startpos = 1
    if line_index == State.screen_top1.line then
    startpos = State.screen_top1.pos
    end
    y, screen_bottom1.pos = Text.draw(State, line_index, y, startpos)
    --? print('=> y', y)
    end
    State.screen_bottom1 = screen_bottom1
    if State.search_term then
    Text.draw_search_bar(State)
    end
    return y
    end
    function edit.update(State, dt)
    if State.next_save and State.next_save < Current_time then
    save_to_disk(State)
    State.next_save = nil
    end
    end
    function schedule_save(State)
    if State.next_save == nil then
    State.next_save = Current_time + 3 -- short enough that you're likely to still remember what you did
    end
    end
    function edit.quit(State)
    -- make sure to save before quitting
    if State.next_save then
    save_to_disk(State)
    -- give some time for the OS to flush everything to disk
    love.timer.sleep(0.1)
    end
    end
    function edit.mouse_press(State, x,y, mouse_button)
    if State.search_term then return end
    --? print_and_log(('edit.mouse_press: cursor at %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if y < State.top then
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=State.screen_top1.line,
    pos=State.screen_top1.pos,
    }
    return
    end
    for line_index,line in ipairs(State.lines) do
    if 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_release should never look at shift state
    --? print_and_log(('edit.mouse_press: in line %d'):format(line_index))
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    return
    end
    end
    -- still here? click is below all screen lines
    State.old_cursor1 = State.cursor1
    State.old_selection1 = State.selection1
    State.mousepress_shift = App.shift_down()
    State.selection1 = {
    line=State.screen_bottom1.line,
    pos=Text.pos_at_end_of_screen_line(State, State.screen_bottom1),
    }
    end
    function edit.mouse_release(State, x,y, mouse_button)
    if State.search_term then return end
    --? print_and_log(('edit.mouse_release(%d,%d): cursor at %d,%d'):format(x,y, State.cursor1.line, State.cursor1.pos))
    for line_index,line in ipairs(State.lines) do
    if Text.in_line(State, line_index, x,y) then
    --? print_and_log(('edit.mouse_release: in line %d'):format(line_index))
    State.cursor1 = {
    line=line_index,
    pos=Text.to_pos_on_line(State, line_index, x, y),
    }
    --? print_and_log(('edit.mouse_release: cursor now %d,%d'):format(State.cursor1.line, State.cursor1.pos))
    if State.mousepress_shift then
    if State.old_selection1.line == nil then
    State.selection1 = State.old_cursor1
    else
    State.selection1 = State.old_selection1
    end
    end
    State.old_cursor1, State.old_selection1, State.mousepress_shift = nil
    if eq(State.cursor1, State.selection1) then
    State.selection1 = {}
    end
    break
    end
    end
    --? print_and_log(('edit.mouse_release: finally selection %s,%s cursor %d,%d'):format(tostring(State.selection1.line), tostring(State.selection1.pos), State.cursor1.line, State.cursor1.pos))
    end
    function edit.mouse_wheel_move(State, dx,dy)
    if dy > 0 then
    State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
    for i=1,math.floor(dy) do
    Text.up(State)
    end
    elseif dy < 0 then
    State.cursor1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}
    for i=1,math.floor(-dy) do
    Text.down(State)
    end
    end
    end
    function edit.text_input(State, t)
    --? print('text input', t)
    if State.search_term then
    State.search_term = State.search_term..t
    Text.search_next(State)
    else
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    Text.text_input(State, t)
    end
    schedule_save(State)
    end
    function 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) and
    chord ~= 'C-a' and chord ~= 'C-c' and chord ~= 'C-x' and chord ~= 'backspace' and chord ~= 'delete' and chord ~= 'C-z' and chord ~= 'C-y' and not App.is_cursor_movement(chord) then
    Text.delete_selection(State, State.left, State.right)
    end
    if State.search_term then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    if chord == 'escape' then
    State.search_term = nil
    State.cursor1 = State.search_backup.cursor
    State.screen_top1 = State.search_backup.screen_top
    State.search_backup = nil
    Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
    elseif chord == 'return' then
    State.search_term = nil
    State.search_backup = nil
    elseif chord == 'backspace' then
    local 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' then
    State.cursor1.pos = State.cursor1.pos+1
    Text.search_next(State)
    elseif chord == 'up' then
    Text.search_previous(State)
    end
    return
    elseif chord == 'C-f' then
    State.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},
    }
    -- zoom
    elseif chord == 'C-=' then
    edit.update_font_settings(State, State.font_height+2)
    Text.redraw_all(State)
    elseif chord == 'C--' then
    if State.font_height > 2 then
    edit.update_font_settings(State, State.font_height-2)
    Text.redraw_all(State)
    end
    elseif chord == 'C-0' then
    edit.update_font_settings(State, 20)
    Text.redraw_all(State)
    -- undo
    elseif chord == 'C-z' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local event = undo_event(State)
    if event then
    local src = event.before
    State.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 leaks
    Text.redraw_all(State)
    schedule_save(State)
    end
    elseif chord == 'C-y' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local event = redo_event(State)
    if event then
    local src = event.after
    State.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 leaks
    Text.redraw_all(State)
    schedule_save(State)
    end
    -- clipboard
    elseif chord == 'C-a' then
    State.selection1 = {line=1, pos=1}
    State.cursor1 = {line=#State.lines, pos=utf8.len(State.lines[#State.lines].data)+1}
    elseif chord == 'C-c' then
    local s = Text.selection(State)
    if s then
    App.set_clipboard(s)
    end
    elseif chord == 'C-x' then
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    local s = Text.cut_selection(State, State.left, State.right)
    if s then
    App.set_clipboard(s)
    end
    schedule_save(State)
    elseif chord == 'C-v' then
    for _,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.line
    local before = snapshot(State, before_line)
    local clipboard_data = App.get_clipboard()
    for _,code in utf8.codes(clipboard_data) do
    local c = utf8.char(code)
    if c == '\n' then
    Text.insert_return(State)
    else
    Text.insert_at_cursor(State, c)
    end
    end
    if Text.cursor_out_of_screen(State) then
    Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
    end
    schedule_save(State)
    record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
    -- dispatch to text
    else
    for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
    Text.keychord_press(State, chord)
    end
    end
    function edit.key_release(State, key, scancode)
    end
    function edit.update_font_settings(State, font_height)
    State.font_height = font_height
    love.graphics.setFont(love.graphics.newFont(State.font_height))
    State.line_height = math.floor(font_height*1.3)
    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 = 25
    Test_margin_right = 0
    function edit.initialize_test_state()
    -- if you change these values, tests will start failing
    return edit.initialize_state(
    15, -- top margin
    Test_margin_left,
    App.screen.width - Test_margin_right,
    14, -- font height assuming default LÖVE font
    15) -- line height
    end
    -- all text_input events are also keypresses
    -- TODO: handle chords of multiple keys
    function 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_input
    function 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)
    end
    function 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)
    end
    function 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)
    end
    function 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