aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--core/args.lua30
-rw-r--r--core/events.lua6
-rw-r--r--core/file_io.lua71
-rw-r--r--core/iface.lua2
-rw-r--r--core/init.lua18
-rw-r--r--core/keys.lua31
-rw-r--r--core/lfs_ext.lua21
-rw-r--r--core/locale.lua10
-rw-r--r--core/ui.lua139
-rw-r--r--test/test.lua135
10 files changed, 283 insertions, 180 deletions
diff --git a/core/args.lua b/core/args.lua
index 83c15917..6bf2b9e4 100644
--- a/core/args.lua
+++ b/core/args.lua
@@ -72,18 +72,18 @@ if not CURSES then
print('Usage: textadept [args] [filenames]')
local list = {}
for name in pairs(switches) do list[#list + 1] = name end
- table.sort(list,
- function(a, b) return a:match('[^-]+') < b:match('[^-]+') end)
- for i = 1, #list do
- local switch = switches[list[i]]
- print(string.format(' %s [%d args]: %s', list[i], switch.narg,
- switch.description))
+ table.sort(
+ list, function(a, b) return a:match('[^-]+') < b:match('[^-]+') end)
+ for _, name in ipairs(list) do
+ local switch = switches[name]
+ print(string.format(
+ ' %s [%d args]: %s', name, switch.narg, switch.description))
end
os.exit()
end, 'Shows this')
-- Shows Textadept version and copyright on the command line.
M.register('-v', '--version', 0, function()
- print(_RELEASE..'\n'.._COPYRIGHT)
+ print(_RELEASE .. '\n' .. _COPYRIGHT)
quit()
end, 'Prints Textadept version and copyright')
-- After Textadept finishes initializing and processes arguments, remove the
@@ -99,21 +99,23 @@ end
-- Set `_G._USERHOME`.
-- This needs to be set as soon as possible since the processing of arguments is
-- positional.
-_USERHOME = os.getenv(not WIN32 and 'HOME' or 'USERPROFILE')..'/.textadept'
-for i = 1, #arg do
- if (arg[i] == '-u' or arg[i] == '--userhome') and arg[i + 1] then
+_USERHOME = os.getenv(not WIN32 and 'HOME' or 'USERPROFILE') .. '/.textadept'
+for i, switch in ipairs(arg) do
+ if (switch == '-u' or switch == '--userhome') and arg[i + 1] then
_USERHOME = arg[i + 1]
break
end
end
local mode = lfs.attributes(_USERHOME, 'mode')
assert(not mode or mode == 'directory', '"%s" is not a directory', _USERHOME)
-if not mode then assert(lfs.mkdir(_USERHOME), 'cannot create %s', _USERHOME) end
-local user_init = _USERHOME..'/init.lua'
+if not mode then
+ assert(lfs.mkdir(_USERHOME), 'cannot create "%s"', _USERHOME)
+end
+local user_init = _USERHOME .. '/init.lua'
mode = lfs.attributes(user_init, 'mode')
-assert(not mode or mode == 'file', '"%s" is not a file', user_init)
+assert(not mode or mode == 'file', '"%s" is not a file (%s)', user_init, mode)
if not mode then
- assert(io.open(user_init, 'w'), 'unable to create %s', user_init):close()
+ assert(io.open(user_init, 'w'), 'unable to create "%s"', user_init):close()
end
-- Placeholders.
diff --git a/core/events.lua b/core/events.lua
index 438bb5bf..0cbd8f2f 100644
--- a/core/events.lua
+++ b/core/events.lua
@@ -294,6 +294,10 @@ local M = {}
-- Emitted by [`buffer.zoom_in()`]() and [`buffer.zoom_out()`]().
module('events')]]
+-- Map of event names to tables of handler functions.
+-- Handler tables are auto-created as needed.
+-- @class table
+-- @name handlers
local handlers = setmetatable({}, {__index = function(t, k)
t[k] = {}
return t[k]
@@ -377,7 +381,7 @@ end)
-- Set event constants.
for _, v in pairs(_SCINTILLA.events) do M[v[1]:upper()] = v[1] end
-local textadept_events = {
+local textadept_events = { -- defined in C
'appleevent_odoc', 'buffer_after_switch', 'buffer_before_switch',
'buffer_deleted', 'buffer_new', 'csi', 'error', 'find', 'focus',
'initialized', 'keypress', 'menu_clicked', 'mouse', 'quit', 'replace',
diff --git a/core/file_io.lua b/core/file_io.lua
index 846b4a86..88db6907 100644
--- a/core/file_io.lua
+++ b/core/file_io.lua
@@ -99,8 +99,8 @@ function io.open_file(filenames, encodings)
if not assert_type(filenames, 'string/table/nil', 1) then
filenames = ui.dialogs.fileselect{
title = _L['Open File'], select_multiple = true,
- with_directory = (buffer.filename or ''):match('^.+[/\\]') or
- lfs.currentdir(),
+ with_directory =
+ (buffer.filename or ''):match('^.+[/\\]') or lfs.currentdir(),
width = CURSES and ui.size[1] - 2 or nil
}
if not filenames then return end
@@ -144,8 +144,7 @@ function io.open_file(filenames, encodings)
-- Detect EOL mode.
buffer.eol_mode = text:find('\r\n') and buffer.EOL_CRLF or buffer.EOL_LF
-- Insert buffer text and set properties.
- buffer:add_text(text, #text)
- buffer:goto_pos(0)
+ buffer:append_text(text)
buffer:empty_undo_buffer()
buffer.mod_time = lfs.attributes(filename, 'modification') or os.time()
buffer.filename = filename
@@ -174,10 +173,9 @@ function io.reload_file()
local text = f:read('a')
f:close()
if buffer.encoding then text = text:iconv('UTF-8', buffer.encoding) end
- buffer:clear_all()
- buffer:add_text(text, #text)
- buffer:line_scroll(0, first_visible_line)
+ buffer:set_text(text)
buffer:goto_pos(pos)
+ buffer.first_visible_line = first_visible_line
buffer:set_save_point()
buffer.mod_time = lfs.attributes(buffer.filename, 'modification')
end
@@ -192,10 +190,9 @@ local function set_encoding(buffer, encoding)
if encoding then text = text:iconv(encoding, buffer.encoding) end
end
if encoding then text = text:iconv('UTF-8', encoding) end
- buffer:clear_all()
- buffer:add_text(text, #text)
- buffer:line_scroll(0, first_visible_line)
+ buffer:set_text(text)
buffer:goto_pos(pos)
+ buffer.first_visible_line = first_visible_line
buffer.encoding = encoding
buffer.code_page = buffer.encoding and buffer.CP_UTF8 or 0
end
@@ -213,8 +210,7 @@ function io.save_file()
events.emit(events.FILE_BEFORE_SAVE, buffer.filename)
local text = buffer:get_text()
if buffer.encoding then text = text:iconv(buffer.encoding, 'UTF-8') end
- local f = assert(io.open(buffer.filename, 'wb'))
- f:write(text):close()
+ assert(io.open(buffer.filename, 'wb')):write(text):close()
buffer:set_save_point()
buffer.mod_time = lfs.attributes(buffer.filename, 'modification')
if buffer._type then buffer._type = nil end
@@ -265,13 +261,15 @@ end
function io.close_buffer()
local filename = buffer.filename or buffer._type or _L['Untitled']
if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end
- local confirm = not buffer.modify or ui.dialogs.msgbox{
- title = _L['Close without saving?'],
- text = _L['There are unsaved changes in'], informative_text = filename,
- icon = 'gtk-dialog-question', button1 = _L['Cancel'],
- button2 = _L['Close without saving']
- } == 2
- if not confirm then return nil end -- nil return won't propagate a key command
+ if buffer.modify then
+ local button = ui.dialogs.msgbox{
+ title = _L['Close without saving?'],
+ text = _L['There are unsaved changes in'], informative_text = filename,
+ icon = 'gtk-dialog-question', button1 = _L['Cancel'],
+ button2 = _L['Close without saving']
+ }
+ if button ~= 2 then return nil end -- do not propagate key command
+ end
buffer:delete()
return true
end
@@ -280,13 +278,13 @@ end
-- Closes all open buffers, prompting the user to continue if there are unsaved
-- buffers, and returns `true` if the user did not cancel.
-- No buffers are saved automatically. They must be saved manually.
--- @return `true` if user did not cancel.
+-- @return `true` if user did not cancel; `nil` otherwise.
-- @see io.close_buffer
-- @name close_all_buffers
function io.close_all_buffers()
while #_BUFFERS > 1 do
view:goto_buffer(_BUFFERS[#_BUFFERS])
- if not io.close_buffer() then return false end
+ if not io.close_buffer() then return nil end -- do not propagate key command
end
return io.close_buffer() -- the last one
end
@@ -299,7 +297,7 @@ local function update_modified_file()
if not mod_time or not buffer.mod_time then return end
if buffer.mod_time < mod_time then
buffer.mod_time = mod_time
- events.emit(events.FILE_CHANGED)
+ events.emit(events.FILE_CHANGED, buffer.filename)
end
end
events_connect(events.BUFFER_AFTER_SWITCH, update_modified_file)
@@ -309,14 +307,14 @@ 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()
+events_connect(events.FILE_CHANGED, function(filename)
local button = ui.dialogs.msgbox{
title = _L['Reload?'], text = _L['Reload modified file?'],
- informative_text = string.format('"%s"\n%s',
- buffer.filename:iconv('UTF-8', _CHARSET),
- _L['has been modified. Reload it?']),
+ informative_text = string.format(
+ '"%s"\n%s', filename:iconv('UTF-8', _CHARSET),
+ _L['has been modified. Reload it?']),
icon = 'gtk-dialog-question', button1 = _L['Yes'], button2 = _L['No'],
- width = CURSES and #buffer.filename > 40 and ui.size[1] - 2 or nil
+ width = CURSES and #filename > 40 and ui.size[1] - 2 or nil
}
if button == 1 then io.reload_file() end
end)
@@ -360,11 +358,13 @@ local vcs = {'.bzr', '.git', '.hg', '.svn'}
-- @return string root or nil
-- @name get_project_root
function io.get_project_root(path)
- local dir = assert_type(path, 'string/nil', 1) or
- (buffer.filename or lfs.currentdir()..'/'):match('^(.+)[/\\]')
+ if not assert_type(path, 'string/nil', 1) then
+ path = buffer.filename or lfs.currentdir()
+ end
+ local dir = path:match('^(.+)[/\\]?')
while dir do
for i = 1, #vcs do
- if lfs.attributes(dir..'/'..vcs[i], 'mode') then return dir end
+ if lfs.attributes(dir .. '/' .. vcs[i], 'mode') then return dir end
end
dir = dir:match('^(.+)[/\\]')
end
@@ -420,26 +420,25 @@ function io.quick_open(paths, filter, opts)
paths = io.get_project_root()
if not paths then return end
end
- assert_type(filter, 'string/table/nil', 2)
- assert_type(opts, 'table/nil', 3)
if type(paths) == 'string' then
if not filter then filter = io.quick_open_filters[paths] end
paths = {paths}
end
+ assert_type(filter, 'string/table/nil', 2)
+ assert_type(opts, 'table/nil', 3)
local utf8_list = {}
for i = 1, #paths do
lfs.dir_foreach(paths[i], function(filename)
if #utf8_list >= io.quick_open_max then return false end
- filename = filename:gsub('^%.[/\\]', '')
utf8_list[#utf8_list + 1] = filename:iconv('UTF-8', _CHARSET)
end, filter or lfs.default_filter)
end
if #utf8_list >= io.quick_open_max then
ui.dialogs.msgbox{
title = _L['File Limit Exceeded'],
- text = string.format('%d %s %d', io.quick_open_max,
- _L['files or more were found. Showing the first'],
- io.quick_open_max),
+ text = string.format(
+ '%d %s %d', io.quick_open_max,
+ _L['files or more were found. Showing the first'], io.quick_open_max),
icon = 'gtk-dialog-info'
}
end
diff --git a/core/iface.lua b/core/iface.lua
index d6d9066e..f3f3847a 100644
--- a/core/iface.lua
+++ b/core/iface.lua
@@ -59,6 +59,7 @@ local marker_number, indic_number, list_type, image_type = -1, -1, 0, 0
-- @see buffer.marker_define
-- @name next_marker_number
function M.next_marker_number()
+ assert(marker_number < M.constants.MARKER_MAX, 'too many markers in use')
marker_number = marker_number + 1
return marker_number
end
@@ -71,6 +72,7 @@ end
-- @see buffer.indic_style
-- @name next_indic_number
function M.next_indic_number()
+ assert(indic_number < M.constants.INDICATOR_MAX, 'too many indicators in use')
indic_number = indic_number + 1
return indic_number
end
diff --git a/core/init.lua b/core/init.lua
index 0f187e0a..8b4f3d48 100644
--- a/core/init.lua
+++ b/core/init.lua
@@ -1,13 +1,13 @@
-- Copyright 2007-2020 Mitchell mitchell.att.foicica.com. See LICENSE.
_RELEASE = 'Textadept 10.8'
-_COPYRIGHT = 'Copyright © 2007-2020 Mitchell. See LICENSE.\n'..
- 'http://foicica.com/textadept'
+_COPYRIGHT =
+ 'Copyright © 2007-2020 Mitchell. See LICENSE.\nhttp://foicica.com/textadept'
-package.path = _HOME..'/core/?.lua;'..package.path
+package.path = string.format('%s/core/?.lua;%s', _HOME, package.path)
---for i = 1, #arg do
--- if arg[i] == '-t' or arg[i] == '--test' then pcall(require, 'luacov') end
+--for _, arg in ipairs(arg) do
+-- if arg == '-t' or arg == '--test' then pcall(require, 'luacov') end
--end
require('assert')
@@ -24,18 +24,18 @@ _M = {} -- language modules table
-- pdcurses compatibility.
if CURSES and WIN32 then
function os.spawn(argv, ...)
- local current_dir = lfs.currentdir()
+ local cwd = lfs.currentdir()
local args, i = {...}, 1
if type(args[i]) == 'string' then
lfs.chdir(args[i]) -- cwd
i = i + 1
end
if type(args[i]) == 'table' then i = i + 1 end -- env (ignore)
- local p = io.popen(argv..' 2>&1')
+ local p = io.popen(assert_type(argv, 'string', 1) .. ' 2>&1')
if type(args[i]) == 'function' then args[i](p:read('a')) end -- stdout_cb
local status = select(3, p:close())
if type(args[i + 2]) == 'function' then args[i + 2](status) end -- exit_cb
- lfs.chdir(current_dir)
+ lfs.chdir(cwd) -- restore
return p
end
end
@@ -51,7 +51,7 @@ local function text_range(buffer, start_pos, end_pos)
if end_pos > buffer.length then end_pos = buffer.length end
buffer:set_target_range(start_pos, end_pos)
local text = buffer.target_text
- buffer:set_target_range(target_start, target_end) -- reset
+ buffer:set_target_range(target_start, target_end) -- restore
return text
end
events.connect(events.BUFFER_NEW, function() buffer.text_range = text_range end)
diff --git a/core/keys.lua b/core/keys.lua
index a6a9207a..f5002ae5 100644
--- a/core/keys.lua
+++ b/core/keys.lua
@@ -118,11 +118,11 @@ M.CLEAR = 'esc'
-- @class table
-- @name KEYSYMS
M.KEYSYMS = {
- -- From Scintilla.h.
- [7] = 'esc', [13] = '\n',
+ -- From Scintilla.h for CURSES.
+ [7] = 'esc', [8] = '\b', [9] = '\t', [13] = '\n',
-- From curses.h.
[263] = '\b', [343] = '\n',
- -- From Scintilla.h.
+ -- From Scintilla.h for CURSES.
[300] = 'down', [301] = 'up', [302] = 'left', [303] = 'right', [304] = 'home',
[305] = 'end', [306] = 'pgup', [307] = 'pgdn', [308] = 'del', [309] = 'ins',
-- From <gdk/gdkkeysyms.h>.
@@ -181,7 +181,8 @@ local function key_command(prefix)
end
if type(key) ~= 'function' and type(key) ~= 'table' then return INVALID end
if type(key) == 'table' then
- ui.statusbar_text = _L['Keychain:']..' '..table.concat(keychain, ' ')
+ ui.statusbar_text = string.format(
+ '%s %s', _L['Keychain:'], table.concat(keychain, ' '))
return CHAIN
end
return select(2, xpcall(key, key_error)) == false and PROPAGATE or HALT
@@ -202,26 +203,22 @@ local function keypress(code, shift, control, alt, meta, caps_lock)
if caps_lock and (shift or control or alt or meta) and code < 256 then
code = string[shift and 'upper' or 'lower'](string.char(code)):byte()
end
- local key = code < 256 and (not CURSES or (code ~= 7 and code ~= 13)) and
- string.char(code) or M.KEYSYMS[code]
+ local key = code >= 32 and code < 256 and string.char(code) or M.KEYSYMS[code]
if not key then return end
-- Since printable characters are uppercased, disable shift.
- if shift and code < 256 and code ~= 9 then shift = false end
+ if shift and code >= 32 and code < 256 then shift = false end
-- For composed keys on OSX, ignore alt.
if OSX and alt and code < 256 then alt = false end
- local key_seq = string.format('%s%s%s%s%s', control and CTRL or '',
- alt and ALT or '', meta and OSX and META or '',
- shift and SHIFT or '', key)
+ local key_seq = string.format(
+ '%s%s%s%s%s', control and CTRL or '', alt and ALT or '',
+ meta and OSX and META or '', shift and SHIFT or '', key)
--print(key_seq)
ui.statusbar_text = ''
--if CURSES then ui.statusbar_text = string.format('"%s"', key_seq) end
- local keychain_size = #keychain
- if keychain_size > 0 and key_seq == M.CLEAR then
- clear_key_sequence()
- return true
- end
- keychain[keychain_size + 1] = key_seq
+ local in_chain = #keychain > 0
+ if in_chain and key_seq == M.CLEAR then clear_key_sequence() return true end
+ keychain[#keychain + 1] = key_seq
local status = PROPAGATE
if not M.MODE then
@@ -232,7 +229,7 @@ local function keypress(code, shift, control, alt, meta, caps_lock)
end
if status ~= CHAIN then clear_key_sequence() end
if status > PROPAGATE then return true end -- CHAIN or HALT
- if status == INVALID and keychain_size > 0 then
+ if status == INVALID and in_chain then
ui.statusbar_text = _L['Invalid sequence']
return true
end
diff --git a/core/lfs_ext.lua b/core/lfs_ext.lua
index 6d9fe2af..ada0431e 100644
--- a/core/lfs_ext.lua
+++ b/core/lfs_ext.lua
@@ -81,7 +81,8 @@ function lfs.dir_foreach(dir, f, filter, n, include_dirs, level)
end
for basename in lfs.dir(dir) do
if basename:find('^%.%.?$') then goto continue end -- ignore . and ..
- local filename = dir..(dir ~= '/' and '/' or '')..basename
+ local filename = string.format(
+ '%s%s%s', dir, dir ~= '/' and '/' or '', basename)
local mode = lfs.attributes(filename, 'mode')
if mode ~= 'directory' and mode ~= 'file' then goto continue end
local include
@@ -98,15 +99,14 @@ function lfs.dir_foreach(dir, f, filter, n, include_dirs, level)
-- Treat inclusive patterns as logical OR.
include = include or (not patt:find('^!') and filename:find(patt))
end
- local dir_sep = not WIN32 and '/' or '\\'
- local os_filename = not WIN32 and filename or filename:gsub('/', dir_sep)
+ local sep = not WIN32 and '/' or '\\'
+ local os_filename = not WIN32 and filename or filename:gsub('/', sep)
if include and mode == 'directory' then
- if include_dirs and f(os_filename..dir_sep) == false then return end
- if not n or (level or 0) < n then
- local halt = lfs.dir_foreach(filename, f, filter, n, include_dirs,
- (level or 0) + 1) == false
- if halt then return false end
- end
+ if include_dirs and f(os_filename .. sep) == false then return false end
+ if n and (level or 0) >= n then goto continue end
+ local halt = lfs.dir_foreach(
+ filename, f, filter, n, include_dirs, (level or 0) + 1) == false
+ if halt then return false end
elseif include and mode == 'file' then
if f(os_filename) == false then return false end
end
@@ -129,7 +129,8 @@ function lfs.abspath(filename, prefix)
if not filename:find(not WIN32 and '^/' or '^%a:[/\\]') and
not (WIN32 and filename:find('^\\\\')) then
if not prefix then prefix = lfs.currentdir() end
- filename = prefix..(not WIN32 and '/' or '\\')..filename
+ filename = string.format(
+ '%s%s%s', prefix, not WIN32 and '/' or '\\', filename)
end
filename = filename:gsub('%f[^/\\]%.[/\\]', '') -- clean up './'
while filename:find('[^/\\]+[/\\]%.%.[/\\]') do
diff --git a/core/locale.lua b/core/locale.lua
index 45a0f855..f6bc32cb 100644
--- a/core/locale.lua
+++ b/core/locale.lua
@@ -11,12 +11,14 @@ local M = {}
-- use.
module('_L')]]
-local f = io.open(_USERHOME..'/locale.conf', 'rb')
+local f = io.open(_USERHOME .. '/locale.conf', 'rb')
if not f then
local lang = (os.getenv('LANG') or ''):match('^[^_.@]+') -- TODO: LC_MESSAGES?
- if lang then f = io.open(_HOME..'/core/locales/locale.'..lang..'.conf') end
+ if lang then
+ f = io.open(string.format('%s/core/locales/locale.%s.conf', _HOME, lang))
+ end
end
-if not f then f = io.open(_HOME..'/core/locale.conf', 'rb') end
+if not f then f = io.open(_HOME .. '/core/locale.conf', 'rb') end
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
@@ -30,5 +32,5 @@ for line in f:lines() do
end
f:close()
-setmetatable(M, {__index = function(_, k) return 'No Localization:'..k end})
+setmetatable(M, {__index = function(_, k) return 'No Localization:' .. k end})
return M
diff --git a/core/ui.lua b/core/ui.lua
index 65b1fb4d..a90bd9b6 100644
--- a/core/ui.lua
+++ b/core/ui.lua
@@ -101,42 +101,38 @@ ui.dialogs = setmetatable({}, {__index = function(_, k)
-- @param options Table of key-value command line options for gtdialog.
-- @return Lua objects depending on the dialog kind
return function(options)
- -- Set up dialog defaults and convert any 1-based indices to 0-based ones.
if not options.button1 then options.button1 = _L['OK'] end
- local select = options.select
- if type(select) == 'number' then
- options.select = select - 1
- elseif type(select) == 'table' then
- for i = 1, #select do select[i] = select[i] - 1 end
- end
-- 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
- args[#args + 1] = '--'..option:gsub('_', '-')
- if type(value) == 'boolean' then goto continue end
+ args[#args + 1] = '--' .. option:gsub('_', '-')
if type(value) == 'table' then
for i, val in ipairs(value) do
- assert_type(val, 'string/number', option..'['..i..']')
+ local narg = string.format('%s[%d]', option, i)
+ assert_type(val, 'string/number', narg)
if option == 'palette' and type(val) == 'number' then
value[i] = torgb(val) -- nil return is okay
+ elseif option == 'select' and assert_type(val, 'number', narg) then
+ value[i] = val - 1 -- convert from 1-based index to 0-based index
end
end
elseif option == 'color' and type(value) == 'number' then
value = torgb(value)
+ elseif option == 'select' and assert_type(value, 'number', option) then
+ value = value - 1 -- convert from 1-based index to 0-based index
end
- args[#args + 1] = value
+ if type(value) ~= 'boolean' then args[#args + 1] = value end
end
- ::continue::
end
-- Call gtdialog, stripping any trailing newline in the standard output.
- local result = ui.dialog(k:gsub('_', '-'), table.unpack(args))
- result = result:match('^(.-)\n?$')
+ local result = ui.dialog(
+ k:gsub('_', '-'), table.unpack(args)):match('^(.-)\n?$')
-- Depending on the dialog type, transform the result into Lua objects.
if k == 'fileselect' or k == 'filesave' then
if result == '' then return nil end
- if not CURSES and WIN32 then result = result:iconv(_CHARSET, 'UTF-8') end
+ if WIN32 and not CURSES then result = result:iconv(_CHARSET, 'UTF-8') end
if k == 'filesave' or not options.select_multiple then return result end
local filenames = {}
for filename in result:gmatch('[^\n]+') do
@@ -153,7 +149,7 @@ ui.dialogs = setmetatable({}, {__index = function(_, k)
options.string_output, options.select_multiple = true, true
end
local items, patt = {}, not k:find('input') and '[^\n]+' or '([^\n]*)\n'
- for item in (value..'\n'):gmatch(patt) do
+ for item in (value .. '\n'):gmatch(patt) do
items[#items + 1] = options.string_output and item or tonumber(item) + 1
end
return button, options.select_multiple and items or items[1]
@@ -180,8 +176,7 @@ local buffers_zorder = {}
-- Adds new buffers to the z-order list.
events.connect(events.BUFFER_NEW, function()
- if buffer == ui.command_entry then return end -- ignore this buffer
- table.insert(buffers_zorder, 1, buffer)
+ if buffer ~= ui.command_entry then table.insert(buffers_zorder, 1, buffer) end
end)
-- Updates the z-order list.
@@ -200,10 +195,10 @@ events.connect(events.BUFFER_AFTER_SWITCH, update_zorder)
events.connect(events.VIEW_AFTER_SWITCH, update_zorder)
-- Saves and restores buffer zorder data during a reset.
-events.connect(events.RESET_BEFORE,
- function(persist) persist.buffers_zorder = buffers_zorder end)
-events.connect(events.RESET_AFTER,
- function(persist) buffers_zorder = persist.buffers_zorder end)
+events.connect(
+ events.RESET_BEFORE, function(persist) persist.ui_zorder = buffers_zorder end)
+events.connect(
+ events.RESET_AFTER, function(persist) buffers_zorder = persist.ui_zorder end)
---
-- Prompts the user to select a buffer to switch to.
@@ -221,7 +216,7 @@ function ui.switch_buffer(zorder)
local filename = buffer.filename or buffer._type or _L['Untitled']
if buffer.filename then filename = filename:iconv('UTF-8', _CHARSET) end
local basename = buffer.filename and filename:match('[^/\\]+$') or filename
- utf8_list[#utf8_list + 1] = (buffer.modify and '*' or '')..basename
+ utf8_list[#utf8_list + 1] = (buffer.modify and '*' or '') .. basename
utf8_list[#utf8_list + 1] = filename
end
local button, i = ui.dialogs.filteredlist{
@@ -255,8 +250,9 @@ end
-- @name goto_file
function ui.goto_file(filename, split, preferred_view, sloppy)
assert_type(filename, 'string', 1)
- local patt = not sloppy and '^'..filename..'$' or
- filename:match('[^/\\]+$')..'$' -- TODO: escape filename properly
+ local patt = string.format( -- TODO: escape filename properly
+ '%s%s$', not sloppy and '^' or '',
+ not sloppy and filename or filename:match('[^/\\]+$'))
if WIN32 then
patt = patt:gsub('%a', function(letter)
return string.format('[%s%s]', letter:upper(), letter:lower())
@@ -285,8 +281,8 @@ local events, events_connect = events, events.connect
events_connect(events.VIEW_NEW, function() events.emit(events.UPDATE_UI) end)
-- Switches between buffers when a tab is clicked.
-events_connect(events.TAB_CLICKED,
- function(index, button) view:goto_buffer(_BUFFERS[index]) end)
+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.
local function set_title()
@@ -295,9 +291,9 @@ local function set_title()
filename = select(2, pcall(string.iconv, filename, 'UTF-8', _CHARSET))
end
local basename = buffer.filename and filename:match('[^/\\]+$') or filename
- ui.title = string.format('%s %s Textadept (%s)', basename,
- buffer.modify and '*' or '-', filename)
- buffer.tab_label = basename..(buffer.modify and '*' or '')
+ ui.title = string.format(
+ '%s %s Textadept (%s)', basename, buffer.modify and '*' or '-', filename)
+ buffer.tab_label = basename .. (buffer.modify and '*' or '')
end
-- Changes Textadept title to show the buffer as being "clean" or "dirty".
@@ -306,40 +302,37 @@ events_connect(events.SAVE_POINT_LEFT, set_title)
-- Open uri(s).
events_connect(events.URI_DROPPED, function(utf8_uris)
- for utf8_uri in utf8_uris:gmatch('[^\r\n]+') do
- if utf8_uri:find('^file://') then
- local path = utf8_uri:match('^file://([^\r\n]+)')
- path = path:gsub('%%(%x%x)', function(hex)
- return string.char(tonumber(hex, 16))
- end):iconv(_CHARSET, 'UTF-8')
- -- In WIN32, ignore a leading '/', but not '//' (network path).
- if WIN32 and not path:match('^//') then path = path:sub(2, -1) end
- local mode = lfs.attributes(path, 'mode')
- if mode and mode ~= 'directory' then io.open_file(path) end
- end
+ 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))
+ end):iconv(_CHARSET, 'UTF-8')
+ -- In WIN32, ignore a leading '/', but not '//' (network path).
+ if WIN32 and not path:match('^//') then path = path:sub(2, -1) end
+ local mode = lfs.attributes(path, 'mode')
+ if mode and mode ~= 'directory' then io.open_file(path) end
end
ui.goto_view(view) -- work around any view focus synchronization issues
end)
events_connect(events.APPLEEVENT_ODOC, function(uri)
- return events.emit(events.URI_DROPPED, 'file://'..uri)
+ return events.emit(events.URI_DROPPED, 'file://' .. uri)
end)
-local GETLEXERLANGUAGE = _SCINTILLA.properties.lexer_language[1]
-- Sets buffer statusbar text.
events_connect(events.UPDATE_UI, function(updated)
if updated and 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 line, max = buffer:line_from_position(pos) + 1, buffer.line_count
local col = buffer.column[pos] + 1
- local lexer = buffer:private_lexer_call(GETLEXERLANGUAGE):match('^[^/]+')
+ local lexer = 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 tabs = string.format(
+ '%s %d', buffer.use_tabs and _L['Tabs:'] or _L['Spaces:'], buffer.tab_width)
local enc = buffer.encoding or ''
- local text = not CURSES and '%s %d/%d %s %d %s %s %s %s' or
- '%s %d/%d %s %d %s %s %s %s'
- ui.bufstatusbar_text = string.format(text, _L['Line:'], line, max, _L['Col:'],
- col, lexer, eol, tabs, enc)
+ ui.bufstatusbar_text = string.format(
+ text, _L['Line:'], line, max, _L['Col:'], col, lexer, eol, tabs, enc)
end)
-- Save buffer properties.
@@ -353,11 +346,11 @@ events_connect(events.BUFFER_BEFORE_SWITCH, function()
buffer._top_line = buffer:doc_line_from_visible(buffer.first_visible_line)
buffer._x_offset = buffer.x_offset
-- Save fold state.
- buffer._folds = {}
- local folds, i = buffer._folds, buffer:contracted_fold_next(0)
- while i >= 0 do
+ local folds, i = {}, buffer:contracted_fold_next(0)
+ while i ~= -1 do
folds[#folds + 1], i = i, buffer:contracted_fold_next(i + 1)
end
+ buffer._folds = folds
end)
-- Restore buffer properties.
@@ -368,20 +361,17 @@ events_connect(events.BUFFER_AFTER_SWITCH, function()
for i = 1, #buffer._folds do buffer:toggle_fold(buffer._folds[i]) end
-- Restore view state.
buffer:set_sel(buffer._anchor, buffer._current_pos)
- local n = buffer.main_selection
- buffer.selection_n_anchor_virtual_space[n] = buffer._anchor_virtual_space
- buffer.selection_n_caret_virtual_space[n] = buffer._caret_virtual_space
+ buffer.selection_n_anchor_virtual_space[0] = buffer._anchor_virtual_space
+ buffer.selection_n_caret_virtual_space[0] = buffer._caret_virtual_space
buffer:choose_caret_x()
- buffer:line_scroll(0, buffer:visible_from_doc_line(buffer._top_line) -
- buffer.first_visible_line)
+ local _top_line, top_line = buffer._top_line, buffer.first_visible_line
+ buffer:line_scroll(0, buffer:visible_from_doc_line(_top_line) - top_line)
buffer.x_offset = buffer._x_offset or 0
end)
-- Updates titlebar and statusbar.
local function update_bars()
set_title()
- local SETDIRECTPOINTER = _SCINTILLA.properties.doc_pointer[2]
- buffer:private_lexer_call(SETDIRECTPOINTER, buffer.direct_pointer)
events.emit(events.UPDATE_UI)
end
events_connect(events.BUFFER_NEW, update_bars)
@@ -416,8 +406,8 @@ end
events_connect(events.BUFFER_AFTER_SWITCH, restore_view_state)
events_connect(events.VIEW_AFTER_SWITCH, restore_view_state)
-events_connect(events.RESET_AFTER,
- function() ui.statusbar_text = _L['Lua reset'] end)
+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()
@@ -429,19 +419,20 @@ events_connect(events.QUIT, function()
utf8_list[#utf8_list + 1] = filename
end
end
- local cancel = #utf8_list > 0 and ui.dialogs.msgbox{
+ if #utf8_list == 0 then return end
+ local button = ui.dialogs.msgbox{
title = _L['Quit without saving?'],
text = _L['The following buffers are unsaved:'],
informative_text = table.concat(utf8_list, '\n'),
icon = 'gtk-dialog-question', button1 = _L['Cancel'],
button2 = _L['Quit without saving']
- } ~= 2
- if cancel then return true end -- prevent quit
+ }
+ 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(events.BUFFER_BEFORE_SWITCH,
- function() view._prev_buffer = buffer end)
+events_connect(
+ events.BUFFER_BEFORE_SWITCH, function() view._prev_buffer = buffer end)
events_connect(events.BUFFER_DELETED, function()
if _BUFFERS[view._prev_buffer] and buffer ~= view._prev_buffer then
view:goto_buffer(view._prev_buffer)
@@ -451,11 +442,11 @@ 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 clipboard_text
- events.connect(events.VIEW_BEFORE_SWITCH,
- function() clipboard_text = ui.clipboard_text end)
- events.connect(events.VIEW_AFTER_SWITCH,
- function() ui.clipboard_text = clipboard_text end)
+ 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)
if not WIN32 then
local function enable_mouse() io.stdout:write("\x1b[?1002h"):flush() end
@@ -477,8 +468,8 @@ if CURSES then
return get_view(view[1], y, x)
elseif vertical and x > size or not vertical and y > size then
-- Zero y or x relative to the other view based on split orientation.
- return get_view(view[2], vertical and y or y - size - 1,
- vertical and x - size - 1 or x)
+ return get_view(
+ view[2], vertical and y or y - size - 1, vertical and x - size - 1 or x)
else
return view -- in-between views; return the split itself
end
diff --git a/test/test.lua b/test/test.lua
index 72883202..450ba474 100644
--- a/test/test.lua
+++ b/test/test.lua
@@ -4,7 +4,6 @@
local function LINE(i) return i - 1 end
local function POS(i) return i - 1 end
local function INDEX(i) return i - 1 end
-if CURSES then function update_ui() end end
local _tostring = tostring
-- Overloads tostring() to print more user-friendly output for `assert_equal()`.
@@ -285,6 +284,7 @@ function test_file_io_open_file_detect_encoding()
assert_raises(function() io.open_file(1) end, 'string/table/nil expected, got number')
assert_raises(function() io.open_file('/tmp/foo', true) end, 'string/table/nil expected, got boolean')
+ -- TODO: encoding failure
end
function test_file_io_open_file_detect_newlines()
@@ -374,6 +374,7 @@ end
function test_file_io_save_file()
buffer.new()
+ buffer._type = '[Foo Buffer]'
buffer:append_text('foo')
local filename = os.tmpname()
io.save_file_as(filename)
@@ -381,6 +382,7 @@ function test_file_io_save_file()
local contents = f:read('a')
f:close()
assert_equal(contents, buffer:get_text())
+ assert(not buffer._type, 'still has a type')
buffer:append_text('bar')
io.save_all_files()
f = assert(io.open(filename))
@@ -395,7 +397,8 @@ end
function test_file_io_file_detect_modified()
local modified = false
- local handler = function()
+ local handler = function(filename)
+ assert_type(filename, 'string', 1)
modified = true
return false -- halt propagation
end
@@ -465,6 +468,7 @@ function test_file_io_get_project_root()
lfs.chdir(cwd)
assert_equal(io.get_project_root(_HOME), _HOME)
assert_equal(io.get_project_root(_HOME .. '/core'), _HOME)
+ assert_equal(io.get_project_root(_HOME .. '/core/init.lua'), _HOME)
assert_equal(io.get_project_root('/tmp'), nil)
assert_raises(function() io.get_project_root(1) end, 'string/nil expected, got number')
@@ -481,6 +485,8 @@ function test_file_io_quick_open_interactive()
assert(buffer.filename:find('%.lua$'), '.lua file filter did not work')
io.close_buffer()
end
+ io.quick_open_filters[dir] = true
+ assert_raises(function() io.quick_open(dir) end, 'string/table/nil expected, got boolean')
io.quick_open_filters[_HOME] = '.lua'
io.quick_open()
if #_BUFFERS > num_buffers then
@@ -494,6 +500,80 @@ function test_file_io_quick_open_interactive()
assert_raises(function() io.quick_open(_HOME, nil, 1) end, 'table/nil expected, got number')
end
+function test_keys_keychain()
+ local ca = keys.ca
+ local foo = false
+ keys.ca = {a = function() foo = true end}
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert(not foo, 'foo set outside keychain')
+ events.emit(events.KEYPRESS, string.byte('a'), false, true)
+ assert_equal(#keys.keychain, 1)
+ --assert_equal(keys.keychain[1], 'ca')
+ events.emit(events.KEYPRESS, not CURSES and 0xFF1B or 7) -- esc
+ assert_equal(#keys.keychain, 0, 'keychain not canceled')
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert(not foo, 'foo set outside keychain')
+ events.emit(events.KEYPRESS, string.byte('a'), false, true)
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert(foo, 'foo not set')
+ keys.ca = ca -- restore
+end
+
+function test_keys_propagation()
+ buffer:new()
+ local foo, bar, baz = false, false, false
+ keys.a = function() foo = true end
+ keys.b = function() bar = true end
+ keys.c = function() baz = true end
+ keys.cpp = {
+ a = function() end, -- halt
+ b = function() return false end, -- propagate
+ c = function()
+ keys.MODE = 'test_mode'
+ return false -- propagate
+ end
+ }
+ buffer:set_lexer('cpp')
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert(not foo, 'foo set')
+ events.emit(events.KEYPRESS, string.byte('b'))
+ assert(bar, 'bar set')
+ events.emit(events.KEYPRESS, string.byte('c'))
+ assert(not baz, 'baz set') -- mode changed, so cannot propagate to keys.c
+ assert_equal(keys.MODE, 'test_mode')
+ keys.MODE = nil
+ keys.a, keys.b, keys.c, keys.cpp = nil, nil, nil, nil -- reset
+ io.close_buffer()
+end
+
+function test_keys_modes()
+ buffer.new()
+ local foo, bar = false, false
+ keys.a = function() foo = true end
+ keys.test_mode = {a = function()
+ bar = true
+ keys.MODE = nil
+ return false -- propagate
+ end}
+ keys.cpp = {a = function() keys.MODE = 'test_mode' end}
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert(foo, 'foo not set')
+ assert(not keys.MODE, 'key mode entered')
+ assert(not bar, 'bar set outside mode')
+ foo = false
+ buffer:set_lexer('cpp')
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert_equal(keys.MODE, 'test_mode')
+ assert(not foo, 'foo set outside mode')
+ assert(not bar, 'bar set outside mode')
+ events.emit(events.KEYPRESS, string.byte('a'))
+ assert(bar, 'bar not set')
+ assert(not keys.MODE, 'key mode still active')
+ assert(not foo, 'foo set') -- TODO: should this propagate?
+ keys.a, keys.test_mode, keys.cpp = nil, nil, nil -- reset
+ io.close_buffer()
+end
+
function test_lfs_ext_dir_foreach()
local files, directories = 0, 0
lfs.dir_foreach(_HOME .. '/core', function(filename)
@@ -556,6 +636,27 @@ function test_lfs_ext_dir_foreach_max_depth()
assert_equal(count, 1) -- init.lua
end
+function test_lfs_ext_dir_foreach_halt()
+ local count, count_at_halt = 0, 0
+ lfs.dir_foreach(_HOME .. '/core', function(filename)
+ count = count + 1
+ if filename:find('/locales/.') then
+ count_at_halt = count
+ return false
+ end
+ end)
+ assert_equal(count, count_at_halt)
+
+ lfs.dir_foreach(_HOME .. '/core', function(filename)
+ count = count + 1
+ if filename:find('[/\\]$') then
+ count_at_halt = count
+ return false
+ end
+ end, nil, nil, true)
+ assert_equal(count, count_at_halt)
+end
+
function test_lfs_ext_dir_foreach_win32()
local win32 = _G.WIN32
_G.WIN32 = true
@@ -660,6 +761,7 @@ function test_ui_dialogs_dropdown_interactive()
assert_equal(i, 2)
end
+ assert_raises(function() ui.dialogs.dropdown{items = {'foo', 'bar', 'baz'}, select = true} end, "bad argument #select to 'dropdown' (number expected, got boolean")
assert_raises(function() ui.dialogs.dropdown{items = {'foo', 'bar', 'baz', true}} end, "bad argument #items[4] to 'dropdown' (string/number expected, got boolean")
end
@@ -755,6 +857,8 @@ function test_ui_dialogs_optionselect_interactive()
items = {'foo', 'bar', 'baz'}, select = {1, 3}, string_output = true
}
assert_equal(selected, {'foo', 'baz'})
+
+ assert_raises(function() ui.dialogs.optionselect{items = {'foo', 'bar', 'baz'}, select = {1, 'bar'}} end, "bad argument #select[2] to 'optionselect' (number expected, got string")
end
function test_ui_dialogs_textbox_interactive()
@@ -924,12 +1028,12 @@ function test_command_entry_run()
ui.command_entry.run(function(command) command_run = command end, {
['\t'] = function() tab_pressed = true end
}, nil, 2)
- update_ui() -- redraw command entry
+ ui.update() -- redraw command entry
assert_equal(ui.command_entry:get_lexer(), 'text')
assert(ui.command_entry.height > ui.command_entry:text_height(0), 'height < 2 lines')
ui.command_entry:set_text('foo')
events.emit(events.KEYPRESS, string.byte('\t'))
- events.emit(events.KEYPRESS, string.byte('\n'))
+ events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
assert_equal(command_run, 'foo')
assert(tab_pressed, '\\t not registered')
@@ -943,7 +1047,7 @@ local function run_lua_command(command)
ui.command_entry:set_text(command)
ui.command_entry.run()
assert_equal(ui.command_entry:get_lexer(), 'lua')
- events.emit(events.KEYPRESS, string.byte('\n'))
+ events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
end
function test_command_entry_run_lua()
@@ -1785,7 +1889,7 @@ function test_ui_find_incremental()
ui.command_entry:add_text('o') -- simulate keypress
assert_equal(buffer.selection_start, POS(1) + 1)
assert_equal(buffer.selection_end, buffer.selection_start + 3)
- events.emit(events.KEYPRESS, string.byte('\n'))
+ events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
assert_equal(buffer.selection_start, buffer:position_from_line(LINE(2)))
assert_equal(buffer.selection_end, buffer.selection_start + 3)
events.emit(events.KEYPRESS, string.byte('q'))
@@ -1796,11 +1900,11 @@ function test_ui_find_incremental()
ui.command_entry:delete_back() -- simulate keypress
assert_equal(buffer.selection_start, buffer:position_from_line(LINE(2)))
assert_equal(buffer.selection_end, buffer.selection_start + 3)
- events.emit(events.KEYPRESS, string.byte('\n'))
+ events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n
assert_equal(buffer.selection_start, buffer:position_from_line(LINE(3)))
assert_equal(buffer.selection_end, buffer.selection_start + 3)
ui.find.match_case = true
- events.emit(events.KEYPRESS, string.byte('\n')) -- wrap
+ events.emit(events.KEYPRESS, not CURSES and 0xFF0D or 343) -- \n, wrap
assert_equal(buffer.selection_start, POS(1) + 1)
assert_equal(buffer.selection_end, buffer.selection_start + 3)
ui.find.match_case = false
@@ -2015,7 +2119,7 @@ function test_menu_menu_functions()
textadept.menu.menubar[_L['Buffer']][_L['Toggle View Whitespace']][2]()
assert(buffer.view_ws ~= view_whitespace, 'view whitespace not toggled')
view:split()
- update_ui()
+ ui.update()
local size = view.size
textadept.menu.menubar[_L['View']][_L['Grow View']][2]()
assert(view.size > size, 'view shrunk')
@@ -2062,7 +2166,7 @@ function test_run_compile_run()
textadept.run.compile(compile_file)
assert_equal(#_BUFFERS, 2)
assert_equal(buffer._type, _L['[Message Buffer]'])
- update_ui() -- process output
+ ui.update() -- process output
assert(buffer:get_text():find("'end' expected"), 'no compile error')
assert(buffer:get_text():find('> exit status: 256'), 'no compile error')
if #_VIEWS > 1 then view:unsplit() end
@@ -2082,7 +2186,7 @@ function test_run_compile_run()
assert_equal(buffer.filename, compile_file)
local compile_command = textadept.run.compile_commands.lua
textadept.run.compile() -- clears annotation
- update_ui() -- process output
+ ui.update() -- process output
view:goto_buffer(1)
assert(not buffer.annotation_text[LINE(3)]:find("'end' expected"), 'annotation visible')
io.close_buffer() -- compile_file
@@ -2094,7 +2198,7 @@ function test_run_compile_run()
io.open_file(run_file)
textadept.run.run()
assert_equal(buffer._type, _L['[Message Buffer]'])
- update_ui() -- process output
+ ui.update() -- process output
assert(buffer:get_text():find('attempt to call a nil value'), 'no run error')
textadept.run.goto_error(nil, false)
assert_equal(buffer.filename, run_file)
@@ -2127,8 +2231,9 @@ function test_run_build()
os.execute('sleep 0.1') -- ensure process is running
buffer:add_text('foo')
buffer:new_line() -- should send previous line as stdin
+ os.execute('sleep 0.1') -- ensure process processed stdin
textadept.run.stop()
- update_ui() -- process output
+ ui.update() -- process output
assert(buffer:get_text():find('> cd '), 'did not change directory')
assert(buffer:get_text():find('build%.lua'), 'did not run build command')
assert(buffer:get_text():find('read "foo"'), 'did not send stdin')
@@ -2618,12 +2723,12 @@ for i = 1, #tests do
local name, f, attempts = tests[i], _ENV[tests[i]], 1
::retry::
print(string.format('Running %s', name))
- update_ui()
+ ui.update()
local ok, errmsg = xpcall(f, function(errmsg)
local fail = not expected_failures[f] and 'Failed!' or 'Expected failure.'
return string.format('%s %s', fail, debug.traceback(errmsg, 3))
end)
- update_ui()
+ ui.update()
if not ok and unstable_tests[f] and attempts < 3 then
cleanup()
print('Failed, but unstable. Trying again.')