about summary refs log tree commit diff stats
path: root/search.lua
blob: 2b4ea99f605ad85993aab67519338ea49b35a6f1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
-- helpers for the search bar (C-f)

function Text.draw_search_bar(State)
  local h = State.line_height+2
  local y = App.screen.height-h
  love.graphics.setColor(0.9,0.9,0.9)
  love.graphics.rectangle('fill', 0, y-10, App.screen.width-1, h+8)
  love.graphics.setColor(0.6,0.6,0.6)
  love.graphics.line(0, y-10, App.screen.width-1, y-10)
  love.graphics.setColor(1,1,1)
  love.graphics.rectangle('fill', 20, y-6, App.screen.width-40, h+2, 2,2)
  love.graphics.setColor(0.6,0.6,0.6)
  love.graphics.rectangle('line', 20, y-6, App.screen.width-40, h+2, 2,2)
  App.color(Text_color)
  App.screen.print(State.search_term, 25,y-5)
  Text.draw_cursor(State, 25+State.font:getWidth(State.search_term),y-5)
end

function Text.search_next(State)
  assert(#State.search_term > 0)
  -- search current line from cursor
  local curr_pos = State.cursor1.pos
  local curr_line = State.lines[State.cursor1.line].data
  local curr_offset = Text.offset(curr_line, curr_pos)
  local offset = find(curr_line, State.search_term, curr_offset, --[[literal]] true)
  if offset then
    State.cursor1.pos = utf8.len(curr_line, 1, offset)
  end
  if offset == nil then
    -- search lines below cursor
    for i=State.cursor1.line+1,#State.lines do
      local curr_line = State.lines[i].data
      offset = find(curr_line, State.search_term, --[[from start]] nil, --[[literal]] true)
      if offset then
        State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
        break
      end
    end
  end
  if offset == nil then
    -- wrap around
    for i=1,State.cursor1.line-1 do
      local curr_line = State.lines[i].data
      offset = find(curr_line, State.search_term, --[[from start]] nil, --[[literal]] true)
      if offset then
        State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
        break
      end
    end
  end
  if offset == nil then
    -- search current line until cursor
    local curr_line = State.lines[State.cursor1.line].data
    offset = find(curr_line, State.search_term, --[[from start]] nil, --[[literal]] true)
    local pos = utf8.len(curr_line, 1, offset)
    if pos and pos < State.cursor1.pos then
      State.cursor1.pos = pos
    end
  end
  if offset == nil then
    State.cursor1.line = State.search_backup.cursor.line
    State.cursor1.pos = State.search_backup.cursor.pos
    State.screen_top1.line = State.search_backup.screen_top.line
    State.screen_top1.pos = State.search_backup.screen_top.pos
  end
  local screen_bottom1 = Text.screen_bottom1(State)
  if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(screen_bottom1, State.cursor1) then
    State.screen_top1.line = State.cursor1.line
    local pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
    State.screen_top1.pos = pos
  end
end

function Text.search_previous(State)
  assert(#State.search_term > 0)
  -- search current line before cursor
  local curr_pos = State.cursor1.pos
  local curr_line = State.lines[State.cursor1.line].data
  local curr_offset = Text.offset(curr_line, curr_pos)
  local offset = rfind(curr_line, State.search_term, curr_offset-1, --[[literal]] true)
  if offset then
    State.cursor1.pos = utf8.len(curr_line, 1, offset)
  end
  if offset == nil then
    -- search lines above cursor
    for i=State.cursor1.line-1,1,-1 do
      local curr_line = State.lines[i].data
      offset = rfind(curr_line, State.search_term, --[[from end]] nil, --[[literal]] true)
      if offset then
        State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
        break
      end
    end
  end
  if offset == nil then
    -- wrap around
    for i=#State.lines,State.cursor1.line+1,-1 do
      local curr_line = State.lines[i].data
      offset = rfind(curr_line, State.search_term, --[[from end]] nil, --[[literal]] true)
      if offset then
        State.cursor1 = {line=i, pos=utf8.len(curr_line, 1, offset)}
        break
      end
    end
  end
  if offset == nil then
    -- search current line after cursor
    local curr_line = State.lines[State.cursor1.line].data
    offset = rfind(curr_line, State.search_term, --[[from end]] nil, --[[literal]] true)
    local pos = utf8.len(curr_line, 1, offset)
    if pos and pos > State.cursor1.pos then
      State.cursor1.pos = pos
    end
  end
  if offset == nil then
    State.cursor1.line = State.search_backup.cursor.line
    State.cursor1.pos = State.search_backup.cursor.pos
    State.screen_top1.line = State.search_backup.screen_top.line
    State.screen_top1.pos = State.search_backup.screen_top.pos
  end
  local screen_bottom1 = Text.screen_bottom1(State)
  if Text.lt1(State.cursor1, State.screen_top1) or Text.lt1(screen_bottom1, State.cursor1) then
    State.screen_top1.line = State.cursor1.line
    local pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
    State.screen_top1.pos = pos
  end
end

function find(s, pat, i, plain)
  if s == nil then return end
  return s:find(pat, i, plain)
end

-- TODO: avoid the expensive reverse() operations
-- Particularly if we only care about literal matches, we don't need all of string.find
function rfind(s, pat, i, plain)
  if s == nil then return end
  if #pat == 0 then return #s end
  local rs = s:reverse()
  local rpat = pat:reverse()
  if i == nil then i = #s end
  local ri = #s - i + 1
  local rendpos = rs:find(rpat, ri, plain)
  if rendpos == nil then return nil end
  local endpos = #s - rendpos + 1
  assert (endpos >= #pat, ('rfind: endpos %d should be >= #pat %d at this point'):format(endpos, #pat))
  return endpos-#pat+1
end

function test_rfind()
  check_eq(rfind('abc', ''), 3, 'empty pattern')
  check_eq(rfind('abc', 'c'), 3, 'final char')
  check_eq(rfind('acbc', 'c', 3), 2, 'previous char')
  check_nil(rfind('abc', 'd'), 'missing char')
  check_nil(rfind('abc', 'c', 2), 'no more char')
end