-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnav.lua
More file actions
305 lines (263 loc) · 8.11 KB
/
nav.lua
File metadata and controls
305 lines (263 loc) · 8.11 KB
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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
local micro = import("micro")
local config = import("micro/config")
local buffer = import("micro/buffer")
local shell = import("micro/shell")
local util = import("micro/util")
local strings = import("strings")
local filepath = import("filepath")
local os_ = import("os")
local regexp = import("regexp")
local jumpStack = {}
function jumpPush(bp, s)
local id = bp:ID()
if jumpStack[id] == nil then
jumpStack[id] = {}
end
table.insert(jumpStack[id], s)
end
function jumpPop(bp)
local id = bp:ID()
if jumpStack[id] == nil then
return nil
end
local s = table.remove(jumpStack[id])
if #jumpStack[id] == 0 then
jumpStack[id] = nil
end
return s
end
function canJump(bp, path, loc)
if bp.Buf.Type.Kind ~= buffer.BTDefault then
micro.InfoBar():Error("Jumping not supported for non-file buffers")
return false
end
if filepath.Abs(path) ~= bp.Buf.AbsPath then
if bp.Buf:Modified() then
micro.InfoBar():Error("Save changes before jumping to ", path)
return false
end
local buf, err = buffer.NewBufferFromFile(path)
if err ~= nil then
micro.InfoBar():Error(err)
return false
end
return true, buf
end
return true, bp.Buf
end
function doJump(bp, buf, loc, push)
if push and bp.Buf.Path ~= "" and (bp.Buf ~= buf or loc.Y ~= bp.Cursor.Loc.Y) then
local s = {
path = bp.Buf.AbsPath,
loc = -bp.Cursor.Loc,
sel = -bp.Cursor.CurSelection,
osel = -bp.Cursor.OrigSelection,
view = -bp:GetView(),
bview = bp:BufView(),
}
jumpPush(bp, s)
end
if bp.Buf ~= buf then
buf:GetActiveCursor():GotoLoc(loc)
bp:OpenBuffer(buf)
else
bp:GotoLoc(loc)
end
bp:ClearInfo()
end
function jump(bp, path, loc, push)
local ok, buf = canJump(bp, path, loc)
if ok then
doJump(bp, buf, loc, push)
return true
end
return false
end
function jumpBack(bp)
local s = jumpPop(bp)
if s == nil then
return false
end
local path = s.path
local rel, err = filepath.Rel(os_.Getwd(), path)
if err == nil then
path = rel
end
if not jump(bp, path, s.loc, false) then
jumpPush(bp, s)
return false
end
bp.Cursor.CurSelection = s.sel
bp.Cursor.OrigSelection = s.osel
local v = bp:GetView()
local bv = bp:BufView()
if bv.Height == s.bview.Height then
v.StartLine = s.view.StartLine
end
if bv.Width == s.bview.Width then
v.StartCol = s.view.StartCol
end
return true
end
function selectAtCurLine(bp, regex, loc)
local loc = -bp.Cursor.Loc
local eol = buffer.Loc(util.CharacterCountInString(bp.Buf:Line(loc.Y)), loc.Y)
local floc, found = bp.Buf:FindNext(regex, loc, eol, loc, true, true)
if found then
bp.Cursor:SetSelectionStart(floc[1])
bp.Cursor:SetSelectionEnd(floc[2])
bp.Cursor.OrigSelection = -bp.Cursor.CurSelection
bp.Cursor:GotoLoc(floc[2])
bp:Relocate()
end
end
function parse(str)
local split = strings.SplitN(str, ":", 3)
if #split < 3 then
return false
end
local line = tonumber(split[2])
if line == fail or line <= 0 then
return false
end
return true, split[1], line - 1, split[3]
end
function isMultiline(output)
local m = string.find(output, "\n")
return m ~= fail and m < string.len(output)
end
local grepProg = "grep -rnEI --exclude-dir=.git --color=always"
local tagProg = "global --result=grep"
local fzfProg = "fzf --layout=reverse --ansi"
function grep(bp, args)
if #args < 1 then
return
end
local output, err = shell.RunInteractiveShell("sh -c '" .. grepProg .. " " ..
strings.Join(args, " ") .. " | " .. fzfProg .. "'", false, true)
if err ~= nil then
return
end
if output == "" then
micro.InfoBar():Message("No matches found")
return
end
local ok, path, line, _ = parse(output)
if not ok then
micro.InfoBar():Error("Invalid grep output: ", output)
return
end
local loc = buffer.Loc(0, line)
if jump(bp, path, loc, true) then
local pattern = args[#args] -- TODO: other cases
selectAtCurLine(bp, pattern)
end
end
function tagPatternRegex(pattern)
if regexp.QuoteMeta(pattern) == pattern then
return "\\b" .. pattern .. "\\b"
end
return pattern
end
function tagFullpatternRegex(pattern)
local regex = regexp.QuoteMeta(pattern)
regex = regex:gsub("%s+", "\\s+") -- gtags merges spaces
regex = "^\\s*" .. regex .. "\\s*$"
return regex
end
function tag(bp, args)
if #args < 1 then
return
end
local pattern = args[1]
local output, err = shell.RunCommand(tagProg .. " " .. pattern)
if err ~= nil then
micro.InfoBar():Error(output)
return
end
if isMultiline(output) then
output, err = shell.RunInteractiveShell("sh -c '" .. tagProg ..
" --color=always " .. pattern .. " | " .. fzfProg .. "'", false, true)
if err ~= nil then
return
end
end
output = strings.TrimRight(output, "\r\n")
if output == "" then
micro.InfoBar():Message("No tag found for ", pattern)
return
end
local ok, path, line, fullpattern = parse(output)
if not ok then
micro.InfoBar():Error("Invalid tag output: ", output)
return
end
local loc = buffer.Loc(0, line)
local ok, buf = canJump(bp, path, loc)
if ok then
local bStart = buf:Start()
local bEnd = buf:End()
-- try to find a precise match
local regex = tagFullpatternRegex(fullpattern)
local floc, found = buf:FindNext(regex, bStart, bEnd, loc, true, true)
if found then
doJump(bp, buf, floc[1], true)
selectAtCurLine(bp, tagPatternRegex(pattern))
if floc[1].Y ~= loc.Y then
micro.InfoBar():Message("Found tag for ", pattern, " with offset ",
floc[1].Y - loc.Y)
end
else
-- try to guess an imprecise match
regex = tagPatternRegex(pattern)
floc, found = buf:FindNext(regex, bStart, bEnd, loc, true, true)
if found then
local nloc = floc[1]
-- try also searching backwards, maybe there is a closer match
floc, found = buf:FindNext(regex, bStart, bEnd, loc, false, true)
if found and math.abs(loc.Y - floc[1].Y) < math.abs(loc.Y - nloc.Y) then
nloc = floc[1]
end
doJump(bp, buf, nloc, true)
selectAtCurLine(bp, regex)
micro.InfoBar():Message("Guessed tag for ", pattern,
". Try regenerating GTAGS.")
else
micro.InfoBar():Message("No tag found for ", pattern, " in ", path,
". Try regenerating GTAGS.")
end
end
end
end
function doClick(bp, e, func)
local mx, my = e:Position()
if my >= bp:BufView().Y + bp:BufView().Height then
return
end
local loc = bp:LocFromVisual(buffer.Loc(mx, my))
if util.IsWordChar(util.RuneAt(bp.Buf:LineBytes(loc.Y), loc.X)) then
bp.Cursor.Loc = loc
bp.Cursor:SelectWord()
func(bp)
else
jumpBack(bp)
end
end
function grepClick(bp, e)
doClick(bp, e, function(bp)
grep(bp, {"-w", util.String(bp.Cursor:GetSelection())})
end)
end
function tagClick(bp, e)
doClick(bp, e, function(bp)
tag(bp, {util.String(bp.Cursor:GetSelection())})
end)
end
function init()
config.MakeCommand("tag", tag, config.NoComplete)
config.MakeCommand("grep", grep, config.NoComplete)
config.TryBindKey("Ctrl-MouseRight", "lua:nav.tagClick", false)
config.TryBindKey("Alt-MouseRight", "lua:nav.grepClick", false)
config.TryBindKey("F9", "lua:nav.jumpBack", false)
config.AddRuntimeFile("nav", config.RTHelp, "help/nav.md")
end