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')