about summary refs log tree commit diff stats
path: root/source_select.lua
blob: 297a7bcf34e05c40d31823d945ece6e1860cd03b (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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
-- helpers for selecting portions of text
-- To keep things simple, we'll ignore the B side when selections start on the
-- A side, and stick to within a single B side selections start in.

-- Return any intersection of the region from State.selection1 to State.cursor1 (or
-- current mouse, if mouse is pressed; or recent mouse if mouse is pressed and
-- currently over a drawing) with the region between {line=line_index, pos=apos}
-- and {line=line_index, pos=bpos}.
-- apos must be less than bpos. However State.selection1 and State.cursor1 can be in any order.
-- Result: positions spos,epos between apos,bpos.
function Text.clip_selection(State, line_index, apos, bpos)
  if State.selection1.line == nil then return nil,nil end
  -- min,max = sorted(State.selection1,State.cursor1)
  local minl,minp = State.selection1.line,State.selection1.pos
  local maxl,maxp
  if App.mouse_down(1) then
    maxl,maxp = Text.mouse_pos(State)
  else
    maxl,maxp = State.cursor1.line,State.cursor1.pos
  end
  if Text.lt1({line=maxl, pos=maxp},
              {line=minl, pos=minp}) then
    minl,maxl = maxl,minl
    minp,maxp = maxp,minp
  end
  -- check if intervals are disjoint
  if line_index < minl then return nil,nil end
  if line_index > maxl then return nil,nil end
  if line_index == minl and bpos <= minp then return nil,nil end
  if line_index == maxl and apos >= maxp then return nil,nil end
  -- compare bounds more carefully (start inclusive, end exclusive)
  local a_ge = Text.le1({line=minl, pos=minp}, {line=line_index, pos=apos})
  local b_lt = Text.lt1({line=line_index, pos=bpos}, {line=maxl, pos=maxp})
--?   print(minl,line_index,maxl, '--', minp,apos,bpos,maxp, '--', a_ge,b_lt)
  if a_ge and b_lt then
    -- fully contained
    return apos,bpos
  elseif a_ge then
    assert(maxl == line_index)
    return apos,maxp
  elseif b_lt then
    assert(minl == line_index)
    return minp,bpos
  else
    assert(minl == maxl and minl == line_index)
    return minp,maxp
  end
end

-- draw highlight for line corresponding to (lo,hi) given an approximate x,y and pos on the same screen line
-- Creates text objects every time, so use this sparingly.
-- Returns some intermediate computation useful elsewhere.
function Text.draw_highlight(State, line, x,y, pos, lo,hi)
  if lo then
    local lo_offset = Text.offset(line.data, lo)
    local hi_offset = Text.offset(line.data, hi)
    local pos_offset = Text.offset(line.data, pos)
    local lo_px
    if pos == lo then
      lo_px = 0
    else
      local before = line.data:sub(pos_offset, lo_offset-1)
      local before_text = App.newText(love.graphics.getFont(), before)
      lo_px = App.width(before_text)
    end
--?     print(lo,pos,hi, '--', lo_offset,pos_offset,hi_offset, '--', lo_px)
    local s = line.data:sub(lo_offset, hi_offset-1)
    local text = App.newText(love.graphics.getFont(), s)
    local text_width = App.width(text)
    App.color(Highlight_color)
    love.graphics.rectangle('fill', x+lo_px,y, text_width,State.line_height)
    App.color(Text_color)
    return lo_px
  end
end

-- inefficient for some reason, so don't do it on every frame
function Text.mouse_pos(State)
  local time = love.timer.getTime()
  if State.recent_mouse.time and State.recent_mouse.time > time-0.1 then
    return State.recent_mouse.line, State.recent_mouse.pos
  end
  State.recent_mouse.time = time
  local line,pos = Text.to_pos(State, App.mouse_x(), App.mouse_y())
  if line then
    State.recent_mouse.line = line
    State.recent_mouse.pos = pos
  end
  return State.recent_mouse.line, State.recent_mouse.pos
end

function Text.to_pos(State, x,y)
  for line_index,line in ipairs(State.lines) do
    if line.mode == 'text' then
      if Text.in_line(State, line_index, x,y) then
        return line_index, Text.to_pos_on_line(State, line_index, x,y)
      end
    end
  end
end

function Text.cut_selection(State)
  if State.selection1.line == nil then return end
  local result = Text.selection(State)
  Text.delete_selection(State)
  return result
end

function Text.delete_selection(State)
  if State.selection1.line == nil then return end
  local minl,maxl = minmax(State.selection1.line, State.cursor1.line)
  local before = snapshot(State, minl, maxl)
  Text.delete_selection_without_undo(State)
  record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
end

function Text.delete_selection_without_undo(State)
  if State.selection1.line == nil then return end
  -- min,max = sorted(State.selection1,State.cursor1)
  local minl,minp = State.selection1.line,State.selection1.pos
  local maxl,maxp = State.cursor1.line,State.cursor1.pos
  if minl > maxl then
    minl,maxl = maxl,minl
    minp,maxp = maxp,minp
  elseif minl == maxl then
    if minp > maxp then
      minp,maxp = maxp,minp
    end
  end
  -- update State.cursor1 and State.selection1
  State.cursor1.line = minl
  State.cursor1.pos = minp
  if Text.lt1(State.cursor1, State.screen_top1) then
    State.screen_top1.line = State.cursor1.line
    State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
  end
  State.selection1 = {}
  -- delete everything between min (inclusive) and max (exclusive)
  Text.clear_screen_line_cache(State, minl)
  local min_offset = Text.offset(State.lines[minl].data, minp)
  local max_offset = Text.offset(State.lines[maxl].data, maxp)
  if minl == maxl then
--?     print('minl == maxl')
    State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..State.lines[minl].data:sub(max_offset)
    return
  end
  assert(minl < maxl)
  local rhs = State.lines[maxl].data:sub(max_offset)
  for i=maxl,minl+1,-1 do
    table.remove(State.lines, i)
    table.remove(State.line_cache, i)
  end
  State.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhs
end

function Text.selection(State)
  if State.selection1.line == nil then return end
  -- min,max = sorted(State.selection1,State.cursor1)
  local minl,minp = State.selection1.line,State.selection1.pos
  local maxl,maxp = State.cursor1.line,State.cursor1.pos
  if minl > maxl then
    minl,maxl = maxl,minl
    minp,maxp = maxp,minp
  elseif minl == maxl then
    if minp > maxp then
      minp,maxp = maxp,minp
    end
  end
  local min_offset = Text.offset(State.lines[minl].data, minp)
  local max_offset = Text.offset(State.lines[maxl].data, maxp)
  if minl == maxl then
    return State.lines[minl].data:sub(min_offset, max_offset-1)
  end
  assert(minl < maxl)
  local result = {State.lines[minl].data:sub(min_offset)}
  for i=minl+1,maxl-1 do
    if State.lines[i].mode == 'text' then
      table.insert(result, State.lines[i].data)
    end
  end
  table.insert(result, State.lines[maxl].data:sub(1, max_offset-1))
  return table.concat(result, '\n')
end