From 58dc16406976d8c18191470a6bafaeda793ed523 Mon Sep 17 00:00:00 2001 From: mitchell <70453897+667e-11@users.noreply.github.com> Date: Mon, 6 Aug 2007 05:04:35 -0400 Subject: Initial import of the textadept module. --- modules/textadept/snippets.lua | 438 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 438 insertions(+) create mode 100644 modules/textadept/snippets.lua (limited to 'modules/textadept/snippets.lua') diff --git a/modules/textadept/snippets.lua b/modules/textadept/snippets.lua new file mode 100644 index 00000000..e8b01700 --- /dev/null +++ b/modules/textadept/snippets.lua @@ -0,0 +1,438 @@ +-- Copyright 2007 Mitchell mitchellcaladbolg.net. See LICENSE. + +--- +-- Provides Textmate-like snippets for the textadept module. +-- There are several option variables used: +-- MARK_SNIPPET: The integer mark used to identify the line that marks the +-- end of a snippet. +-- SCOPES_ENABLED: Flag indicating whether scopes/styles can be used for +-- snippets. +-- FILE_IN: Location of the temporary file used as STDIN for regex mirrors. +-- FILE_OUT: Location of the temporary file that will contain output for +-- regex mirrors. +-- REDIRECT: The command line symbol used for redirecting STDOUT to a file. +-- RUBY_CMD: The command that executes the Ruby interpreter. +-- MARK_SNIPPET_COLOR: The Scintilla color used for the line that marks the +-- end of the snippet. +module('modules.textadept.snippets', package.seeall) + +-- options +local MARK_SNIPPET = 4 +local SCOPES_ENABLED = true +local MARK_SNIPPET_COLOR = 0x4D9999 +local DEBUG = false +local RUN_TESTS = false +-- end options + +--- +-- Global container that holds all snippet definitions. +-- @class table +-- @name snippets +_G.snippets = {} + +-- some default 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}} }" + +--- +-- [Local table] The current snippet. +-- @class table +-- @name snippet +local snippet = {} + +--- +-- [Local table] The stack of currently running snippets. +-- @class table +-- @name snippet_stack +local snippet_stack = {} + +-- local functions +local next_snippet_item +local snippet_text, match_indention, join_lines, load_scopes +local escape, unescape, remove_escapes, _DEBUG + +--- +-- Begins expansion of a snippet. +-- @param snippet_arg Optional snippet to expand. If none is specified, the +-- snippet is determined from the trigger word to the left of the caret, the +-- lexer, and scope. +function insert(snippet_arg) + local buffer = buffer + local orig_pos, new_pos, s_name + local selected_text = buffer:get_sel_text() + if not snippet_arg then + orig_pos = buffer.current_pos buffer:word_left_extend() + new_pos = buffer.current_pos + lexer = buffer:get_lexer_language() + style = buffer.style_at[orig_pos] + scope = buffer:get_style_name(style) + s_name = buffer:get_sel_text() + else + if buffer.current_pos > buffer.anchor then + buffer.current_pos, buffer.anchor = buffer.anchor, buffer.current_pos + end + orig_pos, new_pos = buffer.current_pos, buffer.current_pos + end + + -- Get snippet text by lexer, scope, and/or trigger word. + local s_text + if s_name then + _DEBUG('s_name: '..s_name..', lexer: '..lexer..', scope: '..scope) + local function try_get_snippet(...) + local table = _G.snippets + for _, idx in ipairs(arg) do table = table[idx] end + if table and type(table) == 'string' then return table end + end + local ret + if SCOPES_ENABLED then + ret, s_text = pcall(try_get_snippet, lexer, scope, s_name) + end + 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 + else + s_text = snippet_arg + end + + buffer:begin_undo_action() + if s_text then + s_text = escape(s_text) + _DEBUG('s_text escaped:\n'..s_text) + + -- Replace Lua code return. + s_text = s_text:gsub('$(%b())', + function(s) + local ret, val = pcall( loadstring( 'return '..s:sub(2, -2) ) ) + if ret then return val or '' end + buffer:goto_pos(orig_pos) + error(val) + end) + + -- Execute any shell code. + s_text = s_text:gsub('`(.-)`', + function(code) + local out = io.popen(code):read('*all') + if out:sub(-1) == '\n' then return out:sub(1, -2) end + end) + + -- If another snippet is running, push it onto the stack. + if snippet.index then snippet_stack[#snippet_stack + 1] = snippet end + + snippet = {} + snippet.index = 0 + snippet.start_pos = buffer.current_pos + snippet.cursor = nil + snippet.sel_text = selected_text + + -- Make a table of placeholders and tab stops. + local patt, patt2 = '($%b{})', '^%${(%d+):.*}$' + local s, _, item = s_text:find(patt) + while item do + local num = item:match(patt2) + if num then snippet[ tonumber(num) ] = unescape(item) end + local i = s + 1 + s, _, item = s_text:find(patt, i) + end + + s_text = unescape(s_text) + _DEBUG('s_text unescaped:\n'..s_text) + + -- Insert the snippet and set a mark defining the end of it. + buffer:replace_sel(s_text) + buffer:new_line() + 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) + _DEBUG('snippet:') + if DEBUG then table.foreach(snippet, print) end + + -- Indent all lines inserted. + buffer.current_pos = new_pos + local count, i = -1, -1 + repeat + count = count + 1 + i = s_text:find('\n', i + 1) + until i == nil + match_indention( buffer:line_from_position(orig_pos), count ) + else + buffer:goto_pos(orig_pos) + end + buffer:end_undo_action() + + next_snippet_item() +end + +--- +-- [Local function] Mirror or transform most recently modified field in the +-- current snippet and move on to the next field. +next_snippet_item = function() + if not snippet.index then return end + local buffer = buffer + local s_start, s_end, s_text = snippet_text() + + -- If something went wrong and the snippet has been 'messed' up + -- (e.g. by undo/redo commands). + if not s_text then cancel_current() return end + + -- Mirror and transform. + buffer:begin_undo_action() + if snippet.index > 0 then + if snippet.cursor then + buffer:set_sel(snippet.cursor, buffer.current_pos) + else + buffer:word_left_extend() + end + local last_item = buffer:get_sel_text() + _DEBUG('last_item:\n'..last_item) + + buffer:set_sel(s_start, s_end) + s_text = escape(s_text) + _DEBUG('s_text escaped:\n'..s_text) + + -- Regex mirror. + patt = '%${'..snippet.index..'/(.-)/(.-)/([iomxneus]*)}' + s_text = s_text:gsub(patt, + function(pattern, replacement, options) + local script = [[ + li = %q(last_item) + rep = %q(replacement) + li =~ /pattern/options + if data = $~ + rep.gsub!(/\#\{(.+?)\}/) do + expr = $1.gsub(/\$(\d\d?)/, 'data[\1]') + eval expr + end + puts rep.gsub(/\$(\d\d?)/) { data[$1.to_i] } + end + ]] + pattern = unescape(pattern) + replacement = unescape(replacement) + script = script:gsub('last_item', last_item) + script = script:gsub('pattern', pattern) + script = script:gsub('options', options) + script = script:gsub('replacement', replacement) + _DEBUG('script:\n'..script) + + local out = io.popen("ruby 2>&1 <<'_EOF'\n".. + script..'\n_EOF'):read('*all') + _DEBUG('regex out:\n'..out) + if out:sub(-1) == '\n' then out = out:sub(1, -2) end -- chomp + return out + end) + _DEBUG('patterns replaced:\n'..s_text) + + -- Plain text mirror. + local mirror = '%${'..snippet.index..'}' + s_text = s_text:gsub(mirror, last_item) + _DEBUG('mirrors replaced:\n'..s_text) + else + s_text = escape(s_text) + _DEBUG('s_text escaped:\n'..s_text) + end + buffer:end_undo_action() + + buffer:set_sel(s_start, s_end) + + -- Find next snippet item or finish. + buffer:begin_undo_action() + snippet.index = snippet.index + 1 + if snippet[snippet.index] then + _DEBUG('next index: '..snippet.index) + local s = s_text:find('${'..snippet.index..':') + local next_item = s_text:match('($%b{})', s) + s_text = unescape(s_text) + _DEBUG('s_text unescaped:\n'..s_text) + buffer:replace_sel(s_text) + if s and next_item then + next_item = unescape(next_item) + _DEBUG('next_item:\n'..next_item) + local s, e = buffer:find(next_item, 0, s_start) + if s and e then + buffer:set_sel(s, e) + snippet.cursor = s + local patt = '^%${'..snippet.index..':(.*)}$' + local default = next_item:match(patt) + buffer:replace_sel(default) + buffer:set_sel(s, s + #default) + else + _DEBUG('search failed:\n'..next_item) + next_snippet_item() + end + else + _DEBUG('no item for '..snippet.index) + next_snippet_item() + end + else -- finished + _DEBUG('snippet finishing...') + s_text = s_text:gsub('${0}', '$CURSOR', 1) + s_text = unescape(s_text) + _DEBUG('s_text unescaped:\n'..s_text) + s_text = remove_escapes(s_text) + _DEBUG('s_text escapes removed:\n'..s_text) + buffer:replace_sel(s_text) + local _, s_end = snippet_text() + if s_end then + -- Compensate for extra char in CR+LF line endings. + if buffer.eol_mode == 0 then s_end = s_end - 1 end + buffer:goto_pos(s_end) + join_lines() + end + + local s, e = buffer:find('$CURSOR', 4, s_start) + if s and e then + buffer:set_sel(s, e) + buffer:replace_sel('') + else + buffer:goto_pos(s_end) -- at snippet end marker + end + buffer:marker_delete_handle(snippet.end_marker) + snippet = {} + + -- Restore previous running snippet (if any). + if #snippet_stack > 0 then snippet = table.remove(snippet_stack) end + end + buffer:end_undo_action() +end + +--- +-- Cancels active snippet, reverting to the state before the snippet was +-- activated. +function cancel_current() + if not snippet.index then return end + local buffer = buffer + local s_start, s_end = snippet_text() + if s_start and s_end then + buffer:set_sel(s_start, s_end) + buffer:replace_sel('') join_lines() + end + if snippet.sel_text then + buffer:add_text(snippet.sel_text) + buffer.anchor = buffer.anchor - #snippet.sel_text + end + buffer:marker_delete_handle(snippet.end_marker) + snippet = {} + + -- Restore previous running snippet (if any). + if #snippet_stack > 0 then snippet = table.remove(snippet_stack) end +end + +--- +-- List available snippet triggers as an autocompletion list. +-- Global snippets and snippets in the current lexer and scope are used. +function list() + local buffer = buffer + local list, list_str = {}, '' + + local function add_snippets(snippets) + for s_name in pairs(snippets) do table.insert(list, s_name) end + end + + local snippets = _G.snippets + add_snippets(snippets) + if SCOPES_ENABLED then + local lexer = buffer:get_lexer_language() + local style = buffer.style_at[buffer.current_pos] + local scope = buffer:get_style_name(style) + if snippets[lexer] and type( snippets[lexer] ) == 'table' then + add_snippets( snippets[lexer] ) + if snippets[lexer][scope] then add_snippets( snippets[lexer][scope] ) end + end + end + + table.sort(list) + local sep = string.char(buffer.auto_c_separator) + for _, v in pairs(list) do list_str = list_str..v..sep end + list_str = list_str:sub(1, -2) -- chop + buffer:auto_c_show(0, list_str) +end + +--- +-- Show the scope/style at the current caret position as a calltip. +function show_scope() + if not SCOPES_ENABLED then print('Scopes disabled') return end + local buffer = buffer + local lexer = buffer:get_lexer_language() + local scope = buffer.style_at[buffer.current_pos] + local text = 'Lexer: '..lexer..'\nScope: '.. + buffer:get_style_name(scope)..' ('..scope..')' + buffer:call_tip_show(buffer.current_pos, text) +end + +--- +-- [Local function] Gets the text of the snippet. +-- This is the text bounded by the start of the trigger word to the end snippet +-- marker on the line after the snippet's end. +snippet_text = function() + 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 + +--- +-- [Local function] Replace escaped snippet characters with their octal +-- equivalents. +escape = function(text) + return text:gsub('\\([$/}`])', + function(char) return ("\\%03d"):format( char:byte() ) end) +end + +--- +-- [Local function] Replace octal snippet characters with their escaped +-- equivalents. +unescape = function(text) + return text:gsub('\\(%d%d%d)', + function(value) return '\\'..string.char(value) end) +end + +--- +-- [Local function] Remove escaping forward-slashes from escaped snippet +-- characters. +-- At this point, they are no longer necessary. +remove_escapes = function(text) return text:gsub('\\([$/}`])', '%1') end + +--- +-- [Local function] When snippets are inserted, match their indentation level +-- with their surroundings. +match_indention = function(ref_line, num_lines) + if num_lines == 0 then return end + local buffer = buffer + local isize = buffer.indent + local ibase = buffer.line_indentation[ref_line] + local inum = ibase / isize -- num of indents needed to match + local line = ref_line + 1 + for i = 0, num_lines - 1 do + local linei = buffer.line_indentation[line + i] + buffer.line_indentation[line + i] = linei + isize * inum + end +end + +--- +-- [Local function] Joins current line with the line below it, eliminating +-- whitespace. +-- This is used to remove the empty line containing the end of snippet marker. +join_lines = function() + local buffer = buffer + buffer:line_down() buffer:vc_home() + if buffer.column[buffer.current_pos] == 0 then buffer:vc_home() end + buffer:home_extend() + if #buffer:get_sel_text() > 0 then buffer:delete_back() end + buffer:delete_back() +end + +--- +-- [Local function] Called for printing debug text if DEBUG flag +-- is set. +-- @param text Debug text to print. +_DEBUG = function(text) if DEBUG then print('---\n'..text) end end + +-- run tests +if RUN_TESTS then + function next_item() next_snippet_item() end + if not package.path:find(_HOME) then + package.path = package.path..';'.._HOME..'/scripts/' + end + require 'utils/test_snippets' +end -- cgit v1.2.3