diff options
39 files changed, 729 insertions, 813 deletions
diff --git a/core/args.lua b/core/args.lua index 3d249c9a..af605d36 100644 --- a/core/args.lua +++ b/core/args.lua @@ -11,7 +11,7 @@ module('args')]] events.ARG_NONE = 'arg_none' --- Contains registered command line options. +-- Map of registered command line options. -- @class table -- @name options local options = {} @@ -44,6 +44,8 @@ end -- Emits an `ARG_NONE` event when no arguments are present unless -- *no_emit_arg_none* is `true`. -- @param arg Argument table. +-- @param no_emit_arg_none When `true`, do not emit `ARG_NONE` when no arguments +-- are present. The default value is `false`. -- @see register -- @see _G.events local function process(arg, no_emit_arg_none) @@ -135,7 +137,7 @@ M.register('-t', '--test', 1, function(patterns) local arg = {} for patt in (patterns or ''):gmatch('[^,]+') do arg[#arg + 1] = patt end local env = setmetatable({arg = arg}, {__index = _G}) - assert(loadfile(_HOME..'/test/test.lua', 't', env))() + assert(loadfile(_HOME .. '/test/test.lua', 't', env))() end) end, 'Runs unit tests indicated by comma-separated list of patterns (or all)') diff --git a/core/assert.lua b/core/assert.lua index baa08d5b..80e50a6e 100644 --- a/core/assert.lua +++ b/core/assert.lua @@ -31,7 +31,7 @@ end -- This is intended to be used with API function arguments so users receive more -- helpful error messages. -- @param v Value to assert the type of. --- @param expected_type String type to assert. It may be a punctuation-delimited +-- @param expected_type String type to assert. It may be a non-letter-delimited -- list of type options. -- @param narg The positional argument number *v* is associated with. This is -- not required to be a number. diff --git a/core/events.lua b/core/events.lua index 7282172d..1c146145 100644 --- a/core/events.lua +++ b/core/events.lua @@ -391,11 +391,10 @@ end -- Handles Scintilla notifications. M.connect('SCN', function(notification) - local f = _SCINTILLA.events[notification.code] - if not f then return end + local iface = _SCINTILLA.events[notification.code] local args = {} - for i = 2, #f do args[i - 1] = notification[f[i]] end - return M.emit(f[1], table.unpack(args)) + for i = 2, #iface do args[i - 1] = notification[iface[i]] end + return M.emit(iface[1], table.unpack(args)) end) -- Set event constants. diff --git a/core/file_io.lua b/core/file_io.lua index 39d005b2..0426083a 100644 --- a/core/file_io.lua +++ b/core/file_io.lua @@ -42,13 +42,8 @@ module('io')]] -- Events. -local events, events_connect = events, events.connect -events.FILE_OPENED = 'file_opened' -events.FILE_BEFORE_RELOAD = 'file_before_reload' -events.FILE_AFTER_RELOAD = 'file_after_reload' -events.FILE_BEFORE_SAVE = 'file_before_save' -events.FILE_AFTER_SAVE = 'file_after_save' -events.FILE_CHANGED = 'file_changed' +local file_io_events = {'file_opened','file_before_reload','file_after_reload','file_before_save','file_after_save','file_changed'} +for _, v in ipairs(file_io_events) do events[v:upper()] = v end io.quick_open_max = 1000 @@ -137,7 +132,7 @@ function io.open_file(filenames, encodings) buffer.code_page = buffer.encoding and buffer.CP_UTF8 or 0 -- Detect EOL mode. local s, e = text:find('\r?\n') - if s then buffer.eol_mode = buffer[s < e and 'EOL_CRLF' or 'EOL_LF'] end + if s then buffer.eol_mode = buffer[s ~= e and 'EOL_CRLF' or 'EOL_LF'] end -- Insert buffer text and set properties. buffer:append_text(text) buffer:empty_undo_buffer() @@ -225,22 +220,17 @@ end -- @see buffer.save -- @name save_all_files function io.save_all_files() - local current_buffer = buffer for _, buffer in ipairs(_BUFFERS) do - if buffer.filename and buffer.modify then - view:goto_buffer(buffer) - buffer:save() - end + if buffer.filename and buffer.modify then buffer:save() end end - view:goto_buffer(current_buffer) end -- LuaDoc is in core/.buffer.luadoc. local function close(buffer, force) if not buffer then buffer = _G.buffer end - local filename = buffer.filename or buffer._type or _L['Untitled'] - if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end if buffer.modify and not force then + local filename = buffer.filename or buffer._type or _L['Untitled'] + if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end local button = ui.dialogs.msgbox{ title = _L['Close without saving?'], text = _L['There are unsaved changes in'], informative_text = filename, @@ -262,15 +252,12 @@ end -- @see buffer.close -- @name close_all_buffers function io.close_all_buffers() - while #_BUFFERS > 1 do - view:goto_buffer(_BUFFERS[#_BUFFERS]) - if not buffer:close() then return nil end -- do not propagate key command - end + while #_BUFFERS > 1 do if not buffer:close() then return nil end end return buffer:close() -- the last one end -- Sets buffer io methods and the default buffer encoding. -events_connect(events.BUFFER_NEW, function() +events.connect(events.BUFFER_NEW, function() buffer.reload = reload buffer.set_encoding, buffer.encoding = set_encoding, 'UTF-8' buffer.save, buffer.save_as, buffer.close = save, save_as, close @@ -286,20 +273,19 @@ io._reload, io._save, io._save_as, io._close = reload, save, save_as, close local function update_modified_file() if not buffer.filename then return end local mod_time = lfs.attributes(buffer.filename, 'modification') - if not mod_time or not buffer.mod_time then return end - if buffer.mod_time < mod_time then + if mod_time and buffer.mod_time and buffer.mod_time < mod_time then buffer.mod_time = mod_time events.emit(events.FILE_CHANGED, buffer.filename) end end -events_connect(events.BUFFER_AFTER_SWITCH, update_modified_file) -events_connect(events.VIEW_AFTER_SWITCH, update_modified_file) -events_connect(events.FOCUS, update_modified_file) -events_connect(events.RESUME, update_modified_file) +events.connect(events.BUFFER_AFTER_SWITCH, update_modified_file) +events.connect(events.VIEW_AFTER_SWITCH, update_modified_file) +events.connect(events.FOCUS, update_modified_file) +events.connect(events.RESUME, update_modified_file) -- Prompts the user to reload the current file if it has been externally -- modified. -events_connect(events.FILE_CHANGED, function(filename) +events.connect(events.FILE_CHANGED, function(filename) local button = ui.dialogs.msgbox{ title = _L['Reload?'], text = _L['Reload modified file?'], informative_text = string.format( @@ -312,12 +298,10 @@ events_connect(events.FILE_CHANGED, function(filename) end) -- Closes the initial "Untitled" buffer when another buffer is opened. -events_connect(events.FILE_OPENED, function() - local buf = _BUFFERS[1] - if #_BUFFERS == 2 and not (buf.filename or buf._type or buf.modify) then - view:goto_buffer(_BUFFERS[1]) - buffer:close() - end +events.connect(events.FILE_OPENED, function() + if #_BUFFERS > 2 then return end + local first = _BUFFERS[1] + if not (first.filename or first._type or first.modify) then first:close() end end) --- @@ -392,8 +376,8 @@ io.quick_open_filters = {} -- search. The default value is the current project's root directory, if -- available. -- @param filter Optional filter for files and directories to include and/or --- exclude. The default value is `lfs.default_filter` unless *paths* is a --- string and a filter for it is defined in `io.quick_open_filters`. +-- exclude. The default value is `lfs.default_filter` unless a filter for +-- *paths* is defined in `io.quick_open_filters`. -- @param opts Optional table of additional options for -- `ui.dialogs.filteredlist()`. -- @usage io.quick_open(buffer.filename:match('^(.+)[/\\]')) -- list all files @@ -412,15 +396,13 @@ function io.quick_open(paths, filter, opts) paths = io.get_project_root() if not paths then return end end - if type(paths) == 'string' then - if not filter then filter = io.quick_open_filters[paths] end - paths = {paths} + if not assert_type(filter, 'string/table/nil', 2) then + filter = io.quick_open_filters[paths] or lfs.default_filter end - assert_type(filter, 'string/table/nil', 2) assert_type(opts, 'table/nil', 3) local utf8_list = {} - for i = 1, #paths do - for filename in lfs.walk(paths[i], filter or lfs.default_filter) do + for _, path in ipairs(type(paths) == 'table' and paths or {paths}) do + for filename in lfs.walk(path, filter) do if #utf8_list >= io.quick_open_max then break end utf8_list[#utf8_list + 1] = filename:iconv('UTF-8', _CHARSET) end diff --git a/core/init.lua b/core/init.lua index f71cd6be..e2674e48 100644 --- a/core/init.lua +++ b/core/init.lua @@ -44,22 +44,20 @@ end -- argument. -- Documentation is in core/.buffer.luadoc. local function text_range(buffer, start_pos, end_pos) - assert_type(start_pos, 'number', 2) - assert_type(end_pos, 'number', 3) local target_start, target_end = buffer.target_start, buffer.target_end - if start_pos < 1 then start_pos = 1 end - if end_pos > buffer.length + 1 then end_pos = buffer.length + 1 end - buffer:set_target_range(start_pos, end_pos) + buffer:set_target_range( + math.max(1, assert_type(start_pos, 'number', 2)), + math.min(assert_type(end_pos, 'number', 3), buffer.length + 1)) local text = buffer.target_text buffer:set_target_range(target_start, target_end) -- restore return text end +local GETNAMEDSTYLE = _SCINTILLA.properties.named_styles[1] -- Documentation is in core/.buffer.luadoc. local function style_of_name(buffer, style_name) - assert_type(style_name, 'string', 2) - local GETNAMEDSTYLE = _SCINTILLA.properties.named_styles[1] - return buffer:private_lexer_call(GETNAMEDSTYLE, style_name) + return buffer:private_lexer_call( + GETNAMEDSTYLE, assert_type(style_name, 'string', 2)) end events.connect(events.BUFFER_NEW, function() diff --git a/core/keys.lua b/core/keys.lua index ac9523ab..2d4d8781 100644 --- a/core/keys.lua +++ b/core/keys.lua @@ -112,8 +112,8 @@ local M = {} -- The default value is `nil`. module('keys')]] -local CTRL, ALT, SHIFT = 'ctrl+', not CURSES and 'alt+' or 'meta+', 'shift+' -local CMD = 'cmd+' +local CTRL, ALT, CMD, SHIFT = 'ctrl+', 'alt+', 'cmd+', 'shift+' +if CURSES then ALT = 'meta+' end M.CLEAR = 'esc' --- @@ -151,7 +151,7 @@ local INVALID, PROPAGATE, CHAIN, HALT = -1, 0, 1, 2 -- Error handler for key commands that simply emits the error. This is needed -- so `key_command()` can return `HALT` instead of never returning due to the -- error. -local function key_error(e) events.emit(events.ERROR, e) end +local function key_error(errmsg) events.emit(events.ERROR, errmsg) end -- Runs a key command associated with the current keychain. -- @param prefix Optional prefix name for mode/lexer-specific commands. @@ -171,19 +171,11 @@ local function key_command(prefix) return select(2, xpcall(key, key_error)) == false and PROPAGATE or HALT end --- Handles Textadept keypresses. --- It is called every time a key is pressed, and based on a mode or lexer, --- executes a command. The command is looked up in the `_G.keys` table. --- @param code The keycode. --- @param shift Whether or not the Shift modifier is pressed. --- @param control Whether or not the Control modifier is pressed. --- @param alt Whether or not the Alt/option modifier is pressed. --- @param cmd Whether or not the Command modifier on macOS is pressed. --- @param caps_lock Whether or not Caps Lock is enabled. --- @return `true` to stop handling the key; `nil` otherwise. -local function keypress(code, shift, control, alt, cmd, caps_lock) - --print(code, M.KEYSYMS[code], shift, control, alt, cmd, caps_lock) - if caps_lock and (shift or control or alt or cmd) and code < 256 then +-- Handles Textadept keypresses, executing commands based on a mode or lexer as +-- necessary. +events.connect(events.KEYPRESS, function(code, shift, control, alt, cmd, caps) + --print(code, M.KEYSYMS[code], shift, control, alt, cmd, caps) + if caps and (shift or control or alt or cmd) and code < 256 then code = string[shift and 'upper' or 'lower'](string.char(code)):byte() end local key = code >= 32 and code < 256 and string.char(code) or M.KEYSYMS[code] @@ -216,8 +208,7 @@ local function keypress(code, shift, control, alt, cmd, caps_lock) return true end -- PROPAGATE otherwise. -end -events.connect(events.KEYPRESS, keypress) +end) --[[ This comment is for LuaDoc. --- diff --git a/core/lfs_ext.lua b/core/lfs_ext.lua index 86a0cada..c24ee2e9 100644 --- a/core/lfs_ext.lua +++ b/core/lfs_ext.lua @@ -22,33 +22,6 @@ lfs.default_filter = {--[[Extensions]]'!.a','!.bmp','!.bz2','!.class','!.dll','! -- @param level Utility value indicating the directory level this function is -- at. local function walk(dir, filter, n, include_dirs, level) - if not level then - -- Convert filter to a table from nil or string arguments. - if not filter then filter = lfs.default_filter end - if type(filter) == 'string' then filter = {filter} end - -- Process the given filter into something that can match files more easily - -- and/or quickly. For example, convert '.ext' shorthand to '%.ext$', - -- substitute '/' with '[/\\]', and enable hash lookup for file extensions - -- to include or exclude. - local processed_filter = { - consider_any = true, - exts = setmetatable({}, {__index = function() return true end}) - } - for _, patt in ipairs(filter) do - patt = patt:gsub('^(!?)%%?%.([^.]+)$', '%1%%.%2$') -- '.lua' to '%.lua$' - patt = patt:gsub('/([^\\])', '[/\\]%1') -- '/' to '[/\\]' - local include = not patt:find('^!') - local ext = patt:match('^!?%%.([^.]+)%$$') - if ext then - processed_filter.exts[ext] = include - if include then setmetatable(processed_filter.exts, nil) end - else - if include then processed_filter.consider_any = false end - processed_filter[#processed_filter + 1] = patt - end - end - filter = processed_filter - end for basename in lfs.dir(dir) do if basename:find('^%.%.?$') then goto continue end -- ignore . and .. local filename = dir .. (dir ~= '/' and '/' or '') .. basename @@ -68,14 +41,15 @@ local function walk(dir, filter, n, include_dirs, level) -- Treat inclusive patterns as logical OR. include = include or (not patt:find('^!') and filename:find(patt)) end + if not include then goto continue end local sep = not WIN32 and '/' or '\\' local os_filename = not WIN32 and filename or filename:gsub('/', sep) - if include and mode == 'directory' then + if mode == 'file' then + coroutine.yield(os_filename) + elseif mode == 'directory' then if include_dirs then coroutine.yield(os_filename .. sep) end if n and (level or 0) >= n then goto continue end walk(filename, filter, n, include_dirs, (level or 0) + 1) - elseif include and mode == 'file' then - coroutine.yield(os_filename) end ::continue:: end @@ -104,9 +78,33 @@ end -- @name walk function lfs.walk(dir, filter, n, include_dirs) assert_type(dir, 'string', 1) - assert_type(filter, 'string/table/nil', 2) + if not assert_type(filter, 'string/table/nil', 2) then + filter = lfs.default_filter + end assert_type(n, 'number/nil', 3) - local co = coroutine.create(function() walk(dir, filter, n, include_dirs) end) + -- Process the given filter into something that can match files more easily + -- and/or quickly. For example, convert '.ext' shorthand to '%.ext$', + -- substitute '/' with '[/\\]', and enable hash lookup for file extensions + -- to include or exclude. + local processed_filter = { + consider_any = true, + exts = setmetatable({}, {__index = function() return true end}) + } + for _, patt in ipairs(type(filter) == 'table' and filter or {filter}) do + patt = patt:gsub('^(!?)%%?%.([^.]+)$', '%1%%.%2$') -- '.lua' to '%.lua$' + patt = patt:gsub('/([^\\])', '[/\\]%1') -- '/' to '[/\\]' + local include = not patt:find('^!') + local ext = patt:match('^!?%%.([^.]+)%$$') + if ext then + processed_filter.exts[ext] = include + if include then setmetatable(processed_filter.exts, nil) end + else + if include then processed_filter.consider_any = false end + processed_filter[#processed_filter + 1] = patt + end + end + local co = coroutine.create( + function() walk(dir, processed_filter, n, include_dirs) end) return function() return select(2, coroutine.resume(co)) end end diff --git a/core/locale.conf b/core/locale.conf index d5f60436..18a62e2b 100644 --- a/core/locale.conf +++ b/core/locale.conf @@ -336,9 +336,6 @@ Help = _Help Show Manual = Show _Manual Show LuaDoc = Show _LuaDoc About = _About -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Unknown command: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Run Command diff --git a/core/locale.lua b/core/locale.lua index 48701116..3a661fae 100644 --- a/core/locale.lua +++ b/core/locale.lua @@ -23,14 +23,14 @@ assert(f, '"core/locale.conf" not found') for line in f:lines() do -- Any line that starts with a non-word character except '[' is considered a -- comment. - if not line:find('^%s*[^%w_%[]') then - local id, str = line:match('^(.-)%s*=%s*(.-)\r?$') - if id and str and assert(not M[id], 'duplicate locale key "%s"', id) then - M[id] = not CURSES and str or str:gsub('_', '') - end + if not line:find('^%s*[%w_%[]') then goto continue end + local id, str = line:match('^(.-)%s*=%s*(.-)\r?$') + if id and str and assert(not M[id], 'duplicate locale key "%s"', id) then + M[id] = not CURSES and str or str:gsub('_', '') end + ::continue:: end f:close() -setmetatable(M, {__index = function(_, k) return 'No Localization:' .. k end}) -return M +return setmetatable( + M, {__index = function(_, k) return 'No Localization:' .. k end}) diff --git a/core/locales/locale.ar.conf b/core/locales/locale.ar.conf index 953d3d38..e894445e 100644 --- a/core/locales/locale.ar.conf +++ b/core/locales/locale.ar.conf @@ -336,9 +336,6 @@ Help = م_ساعدة Show Manual = عرض الد_ليل Show LuaDoc = عرض و_ثائق لُوَ About = ع_ن -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = أمر مجهول: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = شغِّل الأمر diff --git a/core/locales/locale.de.conf b/core/locales/locale.de.conf index 22d46b5c..d73ca229 100644 --- a/core/locales/locale.de.conf +++ b/core/locales/locale.de.conf @@ -336,9 +336,6 @@ Help = _Hilfe Show Manual = Handbuch anzeigen Show LuaDoc = LuaDoc anzeigen About = _Über Textadept -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Unbekannter Befehl: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Befehl ausführen diff --git a/core/locales/locale.es.conf b/core/locales/locale.es.conf index 354a33fc..273b90ca 100644 --- a/core/locales/locale.es.conf +++ b/core/locales/locale.es.conf @@ -336,9 +336,6 @@ Help = A_yuda Show Manual = Mostrar _manual Show LuaDoc = Mostrar _LuaDoc About = Acerca _de -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Comando desconocido: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Ejecutar comando diff --git a/core/locales/locale.fr.conf b/core/locales/locale.fr.conf index b2fff92d..f3afccfa 100644 --- a/core/locales/locale.fr.conf +++ b/core/locales/locale.fr.conf @@ -337,9 +337,6 @@ Help = Aid_e Show Manual = Voir _manuel Show LuaDoc = Voir la documentation sur l’_API About = À _propos -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Commande inconnue: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Lancer une commande diff --git a/core/locales/locale.it.conf b/core/locales/locale.it.conf index 91d4958a..d9c508a1 100644 --- a/core/locales/locale.it.conf +++ b/core/locales/locale.it.conf @@ -336,9 +336,6 @@ Help = _Aiuto Show Manual = _Manuale Show LuaDoc = _Documentazione sull’API About = _Informazioni -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Comando sconosciuto: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Scegli un comando diff --git a/core/locales/locale.pl.conf b/core/locales/locale.pl.conf index bbe34282..a33b1d69 100644 --- a/core/locales/locale.pl.conf +++ b/core/locales/locale.pl.conf @@ -337,9 +337,6 @@ Help = P_omoc Show Manual = Po_dręcznik użytkownika... Show LuaDoc = _LuaDoc... About = _O programie... -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Nieznane polecenie: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Wykonaj polecenie diff --git a/core/locales/locale.ru.conf b/core/locales/locale.ru.conf index 3bfb7198..476d0f63 100644 --- a/core/locales/locale.ru.conf +++ b/core/locales/locale.ru.conf @@ -336,9 +336,6 @@ Help = _Справка Show Manual = Показать _руководство Show LuaDoc = Показать документацию по _lua About = _О программе -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Неизвестная команда: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Выполнить команду diff --git a/core/locales/locale.sv.conf b/core/locales/locale.sv.conf index d56d2456..f6f01d17 100644 --- a/core/locales/locale.sv.conf +++ b/core/locales/locale.sv.conf @@ -336,9 +336,6 @@ Help = _Hjälp Show Manual = _Manual Show LuaDoc = _LuaDoc About = _Om -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = Okänt kommando: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = Kör kommando diff --git a/core/locales/locale.zh.conf b/core/locales/locale.zh.conf index dbce6e55..54476596 100644 --- a/core/locales/locale.zh.conf +++ b/core/locales/locale.zh.conf @@ -336,9 +336,6 @@ Help = 帮助(_H) Show Manual = 打开手册(_M) Show LuaDoc = 打开_LuaDoc About = 关于(_A) -# The error message displayed when activating a menu item associated with an -# unknown command. -Unknown command: = 未知命令: # The text displayed in the dialog for running an arbitrary menu command. Any # key binding associated with commands are also shown. Run Command = 运行命令 diff --git a/core/ui.lua b/core/ui.lua index 343b4510..73616719 100644 --- a/core/ui.lua +++ b/core/ui.lua @@ -37,6 +37,12 @@ module('ui')]] ui.silent_print = false +-- Helper function for jumping to another view to print to, or creating a new +-- view to print to (the latter depending on settings). +local function prepare_view() + if #_VIEWS > 1 then ui.goto_view(1) elseif not ui.tabs then view:split() end +end + -- Helper function for printing messages to buffers. -- @see ui._print local function _print(buffer_type, ...) @@ -45,21 +51,19 @@ local function _print(buffer_type, ...) if buf._type == buffer_type then buffer = buf break end end if not buffer then - if not ui.tabs then view:split() end + prepare_view() buffer = _G.buffer.new() buffer._type = buffer_type elseif not ui.silent_print then for _, view in ipairs(_VIEWS) do - if view.buffer._type == buffer_type then ui.goto_view(view) break end - end - if view.buffer._type ~= buffer_type then - if #_VIEWS > 1 then - ui.goto_view(1) - elseif not ui.tabs then - view:split() + if view.buffer._type == buffer_type then + ui.goto_view(view) + goto view_found end - view:goto_buffer(buffer) end + prepare_view() + view:goto_buffer(buffer) + ::view_found:: end local args, n = {...}, select('#', ...) for i = 1, n do args[i] = tostring(args[i]) end @@ -115,8 +119,7 @@ ui.dialogs = setmetatable({}, {__index = function(_, k) -- Transform key-value pairs into command line arguments. local args = {} for option, value in pairs(options) do - assert_type(value, 'string/number/table/boolean', option) - if value then + if assert_type(value, 'string/number/table/boolean', option) then args[#args + 1] = '--' .. option:gsub('_', '-') if type(value) == 'table' then for i, val in ipairs(value) do @@ -139,7 +142,7 @@ ui.dialogs = setmetatable({}, {__index = function(_, k) if k == 'progressbar' then args[#args + 1] = assert_type(f, 'function', 2) end - -- Call gtdialog, stripping any trailing newline in the standard output. + -- Call gtdialog, stripping any trailing newline in the output. local result = ui.dialog( k:gsub('_', '-'), table.unpack(args)):match('^(.-)\n?$') -- Depending on the dialog type, transform the result into Lua objects. @@ -185,12 +188,10 @@ ui.dialogs = setmetatable({}, {__index = function(_, k) end end}) -local events, events_connect = events, events.connect - local buffers_zorder = {} -- Adds new buffers to the z-order list. -events_connect(events.BUFFER_NEW, function() +events.connect(events.BUFFER_NEW, function() if buffer ~= ui.command_entry then table.insert(buffers_zorder, 1, buffer) end end) @@ -206,13 +207,14 @@ local function update_zorder() end table.insert(buffers_zorder, 1, buffer) end -events_connect(events.BUFFER_AFTER_SWITCH, update_zorder) -events_connect(events.VIEW_AFTER_SWITCH, update_zorder) +events.connect(events.BUFFER_AFTER_SWITCH, update_zorder) +events.connect(events.VIEW_AFTER_SWITCH, update_zorder) +events.connect(events.BUFFER_DELETED, update_zorder) -- Saves and restores buffer zorder data during a reset. -events_connect( +events.connect( events.RESET_BEFORE, function(persist) persist.ui_zorder = buffers_zorder end) -events_connect( +events.connect( events.RESET_AFTER, function(persist) buffers_zorder = persist.ui_zorder end) --- @@ -237,9 +239,8 @@ function ui.switch_buffer(zorder) local button, i = ui.dialogs.filteredlist{ title = _L['Switch Buffers'], columns = columns, items = utf8_list } - if button == 1 and i then - view:goto_buffer(buffers[not zorder and i or i + 1]) - end + if button ~= 1 or not i then return end + view:goto_buffer(buffers[not zorder and i or i + 1]) end --- @@ -290,10 +291,10 @@ function ui.goto_file(filename, split, preferred_view, sloppy) end -- Ensure title, statusbar, etc. are updated for new views. -events_connect(events.VIEW_NEW, function() events.emit(events.UPDATE_UI, 3) end) +events.connect(events.VIEW_NEW, function() events.emit(events.UPDATE_UI, 3) end) -- Switches between buffers when a tab is clicked. -events_connect( +events.connect( events.TAB_CLICKED, function(index) view:goto_buffer(_BUFFERS[index]) end) -- Sets the title of the Textadept window to the buffer's filename. @@ -309,11 +310,11 @@ local function set_title() end -- Changes Textadept title to show the buffer as being "clean" or "dirty". -events_connect(events.SAVE_POINT_REACHED, set_title) -events_connect(events.SAVE_POINT_LEFT, set_title) +events.connect(events.SAVE_POINT_REACHED, set_title) +events.connect(events.SAVE_POINT_LEFT, set_title) -- Open uri(s). -events_connect(events.URI_DROPPED, function(utf8_uris) +events.connect(events.URI_DROPPED, function(utf8_uris) for utf8_path in utf8_uris:gmatch('file://([^\r\n]+)') do local path = utf8_path:gsub('%%(%x%x)', function(hex) return string.char(tonumber(hex, 16)) @@ -325,25 +326,25 @@ events_connect(events.URI_DROPPED, function(utf8_uris) end ui.goto_view(view) -- work around any view focus synchronization issues end) -events_connect(events.APPLEEVENT_ODOC, function(uri) +events.connect(events.APPLEEVENT_ODOC, function(uri) return events.emit(events.URI_DROPPED, 'file://' .. uri) end) -- Sets buffer statusbar text. -events_connect(events.UPDATE_UI, function(updated) +events.connect(events.UPDATE_UI, function(updated) if updated & 3 == 0 then return end -- ignore scrolling local text = not CURSES and '%s %d/%d %s %d %s %s %s %s' or '%s %d/%d %s %d %s %s %s %s' - local pos = buffer.selection_n_caret[buffer.main_selection] + local pos = buffer.current_pos local line, max = buffer:line_from_position(pos), buffer.line_count local col = buffer.column[pos] local lang = buffer:get_lexer() local eol = buffer.eol_mode == buffer.EOL_CRLF and _L['CRLF'] or _L['LF'] local tabs = string.format( '%s %d', buffer.use_tabs and _L['Tabs:'] or _L['Spaces:'], buffer.tab_width) - local enc = buffer.encoding or '' + local encoding = buffer.encoding or '' ui.buffer_statusbar_text = string.format( - text, _L['Line:'], line, max, _L['Col:'], col, lang, eol, tabs, enc) + text, _L['Line:'], line, max, _L['Col:'], col, lang, eol, tabs, encoding) end) -- Save buffer properties. @@ -357,19 +358,17 @@ local function save_buffer_state() buffer._x_offset = view.x_offset -- Save fold state. local folds, i = {}, view:contracted_fold_next(1) - while i >= 1 do - folds[#folds + 1], i = i, view:contracted_fold_next(i + 1) - end + while i >= 1 do folds[#folds + 1], i = i, view:contracted_fold_next(i + 1) end buffer._folds = folds end -events_connect(events.BUFFER_BEFORE_SWITCH, save_buffer_state) -events_connect(events.FILE_BEFORE_RELOAD, save_buffer_state) +events.connect(events.BUFFER_BEFORE_SWITCH, save_buffer_state) +events.connect(events.FILE_BEFORE_RELOAD, save_buffer_state) -- Restore buffer properties. local function restore_buffer_state() if not buffer._folds then return end -- Restore fold state. - for i = 1, #buffer._folds do view:toggle_fold(buffer._folds[i]) end + for _, line in ipairs(buffer._folds) do view:toggle_fold(line) end -- Restore view state. buffer:set_sel(buffer._anchor, buffer._current_pos) buffer.selection_n_anchor_virtual_space[1] = buffer._anchor_virtual_space @@ -379,17 +378,17 @@ local function restore_buffer_state() view:line_scroll(0, view:visible_from_doc_line(_top_line) - top_line) view.x_offset = buffer._x_offset or 0 end -events_connect(events.BUFFER_AFTER_SWITCH, restore_buffer_state) -events_connect(events.FILE_AFTER_RELOAD, restore_buffer_state) +events.connect(events.BUFFER_AFTER_SWITCH, restore_buffer_state) +events.connect(events.FILE_AFTER_RELOAD, restore_buffer_state) -- Updates titlebar and statusbar. local function update_bars() set_title() events.emit(events.UPDATE_UI, 3) end -events_connect(events.BUFFER_NEW, update_bars) -events_connect(events.BUFFER_AFTER_SWITCH, update_bars) -events_connect(events.VIEW_AFTER_SWITCH, update_bars) +events.connect(events.BUFFER_NEW, update_bars) +events.connect(events.BUFFER_AFTER_SWITCH, update_bars) +events.connect(events.VIEW_AFTER_SWITCH, update_bars) -- Save view state. local function save_view_state() @@ -400,8 +399,8 @@ local function save_view_state() buffer._margin_width_n[i] = view.margin_width_n[i] end end -events_connect(events.BUFFER_BEFORE_SWITCH, save_view_state) -events_connect(events.VIEW_BEFORE_SWITCH, save_view_state) +events.connect(events.BUFFER_BEFORE_SWITCH, save_view_state) +events.connect(events.VIEW_BEFORE_SWITCH, save_view_state) -- Restore view state. local function restore_view_state() @@ -412,21 +411,21 @@ local function restore_view_state() view.margin_width_n[i] = buffer._margin_width_n[i] end end -events_connect(events.BUFFER_AFTER_SWITCH, restore_view_state) -events_connect(events.VIEW_AFTER_SWITCH, restore_view_state) +events.connect(events.BUFFER_AFTER_SWITCH, restore_view_state) +events.connect(events.VIEW_AFTER_SWITCH, restore_view_state) -events_connect( +events.connect( events.RESET_AFTER, function() ui.statusbar_text = _L['Lua reset'] end) -- Prompts for confirmation if any buffers are modified. -events_connect(events.QUIT, function() +events.connect(events.QUIT, function() local utf8_list = {} for _, buffer in ipairs(_BUFFERS) do - if buffer.modify then - local filename = buffer.filename or buffer._type or _L['Untitled'] - if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end - utf8_list[#utf8_list + 1] = filename - end + if not buffer.modify then goto continue end + local filename = buffer.filename or buffer._type or _L['Untitled'] + if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end + utf8_list[#utf8_list + 1] = filename + ::continue:: end if #utf8_list == 0 then return end local button = ui.dialogs.msgbox{ @@ -440,10 +439,10 @@ events_connect(events.QUIT, function() if button ~= 2 then return true end -- prevent quit end) --- Keeps track of and switches back to the previous buffer after buffer close. -events_connect( +-- Keeps track of, and switches back to the previous buffer after buffer close. +events.connect( events.BUFFER_BEFORE_SWITCH, function() view._prev_buffer = buffer end) -events_connect(events.BUFFER_DELETED, function() +events.connect(events.BUFFER_DELETED, function() if _BUFFERS[view._prev_buffer] and buffer ~= view._prev_buffer then view:goto_buffer(view._prev_buffer) end @@ -452,19 +451,20 @@ end) -- Properly handle clipboard text between views in curses, enables and disables -- mouse mode, and focuses and resizes views based on mouse events. if CURSES then - local clip_text - events.connect( - events.VIEW_BEFORE_SWITCH, function() clip_text = ui.clipboard_text end) - events.connect( - events.VIEW_AFTER_SWITCH, function() ui.clipboard_text = clip_text end) + events.connect(events.VIEW_BEFORE_SWITCH, function() + ui._clipboard_text = ui.clipboard_text + end) + events.connect(events.VIEW_AFTER_SWITCH, function() + ui.clipboard_text = ui._clipboard_text + end) if not WIN32 then local function enable_mouse() io.stdout:write("\x1b[?1002h"):flush() end local function disable_mouse() io.stdout:write("\x1b[?1002l"):flush() end enable_mouse() - events_connect(events.SUSPEND, disable_mouse) - events_connect(events.RESUME, enable_mouse) - events_connect(events.QUIT, disable_mouse) + events.connect(events.SUSPEND, disable_mouse) + events.connect(events.RESUME, enable_mouse) + events.connect(events.QUIT, disable_mouse) end -- Retrieves the view or split at the given terminal coordinates. @@ -486,7 +486,7 @@ if CURSES then end local resize - events_connect(events.MOUSE, function(event, button, y, x) + events.connect(events.MOUSE, function(event, button, y, x) if event == view.MOUSE_RELEASE or button ~= 1 then return end if event == view.MOUSE_PRESS then local view = get_view(ui.get_split_table(), y - 1, x) -- title is at y = 1 @@ -511,7 +511,7 @@ events.connect(events.INITIALIZED, function() -- Print internal Lua error messages as they are reported. -- Attempt to mimic the Lua interpreter's error message format so tools that -- look for it can recognize these errors too. - events_connect(events.ERROR, function(text) + events.connect(events.ERROR, function(text) if text and text:find(lua_error) then text = 'lua: ' .. text end ui.print(text) end) diff --git a/docs/api.md b/docs/api.md index 9a7bcdb2..5452dc23 100644 --- a/docs/api.md +++ b/docs/api.md @@ -610,7 +610,7 @@ helpful error messages. Parameters: * *`v`*: Value to assert the type of. -* *`expected_type`*: String type to assert. It may be a punctuation-delimited +* *`expected_type`*: String type to assert. It may be a non-letter-delimited list of type options. * *`narg`*: The positional argument number *v* is associated with. This is not required to be a number. @@ -4465,8 +4465,8 @@ Parameters: search. The default value is the current project's root directory, if available. * *`filter`*: Optional filter for files and directories to include and/or - exclude. The default value is `lfs.default_filter` unless *paths* is a - string and a filter for it is defined in `io.quick_open_filters`. + exclude. The default value is `lfs.default_filter` unless a filter for + *paths* is defined in `io.quick_open_filters`. * *`opts`*: Optional table of additional options for `ui.dialogs.filteredlist()`. @@ -7403,6 +7403,27 @@ See also: * [`textadept.run.run_commands`](#textadept.run.run_commands) * [`events`](#events) +<a id="textadept.run.set_arguments"></a> +#### `textadept.run.set_arguments`(*filename, run, compile*) + +Appends the command line argument strings *run* and *compile* to their +respective run and compile commands for file *filename* or the current file. +If either is `nil`, prompts the user for missing the arguments. Each filename +has its own set of compile and run arguments. + +Parameters: + +* *`filename`*: Optional path to the file to set run/compile arguments for. +* *`run`*: Optional string run arguments to set. If `nil`, the user is + prompted for them. Pass the empty string for no run arguments. +* *`compile`*: Optional string compile arguments to set. If `nil`, the user + is prompted for them. Pass the empty string for no compile arguments. + +See also: + +* [`textadept.run.run_commands`](#textadept.run.run_commands) +* [`textadept.run.compile_commands`](#textadept.run.compile_commands) + <a id="textadept.run.stop"></a> #### `textadept.run.stop`() @@ -12,8 +12,8 @@ package.cpath = table.concat({ -- Populate initial `_G.buffer` with temporarily exported io functions now that -- it exists. This is needed for menus and key bindings. -for _, name in ipairs{'reload', 'save', 'save_as', 'close'} do - buffer[name], io['_' .. name] = io['_' .. name], nil +for name, f in pairs(io) do + if name:find('^_') then buffer[name:sub(2)], io[name] = f, nil end end textadept = require('textadept') @@ -51,11 +51,12 @@ buffer.set_theme=function(...)view:set_theme(select(2,...));events.connect(event local function en_au_to_us()for au,us in pairs{CASEINSENSITIVEBEHAVIOUR_IGNORECASE=buffer.CASEINSENSITIVEBEHAVIOR_IGNORECASE,CASEINSENSITIVEBEHAVIOUR_RESPECTCASE=buffer.CASEINSENSITIVEBEHAVIOR_RESPECTCASE,INDIC_GRADIENTCENTRE=buffer.INDIC_GRADIENTCENTER,MARGIN_COLOUR=buffer.MARGIN_COLOR,auto_c_case_insensitive_behaviour=buffer.auto_c_case_insensitive_behavior,colourise=buffer.colorize,edge_colour=buffer.edge_color,set_fold_margin_colour=function()ui.dialogs.msgbox{title='Compatibility issue',text="Please update your theme's use of renamed buffer/view fields"};return buffer.set_fold_margin_color end,set_fold_margin_hi_colour=buffer.set_fold_margin_hi_color,vertical_centre_caret=buffer.vertical_center_caret}do buffer[au]=us;view[au]=us end end;events.connect(events.BUFFER_NEW,en_au_to_us);en_au_to_us() events.connect(events.INITIALIZED,function()if _NOCOMPAT then return end;local update_keys={};local function translate_keys(keys,new_keys)for k,v in pairs(keys)do if type(k)=='string'and k:find('^[cmas]+.$')and not k:find('ctrl')and not k:find('cmd')and not k:find('alt')and not k:find('meta')and not k:find('shift')and k~='css'then update_keys[#update_keys+1]=k;k=k:gsub('^(c?m?a?)s(.)','%1shift+%2'):gsub('^(c?m?)a(.)','%1alt+%2'):gsub('^(c?)m(.)',string.format('%%1%s%%2',OSX and'cmd+'or'meta+')):gsub('^c(.)','ctrl+%1')end rawset(new_keys,k,type(v)=='table'and translate_keys(v,setmetatable({},getmetatable(v)))or v)end return new_keys end;for k,v in pairs(translate_keys(keys,{}))do keys[k]=v end;if#update_keys>0then ui.dialogs.msgbox{title='Compatibility issue',text='Please update your keys to use the new modifiers:\n'..table.concat(update_keys,'\n')..'\n\nSet _NOCOMPAT=true to disable this warning'}end end) --- The remainder of this file defines default buffer properties and applies them --- to subsequent buffers. Normally, a setting like `buffer.use_tabs = false` --- only applies to the current (initial) buffer. However, temporarily tap into --- buffer's metatable in order to capture these initial buffer settings (both --- from Textadept's init.lua and from the user's init.lua). +-- The remainder of this file defines default buffer and view properties and +-- applies them to subsequent buffers and views. Normally, a setting like +-- `buffer.use_tabs = false` only applies to the current (initial) buffer. +-- However, temporarily tap into buffer and view's metatables in order to +-- capture these initial settings (both from Textadept's init.lua and from the +-- user's init.lua) so they can be applied to subsequent buffers and views. local settings = {} @@ -119,8 +120,7 @@ local styles = setmetatable({}, {__newindex = function(_, name, props) end}) lexer = setmetatable({colors = colors, styles = styles}, { __newindex = function(_, k, v) - if k == 'folding' then k = 'fold' end - property[k:gsub('_', '.')] = v and '1' or '0' + property[k ~= 'folding' and k:gsub('_', '.') or 'fold'] = v and '1' or '0' end }) @@ -130,8 +130,7 @@ local buffer, view = buffer, view view:set_theme(not CURSES and 'light' or 'term') -- Multiple Selection and Virtual Space -buffer.multiple_selection = true -buffer.additional_selection_typing = true +buffer.multiple_selection, buffer.additional_selection_typing = true, true buffer.multi_paste = buffer.MULTIPASTE_EACH --buffer.virtual_space_options = buffer.VS_RECTANGULARSELECTION | -- buffer.VS_USERACCESSIBLE @@ -198,8 +197,7 @@ view.margin_mask_n[3] = view.MASK_FOLDERS -- Other Margins. for i = 2, view.margins do view.margin_type_n[i] = view.MARGIN_SYMBOL - view.margin_sensitive_n[i] = true - view.margin_cursor_n[i] = view.CURSORARROW + view.margin_sensitive_n[i], view.margin_cursor_n[i] = true, view.CURSORARROW if i > 3 then view.margin_width_n[i] = 0 end end @@ -215,11 +213,9 @@ buffer.buffered_draw = not CURSES and not OSX -- Quartz buffers drawing on macOS -- Tabs and Indentation Guides. -- Note: tab and indentation settings apply to individual buffers. -buffer.tab_width = 2 -buffer.use_tabs = false +buffer.tab_width, buffer.use_tabs = 2, false --buffer.indent = 2 -buffer.tab_indents = true -buffer.back_space_un_indents = true +buffer.tab_indents, buffer.back_space_un_indents = true, true view.indentation_guides = not CURSES and view.IV_LOOKBOTH or view.IV_NONE -- Margin Markers. @@ -265,16 +261,13 @@ view:marker_define(buffer.MARKNUM_FOLDERMIDTAIL, view.MARK_TCORNER) -- Indicators. view.indic_style[ui.find.INDIC_FIND] = view.INDIC_ROUNDBOX -if not CURSES then view.indic_under[ui.find.INDIC_FIND] = true end -local INDIC_BRACEMATCH = textadept.editing.INDIC_BRACEMATCH -view.indic_style[INDIC_BRACEMATCH] = view.INDIC_BOX -view:brace_highlight_indicator(not CURSES, INDIC_BRACEMATCH) -local INDIC_HIGHLIGHT = textadept.editing.INDIC_HIGHLIGHT -view.indic_style[INDIC_HIGHLIGHT] = view.INDIC_ROUNDBOX -if not CURSES then view.indic_under[INDIC_HIGHLIGHT] = true end -local INDIC_PLACEHOLDER = textadept.snippets.INDIC_PLACEHOLDER -view.indic_style[INDIC_PLACEHOLDER] = not CURSES and view.INDIC_DOTBOX or - view.INDIC_STRAIGHTBOX +view.indic_under[ui.find.INDIC_FIND] = not CURSES +view.indic_style[textadept.editing.INDIC_BRACEMATCH] = view.INDIC_BOX +view:brace_highlight_indicator(not CURSES, textadept.editing.INDIC_BRACEMATCH) +view.indic_style[textadept.editing.INDIC_HIGHLIGHT] = view.INDIC_ROUNDBOX +view.indic_under[textadept.editing.INDIC_HIGHLIGHT] = not CURSES +view.indic_style[textadept.snippets.INDIC_PLACEHOLDER] = + not CURSES and view.INDIC_DOTBOX or view.INDIC_STRAIGHTBOX -- Autocompletion. --buffer.auto_c_separator = @@ -332,19 +325,19 @@ if lfs.attributes(user_init) then end -- Generate default buffer settings for subsequent buffers and remove temporary --- buffer metatable listener. +-- buffer and view metatable listeners. local load_settings = load(table.concat(settings, '\n')) for _, mt in ipairs{buffer_mt, view_mt} do mt.__index, mt.__newindex = mt.__orig_index, mt.__orig_newindex end +local SETDIRECTFUNCTION = _SCINTILLA.properties.direct_function[1] +local SETDIRECTPOINTER = _SCINTILLA.properties.doc_pointer[2] +local SETLUASTATE = _SCINTILLA.functions.change_lexer_state[1] +local LOADLEXERLIBRARY = _SCINTILLA.functions.load_lexer_library[1] -- Sets default properties for a Scintilla document. events.connect(events.BUFFER_NEW, function() local buffer = _G.buffer - local SETDIRECTFUNCTION = _SCINTILLA.properties.direct_function[1] - local SETDIRECTPOINTER = _SCINTILLA.properties.doc_pointer[2] - local SETLUASTATE = _SCINTILLA.functions.change_lexer_state[1] - local LOADLEXERLIBRARY = _SCINTILLA.functions.load_lexer_library[1] buffer:private_lexer_call(SETDIRECTFUNCTION, buffer.direct_function) buffer:private_lexer_call(SETDIRECTPOINTER, buffer.direct_pointer) buffer:private_lexer_call(SETLUASTATE, _LUA) 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 diff --git a/test/test.lua b/test/test.lua index 1505f2c2..047f6601 100644 --- a/test/test.lua +++ b/test/test.lua @@ -786,6 +786,23 @@ function test_ui_print() ui.silent_print = silent_print end +function test_ui_print_to_other_view() + local silent_print = ui.silent_print + + ui.silent_print = false + view:split() + ui.goto_view(-1) + assert_equal(_VIEWS[view], 1) + ui.print('foo') -- should print to other view, not split again + assert_equal(#_VIEWS, 2) + assert_equal(_VIEWS[view], 2) + buffer:close() + ui.goto_view(-1) + view:unsplit() + + ui.silent_print = silent_print +end + function test_ui_dialogs_colorselect_interactive() local color = ui.dialogs.colorselect{title = 'Blue', color = 0xFF0000} assert_equal(color, 0xFF0000) @@ -1729,6 +1746,12 @@ function test_editing_transpose_chars() buffer:char_left() textadept.editing.transpose_chars() assert_equal(buffer:get_text(), '⌘⇧⌥') + buffer:clear_all() + textadept.editing.transpose_chars() + assert_equal(buffer:get_text(), '') + buffer:add_text('a') + textadept.editing.transpose_chars() + assert_equal(buffer:get_text(), 'a') -- TODO: multiple selection? buffer:close(true) end |