aboutsummaryrefslogtreecommitdiff
path: root/modules/textadept/editing.lua
diff options
context:
space:
mode:
authormitchell <70453897+667e-11@users.noreply.github.com>2007-08-06 05:04:35 -0400
committermitchell <70453897+667e-11@users.noreply.github.com>2007-08-06 05:04:35 -0400
commit58dc16406976d8c18191470a6bafaeda793ed523 (patch)
treee0ebc0d98baf143ae90fb63fe347aefeaa110d96 /modules/textadept/editing.lua
parenta20651bf0b5e9fb00ac8e7b6f5608f332d15cab6 (diff)
downloadtextadept-58dc16406976d8c18191470a6bafaeda793ed523.tar.gz
textadept-58dc16406976d8c18191470a6bafaeda793ed523.zip
Initial import of the textadept module.
Diffstat (limited to 'modules/textadept/editing.lua')
-rw-r--r--modules/textadept/editing.lua622
1 files changed, 622 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