diff options
-rw-r--r-- | modules/textadept/command_entry.lua | 2 | ||||
-rw-r--r-- | modules/textadept/editing.lua | 2 | ||||
-rw-r--r-- | modules/textadept/menu.lua | 105 | ||||
-rw-r--r-- | modules/textadept/run.lua | 143 | ||||
-rw-r--r-- | test/test.lua | 6 |
5 files changed, 133 insertions, 125 deletions
diff --git a/modules/textadept/command_entry.lua b/modules/textadept/command_entry.lua index 87f1465a..5df63a25 100644 --- a/modules/textadept/command_entry.lua +++ b/modules/textadept/command_entry.lua @@ -78,7 +78,7 @@ local function run_lua(code) if type(result) == 'table' then local items = {} for k, v in pairs(result) do - items[#items + 1] = string.format('%s = %s', tostring(k), tostring(v)) + items[#items + 1] = string.format('%s = %s', k, v) end table.sort(items) result = string.format('{%s}', table.concat(items, ', ')) diff --git a/modules/textadept/editing.lua b/modules/textadept/editing.lua index 9d2f5e7b..bc5d8ffa 100644 --- a/modules/textadept/editing.lua +++ b/modules/textadept/editing.lua @@ -185,7 +185,7 @@ end) -- Auto-indent on return. events.connect(events.CHAR_ADDED, function(code) - if not M.auto_indent or code ~= 10 then return end + if not M.auto_indent or code ~= string.byte('\n') then return end local line = buffer:line_from_position(buffer.current_pos) if line > 0 and buffer:get_line(line - 1):find('^[\r\n]+$') and buffer:get_line(line):find('^[^\r\n]') then diff --git a/modules/textadept/menu.lua b/modules/textadept/menu.lua index 24790f96..df800e02 100644 --- a/modules/textadept/menu.lua +++ b/modules/textadept/menu.lua @@ -42,7 +42,7 @@ local function set_encoding(encoding) end local function open_page(url) local cmd = (WIN32 and 'start ""') or (OSX and 'open') or 'xdg-open' - os.spawn(string.format('%s "%s"', cmd, not OSX and url or 'file://'..url)) + os.spawn(string.format('%s "%s"', cmd, not OSX and url or 'file://' .. url)) end --- @@ -91,7 +91,7 @@ local default_menubar = { SEPARATOR, {_L['Match Brace'], function() local match_pos = buffer:brace_match(buffer.current_pos, 0) - if match_pos >= 0 then buffer:goto_pos(match_pos) end + if match_pos ~= -1 then buffer:goto_pos(match_pos) end end}, {_L['Complete Word'], function() textadept.editing.autocomplete('word') @@ -120,9 +120,9 @@ local default_menubar = { {_L['Enclose as XML Tags'], function() buffer:begin_undo_action() enc('<', '>') - local pos = buffer.current_pos - while buffer.char_at[pos - 1] ~= 60 do pos = pos - 1 end -- '<' - buffer:insert_text(-1, '</'..buffer:text_range(pos, buffer.current_pos)) + local s, e = buffer.current_pos, buffer.current_pos + while buffer.char_at[s - 1] ~= 60 do s = s - 1 end -- '<' + buffer:insert_text(-1, '</' .. buffer:text_range(s, e)) buffer:end_undo_action() end}, {_L['Enclose as Single XML Tag'], function() enc('<', ' />') end}, @@ -177,7 +177,7 @@ local default_menubar = { -- Compare the base run/compile command with the one for the current -- file. The difference is any additional arguments set previously. base_commands[i] = commands[buffer.filename:match('[^.]+$')] or - commands[buffer:get_lexer()] or '' + commands[buffer:get_lexer()] or '' local current_command = commands[buffer.filename] or '' local args = current_command:sub(#base_commands[i] + 2) utf8_args[i] = args:iconv('UTF-8', _CHARSET) @@ -191,8 +191,8 @@ local default_menubar = { for i, commands in ipairs{run_commands, compile_commands} do -- Add the additional arguments to the base run/compile command and set -- the new command to be the one used for the current file. - commands[buffer.filename] = base_commands[i]..' '.. - utf8_args[i]:iconv(_CHARSET, 'UTF-8') + commands[buffer.filename] = string.format('%s %s', base_commands[i], + utf8_args[i]:iconv(_CHARSET, 'UTF-8')) end end}, {_L['Build'], textadept.run.build}, @@ -249,14 +249,14 @@ local default_menubar = { {_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)) + 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] - local text = string.format("'%s' (U+%04X:%s)\n%s %s\n%s %s (%d)", char, - utf8.codepoint(char), bytes, _L['Lexer'], - buffer:get_lexer(true), _L['Style'], - buffer:name_of_style(style), style) + local text = string.format( + "'%s' (U+%04X:%s)\n%s %s\n%s %s (%d)", char, utf8.codepoint(char), + bytes, _L['Lexer'], buffer:get_lexer(true), _L['Style'], + buffer:name_of_style(style), style) buffer:call_tip_show(buffer.current_pos, text) end} }, @@ -343,14 +343,14 @@ local default_menubar = { }, { title = _L['Help'], - {_L['Show Manual'], function() open_page(_HOME..'/doc/manual.html') end}, - {_L['Show LuaDoc'], function() open_page(_HOME..'/doc/api.html') end}, + {_L['Show Manual'], function() open_page(_HOME .. '/doc/manual.html') end}, + {_L['Show LuaDoc'], function() open_page(_HOME .. '/doc/api.html') end}, SEPARATOR, {_L['About'], function() - ui.dialogs.msgbox({ + ui.dialogs.msgbox{ title = 'Textadept', text = _RELEASE, informative_text = _COPYRIGHT, - icon_file = _HOME..'/core/images/ta_64x64.png' - }) + icon_file = _HOME .. '/core/images/ta_64x64.png' + } end} } } @@ -403,8 +403,8 @@ local function get_gdk_key(key_seq) local mods, key = key_seq:match('^([cams]*)(.+)$') if not mods or not key then return nil end local modifiers = ((mods:find('s') or key:lower() ~= key) and 1 or 0) + - (mods:find('c') and 4 or 0) + (mods:find('a') and 8 or 0) + - (mods:find('m') and 0x10000000 or 0) + (mods:find('c') and 4 or 0) + (mods:find('a') and 8 or 0) + + (mods:find('m') and 0x10000000 or 0) local code = string.byte(key) if #key > 1 or code < 32 then for i, s in pairs(keys.KEYSYMS) do @@ -424,18 +424,17 @@ end local function read_menu_table(menu, contextmenu) local gtkmenu = {} gtkmenu.title = menu.title - for i = 1, #menu do - if menu[i].title then - gtkmenu[#gtkmenu + 1] = read_menu_table(menu[i], contextmenu) - else - local label, f = menu[i][1], menu[i][2] - local menu_id = not contextmenu and #menu_items + 1 or - #contextmenu_items + 1000 + 1 - local key, mods = get_gdk_key(key_shortcuts[tostring(f)]) - gtkmenu[#gtkmenu + 1] = {label, menu_id, key, mods} - if f then + for _, item in ipairs(menu) do + if item.title then + gtkmenu[#gtkmenu + 1] = read_menu_table(item, contextmenu) + else -- item = {label, function} + local menu_id = + not contextmenu and #menu_items + 1 or #contextmenu_items + 1000 + 1 + local key, mods = get_gdk_key(key_shortcuts[tostring(item[2])]) + gtkmenu[#gtkmenu + 1] = {item[1], menu_id, key, mods} + if item[2] then local menu_items = not contextmenu and menu_items or contextmenu_items - menu_items[menu_id < 1000 and menu_id or menu_id - 1000] = menu[i] + menu_items[menu_id < 1000 and menu_id or menu_id - 1000] = item end end end @@ -456,8 +455,8 @@ local function proxy_menu(menu, update, menubar) if type(k) == 'number' or k == 'title' then v = menu[k] elseif type(k) == 'string' then - for i = 1, #menu do - if menu[i].title == k or menu[i][1] == k then v = menu[i] break end + for _, item in ipairs(menu) do + if item.title == k or item[1] == k then v = item break end end end return type(v) == 'table' and proxy_menu(v, update, menubar or menu) or v @@ -497,6 +496,8 @@ local function set_menubar(menubar) end events.connect(events.INITIALIZED, function() set_menubar(default_menubar) end) -- Define menu proxy for use by keys.lua and user scripts. +-- Do not use an update function because this is expensive at startup, and +-- `events.INITIALIZED` will create the first visible menubar and proper proxy. proxies.menubar = proxy_menu(default_menubar, function() end) -- Sets `ui.context_menu` and `ui.tab_context_menu` from menu item lists @@ -514,17 +515,22 @@ proxies.menubar = proxy_menu(default_menubar, function() end) -- @see ui.menu local function set_contextmenus(buffer_menu, tab_menu) contextmenu_items = {} -- reset - local menu = buffer_menu or default_context_menu - ui.context_menu = ui.menu(read_menu_table(menu, true)) - proxies.context_menu = proxy_menu(menu, set_contextmenus) - menu = tab_menu or default_tab_context_menu - ui.tab_context_menu = ui.menu(read_menu_table(menu, true)) - proxies.tab_context_menu = proxy_menu(menu, function() - set_contextmenus(nil, menu) - end) + local menus = { + context_menu = buffer_menu or default_context_menu, + tab_context_menu = tab_menu or default_tab_context_menu + } + for name, menu in pairs(menus) do + ui[name] = ui.menu(read_menu_table(menu, true)) + proxies[name] = proxy_menu(menu, function() + set_contextmenus(menus.context_menu, menus.tab_context_menu) + end) + end end events.connect(events.INITIALIZED, set_contextmenus) -- Define menu proxies for use by user scripts. +-- Do not use an update function because this is expensive at startup, and +-- `events.INITIALIZED` will create these visible menus and their proper +-- proxies. proxies.context_menu = proxy_menu(default_context_menu, function() end) proxies.tab_context_menu = proxy_menu(default_tab_context_menu, function() end) @@ -532,8 +538,8 @@ proxies.tab_context_menu = proxy_menu(default_tab_context_menu, function() end) 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', - _L['Unknown command:']..' '..tostring(action)) + assert(type(action) == 'function', string.format( + '%s %s', _L['Unknown command:'], action)) action() end) @@ -545,13 +551,14 @@ function M.select_command() -- Builds the item tables for the filtered list dialog. -- @param menu The menu to read from. local function build_command_tables(menu) - for i = 1, #menu do - if menu[i].title then - build_command_tables(menu[i]) - elseif menu[i][1] ~= '' then - local label = menu.title and menu.title..': '..menu[i][1] or menu[i][1] + for _, item in ipairs(menu) do + if item.title then + build_command_tables(item) + elseif item[1] ~= '' then -- item = {label, function} + local label = + menu.title and string.format('%s: %s', menu.title, item[1]) or item[1] items[#items + 1] = label:gsub('_([^_])', '%1') - items[#items + 1] = key_shortcuts[tostring(menu[i][2])] or '' + items[#items + 1] = key_shortcuts[tostring(item[2])] or '' end end end diff --git a/modules/textadept/run.lua b/modules/textadept/run.lua index f23babd7..cd7f7b51 100644 --- a/modules/textadept/run.lua +++ b/modules/textadept/run.lua @@ -73,33 +73,33 @@ local proc, cwd, preferred_view local function scan_for_error(message, ext_or_lexer) for key, patterns in pairs(M.error_patterns) do if ext_or_lexer and key ~= ext_or_lexer then goto continue end - for i = 1, #patterns do - if not message:find(patterns[i]) then goto continue end + for _, patt in ipairs(patterns) do + if not message:find(patt) then goto continue end -- Extract details from the warning or error. - local details, j = {message:match(patterns[i])}, 1 - for capture in patterns[i]:gmatch('[^%%](%b())') do + local detail, i = {message:match(patt)}, 1 + for capture in patt:gmatch('[^%%](%b())') do if capture == '(.-)' then - details.filename = details[j] + detail.filename = detail[i] elseif capture == '(%d+)' then - local line_or_column = not details.line and 'line' or 'column' - details[line_or_column] = tonumber(details[j]) + local line_or_column = not detail.line and 'line' or 'column' + detail[line_or_column] = tonumber(detail[i]) else - details.message = details[j] + detail.message = detail[i] end - j = j + 1 + i = i + 1 end - details.warning = message:lower():find('warning') and - not message:lower():find('error') + detail.warning = + message:lower():find('warning') and not message:lower():find('error') -- Compile and run commands specify the file extension or lexer used to -- determine the command, so the error patterns used are guaranteed to be -- correct. Build commands have no such context and instead iterate -- through all possible error patterns. Only consider the error/warning -- valid if the extracted filename's extension or lexer matches the error -- pattern's extension or lexer. - if ext_or_lexer then return details end - local ext = details.filename:match('[^/\\.]+$') + if ext_or_lexer then return detail end + local ext = detail.filename:match('[^/\\.]+$') local lexer = textadept.file_types.extensions[ext] - if ext == key or lexer == key then return details end + if ext == key or lexer == key then return detail end ::continue:: end ::continue:: @@ -117,14 +117,14 @@ end -- run commands. local function print_line(line, ext_or_lexer) local error = scan_for_error(line, ext_or_lexer) - ui.silent_print = (M.run_in_background or ext_or_lexer or - not line:find('^> ') or line:find('^> exit')) and true + ui.silent_print = M.run_in_background or ext_or_lexer or + not line:find('^> ') or line:find('^> exit') ui.print(not error and line or line:iconv('UTF-8', _CHARSET)) ui.silent_print = false if error then -- Current position is one line below the error due to ui.print()'s '\n'. - buffer:marker_add(buffer.line_count - 2, - error.warning and M.MARK_WARNING or M.MARK_ERROR) + buffer:marker_add( + buffer.line_count - 2, error.warning and M.MARK_WARNING or M.MARK_ERROR) end end @@ -137,19 +137,46 @@ local output_buffer -- run commands. local function print_output(output, ext_or_lexer) if output then - if output_buffer then output = output_buffer..output end + if output_buffer then output = output_buffer .. output end local remainder = 1 for line, e in output:gmatch('([^\r\n]*)\r?\n()') do print_line(line, ext_or_lexer) remainder = e end - output_buffer = remainder <= #output and string.sub(output, remainder) + output_buffer = remainder <= #output and output:sub(remainder) elseif output_buffer then print_line(output_buffer, ext_or_lexer) output_buffer = nil end end +-- Runs command *command* in working directory *dir*, emitting events of type +-- *event* with any output received. +-- @param command String command to run, or a function returning such a string +-- and optional working directory. A returned working directory overrides +-- *dir*. +-- @param dir String working directory to run *command* in. +-- @param event String event name to emit command output with. +-- @param macros Optional table of '%[char]' macros to expand within *command*. +-- @param ext_or_lexer Optional file extension or lexer name associated with the +-- executed command. This is used for better error detection in compile and +-- run commands. +local function run_command(command, dir, event, macros, ext_or_lexer) + local working_dir + if type(command) == 'function' then command, working_dir = command() end + if not command then return end + if macros then command = command:gsub('%%%a', macros) end + preferred_view = view + local function emit(output) events.emit(event, output, ext_or_lexer) end + cwd = (working_dir or dir):gsub('[/\\]$', '') + events.emit(event, string.format('> cd %s\n', cwd)) + events.emit(event, string.format('> %s\n', command:iconv('UTF-8', _CHARSET))) + proc = assert(os.spawn(command, cwd, emit, emit, function(status) + emit() -- flush + events.emit(event, string.format('> exit status: %d\n', status)) + end)) +end + -- Compiles or runs file *filename* based on a shell command in *commands*. -- @param filename The file to run. -- @param commands Either `compile_commands` or `run_commands`. @@ -158,37 +185,21 @@ local function compile_or_run(filename, commands) buffer:annotation_clear_all() io.save_file() end - -- Determine the command. local ext = filename:match('[^/\\.]+$') local lexer = filename == buffer.filename and buffer:get_lexer() or - textadept.file_types.extensions[ext] + textadept.file_types.extensions[ext] local command = commands[filename] or commands[ext] or commands[lexer] - local working_dir - if type(command) == 'function' then command, working_dir = command() end - if not command then return end - -- Replace macros in the command. local dirname, basename = '', filename if filename:find('[/\\]') then dirname, basename = filename:match('^(.+)[/\\]([^/\\]+)$') end - local basename_no_ext = basename:match('^(.+)%.') - command = command:gsub('%%([pdfe])', { - p = filename, d = dirname, f = basename, e = basename_no_ext - }) - -- Prepare to run the command. - preferred_view = view local event = commands == M.compile_commands and events.COMPILE_OUTPUT or - events.RUN_OUTPUT - local ext_or_lexer = commands[ext] and ext or lexer - local function emit(output) events.emit(event, output, ext_or_lexer) end - -- Run the command. - cwd = (working_dir or dirname):gsub('[/\\]$', '') - if cwd ~= dirname then events.emit(event, '> cd '..cwd..'\n') end - events.emit(event, '> '..command:iconv('UTF-8', _CHARSET)..'\n') - proc = assert(os.spawn(command, cwd, emit, emit, function(status) - emit() -- flush - events.emit(event, '> exit status: '..status..'\n') - end)) + events.RUN_OUTPUT + local macros = { + ['%p'] = filename, ['%d'] = dirname, ['%f'] = basename, + ['%e'] = basename:match('^(.+)%.') -- no extension + } + run_command(command, dirname, event, macros, commands[ext] and ext or lexer) end --- @@ -287,11 +298,10 @@ function M.build(root_directory) if not root_directory then return end end for i = 1, #_BUFFERS do _BUFFERS[i]:annotation_clear_all() end - -- Determine command. local command = M.build_commands[root_directory] if not command then for build_file, build_command in pairs(M.build_commands) do - if lfs.attributes(root_directory..'/'..build_file) then + if lfs.attributes(string.format('%s/%s', root_directory, build_file)) then local button, utf8_command = ui.dialogs.inputbox{ title = _L['Command'], informative_text = root_directory, text = build_command, button1 = _L['OK'], button2 = _L['Cancel'] @@ -301,20 +311,7 @@ function M.build(root_directory) end end end - local working_dir - if type(command) == 'function' then command, working_dir = command() end - if not command then return end - -- Prepare to run the command. - preferred_view = view - local function emit(output) events.emit(events.BUILD_OUTPUT, output) end - -- Run the command. - cwd = (working_dir or root_directory):gsub('[/\\]$', '') - events.emit(events.BUILD_OUTPUT, '> cd '..cwd..'\n') - events.emit(events.BUILD_OUTPUT, '> '..command:iconv('UTF-8', _CHARSET)..'\n') - proc = assert(os.spawn(command, cwd, emit, emit, function(status) - emit() -- flush - events.emit(events.BUILD_OUTPUT, '> exit status: '..status..'\n') - end)) + run_command(command, root_directory, events.BUILD_OUTPUT) end events.connect(events.BUILD_OUTPUT, print_output) @@ -325,7 +322,7 @@ function M.stop() if proc then proc:kill() end end -- Send line as input to process stdin on return. events.connect(events.CHAR_ADDED, function(code) - if code == 10 and proc and proc:status() == 'running' and + if code == string.byte('\n') and proc and proc:status() == 'running' and buffer._type == _L['[Message Buffer]'] then local line_num = buffer:line_from_position(buffer.current_pos) - 1 proc:write((buffer:get_line(line_num))) @@ -379,7 +376,7 @@ function M.goto_error(line, next) -- If no line was given, find the next warning or error marker. if not assert_type(line, 'number/nil', 1) and next ~= nil then - local f = buffer['marker_'..(next and 'next' or 'previous')] + local f = next and buffer.marker_next or buffer.marker_previous line = buffer:line_from_position(buffer.current_pos) local wrapped = false ::retry:: @@ -398,25 +395,25 @@ function M.goto_error(line, next) goto retry end end - textadept.editing.goto_line(line) -- ensure visible + buffer:goto_line(line) -- Goto the warning or error and show an annotation. local line = buffer:get_line(line):match('^[^\r\n]*') - local error = scan_for_error(line:iconv(_CHARSET, 'UTF-8')) - if not error then return end + local detail = scan_for_error(line:iconv(_CHARSET, 'UTF-8')) + if not detail then return end textadept.editing.select_line() - if not error.filename:find(not WIN32 and '^/' or '^%a:[/\\]') then - error.filename = cwd..(not WIN32 and '/' or '\\')..error.filename + if not detail.filename:find(not WIN32 and '^/' or '^%a:[/\\]') then + detail.filename = cwd .. (not WIN32 and '/' or '\\') .. detail.filename end - ui.goto_file(error.filename, true, preferred_view, true) - textadept.editing.goto_line(error.line - 1) - if error.column then - buffer:goto_pos(buffer:find_column(error.line - 1, error.column - 1)) + ui.goto_file(detail.filename, true, preferred_view, true) + textadept.editing.goto_line(detail.line - 1) + if detail.column then + buffer:goto_pos(buffer:find_column(detail.line - 1, detail.column - 1)) end - if error.message then - buffer.annotation_text[error.line - 1] = error.message + if detail.message then + buffer.annotation_text[detail.line - 1] = detail.message -- Style number 8 is the error style. - if not error.warning then buffer.annotation_style[error.line - 1] = 8 end + if not detail.warning then buffer.annotation_style[detail.line - 1] = 8 end end end events.connect(events.KEYPRESS, function(code) diff --git a/test/test.lua b/test/test.lua index b0d035b5..a4760039 100644 --- a/test/test.lua +++ b/test/test.lua @@ -29,7 +29,7 @@ function assert_equal(v1, v2) end ::continue:: end - error(string.format('%s ~= %s', tostring(v1), tostring(v2)), 2) + error(string.format('%s ~= %s', v1, v2), 2) end @@ -2183,6 +2183,8 @@ function test_menu_functions_interactive() io.close_buffer() end +-- TODO: test set arguments more thoroughly. + function test_menu_select_command_interactive() local num_buffers = #_BUFFERS textadept.menu.select_command() @@ -2259,6 +2261,7 @@ function test_run_build() textadept.run.build_commands[_HOME] = function() return 'lua modules/textadept/run/build.lua', _HOME .. '/test/' -- intentional trailing '/' end + textadept.run.stop() -- should not do anything textadept.run.build(_HOME) if #_VIEWS > 1 then view:unsplit() end assert_equal(buffer._type, _L['[Message Buffer]']) @@ -2272,6 +2275,7 @@ function test_run_build() assert(buffer:get_text():find('build%.lua'), 'did not run build command') assert(buffer:get_text():find('read "foo"'), 'did not send stdin') assert(buffer:get_text():find('> exit status: 9'), 'build not stopped') + textadept.run.stop() -- should not do anything io.close_buffer() -- TODO: chdir(_HOME) and textadept.run.build() -- no param. -- TODO: project whose makefile is autodetected. |