aboutsummaryrefslogtreecommitdiff
path: root/lua/vis.lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua/vis.lua')
-rw-r--r--lua/vis.lua335
1 files changed, 335 insertions, 0 deletions
diff --git a/lua/vis.lua b/lua/vis.lua
new file mode 100644
index 0000000..5473f17
--- /dev/null
+++ b/lua/vis.lua
@@ -0,0 +1,335 @@
+---
+-- 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 status ~= 0 then
+-- vis:info(err)
+-- else
+-- file:delete(range)
+-- file:insert(range.start, out)
+-- end
+-- return range.start -- new cursor location
+-- end, "Formatting 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 option
+-- @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(_, 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')
+
+ --- Cache of loaded lexers
+ --
+ -- Caching lexers causes lexer tables to be constructed once and reused
+ -- during each HIGHLIGHT event. Additionally it allows to modify the lexer
+ -- used for syntax highlighting from Lua code.
+ local lexers = {}
+ local load_lexer = vis.lexers.load
+ vis.lexers.load = function (name, alt_name, cache)
+ if cache and lexers[alt_name or name] then return lexers[alt_name or name] end
+ local lexer = load_lexer(name, alt_name)
+ if cache then lexers[alt_name or name] = lexer end
+ return lexer
+ end
+ 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}
+ PROCESS_RESPONSE = "Event::PROCESS_RESPONSE", -- see @{process_response}
+ UI_DRAW = "Event::UI_DRAW", -- see @{ui_draw}
+}
+
+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
+events.process_response = function(...) events.emit(events.PROCESS_RESPONSE, ...) end
+events.ui_draw = function(...) events.emit(events.UI_DRAW, ...) 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 parameters 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 id, token_name in ipairs(lexer._TAGS) do
+ local style = lexers['STYLE_' .. token_name:upper():gsub("%.", "_")] or ''
+ if type(style) == 'table' then
+ local s = ''
+ if style.attr then
+ s = string.format("%s,%s", s, attr)
+ elseif style.fore then
+ s = string.format("%s,fore:%s", s, style.fore)
+ elseif style.back then
+ s = string.format("%s,back:%s", s, style.back)
+ end
+ style = s
+ end
+ if style ~= nil then win:style_define(id, style) end
+ 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')