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
184
185
186
187
188
189
190
191
|
-- mod-version:3
local core = require "core"
local keymap = require "core.keymap"
local command = require "core.command"
-- Compatibility with latest lite-xl regex changes.
local regex_match = regex.find_offsets or regex.match
-- Will iterate back through any UTF-8 bytes so that we don't replace bits
-- mid character.
local function previous_character(str, index)
local byte
repeat
index = index - 1
byte = string.byte(str, index)
until byte < 128 or byte >= 192
return index
end
-- Moves to the end of the identified character.
local function end_character(str, index)
local byte = string.byte(str, index + 1)
while byte and byte >= 128 and byte < 192 do
index = index + 1
byte = string.byte(str, index + 1)
end
return index
end
-- Build off matching. For now, only support basic replacements, but capture
-- groupings should be doable. We can even have custom group replacements and
-- transformations and stuff in lua. Currently, this takes group replacements
-- as \1 - \9.
-- Should work on UTF-8 text.
local function substitute(pattern_string, str, replacement)
local pattern = type(pattern_string) == "table" and
pattern_string or regex.compile(pattern_string)
local result, indices = {}
local matches, replacements = {}, {}
local offset = 0
repeat
indices = { regex.cmatch(pattern, str, offset) }
if #indices > 0 then
table.insert(matches, indices)
local currentReplacement = replacement
if #indices > 2 then
for i = 1, (#indices/2 - 1) do
currentReplacement = string.gsub(
currentReplacement,
"\\" .. i,
str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1))
)
end
end
currentReplacement = string.gsub(currentReplacement, "\\%d", "")
table.insert(replacements, { indices[1], #currentReplacement+indices[1] })
if indices[1] > 1 then
table.insert(result, str:sub(offset, previous_character(str, indices[1])) .. currentReplacement)
else
table.insert(result, currentReplacement)
end
offset = indices[2]
end
until #indices == 0 or indices[1] == indices[2]
return table.concat(result) .. str:sub(offset), matches, replacements
end
-- Takes the following pattern: /pattern/replace/
-- Capture groupings can be replaced using \1 through \9
local function regex_replace_file(view, pattern, old_lines, raw, start_line, end_line)
local doc = view.doc
local start_pattern, end_pattern, end_replacement, start_replacement = 2, 2;
repeat
end_pattern = string.find(pattern, "/", end_pattern)
until end_pattern == nil or pattern[end_pattern-1] ~= "\\"
if end_pattern == nil then
end_pattern = #pattern + 1
else
end_pattern = end_pattern - 1
start_replacement = end_pattern+2;
end_replacement = end_pattern+2;
repeat
end_replacement = string.find(pattern, "/", end_replacement)
until end_replacement == nil or pattern[end_replacement-1] ~= "\\"
end
end_replacement = end_replacement and (end_replacement - 1)
local re = start_pattern ~= end_pattern
and regex.compile(pattern:sub(start_pattern, end_pattern))
local replacement = end_replacement and pattern:sub(
start_replacement, end_replacement
)
local replace_line = raw and function(line, new_text)
if line == #doc.lines then
doc:raw_remove(line, 1, line, #doc.lines[line], { idx = 1 }, 0)
else
doc:raw_remove(line, 1, line+1, 1, { idx = 1 }, 0)
end
doc:raw_insert(line, 1, new_text, { idx = 1 }, 0)
end or function(line, new_text)
if line == #doc.lines then
doc:remove(line, 1, line, #doc.lines[line])
else
doc:remove(line, 1, line+1, 1)
end
doc:insert(line, 1, new_text)
end
local line_scroll = nil
if re then
for i = (start_line or 1), (end_line or #doc.lines) do
local new_text, matches, rmatches
local old_text = old_lines[i] or doc.lines[i]
local old_length = #old_text
if replacement then
new_text, matches, rmatches = substitute(re, old_text, replacement)
end
if matches and #matches > 0 then
old_lines[i] = old_text
replace_line(i, new_text)
if line_scroll == nil then
line_scroll = i
doc:set_selection(i, rmatches[1][1], i, rmatches[1][2])
end
elseif old_lines[i] then
replace_line(i, old_lines[i])
old_lines[i] = nil
end
if not replacement then
local s,e = regex_match(re, old_text)
if s then
line_scroll = i
doc:set_selection(i, s, i, e)
break
end
end
end
if line_scroll then
view:scroll_to_line(line_scroll, true)
end
end
if replacement == nil then
for k,v in pairs(old_lines) do
replace_line(k, v)
end
old_lines = {}
end
return old_lines, line_scroll ~= nil
end
command.add("core.docview!", {
["regex-replace-preview:find-replace-regex"] = function(view)
local old_lines = {}
local doc = view.doc
local original_selection = { doc:get_selection(true) }
local selection = doc:has_selection() and { doc:get_selection(true) } or {}
core.command_view:enter("Regex Replace (enter pattern as /old/new/)", {
text = "/",
submit = function(pattern)
regex_replace_file(view, pattern, {}, false, selection[1], selection[3])
end,
suggest = function(pattern)
local incremental, has_replacement = regex_replace_file(
view, pattern, old_lines, true, selection[1], selection[3]
)
if incremental then
old_lines = incremental
end
if not has_replacement then
doc:set_selection(table.unpack(original_selection))
end
end,
cancel = function(pattern)
for k,v in pairs(old_lines) do
if v then
if k == #doc.lines then
doc:raw_remove(k, 1, k, #doc.lines[k], { idx = 1 }, 0)
else
doc:raw_remove(k, 1, k+1, 1, { idx = 1 }, 0)
end
doc:raw_insert(k, 1, v, { idx = 1 }, 0)
end
end
doc:set_selection(table.unpack(original_selection))
end
})
end
})
keymap.add { ["ctrl+shift+r"] = "regex-replace-preview:find-replace-regex" }
|