-- Copyright 2007-2011 Mitchell mitchellcaladbolg.net. See LICENSE. local L = _G.locale.localize --- -- Provides Lua-style snippets for Textadept. module('_m.textadept.snippets', package.seeall) -- Markdown: -- ## 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 text should contain spaces instead of tabs since Textadept -- automatically converts spaces to tabs depending on the current settings. -- -- ## 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 -- -- ## 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 `. -- 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](../manual/14_Appendix.html#lua_patterns) and -- replacement. Like in placeholders, `replacement` can contain Lua code -- executed at run-time as well as the standard Lua capture (`%n`) sequences. -- 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 file = "%(buffer.filename)" path = "%((buffer.filename or ''):match('^.+/'))" tab = "%%%1(1)(%2(default))" 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 = buffer.selection_end local ph_text = buffer:text_range(snippet.ph_pos, caret) -- Transform mirror. local function transform_mirror(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 s_text = s_text:gsub('%%'..index..'(%b())', transform_mirror) -- 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 inside transform, do not do anything. if s and pos > s and pos < e then return nil end 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() 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() 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() local style_num = buffer.style_at[buffer.current_pos] local style = buffer:get_style_name(style_num) local text = string.format("%s %s\n%s %s (%d)", L('Lexer'), lexer, L('Style'), style, style_num) buffer:call_tip_show(buffer.current_pos, text) end --- -- Provides access to snippets from _G. -- @class table -- @name _G.snippets _G.snippets = _M