∅:D[
3.8020] → [
4.121188:121509]
B:BD[
4.121188] → [
4.121188:121509]
B:BD[
4.121188] → [
3.7983:8020]
B:BD[
7.9044] → [
4.121169:121188]
B:BD[
4.136192] → [
8.177:177]
B:BD[
4.112296] → [
9.89:89]
∅:D[
10.153] → [
4.112145:112296]
∅:D[
11.2571] → [
4.112145:112296]
B:BD[
4.112145] → [
4.112145:112296]
B:BD[
4.112145] → [
11.2522:2571]
∅:D[
9.87] → [
4.111986:112145]
B:BD[
7.7527] → [
4.111986:112145]
B:BD[
4.111985] → [
9.10:87]
B:BD[
4.111959] → [
4.111959:111985]
B:BD[
4.111717] → [
4.111717:111728]
B:BD[
4.115273] → [
2.13408:13408]
B:BD[
4.115062] → [
4.115062:115273]
∅:D[
2.13406] → [
4.113657:113936]
B:BD[
4.113657] → [
4.113657:113936]
B:BD[
4.113543] → [
2.13280:13406]
∅:D[
6.3935] → [
4.112728:113543]
B:BD[
4.112728] → [
4.112728:113543]
B:BD[
4.112638] → [
6.3879:3935]
B:BD[
7.7954] → [
4.112297:112638]
∅:D[
13.1455] → [
7.7883:7954]
B:BD[
7.7883] → [
7.7883:7954]
B:BD[
7.7779] → [
13.1339:1455]
∅:D[
9.89] → [
7.7528:7779]
∅:D[
10.155] → [
7.7528:7779]
∅:D[
4.112296] → [
7.7528:7779]
B:BD[
5.115209] → [
7.7528:7779]
∅:D[
2.14284] → [
4.107178:107182]
B:BD[
4.107178] → [
4.107178:107182]
B:BD[
14.7962] → [
2.14284:14284]
B:BD[
15.1154] → [
14.7752:7962]
∅:D[
14.7751] → [
15.1149:1154]
B:BD[
15.1149] → [
15.1149:1154]
B:BD[
15.1068] → [
14.7563:7751]
B:BD[
16.478] → [
16.478:479]
∅:D[
11.3283] → [
14.7004:7008]
∅:D[
6.7201] → [
14.7004:7008]
B:BD[
14.7004] → [
14.7004:7008]
B:BD[
4.143322] → [
11.3283:3283]
∅:D[
18.3109] → [
4.143153:143322]
B:BD[
4.143153] → [
4.143153:143322]
B:BD[
4.143090] → [
18.3059:3109]
B:BD[
4.143028] → [
4.143028:143090]
∅:D[
19.1061] → [
4.142871:142933]
B:BD[
4.142871] → [
4.142871:142933]
∅:D[
17.567] → [
4.142786:142871]
B:BD[
4.142786] → [
4.142786:142871]
B:BD[
4.142786] → [
17.289:567]
∅:D[
19.1036] → [
4.142715:142786]
B:BD[
4.142715] → [
4.142715:142786]
∅:D[
18.3058] → [
4.142374:142715]
B:BD[
4.142374] → [
4.142374:142715]
B:BD[
4.142361] → [
18.3039:3058]
∅:D[
6.7199] → [
4.142250:142361]
B:BD[
4.142250] → [
4.142250:142361]
B:BD[
4.142062] → [
6.7020:7199]
∅:D[
6.7019] → [
4.140912:142062]
B:BD[
4.140912] → [
4.140912:142062]
B:BD[
4.139127] → [
6.6906:7019]
∅:D[
6.6905] → [
4.139064:139127]
B:BD[
4.139064] → [
4.139064:139127]
B:BD[
4.139064] → [
6.6866:6905]
∅:D[
6.6865] → [
4.139057:139064]
B:BD[
4.139057] → [
4.139057:139064]
B:BD[
4.139057] → [
6.6749:6865]
B:BD[
4.138907] → [
4.138907:139057]
∅:D[
11.3281] → [
4.138643:138712]
B:BD[
4.138643] → [
4.138643:138712]
∅:D[
20.351] → [
4.138620:138626]
B:BD[
4.138620] → [
4.138620:138626]
B:BD[
4.138597] → [
20.181:351]
∅:D[
6.6748] → [
4.138467:138597]
B:BD[
4.138467] → [
4.138467:138597]
B:BD[
4.138321] → [
6.6724:6748]
∅:D[
6.6723] → [
4.138298:138321]
B:BD[
4.138298] → [
4.138298:138321]
B:BD[
4.138268] → [
6.6697:6723]
∅:D[
6.6696] → [
4.138246:138268]
B:BD[
4.138246] → [
4.138246:138268]
B:BD[
4.138195] → [
6.6618:6696]
∅:D[
6.6617] → [
4.138069:138195]
B:BD[
4.138069] → [
4.138069:138195]
B:BD[
4.137812] → [
6.6548:6617]
B:BD[
4.137589] → [
4.137589:137812]
∅:D[
11.3194] → [
4.137393:137445]
B:BD[
4.137393] → [
4.137393:137445]
B:BD[
4.136712] → [
11.3113:3194]
B:BD[
4.136331] → [
4.136331:136712]
B:BD[
7.10486] → [
4.136193:136199]
∅:D[
8.177] → [
7.10378:10486]
∅:D[
11.3112] → [
7.10378:10486]
∅:D[
6.6547] → [
7.10378:10486]
∅:D[
21.116507] → [
7.10378:10486]
∅:D[
4.136192] → [
7.10378:10486]
B:BD[
5.139954] → [
7.10378:10486]
∅:D[
2.14282] → [
4.136156:136192]
∅:D[
21.116505] → [
4.136156:136192]
B:BD[
4.136156] → [
4.136156:136192]
B:BD[
4.136055] → [
2.14249:14282]
∅:D[
2.14248] → [
4.135980:136055]
B:BD[
4.135980] → [
4.135980:136055]
B:BD[
4.135956] → [
2.14218:14248]
∅:D[
2.14217] → [
4.135951:135956]
∅:D[
21.116475] → [
4.135951:135956]
B:BD[
4.135951] → [
4.135951:135956]
B:BD[
4.135850] → [
2.14184:14217]
∅:D[
8.175] → [
4.135780:135850]
B:BD[
4.135780] → [
4.135780:135850]
∅:D[
2.14183] → [
8.93:175]
B:BD[
4.135725] → [
8.93:175]
B:BD[
4.135695] → [
2.14147:14183]
∅:D[
11.3110] → [
4.135690:135695]
B:BD[
4.135690] → [
4.135690:135695]
B:BD[
4.135674] → [
11.3062:3110]
∅:D[
2.14146] → [
4.135406:135674]
B:BD[
4.135406] → [
4.135406:135674]
B:BD[
4.135310] → [
2.14038:14146]
∅:D[
2.14037] → [
4.135176:135310]
B:BD[
4.135176] → [
4.135176:135310]
B:BD[
4.135136] → [
2.13991:14037]
∅:D[
2.13990] → [
4.135054:135136]
B:BD[
4.135054] → [
4.135054:135136]
B:BD[
4.135009] → [
2.13939:13990]
∅:D[
11.3061] → [
4.134866:135009]
B:BD[
4.134866] → [
4.134866:135009]
B:BD[
4.134850] → [
11.3013:3061]
∅:D[
2.13938] → [
4.134470:134850]
B:BD[
4.134470] → [
4.134470:134850]
B:BD[
4.134378] → [
2.13834:13938]
∅:D[
2.13833] → [
4.134079:134378]
B:BD[
4.134079] → [
4.134079:134378]
B:BD[
4.134038] → [
2.13786:13833]
∅:D[
2.13785] → [
4.133971:134038]
B:BD[
4.133971] → [
4.133971:134038]
B:BD[
4.133923] → [
2.13731:13785]
B:BD[
4.133805] → [
4.133805:133923]
∅:D[
2.13730] → [
4.133375:133534]
∅:D[
21.116445] → [
4.133375:133534]
B:BD[
4.133375] → [
4.133375:133534]
B:BD[
4.133166] → [
2.13688:13730]
B:BD[
4.133160] → [
4.133160:133166]
∅:D[
11.3012] → [
4.131674:132271]
B:BD[
4.131674] → [
4.131674:132271]
B:BD[
4.131658] → [
11.2963:3012]
∅:D[
6.6545] → [
4.131630:131658]
∅:D[
2.13687] → [
4.131630:131658]
B:BD[
4.131630] → [
4.131630:131658]
B:BD[
6.6303] → [
2.13409:13687]
B:BD[
4.131307] → [
6.6231:6303]
∅:D[
6.6230] → [
4.131297:131307]
B:BD[
4.131297] → [
4.131297:131307]
B:BD[
4.131216] → [
6.6156:6230]
∅:D[
6.6155] → [
4.131160:131216]
B:BD[
4.131160] → [
4.131160:131216]
B:BD[
4.130900] → [
6.6001:6155]
∅:D[
6.6000] → [
4.130619:130900]
B:BD[
4.130619] → [
4.130619:130900]
B:BD[
4.127291] → [
6.5482:6000]
∅:D[
11.2962] → [
4.127220:127291]
B:BD[
4.127220] → [
4.127220:127291]
B:BD[
4.127186] → [
11.2895:2962]
B:BD[
4.127041] → [
4.127041:127186]
∅:D[
6.5481] → [
4.126519:126575]
B:BD[
4.126519] → [
4.126519:126575]
B:BD[
4.125249] → [
6.5244:5481]
∅:D[
6.5243] → [
4.124890:125249]
B:BD[
4.124890] → [
4.124890:125249]
B:BD[
4.124657] → [
6.5081:5243]
B:BD[
7.10377] → [
4.124398:124657]
∅:D[
22.2104] → [
7.10057:10377]
B:BD[
7.10057] → [
7.10057:10377]
B:BD[
7.9972] → [
22.1929:2104]
∅:D[
6.5080] → [
7.9781:9972]
∅:D[
4.124397] → [
7.9781:9972]
B:BD[
5.128059] → [
7.9781:9972]
B:BD[
14.5445] → [
6.5017:5080]
∅:D[
6.5016] → [
14.5373:5445]
B:BD[
5.127858] → [
14.5373:5445]
B:BD[
4.124196] → [
6.5016:5016]
∅:D[
6.5014] → [
4.123968:124196]
B:BD[
4.123968] → [
4.123968:124196]
B:BD[
4.123621] → [
6.4808:5014]
∅:D[
6.4807] → [
4.123586:123621]
B:BD[
4.123586] → [
4.123586:123621]
B:BD[
4.123480] → [
6.4738:4807]
∅:D[
6.4737] → [
4.123434:123480]
B:BD[
4.123434] → [
4.123434:123480]
B:BD[
4.123355] → [
6.4678:4737]
B:BD[
7.9780] → [
4.123179:123355]
∅:D[
22.1928] → [
7.9558:9780]
B:BD[
7.9558] → [
7.9558:9780]
B:BD[
7.9509] → [
22.1820:1928]
∅:D[
23.162] → [
7.9398:9509]
B:BD[
7.9398] → [
7.9398:9509]
B:BD[
7.9398] → [
23.95:162]
∅:D[
6.4677] → [
7.9053:9398]
∅:D[
3.8441] → [
7.9053:9398]
∅:D[
4.123178] → [
7.9053:9398]
B:BD[
5.126781] → [
7.9053:9398]
B:BD[
24.4251] → [
3.8025:8441]
∅:D[
22.1819] → [
24.4246:4251]
B:BD[
24.4246] → [
24.4246:4251]
B:BD[
24.4230] → [
22.1766:1819]
∅:D[
11.2894] → [
24.3775:4230]
∅:D[
3.8024] → [
24.3775:4230]
B:BD[
5.125684] → [
24.3775:4230]
B:BD[
4.123087] → [
11.2894:2894]
∅:D[
11.2892] → [
4.123082:123087]
∅:D[
6.4675] → [
4.123082:123087]
B:BD[
4.123082] → [
4.123082:123087]
B:BD[
4.122060] → [
11.2839:2892]
∅:D[
3.8022] → [
4.122054:122060]
B:BD[
7.9052] → [
4.122054:122060]
B:BD[
7.9052] → [
3.8022:8022]
∅:D[
4.122053] → [
7.9044:9052]
B:BD[
7.9044] → [
7.9044:9052]
∅:D[
4.121168] → [
7.8772:9044]
B:BD[
5.124718] → [
7.8772:9044]
∅:D[
4.121134] → [
7.8764:8771]
B:BD[
5.124634] → [
7.8764:8771]
B:BD[
4.120824] → [
4.120824:120946]
∅:D[
11.2838] → [
22.1634:1711]
B:BD[
5.124061] → [
22.1634:1711]
B:BD[
4.120664] → [
11.2838:2838]
B:BD[
4.120458] → [
4.120458:120664]
∅:D[
6.4614] → [
4.119979:119990]
B:BD[
4.119979] → [
4.119979:119990]
B:BD[
4.119831] → [
6.4390:4614]
∅:D[
11.2836] → [
4.119772:119831]
B:BD[
7.8417] → [
4.119772:119831]
B:BD[
7.8417] → [
11.2836:2836]
∅:D[
4.119771] → [
7.8070:8417]
B:BD[
7.8070] → [
7.8070:8417]
∅:D[
11.2834] → [
4.119688:119771]
∅:D[
6.4389] → [
4.119688:119771]
B:BD[
4.119688] → [
4.119688:119771]
B:BD[
4.119571] → [
11.2757:2834]
∅:D[
11.2756] → [
4.119466:119571]
B:BD[
4.119466] → [
4.119466:119571]
B:BD[
4.119430] → [
11.2646:2756]
B:BD[
4.119387] → [
4.119387:119430]
∅:D[
6.4331] → [
4.119277:119364]
B:BD[
4.119277] → [
4.119277:119364]
B:BD[
4.118962] → [
6.4240:4331]
B:BD[
4.118956] → [
4.118956:118962]
B:BD[
4.118714] → [
4.118714:118919]
B:BD[
4.118602] → [
4.118602:118639]
B:BD[
4.118377] → [
4.118377:118393]
B:BD[
4.118080] → [
4.118080:118328]
∅:D[
6.4239] → [
4.117425:117441]
B:BD[
4.117425] → [
4.117425:117441]
B:BD[
4.117378] → [
6.4181:4239]
B:BD[
4.117367] → [
4.117367:117378]
∅:D[
11.2645] → [
4.117238:117346]
B:BD[
4.117238] → [
4.117238:117346]
B:BD[
4.117204] → [
11.2576:2645]
B:BD[
4.117149] → [
4.117149:117204]
B:BD[
4.117054] → [
4.117054:117075]
∅:D[
6.4180] → [
4.116872:116888]
B:BD[
4.116872] → [
4.116872:116888]
B:BD[
4.116824] → [
6.4150:4180]
B:BD[
4.116618] → [
4.116618:116824]
∅:D[
11.2575] → [
14.4575:4601]
∅:D[
6.4149] → [
14.4575:4601]
B:BD[
14.4575] → [
14.4575:4601]
B:BD[
4.115791] → [
11.2575:2575]
∅:D[
6.4147] → [
4.115658:115791]
B:BD[
4.115658] → [
4.115658:115791]
B:BD[
4.115465] → [
6.4074:4147]
∅:D[
6.4073] → [
4.115421:115465]
B:BD[
7.8062] → [
4.115421:115465]
∅:D[
11.2573] → [
6.3938:4073]
B:BD[
14.4327] → [
6.3938:4073]
B:BD[
14.4327] → [
11.2573:2573]
∅:D[
6.3937] → [
14.4303:4327]
∅:D[
2.13408] → [
14.4303:4327]
B:BD[
5.118441] → [
14.4303:4327]
∅:D[
11.2519] → [
14.4021:4245]
∅:D[
6.3878] → [
14.4021:4245]
∅:D[
2.13279] → [
14.4021:4245]
B:BD[
5.111230] → [
14.4021:4245]
B:BD[
13.1338] → [
2.13155:13279]
∅:D[
2.13154] → [
13.1212:1338]
B:BD[
5.110992] → [
13.1212:1338]
function 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 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return spos,i
-- result: pos, index of screen line
end
end
end
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')
assert(State.cursor1.pos, 'cursor has no pos')
--? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
function Text.down(State)
end
end
end
end
--? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
end
function Text.start_of_line(State)
--? 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)
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
local 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)
else
-- move down one screen line in current line
end
if 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)
end
}
--? print(State.cursor1.pos)
break
end
pos = Text.nearest_cursor_pos(State.font, State.lines[new_cursor_line].data, State.cursor_x, State.left),
local new_cursor_line = State.cursor1.line
while new_cursor_line < #State.lines do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {
line = new_cursor_line,
end
function ends_with(s, suffix)
if #s < #suffix then
return false
end
for i=0,#suffix-1 do
if s:sub(#s-i,#s-i) ~= suffix:sub(#suffix-i,#suffix-i) then
return false
end
end
return true
end
function starts_with(s, prefix)
if #s < #prefix then
return false
end
for i=1,#prefix do
if s:sub(i,i) ~= prefix:sub(i,i) then
return false
end
end
return true
end
end
function trim(s)
return s:gsub('^%s+', ''):gsub('%s+$', '')
end
function ltrim(s)
return s:gsub('^%s+', '')
end
function rtrim(s)
return s:gsub('%s+$', '')
State.line_cache[line_index].link_offsets = nil
State.line_cache[line_index].screen_line_starting_pos = nil
end
function Text.clear_screen_line_cache(State, line_index)
State.line_cache = {}
for i=1,#State.lines do
State.line_cache[i] = {}
end
-- Perform some early sanity checking here, in hopes that we correctly call
-- this whenever we change editor state.
if State.right <= State.left then
assert(false, ('Right margin %d must be to the right of the left margin %d'):format(State.right, State.left))
end
end
function Text.redraw_all(State)
--? print('clearing fragments')
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)
edit.draw(State)
end
end
end
-- slightly expensive since it redraws the screen
function Text.cursor_out_of_screen(State)
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),
}
end
end
-- resize helper
function Text.tweak_screen_top_and_cursor(State)
if State.screen_top1.pos == 1 then return end
Text.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 do
local pos = line_cache.screen_line_starting_pos[i]
if pos == State.screen_top1.pos then
break
end
if pos > State.screen_top1.pos then
-- make sure screen top is at start of a screen line
local prev = line_cache.screen_line_starting_pos[i-1]
if State.screen_top1.pos - prev < pos - State.screen_top1.pos then
State.screen_top1.pos = prev
else
State.screen_top1.pos = pos
end
break
end
end
-- make sure cursor is on screen
if Text.lt1(State.cursor1, State.screen_top1) then
State.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')
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
Text.populate_screen_line_starting_pos(State, loc2.line-1)
local l = State.lines[loc2.line-1]
else
elseif State.lines[loc2.line-1].mode == 'drawing' then
return {line=loc2.line-1, screen_line=1, screen_pos=1}
if loc2.screen_line > 1 then
return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
elseif loc2.line == 1 then
return loc2
return result
end
function Text.previous_screen_line(State, loc2)
end
assert(false, ('Text.offset(%d) called on a string of length %d (byte size %d); this is likely a failure to handle utf8\n\n^%s$\n'):format(pos1, utf8.len(s), #s, s))
end
function Text.offset(s, pos1)
if pos1 == 1 then return 1 end
local result = utf8.offset(s, pos1)
if result == nil then
return a.pos <= b.pos
return false
end
if a.line > b.line then
return true
end
return a.pos < b.pos
end
function Text.le1(a, b)
if a.line < b.line then
end
function Text.lt1(a, b)
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
function Text.eq1(a, b)
return a.line == b.line and a.pos == b.pos
local result = {line=loc2.line, pos=loc2.screen_pos}
if loc2.screen_line > 1 then
result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
end
return result
end
return result
end
function Text.to1(State, loc2)
assert(result.screen_pos, 'failed to convert schema-1 coordinate to schema-2')
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 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
result.screen_line = i
result.screen_pos = loc1.pos - spos + 1
break
end
end
end
if State.lines[loc1.line].mode == 'drawing' then
return {line=loc1.line, screen_line=1, screen_pos=1}
end
function Text.to2(State, loc1)
return font:getWidth(s_before)
local offset = Text.offset(s, pos)
local s_before = s:sub(1, offset-1)
function Text.x(font, s, pos)
end
return font:getWidth(s_before)
local s_before = s:sub(1, offset-1)
--? print('^'..s_before..'$')
local len = utf8.len(s)
local offset = Text.offset(s, math.min(pos+1, len+1))
function Text.x_after(font, s, pos)
end
assert(false, 'failed to map x pixel to pos')
--? print('', x, left, right, curr, currxmin, currxmax)
if currxmin <= x and x < currxmax then
return curr
end
if left >= right-1 then
return left
end
if currxmin > x then
right = curr
else
left = curr
end
end
local currxmin = Text.x_after(font, line, curr+1)
local currxmax = Text.x_after(font, line, curr+2)
if x > max_x then
return len+1
end
local left, right = 0, len+1
while true do
local curr = math.floor((left+right)/2)
local max_x = Text.x_after(font, line, len)
--? print('', '-- nearest_pos_less_than', line, x)
local len = utf8.len(line)
function Text.nearest_pos_less_than(font, line, x)
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+1
assert(false, 'failed to map x pixel to pos')
--? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
if currxmin <= x and x < currxmax then
if x-currxmin < currxmax-x then
return curr
else
return curr+1
end
end
if leftpos >= rightpos-1 then
return rightpos
end
if currxmin > x then
rightpos = curr
else
leftpos = curr
end
end
local currxmin = left+Text.x(font, line, curr)
local currxmax = left+Text.x(font, line, curr+1)
if x > max_x then
return len+1
end
local leftpos, rightpos = 1, len+1
--? print('-- nearest', x)
while true do
--? print('nearest', x, '^'..line..'$', leftpos, rightpos)
if leftpos == rightpos then
return leftpos
end
local curr = math.floor((leftpos+rightpos)/2)
local max_x = left+Text.x(font, line, len+1)
if x < left then
return 1
end
local len = utf8.len(line)
function Text.nearest_cursor_pos(font, line, x, left)
return i
end
end
end
-- convert x pixel coordinate to pos
-- oblivious to wrapping
-- result: 1 to len+1
end
function Text.screen_line_index(screen_line_starting_pos, pos)
for i = #screen_line_starting_pos,1,-1 do
if screen_line_starting_pos[i] <= pos then
return State.font:getWidth(screen_line)
end
end
function 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_line
if i < #line_cache.screen_line_starting_pos then
local 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)
else
screen_line = string.sub(line.data, start_pos)
assert(false, 'failed to map y pixel to line')
end
y = nexty
end
--? print('return', mx, Text.nearest_cursor_pos(State.font, s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1)
return screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1
local s = string.sub(line.data, screen_line_starting_byte_offset)
end
return line_cache.screen_line_starting_pos[screen_line_index+1]-1
--? print('past end of non-final line; return')
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
local nexty = y + State.line_height
if 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.)
local 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 do
local 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))
-- duplicate some logic from Text.draw
local y = line_cache.starty
assert(my >= line_cache.starty, 'failed to map y pixel to line')
function Text.to_pos_on_line(State, line_index, mx, my)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
end
-- convert mx,my in pixels to schema-1 coordinates
Text.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)
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
end
function 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 page
if y < line_cache.starty then return false end
--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
end
top2 = 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)
-- We currently can't draw partial drawings, so either skip it entirely
-- or not at all.
local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
if y - h < State.top then
break
end
--? print('skipping drawing of height', h)
y = y - h
assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')
assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")
if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then
local h = State.line_height
if y - h < State.top then
break
end
y = y - h
else
if top2.line == 1 and top2.screen_line == 1 then break end
--? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_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.draw
while true do
top2.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)
-- slide to start of screen line
--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)
local top2 = Text.to2(State, State.cursor1)
--? print('to2:', State.cursor1.line, State.cursor1.pos)
--? print('scroll up')
Text.snap_cursor_to_bottom_of_screen(State)
end
end
-- should never modify State.cursor1
function Text.snap_cursor_to_bottom_of_screen(State)
table.insert(State.lines, {mode='text', data=''})
table.insert(State.line_cache, {})
end
--? print(y, App.screen.height, App.screen.height-State.line_height)
if y > App.screen.height - State.line_height then
assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')
end
-- hack: insert a text line at bottom of file if necessary
if State.cursor1.line > #State.lines then
end
if State.cursor1.pos == nil then
State.cursor1.pos = 1
local y = State.top
while State.cursor1.line <= #State.lines do
if State.lines[State.cursor1.line].mode == 'text' then
break
end
--? print('cursor skips', State.cursor1.line)
y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width)
State.cursor1.line = State.cursor1.line + 1
function 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.pos
end
function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
end
assert(false, ('invalid pos %d'):format(loc1.pos))
function Text.pos_at_end_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1
for i=#line_cache.screen_line_starting_pos,1,-1 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return most_recent_final_pos
end
most_recent_final_pos = spos-1
end
end
assert(false, ('invalid pos %d'):format(loc1.pos))
end
end
local new_cursor_line = State.cursor1.line
while new_cursor_line <= #State.lines-1 do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {line=new_cursor_line, pos=1}
break
end
else
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
State.cursor1.pos = State.cursor1.pos+1
assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
function Text.right(State)
Text.right_without_scroll(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.right_without_scroll(State)
end
end
State.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 leaks
end
if Text.lt1(State.cursor1, State.screen_top1) then
local new_cursor_line = State.cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line-1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {
line = new_cursor_line,
pos = utf8.len(State.lines[new_cursor_line].data) + 1,
}
break
end
end
if State.cursor1.pos > 1 then
State.cursor1.pos = State.cursor1.pos-1
else
assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
local curr = s:sub(start_offset, end_offset-1)
return curr:match(pat)
end
function Text.left(State)
assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))
local end_offset = Text.offset(s, pos+1)
end
end
function Text.match(s, pos, pat)
local start_offset = Text.offset(s, pos)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
break
end
Text.right_without_scroll(State)
while true do
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
break
end
Text.right_without_scroll(State)
end
while true do
function Text.word_right(State)
-- skip some whitespace
end
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
break
end
assert(State.cursor1.pos > 1, 'bumped up against start of line')
if State.cursor1.pos == 1 then
break
end
Text.left(State)
while true do
-- skip some non-whitespace
while true do
if State.cursor1.pos == 1 then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
break
end
Text.left(State)
end
-- skip some whitespace
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.word_left(State)
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
end
end
function Text.end_of_line(State)
if Text.lt1(State.cursor1, State.screen_top1) then
State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy
State.cursor1.pos = 1
State.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 leaks
--? print('cursor pos is now '..tostring(State.cursor1.pos))
end
if Text.lt1(State.cursor1, State.screen_top1) then
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, State.cursor_x, State.left) - 1
--? print('cursor pos is now '..tostring(State.cursor1.pos))
end
if Text.lt1(State.cursor1, State.screen_top1) then
State.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 leaks
end
end
function Text.down(State)
assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
--? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
assert(State.cursor1.pos, 'cursor has no 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')
local new_cursor_line = State.cursor1.line
while new_cursor_line < #State.lines do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {
line = new_cursor_line,
pos = Text.nearest_cursor_pos(State.font, State.lines[new_cursor_line].data, State.cursor_x, State.left),
}
--? print(State.cursor1.pos)
break
end
end
if 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)
end
else
-- move down one screen line in current line
local 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(State.font, 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)
end
end
--? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
end
function Text.start_of_line(State)
State.cursor1.pos = 1
if Text.lt1(State.cursor1, State.screen_top1) then
State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos} -- copy
end
end
function Text.end_of_line(State)
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.word_left(State)
-- skip some whitespace
while true do
if State.cursor1.pos == 1 then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
break
end
Text.left(State)
end
-- skip some non-whitespace
while true do
Text.left(State)
if State.cursor1.pos == 1 then
break
end
assert(State.cursor1.pos > 1, 'bumped up against start of line')
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
break
end
end
end
function Text.word_right(State)
-- skip some whitespace
while true do
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
break
end
Text.right_without_scroll(State)
end
while true do
Text.right_without_scroll(State)
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
break
end
end
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.match(s, pos, pat)
local start_offset = Text.offset(s, pos)
local end_offset = Text.offset(s, pos+1)
assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))
local curr = s:sub(start_offset, end_offset-1)
return curr:match(pat)
end
function Text.left(State)
assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
if State.cursor1.pos > 1 then
State.cursor1.pos = State.cursor1.pos-1
else
local new_cursor_line = State.cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line-1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {
line = new_cursor_line,
pos = utf8.len(State.lines[new_cursor_line].data) + 1,
}
break
end
end
end
if Text.lt1(State.cursor1, State.screen_top1) then
State.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 leaks
end
end
function Text.right(State)
Text.right_without_scroll(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.right_without_scroll(State)
assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
State.cursor1.pos = State.cursor1.pos+1
else
local new_cursor_line = State.cursor1.line
while new_cursor_line <= #State.lines-1 do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {line=new_cursor_line, pos=1}
break
end
end
end
end
-- result: pos, index of screen line
function 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 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return spos,i
end
end
assert(false, ('invalid pos %d'):format(loc1.pos))
end
function Text.pos_at_end_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
local most_recent_final_pos = utf8.len(State.lines[loc1.line].data)+1
for i=#line_cache.screen_line_starting_pos,1,-1 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return most_recent_final_pos
end
most_recent_final_pos = spos-1
end
assert(false, ('invalid pos %d'):format(loc1.pos))
end
function 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.pos
end
function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)
local y = State.top
while State.cursor1.line <= #State.lines do
if State.lines[State.cursor1.line].mode == 'text' then
break
end
--? print('cursor skips', State.cursor1.line)
y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width)
State.cursor1.line = State.cursor1.line + 1
end
if State.cursor1.pos == nil then
State.cursor1.pos = 1
end
-- hack: insert a text line at bottom of file if necessary
if State.cursor1.line > #State.lines then
assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')
table.insert(State.lines, {mode='text', data=''})
table.insert(State.line_cache, {})
end
--? print(y, App.screen.height, App.screen.height-State.line_height)
if y > App.screen.height - State.line_height then
--? print('scroll up')
Text.snap_cursor_to_bottom_of_screen(State)
end
end
-- should never modify State.cursor1
function 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 line
top2.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.draw
while true do
--? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos)
if top2.line == 1 and top2.screen_line == 1 then break end
if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then
local h = State.line_height
if y - h < State.top then
break
end
y = y - h
else
assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')
assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")
-- We currently can't draw partial drawings, so either skip it entirely
-- or not at all.
local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width)
if y - h < State.top then
break
end
--? print('skipping drawing of height', h)
y = y - h
end
top2 = 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 leaks
end
function 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 page
if y < line_cache.starty then return false end
Text.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 coordinates
function 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, 'failed to map y pixel to line')
-- duplicate some logic from Text.draw
local y = line_cache.starty
local 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 do
local 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_height
if 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]-1
end
local s = string.sub(line.data, screen_line_starting_byte_offset)
--? print('return', mx, Text.nearest_cursor_pos(State.font, s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1)
return screen_line_starting_pos + Text.nearest_cursor_pos(State.font, s, mx, State.left) - 1
end
y = nexty
end
assert(false, 'failed to map y pixel to line')
end
function 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_line
if i < #line_cache.screen_line_starting_pos then
local 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)
else
screen_line = string.sub(line.data, start_pos)
end
return State.font:getWidth(screen_line)
end
function Text.screen_line_index(screen_line_starting_pos, pos)
for i = #screen_line_starting_pos,1,-1 do
if screen_line_starting_pos[i] <= pos then
return i
end
end
end
-- convert x pixel coordinate to pos
-- oblivious to wrapping
-- result: 1 to len+1
function Text.nearest_cursor_pos(font, line, x, left)
if x < left then
return 1
end
local len = utf8.len(line)
local max_x = left+Text.x(font, line, len+1)
if x > max_x then
return len+1
end
local leftpos, rightpos = 1, len+1
--? print('-- nearest', x)
while true do
--? print('nearest', x, '^'..line..'$', leftpos, rightpos)
if leftpos == rightpos then
return leftpos
end
local curr = math.floor((leftpos+rightpos)/2)
local currxmin = left+Text.x(font, line, curr)
local currxmax = left+Text.x(font, line, curr+1)
--? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)
if currxmin <= x and x < currxmax then
if x-currxmin < currxmax-x then
return curr
else
return curr+1
end
end
if leftpos >= rightpos-1 then
return rightpos
end
if currxmin > x then
rightpos = curr
else
leftpos = curr
end
end
assert(false, 'failed to map x pixel to pos')
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+1
function Text.nearest_pos_less_than(font, line, x)
--? print('', '-- nearest_pos_less_than', line, x)
local len = utf8.len(line)
local max_x = Text.x_after(font, line, len)
if x > max_x then
return len+1
end
local left, right = 0, len+1
while true do
local curr = math.floor((left+right)/2)
local currxmin = Text.x_after(font, line, curr+1)
local currxmax = Text.x_after(font, line, curr+2)
--? print('', x, left, right, curr, currxmin, currxmax)
if currxmin <= x and x < currxmax then
return curr
end
if left >= right-1 then
return left
end
if currxmin > x then
right = curr
else
left = curr
end
end
assert(false, 'failed to map x pixel to pos')
end
function Text.x_after(font, s, pos)
local len = utf8.len(s)
local offset = Text.offset(s, math.min(pos+1, len+1))
local s_before = s:sub(1, offset-1)
--? print('^'..s_before..'$')
return font:getWidth(s_before)
end
function Text.x(font, s, pos)
local offset = Text.offset(s, pos)
local s_before = s:sub(1, offset-1)
return font:getWidth(s_before)
end
function Text.to2(State, loc1)
if State.lines[loc1.line].mode == 'drawing' then
return {line=loc1.line, screen_line=1, screen_pos=1}
end
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 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
result.screen_line = i
result.screen_pos = loc1.pos - spos + 1
break
end
end
assert(result.screen_pos, 'failed to convert schema-1 coordinate to schema-2')
return result
end
function Text.to1(State, loc2)
local result = {line=loc2.line, pos=loc2.screen_pos}
if loc2.screen_line > 1 then
result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
end
return result
end
function Text.eq1(a, b)
return a.line == b.line and a.pos == b.pos
end
function Text.lt1(a, b)
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
return a.pos < b.pos
end
function Text.le1(a, b)
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
return a.pos <= b.pos
end
function Text.offset(s, pos1)
if pos1 == 1 then return 1 end
local result = utf8.offset(s, pos1)
if result == nil then
assert(false, ('Text.offset(%d) called on a string of length %d (byte size %d); this is likely a failure to handle utf8\n\n^%s$\n'):format(pos1, utf8.len(s), #s, s))
end
return result
end
function Text.previous_screen_line(State, loc2)
if loc2.screen_line > 1 then
return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
elseif loc2.line == 1 then
return loc2
elseif State.lines[loc2.line-1].mode == 'drawing' then
return {line=loc2.line-1, screen_line=1, screen_pos=1}
else
local 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}
end
end
-- resize helper
function Text.tweak_screen_top_and_cursor(State)
if State.screen_top1.pos == 1 then return end
Text.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 do
local pos = line_cache.screen_line_starting_pos[i]
if pos == State.screen_top1.pos then
break
end
if pos > State.screen_top1.pos then
-- make sure screen top is at start of a screen line
local prev = line_cache.screen_line_starting_pos[i-1]
if State.screen_top1.pos - prev < pos - State.screen_top1.pos then
State.screen_top1.pos = prev
else
State.screen_top1.pos = pos
end
break
end
end
-- make sure cursor is on screen
if Text.lt1(State.cursor1, State.screen_top1) then
State.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),
}
end
end
end
-- slightly expensive since it redraws the screen
function Text.cursor_out_of_screen(State)
edit.draw(State)
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)
end
function Text.redraw_all(State)
--? print('clearing fragments')
-- Perform some early sanity checking here, in hopes that we correctly call
-- this whenever we change editor state.
if State.right <= State.left then
assert(false, ('Right margin %d must be to the right of the left margin %d'):format(State.right, State.left))
end
State.line_cache = {}
for i=1,#State.lines do
State.line_cache[i] = {}
end
end
function Text.clear_screen_line_cache(State, line_index)
State.line_cache[line_index].screen_line_starting_pos = nil
State.line_cache[line_index].link_offsets = nil
end
function trim(s)
return s:gsub('^%s+', ''):gsub('%s+$', '')
end
function ltrim(s)
return s:gsub('^%s+', '')
end
function rtrim(s)
return s:gsub('%s+$', '')
end
function starts_with(s, prefix)
if #s < #prefix then
return false
end
for i=1,#prefix do
if s:sub(i,i) ~= prefix:sub(i,i) then
return false
end
end
return true
end
function ends_with(s, suffix)
if #s < #suffix then
return false
end
for i=0,#suffix-1 do
if s:sub(#s-i,#s-i) ~= suffix:sub(#suffix-i,#suffix-i) then
return false
end
end
return true
end