From 9cc9f312c3c156aaf2f97713bb3a760bd224c52d Mon Sep 17 00:00:00 2001 From: mitchell <70453897+667e-11@users.noreply.github.com> Date: Fri, 11 Jun 2010 19:04:10 -0400 Subject: Renamed modules/textadept/key_commands.lua to modules/textadept/keys.lua. --- doc/manual/6_Startup.md | 2 +- modules/textadept/init.lua | 2 +- modules/textadept/key_commands.lua | 652 ------------------------------------- modules/textadept/keys.lua | 652 +++++++++++++++++++++++++++++++++++++ 4 files changed, 654 insertions(+), 654 deletions(-) delete mode 100644 modules/textadept/key_commands.lua create mode 100644 modules/textadept/keys.lua diff --git a/doc/manual/6_Startup.md b/doc/manual/6_Startup.md index e5c98dac..4adbedc8 100644 --- a/doc/manual/6_Startup.md +++ b/doc/manual/6_Startup.md @@ -53,7 +53,7 @@ example: -- These need to be loaded last. --require 'textadept.menu' - require 'textadept.key_commands' + require 'textadept.keys' Please note Textadept does NOT load your `~/.textadept/init.lua`'s modules in addition to its own. This defeats the purpose of maximum extensibility. If your diff --git a/modules/textadept/init.lua b/modules/textadept/init.lua index d2c038f3..01acde58 100644 --- a/modules/textadept/init.lua +++ b/modules/textadept/init.lua @@ -16,4 +16,4 @@ require 'textadept.snippets' -- These need to be loaded last. require 'textadept.menu' -require 'textadept.key_commands' +require 'textadept.keys' diff --git a/modules/textadept/key_commands.lua b/modules/textadept/key_commands.lua deleted file mode 100644 index cdfda538..00000000 --- a/modules/textadept/key_commands.lua +++ /dev/null @@ -1,652 +0,0 @@ --- Copyright 2007-2010 Mitchell mitchellcaladbolg.net. See LICENSE. - -local locale = _G.locale -local events = _G.events - ---- --- Manages and defines key commands in Textadept. --- This set of key commands is pretty standard among other text editors. -module('_m.textadept.keys', package.seeall) - --- Markdown: --- ## Overview --- --- Key commands are defined in the global table `keys`. Each key-value pair in --- `keys` consists of either: --- --- * A string representing a key command and an associated action table. --- * A string language name and its associated `keys`-like table. --- * A string style name and its associated `keys`-like table. --- * A string representing a key command and its associated `keys`-like table. --- (This is a keychain sequence.) --- --- A key command string is built from a combination of the `CTRL`, `SHIFT`, --- `ALT`, and `ADD` constants as well as the pressed key itself. The value of --- `ADD` is inserted between each of `CTRL`, `SHIFT`, `ALT`, and the key. --- For example: --- --- -- keys.lua: --- CTRL = 'Ctrl' --- SHIFT = 'Shift' --- ALT = 'Alt' --- ADD = '+' --- -- pressing control, shift, alt and 'a' yields: 'Ctrl+Shift+Alt+A' --- --- For key values less than 255, Lua's [`string.char()`][string_char] is used to --- determine the key's string representation. Otherwise, the --- [`KEYSYMS`][keysyms] lookup table is used. --- --- [string_char]: http://www.lua.org/manual/5.1/manual.html#pdf-string.char --- [keysyms]: ../modules/keys.html#KEYSYMS --- --- An action table is a table consisting of either: --- --- * A Lua function followed by a list of arguments to pass to that function. --- * A string representing a [buffer][buffer] or [view][view] function followed --- by its respective `'buffer'` or `'view'` string and then any arguments to --- pass to the resulting function. --- --- `buffer.`_`function`_ by itself cannot be used because at the time of --- evaluation, `buffer.`_`function`_ would apply only to the current --- buffer, not for all buffers. By using this string reference system, the --- correct `buffer.`_`function`_ will be evaluated every time. The same --- applies to `view`. --- --- [buffer]: ../modules/buffer.html --- [view]: ../modules/view.html --- --- Language names are the names of the lexer files in `lexers/` such as `cpp` --- and `lua`. Style names are different lexer styles, most of which are in --- `lexers/lexer.lua`; examples are `whitespace`, `comment`, and `string`. --- --- Key commands can be chained like in Emacs using keychain sequences. By --- default, the `Esc` key cancels the current keychain, but it can be redefined --- by setting the `keys.clear_sequence` field. Naturally, the clear sequence --- cannot be chained. --- --- ## Settings --- --- * `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 (the Apple key on Mac OSX). --- * `ADD`: The string representing used to join together a sequence of Control, --- Shift, or Alt modifier keys. --- --- ## Key Command Precedence --- --- When searching for a key command to execute in the `keys` table, key commands --- in the current style have priority, followed by the ones in the current lexer, --- and finally the ones in the global table. --- --- ## Example --- --- keys = { --- ['ctrl+f'] = { 'char_right', 'buffer' }, --- ['ctrl+b'] = { 'char_left', 'buffer' }, --- lua = { --- ['ctrl+c'] = { 'add_text', 'buffer', '-- ' }, --- whitespace = { --- ['ctrl+f'] = { function() print('whitespace') end } --- } --- } --- } --- --- The first two key commands are global and call `buffer:char_right()` and --- `buffer:char_left()` respectively. The last two commands apply only in the --- Lua lexer with the very last one only being available in Lua's `whitespace` --- style. If `ctrl+f` is pressed when the current style is `whitespace` in the --- `lua` lexer, the global key command with the same shortcut is overriden and --- `whitespace` is printed to standard out. --- --- ## Problems --- --- All Lua functions must be defined BEFORE they are reference in key commands. --- Therefore, any module containing key commands should be loaded after all --- other modules, whose functions are being referenced, have been loaded. --- --- ## Configuration --- --- It is not recommended to edit Textadept's `core/ext/key_commands.lua`. You --- can either override or add to default key commands in your --- `~/.textadept/key_commands.lua` or `require` a separate module in your --- `~/.textadept/init.lua` instead of `ext/key_commands`. - --- Windows and Linux key commands are listed in the first block. --- Mac OSX key commands are listed in the second block. - --- settings -local SCOPES_ENABLED = true -local ADD = '' -local CTRL = 'c'..ADD -local SHIFT = 's'..ADD -local ALT = 'a'..ADD --- end settings - -local keys = _M -local b, v = 'buffer', 'view' -local gui = gui - --- CTRL = 'c' --- SHIFT = 's' --- ALT = 'a' --- ADD = '' --- Control, Shift, Alt, and 'a' = 'caA' --- Control, Shift, Alt, and '\t' = 'csa\t' - -if not MAC then - -- Windows and Linux key commands. - - --[[ - C: D H I J K M U - A: A B C D E F G H J K L M N P R S T U V W X Y Z - CS: A B C D G H I J K L M N O Q T U V X Y Z - SA: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z - CA: A B C D E F G H J K L M N O Q R S T U V W X Y Z - CSA: A B C D E F G H J K L M N O P Q R S T U V W X Y Z - ]]-- - - keys.clear_sequence = 'esc' - - keys.ct = {} -- Textadept command chain - - -- File - local m_session = _m.textadept.session - keys.cn = { new_buffer } - keys.co = { io.open_file } - -- TODO: { 'reload', b } - keys.cs = { 'save', b } - keys.cS = { 'save_as', b } - keys.cw = { 'close', b } - keys.cW = { io.close_all } - -- TODO: { m_session.load } after prompting with open dialog - -- TODO: { m_session.save } after prompting with save dialog - keys.aq = { quit } - - -- Edit - local m_editing = _m.textadept.editing - keys.cz = { 'undo', b } - keys.cy = { 'redo', b } - keys.cx = { 'cut', b } - keys.cc = { 'copy', b } - keys.cv = { 'paste', b } - -- Delete is delete. - keys.ca = { 'select_all', b } - keys.ce = { m_editing.match_brace } - keys.cE = { m_editing.match_brace, 'select' } - keys['c\n'] = { m_editing.autocomplete_word, '%w_' } - keys['c\n\r'] = { m_editing.autocomplete_word, '%w_' } -- win32 - keys.cq = { m_editing.block_comment } - -- TODO: { m_editing.current_word, 'delete' } - -- TODO: { m_editing.transpose_chars } - -- TODO: { m_editing.squeeze } - -- TODO: { m_editing.convert_indentation } - -- TODO: { m_editing.smart_cutcopy } - -- TODO: { m_editing.smart_cutcopy, 'copy' } - keys.ac = { -- enClose in... - t = { m_editing.enclose, 'tag' }, - T = { m_editing.enclose, 'single_tag' }, - ['"'] = { 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... - t = { m_editing.select_enclosed, 'tags' }, - ['"'] = { 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 }, - b = { m_editing.select_indented_block }, - s = { m_editing.select_scope }, - g = { m_editing.grow_selection, 1 }, - } - - -- Search - keys.cf = { gui.find.focus } -- find/replace - keys['f3'] = { gui.find.find_next } - -- Find Next is an when find pane is focused. - -- Find Prev is ap when find pane is focused. - -- Replace is ar when find pane is focused. - keys.cF = { gui.find.find_incremental } - -- Find in Files is ai when find pane is focused. - -- TODO: { gui.find.goto_file_in_list, true } - -- TODO: { gui.find.goto_file_in_list, false } - keys.cg = { m_editing.goto_line } - - -- Tools - keys['f2'] = { gui.command_entry.focus } - -- Run - local m_run = _m.textadept.run - keys.cr = { m_run.run } - keys.cR = { m_run.compile } - -- Snippets - local m_snippets = _m.textadept.snippets - keys['\t'] = { m_snippets.insert } - keys['s\t'] = { m_snippets.prev } - keys.cai = { m_snippets.cancel_current } - keys.caI = { m_snippets.list } - keys.ai = { m_snippets.show_style } - - -- Buffers - keys.cb = { gui.switch_buffer } - keys['c\t'] = { 'goto_buffer', v, 1, false } - keys['cs\t'] = { 'goto_buffer', v, -1, false } - 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 - events.emit('update_ui') -- for updating statusbar - end - keys.ct.v = { - e = { toggle_setting, 'view_eol' }, - w = { toggle_setting, 'wrap_mode' }, - i = { toggle_setting, 'indentation_guides' }, - ['\t'] = { toggle_setting, 'use_tabs' }, - [' '] = { toggle_setting, 'view_ws' }, - } - keys.cl = { _m.textadept.mime_types.select_lexer } - keys['f5'] = { 'colourise', b, 0, -1 } - - -- Views - keys.cav = { - n = { gui.goto_view, 1, false }, - p = { gui.goto_view, -1, false }, - S = { 'split', v }, -- vertical - s = { 'split', v, false }, -- horizontal - w = { function() view:unsplit() return true end }, - W = { function() while view:unsplit() do end end }, - -- TODO: { function() view.size = view.size + 10 end } - -- TODO: { function() view.size = view.size - 10 end } - } - keys.c0 = { function() buffer.zoom = 0 end } - - -- Miscellaneous not in standard menu. - -- Recent files. - local RECENT_FILES = 1 - events.connect('user_list_selection', - function(type, text) - if type == RECENT_FILES then io.open_file(text) end - end) - keys.ao = { - function() - local buffer = buffer - local files = {} - for _, filename in ipairs(io.recent_files) do - table.insert(files, 1, filename) - end - local sep = buffer.auto_c_separator - buffer.auto_c_separator = ('|'):byte() - buffer:user_list_show(RECENT_FILES, table.concat(files, '|')) - buffer.auto_c_separator = sep - end - } - -else - -- Mac OSX key commands - - --[[ - C: J M U W X Z - A: D E H J K L U Y - CS: C D G H I J K L M O Q S T U V W X Y Z - SA: A B C D H I J K L M N O Q R T U V X Y - CA: A C E J K L M N O Q R S T U V W X Y Z - CSA: A C D E H J K L M N O P Q R S T U V W X Y Z - ]]-- - - keys.clear_sequence = 'aesc' - - keys.at = {} -- Textadept command chain - - -- File - local m_session = _m.textadept.session - keys.an = { new_buffer } - keys.ao = { io.open_file } - -- TODO: { 'reload', b } - keys.as = { 'save', b } - keys.aS = { 'save_as', b } - keys.aw = { 'close', b } - keys.aW = { io.close_all } - -- TODO: { m_session.load } after prompting with open dialog - -- TODO: { m_session.save } after prompting with save dialog - keys.aq = { quit } - - -- Edit - local m_editing = _m.textadept.editing - keys.az = { 'undo', b } - keys.aZ = { 'redo', b } - keys.ax = { 'cut', b } - keys.ac = { 'copy', b } - keys.av = { 'paste', b } - -- Delete is delete. - keys.aa = { 'select_all', b } - keys.cm = { m_editing.match_brace } - keys.aE = { m_editing.match_brace, 'select' } - keys.esc = { m_editing.autocomplete_word, '%w_' } - keys.cq = { m_editing.block_comment } - -- TODO: { m_editing.current_word, 'delete' } - keys.ct = { m_editing.transpose_chars } - -- TODO: { m_editing.squeeze } - -- TODO: { m_editing.convert_indentation } - keys.ck = { m_editing.smart_cutcopy } - -- TODO: { m_editing.smart_cutcopy, 'copy' } - keys.cy = { 'paste', b } - keys.cc = { -- enClose in... - t = { m_editing.enclose, 'tag' }, - T = { m_editing.enclose, 'single_tag' }, - ['"'] = { 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.cs = { -- select in... - e = { m_editing.select_enclosed }, - t = { m_editing.select_enclosed, 'tags' }, - ['"'] = { 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 }, - b = { m_editing.select_indented_block }, - s = { m_editing.select_scope }, - g = { m_editing.grow_selection, 1 }, - } - - -- Search - keys.af = { gui.find.focus } -- find/replace - keys.ag = { gui.find.find_next } - keys.aG = { gui.find.find_prev } - keys.ar = { gui.find.replace } - keys.ai = { gui.find.find_incremental } - keys.aF = { - function() - gui.find.in_files = true - gui.find.focus() - end - } - keys.cag = { gui.find.goto_file_in_list, true } - keys.caG = { gui.find.goto_file_in_list, false } - keys.cg = { m_editing.goto_line } - - -- Tools - keys['f2'] = { gui.command_entry.focus } - -- Run - local m_run = _m.textadept.run - keys.cr = { m_run.run } - keys.cR = { m_run.compile } - -- Snippets - local m_snippets = _m.textadept.snippets - keys['\t'] = { m_snippets.insert } - keys['s\t'] = { m_snippets.prev } - keys.cai = { m_snippets.cancel_current } - keys.caI = { m_snippets.list } - keys.ci = { m_snippets.show_style } - - -- Buffers - keys.ab = { gui.switch_buffer } - keys['c\t'] = { 'goto_buffer', v, 1, false } - keys['cs\t'] = { 'goto_buffer', v, -1, false } - 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 - events.emit('update_ui') -- for updating statusbar - end - keys.at.v = { - e = { toggle_setting, 'view_eol' }, - w = { toggle_setting, 'wrap_mode' }, - i = { toggle_setting, 'indentation_guides' }, - ['\t'] = { toggle_setting, 'use_tabs' }, - [' '] = { toggle_setting, 'view_ws' }, - } - keys.cl = { _m.textadept.mime_types.select_lexer } - keys['f5'] = { 'colourise', b, 0, -1 } - - -- Views - keys.cv = { - n = { gui.goto_view, 1, false }, - p = { gui.goto_view, -1, false }, - S = { 'split', v }, -- vertical - s = { 'split', v, false }, -- horizontal - w = { function() view:unsplit() return true end }, - W = { function() while view:unsplit() do end end }, - -- TODO: { function() view.size = view.size + 10 end } - -- TODO: { function() view.size = view.size - 10 end } - } - keys.c0 = { function() buffer.zoom = 0 end } - - -- Miscellaneous not in standard menu. - -- Recent files. - local RECENT_FILES = 1 - events.connect('user_list_selection', - function(type, text) - if type == RECENT_FILES then io.open_file(text) end - end) - keys.co = { - function() - local buffer = buffer - local files = {} - for _, filename in ipairs(io.recent_files) do - table.insert(files, 1, filename) - end - local sep = buffer.auto_c_separator - buffer.auto_c_separator = ('|'):byte() - buffer:user_list_show(RECENT_FILES, table.concat(files, '|')) - buffer.auto_c_separator = sep - end - } - - -- Movement/selection commands - keys.cf = { 'char_right', b } - keys.cF = { 'char_right_extend', b } - keys.caf = { 'word_right', b } - keys.caF = { 'word_right_extend', b } - keys.cb = { 'char_left', b } - keys.cB = { 'char_left_extend', b } - keys.cab = { 'word_left', b } - keys.caB = { 'word_left_extend', b } - keys.cn = { 'line_down', b } - keys.cN = { 'line_down_extend', b } - keys.cp = { 'line_up', b } - keys.cP = { 'line_up_extend', b } - keys.ca = { 'vc_home', b } - keys.cA = { 'home_extend', b } - keys.ce = { 'line_end', b } - keys.cE = { 'line_end_extend', b } - keys.ch = { 'delete_back', b } - keys.cah = { 'del_word_left', b } - keys.cd = { 'clear', b } - keys.cad = { 'del_word_right', b } -end - -user_dofile('key_commands.lua') -- load user key commands - --- Do not edit below this line. - --- optimize for speed -local string = _G.string -local string_char = string.char -local string_format = string.format -local pcall = _G.pcall -local ipairs = _G.ipairs -local next = _G.next -local type = _G.type -local unpack = _G.unpack -local MAC = _G.MAC - ---- --- Lookup table for key values higher than 255. --- If a key value given to 'keypress' is higher than 255, this table is used to --- return a string representation of the key if it exists. --- @class table --- @name KEYSYMS -KEYSYMS = { -- from - [65056] = '\t', -- backtab; will be 'shift'ed - [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', -} - --- The current key sequence. -local keychain = {} - --- Clears the current key sequence. -local function clear_key_sequence() - keychain = {} - gui.statusbar_text = '' -end - --- Helper function that gets commands associated with the current keychain from --- 'keys'. --- 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. -local function try_get_cmd(active_table) - for _, key_seq in ipairs(keychain) do active_table = active_table[key_seq] end - if #active_table == 0 and next(active_table) then - gui.statusbar_text = locale.KEYCHAIN..table.concat(keychain, ' ') - 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(locale.KEYS_UNKNOWN_COMMAND..tostring(func)) - end - end -end - --- Tries to get a key command based on the lexer and current scope. -local function try_get_cmd1(keys, lexer, scope) - return try_get_cmd(keys[lexer][scope]) -end - --- Tries to get a key command based on the lexer. -local function try_get_cmd2(keys, lexer) - return try_get_cmd(keys[lexer]) -end - --- Tries to get a global key command. -local function try_get_cmd3(keys) - return try_get_cmd(keys) -end - --- Handles Textadept keypresses. --- It is called every time a key is pressed, and based on lexer and scope, --- executes a command. The command is looked up in the global 'keys' key --- command table. --- @return whatever the executed command returns, true by default. A true --- return value will tell Textadept not to handle the key afterwords. -local function keypress(code, shift, control, alt) - local buffer = buffer - local key - --print(code, string.char(code)) - if code < 256 then - key = string_char(code) - shift = false -- for printable characters, key is upper case - if MAC and not shift and not control and not alt then - local ch = string_char(code) - -- work around native GTK-OSX's handling of Alt key - if ch:find('[%p%d]') and #keychain == 0 then - if buffer.anchor ~= buffer.current_pos then buffer:delete_back() end - buffer:add_text(ch) - events.emit('char_added', code) - return true - end - end - else - if not KEYSYMS[code] then return end - key = KEYSYMS[code] - end - control = control and CTRL or '' - shift = shift and SHIFT or '' - alt = alt and ALT or '' - local key_seq = string_format('%s%s%s%s', control, shift, alt, key) - - if #keychain > 0 and key_seq == keys.clear_sequence then - clear_key_sequence() - return true - end - - local lexer = buffer:get_lexer_language() - keychain[#keychain + 1] = key_seq - local ret, func, args - if SCOPES_ENABLED then - local style = buffer.style_at[buffer.current_pos] - local scope = buffer:get_style_name(style) - --print(key_seq, 'Lexer: '..lexer, 'Scope: '..scope) - ret, func, args = pcall(try_get_cmd1, keys, lexer, scope) - end - if not ret and func ~= -1 then - ret, func, args = pcall(try_get_cmd2, keys, lexer) - end - if not ret and func ~= -1 then - ret, func, args = pcall(try_get_cmd3, keys) - 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 - error(retval) - end - 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 - gui.statusbar_text = locale.KEYS_INVALID - return true - end - else - return true - end - end -end -events.connect('keypress', keypress, 1) diff --git a/modules/textadept/keys.lua b/modules/textadept/keys.lua new file mode 100644 index 00000000..cdfda538 --- /dev/null +++ b/modules/textadept/keys.lua @@ -0,0 +1,652 @@ +-- Copyright 2007-2010 Mitchell mitchellcaladbolg.net. See LICENSE. + +local locale = _G.locale +local events = _G.events + +--- +-- Manages and defines key commands in Textadept. +-- This set of key commands is pretty standard among other text editors. +module('_m.textadept.keys', package.seeall) + +-- Markdown: +-- ## Overview +-- +-- Key commands are defined in the global table `keys`. Each key-value pair in +-- `keys` consists of either: +-- +-- * A string representing a key command and an associated action table. +-- * A string language name and its associated `keys`-like table. +-- * A string style name and its associated `keys`-like table. +-- * A string representing a key command and its associated `keys`-like table. +-- (This is a keychain sequence.) +-- +-- A key command string is built from a combination of the `CTRL`, `SHIFT`, +-- `ALT`, and `ADD` constants as well as the pressed key itself. The value of +-- `ADD` is inserted between each of `CTRL`, `SHIFT`, `ALT`, and the key. +-- For example: +-- +-- -- keys.lua: +-- CTRL = 'Ctrl' +-- SHIFT = 'Shift' +-- ALT = 'Alt' +-- ADD = '+' +-- -- pressing control, shift, alt and 'a' yields: 'Ctrl+Shift+Alt+A' +-- +-- For key values less than 255, Lua's [`string.char()`][string_char] is used to +-- determine the key's string representation. Otherwise, the +-- [`KEYSYMS`][keysyms] lookup table is used. +-- +-- [string_char]: http://www.lua.org/manual/5.1/manual.html#pdf-string.char +-- [keysyms]: ../modules/keys.html#KEYSYMS +-- +-- An action table is a table consisting of either: +-- +-- * A Lua function followed by a list of arguments to pass to that function. +-- * A string representing a [buffer][buffer] or [view][view] function followed +-- by its respective `'buffer'` or `'view'` string and then any arguments to +-- pass to the resulting function. +-- +-- `buffer.`_`function`_ by itself cannot be used because at the time of +-- evaluation, `buffer.`_`function`_ would apply only to the current +-- buffer, not for all buffers. By using this string reference system, the +-- correct `buffer.`_`function`_ will be evaluated every time. The same +-- applies to `view`. +-- +-- [buffer]: ../modules/buffer.html +-- [view]: ../modules/view.html +-- +-- Language names are the names of the lexer files in `lexers/` such as `cpp` +-- and `lua`. Style names are different lexer styles, most of which are in +-- `lexers/lexer.lua`; examples are `whitespace`, `comment`, and `string`. +-- +-- Key commands can be chained like in Emacs using keychain sequences. By +-- default, the `Esc` key cancels the current keychain, but it can be redefined +-- by setting the `keys.clear_sequence` field. Naturally, the clear sequence +-- cannot be chained. +-- +-- ## Settings +-- +-- * `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 (the Apple key on Mac OSX). +-- * `ADD`: The string representing used to join together a sequence of Control, +-- Shift, or Alt modifier keys. +-- +-- ## Key Command Precedence +-- +-- When searching for a key command to execute in the `keys` table, key commands +-- in the current style have priority, followed by the ones in the current lexer, +-- and finally the ones in the global table. +-- +-- ## Example +-- +-- keys = { +-- ['ctrl+f'] = { 'char_right', 'buffer' }, +-- ['ctrl+b'] = { 'char_left', 'buffer' }, +-- lua = { +-- ['ctrl+c'] = { 'add_text', 'buffer', '-- ' }, +-- whitespace = { +-- ['ctrl+f'] = { function() print('whitespace') end } +-- } +-- } +-- } +-- +-- The first two key commands are global and call `buffer:char_right()` and +-- `buffer:char_left()` respectively. The last two commands apply only in the +-- Lua lexer with the very last one only being available in Lua's `whitespace` +-- style. If `ctrl+f` is pressed when the current style is `whitespace` in the +-- `lua` lexer, the global key command with the same shortcut is overriden and +-- `whitespace` is printed to standard out. +-- +-- ## Problems +-- +-- All Lua functions must be defined BEFORE they are reference in key commands. +-- Therefore, any module containing key commands should be loaded after all +-- other modules, whose functions are being referenced, have been loaded. +-- +-- ## Configuration +-- +-- It is not recommended to edit Textadept's `core/ext/key_commands.lua`. You +-- can either override or add to default key commands in your +-- `~/.textadept/key_commands.lua` or `require` a separate module in your +-- `~/.textadept/init.lua` instead of `ext/key_commands`. + +-- Windows and Linux key commands are listed in the first block. +-- Mac OSX key commands are listed in the second block. + +-- settings +local SCOPES_ENABLED = true +local ADD = '' +local CTRL = 'c'..ADD +local SHIFT = 's'..ADD +local ALT = 'a'..ADD +-- end settings + +local keys = _M +local b, v = 'buffer', 'view' +local gui = gui + +-- CTRL = 'c' +-- SHIFT = 's' +-- ALT = 'a' +-- ADD = '' +-- Control, Shift, Alt, and 'a' = 'caA' +-- Control, Shift, Alt, and '\t' = 'csa\t' + +if not MAC then + -- Windows and Linux key commands. + + --[[ + C: D H I J K M U + A: A B C D E F G H J K L M N P R S T U V W X Y Z + CS: A B C D G H I J K L M N O Q T U V X Y Z + SA: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z + CA: A B C D E F G H J K L M N O Q R S T U V W X Y Z + CSA: A B C D E F G H J K L M N O P Q R S T U V W X Y Z + ]]-- + + keys.clear_sequence = 'esc' + + keys.ct = {} -- Textadept command chain + + -- File + local m_session = _m.textadept.session + keys.cn = { new_buffer } + keys.co = { io.open_file } + -- TODO: { 'reload', b } + keys.cs = { 'save', b } + keys.cS = { 'save_as', b } + keys.cw = { 'close', b } + keys.cW = { io.close_all } + -- TODO: { m_session.load } after prompting with open dialog + -- TODO: { m_session.save } after prompting with save dialog + keys.aq = { quit } + + -- Edit + local m_editing = _m.textadept.editing + keys.cz = { 'undo', b } + keys.cy = { 'redo', b } + keys.cx = { 'cut', b } + keys.cc = { 'copy', b } + keys.cv = { 'paste', b } + -- Delete is delete. + keys.ca = { 'select_all', b } + keys.ce = { m_editing.match_brace } + keys.cE = { m_editing.match_brace, 'select' } + keys['c\n'] = { m_editing.autocomplete_word, '%w_' } + keys['c\n\r'] = { m_editing.autocomplete_word, '%w_' } -- win32 + keys.cq = { m_editing.block_comment } + -- TODO: { m_editing.current_word, 'delete' } + -- TODO: { m_editing.transpose_chars } + -- TODO: { m_editing.squeeze } + -- TODO: { m_editing.convert_indentation } + -- TODO: { m_editing.smart_cutcopy } + -- TODO: { m_editing.smart_cutcopy, 'copy' } + keys.ac = { -- enClose in... + t = { m_editing.enclose, 'tag' }, + T = { m_editing.enclose, 'single_tag' }, + ['"'] = { 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... + t = { m_editing.select_enclosed, 'tags' }, + ['"'] = { 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 }, + b = { m_editing.select_indented_block }, + s = { m_editing.select_scope }, + g = { m_editing.grow_selection, 1 }, + } + + -- Search + keys.cf = { gui.find.focus } -- find/replace + keys['f3'] = { gui.find.find_next } + -- Find Next is an when find pane is focused. + -- Find Prev is ap when find pane is focused. + -- Replace is ar when find pane is focused. + keys.cF = { gui.find.find_incremental } + -- Find in Files is ai when find pane is focused. + -- TODO: { gui.find.goto_file_in_list, true } + -- TODO: { gui.find.goto_file_in_list, false } + keys.cg = { m_editing.goto_line } + + -- Tools + keys['f2'] = { gui.command_entry.focus } + -- Run + local m_run = _m.textadept.run + keys.cr = { m_run.run } + keys.cR = { m_run.compile } + -- Snippets + local m_snippets = _m.textadept.snippets + keys['\t'] = { m_snippets.insert } + keys['s\t'] = { m_snippets.prev } + keys.cai = { m_snippets.cancel_current } + keys.caI = { m_snippets.list } + keys.ai = { m_snippets.show_style } + + -- Buffers + keys.cb = { gui.switch_buffer } + keys['c\t'] = { 'goto_buffer', v, 1, false } + keys['cs\t'] = { 'goto_buffer', v, -1, false } + 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 + events.emit('update_ui') -- for updating statusbar + end + keys.ct.v = { + e = { toggle_setting, 'view_eol' }, + w = { toggle_setting, 'wrap_mode' }, + i = { toggle_setting, 'indentation_guides' }, + ['\t'] = { toggle_setting, 'use_tabs' }, + [' '] = { toggle_setting, 'view_ws' }, + } + keys.cl = { _m.textadept.mime_types.select_lexer } + keys['f5'] = { 'colourise', b, 0, -1 } + + -- Views + keys.cav = { + n = { gui.goto_view, 1, false }, + p = { gui.goto_view, -1, false }, + S = { 'split', v }, -- vertical + s = { 'split', v, false }, -- horizontal + w = { function() view:unsplit() return true end }, + W = { function() while view:unsplit() do end end }, + -- TODO: { function() view.size = view.size + 10 end } + -- TODO: { function() view.size = view.size - 10 end } + } + keys.c0 = { function() buffer.zoom = 0 end } + + -- Miscellaneous not in standard menu. + -- Recent files. + local RECENT_FILES = 1 + events.connect('user_list_selection', + function(type, text) + if type == RECENT_FILES then io.open_file(text) end + end) + keys.ao = { + function() + local buffer = buffer + local files = {} + for _, filename in ipairs(io.recent_files) do + table.insert(files, 1, filename) + end + local sep = buffer.auto_c_separator + buffer.auto_c_separator = ('|'):byte() + buffer:user_list_show(RECENT_FILES, table.concat(files, '|')) + buffer.auto_c_separator = sep + end + } + +else + -- Mac OSX key commands + + --[[ + C: J M U W X Z + A: D E H J K L U Y + CS: C D G H I J K L M O Q S T U V W X Y Z + SA: A B C D H I J K L M N O Q R T U V X Y + CA: A C E J K L M N O Q R S T U V W X Y Z + CSA: A C D E H J K L M N O P Q R S T U V W X Y Z + ]]-- + + keys.clear_sequence = 'aesc' + + keys.at = {} -- Textadept command chain + + -- File + local m_session = _m.textadept.session + keys.an = { new_buffer } + keys.ao = { io.open_file } + -- TODO: { 'reload', b } + keys.as = { 'save', b } + keys.aS = { 'save_as', b } + keys.aw = { 'close', b } + keys.aW = { io.close_all } + -- TODO: { m_session.load } after prompting with open dialog + -- TODO: { m_session.save } after prompting with save dialog + keys.aq = { quit } + + -- Edit + local m_editing = _m.textadept.editing + keys.az = { 'undo', b } + keys.aZ = { 'redo', b } + keys.ax = { 'cut', b } + keys.ac = { 'copy', b } + keys.av = { 'paste', b } + -- Delete is delete. + keys.aa = { 'select_all', b } + keys.cm = { m_editing.match_brace } + keys.aE = { m_editing.match_brace, 'select' } + keys.esc = { m_editing.autocomplete_word, '%w_' } + keys.cq = { m_editing.block_comment } + -- TODO: { m_editing.current_word, 'delete' } + keys.ct = { m_editing.transpose_chars } + -- TODO: { m_editing.squeeze } + -- TODO: { m_editing.convert_indentation } + keys.ck = { m_editing.smart_cutcopy } + -- TODO: { m_editing.smart_cutcopy, 'copy' } + keys.cy = { 'paste', b } + keys.cc = { -- enClose in... + t = { m_editing.enclose, 'tag' }, + T = { m_editing.enclose, 'single_tag' }, + ['"'] = { 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.cs = { -- select in... + e = { m_editing.select_enclosed }, + t = { m_editing.select_enclosed, 'tags' }, + ['"'] = { 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 }, + b = { m_editing.select_indented_block }, + s = { m_editing.select_scope }, + g = { m_editing.grow_selection, 1 }, + } + + -- Search + keys.af = { gui.find.focus } -- find/replace + keys.ag = { gui.find.find_next } + keys.aG = { gui.find.find_prev } + keys.ar = { gui.find.replace } + keys.ai = { gui.find.find_incremental } + keys.aF = { + function() + gui.find.in_files = true + gui.find.focus() + end + } + keys.cag = { gui.find.goto_file_in_list, true } + keys.caG = { gui.find.goto_file_in_list, false } + keys.cg = { m_editing.goto_line } + + -- Tools + keys['f2'] = { gui.command_entry.focus } + -- Run + local m_run = _m.textadept.run + keys.cr = { m_run.run } + keys.cR = { m_run.compile } + -- Snippets + local m_snippets = _m.textadept.snippets + keys['\t'] = { m_snippets.insert } + keys['s\t'] = { m_snippets.prev } + keys.cai = { m_snippets.cancel_current } + keys.caI = { m_snippets.list } + keys.ci = { m_snippets.show_style } + + -- Buffers + keys.ab = { gui.switch_buffer } + keys['c\t'] = { 'goto_buffer', v, 1, false } + keys['cs\t'] = { 'goto_buffer', v, -1, false } + 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 + events.emit('update_ui') -- for updating statusbar + end + keys.at.v = { + e = { toggle_setting, 'view_eol' }, + w = { toggle_setting, 'wrap_mode' }, + i = { toggle_setting, 'indentation_guides' }, + ['\t'] = { toggle_setting, 'use_tabs' }, + [' '] = { toggle_setting, 'view_ws' }, + } + keys.cl = { _m.textadept.mime_types.select_lexer } + keys['f5'] = { 'colourise', b, 0, -1 } + + -- Views + keys.cv = { + n = { gui.goto_view, 1, false }, + p = { gui.goto_view, -1, false }, + S = { 'split', v }, -- vertical + s = { 'split', v, false }, -- horizontal + w = { function() view:unsplit() return true end }, + W = { function() while view:unsplit() do end end }, + -- TODO: { function() view.size = view.size + 10 end } + -- TODO: { function() view.size = view.size - 10 end } + } + keys.c0 = { function() buffer.zoom = 0 end } + + -- Miscellaneous not in standard menu. + -- Recent files. + local RECENT_FILES = 1 + events.connect('user_list_selection', + function(type, text) + if type == RECENT_FILES then io.open_file(text) end + end) + keys.co = { + function() + local buffer = buffer + local files = {} + for _, filename in ipairs(io.recent_files) do + table.insert(files, 1, filename) + end + local sep = buffer.auto_c_separator + buffer.auto_c_separator = ('|'):byte() + buffer:user_list_show(RECENT_FILES, table.concat(files, '|')) + buffer.auto_c_separator = sep + end + } + + -- Movement/selection commands + keys.cf = { 'char_right', b } + keys.cF = { 'char_right_extend', b } + keys.caf = { 'word_right', b } + keys.caF = { 'word_right_extend', b } + keys.cb = { 'char_left', b } + keys.cB = { 'char_left_extend', b } + keys.cab = { 'word_left', b } + keys.caB = { 'word_left_extend', b } + keys.cn = { 'line_down', b } + keys.cN = { 'line_down_extend', b } + keys.cp = { 'line_up', b } + keys.cP = { 'line_up_extend', b } + keys.ca = { 'vc_home', b } + keys.cA = { 'home_extend', b } + keys.ce = { 'line_end', b } + keys.cE = { 'line_end_extend', b } + keys.ch = { 'delete_back', b } + keys.cah = { 'del_word_left', b } + keys.cd = { 'clear', b } + keys.cad = { 'del_word_right', b } +end + +user_dofile('key_commands.lua') -- load user key commands + +-- Do not edit below this line. + +-- optimize for speed +local string = _G.string +local string_char = string.char +local string_format = string.format +local pcall = _G.pcall +local ipairs = _G.ipairs +local next = _G.next +local type = _G.type +local unpack = _G.unpack +local MAC = _G.MAC + +--- +-- Lookup table for key values higher than 255. +-- If a key value given to 'keypress' is higher than 255, this table is used to +-- return a string representation of the key if it exists. +-- @class table +-- @name KEYSYMS +KEYSYMS = { -- from + [65056] = '\t', -- backtab; will be 'shift'ed + [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', +} + +-- The current key sequence. +local keychain = {} + +-- Clears the current key sequence. +local function clear_key_sequence() + keychain = {} + gui.statusbar_text = '' +end + +-- Helper function that gets commands associated with the current keychain from +-- 'keys'. +-- 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. +local function try_get_cmd(active_table) + for _, key_seq in ipairs(keychain) do active_table = active_table[key_seq] end + if #active_table == 0 and next(active_table) then + gui.statusbar_text = locale.KEYCHAIN..table.concat(keychain, ' ') + 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(locale.KEYS_UNKNOWN_COMMAND..tostring(func)) + end + end +end + +-- Tries to get a key command based on the lexer and current scope. +local function try_get_cmd1(keys, lexer, scope) + return try_get_cmd(keys[lexer][scope]) +end + +-- Tries to get a key command based on the lexer. +local function try_get_cmd2(keys, lexer) + return try_get_cmd(keys[lexer]) +end + +-- Tries to get a global key command. +local function try_get_cmd3(keys) + return try_get_cmd(keys) +end + +-- Handles Textadept keypresses. +-- It is called every time a key is pressed, and based on lexer and scope, +-- executes a command. The command is looked up in the global 'keys' key +-- command table. +-- @return whatever the executed command returns, true by default. A true +-- return value will tell Textadept not to handle the key afterwords. +local function keypress(code, shift, control, alt) + local buffer = buffer + local key + --print(code, string.char(code)) + if code < 256 then + key = string_char(code) + shift = false -- for printable characters, key is upper case + if MAC and not shift and not control and not alt then + local ch = string_char(code) + -- work around native GTK-OSX's handling of Alt key + if ch:find('[%p%d]') and #keychain == 0 then + if buffer.anchor ~= buffer.current_pos then buffer:delete_back() end + buffer:add_text(ch) + events.emit('char_added', code) + return true + end + end + else + if not KEYSYMS[code] then return end + key = KEYSYMS[code] + end + control = control and CTRL or '' + shift = shift and SHIFT or '' + alt = alt and ALT or '' + local key_seq = string_format('%s%s%s%s', control, shift, alt, key) + + if #keychain > 0 and key_seq == keys.clear_sequence then + clear_key_sequence() + return true + end + + local lexer = buffer:get_lexer_language() + keychain[#keychain + 1] = key_seq + local ret, func, args + if SCOPES_ENABLED then + local style = buffer.style_at[buffer.current_pos] + local scope = buffer:get_style_name(style) + --print(key_seq, 'Lexer: '..lexer, 'Scope: '..scope) + ret, func, args = pcall(try_get_cmd1, keys, lexer, scope) + end + if not ret and func ~= -1 then + ret, func, args = pcall(try_get_cmd2, keys, lexer) + end + if not ret and func ~= -1 then + ret, func, args = pcall(try_get_cmd3, keys) + 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 + error(retval) + end + 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 + gui.statusbar_text = locale.KEYS_INVALID + return true + end + else + return true + end + end +end +events.connect('keypress', keypress, 1) -- cgit v1.2.3