vis
a vi-like editor based on Plan 9's structural regular expressions
git clone https://9o.is/git/vis.git
vis.lua
(11770B)
1 ---
2 -- Vis Lua plugin API standard library
3 -- @module vis
4
5 ---
6 -- @type Vis
7
8 --- Map a new operator.
9 --
10 -- Sets up a mapping in normal, visual and operator pending mode.
11 -- The operator function will receive the @{File}, @{Range} and position
12 -- to operate on and is expected to return the new cursor position.
13 --
14 -- @tparam string key the key to associate with the new operator
15 -- @tparam function operator the operator logic implemented as Lua function
16 -- @tparam[opt] string help the single line help text as displayed in `:help`
17 -- @treturn bool whether the new operator could be installed
18 -- @usage
19 -- vis:operator_new("gq", function(file, range, pos)
20 -- local status, out, err = vis:pipe(file, range, "fmt")
21 -- if status ~= 0 then
22 -- vis:info(err)
23 -- else
24 -- file:delete(range)
25 -- file:insert(range.start, out)
26 -- end
27 -- return range.start -- new cursor location
28 -- end, "Formatting operator, filter range through fmt(1)")
29 --
30 vis.operator_new = function(vis, key, operator, help)
31 local id = vis:operator_register(operator)
32 if id < 0 then
33 return false
34 end
35 local binding = function()
36 vis:operator(id)
37 end
38 vis:map(vis.modes.NORMAL, key, binding, help)
39 vis:map(vis.modes.VISUAL, key, binding, help)
40 vis:map(vis.modes.OPERATOR_PENDING, key, binding, help)
41 return true
42 end
43
44 --- Map a new motion.
45 --
46 -- Sets up a mapping in normal, visual and operator pending mode.
47 -- The motion function will receive the @{Window} and an initial position
48 -- (in bytes from the start of the file) as argument and is expected to
49 -- return the resulting position.
50 -- @tparam string key the key to associate with the new option
51 -- @tparam function motion the motion logic implemented as Lua function
52 -- @tparam[opt] string help the single line help text as displayed in `:help`
53 -- @treturn bool whether the new motion could be installed
54 -- @usage
55 -- vis:motion_new("<C-l>", function(win, pos)
56 -- return pos+1
57 -- end, "Advance to next byte")
58 --
59 vis.motion_new = function(vis, key, motion, help)
60 local id = vis:motion_register(motion)
61 if id < 0 then
62 return false
63 end
64 local binding = function()
65 vis:motion(id)
66 end
67 vis:map(vis.modes.NORMAL, key, binding, help)
68 vis:map(vis.modes.VISUAL, key, binding, help)
69 vis:map(vis.modes.OPERATOR_PENDING, key, binding, help)
70 return true
71 end
72
73 --- Map a new text object.
74 --
75 -- Sets up a mapping in visual and operator pending mode.
76 -- The text object function will receive the @{Window} and an initial
77 -- position(in bytes from the start of the file) as argument and is
78 -- expected to return the resulting range or `nil`.
79 -- @tparam string key the key associated with the new text object
80 -- @tparam function textobject the text object logic implemented as Lua function
81 -- @tparam[opt] string help the single line help text as displayed in `:help`
82 -- @treturn bool whether the new text object could be installed
83 -- @usage
84 -- vis:textobject_new("<C-l>", function(win, pos)
85 -- return pos, pos+1
86 -- end, "Single byte text object")
87 --
88 vis.textobject_new = function(vis, key, textobject, help)
89 local id = vis:textobject_register(textobject)
90 if id < 0 then
91 return false
92 end
93 local binding = function()
94 vis:textobject(id)
95 end
96 vis:map(vis.modes.VISUAL, key, binding, help)
97 vis:map(vis.modes.OPERATOR_PENDING, key, binding, help)
98 return true
99 end
100
101 --- Check whether a Lua module exists
102 --
103 -- Checks whether a subsequent @{require} call will succeed.
104 -- @tparam string name the module name to check
105 -- @treturn bool whether the module was found
106 vis.module_exist = function(_, name)
107 for _, searcher in ipairs(package.searchers or package.loaders) do
108 local loader = searcher(name)
109 if type(loader) == 'function' then
110 return true
111 end
112 end
113 return false
114 end
115
116 vis.lexers = {}
117
118 if not vis:module_exist('lpeg') then
119 vis:info('WARNING: could not find lpeg module')
120 elseif not vis:module_exist('lexer') then
121 vis:info('WARNING: could not find lexer module')
122 else
123 vis.lexers = require('lexer')
124
125 --- Cache of loaded lexers
126 --
127 -- Caching lexers causes lexer tables to be constructed once and reused
128 -- during each HIGHLIGHT event. Additionally it allows to modify the lexer
129 -- used for syntax highlighting from Lua code.
130 local lexers = {}
131 local load_lexer = vis.lexers.load
132 vis.lexers.load = function (name, alt_name, cache)
133 if cache and lexers[alt_name or name] then return lexers[alt_name or name] end
134 local status, lexer = pcall(load_lexer, name, alt_name)
135 if not status then return nil end
136 if cache then lexers[alt_name or name] = lexer end
137 return lexer
138 end
139 vis.lpeg = require('lpeg')
140 end
141
142 --- Events.
143 --
144 -- User scripts can subscribe Lua functions to certain events. Multiple functions
145 -- can be associated with the same event. They will be called in the order they were
146 -- registered. The first function which returns a non `nil` value terminates event
147 -- propagation. The remaining event handler will not be called.
148 --
149 -- Keep in mind that the editor is blocked while the event handlers
150 -- are being executed, avoid long running tasks.
151 --
152 -- @section Events
153
154 --- Event names.
155 --- @table events
156 local events = {
157 FILE_CLOSE = "Event::FILE_CLOSE", -- see @{file_close}
158 FILE_OPEN = "Event::FILE_OPEN", -- see @{file_open}
159 FILE_SAVE_POST = "Event::FILE_SAVE_POST", -- see @{file_save_post}
160 FILE_SAVE_PRE = "Event::FILE_SAVE_PRE", -- see @{file_save_pre}
161 INIT = "Event::INIT", -- see @{init}
162 INPUT = "Event::INPUT", -- see @{input}
163 QUIT = "Event::QUIT", -- see @{quit}
164 START = "Event::START", -- see @{start}
165 WIN_CLOSE = "Event::WIN_CLOSE", -- see @{win_close}
166 WIN_HIGHLIGHT = "Event::WIN_HIGHLIGHT", -- see @{win_highlight}
167 WIN_OPEN = "Event::WIN_OPEN", -- see @{win_open}
168 WIN_STATUS = "Event::WIN_STATUS", -- see @{win_status}
169 TERM_CSI = "Event::TERM_CSI", -- see @{term_csi}
170 PROCESS_RESPONSE = "Event::PROCESS_RESPONSE", -- see @{process_response}
171 UI_DRAW = "Event::UI_DRAW", -- see @{ui_draw}
172 }
173
174 events.file_close = function(...) events.emit(events.FILE_CLOSE, ...) end
175 events.file_open = function(...) events.emit(events.FILE_OPEN, ...) end
176 events.file_save_post = function(...) events.emit(events.FILE_SAVE_POST, ...) end
177 events.file_save_pre = function(...) return events.emit(events.FILE_SAVE_PRE, ...) end
178 events.init = function(...) events.emit(events.INIT, ...) end
179 events.input = function(...) return events.emit(events.INPUT, ...) end
180 events.quit = function(...) events.emit(events.QUIT, ...) end
181 events.start = function(...) events.emit(events.START, ...) end
182 events.win_close = function(...) events.emit(events.WIN_CLOSE, ...) end
183 events.win_highlight = function(...) events.emit(events.WIN_HIGHLIGHT, ...) end
184 events.win_open = function(...) events.emit(events.WIN_OPEN, ...) end
185 events.win_status = function(...) events.emit(events.WIN_STATUS, ...) end
186 events.term_csi = function(...) events.emit(events.TERM_CSI, ...) end
187 events.process_response = function(...) events.emit(events.PROCESS_RESPONSE, ...) end
188 events.ui_draw = function(...) events.emit(events.UI_DRAW, ...) end
189
190 local handlers = {}
191
192 --- Subscribe to an event.
193 --
194 -- Register an event handler.
195 -- @tparam string event the event name
196 -- @tparam function handler the event handler
197 -- @tparam[opt] int index the index at which to insert the handler (1 is the highest priority)
198 -- @usage
199 -- vis.events.subscribe(vis.events.FILE_SAVE_PRE, function(file, path)
200 -- -- do something useful
201 -- return true
202 -- end)
203 events.subscribe = function(event, handler, index)
204 if not event then error("Invalid event name") end
205 if type(handler) ~= 'function' then error("Invalid event handler") end
206 if not handlers[event] then handlers[event] = {} end
207 events.unsubscribe(event, handler)
208 table.insert(handlers[event], index or #handlers[event]+1, handler)
209 end
210
211 --- Unsubscribe from an event.
212 --
213 -- Remove a registered event handler.
214 -- @tparam string event the event name
215 -- @tparam function handler the event handler to unsubscribe
216 -- @treturn bool whether the handler was successfully removed
217 events.unsubscribe = function(event, handler)
218 local h = handlers[event]
219 if not h then return end
220 for i = 1, #h do
221 if h[i] == handler then
222 table.remove(h, i)
223 return true
224 end
225 end
226 return false
227 end
228
229 --- Generate event.
230 --
231 -- Invokes all event handlers in the order they were registered.
232 -- Passes all arguments to the handler. The first handler which returns a non `nil`
233 -- value terminates the event propagation. The other handlers will not be called.
234 --
235 -- @tparam string event the event name
236 -- @tparam ... ... the remaining parameters are passed on to the handler
237 events.emit = function(event, ...)
238 local h = handlers[event]
239 if not h then return end
240 for i = 1, #h do
241 local ret = h[i](...)
242 if type(ret) ~= 'nil' then return ret end
243 end
244 end
245
246 vis.events = events
247
248 ---
249 -- @type Window
250
251 --- The file type associated with this window.
252 -- @tfield string syntax the syntax lexer name or `nil` if unset
253
254 --- Change syntax lexer to use for this window
255 -- @function set_syntax
256 -- @tparam string syntax the syntax lexer name or `nil` to disable syntax highlighting
257 -- @treturn bool whether the lexer could be changed
258 vis.types.window.set_syntax = function(win, syntax)
259
260 local lexers = vis.lexers
261
262 win:style_define(win.STYLE_DEFAULT, lexers.STYLE_DEFAULT or '')
263 win:style_define(win.STYLE_CURSOR, lexers.STYLE_CURSOR or '')
264 win:style_define(win.STYLE_CURSOR_PRIMARY, lexers.STYLE_CURSOR_PRIMARY or '')
265 win:style_define(win.STYLE_CURSOR_LINE, lexers.STYLE_CURSOR_LINE or '')
266 win:style_define(win.STYLE_SELECTION, lexers.STYLE_SELECTION or '')
267 win:style_define(win.STYLE_LINENUMBER, lexers.STYLE_LINENUMBER or '')
268 win:style_define(win.STYLE_LINENUMBER_CURSOR, lexers.STYLE_LINENUMBER_CURSOR or '')
269 win:style_define(win.STYLE_COLOR_COLUMN, lexers.STYLE_COLOR_COLUMN or '')
270 win:style_define(win.STYLE_STATUS, lexers.STYLE_STATUS or '')
271 win:style_define(win.STYLE_STATUS_FOCUSED, lexers.STYLE_STATUS_FOCUSED or '')
272 win:style_define(win.STYLE_SEPARATOR, lexers.STYLE_SEPARATOR or '')
273 win:style_define(win.STYLE_INFO, lexers.STYLE_INFO or '')
274 win:style_define(win.STYLE_EOF, lexers.STYLE_EOF or '')
275
276 if syntax == nil or syntax == 'off' then
277 win.syntax = nil
278 return true
279 end
280 win.syntax = syntax
281
282 if not lexers.load then return false end
283 local lexer = lexers.load(syntax)
284 if not lexer then return false end
285
286 for id, token_name in ipairs(lexer._TAGS) do
287 local style = lexers['STYLE_' .. token_name:upper():gsub("%.", "_")] or ''
288 if type(style) == 'table' then
289 local s = ''
290 if style.attr then
291 s = string.format("%s,%s", s, style.attr)
292 elseif style.fore then
293 s = string.format("%s,fore:%s", s, style.fore)
294 elseif style.back then
295 s = string.format("%s,back:%s", s, style.back)
296 end
297 style = s
298 end
299 if style ~= nil then win:style_define(id, style) end
300 end
301
302 return true
303 end
304
305 ---
306 -- @type File
307
308 --- Check whether LPeg pattern matches at a given file position.
309 -- @function match_at
310 -- @param pattern the LPeg pattern
311 -- @tparam int pos the absolute file position which should be tested for a match
312 -- @tparam[opt] int horizon the number of bytes around `pos` to consider (defaults to 1K)
313 -- @treturn int start,end the range of the matched region or `nil`
314 vis.types.file.match_at = function(file, pattern, pos, horizon)
315 horizon = horizon or 1024
316 local lpeg = vis.lpeg
317 if not lpeg then return nil end
318 local before, after = pos - horizon, pos + horizon
319 if before < 0 then before = 0 end
320 local data = file:content(before, after - before)
321 local string_pos = pos - before + 1
322
323 local I = lpeg.Cp()
324 local p = lpeg.P{ I * pattern * I + 1 * lpeg.V(1) }
325 local s, e = 1
326 while true do
327 s, e = p:match(data, s)
328 if not s then return nil end
329 if s <= string_pos and string_pos < e then
330 return before + s - 1, before + e - 1
331 end
332 s = e
333 end
334 end
335
336 require('vis-std')