aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar mitchell <70453897+667e-11@users.noreply.github.com>2007-08-06 05:04:35 -0400
committerGravatar mitchell <70453897+667e-11@users.noreply.github.com>2007-08-06 05:04:35 -0400
commit58dc16406976d8c18191470a6bafaeda793ed523 (patch)
treee0ebc0d98baf143ae90fb63fe347aefeaa110d96
parenta20651bf0b5e9fb00ac8e7b6f5608f332d15cab6 (diff)
Initial import of the textadept module.
-rw-r--r--modules/textadept/editing.lua622
-rw-r--r--modules/textadept/init.lua12
-rw-r--r--modules/textadept/key_commands.lua187
-rw-r--r--modules/textadept/keys.lua183
-rw-r--r--modules/textadept/mlines.lua134
-rw-r--r--modules/textadept/snippets.lua438
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