aboutsummaryrefslogtreecommitdiff
path: root/modules/textadept/snippets.lua
diff options
context:
space:
mode:
authormitchell <70453897+667e-11@users.noreply.github.com>2010-06-10 23:42:30 -0400
committermitchell <70453897+667e-11@users.noreply.github.com>2010-06-10 23:42:30 -0400
commit2ddb3c692bebf8011207f8c961ea5c89fb8beabb (patch)
treec295fa70bd72596435ffbf8bedb6885c153b0abd /modules/textadept/snippets.lua
parentfbe7fcf467bd7f947da72cc7a098728d4a6bf364 (diff)
downloadtextadept-2ddb3c692bebf8011207f8c961ea5c89fb8beabb.tar.gz
textadept-2ddb3c692bebf8011207f8c961ea5c89fb8beabb.zip
Renamed _m.textadept.lsnippets to _m.textadept.snippets.
Diffstat (limited to 'modules/textadept/snippets.lua')
-rw-r--r--modules/textadept/snippets.lua480
1 files changed, 480 insertions, 0 deletions
diff --git a/modules/textadept/snippets.lua b/modules/textadept/snippets.lua
new file mode 100644
index 00000000..07ab0c8f
--- /dev/null
+++ b/modules/textadept/snippets.lua
@@ -0,0 +1,480 @@
+-- Copyright 2007-2010 Mitchell mitchell<att>caladbolg.net. See LICENSE.
+
+local textadept = _G.textadept
+local locale = _G.locale
+
+---
+-- Provides Lua-style snippets for Textadept.
+module('_m.textadept.snippets', package.seeall)
+
+-- Markdown:
+-- ## Settings
+--
+-- * `MARK_SNIPPET`: The unique integer mark used to identify the line that
+-- marks the end of a snippet.
+-- * `MARK_SNIPPET_COLOR`: The [Scintilla color][scintilla_color] used for the
+-- line that marks the end of the snippet.
+--
+-- [scintilla_color]: http://scintilla.org/ScintillaDoc.html#colour
+--
+-- ## Overview
+--
+-- Snippets are basically pieces of text inserted into a document, but can
+-- execute code, contain placeholders you can enter dynamic text for, and
+-- perform transformations on that text. This is much more powerful than
+-- standard text templating.
+--
+-- Snippets are defined in the global table `snippets`. Each key-value pair in
+-- `snippets` consist of either:
+--
+-- * A string snippet trigger word and its expanded text.
+-- * A string language name and its associated `snippets`-like table.
+-- * A string style name and its associated `snippets`-like table.
+--
+-- Language names are the names of the lexer files in `lexers/` such as `cpp`
+-- and `lua`. Style names are different lexer styles, most of which are in
+-- `lexers/lexer.lua`; examples are `whitespace`, `comment`, and `string`.
+--
+-- ## Snippet Precedence
+--
+-- When searching for a snippet to expand in the `snippets` table, snippets in
+-- the current style have priority, followed by the ones in the current lexer,
+-- and finally the ones in the global table.
+--
+-- ## Snippet Syntax
+--
+-- A snippet to insert may contain any of the following:
+--
+-- #### Plain Text
+--
+-- Any plain text characters may be used with the exception of `%` and &#96;.
+-- These are special characters and must be "escaped" by prefixing one with a
+-- `%`. As an example, `%%` inserts a single `%` in the snippet.
+--
+-- #### Lua and Shell Code
+--
+-- %(lua_code)
+-- `shell_code`
+--
+-- The code is executed the moment the snippet is inserted.
+--
+-- For Lua code, the global Lua state is available as well as a `selected_text`
+-- variable (containing the current selection in the buffer) for convenience.
+-- Only the return value of the code execution is inserted, not standard out.
+-- Therefore any `print()` statements are meaningless.
+--
+-- Shell code is run via Lua's [`io.popen()`][io_popen].
+--
+-- [io_popen]: http://www.lua.org/manual/5.1/manual.html#pdf-io.popen
+--
+-- #### Tab Stops and Mirrors
+--
+-- %num
+--
+-- These are visited in numeric order (1, 2, 3, etc.) with %0 being the final
+-- position of the caret, or the end of the snippet if %0 is not specified. If
+-- there is a placeholder (described below) with the specified `num`, its text
+-- is mirrored here.
+--
+-- #### Placeholders
+--
+-- %num(text)
+--
+-- These are also visited in numeric order, but have precedence over tab stops,
+-- and insert the specified `text` at the current position upon entry. `text`
+-- can contain Lua code executed at run-time:
+--
+-- %num(#(lua_code))
+--
+-- The global Lua state is available as well as a `selected_text` variable
+-- (containing the current selection in the buffer) for convenience.
+--
+-- `#`'s will have to be escaped with `%` for plain text. Any mis-matched `)`'s
+-- must also be escaped, but balanced `()`'s need not be.
+--
+-- #### Transformations
+--
+-- %num(pattern|replacement)
+--
+-- These act like mirrors, but transform the text that would be inserted using
+-- a given [Lua pattern][lua_pattern] and replacement. Like in placeholders,
+-- `replacement` can contain Lua code executed at run-time as well as the
+-- standard Lua capture sequences: `%n` where 1 <= `n` <= 9.
+--
+-- [lua_pattern]: http://www.lua.org/manual/5.1/manual.html#5.4.1
+--
+-- Any `|`'s after the first one do not need to be escaped.
+--
+-- ## Example
+--
+-- snippets = {
+-- file = '%(buffer.filename)',
+-- lua = {
+-- f = 'function %1(name)(%2(args))\n %0\nend',
+-- string = { [string-specific snippets here] }
+-- }
+-- }
+--
+-- The first snippet is global and runs the Lua code to determine the current
+-- buffer's filename and inserts it. The other snippets apply only in the `lua`
+-- lexer. Any snippets in the `string` table are available only when the current
+-- style is `string` in the `lua` lexer.
+
+-- settings
+MARK_SNIPPET = 4
+MARK_SNIPPET_COLOR = 0x4D9999
+-- end settings
+
+---
+-- Global container that holds all snippet definitions.
+-- @class table
+-- @name _G.snippets
+_G.snippets = {}
+
+_G.snippets.file = "%(buffer.filename)"
+_G.snippets.path = "%((buffer.filename or ''):match('^.+/'))"
+_G.snippets.tab = "%%%1(1)(%2(default))"
+_G.snippets.key = "['%1'] = { %2(func)%3(, %4(arg)) }"
+
+-- The current snippet.
+local snippet = {}
+
+-- The stack of currently running snippets.
+local snippet_stack = {}
+
+-- Replaces escaped characters with their octal equivalents in a given string.
+-- @param s The string to handle escapes in.
+-- @return string with escapes handled.
+local function handle_escapes(s)
+ return s:gsub('%%([%%`%)|#])',
+ function(char) return ("\\%03d"):format(char:byte()) end)
+end
+
+-- Replaces octal characters with their escaped equivalents in a given string.
+-- @param s The string to unhandle escapes in.
+-- @return string with escapes unhandled.
+local function unhandle_escapes(s)
+ local char = string.char
+ return s:gsub('\\(%d%d%d)', function(byte) return '%'..char(byte) end)
+end
+
+-- Replaces escaped characters with the actual characters in a given string.
+-- This is used when escape sequences are no longer needed.
+-- @param s The string to unescape escapes in.
+-- @return string with escapes unescaped.
+local function unescape(s) return s:gsub('%%([%%`%)|#])', '%1') end
+
+-- Gets the start position, end position, and text of the currently running
+-- snippet.
+-- @return start pos, end pos, and snippet text.
+local function snippet_info()
+ local buffer = buffer
+ local s = snippet.start_pos
+ local e =
+ buffer:position_from_line(
+ buffer:marker_line_from_handle(snippet.end_marker)) - 1
+ if e >= s then return s, e, buffer:text_range(s, e) end
+end
+
+-- Runs the given Lua code.
+-- @param code The Lua code to run.
+-- @return string result from the code run.
+local function run_lua_code(code)
+ code = unhandle_escapes(code)
+ local env =
+ setmetatable({ selected_text = buffer:get_sel_text() }, { __index = _G })
+ local _, val = pcall(setfenv(loadstring('return '..code), env))
+ return val or ''
+end
+
+-- If previously at a placeholder or tab stop, attempts to mirror and/or
+-- transform the entered text at all appropriate mirrors before moving on to
+-- the next placeholder or tab stop.
+-- @return false if no snippet was expanded; nil otherwise
+local function next_tab_stop()
+ if not snippet.index then return false end -- no snippet active
+ local buffer = buffer
+ local s_start, s_end, s_text = snippet_info()
+ if not s_text then
+ cancel_current()
+ return
+ end
+
+ local index = snippet.index
+ snippet.snapshots[index] = s_text
+ if index > 0 then
+ buffer:begin_undo_action()
+ local caret = math.max(buffer.anchor, buffer.current_pos)
+ local ph_text = buffer:text_range(snippet.ph_pos, caret)
+
+ -- Transform mirror.
+ s_text =
+ s_text:gsub('%%'..index..'(%b())',
+ function(mirror)
+ local pattern, replacement = mirror:match('^%(([^|]+)|(.+)%)$')
+ if not pattern and not replacement then return ph_text end
+ return ph_text:gsub(unhandle_escapes(pattern),
+ function(...)
+ local arg = {...}
+ local repl = replacement:gsub('%%(%d+)',
+ function(i) return arg[tonumber(i)] or '' end)
+ return repl:gsub('#(%b())', run_lua_code)
+ end, 1)
+ end)
+
+ -- Regular mirror.
+ s_text = s_text:gsub('()%%'..index,
+ function(pos)
+ for mirror, e in s_text:gmatch('%%%d+(%b())()') do
+ local s = mirror:find('|')
+ if s and pos > s and pos < e then return nil end -- inside transform
+ end
+ return ph_text
+ end)
+
+ buffer:set_sel(s_start, s_end)
+ buffer:replace_sel(s_text)
+ s_start, s_end = snippet_info()
+ buffer:end_undo_action()
+ end
+
+ buffer:begin_undo_action()
+ index = index + 1
+ if index <= snippet.max_index then
+ -- Find the next tab stop.
+ local s, e, next_item
+ repeat -- ignore replacement mirrors
+ s, e, next_item = s_text:find('%%'..index..'(%b())', e)
+ until not s or next_item and not next_item:find('|')
+ if next_item then -- placeholder
+ buffer.target_start, buffer.target_end = s_start, buffer.length
+ buffer.search_flags = 0
+ buffer:search_in_target('%'..index..next_item)
+ next_item = next_item:gsub('#(%b())', run_lua_code)
+ next_item = unhandle_escapes(next_item:sub(2, -2))
+ buffer:replace_target(next_item)
+ buffer:set_sel(buffer.target_start, buffer.target_start + #next_item)
+ snippet.ph_pos = buffer.target_start
+ else
+ repeat -- ignore placeholders
+ local found = true
+ s, e = (s_text..' '):find('%%'..index..'[^(]', e)
+ if not s then
+ snippet.index = index + 1
+ next_tab_stop()
+ return
+ end
+ for p_s, p_e in s_text:gmatch('%%%d+()%b()()') do
+ if s > p_s and s < p_e then
+ found = false
+ break
+ end
+ end
+ until found
+ buffer:set_sel(s_start + s - 1, s_start + e - 1)
+ buffer:replace_sel('') -- replace_target() doesn't place caret
+ snippet.ph_pos = s_start + s - 1
+ end
+ -- Place additional carets at mirrors.
+ local _, _, text = snippet_info()
+ text = text:gsub('(%%%d+%b())',
+ function(mirror)
+ -- Lua code in replacement mirrors may contain '%' sequences; do not
+ -- treat as mirrors
+ if mirror:find('|') then return string.rep('_', #mirror) end
+ end)
+ for s, e in text:gmatch('()%%'..index..'()[^(]') do
+ buffer:add_selection(s_start + s - 1, s_start + e - 1)
+ end
+ buffer.main_selection = 0 -- original placeholder/mirror
+ -- Done.
+ snippet.index = index
+ else
+ -- Finished. Find '%0' and place the caret there.
+ s_text = unescape(unhandle_escapes(s_text))
+ buffer:set_sel(s_start, s_end)
+ buffer:replace_sel(s_text)
+ s_start, s_end = snippet_info()
+ if s_end then
+ buffer:goto_pos(s_end + 1)
+ buffer:delete_back()
+ end
+ local s, e = s_text:find('%%0')
+ if s and e then
+ buffer:set_sel(s_start + s - 1, s_start + e)
+ buffer:replace_sel('')
+ end
+ buffer:marker_delete_handle(snippet.end_marker)
+ snippet = #snippet_stack > 0 and table.remove(snippet_stack) or {}
+ end
+ buffer:end_undo_action()
+end
+
+---
+-- Begins expansion of a snippet.
+-- The text inserted has escape sequences handled.
+-- @param s_text Optional snippet to expand. If none is specified, the snippet
+-- is determined from the trigger word (left of the caret), lexer, and style.
+-- @return false if no snippet was expanded; true otherwise.
+function insert(s_text)
+ local buffer = buffer
+ local anchor, caret = buffer.anchor, buffer.current_pos
+ local lexer, style, start, s_name
+ if not s_text then
+ lexer = buffer:get_lexer_language()
+ style = buffer:get_style_name(buffer.style_at[caret])
+ buffer:word_left_extend()
+ start = buffer.current_pos
+ s_name = buffer:get_sel_text()
+ end
+ if s_name then
+ local function try_get_snippet(...)
+ local table = _G.snippets
+ for _, idx in ipairs{...} do table = table[idx] end
+ return type(table) == 'string' and table or error()
+ end
+ local ret
+ ret, s_text = pcall(try_get_snippet, lexer, style, s_name)
+ if not ret then ret, s_text = pcall(try_get_snippet, lexer, s_name) end
+ if not ret then ret, s_text = pcall(try_get_snippet, s_name) end
+ if not ret then buffer:set_sel(anchor, caret) end -- restore caret
+ end
+
+ if s_text then
+ buffer:begin_undo_action()
+ s_text = handle_escapes(s_text)
+
+ -- Take into account tab settings.
+ if not buffer.use_tabs then
+ s_text = s_text:gsub('\t', string.rep(' ', buffer.tab_width))
+ end
+
+ -- Execute Lua and shell code.
+ s_text = s_text:gsub('%%(%b())', run_lua_code)
+ s_text =
+ s_text:gsub('`([^`]+)`',
+ function(code)
+ local p = io.popen(code)
+ local out = p:read('*all'):sub(1, -2)
+ p:close()
+ return out
+ end)
+
+ -- Initialize the new snippet. If one is running, push it onto the stack.
+ if snippet.index then snippet_stack[#snippet_stack + 1] = snippet end
+ snippet = {}
+ snippet.snapshots = {}
+ snippet.start_pos = start or caret
+ snippet.prev_sel_text = buffer:get_sel_text()
+ snippet.index, snippet.max_index = 0, 0
+ for i in s_text:gsub('(%%%d+)%b()', '%1'):gmatch('%%(%d+)') do
+ -- placeholders may contain Lua code that has %n sequences that mess up
+ -- this calculation; the above gsub accounts for this
+ i = tonumber(i)
+ if i > snippet.max_index then snippet.max_index = i end
+ end
+
+ -- Insert the snippet and set a mark defining the end of it.
+ buffer:replace_sel(s_text)
+ buffer:add_text('\n')
+ local line = buffer:line_from_position(buffer.current_pos)
+ snippet.end_marker = buffer:marker_add(line, MARK_SNIPPET)
+ buffer:marker_set_back(MARK_SNIPPET, MARK_SNIPPET_COLOR)
+
+ -- Indent all lines inserted.
+ buffer.current_pos = snippet.start_pos
+ local count = 0
+ for _ in s_text:gmatch('\n') do count = count + 1 end
+ if count > 0 then
+ local ref_line = buffer:line_from_position(start)
+ local isize, ibase = buffer.indent, buffer.line_indentation[ref_line]
+ local inum = ibase / isize -- number of indents needed to match
+ for i = 1, count do
+ local linei = buffer.line_indentation[ref_line + i]
+ buffer.line_indentation[ref_line + i] = linei + isize * inum
+ end
+ end
+ buffer:end_undo_action()
+ end
+
+ return next_tab_stop() ~= false
+end
+
+---
+-- Goes back to the previous placeholder or tab stop, reverting changes made to
+-- subsequent ones.
+-- @return false if no snippet is active; nil otherwise
+function prev()
+ if not snippet.index then return false end -- no snippet active
+ local buffer = buffer
+ local index = snippet.index
+ if index > 1 then
+ local s_start, s_end = snippet_info()
+ local s_text = snippet.snapshots[index - 2]
+ buffer:set_sel(s_start, s_end)
+ buffer:replace_sel(s_text)
+ snippet.index = index - 2
+ next_tab_stop()
+ else
+ cancel_current()
+ end
+end
+
+---
+-- Cancels the active snippet, reverting to the state before its activation,
+-- and restores the previous running snippet (if any).
+function cancel_current()
+ if not snippet.index then return end
+ local buffer = buffer
+ local s_start, s_end = snippet_info()
+ buffer:begin_undo_action()
+ if s_start and s_end then
+ buffer:set_sel(s_start, s_end)
+ buffer:replace_sel('')
+ s_start, s_end = snippet_info()
+ buffer:goto_pos(s_end + 1)
+ buffer:delete_back()
+ end
+ if snippet.prev_sel_text then buffer:add_text(snippet.prev_sel_text) end
+ buffer:end_undo_action()
+ buffer:marker_delete_handle(snippet.end_marker)
+ snippet = #snippet_stack > 0 and table.remove(snippet_stack) or {}
+end
+
+---
+-- Lists available snippets in an autocompletion list.
+-- Global snippets and snippets in the current lexer and style are used.
+function list()
+ local buffer = buffer
+ local list = {}
+ local function add_snippets(snippets)
+ for s_name in pairs(snippets) do list[#list + 1] = s_name end
+ end
+ local snippets = _G.snippets
+ add_snippets(snippets)
+ local lexer = buffer:get_lexer_language()
+ local style = buffer:get_style_name(buffer.style_at[buffer.current_pos])
+ if snippets[lexer] and type(snippets[lexer]) == 'table' then
+ add_snippets(snippets[lexer])
+ if snippets[lexer][style] then add_snippets(snippets[lexer][style]) end
+ end
+ table.sort(list)
+ local caret = buffer.current_pos
+ buffer:auto_c_show(caret - buffer:word_start_position(caret, true),
+ table.concat(list, string.char(buffer.auto_c_separator)))
+end
+
+---
+-- Shows the style at the current caret position in a call tip.
+function show_style()
+ local buffer = buffer
+ local lexer = buffer:get_lexer_language()
+ local style_num = buffer.style_at[buffer.current_pos]
+ local style = buffer:get_style_name(style_num)
+ local text =
+ string.format(locale.M_TEXTADEPT_SNIPPETS_SHOW_STYLE, lexer, style,
+ style_num)
+ buffer:call_tip_show(buffer.current_pos, text)
+end
+
+textadept.user_dofile('snippets.lua') -- load user snippets