Merge lines.love
[text.love.git] / search.lua
blob4aa1f0224388ffb13d055c9f0b6e12b5b48faed7
1 -- helpers for the search bar (C-f)
3 function Text.draw_search_bar(State)
4 local h = State.line_height+2
5 local y = App.screen.height-h
6 love.graphics.setColor(0.9,0.9,0.9)
7 love.graphics.rectangle('fill', 0, y-10, App.screen.width-1, h+8)
8 love.graphics.setColor(0.6,0.6,0.6)
9 love.graphics.line(0, y-10, App.screen.width-1, y-10)
10 love.graphics.setColor(1,1,1)
11 love.graphics.rectangle('fill', 20, y-6, App.screen.width-40, h+2, 2,2)
12 love.graphics.setColor(0.6,0.6,0.6)
13 love.graphics.rectangle('line', 20, y-6, App.screen.width-40, h+2, 2,2)
14 App.color(Text_color)
15 App.screen.print(State.search_term, 25,y-5)
16 Text.draw_cursor(State, 25+State.font:getWidth(State.search_term),y-5)
17 end
19 function Text.search_next(State)
20 if #State.search_term == 0 then return end
21 -- search current line from cursor
22 local curr_pos = State.cursor1.pos
23 local curr_line = State.lines[State.cursor1.line].data
24 local curr_offset = Text.offset(curr_line, curr_pos)
25 local offset = find(curr_line, State.search_term, curr_offset, --[[literal]] true)
26 if offset then
27 State.cursor1.pos = utf8.len(curr_line, 1, offset)
28 end
29 if offset == nil then
30 -- search lines below cursor
31 for i=State.cursor1.line+1,#State.lines do
32 local curr_line = State.lines[i].data
33 offset = find(curr_line, State.search_term, --[[from start]] nil, --[[literal]] true)
34 if offset then
35 State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
36 break
37 end
38 end
39 end
40 if offset == nil then
41 -- wrap around
42 for i=1,State.cursor1.line-1 do
43 local curr_line = State.lines[i].data
44 offset = find(curr_line, State.search_term, --[[from start]] nil, --[[literal]] true)
45 if offset then
46 State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
47 break
48 end
49 end
50 end
51 if offset == nil then
52 -- search current line until cursor
53 local curr_line = State.lines[State.cursor1.line].data
54 offset = find(curr_line, State.search_term, --[[from start]] nil, --[[literal]] true)
55 local pos = utf8.len(curr_line, 1, offset)
56 if pos and pos < State.cursor1.pos then
57 State.cursor1.pos = pos
58 end
59 end
60 if offset == nil then
61 State.cursor1.line = State.search_backup.cursor.line
62 State.cursor1.pos = State.search_backup.cursor.pos
63 State.screen_top1.line = State.search_backup.screen_top.line
64 State.screen_top1.pos = State.search_backup.screen_top.pos
65 end
66 local screen_bottom1 = Text.screen_bottom1(State)
67 if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(screen_bottom1, State.cursor1) then
68 State.screen_top1.line = State.cursor1.line
69 local pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
70 State.screen_top1.pos = pos
71 end
72 end
74 function Text.search_previous(State)
75 if #State.search_term == 0 then return end
76 -- search current line before cursor
77 local curr_pos = State.cursor1.pos
78 local curr_line = State.lines[State.cursor1.line].data
79 local curr_offset = Text.offset(curr_line, curr_pos)
80 local offset = rfind(curr_line, State.search_term, curr_offset-1, --[[literal]] true)
81 if offset then
82 State.cursor1.pos = utf8.len(curr_line, 1, offset)
83 end
84 if offset == nil then
85 -- search lines above cursor
86 for i=State.cursor1.line-1,1,-1 do
87 local curr_line = State.lines[i].data
88 offset = rfind(curr_line, State.search_term, --[[from end]] nil, --[[literal]] true)
89 if offset then
90 State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
91 break
92 end
93 end
94 end
95 if offset == nil then
96 -- wrap around
97 for i=#State.lines,State.cursor1.line+1,-1 do
98 local curr_line = State.lines[i].data
99 offset = rfind(curr_line, State.search_term, --[[from end]] nil, --[[literal]] true)
100 if offset then
101 State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
102 break
106 if offset == nil then
107 -- search current line after cursor
108 local curr_line = State.lines[State.cursor1.line].data
109 offset = rfind(curr_line, State.search_term, --[[from end]] nil, --[[literal]] true)
110 local pos = utf8.len(curr_line, 1, offset)
111 if pos and pos > State.cursor1.pos then
112 State.cursor1.pos = pos
115 if offset == nil then
116 State.cursor1.line = State.search_backup.cursor.line
117 State.cursor1.pos = State.search_backup.cursor.pos
118 State.screen_top1.line = State.search_backup.screen_top.line
119 State.screen_top1.pos = State.search_backup.screen_top.pos
121 local screen_bottom1 = Text.screen_bottom1(State)
122 if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(screen_bottom1, State.cursor1) then
123 State.screen_top1.line = State.cursor1.line
124 local pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
125 State.screen_top1.pos = pos
129 function find(s, pat, i, plain)
130 if s == nil then return end
131 return s:find(pat, i, plain)
134 -- TODO: avoid the expensive reverse() operations
135 -- Particularly if we only care about literal matches, we don't need all of string.find
136 function rfind(s, pat, i, plain)
137 if s == nil then return end
138 if #pat == 0 then return #s end
139 local rs = s:reverse()
140 local rpat = pat:reverse()
141 if i == nil then i = #s end
142 local ri = #s - i + 1
143 local rendpos = rs:find(rpat, ri, plain)
144 if rendpos == nil then return nil end
145 local endpos = #s - rendpos + 1
146 assert (endpos >= #pat, ('rfind: endpos %d should be >= #pat %d at this point'):format(endpos, #pat))
147 return endpos-#pat+1
150 function test_rfind()
151 check_eq(rfind('abc', ''), 3, 'empty pattern')
152 check_eq(rfind('abc', 'c'), 3, 'final char')
153 check_eq(rfind('acbc', 'c', 3), 2, 'previous char')
154 check_nil(rfind('abc', 'd'), 'missing char')
155 check_nil(rfind('abc', 'c', 2), 'no more char')