diff options
author | mitchell <70453897+orbitalquark@users.noreply.github.com> | 2020-10-20 15:29:03 -0400 |
---|---|---|
committer | mitchell <70453897+orbitalquark@users.noreply.github.com> | 2020-10-20 15:29:03 -0400 |
commit | 03c4016d07477781aa3adcc9edf340c0bec9c6c8 (patch) | |
tree | d3be089e9020807326a4e56562876ecb7bcf7892 /modules | |
parent | b682fbd4a6e53185e2556686079532ad0e42be94 (diff) |
Code cleanup.
Of note:
* io.save_all_files() does not visit each buffer to save anymore. An unintended
side-effect was checking for outside modification (but only if the file itself
was modified), so outside changes will always be saved over now.
* The menu clicked handler uses assert_type(), so the 'Unknown command'
localization is no longer needed.
* When printing to a new buffer type would split the view, use an existing split
view when possible.
* Prefer 'goto continue' construct in loops over nested 'if's.
* Fixed clearing of ui.find.replace_entry_text on reset in the GUI version.
* Fixed lack of statusbar updating when setting options like buffer EOL mode,
indentation, and encoding.
* Renamed internal new_snippet() to new() and put it in the snippet metatable.
Diffstat (limited to 'modules')
-rw-r--r-- | modules/ansi_c/init.lua | 13 | ||||
-rw-r--r-- | modules/lua/init.lua | 35 | ||||
-rw-r--r-- | modules/lua/ta_api | 5 | ||||
-rw-r--r-- | modules/lua/ta_tags | 1 | ||||
-rw-r--r-- | modules/textadept/bookmarks.lua | 10 | ||||
-rw-r--r-- | modules/textadept/command_entry.lua | 30 | ||||
-rw-r--r-- | modules/textadept/editing.lua | 62 | ||||
-rw-r--r-- | modules/textadept/file_types.lua | 2 | ||||
-rw-r--r-- | modules/textadept/find.lua | 71 | ||||
-rw-r--r-- | modules/textadept/history.lua | 28 | ||||
-rw-r--r-- | modules/textadept/init.lua | 20 | ||||
-rw-r--r-- | modules/textadept/keys.lua | 167 | ||||
-rw-r--r-- | modules/textadept/macros.lua | 4 | ||||
-rw-r--r-- | modules/textadept/menu.lua | 43 | ||||
-rw-r--r-- | modules/textadept/run.lua | 30 | ||||
-rw-r--r-- | modules/textadept/session.lua | 16 | ||||
-rw-r--r-- | modules/textadept/snippets.lua | 536 |
17 files changed, 506 insertions, 567 deletions
diff --git a/modules/ansi_c/init.lua b/modules/ansi_c/init.lua index 10a73ff6..3f5288c4 100644 --- a/modules/ansi_c/init.lua +++ b/modules/ansi_c/init.lua @@ -26,11 +26,7 @@ M.tags = { M.autocomplete_snippets = true local XPM = textadept.editing.XPM_IMAGES -local xpms = setmetatable({ - c = XPM.CLASS, d = XPM.SLOT, e = XPM.VARIABLE, f = XPM.METHOD, - g = XPM.TYPEDEF, m = XPM.VARIABLE, s = XPM.STRUCT, t = XPM.TYPEDEF, - v = XPM.VARIABLE -}, {__index = function() return 0 end}) +local xpms = setmetatable({c=XPM.CLASS,d=XPM.SLOT,e=XPM.VARIABLE,f=XPM.METHOD,g=XPM.TYPEDEF,m=XPM.VARIABLE,s=XPM.STRUCT,t=XPM.TYPEDEF,v=XPM.VARIABLE},{__index=function()return 0 end}) textadept.editing.autocompleters.ansi_c = function() -- Retrieve the symbol behind the caret. @@ -85,10 +81,9 @@ textadept.editing.autocompleters.ansi_c = function() return #part, list end -local api_files = textadept.editing.api_files -api_files.ansi_c[#api_files.ansi_c + 1] = _HOME .. '/modules/ansi_c/api' -api_files.ansi_c[#api_files.ansi_c + 1] = _HOME .. '/modules/ansi_c/lua_api' -api_files.ansi_c[#api_files.ansi_c + 1] = _USERHOME .. '/modules/ansi_c/api' +for _, tags in ipairs(M.tags) do + table.insert(textadept.editing.api_files.ansi_c, (tags:gsub('tags$', 'api'))) +end -- Commands. diff --git a/modules/lua/init.lua b/modules/lua/init.lua index d3ef2419..abaa36aa 100644 --- a/modules/lua/init.lua +++ b/modules/lua/init.lua @@ -22,10 +22,8 @@ local function ta_api(filename) local userhome = '^' .. _USERHOME:gsub('%p', '%%%0'):gsub('%%[/\\]', '[/\\]') return function() local buffer_filename = buffer.filename or '' - if buffer_filename:find(home) or buffer_filename:find(userhome) or - buffer == ui.command_entry or buffer._type then - return filename - end + return (buffer_filename:find(home) or buffer_filename:find(userhome) or + buffer == ui.command_entry or buffer._type) and filename end end @@ -73,11 +71,11 @@ textadept.editing.autocompleters.lua = function() local assignment = '%f[%w_]' .. symbol:gsub('(%p)', '%%%1') .. '%s*=%s*(.*)$' for i = buffer:line_from_position(buffer.current_pos) - 1, 1, -1 do local expr = buffer:get_line(i):match(assignment) - if expr then - for patt, type in pairs(M.expr_types) do - if expr:find(patt) then symbol = type break end - end + if not expr then goto continue end + for patt, type in pairs(M.expr_types) do + if expr:find(patt) then symbol = type break end end + ::continue:: end -- Search through ctags for completions for that symbol. local name_patt = '^' .. part @@ -87,14 +85,13 @@ textadept.editing.autocompleters.lua = function() if not filename or not lfs.attributes(filename) then goto continue end for tag_line in io.lines(filename) do local name = tag_line:match('^%S+') - if name:find(name_patt) and not list[name] then - local fields = tag_line:match(';"\t(.*)$') - local k, class = fields:sub(1, 1), fields:match('class:(%S+)') or '' - if class == symbol and (op ~= ':' or k == 'f') then - list[#list + 1] = name .. sep .. xpms[k] - list[name] = true - end + if not name:find(name_patt) or list[name] then goto continue end + local fields = tag_line:match(';"\t(.*)$') + local k, class = fields:sub(1, 1), fields:match('class:(%S+)') or '' + if class == symbol and (op ~= ':' or k == 'f') then + list[#list + 1], list[name] = name .. sep .. xpms[k], true end + ::continue:: end ::continue:: end @@ -102,10 +99,10 @@ textadept.editing.autocompleters.lua = function() return #part, list end -local api_files = textadept.editing.api_files -api_files.lua[#api_files.lua + 1] = _HOME .. '/modules/lua/api' -api_files.lua[#api_files.lua + 1] = _USERHOME .. '/modules/lua/api' -api_files.lua[#api_files.lua + 1] = ta_api(_HOME .. '/modules/lua/ta_api') +local api_files = textadept.editing.api_files.lua +table.insert(api_files, _HOME .. '/modules/lua/api') +table.insert(api_files, _USERHOME .. '/modules/lua/api') +table.insert(api_files, ta_api(_HOME .. '/modules/lua/ta_api')) -- Commands. diff --git a/modules/lua/ta_api b/modules/lua/ta_api index c5e573e6..631fcb61 100644 --- a/modules/lua/ta_api +++ b/modules/lua/ta_api @@ -340,7 +340,7 @@ args _G.args (module)\nProcesses command line arguments for Textadept. ascii lexer.ascii (pattern)\nA pattern that matches any ASCII character (codes 0 to 127). assert _G.assert (module)\nExtends `_G` with formatted assertions and function argument type checks. assert _G.assert(v, message, ...)\nAsserts that value *v* is not `false` or `nil` and returns *v*, or calls\n`error()` with *message* as the error message, defaulting to "assertion\nfailed!". If *message* is a format string, the remaining arguments are passed\nto `string.format()` and the resulting string becomes the error message.\n@param v Value to assert.\n@param message Optional error message to show on error. The default value is\n "assertion failed!".\n@param ... If *message* is a format string, these arguments are passed to\n `string.format()`. -assert_type _G.assert_type(v, expected_type, narg)\nAsserts that value *v* has type string *expected_type* and returns *v*, or\ncalls `error()` with an error message that implicates function argument\nnumber *narg*.\nThis is intended to be used with API function arguments so users receive more\nhelpful error messages.\n@param v Value to assert the type of.\n@param expected_type String type to assert. It may be a punctuation-delimited\n list of type options.\n@param narg The positional argument number *v* is associated with. This is\n not required to be a number.\n@usage assert_type(filename, 'string/nil', 1)\n@usage assert_type(option.setting, 'number', 'setting') -- implicates key +assert_type _G.assert_type(v, expected_type, narg)\nAsserts that value *v* has type string *expected_type* and returns *v*, or\ncalls `error()` with an error message that implicates function argument\nnumber *narg*.\nThis is intended to be used with API function arguments so users receive more\nhelpful error messages.\n@param v Value to assert the type of.\n@param expected_type String type to assert. It may be a non-letter-delimited\n list of type options.\n@param narg The positional argument number *v* is associated with. This is\n not required to be a number.\n@usage assert_type(filename, 'string/nil', 1)\n@usage assert_type(option.setting, 'number', 'setting') -- implicates key auto_c_active buffer.auto_c_active(buffer)\nReturns whether or not an autocompletion or user list is visible.\n@param buffer A buffer.\n@return bool auto_c_auto_hide buffer.auto_c_auto_hide (bool)\nAutomatically cancel an autocompletion or user list when no entries match\ntyped text.\nThe default value is `true`. auto_c_cancel buffer.auto_c_cancel(buffer)\nCancels the displayed autocompletion or user list.\n@param buffer A buffer. @@ -770,7 +770,7 @@ property_int lexer.property_int (table, Read-only)\nMap of key-value pairs with property_int view.property_int (table, Read-only)\nMap of key-value pairs used by lexers with values interpreted as numbers,\nor `0` if not found. punct lexer.punct (pattern)\nA pattern that matches any punctuation character ('!' to '/', ':' to '@',\n'[' to ''', '{' to '~'). punctuation_chars buffer.punctuation_chars (string)\nThe string set of characters recognized as punctuation characters.\nSet this only after setting `buffer.word_chars`.\nThe default value is a string that contains all non-word and non-whitespace\ncharacters. -quick_open io.quick_open(paths, filter, opts)\nPrompts the user to select files to be opened from *paths*, a string\ndirectory path or list of directory paths, using a filtered list dialog.\nIf *paths* is `nil`, uses the current project's root directory, which is\nobtained from `io.get_project_root()`.\nString or list *filter* determines which files to show in the dialog, with\nthe default filter being `io.quick_open_filters[path]` (if it exists) or\n`lfs.default_filter`. A filter consists of Lua patterns that match file and\ndirectory paths to include or exclude. Patterns are inclusive by default.\nExclusive patterns begin with a '!'. If no inclusive patterns are given, any\npath is initially considered. As a convenience, file extensions can be\nspecified literally instead of as a Lua pattern (e.g. '.lua' vs. '%.lua$'),\nand '/' also matches the Windows directory separator ('[/\\]' is not needed).\nThe number of files in the list is capped at `quick_open_max`.\nIf *filter* is `nil` and *paths* is ultimately a string, the filter from the\n`io.quick_open_filters` table is used. If that filter does not exist,\n`lfs.default_filter` is used.\n*opts* is an optional table of additional options for\n`ui.dialogs.filteredlist()`.\n@param paths Optional string directory path or table of directory paths to\n search. The default value is the current project's root directory, if\n available.\n@param filter Optional filter for files and directories to include and/or\n exclude. The default value is `lfs.default_filter` unless *paths* is a\n string and a filter for it is defined in `io.quick_open_filters`.\n@param opts Optional table of additional options for\n `ui.dialogs.filteredlist()`.\n@usage io.quick_open(buffer.filename:match('^(.+)[/\\]')) -- list all files\n in the current file's directory, subject to the default filter\n@usage io.quick_open(io.get_current_project(), '.lua') -- list all Lua files\n in the current project\n@usage io.quick_open(io.get_current_project(), '!/build') -- list all files\n in the current project except those in the build directory\n@see io.quick_open_filters\n@see lfs.default_filter\n@see quick_open_max\n@see ui.dialogs.filteredlist +quick_open io.quick_open(paths, filter, opts)\nPrompts the user to select files to be opened from *paths*, a string\ndirectory path or list of directory paths, using a filtered list dialog.\nIf *paths* is `nil`, uses the current project's root directory, which is\nobtained from `io.get_project_root()`.\nString or list *filter* determines which files to show in the dialog, with\nthe default filter being `io.quick_open_filters[path]` (if it exists) or\n`lfs.default_filter`. A filter consists of Lua patterns that match file and\ndirectory paths to include or exclude. Patterns are inclusive by default.\nExclusive patterns begin with a '!'. If no inclusive patterns are given, any\npath is initially considered. As a convenience, file extensions can be\nspecified literally instead of as a Lua pattern (e.g. '.lua' vs. '%.lua$'),\nand '/' also matches the Windows directory separator ('[/\\]' is not needed).\nThe number of files in the list is capped at `quick_open_max`.\nIf *filter* is `nil` and *paths* is ultimately a string, the filter from the\n`io.quick_open_filters` table is used. If that filter does not exist,\n`lfs.default_filter` is used.\n*opts* is an optional table of additional options for\n`ui.dialogs.filteredlist()`.\n@param paths Optional string directory path or table of directory paths to\n search. The default value is the current project's root directory, if\n available.\n@param filter Optional filter for files and directories to include and/or\n exclude. The default value is `lfs.default_filter` unless a filter for\n *paths* is defined in `io.quick_open_filters`.\n@param opts Optional table of additional options for\n `ui.dialogs.filteredlist()`.\n@usage io.quick_open(buffer.filename:match('^(.+)[/\\]')) -- list all files\n in the current file's directory, subject to the default filter\n@usage io.quick_open(io.get_current_project(), '.lua') -- list all Lua files\n in the current project\n@usage io.quick_open(io.get_current_project(), '!/build') -- list all files\n in the current project except those in the build directory\n@see io.quick_open_filters\n@see lfs.default_filter\n@see quick_open_max\n@see ui.dialogs.filteredlist quick_open_filters io.quick_open_filters (table)\nMap of directory paths to filters used by `io.quick_open()`.\n@see quick_open quick_open_max io.quick_open_max (number)\nThe maximum number of files listed in the quick open dialog.\nThe default value is `1000`. quit _G.quit()\nEmits a `QUIT` event, and unless any handler returns `false`, quits\nTextadept.\n@see events.QUIT @@ -857,6 +857,7 @@ selection_n_start_virtual_space buffer.selection_n_start_virtual_space (number, selection_start buffer.selection_start (number)\nThe position of the beginning of the selected text.\nWhen set, becomes the anchor, but is not scrolled into view. selections buffer.selections (number, Read-only)\nThe number of active selections. There is always at least one selection. session textadept.session (module)\nSession support for Textadept. +set_arguments textadept.run.set_arguments(filename, run, compile)\nAppends the command line argument strings *run* and *compile* to their\nrespective run and compile commands for file *filename* or the current file.\nIf either is `nil`, prompts the user for missing the arguments. Each filename\nhas its own set of compile and run arguments.\n@param filename Optional path to the file to set run/compile arguments for.\n@param run Optional string run arguments to set. If `nil`, the user is\n prompted for them. Pass the empty string for no run arguments.\n@param compile Optional string compile arguments to set. If `nil`, the user\n is prompted for them. Pass the empty string for no compile arguments.\n@see run_commands\n@see compile_commands set_chars_default buffer.set_chars_default(buffer)\nResets `buffer.word_chars`, `buffer.whitespace_chars`, and\n`buffer.punctuation_chars` to their respective defaults.\n@param buffer A buffer.\n@see word_chars\n@see whitespace_chars\n@see punctuation_chars set_default_fold_display_text view.set_default_fold_display_text(view, text)\nSets the default fold display text to string *text*.\n@param view A view.\n@param text The text to display by default next to folded lines.\n@see toggle_fold_show_text set_empty_selection buffer.set_empty_selection(buffer, pos)\nMoves the caret to position *pos* without scrolling the view and removes any\nselections.\n@param buffer A buffer\n@param pos The position in *buffer* to move to. diff --git a/modules/lua/ta_tags b/modules/lua/ta_tags index a213f6ef..e186e8e3 100644 --- a/modules/lua/ta_tags +++ b/modules/lua/ta_tags @@ -859,6 +859,7 @@ selection_n_start_virtual_space _HOME/core/.buffer.luadoc /^module('buffer')$/;" selection_start _HOME/core/.buffer.luadoc /^module('buffer')$/;" F class:buffer selections _HOME/core/.buffer.luadoc /^module('buffer')$/;" F class:buffer session _HOME/modules/textadept/session.lua /^module('textadept.session')]]$/;" m class:textadept +set_arguments _HOME/modules/textadept/run.lua /^function M.set_arguments(filename, run, compile)$/;" f class:textadept.run set_chars_default _HOME/core/.buffer.luadoc /^function set_chars_default(buffer) end$/;" f class:buffer set_default_fold_display_text _HOME/core/.view.luadoc /^function set_default_fold_display_text(view, text) end$/;" f class:view set_empty_selection _HOME/core/.buffer.luadoc /^function set_empty_selection(buffer, pos) end$/;" f class:buffer diff --git a/modules/textadept/bookmarks.lua b/modules/textadept/bookmarks.lua index c21f5dc4..de6f7470 100644 --- a/modules/textadept/bookmarks.lua +++ b/modules/textadept/bookmarks.lua @@ -28,10 +28,10 @@ function M.clear() buffer:marker_delete_all(M.MARK_BOOKMARK) end -- Returns an iterator for all bookmarks in the given buffer. local function bookmarks(buffer) - return function(_, line) + return function(buffer, line) line = buffer:marker_next(line + 1, 1 << M.MARK_BOOKMARK - 1) return line >= 1 and line or nil - end, nil, 0 + end, buffer, 0 end --- @@ -46,17 +46,17 @@ end function M.goto_mark(next) if next ~= nil then local f = next and buffer.marker_next or buffer.marker_previous - local current_line = buffer:line_from_position(buffer.current_pos) + local line = buffer:line_from_position(buffer.current_pos) local BOOKMARK_BIT = 1 << M.MARK_BOOKMARK - 1 - local line = f(buffer, current_line + (next and 1 or -1), BOOKMARK_BIT) + line = f(buffer, line + (next and 1 or -1), BOOKMARK_BIT) if line == -1 then line = f(buffer, (next and 1 or buffer.line_count), BOOKMARK_BIT) end if line >= 1 then textadept.editing.goto_line(line) end return end - local scan_this_buffer, utf8_list, buffers = true, {}, {} -- List the current buffer's marks, and then all other buffers' marks. + local scan_this_buffer, utf8_list, buffers = true, {}, {} ::rescan:: for _, buffer in ipairs(_BUFFERS) do if not (scan_this_buffer == (buffer == _G.buffer)) then goto continue end diff --git a/modules/textadept/command_entry.lua b/modules/textadept/command_entry.lua index 18f72e20..3e344f81 100644 --- a/modules/textadept/command_entry.lua +++ b/modules/textadept/command_entry.lua @@ -59,8 +59,7 @@ end -- @name editing_keys M.editing_keys = {__index = {}} --- Fill in default platform-specific key bindings. -local ekeys, plat = M.editing_keys.__index, CURSES and 3 or OSX and 2 or 1 +-- Fill in default key bindings for Linux/Win32, macOS, Terminal. local bindings = { -- Note: cannot use `M.cut`, `M.copy`, etc. since M is never considered the -- global buffer. @@ -81,8 +80,9 @@ local bindings = { [function() M:line_end() end] = {nil, 'ctrl+e', 'ctrl+e'}, [function() M:clear() end] = {nil, 'ctrl+d', 'ctrl+d'} } +local plat = CURSES and 3 or OSX and 2 or 1 for f, plat_keys in pairs(bindings) do - if plat_keys[plat] then ekeys[plat_keys[plat]] = f end + if plat_keys[plat] then M.editing_keys.__index[plat_keys[plat]] = f end end -- Environment for abbreviated Lua commands. @@ -100,13 +100,13 @@ local env = setmetatable({}, { __newindex = function(self, k, v) local ok, value = pcall(function() return buffer[k] end) if ok and value ~= nil or not ok and value:find('write-only property') then - buffer[k] = v + buffer[k] = v -- buffer and view are interchangeable in this case return end if view[k] ~= nil then view[k] = v return end if ui[k] ~= nil then ui[k] = v return end rawset(self, k, v) - end, + end }) -- Executes string *code* as Lua code that is subject to an "abbreviated" @@ -135,7 +135,7 @@ local function run_lua(code) end end if result ~= nil or code:find('^return ') then ui.print(result) end - events.emit(events.UPDATE_UI, 0) + events.emit(events.UPDATE_UI, 1) -- update UI if necessary (e.g. statusbar) end args.register('-e', '--execute', 1, run_lua, 'Execute Lua code') @@ -151,8 +151,8 @@ local function complete_lua() if (not ok or type(result) ~= 'table') and symbol ~= '' then return end local cmpls = {} part = '^' .. part - local sep = string.char(M.auto_c_type_separator) local XPM = textadept.editing.XPM_IMAGES + local sep = string.char(M.auto_c_type_separator) if not ok or symbol == 'buffer' or symbol == 'view' then local sci = _SCINTILLA local global_envs = not ok and { @@ -161,11 +161,11 @@ local function complete_lua() } or op == ':' and {sci.functions} or {sci.properties, sci.constants} for _, env in ipairs(global_envs) do for k, v in pairs(env) do - if type(k) == 'string' and k:find(part) then - local xpm = (type(v) == 'function' or env == sci.functions) and - XPM.METHOD or XPM.VARIABLE - cmpls[#cmpls + 1] = k .. sep .. xpm - end + if type(k) ~= 'string' or not k:find(part) then goto continue end + local xpm = (type(v) == 'function' or env == sci.functions) and + XPM.METHOD or XPM.VARIABLE + cmpls[#cmpls + 1] = k .. sep .. xpm + ::continue:: end end else @@ -216,7 +216,6 @@ local prev_key_mode -- @usage ui.command_entry.run(ui.print) -- @name run function M.run(f, keys, lang, height) - if M:auto_c_active() then M:auto_c_cancel() end -- may happen in curses if not assert_type(f, 'function/nil', 1) and not keys then f, keys, lang = run_lua, lua_keys, 'lua' elseif type(assert_type(keys, 'table/string/nil', 2)) == 'string' then @@ -231,9 +230,8 @@ function M.run(f, keys, lang, height) keys['\n'] = function() if M:auto_c_active() then return false end -- allow Enter to autocomplete M.focus() -- hide - if not f then return end M.append_history(M:get_text()) - f(M:get_text()) + if f then f(M:get_text()) end end end if not getmetatable(keys) then setmetatable(keys, M.editing_keys) end @@ -242,7 +240,7 @@ function M.run(f, keys, lang, height) local mode_history = history[history.mode] M:set_text(mode_history and mode_history[mode_history.pos] or '') M:select_all() - prev_key_mode = _G.keys.mode + prev_key_mode = _G.keys.mode -- save before M.focus() M.focus() M:set_lexer(lang or 'text') M.height = M:text_height(1) * (height or 1) diff --git a/modules/textadept/editing.lua b/modules/textadept/editing.lua index 6eda7a32..0c35942e 100644 --- a/modules/textadept/editing.lua +++ b/modules/textadept/editing.lua @@ -155,8 +155,7 @@ events.connect(events.KEYPRESS, function(code) local pos = buffer.selection_n_caret[i] local complement = M.auto_pairs[buffer.char_at[pos - 1]] if complement and buffer.char_at[pos] == string.byte(complement) then - buffer:set_target_range(pos, pos + 1) - buffer:replace_target('') + buffer:delete_range(pos, 1) end end buffer:end_undo_action() @@ -166,14 +165,13 @@ end, 1) -- need index of 1 because default key handler halts propagation -- Highlights matching braces. events.connect(events.UPDATE_UI, function(updated) if updated & 3 == 0 then return end -- ignore scrolling - local pos = buffer.selection_n_caret[buffer.main_selection] - if M.brace_matches[buffer.char_at[pos]] then - local match = buffer:brace_match(pos, 0) + if M.brace_matches[buffer.char_at[buffer.current_pos]] then + local match = buffer:brace_match(buffer.current_pos, 0) local f = match ~= -1 and view.brace_highlight or view.brace_bad_light - f(buffer, pos, match) - return + f(buffer, buffer.current_pos, match) + else + view:brace_bad_light(-1) end - view:brace_bad_light(-1) end) -- Clears highlighted word indicators. @@ -221,10 +219,10 @@ events.connect(events.KEYPRESS, function(code) local handled = false for i = 1, buffer.selections do local s, e = buffer.selection_n_start[i], buffer.selection_n_end[i] - if s == e and buffer.char_at[s] == code then - buffer.selection_n_start[i], buffer.selection_n_end[i] = s + 1, s + 1 - handled = true - end + if s ~= e or buffer.char_at[s] ~= code then goto continue end + buffer.selection_n_start[i], buffer.selection_n_end[i] = s + 1, s + 1 + handled = true + ::continue:: end if handled then return true end -- prevent typing end @@ -414,9 +412,9 @@ function M.goto_line(line) view:ensure_visible_enforce_policy(line) buffer:goto_line(line) end -args.register( - '-l', '--line', 1, function(line) M.goto_line(tonumber(line) or line) end, - 'Go to line') +args.register('-l', '--line', 1, function(line) + M.goto_line(tonumber(line) or line) +end, 'Go to line') --- -- Transposes characters intelligently. @@ -424,7 +422,6 @@ args.register( -- the caret. Otherwise, the characters to the left and right are. -- @name transpose_chars function M.transpose_chars() - if buffer.current_pos == 1 then return end local pos = buffer.current_pos local line_end = buffer.line_end_position[buffer:line_from_position(pos)] if pos == line_end then pos = buffer:position_before(pos) end @@ -525,10 +522,9 @@ function M.select_enclosed(left, right) s = s - 1 end end - if s >= 1 and e >= 1 then - if s + #left == anchor and e == pos then s, e = s - #left, e + #right end - buffer:set_sel(s + #left, e) - end + if s == -1 or e == -1 then return end + if s + #left == anchor and e == pos then s, e = s - #left, e + #right end + buffer:set_sel(s + #left, e) end --- @@ -545,7 +541,7 @@ function M.select_word(all) buffer.search_flags = buffer.FIND_MATCHCASE if buffer.selection_empty or buffer:is_range_word(buffer.selection_start, buffer.selection_end) then - buffer.search_flags = buffer.search_flags + buffer.FIND_WHOLEWORD + buffer.search_flags = buffer.search_flags | buffer.FIND_WHOLEWORD if all then buffer:multiple_select_add_next() end -- select word first end buffer['multiple_select_add_' .. (not all and 'next' or 'each')](buffer) @@ -590,10 +586,10 @@ function M.convert_indentation() else new_indentation = string.rep(' ', indent) end - if current_indentation ~= new_indentation then - buffer:set_target_range(s, e) - buffer:replace_target(new_indentation) - end + if current_indentation == new_indentation then goto continue end + buffer:set_target_range(s, e) + buffer:replace_target(new_indentation) + ::continue:: end buffer:end_undo_action() end @@ -727,19 +723,19 @@ function M.show_documentation(pos, ignore_case) if symbol ~= '' then local symbol_patt = '^' .. symbol:gsub('(%p)', '%%%1') if ignore_case then - symbol_patt = symbol_patt:gsub('%a', function(ch) - return string.format('[%s%s]', ch:upper(), ch:lower()) + symbol_patt = symbol_patt:gsub('%a', function(letter) + return string.format('[%s%s]', letter:upper(), letter:lower()) end) end for _, file in ipairs(api_files) do if type(file) == 'function' then file = file() end - if file and lfs.attributes(file) then - for line in io.lines(file) do - if line:find(symbol_patt) then - api_docs[#api_docs + 1] = line:match(symbol_patt .. '%s+(.+)$') - end - end + if not file or not lfs.attributes(file) then goto continue end + for line in io.lines(file) do + if not line:find(symbol_patt) then goto continue end + api_docs[#api_docs + 1] = line:match(symbol_patt .. '%s+(.+)$') + ::continue:: end + ::continue:: end end -- Search backwards for an open function call and show API documentation for diff --git a/modules/textadept/file_types.lua b/modules/textadept/file_types.lua index 4bf00465..37743fbd 100644 --- a/modules/textadept/file_types.lua +++ b/modules/textadept/file_types.lua @@ -93,13 +93,13 @@ events.connect(events.VIEW_AFTER_SWITCH, restore_lexer) events.connect(events.VIEW_NEW, restore_lexer) events.connect(events.RESET_AFTER, restore_lexer) +local LEXERNAMES = _SCINTILLA.functions.property_names[1] --- -- Prompts the user to select a lexer for the current buffer. -- @see buffer.set_lexer -- @name select_lexer function M.select_lexer() local lexers = {} - local LEXERNAMES = _SCINTILLA.functions.property_names[1] for name in buffer:private_lexer_call(LEXERNAMES):gmatch('[^\n]+') do lexers[#lexers + 1] = name end diff --git a/modules/textadept/find.lua b/modules/textadept/find.lua index 5590e356..7ea83e18 100644 --- a/modules/textadept/find.lua +++ b/modules/textadept/find.lua @@ -77,8 +77,7 @@ local M = ui.find module('ui.find')]] local _L = _L -M.find_label_text = _L['Find:'] -M.replace_label_text = _L['Replace:'] +M.find_label_text, M.replace_label_text = _L['Find:'], _L['Replace:'] M.find_next_button_text = not CURSES and _L['Find Next'] or _L['[Next]'] M.find_prev_button_text = not CURSES and _L['Find Prev'] or _L['[Prev]'] M.replace_button_text = not CURSES and _L['Replace'] or _L['[Replace]'] @@ -92,7 +91,8 @@ M.highlight_all_matches = false M.INDIC_FIND = _SCINTILLA.next_indic_number() -- Events. -events.FIND_RESULT_FOUND, events.FIND_WRAPPED = 'find_found', 'find_wrapped' +local find_events = {'find_result_found', 'find_wrapped'} +for _, v in ipairs(find_events) do events[v:upper()] = v end -- When finding in files, note the current view since results are shown in a -- split view. Jumping between results should be done in the original view. @@ -113,7 +113,7 @@ M.find_in_files_filters = {} -- text has changed, the user is still typing; if text is the same, the user -- clicked "Find Next" or "Find Prev"). Keep track of repl_text for -- non-"In files" in order to restore it from filter text as necessary. -local find_text, found_text, repl_text = nil, nil, '' +local find_text, found_text, repl_text = nil, nil, ui.find.replace_entry_text -- Returns a reasonable initial directory for use with Find in Files. local function ff_dir() @@ -137,7 +137,7 @@ function M.focus(options) local filter = M.find_in_files_filters[ff_dir()] or lfs.default_filter M.replace_entry_text = type(filter) == 'string' and filter or table.concat(filter, ',') - elseif repl_text and M.replace_entry_text ~= repl_text then + elseif M.replace_entry_text ~= repl_text then M.replace_entry_text = repl_text -- restore end orig_focus() @@ -226,12 +226,10 @@ local function find(text, next, flags, no_wrap, wrapped) view:line_scroll(0, first_visible_line - view.first_visible_line) buffer:goto_pos(incremental_orig_pos or anchor) end - elseif not wrapped then - ui.statusbar_text = '' end -- Count and optionally highlight all found occurrences. - local count, current = 0 + local count, current = 0, 1 clear_highlighted_matches() if pos ~= -1 then buffer.search_flags = flags @@ -260,7 +258,7 @@ end events.connect(events.FIND, find) events.connect(events.FIND_TEXT_CHANGED, function() if not M.incremental then return end - return events.emit(events.FIND, M.find_entry_text, true) or true -- refresh + return events.emit(events.FIND, M.find_entry_text, true) -- refresh end) events.connect( events.FIND_WRAPPED, function() ui.statusbar_text = _L['Search wrapped'] end) @@ -341,24 +339,24 @@ function M.find_in_files(dir, filter) found = true if binary == nil then binary = buffer:text_range(1, 65536):find('\0') end if binary then - _G.buffer:append_text(string.format( + _G.buffer:add_text(string.format( '%s:1:%s\n', utf8_filenames[i], _L['Binary file matches.'])) break end local line_num = buffer:line_from_position(buffer.target_start) local line = buffer:get_line(line_num) - _G.buffer:append_text( + _G.buffer:add_text( string.format('%s:%d:%s', utf8_filenames[i], line_num, line)) - local pos = _G.buffer.length + 1 - #line + + local pos = _G.buffer.current_pos - #line + buffer.target_start - buffer:position_from_line(line_num) _G.buffer:indicator_fill_range( pos, buffer.target_end - buffer.target_start) - if not line:find('\n$') then _G.buffer:append_text('\n') end + if not line:find('\n$') then _G.buffer:add_text('\n') end buffer:set_target_range(buffer.target_end, buffer.length + 1) end buffer:clear_all() buffer:empty_undo_buffer() - _G.buffer:goto_pos(_G.buffer.length + 1) -- [Files Found Buffer] + view:scroll_caret() -- [Files Found Buffer] i = i + 1 if i > #filenames then return nil end return i * 100 / #filenames, utf8_filenames[i] @@ -370,44 +368,37 @@ function M.find_in_files(dir, filter) not found and _L['No results found'] .. '\n' or '') end --- Unescapes \uXXXX sequences in the string *text* and returns the result. --- Just like with \n, \t, etc., escape sequence interpretation only happens for --- regex search and replace. --- @param text String text to unescape. --- @return unescaped text -local function unescape(text) - return M.regex and text:gsub('%f[\\]\\u(%x%x%x%x)', function(code) - return utf8.char(tonumber(code, 16)) - end) or text -end - -local P, V, upper, lower = lpeg.P, lpeg.V, string.upper, string.lower +local P, V, C, upper, lower = lpeg.P, lpeg.V, lpeg.C, string.upper, string.lower local re_patt = lpeg.Cs(P{ (V('text') + V('u') + V('l') + V('U') + V('L'))^1, text = (1 - '\\' * lpeg.S('uUlLE'))^1, - u = '\\u' * lpeg.C(1) / upper, l = '\\l' * lpeg.C(1) / lower, + u = '\\u' * C(1) / upper, l = '\\l' * C(1) / lower, U = P('\\U') / '' * (V('text') / upper + V('u') + V('l'))^0 * V('E')^-1, L = P('\\L') / '' * (V('text') / lower + V('u') + V('l'))^0 * V('E')^-1, - E = P('\\E') / '', + E = P('\\E') / '' }) --- Replaces the text in the target range with string *text* subject to: +-- Returns string *text* with the following sequences unescaped: +-- * "\uXXXX" sequences replaced with the equivalent UTF-8 character. -- * "\d" sequences replaced with the text of capture number *d* from the --- regular expression (or the entire match for *d* = 0) +-- regular expression (or the entire match for *d* = 0). -- * "\U" and "\L" sequences convert everything up to the next "\U", "\L", or -- "\E" to uppercase and lowercase, respectively. -- * "\u" and "\l" sequences convert the next character to uppercase and -- lowercase, respectively. They may appear within "\U" and "\L" constructs. -local function replace_target_re(buffer, rtext) - rtext = rtext:gsub('\\0', buffer.target_text):gsub('\\(%d)', buffer.tag) - buffer:replace_target(lpeg.match(re_patt, rtext) or rtext) +-- @param text String text to unescape. +-- @return unescaped text +local function unescape(text) + text = text:gsub('%f[\\]\\u(%x%x%x%x)', function(code) + return utf8.char(tonumber(code, 16)) + end):gsub('\\0', buffer.target_text):gsub('\\(%d)', buffer.tag) + return re_patt:match(text) or text end -- Replaces found (selected) text. -events.connect(events.REPLACE, function(rtext) +events.connect(events.REPLACE, function(text) if buffer.selection_empty then return end buffer:target_from_selection() - local f = not M.regex and buffer.replace_target or replace_target_re - f(buffer, unescape(rtext)) + buffer:replace_target(not M.regex and text or unescape(text)) buffer:set_sel(buffer.target_start, buffer.target_end) end) @@ -426,8 +417,7 @@ events.connect(events.REPLACE_ALL, function(ftext, rtext) buffer:indicator_fill_range(e, 1) end local EOF = replace_in_sel and e == buffer.length + 1 -- no indicator at EOF - local f = not M.regex and buffer.replace_target or replace_target_re - rtext, repl_text = unescape(rtext), rtext -- save for ui.find.focus() + repl_text = rtext -- save for ui.find.focus() -- Perform the search and replace. buffer:begin_undo_action() @@ -436,7 +426,7 @@ events.connect(events.REPLACE_ALL, function(ftext, rtext) while buffer:search_in_target(ftext) ~= -1 and (not replace_in_sel or buffer.target_end <= buffer:indicator_end(INDIC_REPLACE, s) or EOF) do if buffer.target_start == buffer.target_end then break end -- prevent loops - f(buffer, rtext) + buffer:replace_target(not M.regex and rtext or unescape(rtext)) count = count + 1 buffer:set_target_range(buffer.target_end, buffer.length + 1) end @@ -493,8 +483,7 @@ function M.goto_file_found(line_num, next) local line = buffer:get_cur_line() local utf8_filename, pos utf8_filename, line_num, pos = line:match('^(.+):(%d+):()') - if not utf8_filename then return end - line_num = tonumber(line_num) + if not utf8_filename then return else line_num = tonumber(line_num) end textadept.editing.select_line() pos = buffer.selection_start + pos - 1 -- absolute pos of result text on line local s = buffer:indicator_end(M.INDIC_FIND, buffer.selection_start) diff --git a/modules/textadept/history.lua b/modules/textadept/history.lua index 4729322c..d619363f 100644 --- a/modules/textadept/history.lua +++ b/modules/textadept/history.lua @@ -34,21 +34,19 @@ local view_history = setmetatable({}, {__index = function(t, view) end}) -- Listens for text insertion and deletion events and records their locations. -events.connect(events.MODIFIED, function(position, mod_type, text, length) +events.connect(events.MODIFIED, function(position, mod, text, length) local buffer = buffer -- Only interested in text insertion or deletion. - if mod_type & buffer.MOD_INSERTTEXT > 0 then + if mod & buffer.MOD_INSERTTEXT > 0 then if length == buffer.length then return end -- ignore file loading position = position + length - elseif mod_type & buffer.MOD_DELETETEXT > 0 then + elseif mod & buffer.MOD_DELETETEXT > 0 then if buffer.length == 0 then return end -- ignore replacing buffer contents else return end -- Ignore undo/redo. - if mod_type & (buffer.PERFORMED_UNDO | buffer.PERFORMED_REDO) > 0 then - return - end + if mod & (buffer.PERFORMED_UNDO | buffer.PERFORMED_REDO) > 0 then return end M.record(nil, buffer:line_from_position(position), buffer.column[position]) end) @@ -56,13 +54,11 @@ end) -- forwards. local jumping = false --- Jumps to the current position in the current view's history after adjusting --- that position backwards or forwards. -local function goto_record() +-- Jumps to the given record in the current view's history. +-- @param record History record to jump to. +local function jump(record) jumping = true - local history = view_history[view] - local record = history[history.pos] - local filename, line, column = record.filename, record.line, record.column + local filename = record.filename if lfs.attributes(filename) then io.open_file(filename) else @@ -75,7 +71,7 @@ local function goto_record() end end end - buffer:goto_pos(buffer:find_column(line, column)) + buffer:goto_pos(buffer:find_column(record.line, record.column)) jumping = false end @@ -91,13 +87,13 @@ function M.back() math.abs(record.line - line) > M.minimum_line_distance then -- When navigated away from the most recent record, and if that record is -- not a soft record, jump back to it first, then navigate backwards. - if not record.soft then goto_record() return end + if not record.soft then jump(record) return end -- Otherwise, update the soft record with the current position and -- immediately navigate backwards. M.record(record.filename, nil, nil, record.soft) end if history.pos > 1 then history.pos = history.pos - 1 end - goto_record() + jump(history[history.pos]) end --- @@ -109,7 +105,7 @@ function M.forward() local record = history[history.pos] if record.soft then M.record(record.filename, nil, nil, record.soft) end history.pos = history.pos + 1 - goto_record() + jump(history[history.pos]) end --- diff --git a/modules/textadept/init.lua b/modules/textadept/init.lua index 435b34dd..0195ae31 100644 --- a/modules/textadept/init.lua +++ b/modules/textadept/init.lua @@ -9,19 +9,11 @@ textadept = M -- forward declaration -- It provides utilities for editing text in Textadept. module('textadept')]] -M.bookmarks = require('textadept.bookmarks') -require('textadept.command_entry') -M.editing = require('textadept.editing') -M.file_types = require('textadept.file_types') -require('textadept.find') -M.history = require('textadept.history') -M.macros = require('textadept.macros') -M.run = require('textadept.run') -M.session = require('textadept.session') -M.snippets = require('textadept.snippets') - --- These need to be loaded last. -M.menu = require('textadept.menu') -M.keys = require('textadept.keys') +local modules = { + 'bookmarks', 'command_entry', 'editing', 'file_types', 'find', 'history', + 'macros', 'run', 'session', 'snippets', --[[need to be last]] 'menu', 'keys' +} +for _, name in ipairs(modules) do M[name] = require('textadept.' .. name) end +M.command_entry, M.find = nil, nil -- ui.command_entry, ui.find return M diff --git a/modules/textadept/keys.lua b/modules/textadept/keys.lua index 25db537b..eb5c9301 100644 --- a/modules/textadept/keys.lua +++ b/modules/textadept/keys.lua @@ -271,17 +271,16 @@ module('textadept.keys')]] -- Control, Meta, and 'a' = 'ctrl+meta+a' local _L = _L -local m_edit = textadept.menu.menubar[_L['Edit']] -local m_sel, m_seln = m_edit[_L['Select']], m_edit[_L['Selection']] -local m_search = textadept.menu.menubar[_L['Search']] -local m_tools = textadept.menu.menubar[_L['Tools']] -local m_bookmark = m_tools[_L['Bookmarks']] -local m_qopen = m_tools[_L['Quick Open']] -local m_snippets = m_tools[_L['Snippets']] -local m_buffer = textadept.menu.menubar[_L['Buffer']] -local m_indentation = m_buffer[_L['Indentation']] -local m_view = textadept.menu.menubar[_L['View']] -local m_help = textadept.menu.menubar[_L['Help']] +-- Returns the menu command associated with the '/'-separated string of menu +-- labels. +-- Labels are automatically localized. +-- @param labels Path to the menu command. +-- @usage m('Edit/Select/Select in XML Tag') +local function m(labels) + local menu = textadept.menu.menubar + for label in labels:gmatch('[^/]+') do menu = menu[_L[label]] end + return menu[2] +end -- Bindings for Linux/Win32, macOS, Terminal. local bindings = { @@ -308,34 +307,33 @@ local bindings = { [textadept.editing.paste_reindent] = {'ctrl+V', 'cmd+V', 'meta+v'}, [buffer.line_duplicate] = {'ctrl+d', 'cmd+d', nil}, [buffer.clear] = {'del', {'del', 'ctrl+d'}, {'del', 'ctrl+d'}}, - [m_edit[_L['Delete Word']][2]] = - {'alt+del', 'ctrl+del', {'meta+del', 'meta+d'}}, + [m('Edit/Delete Word')] = {'alt+del', 'ctrl+del', {'meta+del', 'meta+d'}}, [buffer.select_all] = {'ctrl+a', 'cmd+a', 'meta+a'}, - [m_edit[_L['Match Brace']][2]] = {'ctrl+m', 'ctrl+m', 'meta+m'}, - [m_edit[_L['Complete Word']][2]] = + [m('Edit/Match Brace')] = {'ctrl+m', 'ctrl+m', 'meta+m'}, + [m('Edit/Complete Word')] = {'ctrl+\n', 'ctrl+esc', {'ctrl+meta+j', 'ctrl+\n'}}, [textadept.editing.toggle_comment] = {'ctrl+/', 'ctrl+/', 'meta+/'}, [textadept.editing.transpose_chars] = {'ctrl+t', 'ctrl+t', 'ctrl+t'}, [textadept.editing.join_lines] = {'ctrl+J', 'ctrl+j', 'meta+j'}, - [m_edit[_L['Filter Through']][2]] = {'ctrl+|', 'cmd+|', 'ctrl+\\'}, + [m('Edit/Filter Through')] = {'ctrl+|', 'cmd+|', 'ctrl+\\'}, -- Select. - [m_sel[_L['Select between Matching Delimiters']][2]] = + [m('Edit/Select/Select between Matching Delimiters')] = {'ctrl+M', 'ctrl+M', 'meta+M'}, - [m_sel[_L['Select between XML Tags']][2]] = {'ctrl+<', 'cmd+<', 'meta+<'}, - [m_sel[_L['Select in XML Tag']][2]] = {'ctrl+>', 'cmd+>', nil}, + [m('Edit/Select/Select between XML Tags')] = {'ctrl+<', 'cmd+<', 'meta+<'}, + [m('Edit/Select/Select in XML Tag')] = {'ctrl+>', 'cmd+>', nil}, [textadept.editing.select_word] = {'ctrl+D', 'cmd+D', 'meta+W'}, [textadept.editing.select_line] = {'ctrl+N', 'cmd+N', 'meta+N'}, [textadept.editing.select_paragraph] = {'ctrl+P', 'cmd+P', 'meta+P'}, -- Selection. [buffer.upper_case] = {'ctrl+alt+u', 'ctrl+u', 'ctrl+meta+u'}, [buffer.lower_case] = {'ctrl+alt+U', 'ctrl+U', 'ctrl+meta+l'}, - [m_seln[_L['Enclose as XML Tags']][2]] = {'alt+<', 'ctrl+<', 'meta+>'}, - [m_seln[_L['Enclose as Single XML Tag']][2]] = {'alt+>', 'ctrl+>', nil}, - [m_seln[_L['Enclose in Single Quotes']][2]] = {"alt+'", "ctrl+'", nil}, - [m_seln[_L['Enclose in Double Quotes']][2]] = {'alt+"', 'ctrl+"', nil}, - [m_seln[_L['Enclose in Parentheses']][2]] = {'alt+(', 'ctrl+(', 'meta+)'}, - [m_seln[_L['Enclose in Brackets']][2]] = {'alt+[', 'ctrl+[', 'meta+]'}, - [m_seln[_L['Enclose in Braces']][2]] = {'alt+{', 'ctrl+{', 'meta+}'}, + [m('Edit/Selection/Enclose as XML Tags')] = {'alt+<', 'ctrl+<', 'meta+>'}, + [m('Edit/Selection/Enclose as Single XML Tag')] = {'alt+>', 'ctrl+>', nil}, + [m('Edit/Selection/Enclose in Single Quotes')] = {"alt+'", "ctrl+'", nil}, + [m('Edit/Selection/Enclose in Double Quotes')] = {'alt+"', 'ctrl+"', nil}, + [m('Edit/Selection/Enclose in Parentheses')] = {'alt+(', 'ctrl+(', 'meta+)'}, + [m('Edit/Selection/Enclose in Brackets')] = {'alt+[', 'ctrl+[', 'meta+]'}, + [m('Edit/Selection/Enclose in Braces')] = {'alt+{', 'ctrl+{', 'meta+}'}, [buffer.move_selected_lines_up] = {'ctrl+shift+up', 'ctrl+shift+up', 'ctrl+shift+up'}, [buffer.move_selected_lines_down] = @@ -346,10 +344,10 @@ local bindings = { -- TODO: textadept.history.record -- TODO: textadept.history.clear -- Preferences. - [m_edit[_L['Preferences']][2]] = {'ctrl+p', 'cmd+,', 'meta+~'}, + [m('Edit/Preferences')] = {'ctrl+p', 'cmd+,', 'meta+~'}, -- Search. - [m_search[_L['Find']][2]] = {'ctrl+f', 'cmd+f', {'meta+f', 'meta+F'}}, + [m('Search/Find')] = {'ctrl+f', 'cmd+f', {'meta+f', 'meta+F'}}, [ui.find.find_next] = {{'ctrl+g', 'f3'}, 'cmd+g', 'meta+g'}, [ui.find.find_prev] = {{'ctrl+G', 'shift+f3'}, 'cmd+G', 'meta+G'}, [ui.find.replace] = {'ctrl+alt+r', 'ctrl+r', 'meta+r'}, @@ -358,38 +356,37 @@ local bindings = { -- Find Prev is ap when find pane is focused in GUI. -- Replace is ar when find pane is focused in GUI. -- Replace All is aa when find pane is focused in GUI. - [m_search[_L['Find Incremental']][2]] = - {'ctrl+alt+f', 'ctrl+cmd+f', 'ctrl+meta+f'}, - [m_search[_L['Find in Files']][2]] = {'ctrl+F', 'cmd+F', nil}, + [m('Search/Find Incremental')] = {'ctrl+alt+f', 'ctrl+cmd+f', 'ctrl+meta+f'}, + [m('Search/Find in Files')] = {'ctrl+F', 'cmd+F', nil}, -- Find in Files is ai when find pane is focused in GUI. - [m_search[_L['Goto Next File Found']][2]] = {'ctrl+alt+g', 'ctrl+cmd+g', nil}, - [m_search[_L['Goto Previous File Found']][2]] = - {'ctrl+alt+G', 'ctrl+cmd+G', nil}, + [m('Search/Goto Next File Found')] = {'ctrl+alt+g', 'ctrl+cmd+g', nil}, + [m('Search/Goto Previous File Found')] = {'ctrl+alt+G', 'ctrl+cmd+G', nil}, [textadept.editing.goto_line] = {'ctrl+j', 'cmd+j', 'ctrl+j'}, -- Tools. - [m_tools[_L['Command Entry']][2]] = {'ctrl+e', 'cmd+e', 'meta+c'}, - [m_tools[_L['Select Command']][2]] = {'ctrl+E', 'cmd+E', 'meta+C'}, + [m('Tools/Command Entry')] = {'ctrl+e', 'cmd+e', 'meta+c'}, + [m('Tools/Select Command')] = {'ctrl+E', 'cmd+E', 'meta+C'}, [textadept.run.run] = {'ctrl+r', 'cmd+r', 'ctrl+r'}, [textadept.run.compile] = {'ctrl+R', 'cmd+R', 'ctrl+meta+r'}, - [m_tools[_L['Set Arguments...']][2]] = {'ctrl+A', 'cmd+A', nil}, + [textadept.run.set_arguments] = {'ctrl+A', 'cmd+A', nil}, [textadept.run.build] = {'ctrl+B', 'cmd+B', 'ctrl+meta+b'}, [textadept.run.stop] = {'ctrl+X', 'cmd+X', 'ctrl+meta+x'}, - [m_tools[_L['Next Error']][2]] = {'ctrl+alt+e', 'ctrl+cmd+e', 'meta+x'}, - [m_tools[_L['Previous Error']][2]] = {'ctrl+alt+E', 'ctrl+cmd+E', 'meta+X'}, + [m('Tools/Next Error')] = {'ctrl+alt+e', 'ctrl+cmd+e', 'meta+x'}, + [m('Tools/Previous Error')] = {'ctrl+alt+E', 'ctrl+cmd+E', 'meta+X'}, -- Bookmark. [textadept.bookmarks.toggle] = {'ctrl+f2', 'cmd+f2', 'f1'}, [textadept.bookmarks.clear] = {'ctrl+shift+f2', 'cmd+shift+f2', 'f6'}, - [m_bookmark[_L['Next Bookmark']][2]] = {'f2', 'f2', 'f2'}, - [m_bookmark[_L['Previous Bookmark']][2]] = {'shift+f2', 'shift+f2', 'f3'}, + [m('Tools/Bookmarks/Next Bookmark')] = {'f2', 'f2', 'f2'}, + [m('Tools/Bookmarks/Previous Bookmark')] = {'shift+f2', 'shift+f2', 'f3'}, [textadept.bookmarks.goto_mark] = {'alt+f2', 'alt+f2', 'f4'}, -- Macros. [textadept.macros.record] = {'f9', 'f9', 'f9'}, [textadept.macros.play] = {'shift+f9', 'shift+f9', 'f10'}, -- Quick Open. - [m_qopen[_L['Quickly Open User Home']][2]] = {'ctrl+u', 'cmd+u', 'ctrl+u'}, - -- TODO: m_qopen[_L['Quickly Open Textadept Home']][2] - [m_qopen[_L['Quickly Open Current Directory']][2]] = + [m('Tools/Quick Open/Quickly Open User Home')] = + {'ctrl+u', 'cmd+u', 'ctrl+u'}, + -- TODO: m('Tools/Quickly Open Textadept Home') + [m('Tools/Quick Open/Quickly Open Current Directory')] = {'ctrl+alt+O', 'ctrl+cmd+O', 'meta+O'}, [io.quick_open] = {'ctrl+alt+P', 'ctrl+cmd+P', 'ctrl+meta+p'}, -- Snippets. @@ -397,59 +394,58 @@ local bindings = { [textadept.snippets.insert] = {'\t', '\t', '\t'}, [textadept.snippets.previous] = {'shift+\t', 'shift+\t', 'shift+\t'}, [textadept.snippets.cancel_current] = {'esc', 'esc', 'esc'}, - [m_snippets[_L['Complete Trigger Word']][2]] = {'ctrl+k', 'alt+\t', 'meta+k'}, + [m('Tools/Snippets/Complete Trigger Word')] = {'ctrl+k', 'alt+\t', 'meta+k'}, -- Other. - [m_tools[_L['Complete Symbol']][2]] = {'ctrl+ ', 'alt+esc', 'ctrl+ '}, + [m('Tools/Complete Symbol')] = {'ctrl+ ', 'alt+esc', 'ctrl+ '}, [textadept.editing.show_documentation] = {'ctrl+h', 'ctrl+h', {'meta+h', 'meta+H'}}, - [m_tools[_L['Show Style']][2]] = {'ctrl+i', 'cmd+i', 'meta+I'}, + [m('Tools/Show Style')] = {'ctrl+i', 'cmd+i', 'meta+I'}, -- Buffer. - [m_buffer[_L['Next Buffer']][2]] = {'ctrl+\t', 'ctrl+\t', 'meta+n'}, - [m_buffer[_L['Previous Buffer']][2]] = - {'ctrl+shift+\t', 'ctrl+shift+\t', 'meta+p'}, + [m('Buffer/Next Buffer')] = {'ctrl+\t', 'ctrl+\t', 'meta+n'}, + [m('Buffer/Previous Buffer')] = {'ctrl+shift+\t', 'ctrl+shift+\t', 'meta+p'}, [ui.switch_buffer] = {'ctrl+b', 'cmd+b', {'meta+b', 'meta+B'}}, -- Indentation. - -- TODO: m_indentation[_L['Tab width: 2']][2] - -- TODO: m_indentation[_L['Tab width: 3']][2] - -- TODO: m_indentation[_L['Tab width: 4']][2] - -- TODO: m_indentation[_L['Tab width: 8']][2] - [m_indentation[_L['Toggle Use Tabs']][2]] = + -- TODO: m('Buffer/Indentation/Tab width: 2') + -- TODO: m('Buffer/Indentation/Tab width: 3') + -- TODO: m('Buffer/Indentation/Tab width: 4') + -- TODO: m('Buffer/Indentation/Tab width: 8') + [m('Buffer/Indentation/Toggle Use Tabs')] = {'ctrl+alt+T', 'ctrl+T', {'meta+t', 'meta+T'}}, [textadept.editing.convert_indentation] = {'ctrl+alt+i', 'ctrl+i', 'meta+i'}, -- EOL Mode. - -- TODO: m_buffer[_L['EOL Mode']][_L['CRLF']][2] - -- TODO: m_buffer[_L['EOL Mode']][_L['LF']][2] + -- TODO: m('Buffer/EOL Mode/CRLF') + -- TODO: m('Buffer/EOL Mode/LF') -- Encoding. - -- TODO: m_buffer[_L['Encoding']][_L['UTF-8 Encoding']][2] - -- TODO: m_buffer[_L['Encoding']][_L['ASCII Encoding']][2] - -- TODO: m_buffer[_L['Encoding']][_L['CP-1252 Encoding']][2] - -- TODO: m_buffer[_L['Encoding']][_L['UTF-16 Encoding']][2] - [m_buffer[_L['Toggle Wrap Mode']][2]] = {'ctrl+alt+\\', 'ctrl+\\', nil}, - [m_buffer[_L['Toggle View Whitespace']][2]] = {'ctrl+alt+S', 'ctrl+S', nil}, + -- TODO: m('Buffer/Encoding/UTF-8 Encoding') + -- TODO: m('Buffer/Encoding/ASCII Encoding') + -- TODO: m('Buffer/Encoding/CP-1252 Encoding') + -- TODO: m('Buffer/Encoding/UTF-16 Encoding') + [m('Buffer/Toggle Wrap Mode')] = {'ctrl+alt+\\', 'ctrl+\\', nil}, + [m('Buffer/Toggle View Whitespace')] = {'ctrl+alt+S', 'ctrl+S', nil}, [textadept.file_types.select_lexer] = {'ctrl+L', 'cmd+L', 'meta+L'}, -- View. - [m_view[_L['Next View']][2]] = {'ctrl+alt+n', 'ctrl+alt+\t', nil}, - [m_view[_L['Previous View']][2]] = {'ctrl+alt+p', 'ctrl+alt+shift+\t', nil}, - [m_view[_L['Split View Horizontal']][2]] = + [m('View/Next View')] = {'ctrl+alt+n', 'ctrl+alt+\t', nil}, + [m('View/Previous View')] = {'ctrl+alt+p', 'ctrl+alt+shift+\t', nil}, + [m('View/Split View Horizontal')] = {{'ctrl+alt+s', 'ctrl+alt+h'}, 'ctrl+s', nil}, - [m_view[_L['Split View Vertical']][2]] = {'ctrl+alt+v', 'ctrl+v', nil}, - [m_view[_L['Unsplit View']][2]] = {'ctrl+alt+w', 'ctrl+w', nil}, - [m_view[_L['Unsplit All Views']][2]] = {'ctrl+alt+W', 'ctrl+W', nil}, - [m_view[_L['Grow View']][2]] = + [m('View/Split View Vertical')] = {'ctrl+alt+v', 'ctrl+v', nil}, + [m('View/Unsplit View')] = {'ctrl+alt+w', 'ctrl+w', nil}, + [m('View/Unsplit All Views')] = {'ctrl+alt+W', 'ctrl+W', nil}, + [m('View/Grow View')] = {{'ctrl+alt++', 'ctrl+alt+='}, {'ctrl++', 'ctrl+='}, nil}, - [m_view[_L['Shrink View']][2]] = {'ctrl+alt+-', 'ctrl+-', nil}, - [m_view[_L['Toggle Current Fold']][2]] = {'ctrl+*', 'cmd+*', 'meta+*'}, - [m_view[_L['Toggle Show Indent Guides']][2]] = {'ctrl+alt+I', 'ctrl+I', nil}, - [m_view[_L['Toggle Virtual Space']][2]] = {'ctrl+alt+V', 'ctrl+V', nil}, + [m('View/Shrink View')] = {'ctrl+alt+-', 'ctrl+-', nil}, + [m('View/Toggle Current Fold')] = {'ctrl+*', 'cmd+*', 'meta+*'}, + [m('View/Toggle Show Indent Guides')] = {'ctrl+alt+I', 'ctrl+I', nil}, + [m('View/Toggle Virtual Space')] = {'ctrl+alt+V', 'ctrl+V', nil}, [view.zoom_in] = {'ctrl+=', 'cmd+=', nil}, [view.zoom_out] = {'ctrl+-', 'cmd+-', nil}, - [m_view[_L['Reset Zoom']][2]] = {'ctrl+0', 'cmd+0', nil}, + [m('View/Reset Zoom')] = {'ctrl+0', 'cmd+0', nil}, -- Help. - [m_help[_L['Show Manual']][2]] = {'f1', 'f1', nil}, - [m_help[_L['Show LuaDoc']][2]] = {'shift+f1', 'shift+f1', nil}, + [m('Help/Show Manual')] = {'f1', 'f1', nil}, + [m('Help/Show LuaDoc')] = {'shift+f1', 'shift+f1', nil}, -- Movement commands. -- Unbound keys are handled by Scintilla, but when playing back a macro, this @@ -486,7 +482,7 @@ local bindings = { [buffer.document_start] = {nil, nil, 'ctrl+meta+a'}, [buffer.document_end] = {nil, nil, 'ctrl+meta+e'}, - [function(b) + [function() buffer:line_end_extend() if not buffer.selection_empty then buffer:cut() else buffer:clear() end end] = {nil, 'ctrl+k', 'ctrl+k'}, @@ -519,16 +515,11 @@ end if CURSES then keys['ctrl+meta+v'] = { - n = m_view[_L['Next View']][2], - p = m_view[_L['Previous View']][2], - s = m_view[_L['Split View Horizontal']][2], - h = m_view[_L['Split View Horizontal']][2], - v = m_view[_L['Split View Vertical']][2], - w = m_view[_L['Unsplit View']][2], - W = m_view[_L['Unsplit All Views']][2], - ['+'] = m_view[_L['Grow View']][2], - ['='] = m_view[_L['Grow View']][2], - ['-'] = m_view[_L['Shrink View']][2] + n = m('View/Next View'), p = m('View/Previous View'), + s = m('View/Split View Horizontal'), h = m('View/Split View Horizontal'), + v = m('View/Split View Vertical'), w = m('View/Unsplit View'), + W = m('View/Unsplit All Views'), ['+'] = m('View/Grow View'), + ['='] = m('View/Grow View'), ['-'] = m('View/Shrink View') } end diff --git a/modules/textadept/macros.lua b/modules/textadept/macros.lua index 3428ef11..d1a3758f 100644 --- a/modules/textadept/macros.lua +++ b/modules/textadept/macros.lua @@ -11,8 +11,8 @@ local M = {} local recording, macro --- Commands bound to keys to ignore during macro recording, as the command(s) --- ultimately executed will be recorded in some form. +-- List of commands bound to keys to ignore during macro recording, as the +-- command(s) ultimately executed will be recorded in some form. local ignore events.connect(events.INITIALIZED, function() local m_tools = textadept.menu.menubar[_L['Tools']] diff --git a/modules/textadept/menu.lua b/modules/textadept/menu.lua index d684adc1..2dee4f60 100644 --- a/modules/textadept/menu.lua +++ b/modules/textadept/menu.lua @@ -12,8 +12,7 @@ local M = {} -- assigned to string text. module('textadept.menu')]] -local _L = _L -local SEPARATOR = {''} +local _L, SEPARATOR = _L, {''} -- The following buffer and view functions need to be made constant in order for -- menu items to identify the key associated with the functions. @@ -26,16 +25,16 @@ local sel_enc = textadept.editing.select_enclosed local enc = textadept.editing.enclose local function set_indentation(i) buffer.tab_width = i - events.emit(events.UPDATE_UI, 0) -- for updating statusbar + events.emit(events.UPDATE_UI, 1) -- for updating statusbar end local function set_eol_mode(mode) buffer.eol_mode = mode buffer:convert_eols(mode) - events.emit(events.UPDATE_UI, 0) -- for updating statusbar + events.emit(events.UPDATE_UI, 1) -- for updating statusbar end local function set_encoding(encoding) buffer:set_encoding(encoding) - events.emit(events.UPDATE_UI, 0) -- for updating statusbar + events.emit(events.UPDATE_UI, 1) -- for updating statusbar end local function open_page(url) local cmd = (WIN32 and 'start ""') or (OSX and 'open') or 'xdg-open' @@ -205,9 +204,8 @@ local default_menubar = { {_L['Quickly Open User Home'], function() io.quick_open(_USERHOME) end}, {_L['Quickly Open Textadept Home'], function() io.quick_open(_HOME) end}, {_L['Quickly Open Current Directory'], function() - if buffer.filename then - io.quick_open(buffer.filename:match('^(.+)[/\\]')) - end + if not buffer.filename then return end + io.quick_open(buffer.filename:match('^(.+)[/\\]')) end}, {_L['Quickly Open Current Project'], io.quick_open}, }, @@ -228,8 +226,8 @@ local default_menubar = { end}, {_L['Show Documentation'], textadept.editing.show_documentation}, {_L['Show Style'], function() - local char = buffer:text_range(buffer.current_pos, - buffer:position_after(buffer.current_pos)) + local char = buffer:text_range( + buffer.current_pos, buffer:position_after(buffer.current_pos)) if char == '' then return end -- end of buffer local bytes = string.rep(' 0x%X', #char):format(char:byte(1, #char)) local style = buffer.style_at[buffer.current_pos] @@ -255,7 +253,7 @@ local default_menubar = { SEPARATOR, {_L['Toggle Use Tabs'], function() buffer.use_tabs = not buffer.use_tabs - events.emit(events.UPDATE_UI, 0) -- for updating statusbar + events.emit(events.UPDATE_UI, 1) -- for updating statusbar end}, {_L['Convert Indentation'], textadept.editing.convert_indentation} }, @@ -306,12 +304,12 @@ local default_menubar = { end}, SEPARATOR, {_L['Toggle Show Indent Guides'], function() - local off = view.indentation_guides == 0 - view.indentation_guides = off and view.IV_LOOKBOTH or 0 + view.indentation_guides = + view.indentation_guides == 0 and view.IV_LOOKBOTH or 0 end}, {_L['Toggle Virtual Space'], function() - local off = buffer.virtual_space_options == 0 - buffer.virtual_space_options = off and buffer.VS_USERACCESSIBLE or 0 + buffer.virtual_space_options = + buffer.virtual_space_options == 0 and buffer.VS_USERACCESSIBLE or 0 end}, SEPARATOR, {_L['Zoom In'], view.zoom_in}, @@ -399,8 +397,7 @@ end -- @return GTK menu that can be passed to `ui.menu()`. -- @see ui.menu local function read_menu_table(menu, contextmenu) - local gtkmenu = {} - gtkmenu.title = menu.title + local gtkmenu = {title = menu.title} for _, item in ipairs(menu) do if item.title then gtkmenu[#gtkmenu + 1] = read_menu_table(item, contextmenu) @@ -465,8 +462,8 @@ local function set_menubar(menubar) key_shortcuts, menu_items = {}, {} -- reset for key, f in pairs(keys) do key_shortcuts[tostring(f)] = key end local _menubar = {} - for i = 1, #menubar do - _menubar[#_menubar + 1] = ui.menu(read_menu_table(menubar[i])) + for _, menu in ipairs(menubar) do + _menubar[#_menubar + 1] = ui.menu(read_menu_table(menu)) end ui.menubar = _menubar proxies.menubar = proxy_menu(menubar, set_menubar) @@ -513,11 +510,9 @@ proxies.tab_context_menu = proxy_menu(default_tab_context_menu, function() end) -- Performs the appropriate action when clicking a menu item. events.connect(events.MENU_CLICKED, function(menu_id) - local menu_item = menu_id < 1000 and menu_items or contextmenu_items - local action = menu_item[menu_id < 1000 and menu_id or menu_id - 1000][2] - assert(type(action) == 'function', string.format( - '%s %s', _L['Unknown command:'], action)) - action() + local items = menu_id < 1000 and menu_items or contextmenu_items + local f = items[menu_id < 1000 and menu_id or menu_id - 1000][2] + assert_type(f, 'function', 'command')() end) --- diff --git a/modules/textadept/run.lua b/modules/textadept/run.lua index e2c54db8..9c30aa55 100644 --- a/modules/textadept/run.lua +++ b/modules/textadept/run.lua @@ -51,8 +51,8 @@ M.MARK_WARNING = _SCINTILLA.next_marker_number() M.MARK_ERROR = _SCINTILLA.next_marker_number() -- Events. -events.COMPILE_OUTPUT, events.RUN_OUTPUT = 'compile_output', 'run_output' -events.BUILD_OUTPUT = 'build_output' +local run_events = {'compile_output', 'run_output', 'build_output'} +for _, v in ipairs(run_events) do events[v:upper()] = v end -- Keep track of: the last process spawned in order to kill it if requested; the -- cwd of that process in order to jump to relative file paths in recognized @@ -284,10 +284,12 @@ events.connect(events.RUN_OUTPUT, print_output) -- @see compile_commands -- @name set_arguments function M.set_arguments(filename, run, compile) - assert_type(filename, 'string/nil', 1) + if not assert_type(filename, 'string/nil', 1) then + filename = buffer.filename + if not filename then return end + end assert_type(run, 'string/nil', 2) assert_type(compile, 'string/nil', 3) - if not filename then filename = buffer.filename end local base_commands, utf8_args = {}, {} for i, commands in ipairs{M.run_commands, M.compile_commands} do -- Compare the base run/compile command with the one for the current @@ -300,7 +302,8 @@ function M.set_arguments(filename, run, compile) utf8_args[i] = args:iconv('UTF-8', _CHARSET) end if not run or not compile then - local button, utf8_args = ui.dialogs.inputbox{ + local button + button, utf8_args = ui.dialogs.inputbox{ title = _L['Set Arguments...']:gsub('_', ''), informative_text = { _L['Command line arguments'], _L['For Run:'], _L['For Compile:'] }, text = utf8_args, width = not CURSES and 400 or nil @@ -365,10 +368,13 @@ events.connect(events.BUILD_OUTPUT, print_output) -- @name stop function M.stop() if proc then proc:kill() end end +-- Returns whether or not the given buffer is the message buffer. +local function is_msg_buf(buf) return buf._type == _L['[Message Buffer]'] end + -- Send line as input to process stdin on return. events.connect(events.CHAR_ADDED, function(code) if code == string.byte('\n') and proc and proc:status() == 'running' and - buffer._type == _L['[Message Buffer]'] then + is_msg_buf(buffer) then local line_num = buffer:line_from_position(buffer.current_pos) - 1 proc:write(buffer:get_line(line_num)) end @@ -392,8 +398,6 @@ M.error_patterns = {actionscript={'^(.-)%((%d+)%): col: (%d+) (.+)$'},ada={'^(.- -- Note: ASP,CSS,Desktop,diff,django,gettext,Gtkrc,HTML,ini,JSON,JSP,Markdown,Postscript,Properties,R,RHTML,XML don't have parse-able errors. -- Note: Batch,BibTeX,ConTeXt,Dockerfile,GLSL,Inform,Io,Lisp,MoonScript,Scheme,SQL,TeX cannot be parsed for one reason or another. --- Returns whether or not the given buffer is a message buffer. -local function is_msg_buf(buf) return buf._type == _L['[Message Buffer]'] end --- -- Jumps to the source of the recognized compile/run warning or error on line -- number *line_num* in the message buffer. @@ -458,12 +462,10 @@ function M.goto_error(line_num, next) if detail.column then buffer:goto_pos(buffer:find_column(detail.line, detail.column)) end - if detail.message then - buffer.annotation_text[detail.line] = detail.message - if not detail.warning then - buffer.annotation_style[detail.line] = buffer:style_of_name('error') - end - end + if not detail.message then return end + buffer.annotation_text[detail.line] = detail.message + if detail.warning then return end + buffer.annotation_style[detail.line] = buffer:style_of_name('error') end events.connect(events.KEYPRESS, function(code) if keys.KEYSYMS[code] == '\n' and is_msg_buf(buffer) and diff --git a/modules/textadept/session.lua b/modules/textadept/session.lua index 7083c991..55a33be8 100644 --- a/modules/textadept/session.lua +++ b/modules/textadept/session.lua @@ -29,7 +29,8 @@ module('textadept.session')]] M.save_on_quit = true -- Events. -events.SESSION_SAVE, events.SESSION_LOAD = 'session_save', 'session_load' +local session_events = {'session_save', 'session_load'} +for _, v in ipairs(session_events) do events[v:upper()] = v end local session_file = _USERHOME .. (not CURSES and '/session' or '/session_term') @@ -51,7 +52,6 @@ function M.load(filename) } if not filename then return end end - local f = loadfile(filename, 't', {}) if not f or not io.close_all_buffers() then return end -- fail silently local session = f() @@ -86,12 +86,12 @@ function M.load(filename) local function unserialize_split(split) if type(split) ~= 'table' then view:goto_buffer(_BUFFERS[math.min(split, #_BUFFERS)]) - return - end - for i, view in ipairs{view:split(split.vertical)} do - view.size = split.size - ui.goto_view(view) - unserialize_split(split[i]) + else + for i, view in ipairs{view:split(split.vertical)} do + view.size = split.size + ui.goto_view(view) + unserialize_split(split[i]) + end end end unserialize_split(session.views[1]) diff --git a/modules/textadept/snippets.lua b/modules/textadept/snippets.lua index e236bb4d..2f40d219 100644 --- a/modules/textadept/snippets.lua +++ b/modules/textadept/snippets.lua @@ -158,13 +158,13 @@ local function find_snippet(grep, no_trigger) end for _, snippets in ipairs(snippet_tables) do if not grep and snippets[trigger] then return trigger, snippets[trigger] end - if grep then - for name, text in pairs(snippets) do - if name:find(name_patt) and type(text) ~= 'table' then - matching_snippets[name] = tostring(text) - end + if not grep then goto continue end + for name, text in pairs(snippets) do + if name:find(name_patt) and type(text) ~= 'table' then + matching_snippets[name] = tostring(text) end end + ::continue:: end -- Search in snippet files. for i = 1, #M.paths do @@ -188,59 +188,39 @@ local function find_snippet(grep, no_trigger) if not grep then return nil, nil else return trigger, matching_snippets end end --- Metatable for a snippet object. +-- A snippet object. +-- @field trigger The word that triggered this snippet. +-- @field original_sel_text The text originally selected when this snippet was +-- inserted. +-- @field start_pos This snippet's start position. +-- @field end_pos This snippet's end position. This is a metafield that is +-- computed based on the `INDIC_SNIPPET` sentinel. +-- @field placeholder_pos The beginning of the current placeholder in this +-- snippet. This is used by transforms to identify text to transform. This is +-- a metafield that is computed based on `INDIC_CURRENTPLACEHOLDER`. +-- @field index This snippet's current placeholder index. +-- @field max_index The number of different placeholders in this snippet. +-- @field snapshots A record of this snippet's text over time. The snapshot for +-- a given placeholder index contains the state of the snippet with all +-- placeholders of that index filled in (prior to moving to the next +-- placeholder index). Snippet state consists of a `text` string field and a +-- `placeholders` table field. -- @class table --- @name snippet_mt -local snippet_mt -- defined later +-- @name snippet +local snippet = {} -- The stack of currently running snippets. -local snippet_stack = {} +local stack = {} --- Inserts a new snippet, adds it to the snippet stack, and returns the snippet. +-- Inserts a new snippet and adds it to the snippet stack. -- @param text The new snippet to insert. -- @param trigger The trigger text used to expand the snippet, if any. -local function new_snippet(text, trigger) - -- An inserted snippet. - -- @field trigger The word that triggered this snippet. - -- @field original_sel_text The text originally selected when this snippet was - -- inserted. - -- @field start_pos This snippet's start position. - -- @field end_pos This snippet's end position. This is a metafield that is - -- computed based on the `INDIC_SNIPPET` sentinel. - -- @field placeholder_pos The beginning of the current placeholder in this - -- snippet. This is used by transforms to identify text to transform. This - -- is a metafield that is computed based on `INDIC_CURRENTPLACEHOLDER`. - -- @field index This snippet's current placeholder index. - -- @field max_index The number of different placeholders in this snippet. - -- @field snapshots A record of this snippet's text over time. The snapshot - -- for a given placeholder index contains the state of the snippet with all - -- placeholders of that index filled in (prior to moving to the next - -- placeholder index). Snippet state consists of a `text` string field and a - -- `placeholders` table field. - -- @class table - -- @name snippet - local snippet = setmetatable({ +function snippet.new(text, trigger) + local snip = setmetatable({ trigger = trigger, original_sel_text = buffer:get_sel_text(), - start_pos = buffer.selection_start - (trigger and #trigger or 0), - index = 0, max_index = 0, snapshots = {}, - }, {__index = function(self, k) - if k == 'end_pos' then - local end_pos = buffer:indicator_end(INDIC_SNIPPET, self.start_pos) - return end_pos > self.start_pos and end_pos or self.start_pos - elseif k == 'placeholder_pos' then - -- Normally the marker is one character behind the placeholder. However - -- it will not exist at all if the placeholder is at the beginning of the - -- snippet. Also account for the marker being at the beginning of the - -- snippet. (If so, pos will point to the correct position.) - local pos = buffer:indicator_end(INDIC_CURRENTPLACEHOLDER, self.start_pos) - if pos == 1 then pos = self.start_pos end - return buffer:indicator_all_on_for(pos) & - 1 << INDIC_CURRENTPLACEHOLDER - 1 > 0 and pos + 1 or pos - else - return snippet_mt[k] - end - end}) - snippet_stack[#snippet_stack + 1] = snippet + start_pos = buffer.selection_start - (trigger and #trigger or 0), index = 0, + max_index = 0, snapshots = {} + }, snippet) -- Convert and match indentation. local lines = {} @@ -301,7 +281,7 @@ local function new_snippet(text, trigger) while placeholder do if placeholder.index then local i = placeholder.index - if i > snippet.max_index then snippet.max_index = i end + if i > snip.max_index then snip.max_index = i end placeholder.id = #snapshot.placeholders + 1 snapshot.placeholders[#snapshot.placeholders + 1] = placeholder end @@ -330,254 +310,265 @@ local function new_snippet(text, trigger) default:sub(2, -2), position + #index + 2) end index = tonumber(index) - if index > snippet.max_index then snippet.max_index = index end + if index > snip.max_index then snip.max_index = index end snapshot.placeholders[#snapshot.placeholders + 1] = { - id = #snapshot.placeholders + 1, index = index, - default = default, simple = not default or nil, - length = #(default or ' '), - position = snippet.start_pos + position, + id = #snapshot.placeholders + 1, index = index, default = default, + simple = not default or nil, length = #(default or ' '), + position = snip.start_pos + position } return default or ' ' -- fill empty placeholder for display end - local ph_patt = P{ + return lpeg.match(P{ lpeg.Cs((Cp() * '%' * C(R('09')^1) * C(V('parens'))^-1 / ph + 1)^0), parens = '(' * (1 - S('()') + V('parens'))^0 * ')' - } - return ph_patt:match(s) + }, s) end placeholder.default = process_placeholders( placeholder.default, placeholder.position) end snapshot.text = snapshot.text .. placeholder.default elseif placeholder.transform and not placeholder.index then - snapshot.text = snapshot.text .. snippet:execute_code(placeholder) + snapshot.text = snapshot.text .. snip:execute_code(placeholder) else snapshot.text = snapshot.text .. ' ' -- fill empty placeholder for display end placeholder.length = #snapshot.text - placeholder.position - placeholder.position = snippet.start_pos + placeholder.position -- absolute + placeholder.position = snip.start_pos + placeholder.position -- absolute text_part, placeholder, e = patt:match(text, e) end if text_part ~= '' then snapshot.text = snapshot.text .. text_part:gsub('%%(%p)', '%1') end - snippet.snapshots[0] = snapshot + snip.snapshots[0] = snapshot -- Insert the snippet into the buffer and mark its end position. buffer:begin_undo_action() - buffer:set_target_range(snippet.start_pos, buffer.selection_end) + buffer:set_target_range(snip.start_pos, buffer.selection_end) buffer:replace_target(' ') -- placeholder for snippet text buffer.indicator_current = INDIC_SNIPPET - buffer:indicator_fill_range(snippet.start_pos + 1, 1) - snippet:insert() -- insert into placeholder + buffer:indicator_fill_range(snip.start_pos + 1, 1) + snip:insert() -- insert into placeholder buffer:end_undo_action() - return snippet + stack[#stack + 1] = snip end -snippet_mt = { - -- Inserts the current snapshot (based on `self.index`) of this snippet into - -- the buffer and marks placeholders. - insert = function(self) - buffer:set_target_range(self.start_pos, self.end_pos) - buffer:replace_target(self.snapshots[self.index].text) - buffer.indicator_current = M.INDIC_PLACEHOLDER - for id, placeholder in pairs(self.snapshots[self.index].placeholders) do - buffer.indicator_value = id - buffer:indicator_fill_range(placeholder.position, placeholder.length) - end - end, - - -- Jumps to the next placeholder in this snippet and adds additional carets - -- at mirrors. - next = function(self) - if buffer:auto_c_active() then buffer:auto_c_complete() end - -- Take a snapshot of the current state in order to restore it later if - -- necessary. - if self.index > 0 and self.start_pos < self.end_pos then - local text = buffer:text_range(self.start_pos, self.end_pos) - local placeholders = {} - for pos, ph in self:each_placeholder() do - -- Only the position of placeholders changes between snapshots; save it - -- and keep all other existing properties. - -- Note that nested placeholders will return the same placeholder id - -- twice: once before a nested placeholder, and again after. (e.g. - -- [foo[bar]baz] will will return the '[foo' and 'baz]' portions of the - -- same placeholder.) Only process the first occurrence. - if not placeholders[ph.id] then - placeholders[ph.id] = setmetatable({position = pos}, { - __index = self.snapshots[self.index - 1].placeholders[ph.id] - }) - end - end - self.snapshots[self.index] = {text = text, placeholders = placeholders} - end - self.index = self.index < self.max_index and self.index + 1 or 0 - - -- Find the default placeholder, which may be the first mirror. - local ph = select(2, self:each_placeholder(self.index, 'default')()) or - select(2, self:each_placeholder(self.index, 'choice')()) or - select(2, self:each_placeholder(self.index, 'simple')()) or - self.index == 0 and {position = self.end_pos, length = 0} - if not ph then self:next() return end -- try next placeholder - - -- Mark the position of the placeholder so transforms can identify it. - buffer.indicator_current = INDIC_CURRENTPLACEHOLDER - buffer:indicator_clear_range(self.placeholder_pos - 1, 1) - if ph.position > self.start_pos and self.index > 0 then - -- Place it directly behind the placeholder so it will be preserved. - buffer:indicator_fill_range(ph.position - 1, 1) +-- Provides dynamic field values and methods for this snippet. +function snippet:__index(k) + if k == 'end_pos' then + local end_pos = buffer:indicator_end(INDIC_SNIPPET, self.start_pos) + return end_pos > self.start_pos and end_pos or self.start_pos + elseif k == 'placeholder_pos' then + -- Normally the marker is one character behind the placeholder. However + -- it will not exist at all if the placeholder is at the beginning of the + -- snippet. Also account for the marker being at the beginning of the + -- snippet. (If so, pos will point to the correct position.) + local pos = buffer:indicator_end(INDIC_CURRENTPLACEHOLDER, self.start_pos) + if pos == 1 then pos = self.start_pos end + return buffer:indicator_all_on_for(pos) & + 1 << INDIC_CURRENTPLACEHOLDER - 1 > 0 and pos + 1 or pos + end + return getmetatable(self)[k] +end + +-- Inserts the current snapshot (based on `self.index`) of this snippet into +-- the buffer and marks placeholders. +function snippet:insert() + buffer:set_target_range(self.start_pos, self.end_pos) + buffer:replace_target(self.snapshots[self.index].text) + buffer.indicator_current = M.INDIC_PLACEHOLDER + for id, placeholder in pairs(self.snapshots[self.index].placeholders) do + buffer.indicator_value = id + buffer:indicator_fill_range(placeholder.position, placeholder.length) + end +end + +-- Jumps to the next placeholder in this snippet and adds additional carets +-- at mirrors. +function snippet:next() + if buffer:auto_c_active() then buffer:auto_c_complete() end + -- Take a snapshot of the current state in order to restore it later if + -- necessary. + if self.index > 0 and self.start_pos < self.end_pos then + local text = buffer:text_range(self.start_pos, self.end_pos) + local placeholders = {} + for pos, ph in self:each_placeholder() do + -- Only the position of placeholders changes between snapshots; save it + -- and keep all other existing properties. + -- Note that nested placeholders will return the same placeholder id + -- twice: once before a nested placeholder, and again after. (e.g. + -- [foo[bar]baz] will will return the '[foo' and 'baz]' portions of the + -- same placeholder.) Only process the first occurrence. + if placeholders[ph.id] then goto continue end + placeholders[ph.id] = setmetatable({position = pos}, { + __index = self.snapshots[self.index - 1].placeholders[ph.id] + }) + ::continue:: end + self.snapshots[self.index] = {text = text, placeholders = placeholders} + end + self.index = self.index < self.max_index and self.index + 1 or 0 + + -- Find the default placeholder, which may be the first mirror. + local ph = select(2, self:each_placeholder(self.index, 'default')()) or + select(2, self:each_placeholder(self.index, 'choice')()) or + select(2, self:each_placeholder(self.index, 'simple')()) or + self.index == 0 and {position = self.end_pos, length = 0} + if not ph then self:next() return end -- try next placeholder + + -- Mark the position of the placeholder so transforms can identify it. + buffer.indicator_current = INDIC_CURRENTPLACEHOLDER + buffer:indicator_clear_range(self.placeholder_pos - 1, 1) + if ph.position > self.start_pos and self.index > 0 then + -- Place it directly behind the placeholder so it will be preserved. + buffer:indicator_fill_range(ph.position - 1, 1) + end + + buffer:begin_undo_action() + + -- Jump to the default placeholder and clear its marker. + buffer:set_sel(ph.position, ph.position + ph.length) + local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, ph.position) + buffer.indicator_current = M.INDIC_PLACEHOLDER + buffer:indicator_clear_range(ph.position, e - ph.position) + if not ph.default then buffer:replace_sel('') end -- delete filler ' ' + if ph.choice then + local sep = buffer.auto_c_separator + buffer.auto_c_separator = string.byte(',') + buffer.auto_c_order = buffer.ORDER_CUSTOM + buffer:auto_c_show(0, ph.choice) + buffer.auto_c_separator = sep -- restore + end + + -- Add additional carets at mirrors and clear their markers. + local text = ph.default or '' + ::redo:: + for pos in self:each_placeholder(self.index, 'simple') do + local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, pos) + buffer:indicator_clear_range(pos, e - pos) + buffer:set_target_range(pos, pos + 1) + buffer:replace_target(text) + buffer:add_selection(pos, pos + #text) + goto redo -- indicator positions have changed + end + buffer.main_selection = 1 + + -- Update transforms. + self:update_transforms() - buffer:begin_undo_action() + buffer:end_undo_action() + + if self.index == 0 then self:finish() end +end - -- Jump to the default placeholder and clear its marker. - buffer:set_sel(ph.position, ph.position + ph.length) - local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, ph.position) +-- Jumps to the previous placeholder in this snippet and restores the state +-- associated with that placeholder. +function snippet:previous() + if self.index < 2 then self:finish(true) return end + self.index = self.index - 2 + self:insert() + self:next() +end + +-- Finishes or cancels this snippet depending on boolean *canceling*. +-- The snippet cleans up after itself regardless. +-- @param canceling Whether or not to cancel inserting this snippet. When +-- `true`, the buffer is restored to its state prior to snippet expansion. +function snippet:finish(canceling) + local s, e = self.start_pos, self.end_pos + if e ~= s then buffer:delete_range(e, 1) end -- clear initial padding space + if not canceling then buffer.indicator_current = M.INDIC_PLACEHOLDER - buffer:indicator_clear_range(ph.position, e - ph.position) - if not ph.default then buffer:replace_sel('') end -- delete filler ' ' - if ph.choice then - local sep = buffer.auto_c_separator - buffer.auto_c_separator = string.byte(',') - buffer.auto_c_order = buffer.ORDER_CUSTOM - buffer:auto_c_show(0, ph.choice) - buffer.auto_c_separator = sep -- restore - end + buffer:indicator_clear_range(s, e - s) + else + buffer:set_sel(s, e) + buffer:replace_sel(self.trigger or self.original_sel_text) + end + stack[#stack] = nil +end - -- Add additional carets at mirrors and clear their markers. - local text = ph.default or '' - ::redo:: - for pos in self:each_placeholder(self.index, 'simple') do - local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, pos) - buffer:indicator_clear_range(pos, e - pos) - buffer:set_target_range(pos, pos + 1) - buffer:replace_target(text) - buffer:add_selection(pos, pos + #text) - goto redo -- indicator positions have changed - end - buffer.main_selection = 1 - - -- Update transforms. - self:update_transforms() - - buffer:end_undo_action() - - if self.index == 0 then self:finish() end - end, - - -- Jumps to the previous placeholder in this snippet and restores the state - -- associated with that placeholder. - previous = function(self) - if self.index < 2 then self:finish(true) return end - self.index = self.index - 2 - self:insert() - self:next() - end, - - -- Finishes or cancels this snippet depending on boolean *canceling*. - -- The snippet cleans up after itself regardless. - -- @param canceling Whether or not to cancel inserting this snippet. When - -- `true`, the buffer is restored to its state prior to snippet expansion. - finish = function(self, canceling) - local s, e = self.start_pos, self.end_pos - if e ~= s then buffer:delete_range(e, 1) end -- clear initial padding space - if not canceling then - buffer.indicator_current = M.INDIC_PLACEHOLDER - buffer:indicator_clear_range(s, e - s) - else - buffer:set_sel(s, e) - buffer:replace_sel(self.trigger or self.original_sel_text) - end - snippet_stack[#snippet_stack] = nil - end, - - -- Returns a generator that returns each placeholder's position and state for - -- all placeholders in this snippet. - -- DO NOT modify the buffer while this generator is running. Doing so will - -- affect the generator's state and cause errors. Re-run the generator each - -- time a buffer edit is made (e.g. via `goto`). - -- @param index Optional placeholder index to constrain results to. - -- @param type Optional placeholder type to constrain results to. - each_placeholder = function(self, index, type) - local snapshot = - self.snapshots[self.index > 0 and self.index - 1 or #self.snapshots] - local i = self.start_pos - local PLACEHOLDER_BIT = 1 << M.INDIC_PLACEHOLDER - 1 - return function() - local s = buffer:indicator_end(M.INDIC_PLACEHOLDER, i) - while s > 1 and s <= self.end_pos do - if buffer:indicator_all_on_for(i) & PLACEHOLDER_BIT > 0 then - -- This next indicator comes directly after the previous one; adjust - -- start and end positions to compensate. - s, i = buffer:indicator_start(M.INDIC_PLACEHOLDER, i), s - else - i = buffer:indicator_end(M.INDIC_PLACEHOLDER, s) - end - local id = buffer:indicator_value_at(M.INDIC_PLACEHOLDER, s) - local ph = snapshot.placeholders[id] - if ph and (not index or ph.index == index) and - (not type or ph[type]) then - return s, ph - end - s = buffer:indicator_end(M.INDIC_PLACEHOLDER, i) +-- Returns a generator that returns each placeholder's position and state for +-- all placeholders in this snippet. +-- DO NOT modify the buffer while this generator is running. Doing so will +-- affect the generator's state and cause errors. Re-run the generator each +-- time a buffer edit is made (e.g. via `goto`). +-- @param index Optional placeholder index to constrain results to. +-- @param type Optional placeholder type to constrain results to. +function snippet:each_placeholder(index, type) + local snapshot = + self.snapshots[self.index > 0 and self.index - 1 or #self.snapshots] + local i = self.start_pos + return function() + local s = buffer:indicator_end(M.INDIC_PLACEHOLDER, i) + while s > 1 and s <= self.end_pos do + if buffer:indicator_all_on_for(i) & 1 << M.INDIC_PLACEHOLDER - 1 > 0 then + -- This next indicator comes directly after the previous one; adjust + -- start and end positions to compensate. + s, i = buffer:indicator_start(M.INDIC_PLACEHOLDER, i), s + else + i = buffer:indicator_end(M.INDIC_PLACEHOLDER, s) end + local id = buffer:indicator_value_at(M.INDIC_PLACEHOLDER, s) + local ph = snapshot.placeholders[id] + if ph and (not index or ph.index == index) and (not type or ph[type]) then + return s, ph + end + s = buffer:indicator_end(M.INDIC_PLACEHOLDER, i) end - end, - - -- Returns the result of executing Lua or Shell code, in placeholder table - -- *placeholder*, in the context of this snippet. - -- @param placeholder The placeholder that contains code to execute. - execute_code = function(self, placeholder) - local s, e = self.placeholder_pos, buffer.selection_end - if s > e then s, e = e, s end - local text = self.index and buffer:text_range(s, e) or '' -- %<...>, %[...] - if placeholder.lua_code then - local env = setmetatable( - {text = text, selected_text = self.original_sel_text}, {__index = _G}) - local f, result = load('return ' .. placeholder.lua_code, nil, 't', env) - return f and select(2, pcall(f)) or result or '' - elseif placeholder.sh_code then - -- Note: cannot use spawn since $env variables are not expanded. - local command = placeholder.sh_code:gsub('%f[%%]%%%f[^%%]', text) - local p = io.popen(command) - local result = p:read('a'):sub(1, -2) -- chop '\n' - p:close() - return result - end - end, + end +end - -- Updates transforms in place based on the current placeholder's text. - update_transforms = function(self) - buffer.indicator_current = M.INDIC_PLACEHOLDER - local processed = {} - ::redo:: - for s, ph in self:each_placeholder(nil, 'transform') do - if ph.index == self.index and not processed[ph] then - -- Execute the code and replace any existing transform text. - local result = self:execute_code(ph) - if result == '' then result = ' ' end -- fill for display - local id = buffer:indicator_value_at(M.INDIC_PLACEHOLDER, s) - buffer:set_target_range(s, buffer:indicator_end(M.INDIC_PLACEHOLDER, s)) - buffer:replace_target(result) - buffer.indicator_value = id - buffer:indicator_fill_range(s, #result) -- re-mark - processed[ph] = true - goto redo -- indicator positions have changed - elseif ph.index < self.index or self.index == 0 then - -- Clear obsolete transforms, deleting filler text if necessary. - local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, s) - buffer:indicator_clear_range(s, e - s) - if buffer:text_range(s, e) == ' ' then - buffer:set_target_range(s, e) - buffer:replace_target('') -- delete filler ' ' - goto redo - end +-- Returns the result of executing Lua or Shell code, in placeholder table +-- *placeholder*, in the context of this snippet. +-- @param placeholder The placeholder that contains code to execute. +function snippet:execute_code(placeholder) + local s, e = self.placeholder_pos, buffer.selection_end + if s > e then s, e = e, s end + local text = self.index and buffer:text_range(s, e) or '' -- %<...>, %[...] + if placeholder.lua_code then + local env = setmetatable( + {text = text, selected_text = self.original_sel_text}, {__index = _G}) + local f, result = load('return ' .. placeholder.lua_code, nil, 't', env) + return f and select(2, pcall(f)) or result or '' + elseif placeholder.sh_code then + -- Note: cannot use spawn since $env variables are not expanded. + local command = placeholder.sh_code:gsub('%f[%%]%%%f[^%%]', text) + local p = io.popen(command) + local result = p:read('a'):sub(1, -2) -- chop '\n' + p:close() + return result + end +end + +-- Updates transforms in place based on the current placeholder's text. +function snippet:update_transforms() + buffer.indicator_current = M.INDIC_PLACEHOLDER + local processed = {} + ::redo:: + for s, ph in self:each_placeholder(nil, 'transform') do + if ph.index == self.index and not processed[ph] then + -- Execute the code and replace any existing transform text. + local result = self:execute_code(ph) + if result == '' then result = ' ' end -- fill for display + local id = buffer:indicator_value_at(M.INDIC_PLACEHOLDER, s) + buffer:set_target_range(s, buffer:indicator_end(M.INDIC_PLACEHOLDER, s)) + buffer:replace_target(result) + buffer.indicator_value = id + buffer:indicator_fill_range(s, #result) -- re-mark + processed[ph] = true + goto redo -- indicator positions have changed + elseif ph.index < self.index or self.index == 0 then + -- Clear obsolete transforms, deleting filler text if necessary. + local e = buffer:indicator_end(M.INDIC_PLACEHOLDER, s) + buffer:indicator_clear_range(s, e - s) + if buffer:text_range(s, e) == ' ' then + buffer:delete_range(s, e - s) -- delete filler ' ' + goto redo end - -- TODO: insert initial transform for ph.index > self.index end - end, -} + -- TODO: insert initial transform for ph.index > self.index + end +end --- -- Inserts snippet text *text* or the snippet assigned to the trigger word @@ -593,13 +584,12 @@ snippet_mt = { function M.insert(text) local trigger if not assert_type(text, 'string/nil', 1) then - trigger, text = find_snippet(trigger) + trigger, text = find_snippet() if type(text) == 'function' then text = text() end assert_type(text, 'string/nil', trigger or '?') end - local snippet = type(text) == 'string' and new_snippet(text, trigger) or - snippet_stack[#snippet_stack] - if snippet then snippet:next() else return false end + if text then snippet.new(text, trigger) end + if #stack > 0 then stack[#stack]:next() else return false end end --- @@ -609,8 +599,7 @@ end -- @return `false` if no snippet is active; `nil` otherwise. -- @name previous function M.previous() - if #snippet_stack == 0 then return false end - snippet_stack[#snippet_stack]:previous() + if #stack > 0 then stack[#stack]:previous() else return false end end --- @@ -619,8 +608,7 @@ end -- @return `false` if no snippet is active; `nil` otherwise. -- @name cancel_current function M.cancel_current() - if #snippet_stack == 0 then return false end - snippet_stack[#snippet_stack]:finish(true) + if #stack > 0 then stack[#stack]:finish(true) else return false end end --- @@ -645,12 +633,11 @@ end -- Update snippet transforms when text is added or deleted. events.connect(events.UPDATE_UI, function(updated) - if #snippet_stack > 0 then - if updated & buffer.UPDATE_CONTENT > 0 then - snippet_stack[#snippet_stack]:update_transforms() - end - if #keys.keychain == 0 then ui.statusbar_text = _L['Snippet active'] end + if #stack == 0 then return end + if updated & buffer.UPDATE_CONTENT > 0 then + stack[#stack]:update_transforms() end + if #keys.keychain == 0 then ui.statusbar_text = _L['Snippet active'] end end) events.connect(events.VIEW_NEW, function() @@ -662,8 +649,7 @@ end) -- completions. -- @see textadept.editing.autocomplete textadept.editing.autocompleters.snippet = function() - local list = {} - local trigger, snippets = find_snippet(true) + local list, trigger, snippets = {}, find_snippet(true) local sep = string.char(buffer.auto_c_type_separator) local xpm = textadept.editing.XPM_IMAGES.NAMESPACE for name in pairs(snippets) do list[#list + 1] = name .. sep .. xpm end |