forked from martanne/vis
-
Notifications
You must be signed in to change notification settings - Fork 0
/
vis.lua
306 lines (278 loc) · 10.4 KB
/
vis.lua
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
306
---
-- Vis Lua plugin API standard library
-- @module vis
---
-- @type Vis
--- Map a new operator.
--
-- Sets up a mapping in normal, visual and operator pending mode.
-- The operator function will receive the @{File}, @{Range} and position
-- to operate on and is expected to return the new cursor position.
--
-- @tparam string key the key to associate with the new operator
-- @tparam function operator the operator logic implemented as Lua function
-- @tparam[opt] string help the single line help text as displayed in `:help`
-- @treturn bool whether the new operator could be installed
-- @usage
-- vis:operator_new("gq", function(file, range, pos)
-- local status, out, err = vis:pipe(file, range, "fmt")
-- if not status then
-- vis:info(err)
-- else
-- file:delete(range)
-- file:insert(range.start, out)
-- end
-- return range.start -- new cursor location
-- end, "Formating operator, filter range through fmt(1)")
--
vis.operator_new = function(vis, key, operator, help)
local id = vis:operator_register(operator)
if id < 0 then
return false
end
local binding = function()
vis:operator(id)
end
vis:map(vis.modes.NORMAL, key, binding, help)
vis:map(vis.modes.VISUAL, key, binding, help)
vis:map(vis.modes.OPERATOR_PENDING, key, binding, help)
return true
end
--- Map a new motion.
--
-- Sets up a mapping in normal, visual and operator pending mode.
-- The motion function will receive the @{Window} and an initial position
-- (in bytes from the start of the file) as argument and is expected to
-- return the resulting position.
-- @tparam string key the key to associate with the new mption
-- @tparam function motion the motion logic implemented as Lua function
-- @tparam[opt] string help the single line help text as displayed in `:help`
-- @treturn bool whether the new motion could be installed
-- @usage
-- vis:motion_new("<C-l>", function(win, pos)
-- return pos+1
-- end, "Advance to next byte")
--
vis.motion_new = function(vis, key, motion, help)
local id = vis:motion_register(motion)
if id < 0 then
return false
end
local binding = function()
vis:motion(id)
end
vis:map(vis.modes.NORMAL, key, binding, help)
vis:map(vis.modes.VISUAL, key, binding, help)
vis:map(vis.modes.OPERATOR_PENDING, key, binding, help)
return true
end
--- Map a new text object.
--
-- Sets up a mapping in visual and operator pending mode.
-- The text object function will receive the @{Window} and an initial
-- position(in bytes from the start of the file) as argument and is
-- expected to return the resulting range or `nil`.
-- @tparam string key the key associated with the new text object
-- @tparam function textobject the text object logic implemented as Lua function
-- @tparam[opt] string help the single line help text as displayed in `:help`
-- @treturn bool whether the new text object could be installed
-- @usage
-- vis:textobject_new("<C-l>", function(win, pos)
-- return pos, pos+1
-- end, "Single byte text object")
--
vis.textobject_new = function(vis, key, textobject, help)
local id = vis:textobject_register(textobject)
if id < 0 then
return false
end
local binding = function()
vis:textobject(id)
end
vis:map(vis.modes.VISUAL, key, binding, help)
vis:map(vis.modes.OPERATOR_PENDING, key, binding, help)
return true
end
--- Check whether a Lua module exists
--
-- Checks whether a subsequent @{require} call will succeed.
-- @tparam string name the module name to check
-- @treturn bool whether the module was found
vis.module_exist = function(vis, name)
for _, searcher in ipairs(package.searchers or package.loaders) do
local loader = searcher(name)
if type(loader) == 'function' then
return true
end
end
return false
end
vis.lexers = {}
if not vis:module_exist('lpeg') then
vis:info('WARNING: could not find lpeg module')
elseif not vis:module_exist('lexer') then
vis:info('WARNING: could not find lexer module')
else
vis.lexers = require('lexer')
vis.lpeg = require('lpeg')
end
--- Events.
--
-- User scripts can subscribe Lua functions to certain events. Multiple functions
-- can be associated with the same event. They will be called in the order they were
-- registered. The first function which returns a non `nil` value terminates event
-- propagation. The remaining event handler will not be called.
--
-- Keep in mind that the editor is blocked while the event handlers
-- are being executed, avoid long running tasks.
--
-- @section Events
--- Event names.
--- @table events
local events = {
FILE_CLOSE = "Event::FILE_CLOSE", -- see @{file_close}
FILE_OPEN = "Event::FILE_OPEN", -- see @{file_open}
FILE_SAVE_POST = "Event::FILE_SAVE_POST", -- see @{file_save_post}
FILE_SAVE_PRE = "Event::FILE_SAVE_PRE", -- see @{file_save_pre}
INIT = "Event::INIT", -- see @{init}
INPUT = "Event::INPUT", -- see @{input}
QUIT = "Event::QUIT", -- see @{quit}
START = "Event::START", -- see @{start}
WIN_CLOSE = "Event::WIN_CLOSE", -- see @{win_close}
WIN_HIGHLIGHT = "Event::WIN_HIGHLIGHT", -- see @{win_highlight}
WIN_OPEN = "Event::WIN_OPEN", -- see @{win_open}
WIN_STATUS = "Event::WIN_STATUS", -- see @{win_status}
TERM_CSI = "Event::TERM_CSI", -- see @{term_csi}
}
events.file_close = function(...) events.emit(events.FILE_CLOSE, ...) end
events.file_open = function(...) events.emit(events.FILE_OPEN, ...) end
events.file_save_post = function(...) events.emit(events.FILE_SAVE_POST, ...) end
events.file_save_pre = function(...) return events.emit(events.FILE_SAVE_PRE, ...) end
events.init = function(...) events.emit(events.INIT, ...) end
events.input = function(...) return events.emit(events.INPUT, ...) end
events.quit = function(...) events.emit(events.QUIT, ...) end
events.start = function(...) events.emit(events.START, ...) end
events.win_close = function(...) events.emit(events.WIN_CLOSE, ...) end
events.win_highlight = function(...) events.emit(events.WIN_HIGHLIGHT, ...) end
events.win_open = function(...) events.emit(events.WIN_OPEN, ...) end
events.win_status = function(...) events.emit(events.WIN_STATUS, ...) end
events.term_csi = function(...) events.emit(events.TERM_CSI, ...) end
local handlers = {}
--- Subscribe to an event.
--
-- Register an event handler.
-- @tparam string event the event name
-- @tparam function handler the event handler
-- @tparam[opt] int index the index at which to insert the handler (1 is the highest priority)
-- @usage
-- vis.events.subscribe(vis.events.FILE_SAVE_PRE, function(file, path)
-- -- do something useful
-- return true
-- end)
events.subscribe = function(event, handler, index)
if not event then error("Invalid event name") end
if type(handler) ~= 'function' then error("Invalid event handler") end
if not handlers[event] then handlers[event] = {} end
events.unsubscribe(event, handler)
table.insert(handlers[event], index or #handlers[event]+1, handler)
end
--- Unsubscribe from an event.
--
-- Remove a registered event handler.
-- @tparam string event the event name
-- @tparam function handler the event handler to unsubscribe
-- @treturn bool whether the handler was successfully removed
events.unsubscribe = function(event, handler)
local h = handlers[event]
if not h then return end
for i = 1, #h do
if h[i] == handler then
table.remove(h, i)
return true
end
end
return false
end
--- Generate event.
--
-- Invokes all event handlers in the order they were registered.
-- Passes all arguments to the handler. The first handler which returns a non `nil`
-- value terminates the event propagation. The other handlers will not be called.
--
-- @tparam string event the event name
-- @tparam ... ... the remaining paramters are passed on to the handler
events.emit = function(event, ...)
local h = handlers[event]
if not h then return end
for i = 1, #h do
local ret = h[i](...)
if type(ret) ~= 'nil' then return ret end
end
end
vis.events = events
---
-- @type Window
--- The file type associated with this window.
-- @tfield string syntax the syntax lexer name or `nil` if unset
--- Change syntax lexer to use for this window
-- @function set_syntax
-- @tparam string syntax the syntax lexer name or `nil` to disable syntax highlighting
-- @treturn bool whether the lexer could be changed
vis.types.window.set_syntax = function(win, syntax)
local lexers = vis.lexers
win:style_define(win.STYLE_DEFAULT, lexers.STYLE_DEFAULT or '')
win:style_define(win.STYLE_CURSOR, lexers.STYLE_CURSOR or '')
win:style_define(win.STYLE_CURSOR_PRIMARY, lexers.STYLE_CURSOR_PRIMARY or '')
win:style_define(win.STYLE_CURSOR_LINE, lexers.STYLE_CURSOR_LINE or '')
win:style_define(win.STYLE_SELECTION, lexers.STYLE_SELECTION or '')
win:style_define(win.STYLE_LINENUMBER, lexers.STYLE_LINENUMBER or '')
win:style_define(win.STYLE_LINENUMBER_CURSOR, lexers.STYLE_LINENUMBER_CURSOR or '')
win:style_define(win.STYLE_COLOR_COLUMN, lexers.STYLE_COLOR_COLUMN or '')
win:style_define(win.STYLE_STATUS, lexers.STYLE_STATUS or '')
win:style_define(win.STYLE_STATUS_FOCUSED, lexers.STYLE_STATUS_FOCUSED or '')
win:style_define(win.STYLE_SEPARATOR, lexers.STYLE_SEPARATOR or '')
win:style_define(win.STYLE_INFO, lexers.STYLE_INFO or '')
win:style_define(win.STYLE_EOF, lexers.STYLE_EOF or '')
if syntax == nil or syntax == 'off' then
win.syntax = nil
return true
end
if not lexers.load then return false end
local lexer = lexers.load(syntax)
if not lexer then return false end
for token_name, id in pairs(lexer._TOKENSTYLES) do
local style = lexers['STYLE_'..string.upper(token_name)] or lexer._EXTRASTYLES[token_name]
win:style_define(id, style)
end
win.syntax = syntax
return true
end
---
-- @type File
--- Check whether LPeg pattern matches at a given file position.
-- @function match_at
-- @param pattern the LPeg pattern
-- @tparam int pos the absolute file position which should be tested for a match
-- @tparam[opt] int horizon the number of bytes around `pos` to consider (defaults to 1K)
-- @treturn int start,end the range of the matched region or `nil`
vis.types.file.match_at = function(file, pattern, pos, horizon)
horizon = horizon or 1024
local lpeg = vis.lpeg
if not lpeg then return nil end
local before, after = pos - horizon, pos + horizon
if before < 0 then before = 0 end
local data = file:content(before, after - before)
local string_pos = pos - before + 1
local I = lpeg.Cp()
local p = lpeg.P{ I * pattern * I + 1 * lpeg.V(1) }
local s, e = 1
while true do
s, e = p:match(data, s)
if not s then return nil end
if s <= string_pos and string_pos < e then
return before + s - 1, before + e - 1
end
s = e
end
end
require('vis-std')