filetype: Set "groovy" for Jenkinsfile
[vis.git] / lua / vis.lua
blob699d2e2c1e13ebb3f08f6b99b7149905436995b7
1 ---
2 -- Vis Lua plugin API standard library
3 -- @module vis
5 ---
6 -- @type Vis
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.
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 not status 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, "Formating operator, filter range through fmt(1)")
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
44 --- Map a new motion.
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 mption
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")
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
73 --- Map a new text object.
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")
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
101 --- Check whether a Lua module exists
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
113 return false
116 vis.lexers = {}
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 vis.lpeg = require('lpeg')
127 --- Events.
129 -- User scripts can subscribe Lua functions to certain events. Multiple functions
130 -- can be associated with the same event. They will be called in the order they were
131 -- registered. The first function which returns a non `nil` value terminates event
132 -- propagation. The remaining event handler will not be called.
134 -- Keep in mind that the editor is blocked while the event handlers
135 -- are being executed, avoid long running tasks.
137 -- @section Events
139 --- Event names.
140 --- @table events
141 local events = {
142 FILE_CLOSE = "Event::FILE_CLOSE", -- see @{file_close}
143 FILE_OPEN = "Event::FILE_OPEN", -- see @{file_open}
144 FILE_SAVE_POST = "Event::FILE_SAVE_POST", -- see @{file_save_post}
145 FILE_SAVE_PRE = "Event::FILE_SAVE_PRE", -- see @{file_save_pre}
146 INIT = "Event::INIT", -- see @{init}
147 INPUT = "Event::INPUT", -- see @{input}
148 QUIT = "Event::QUIT", -- see @{quit}
149 START = "Event::START", -- see @{start}
150 WIN_CLOSE = "Event::WIN_CLOSE", -- see @{win_close}
151 WIN_HIGHLIGHT = "Event::WIN_HIGHLIGHT", -- see @{win_highlight}
152 WIN_OPEN = "Event::WIN_OPEN", -- see @{win_open}
153 WIN_STATUS = "Event::WIN_STATUS", -- see @{win_status}
154 TERM_CSI = "Event::TERM_CSI", -- see @{term_csi}
157 events.file_close = function(...) events.emit(events.FILE_CLOSE, ...) end
158 events.file_open = function(...) events.emit(events.FILE_OPEN, ...) end
159 events.file_save_post = function(...) events.emit(events.FILE_SAVE_POST, ...) end
160 events.file_save_pre = function(...) return events.emit(events.FILE_SAVE_PRE, ...) end
161 events.init = function(...) events.emit(events.INIT, ...) end
162 events.input = function(...) return events.emit(events.INPUT, ...) end
163 events.quit = function(...) events.emit(events.QUIT, ...) end
164 events.start = function(...) events.emit(events.START, ...) end
165 events.win_close = function(...) events.emit(events.WIN_CLOSE, ...) end
166 events.win_highlight = function(...) events.emit(events.WIN_HIGHLIGHT, ...) end
167 events.win_open = function(...) events.emit(events.WIN_OPEN, ...) end
168 events.win_status = function(...) events.emit(events.WIN_STATUS, ...) end
169 events.term_csi = function(...) events.emit(events.TERM_CSI, ...) end
171 local handlers = {}
173 --- Subscribe to an event.
175 -- Register an event handler.
176 -- @tparam string event the event name
177 -- @tparam function handler the event handler
178 -- @tparam[opt] int index the index at which to insert the handler (1 is the highest priority)
179 -- @usage
180 -- vis.events.subscribe(vis.events.FILE_SAVE_PRE, function(file, path)
181 -- -- do something useful
182 -- return true
183 -- end)
184 events.subscribe = function(event, handler, index)
185 if not event then error("Invalid event name") end
186 if type(handler) ~= 'function' then error("Invalid event handler") end
187 if not handlers[event] then handlers[event] = {} end
188 events.unsubscribe(event, handler)
189 table.insert(handlers[event], index or #handlers[event]+1, handler)
192 --- Unsubscribe from an event.
194 -- Remove a registered event handler.
195 -- @tparam string event the event name
196 -- @tparam function handler the event handler to unsubscribe
197 -- @treturn bool whether the handler was successfully removed
198 events.unsubscribe = function(event, handler)
199 local h = handlers[event]
200 if not h then return end
201 for i = 1, #h do
202 if h[i] == handler then
203 table.remove(h, i)
204 return true
207 return false
210 --- Generate event.
212 -- Invokes all event handlers in the order they were registered.
213 -- Passes all arguments to the handler. The first handler which returns a non `nil`
214 -- value terminates the event propagation. The other handlers will not be called.
216 -- @tparam string event the event name
217 -- @tparam ... ... the remaining paramters are passed on to the handler
218 events.emit = function(event, ...)
219 local h = handlers[event]
220 if not h then return end
221 for i = 1, #h do
222 local ret = h[i](...)
223 if type(ret) ~= 'nil' then return ret end
227 vis.events = events
230 -- @type Window
232 --- The file type associated with this window.
233 -- @tfield string syntax the syntax lexer name or `nil` if unset
235 --- Change syntax lexer to use for this window
236 -- @function set_syntax
237 -- @tparam string syntax the syntax lexer name or `nil` to disable syntax highlighting
238 -- @treturn bool whether the lexer could be changed
239 vis.types.window.set_syntax = function(win, syntax)
241 local lexers = vis.lexers
243 win:style_define(win.STYLE_DEFAULT, lexers.STYLE_DEFAULT or '')
244 win:style_define(win.STYLE_CURSOR, lexers.STYLE_CURSOR or '')
245 win:style_define(win.STYLE_CURSOR_PRIMARY, lexers.STYLE_CURSOR_PRIMARY or '')
246 win:style_define(win.STYLE_CURSOR_LINE, lexers.STYLE_CURSOR_LINE or '')
247 win:style_define(win.STYLE_SELECTION, lexers.STYLE_SELECTION or '')
248 win:style_define(win.STYLE_LINENUMBER, lexers.STYLE_LINENUMBER or '')
249 win:style_define(win.STYLE_LINENUMBER_CURSOR, lexers.STYLE_LINENUMBER_CURSOR or '')
250 win:style_define(win.STYLE_COLOR_COLUMN, lexers.STYLE_COLOR_COLUMN or '')
251 win:style_define(win.STYLE_STATUS, lexers.STYLE_STATUS or '')
252 win:style_define(win.STYLE_STATUS_FOCUSED, lexers.STYLE_STATUS_FOCUSED or '')
253 win:style_define(win.STYLE_SEPARATOR, lexers.STYLE_SEPARATOR or '')
254 win:style_define(win.STYLE_INFO, lexers.STYLE_INFO or '')
255 win:style_define(win.STYLE_EOF, lexers.STYLE_EOF or '')
257 if syntax == nil or syntax == 'off' then
258 win.syntax = nil
259 return true
262 if not lexers.load then return false end
263 local lexer = lexers.load(syntax)
264 if not lexer then return false end
266 for token_name, id in pairs(lexer._TOKENSTYLES) do
267 local style = lexers['STYLE_'..string.upper(token_name)] or lexer._EXTRASTYLES[token_name]
268 win:style_define(id, style)
271 win.syntax = syntax
272 return true
276 -- @type File
278 --- Check whether LPeg pattern matches at a given file position.
279 -- @function match_at
280 -- @param pattern the LPeg pattern
281 -- @tparam int pos the absolute file position which should be tested for a match
282 -- @tparam[opt] int horizon the number of bytes around `pos` to consider (defaults to 1K)
283 -- @treturn int start,end the range of the matched region or `nil`
284 vis.types.file.match_at = function(file, pattern, pos, horizon)
285 horizon = horizon or 1024
286 local lpeg = vis.lpeg
287 if not lpeg then return nil end
288 local before, after = pos - horizon, pos + horizon
289 if before < 0 then before = 0 end
290 local data = file:content(before, after - before)
291 local string_pos = pos - before + 1
293 local I = lpeg.Cp()
294 local p = lpeg.P{ I * pattern * I + 1 * lpeg.V(1) }
295 local s, e = 1
296 while true do
297 s, e = p:match(data, s)
298 if not s then return nil end
299 if s <= string_pos and string_pos < e then
300 return before + s - 1, before + e - 1
302 s = e
306 require('vis-std')