diff options
author | mitchell <70453897+667e-11@users.noreply.github.com> | 2007-08-06 05:04:35 -0400 |
---|---|---|
committer | mitchell <70453897+667e-11@users.noreply.github.com> | 2007-08-06 05:04:35 -0400 |
commit | 58dc16406976d8c18191470a6bafaeda793ed523 (patch) | |
tree | e0ebc0d98baf143ae90fb63fe347aefeaa110d96 | |
parent | a20651bf0b5e9fb00ac8e7b6f5608f332d15cab6 (diff) |
Initial import of the textadept module.
-rw-r--r-- | modules/textadept/editing.lua | 622 | ||||
-rw-r--r-- | modules/textadept/init.lua | 12 | ||||
-rw-r--r-- | modules/textadept/key_commands.lua | 187 | ||||
-rw-r--r-- | modules/textadept/keys.lua | 183 | ||||
-rw-r--r-- | modules/textadept/mlines.lua | 134 | ||||
-rw-r--r-- | modules/textadept/snippets.lua | 438 |
6 files changed, 1576 insertions, 0 deletions
diff --git a/modules/textadept/editing.lua b/modules/textadept/editing.lua new file mode 100644 index 00000000..b9a59a58 --- /dev/null +++ b/modules/textadept/editing.lua @@ -0,0 +1,622 @@ +-- Copyright 2007 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +--- +-- Editing commands for the textadept module. +module('modules.textadept.editing', package.seeall) + +--- +-- [Local table] The kill-ring. +-- @class table +-- @name kill_ring +-- @field maxn The maximum size of the kill-ring. +local kill_ring = { pos = 1, maxn = 10 } + +--- +-- [Local table] Character matching. +-- Used for auto-matching parentheses, brackets, braces, and quotes. +-- @class table +-- @name char_matches +local char_matches = { + ['('] = ')', ['['] = ']', ['{'] = '}', + ["'"] = "'", ['"'] = '"' +} + +--- +-- [Local table] Brace characters. +-- Used for going to matching brace positions. +-- @class table +-- @name braces +local braces = { -- () [] {} <> + [40] = 1, [91] = 1, [123] = 1, [60] = 1, + [41] = 1, [93] = 1, [125] = 1, [62] = 1, +} + +--- +-- [Local table] The current call tip. +-- Used for displaying call tips. +-- @class table +-- @name current_call_tip +local current_call_tip = {} + +--- +-- [Local table] Enclosures for enclosing or selecting ranges of text. +-- Note chars and tag enclosures are generated at runtime. +-- @class table +-- @name enclosure +local enclosure = { + dbl_quotes = { left = '"', right = '"' }, + sng_quotes = { left = "'", right = "'" }, + parens = { left = '(', right = ')' }, + brackets = { left = '[', right = ']' }, + braces = { left = '{', right = '}' }, + chars = { left = ' ', right = ' ' }, + tags = { left = '>', right = '<' }, + tag = { left = ' ', right = ' ' }, + single_tag = { left = '<', right = ' />' } +} + +textadept.handlers.add_function_to_handler('char_added', + function(c) -- matches characters specified in char_matches + if char_matches[c] then + buffer:insert_text( -1, char_matches[c] ) + end + end) + +-- local functions +local insert_into_kill_ring, scroll_kill_ring +local get_preceding_number, get_sel_or_line + +--- +-- Goes to a matching brace position, selecting the text inside if specified. +-- @param select If true, selects the text between matching braces. +function match_brace(select) + local buffer = buffer + local caret = buffer.current_pos + local match_pos = buffer:brace_match(caret) + if match_pos ~= -1 then + if select then + if match_pos > caret then + buffer:set_sel(caret, match_pos + 1) + else + buffer:set_sel(caret + 1, match_pos) + end + else + buffer:goto_pos(match_pos) + end + end +end + +--- +-- Pops up an autocompletion list for the current word based on other words in +-- the document. +-- @param word_chars String of chars considered to be part of words. +function autocomplete_word(word_chars) + local buffer = buffer + local caret, length = buffer.current_pos, buffer.length + local completions, c_list_str = {}, '' + local buffer_text = buffer:get_text(length) + local root = buffer_text:sub(1, caret):match('['..word_chars..']+$') + if not root or #root == 0 then return end + local match_pos = buffer:find(root, 1048580) -- word start and match case + while match_pos do + local s, e = buffer_text:find('^['..word_chars..']+', match_pos + 1) + local match = buffer_text:sub(s, e) + if not completions[match] and #match > #root then + c_list_str = c_list_str..match..' ' + completions[match] = true + end + match_pos = buffer:find(root, 1048580, match_pos + 1) + end + if #c_list_str > 0 then buffer:auto_c_show(#root, c_list_str:sub(1, -2)) end +end + +--- +-- Displays a call tip based on the word to the left of the cursor and a given +-- API table. +-- @param api Table of functions call tips can be displayed for. Each key is a +-- function name, and each value is a table of tables. Each of those tables +-- represents a function. It has 2 indexes: parameters and a description. +-- This enables call tips for 'overloaded' functions. Even if there is just +-- one function, it must be enclosed in a table. +-- @param start Boolean indicating whether to start a call tip or not. If the +-- user clicks an arrow, you should call show_call_tip again with this value +-- being false to display the next function. +function show_call_tip(api, start) + local buffer = buffer + local funcs + local call_tip = '' + if start then + local s = buffer:word_start_position(buffer.current_pos - 1, true) + local word = buffer:text_range(s, buffer.current_pos) + funcs = api[word] + if not funcs then return end + if #funcs > 1 then call_tip = call_tip..'\001' end + current_call_tip = { + name = word, + num = 1, + max = #funcs, + start_pos = buffer.current_pos, + ['api'] = api + } + elseif buffer:call_tip_active() and current_call_tip.max > 1 then + call_tip = call_tip..'\001' + funcs = api[current_call_tip.name] + else + return + end + local func = funcs[current_call_tip.num] + local name = current_call_tip.name + local params = func[1] + local desc = #funcs == 1 and func[2] or '\002'..func[2] + call_tip = call_tip..name..params..'\n'..desc:gsub('\\n', '\n') + buffer:call_tip_show(current_call_tip.start_pos, call_tip) +end + +textadept.handlers.add_function_to_handler('call_tip_click', + function(position) -- display the next or previous call tip + if not buffer:call_tip_active() then return end + if position == 1 and current_call_tip.num > 1 then + current_call_tip.num = current_call_tip.num - 1 + show_call_tip(current_call_tip.api, false) + elseif position == 2 and current_call_tip.num < current_call_tip.max then + current_call_tip.num = current_call_tip.num + 1 + show_call_tip(current_call_tip.api, false) + end + end) + +--- +-- Comment or uncomments out blocks of code with a given comment string. +-- @param comment The comment string inserted or removed from the beginning of +-- each line in the selection. +function block_comment(comment) + local buffer = buffer + local caret, anchor = buffer.current_pos, buffer.anchor + if caret < anchor then anchor, caret = caret, anchor end + local s = buffer:line_from_position(anchor) + local e = buffer:line_from_position(caret) + local mlines = s ~= e + if mlines and caret == buffer:position_from_line(e) then e = e - 1 end + buffer:begin_undo_action() + for line = s, e do + local pos = buffer:position_from_line(line) + if buffer:text_range(pos, pos + #comment) == comment then + buffer:set_sel(pos, pos + #comment) + buffer:replace_sel('') + caret = caret - #comment + else + buffer:insert_text(pos, comment) + caret = caret + #comment + end + end + buffer:end_undo_action() + if mlines then buffer:set_sel(anchor, caret) else buffer:goto_pos(caret) end +end + +--- +-- Goes to the requested line. +-- @param line Optional line number to go to. +function goto_line(line) + local buffer = buffer + if not line then + line = io.popen('zenity --entry --title "Go To" '.. + '--text "Line Number:"'):read('*all') + if line == '' then return end + line = tonumber(line) + end + buffer:ensure_visible_enforce_policy(line - 1) + buffer:goto_line(line - 1) +end + +--- +-- Prepares the buffer for saving to a file. +-- Strips trailing whitespace off of every line, ensures an ending newline, and +-- converts non-consistent EOLs. +function prepare_for_save() + local buffer = buffer + buffer:begin_undo_action() + -- Strip trailing whitespace. + local lines = buffer.line_count + for line = 0, lines - 1 do + local s = buffer:position_from_line(line) + local e = buffer.line_end_position[line] + local i = e - 1 + local c = buffer.char_at[i] + while i >= s and c == 9 or c == 32 do + i = i - 1 + c = buffer.char_at[i] + end + if i < e - 1 then + buffer.target_start, buffer.target_end = i + 1, e + buffer:replace_target() + end + end + -- Ensure ending newline. + local e = buffer:position_from_line(lines) + if lines == 1 or + lines > 1 and e > buffer:position_from_line(lines - 1) then + buffer:insert_text(e, '\n') + end + -- Convert non-consistent EOLs + buffer:convert_eo_ls(buffer.eol_mode) + buffer:end_undo_action() +end + +--- +-- Cuts or copies text ranges intelligently. (Behaves like Emacs.) +-- If no text is selected, all text from the cursor to the end of the line is +-- cut or copied as indicated by action and pushed onto the kill-ring. If there +-- is text selected, it is cut or copied and pushed onto the kill-ring. +-- @param copy If false, the text is cut. Otherwise it is copied. +-- @see insert_into_kill_ring +function smart_cutcopy(copy) + local buffer = buffer + local txt = buffer:get_sel_text() + if #txt == 0 then buffer:line_end_extend() end + txt = buffer:get_sel_text() + insert_into_kill_ring(txt) + kill_ring.pos = 1 + if copy then buffer:copy() else buffer:cut() end +end + +--- +-- Retrieves the top item off the kill-ring and pastes it. +-- If an action is specified, the text is kept selected for scrolling through +-- the kill-ring. +-- @param action If given, specifies whether to cycle through the kill-ring in +-- normal or reverse order. A value of 'cycle' cycles through normally, +-- 'reverse' in reverse. +-- @see scroll_kill_ring +function smart_paste(action) + local buffer = buffer + local anchor, caret = buffer.anchor, buffer.current_pos + if caret < anchor then anchor = caret end + local txt = buffer:get_sel_text() + if txt == kill_ring[kill_ring.pos] then scroll_kill_ring(action) end + + -- If text was copied to the clipboard from other apps, insert it into the + -- kill-ring so it can be pasted (thanks to Nathan Robinson). + local clip_txt, found = textadept.clipboard_text, false + if clip_txt ~= '' then + for _, ring_txt in ipairs(kill_ring) do + if clip_txt == ring_txt then found = true break end + end + end + if not found then insert_into_kill_ring(clip_txt) end + + txt = kill_ring[kill_ring.pos] + if txt then + buffer:replace_sel(txt) + if action then buffer.anchor = anchor end -- cycle + end +end + +--- +-- Selects the current word under the caret and if action indicates, delete it. +-- @param action Optional action to perform with selected word. If 'delete', it +-- is deleted. +function current_word(action) + local buffer = buffer + local s = buffer:word_start_position(buffer.current_pos) + local e = buffer:word_end_position(buffer.current_pos) + buffer:set_sel(s, e) + if action == 'delete' then buffer:delete_back() end +end + +--- +-- Transposes characters intelligently. +-- If the carat is at the end of the current word, the two characters before +-- the caret are transposed. Otherwise the characters to the left and right of +-- the caret are transposed. +function transpose_chars() + local buffer = buffer + buffer:begin_undo_action() + local caret = buffer.current_pos + local char = buffer.char_at[caret - 1] + buffer:delete_back() + if caret > buffer.length or buffer.char_at[caret - 1] == 32 then + buffer:char_left() + else + buffer:char_right() + end + buffer:insert_text( -1, string.char(char) ) + editor:end_undo_action() + buffer:goto_pos(caret) +end + +--- +-- Reduces multiple characters occurances to just one. +-- If char is not given, the character to be squeezed is the one under the +-- caret. +-- @param char The character (integer) to be used for squeezing. +function squeeze(char) + local buffer = buffer + if not char then char = buffer.char_at[buffer.current_pos - 1] end + local s, e = buffer.current_pos - 1, buffer.current_pos - 1 + while buffer.char_at[s] == char do s = s - 1 end + while buffer.char_at[e] == char do e = e + 1 end + buffer:set_sel(s + 1, e) + buffer:replace_sel( string.char(char) ) +end + +--- +-- Joins the current line with the line below, eliminating whitespace. +function join_lines() + local buffer = buffer + buffer:begin_undo_action() + buffer:line_end() buffer:clear() buffer:add_text(' ') squeeze() + buffer:end_undo_action() +end + +--- +-- Moves the current line in the specified direction up or down. +-- @param direction 'up' moves the current line up, 'down' moves it down. +function move_line(direction) + local buffer = buffer + local column = buffer.column[buffer.current_pos] + buffer:begin_undo_action() + if direction == 'up' then + buffer:line_transpose() + buffer:line_up() + elseif direction == 'down' then + buffer:line_down() + buffer:line_transpose() + column = buffer.current_pos + column -- starts at line home + buffer:goto_pos(column) + end + buffer:end_undo_action() +end + +--- +-- Encloses text in an enclosure set. +-- If text is selected, it is enclosed. Otherwise, the previous word is +-- enclosed. The n previous words can be enclosed by appending n (a number) to +-- the end of the last word. When enclosing with a character, append the +-- character to the end of the word(s). To enclose previous word(s) with n +-- characters, append n (a number) to the end of character set. +-- Examples: +-- enclose this2 -> 'enclose this' (enclose in sng_quotes) +-- enclose this2**2 -> **enclose this** +-- @param str The enclosure type in enclosure. +-- @see enclosure +-- @see get_preceding_number +function enclose(str) + local buffer = buffer + buffer:begin_undo_action() + local txt = buffer:get_sel_text() + if txt == '' then + if str == 'chars' then + local num_chars, len_num_chars = get_preceding_number() + for i = 1, len_num_chars do buffer:delete_back() end + for i = 1, num_chars do buffer:char_left_extend() end + enclosure[str].left = buffer:get_sel_text() + enclosure[str].right = enclosure[str].left + buffer:delete_back() + end + local num_words, len_num_chars = get_preceding_number() + for i = 1, len_num_chars do buffer:delete_back() end + for i = 1, num_words do buffer:word_left_extend() end + txt = buffer:get_sel_text() + end + local len = 0 + if str == 'tag' then + enclosure[str].left = '<'..txt..'>' + enclosure[str].right = '</'..txt..'>' + len = #txt + 3 + txt = '' + end + local left = enclosure[str].left + local right = enclosure[str].right + buffer:replace_sel(left..txt..right) + if str == 'tag' then buffer:goto_pos(buffer.current_pos - len) end + buffer:end_undo_action() +end + +--- +-- Selects text in a specified enclosure. +-- @param str The enclosure type in enclosure. If str is not specified, +-- matching character pairs defined in char_matches are searched for from the +-- caret outwards. +-- @see enclosure +-- @see char_matches +function select_enclosed(str) + local buffer = buffer + if str then + buffer:search_anchor() + local s = buffer:search_prev( 0, enclosure[str].left ) + local e = buffer:search_next( 0, enclosure[str].right ) + if s and e then buffer:set_sel(s + 1, e) end + else + -- TODO: ignore enclosures in comment scopes? + s, e = buffer.anchor, buffer.current_pos + if s > e then s, e = e, s end + local char = string.char( buffer.char_at[s - 1] ) + if s ~= e and char_matches[char] then + s, e = s - 2, e + 1 -- don't match the same enclosure + end + while s >= 0 do + char = string.char( buffer.char_at[s] ) + if char_matches[char] then + local _, e = buffer:find( char_matches[char], 0, e ) + if e then buffer:set_sel(s + 1, e - 1) break end + end + s = s - 1 + end + end +end + +--- +-- Grows the selection by a character amount on either end. +-- @param amount The amount to grow the selection on either end. +function grow_selection(amount) + local buffer = buffer + local anchor, caret = buffer.anchor, buffer.current_pos + if anchor < caret then + buffer:set_sel(anchor - amount, caret + amount) + else + buffer:set_sel(anchor + amount, caret - amount) + end +end + +--- +-- Selects the current line. +function select_line() + local buffer = buffer + buffer:home() buffer:line_end_extend() +end + +--- +-- Selects the current paragraph. +-- Paragraphs are delimited by two or more consecutive newlines. +function select_paragraph() + local buffer = buffer + buffer:para_up() + buffer:para_down_extend() +end + +--- +-- Selects indented blocks intelligently. +-- If no block of text is selected, all text with the current level of +-- indentation is selected. If a block of text is selected and the lines to the +-- top and bottom of it are one indentation level lower, they are added to the +-- selection. In all other cases, the behavior is the same as if no text is +-- selected. +function select_indented_block() + local buffer = buffer + local s = buffer:line_from_position(buffer.anchor) + local e = buffer:line_from_position(buffer.current_pos) + if s > e then s, e = e, s end + local indent = buffer.line_indentation[s] - buffer.indent + if indent < 0 then return end + if buffer:get_sel_text() ~= '' then + if buffer.line_indentation[s - 1] == indent and + buffer.line_indentation[e + 1] == indent then + s, e = s - 1, e + 1 + indent = indent + buffer.indent -- don't run while loops + end + end + while buffer.line_indentation[s - 1] > indent do s = s - 1 end + while buffer.line_indentation[e + 1] > indent do e = e + 1 end + s = buffer:position_from_line(s) + e = buffer.line_end_extend[e] + buffer:set_sel(s, e) +end + +--- +-- Selects all text with the same scope/style as under the caret. +function select_scope() + local buffer = buffer + local start_pos = buffer.current_pos + local base_style = buffer.style_at[start_pos] + local pos = start_pos - 1 + while buffer.style_at[pos] == base_style do pos = pos - 1 end + local start_style = pos + pos = start_pos + 1 + while buffer.style_at[pos] == base_style do pos = pos + 1 end + buffer:set_sel(start_style + 1, pos) +end + +--- +-- Executes the selection or contents of the current line as Ruby code, +-- replacing the text with the output. +function ruby_exec() + local buffer = buffer + local txt = get_sel_or_line() + local out = io.popen("ruby 2>&1 <<'_EOF'\n"..txt..'\n_EOF'):read('*all') + if out:sub(-1) == '\n' then out = out:sub(1, -2) end -- chomp + buffer:replace_sel(out) +end + +--- +-- Executes the selection or contents of the current line as Lua code, +-- replacing the text with the output. +function lua_exec() + local buffer = buffer + local txt = get_sel_or_line() + loadstring(txt) + buffer:goto_pos(buffer.current_pos) +end + +--- +-- Converts indentation between tabs and spaces. +function convert_indentation() + local buffer = buffer + buffer:begin_undo_action() + for line = 0, buffer.line_count do + local s = buffer:position_from_line(line) + local indent = buffer.line_indentation[line] + local indent_pos = buffer.line_indent_position[line] + current_indentation = buffer:text_range(s, indent_pos) + if buffer.use_tabs then + new_indentation = ('\t'):rep(indent / buffer.tab_width) + else + new_indentation = (' '):rep(indent) + end + if current_indentation ~= new_indentation then + buffer.target_start = s + buffer.target_end = indent_pos + buffer:replace_target(new_indentation) + end + end + buffer:end_undo_action() +end + +--- +-- Reformats the selected text or current paragraph using the 'fmt' command. +function reformat_paragraph() + local buffer = buffer + if buffer:get_sel_text() == '' then select_paragraph() end + local txt = buffer:get_sel_text() + local out = io.popen("fmt -c -w 80 <<'_EOF'\n"..txt..'\n_EOF'):read('*all') + if txt:sub(-1) ~= '\n' and out:sub(-1) == '\n' then out = out:sub(1, -2) end + buffer:replace_sel(out) +end + +--- +-- [Local function] Inserts text into kill_ring. +-- If it grows larger than maxn, the oldest inserted text is replaced. +-- @see smart_cutcopy +insert_into_kill_ring = function(txt) + table.insert(kill_ring, 1, txt) + local maxn = kill_ring.maxn + if #kill_ring > maxn then kill_ring[maxn + 1] = nil end +end + +--- +-- [Local function] Scrolls kill_ring in the specified direction. +-- @param direction The direction to scroll: 'forward' (default) or 'reverse'. +-- @see smart_paste +scroll_kill_ring = function(direction) + if direction == 'reverse' then + kill_ring.pos = kill_ring.pos - 1 + if kill_ring.pos < 1 then kill_ring.pos = #kill_ring end + else + kill_ring.pos = kill_ring.pos + 1 + if kill_ring.pos > #kill_ring then kill_ring.pos = 1 end + end +end + +--- +-- [Local function] Returns the number to the left of the caret. +-- This is used for the enclose function. +-- @see enclose +get_preceding_number = function() + local buffer = buffer + local caret = buffer.current_pos + local char = buffer.char_at[caret - 1] + local txt = '' + while tonumber( string.char(char) ) do + txt = txt..string.char(char) + caret = caret - 1 + char = buffer.char_at[caret - 1] + end + return tonumber(txt) or 1, #txt +end + +--- +-- [Local function] Returns the current selection or the contents of the +-- current line. +get_sel_or_line = function() + local buffer = buffer + if buffer:get_sel_text() == '' then select_line() end + return buffer:get_sel_text() +end diff --git a/modules/textadept/init.lua b/modules/textadept/init.lua new file mode 100644 index 00000000..1f882d41 --- /dev/null +++ b/modules/textadept/init.lua @@ -0,0 +1,12 @@ +-- Copyright 2007 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +--- +-- The textadept module. +-- It provides utilities for editing text in Textadept. +module('modules.textadept', package.seeall) + +require 'textadept.editing' +require 'textadept.keys' +require 'textadept.mlines' +require 'textadept.snippets' +require 'textadept.key_commands' -- last diff --git a/modules/textadept/key_commands.lua b/modules/textadept/key_commands.lua new file mode 100644 index 00000000..82672e78 --- /dev/null +++ b/modules/textadept/key_commands.lua @@ -0,0 +1,187 @@ +-- Copyright 2007 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +--- +-- Defines the key commands used by the Textadept key command manager. +module('modules.textadept.key_commands', package.seeall) + +--[[ + C: G Q + A: A C G J K L M O Q R W X Z + CS: C D G J L Q R S T U W + SA: A C D E G H I J K L M O Q R S T W X Z + CA: A C G H J K L M O Q R S T V W X Y Z + CSA: C D G H J K L M O Q R S T U W X Z +]]-- + +--- +-- Global container that holds all key commands. +-- @class table +-- @name keys +_G.keys = {} +local keys = keys + +keys.clear_sequence = 'esc' + +local b, v = 'buffer', 'view' +local textadept = textadept + +keys.cf = { 'char_right', b } +keys.csf = { 'char_right_extend', b } +keys.af = { 'word_right', b } +keys.saf = { 'word_right_extend', b } +keys.cb = { 'char_left', b } +keys.csb = { 'char_left_extend', b } +keys.ab = { 'word_left', b } +keys.sab = { 'word_left_extend', b } +keys.cn = { 'line_down', b } +keys.csn = { 'line_down_extend', b } +keys.cp = { 'line_up', b } +keys.csp = { 'line_up_extend', b } +keys.ca = { 'vc_home', b } +keys.csa = { 'home_extend', b } +keys.ce = { 'line_end', b } +keys.cse = { 'line_end_extend', b } +keys.cv = { 'page_down', b } +keys.csv = { 'page_down_extend', b } +keys.av = { 'para_down', b } +keys.sav = { 'para_down_extend', b } +keys.cy = { 'page_up', b } +keys.csy = { 'page_up_extend', b } +keys.ay = { 'para_up', b } +keys.say = { 'para_up_extend', b } +keys.ch = { 'delete_back', b } +keys.ah = { 'del_word_left', b } +keys.cd = { 'clear', b } +keys.ad = { 'del_word_right', b } + +keys.csaf = { 'char_right_rect_extend', b } +keys.csab = { 'char_left_rect_extend', b } +keys.csan = { 'line_down_rect_extend', b } +keys.csap = { 'line_up_rect_extend', b } +keys.csaa = { 'vc_home_rect_extend', b } +keys.csae = { 'line_end_rect_extend', b } +keys.csav = { 'page_down_rect_extend', b } +keys.csay = { 'page_up_rect_extend', b } + +local m_snippets = modules.textadept.snippets +keys.ci = { m_snippets.insert } +keys.csi = { m_snippets.cancel_current } +keys.cai = { m_snippets.list } +keys.ai = { m_snippets.show_scope } + +local m_editing = modules.textadept.editing +keys.cm = { m_editing.match_brace } +keys.csm = { m_editing.match_brace, 'select' } +keys['c '] = { m_editing.autocomplete_word, '%w_' } +keys.cl = { m_editing.goto_line } +keys.ck = { m_editing.smart_cutcopy, } +keys.csk = { m_editing.smart_cutcopy, 'copy' } +keys.cu = { m_editing.smart_paste, } +keys.au = { m_editing.smart_paste, 'cycle' } +keys.sau = { m_editing.smart_paste, 'reverse' } +keys.cw = { m_editing.current_word, 'delete' } +keys.at = { m_editing.transpose_chars } +keys.csh = { m_editing.squeeze, } +keys.cj = { m_editing.join_lines } +keys.cau = { m_editing.move_line, 'up' } +keys.cad = { m_editing.move_line, 'down' } +keys.csai = { m_editing.convert_indentation } +keys.cae = { -- code execution + r = { m_editing.ruby_exec }, + l = { m_editing.lua_exec } +} +keys.ae = { -- enclose in... + t = { m_editing.enclose, 'tag' }, + st = { m_editing.enclose, 'single_tag' }, + ['s"'] = { m_editing.enclose, 'dbl_quotes' }, + ["'"] = { m_editing.enclose, 'sng_quotes' }, + ['('] = { m_editing.enclose, 'parens' }, + ['['] = { m_editing.enclose, 'brackets' }, + ['{'] = { m_editing.enclose, 'braces' }, + c = { m_editing.enclose, 'chars' }, +} +keys.as = { -- select in... + e = { m_editing.select_enclosed }, + t = { m_editing.select_enclosed, 'tags' }, + ['s"'] = { m_editing.select_enclosed, 'dbl_quotes' }, + ["'"] = { m_editing.select_enclosed, 'sng_quotes' }, + ['('] = { m_editing.select_enclosed, 'parens' }, + ['['] = { m_editing.select_enclosed, 'brackets' }, + ['{'] = { m_editing.select_enclosed, 'braces' }, + w = { m_editing.current_word, 'select' }, + l = { m_editing.select_line }, + p = { m_editing.select_paragraph }, + i = { m_editing.select_indented_block }, + s = { m_editing.select_scope }, + g = { m_editing.grow_selection, 1 }, +} + +local m_mlines = modules.textadept.mlines +keys.am = { + a = { m_mlines.add }, + sa = { m_mlines.add_multiple }, + r = { m_mlines.remove }, + sr = { m_mlines.remove_multiple }, + u = { m_mlines.update }, + c = { m_mlines.clear }, +} + +keys.cr = { textadept.io.open } +keys.co = { 'save', b } +keys.cso = { 'save_as', b } +keys.cx = { 'close', b } +keys.csx = { textadept.io.close_all } +keys.cz = { 'undo', b } +keys.csz = { 'redo', b } +keys.as.a = { 'select_all', b } +keys.an = { 'goto_buffer', v, 1, false } +keys.ap = { 'goto_buffer', v, -1, false } +keys.can = { textadept.goto_view, 1, false } +keys.cap = { textadept.goto_view, -1, false } + +local m_handlers = textadept.handlers +keys.cab = { m_handlers.handle, 'call_tip_click', 1 } +keys.caf = { m_handlers.handle, 'call_tip_click', 2 } + +keys.cs = { textadept.find.focus } +keys['c\t'] = { textadept.pm.focus } +keys.cc = { textadept.focus_command } + +keys.san = { function() view.size = view.size + 10 end } +keys.sap = { function() view.size = view.size - 10 end } + +local function pm_activate(text) + textadept.pm.entry_text = text + textadept.pm.activate() +end + +keys.ct = {} -- Textadept command chain +keys.ct.s = { 'split', v, false } -- horizontal +keys.ct.ss = { 'split', v } -- vertical +keys.ct.n = { textadept.new_buffer } +keys.ct.b = { pm_activate, 'buffers' } +keys.ct.c = { pm_activate, 'ctags' } +keys.ct.x = { function() view:unsplit() return true end } +keys.ct.sx = { function() while view:unsplit() do end end } +keys.ct.f = { function() + local buffer = buffer + buffer:toggle_fold( buffer:line_from_position(buffer.current_pos) ) +end } + +local function toggle_setting(setting) + local state = buffer[setting] + if type(state) == 'boolean' then + buffer[setting] = not state + elseif type(state) == 'number' then + buffer[setting] = buffer[setting] == 0 and 1 or 0 + end + textadept.handlers.update_ui() -- for updating statusbar +end + +keys.ct.v = {} -- view chain +keys.ct.v.e = { toggle_setting, 'view_eol' } +keys.ct.v.w = { toggle_setting, 'wrap_mode' } +keys.ct.v.i = { toggle_setting, 'indentation_guides' } +keys.ct.v.t = { toggle_setting, 'use_tabs' } +keys.ct.v[' '] = { toggle_setting, 'view_ws' } +keys.ct.v.p = { textadept.pm.toggle_visible } diff --git a/modules/textadept/keys.lua b/modules/textadept/keys.lua new file mode 100644 index 00000000..5ef0f8be --- /dev/null +++ b/modules/textadept/keys.lua @@ -0,0 +1,183 @@ +-- Copyright 2007 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +--- +-- Manages key commands in Textadept. +-- Default key commands should be defined in a separate file and loaded after +-- all modules. +-- There are several option variables used: +-- SCOPES_ENABLED: Flag indicating whether scopes/styles can be used for key +-- commands. +-- CTRL: The string representing the Control key. +-- SHIFT: The string representing the Shift key. +-- ALT: The string representing the Alt key. +-- ADD: The string representing used to join together a sequence of Control, +-- Shift, or Alt modifier keys. +module('modules.textadept.keys', package.seeall) + +-- options +local SCOPES_ENABLED = true +local CTRL, SHIFT, ALT, ADD = 'c', 's', 'a', '' +-- end options + +--- +-- [Local table] Lookup table for key values higher than 255. +-- If a key value given to OnKey is higher than 255, this table is used to +-- return a string representation of the key if it exists. +-- @class table +-- @name KEYSYMS +local KEYSYMS = { -- from <gdk/gdkkeysyms.h> + [65288] = '\b', + [65289] = '\t', + [65293] = '\n', + [65307] = 'esc', + [65535] = 'del', + [65360] = 'home', + [65361] = 'left', + [65362] = 'up', + [65363] = 'right', + [65364] = 'down', + [65365] = 'pup', + [65366] = 'pdown', + [65367] = 'end', + [65379] = 'ins', + [65470] = 'f1', [65471] = 'f2', [65472] = 'f3', [65473] = 'f4', + [65474] = 'f5', [65475] = 'f6', [65476] = 'f7', [65477] = 'f8', + [65478] = 'f9', [65479] = 'f10', [65480] = 'f11', [65481] = 'f12', +} + +--- [Local table] The current key sequence. +-- @class table +-- @name keychain +local keychain = {} + +-- local functions +local try_get_cmd1, try_get_cmd2, try_get_cmd3, try_get_cmd + +--- +-- Clears the current key sequence. +function clear_key_sequence() + keychain = {} + textadept.statusbar_text = '' +end + +--- +-- Handles Textadept keypresses. +-- It is called every time a key is pressed and determines which commands to +-- execute or which new key in a chain to enter based on the current key +-- sequence, lexer, and scope. +-- @return keypress returns what the commands it executes return. If nothing is +-- returned, keypress returns true by default. A true return value will tell +-- Textadept not to handle the key afterwords. +function keypress(code, shift, control, alt) + local buffer, textadept = buffer, textadept + local keys = _G.keys + local key_seq = '' + if control then key_seq = key_seq..CTRL..ADD end + if shift then key_seq = key_seq..SHIFT..ADD end + if alt then key_seq = key_seq..ALT..ADD end + --print(code, string.char(code)) + if code < 256 then + key_seq = key_seq..string.char(code):lower() + else + if not KEYSYMS[code] then return end + key_seq = key_seq..KEYSYMS[code] + end + + if key_seq == keys.clear_sequence and #keychain > 0 then + clear_key_sequence() + return true + end + + local lexer = buffer:get_lexer_language() + local style = buffer.style_at[buffer.current_pos] + local scope = buffer:get_style_name(style) + --print(key_seq, 'Lexer: '..lexer, 'Scope: '..scope) + + keychain[#keychain + 1] = key_seq + local ret, func, args + if SCOPES_ENABLED then + ret, func, args = pcall(try_get_cmd1, keys, key_seq, lexer, scope) + end + if not ret and func ~= -1 then + ret, func, args = pcall(try_get_cmd2, keys, key_seq, lexer) + end + if not ret and func ~= -1 then + ret, func, args = pcall(try_get_cmd3, keys, key_seq) + end + + if ret then + clear_key_sequence() + if type(func) == 'function' then + local ret, retval = pcall( func, unpack(args) ) + if ret then + if type(retval) == 'boolean' then return retval end + else textadept.handlers.error(retval) end -- error + end + return true + else + -- Clear key sequence because it's not part of a chain. + -- (try_get_cmd throws error number -1.) + if func ~= -1 then + local size = #keychain - 1 + clear_key_sequence() + if size > 0 then -- previously in a chain + textadept.statusbar_text = 'Invalid Sequence' + return true + end + else return true end + end +end +textadept.handlers.add_function_to_handler('keypress', keypress, 1) + +-- Note the following functions are called inside pcall so error handling or +-- checking if keys exist etc. is not necessary. + +--- +-- [Local function] Tries to get a key command based on the lexer and current +-- scope. +try_get_cmd1 = function(keys, key_seq, lexer, scope) + return try_get_cmd( keys[lexer][scope] ) +end + +--- +-- [Local function] Tries to get a key command based on the lexer. +try_get_cmd2 = function(keys, key_seq, lexer) + return try_get_cmd( keys[lexer] ) +end + +--- +-- [Local function] Tries to get a global key command. +try_get_cmd3 = function(keys, key_seq) + return try_get_cmd(keys) +end + +--- +-- [Local function] Helper function to get commands with the current keychain. +-- If the current item in the keychain is part of a chain, throw an error value +-- of -1. This way, pcall will return false and -1, where the -1 can easily and +-- efficiently be checked rather than using a string error message. +try_get_cmd = function(active_table) + local str_seq = '' + for _, key_seq in ipairs(keychain) do + str_seq = str_seq..key_seq..' ' + active_table = active_table[key_seq] + end + if #active_table == 0 and next(active_table) then + textadept.statusbar_text = 'Keychain: '..str_seq + error(-1, 0) + else + local func = active_table[1] + if type(func) == 'function' then + return func, { unpack(active_table, 2) } + elseif type(func) == 'string' then + local object = active_table[2] + if object == 'buffer' then + return buffer[func], { buffer, unpack(active_table, 3) } + elseif object == 'view' then + return view[func], { view, unpack(active_table, 3) } + end + else + error( 'Unknown command: '..tostring(func) ) + end + end +end diff --git a/modules/textadept/mlines.lua b/modules/textadept/mlines.lua new file mode 100644 index 00000000..fe1f57ce --- /dev/null +++ b/modules/textadept/mlines.lua @@ -0,0 +1,134 @@ +-- Copyright 2007 Mitchell mitchell<att>caladbolg.net. See LICENSE. + +--- +-- Multiple line editing for the textadept module. +-- There are several option variables used: +-- MARK_MLINE: The integer mark used to identify an MLine marked line. +-- MARK_MLINE_COLOR: The Scintilla color used for an MLine marked line. +module('modules.textadept.mlines', package.seeall) + +-- options +local MARK_MLINE = 2 +local MARK_MLINE_COLOR = 0x4D994D +-- end options + +--- +-- [Local table] Contains all MLine marked lines with the column index to edit +-- with respect to for each specific line. +-- @class table +-- @name mlines +local mlines = {} + +local mlines_most_recent + +--- +-- Adds an mline marker to the current line and stores the line number and +-- column position of the caret in the mlines table. +function add() + local buffer = buffer + buffer:marker_set_back(MARK_MLINE, MARK_MLINE_COLOR) + local column = buffer.column[buffer.current_pos] + local line = buffer:line_from_position(buffer.current_pos) + local new_marker = buffer:marker_add(line, MARK_MLINE) + mlines[line] = { marker = new_marker, start_col = column } + mlines_most_recent = line +end + +--- +-- Adds mline markers to all lines from the most recently added line to the +-- current line. +-- The mlines table is updated as in add(), but all column positions are the +-- same as the current column caret position. +function add_multiple() + local buffer = buffer + if mlines_most_recent then + local line = buffer:line_from_position(buffer.current_pos) + local column = buffer.column[buffer.current_pos] + local start_line, end_line + if mlines_most_recent < line then + start_line, end_line = mlines_most_recent, line + else + start_line, end_line = line, mlines_most_recent + end + for curr_line = start_line, end_line do + local new_mark = buffer:marker_add(curr_line, MARK_MLINE) + mlines[curr_line] = { marker = new_mark, start_col = column } + end + mlines_most_recent = line + end +end + +--- +-- Clears the mline marker at the current line. +function remove() + local buffer = buffer + local line = buffer:line_from_position(buffer.current_pos) + buffer:marker_delete(line, MARK_MLINE) + mlines[line] = nil +end + +--- +-- Clears the mline markers from the line whose marker was most recently +-- removed (or the line where a marker was most recently added to) to the +-- current line. +function remove_multiple() + local buffer = buffer + if mlines_most_recent then + local line = buffer:line_from_position(buffer.current_pos) + local start_line, end_line + if mlines_most_recent < line then + start_line, end_line = mlines_most_recent, line + else + start_line, end_line = line, mlines_most_recent + end + for curr_line = start_line, end_line do + buffer:marker_delete(curr_line, MARK_MLINE) + mlines[curr_line] = nil + end + mlines_most_recent = line + end +end + +--- +-- Clears all mline markers and the mlines table. +function clear() + local buffer = buffer + buffer:marker_delete_all(MARK_MLINE) + mlines = {} + mlines_most_recent = nil +end + +--- +-- Applies changes made in the current line relative to the caret column +-- position stored initially to all lines with mline markers in relation to +-- their initial column positions. +function update() + local buffer = buffer + local curr_line = buffer:line_from_position(buffer.current_pos) + local curr_col = buffer.column[buffer.current_pos] + if mlines[curr_line] then + local s = buffer:find_column( curr_line, mlines[curr_line].start_col ) + local e = buffer:find_column(curr_line, curr_col) + local delta = e - s + local txt = '' + if delta > 0 then txt = buffer:text_range(s, e) end + for line_num, item in pairs(mlines) do + if line_num ~= curr_line then + local next_pos = buffer:find_column(line_num, item.start_col) + if delta < 0 then + buffer.current_pos, buffer.anchor = next_pos, next_pos + for i = 1, math.abs(delta) do buffer:delete_back() end + item.start_col = buffer.column[buffer.current_pos] + else + buffer:insert_text(next_pos, txt) + item.start_col = item.start_col + #txt + end + end + end + if delta < 0 then + local pos = buffer:position_from_line(curr_line) + curr_col + buffer:goto_pos(pos) + end + mlines[curr_line].start_col = curr_col + end +end 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 mitchell<att>caladbolg.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 |