diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 3abe56da..ddc2038b 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -321,6 +321,7 @@ jobs: name: ${{ env.IDE_DEBUG_APK_NAME }} path: ${{ env.ARTIFACT_FOLDER }} - uses: actions/download-artifact@v4 + continue-on-error: true with: name: ${{ env.IDE_APK_NAME }} path: ${{ env.ARTIFACT_FOLDER }} @@ -329,6 +330,7 @@ jobs: name: ${{ env.PLAYER_DEBUG_APK_NAME }} path: ${{ env.ARTIFACT_FOLDER }} - uses: actions/download-artifact@v4 + continue-on-error: true with: name: ${{ env.PLAYER_APK_NAME }} path: ${{ env.ARTIFACT_FOLDER }} diff --git a/README.md b/README.md index f8cdf686..c67ca98f 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ a project must be selected first. | :---------------------------------------------------------------- | :-------------------------------------------- | | Clear terminal | Ctrl+L | | Stop project | Ctrl+Shift+S | -| Quit project (stop and close) | Ctrl+Shift+Q | +| Quit project (stop and close) | Ctrl+Q | | Reset application to initial state | Ctrl+Shift+R | | Reset project to initial state | Ctrl+Alt+R | | Exit application | Ctrl+Esc | @@ -59,6 +59,8 @@ a project must be selected first. | Move selection | Ctrl+/ | | Replace selection with input | Enter ⏎ | |        _additionally_ | | +| Insert input contents before selection | Ctrl+Enter ⏎ | +| Insert empty block before current (if input is empty) | Shift+Enter ⏎ | | Delete selected block | Ctrl+Delete | | Delete selected block (if input is empty) | Ctrl+Y | | Wipe input | Ctrl+W | @@ -70,7 +72,8 @@ a project must be selected first. | Scroll down by one line | Shift+PageDown | | Move selection to start | Ctrl+Home | | Move selecion to end | Ctrl+End | -| Stop editor | Ctrl+Shift+S | +| Close editor buffer | Ctrl+S | +| Stop editor (close all buffers) | Ctrl+Shift+S | |        _move mode_ | | | Switch to moving ("pick up" selection) | Ctrl+M | | Move selection | / | @@ -82,13 +85,13 @@ a project must be selected first. | Search definitions | Ctrl+F | | Exit search | Esc | | Jump to selected definition | Enter ⏎ | -| Edit required file / return to previous | Ctrl+O | +| Edit required file under highlight | Ctrl+O | ## Projects A _project_ is a folder in the application's storage which contains at least a `main.lua` file. Projects can be loaded and -ran. At any time, pressing Ctrl-Shift-Q quits and +ran. At any time, pressing Ctrl-Q quits and returns to the console - `list_projects()` diff --git a/justfile b/justfile index e7239e70..1be244fa 100644 --- a/justfile +++ b/justfile @@ -15,11 +15,16 @@ WEBDIST-c := "./dist/web-c" # run unit tests on file change unit_test: - @{{MON}} --exec 'echo -en "\n\n\n\n------------- BUSTED -------------\n"; busted tests' -e 'lua' + @{{MON}} -e 'lua' --exec 'echo -en "\n\n\n\n------------- BUSTED -------------\n"; busted tests' +unit_test_brief: + @{{MON}} -e 'lua' --exec 'echo -en "\n\n\n\n------------- BUSTED -------------\n"; busted tests -o tests/brief_output.lua' unit_test_tag TAG: - @{{MON}} -e lua --exec 'echo -en "\n\n\n\n------------- BUSTED -------------\n" ; busted tests --tags {{TAG}}' + @{{MON}} -e lua \ + --exec 'echo -en "\n\n\n\n------------- BUSTED -------------\n" ; busted tests --defer-print --tags {{TAG}}' unit_test_ast: @SHOW_AST=1 just unit_test_tag ast +unit_test_src: + @SHOW_CODE=1 just unit_test_tag src unit_test_parser: @PARSER_DEBUG=1 just unit_test_tag parser unit_test_analyzer: diff --git a/src/assets/sounds/beep.ogg b/src/assets/sounds/beep.ogg new file mode 100644 index 00000000..ed844367 Binary files /dev/null and b/src/assets/sounds/beep.ogg differ diff --git a/src/assets/sounds/blast.ogg b/src/assets/sounds/blast.ogg new file mode 100644 index 00000000..a52b80fa Binary files /dev/null and b/src/assets/sounds/blast.ogg differ diff --git a/src/assets/sounds/boom.ogg b/src/assets/sounds/boom.ogg new file mode 100644 index 00000000..e83112fa Binary files /dev/null and b/src/assets/sounds/boom.ogg differ diff --git a/src/assets/sounds/correct.ogg b/src/assets/sounds/correct.ogg new file mode 100644 index 00000000..ec823882 Binary files /dev/null and b/src/assets/sounds/correct.ogg differ diff --git a/src/assets/sounds/gameover.ogg b/src/assets/sounds/gameover.ogg new file mode 100644 index 00000000..abbc36b8 Binary files /dev/null and b/src/assets/sounds/gameover.ogg differ diff --git a/src/assets/sounds/hyperjump.ogg b/src/assets/sounds/hyperjump.ogg new file mode 100644 index 00000000..4ec9c9fd Binary files /dev/null and b/src/assets/sounds/hyperjump.ogg differ diff --git a/src/assets/sounds/jump.ogg b/src/assets/sounds/jump.ogg new file mode 100644 index 00000000..72251fa3 Binary files /dev/null and b/src/assets/sounds/jump.ogg differ diff --git a/src/assets/sounds/knock.ogg b/src/assets/sounds/knock.ogg new file mode 100644 index 00000000..cbb58d73 Binary files /dev/null and b/src/assets/sounds/knock.ogg differ diff --git a/src/assets/sounds/lose.ogg b/src/assets/sounds/lose.ogg new file mode 100644 index 00000000..a431ecc8 Binary files /dev/null and b/src/assets/sounds/lose.ogg differ diff --git a/src/assets/sounds/pew.ogg b/src/assets/sounds/pew.ogg new file mode 100644 index 00000000..c3f8c633 Binary files /dev/null and b/src/assets/sounds/pew.ogg differ diff --git a/src/assets/sounds/ping.ogg b/src/assets/sounds/ping.ogg new file mode 100644 index 00000000..1613f7ae Binary files /dev/null and b/src/assets/sounds/ping.ogg differ diff --git a/src/assets/sounds/punch.ogg b/src/assets/sounds/punch.ogg new file mode 100644 index 00000000..31477fed Binary files /dev/null and b/src/assets/sounds/punch.ogg differ diff --git a/src/assets/sounds/rawr.ogg b/src/assets/sounds/rawr.ogg new file mode 100644 index 00000000..acf1f69d Binary files /dev/null and b/src/assets/sounds/rawr.ogg differ diff --git a/src/assets/sounds/shot.ogg b/src/assets/sounds/shot.ogg new file mode 100644 index 00000000..1105676f Binary files /dev/null and b/src/assets/sounds/shot.ogg differ diff --git a/src/assets/sounds/sword.ogg b/src/assets/sounds/sword.ogg new file mode 100644 index 00000000..0405c96f Binary files /dev/null and b/src/assets/sounds/sword.ogg differ diff --git a/src/assets/sounds/toggle.ogg b/src/assets/sounds/toggle.ogg new file mode 100644 index 00000000..381ae109 Binary files /dev/null and b/src/assets/sounds/toggle.ogg differ diff --git a/src/assets/sounds/win.ogg b/src/assets/sounds/win.ogg new file mode 100644 index 00000000..33da9b3d Binary files /dev/null and b/src/assets/sounds/win.ogg differ diff --git a/src/assets/sounds/wow.ogg b/src/assets/sounds/wow.ogg new file mode 100644 index 00000000..60fcf55b Binary files /dev/null and b/src/assets/sounds/wow.ogg differ diff --git a/src/assets/sounds/wrong.ogg b/src/assets/sounds/wrong.ogg new file mode 100644 index 00000000..8fbf6bd8 Binary files /dev/null and b/src/assets/sounds/wrong.ogg differ diff --git a/src/conf.lua b/src/conf.lua index 0e41f9a9..93d2e4db 100644 --- a/src/conf.lua +++ b/src/conf.lua @@ -78,11 +78,11 @@ function love.conf(t) t.identity = 'compy' t.window.resizable = false + local width = 1024 + local height = 600 if start.mode ~= 'play' then local hidpi = os.getenv("HIDPI") - local width = 1024 - local height = 600 if hidpi == 'true' or hidpi == 'TRUE' then t.window.width = width * 2 t.window.height = height * 2 @@ -103,6 +103,9 @@ function love.conf(t) if string.len(gp) > 0 then title = string.format('%s - %s', 'Compy Player', gp) end + t.window.resizable = true + love.fixHeight = height + love.fixWidth = width t.window.title = title end love.test_grid_x = 4 diff --git a/src/controller/consoleController.lua b/src/controller/consoleController.lua index 4a4213a0..d4b52d06 100644 --- a/src/controller/consoleController.lua +++ b/src/controller/consoleController.lua @@ -10,6 +10,13 @@ require("util.key") require("util.table") local TerminalTest = require("util.test_terminal") +local messages = { + file_does_not_exist = function(name) + local n = name or '' + return 'cannot open ' .. n .. ': No such file or directory' + end, +} + --- @class ConsoleController --- @field time number --- @field model Model @@ -161,26 +168,30 @@ function ConsoleController:run_project(name) { "There's already a project running!" }) return end - local P = self.model.projects + local P = self.model.projects + local cur = P.current local ok - if P.current then + if cur and (not name or cur.name == name) then ok = true else ok = self:open_project(name, false) end + if ok then - local runner_env = self:get_project_env() + local runner_env = self:get_project_env() local f, load_err, path = P:run(name, runner_env) if f then local n = name or P.current.name or 'project' Log.info('Running \'' .. n .. '\'') + love.state.app_state = 'running' local rok, run_err = run_user_code(f, self, path) - if rok then - if self.main_ctrl.user_is_blocking() then - love.state.app_state = 'running' - end - else + if not rok then + love.state.app_state = 'project_open' print('Error: ', run_err) + else + if not self.main_ctrl.user_is_blocking() then + love.state.app_state = 'project_open' + end end else --- TODO extract error message here @@ -189,32 +200,45 @@ function ConsoleController:run_project(name) end end -_G.o_require = _G.require ---- @param cc ConsoleController +local o_require = _G.require +_G.o_require = o_require --- @param name string -local function project_require(cc, name) +--- @param run 'run'? +local function project_require(name, run) + if run then + Log.info('req', name) + end + if _G.web and name == 'bit' then + return o_require('util.luabit') + else + return o_require(name) + end +end + +_G.o_dofile = _G.dofile +--- @param cc ConsoleController +--- @param filename string +--- @param env LuaEnv? +local function project_dofile(cc, filename, env) local P = cc.model.projects - local fn = name .. '.lua' + local fn = filename local open = P.current if open then local chunk = open:load_file(fn) - local pr_env = cc:get_project_env() if chunk then - setfenv(chunk, pr_env) - return chunk() - else - --- hack around love.js not having the bit lib - if name == 'bit' and _G.web then - ---@diagnostic disable-next-line: inject-field - pr_env.bit = o_require('util.luabit') + if env then + setfenv(chunk, env) end + return true, chunk() + else + print(messages.file_does_not_exist(filename)) end - --- TODO: is it desirable to allow out-of-project require? - -- else - -- _require(name) end end +-- Set up audio table +local compy_audio = require("util.audio") + function ConsoleController.prepare_env(cc) local prepared = cc.main_env prepared.gfx = love.graphics @@ -222,7 +246,7 @@ function ConsoleController.prepare_env(cc) local P = cc.model.projects prepared.require = function(name) - return project_require(cc, name) + return project_require(name) end --- @param f function @@ -234,6 +258,14 @@ function ConsoleController.prepare_env(cc) end end + prepared.require = project_require + + prepared.dofile = function(name) + return check_open_pr(function() + return project_dofile(cc, name) + end) + end + prepared.list_projects = function() local ps = P:list() if ps:is_empty() then @@ -344,7 +376,8 @@ function ConsoleController.prepare_env(cc) clear = function() return terminal:clear() end - } + }, + audio = compy_audio, } prepared.compy = compy_namespace prepared.tty = compy_namespace.terminal @@ -375,9 +408,19 @@ function ConsoleController.prepare_project_env(cc) local project_env = cc:get_pre_env_c() project_env.gfx = love.graphics + project_env.compy = { + audio = compy_audio + } + project_env.require = function(name) - return project_require(cc, name) + return project_require(name) end + project_env.dofile = function(name) + return project_dofile(cc, name, cc:get_project_env()) + end + -- project_env.require = function(name) + -- return project_require(name, 'run') + -- end --- @param msg string? project_env.pause = function(msg) @@ -548,13 +591,13 @@ function ConsoleController:restart() end ---@return LuaEnv -function ConsoleController:get_console_env() - return self.main_env +function ConsoleController:get_pre_env_c() + return table.clone(self.pre_env) end ---@return LuaEnv -function ConsoleController:get_pre_env_c() - return table.clone(self.pre_env) +function ConsoleController:get_console_env() + return self.main_env end ---@return LuaEnv @@ -567,6 +610,18 @@ function ConsoleController:get_base_env() return self.base_env end +---@return LuaEnv +function ConsoleController:get_effective_env() + if + love.state.app_state == 'running' + or love.state.app_state == 'inspect' + then + return self:get_project_env() + else + return self:get_console_env() + end +end + ---@param t LuaEnv function ConsoleController:_set_project_env(t) self.project_env = t @@ -615,19 +670,20 @@ function ConsoleController:open_project(name, play) print('No project name provided!') return false end + local cur = P.current + if cur then + self:close_project() + end + local open, create, err = P:opreate(name, play) local ok = open or create if ok then - local project_loader = (function() - local cached = self.loaders[name] - if cached then - return cached - else - local loader = P.current:get_loader() - self:cache_loader(name, loader) - return loader - end - end)() + local project_loader = + P.current:get_loader(function() + return self:get_effective_env() + end) + self:cache_loader(name, project_loader) + if not table.is_member(package.loaders, project_loader) then table.insert(package.loaders, 1, project_loader) @@ -664,7 +720,30 @@ function ConsoleController:close_project() return true end +--- @return Project? +function ConsoleController:get_current_project() + local P = self.model.projects + return P.current +end + +function ConsoleController:evacuate_required() + local open = self:get_current_project() + if not open then return end + local files = open:contents() + local lua = '.lua$' + for _, v in ipairs(files) do + if string.matches(v.name, lua, true) then + local fn = v.name + local modname = fn:gsub(lua, '') + if package.loaded[modname] then + package.loaded[modname] = nil + end + end + end +end + function ConsoleController:stop_project_run() + self:evacuate_required() self.main_ctrl.set_default_handlers(self, self.view) self.main_ctrl.set_love_update(self) love.state.user_input = nil @@ -687,14 +766,14 @@ end function ConsoleController:edit(name, state) if love.state.app_state == 'running' then return end - local PS = self.model.projects - local p = PS.current + local PS = self.model.projects + local p = PS.current local filename - if state and state.buffer then - filename = state.buffer.filename - else - filename = name or ProjectService.MAIN - end + -- if state and state.buffer then + -- filename = state.buffer.filename + -- else + filename = name or ProjectService.MAIN + -- end local fpath = p:get_path(filename) local ex = FS.exists(fpath) local text @@ -722,16 +801,26 @@ end --- @return EditorState? function ConsoleController:finish_edit() self.editor:save_state() - local name, newcontent = self.editor:close() - local ok, err = self:_writefile(name, newcontent) + self.editor:close() + local ok = true + local errs = {} + -- local bfs = self.editor:close() + -- for _, bc in ipairs(bfs) do + -- local name, newcontent = bc.name, bc.content + -- local bok, err = self:_writefile(name, newcontent) + -- if not bok then + -- ok = false + -- table.insert(errs, err) + -- end + -- end if ok then love.state.app_state = love.state.prev_state love.state.prev_state = nil - --- TODO clear bufferlist - return self.editor:get_state() else - print(err) + print(string.unlines(errs)) end + self.buffers = {} + return self.editor:get_state() end --- Handlers --- @@ -761,6 +850,11 @@ function ConsoleController:keypressed(k) local function terminal_test() local out = self.model.output + if love.state.app_state ~= 'ready' + or love.state.app_state ~= 'project_open' + then + return + end if not love.state.testing then love.state.testing = 'running' input:cancel() @@ -817,7 +911,7 @@ function ConsoleController:keypressed(k) self.model.output:reset() end if love.DEBUG then - if k == 't' then + if Key.alt() and k == 't' then terminal_test() return end diff --git a/src/controller/controller.lua b/src/controller/controller.lua index ed63a764..de98d29a 100644 --- a/src/controller/controller.lua +++ b/src/controller/controller.lua @@ -47,6 +47,7 @@ local _C, _mode --- @param msg string local function user_error_handler(msg) + Log.debug('user error: ' .. msg) local err = LANG.get_call_error(msg) or '' local user_msg = messages.exec_error(err) _C:suspend_run(user_msg) @@ -400,15 +401,15 @@ Controller = { local uup = Controller._userhandlers.update if user_update and uup then - if love.state.app_state == 'snapshot' then - gfx.captureScreenshot(function(img) - local snap = gfx.newImage(img) - View.snapshot = snap - C:suspend() - end) - end wrap(uup, dt) end + if love.state.app_state == 'snapshot' then + gfx.captureScreenshot(function(img) + local snap = gfx.newImage(img) + View.snapshot = snap + C:suspend() + end) + end if love.harmony then love.harmony.timer_update(dt) @@ -537,7 +538,7 @@ Controller = { handlers.keypressed = function(k) --- Power shortcuts local function quickswitch() - if Key.ctrl() and k == 't' then + if Key.ctrl() and not Key.alt() and k == 't' then if love.state.app_state == 'running' or love.state.app_state == 'inspect' or love.state.app_state == 'project_open' @@ -563,18 +564,22 @@ Controller = { if k == "pause" then C:suspend_run(messages.user_break) end - if Key.shift() then - -- Ensure the user can get back to the console - if k == "q" then - C:quit_project() - end - if k == "s" then - if love.state.app_state == 'running' then - C:stop_project_run() - elseif love.state.app_state == 'editor' then + if k == "q" then + C:quit_project() + end + if k == "s" then + if love.state.app_state == 'running' then + C:stop_project_run() + elseif love.state.app_state == 'editor' then + if Key.shift() then + C:finish_edit() + else C:close_buffer() end end + end + if Key.shift() then + --- Ensure the user can get back to the console if k == "r" then C:reset() end diff --git a/src/controller/editorController.lua b/src/controller/editorController.lua index b60b92ac..3289c9a1 100644 --- a/src/controller/editorController.lua +++ b/src/controller/editorController.lua @@ -69,7 +69,7 @@ function EditorController:open(name, content, save) return parser.trunc(code, self.model.cfg.view.fold_lines) end elseif is_md then - local mdEval = MdEval(name) + local mdEval = MdEval() hl = mdEval.highlighter self.input:set_eval(mdEval) else @@ -85,7 +85,7 @@ function EditorController:open(name, content, save) end --- @private -function EditorController:_dump_bufferlist() +function EditorController:_print_bufferlist() for i, v in ipairs(self.model.buffers) do Log.debug(i, v.name) end @@ -116,16 +116,15 @@ function EditorController:pop_buffer() bs:pop_front() local b = bs:first() self.view:get_current_buffer():open(b) + self:update_status() end function EditorController:close_buffer() local bs = self.model.buffers local n_buffers = bs:length() if n_buffers < 2 then - -- Log.debug('fin', n_buffers) self.console:finish_edit() else - -- Log.debug(':bd', n_buffers) self:pop_buffer() end end @@ -235,13 +234,14 @@ function EditorController:restore_state(state) end end ---- @return string name ---- @return Dequeue content +--- @return {name: string, content: string[]}[] function EditorController:close() - local buf = self:get_active_buffer() self.input:clear() - local content = buf:get_text_content() - return buf.name, content + local bfs = self.model:get_buffers_content() + self.model.buffers = Dequeue() + self.view.buffers = {} + --- TODO is this needed? + return bfs end --- @return BufferModel @@ -269,9 +269,9 @@ function EditorController:_generate_status(sel) local ct = bufview.content_type if ct == 'lua' then local range = bufview.content:get_block_app_pos(sel) - cs = CustomStatus(ct, len, more, sel, m, range) + cs = CustomStatus(buffer.name, ct, len, more, sel, m, range) else - cs = CustomStatus(ct, len, more, sel, m) + cs = CustomStatus(buffer.name, ct, len, more, sel, m) end return cs @@ -329,10 +329,8 @@ function EditorController:_handle_submit(go) local sel = buf:get_selection() local block = buf:get_content():get(sel) if not block then return end - --- TODO: why did I do this? - -- local ln = block.pos.start - -- if ln then go({ Empty(ln) }) end else + local _, raw_chunks = buf.chunker(raw, true) local pretty = buf.printer(raw) if pretty then inter:set_text(pretty) @@ -343,6 +341,16 @@ function EditorController:_handle_submit(go) local ok, res = inter:evaluate() local _, chunks = buf.chunker(pretty, true) if ok then + if #chunks < #raw_chunks then + local rc = raw_chunks + if rc[1].tag == 'empty' then + table.insert(chunks, 1, Empty(0)) + end + if rc[#rc].tag == 'empty' then + local li = chunks[#chunks].pos.fin + table.insert(chunks, Empty(li + 1)) + end + end go(chunks) else local eval_err = res @@ -489,16 +497,23 @@ function EditorController:_normal_mode_keys(k) local buf = self:get_active_buffer() local function newline() - if not Key.ctrl() and Key.shift() and Key.is_enter(k) then - buf:insert_newline() - self:save(buf) - self.view:refresh() - block_input() + if Key.is_enter(k) then + --- insert empty block if input is empty + if is_empty + and (Key.shift() or Key.ctrl()) + and not Key.alt() then + buf:insert_newline() + self:save(buf) + self.view:refresh() + block_input() + end end end local function delete_block() + local t = string.unlines(buf:get_selected_text()) buf:delete_selected_text() + love.system.setClipboardText(t) self:save(buf) self.view:refresh() end @@ -546,9 +561,9 @@ function EditorController:_normal_mode_keys(k) end if is_empty then - newline() copycut() end + newline() paste_k() @@ -573,29 +588,53 @@ function EditorController:_normal_mode_keys(k) --- handlers local function submit() - if not Key.ctrl() and not Key.shift() and Key.is_enter(k) then - local bufv = self.view:get_current_buffer() - local function go(newtext) + local bufv = self.view:get_current_buffer() + local function replace(newtext) + if bufv:is_selection_visible() then + if buf:loaded_is_sel(true) then + local _, n = buf:replace_content(newtext) + buf:clear_loaded() + self:save(buf) + input:clear() + self.view:refresh() + self:_move_sel('down', n) + load_selection() + self:update_status() + else + buf:select_loaded() + bufv:follow_selection() + end + else + bufv:follow_selection() + end + end + + if Key.ctrl() + and not Key.shift() + and not Key.alt() + and Key.is_enter(k) then + local function add(newtext) if bufv:is_selection_visible() then - if buf:loaded_is_sel(true) then - local _, n = buf:replace_selected_text(newtext) - buf:clear_loaded() - self:save(buf) - input:clear() - self.view:refresh() - self:_move_sel('down', n) - load_selection() - self:update_status() - else - buf:select_loaded() - bufv:follow_selection() - end + local sel = buf:get_selection() + local _, n = buf:insert_content(newtext, sel) + self:save(buf) + self.view:refresh() + self:_move_sel('down', n) + buf:clear_loaded() + self:update_status() else bufv:follow_selection() end end - self:_handle_submit(go) + self:_handle_submit(add) + end + + if not Key.ctrl() + and not Key.shift() + and not Key.alt() + and Key.is_enter(k) then + self:_handle_submit(replace) end end local function load() @@ -666,7 +705,7 @@ function EditorController:_normal_mode_keys(k) end -- step into - if love.DEBUG and Key.ctrl() then + if Key.ctrl() then if k == "o" then self:follow_require() end diff --git a/src/controller/searchController.lua b/src/controller/searchController.lua index 4f549dc0..5f2806b4 100644 --- a/src/controller/searchController.lua +++ b/src/controller/searchController.lua @@ -129,10 +129,14 @@ function SearchController:keypressed(k) removers() if Key.is_enter(k) then + --- no linebreaks in search + if Key.shift() or Key.ctrl() then return end local sel = self.model.selection local r = self.model.resultset[sel].r + self.input:update_view() return r end + self.input:update_view() end --- @param t string diff --git a/src/controller/userInputController.lua b/src/controller/userInputController.lua index 60885cd8..ed087f7f 100644 --- a/src/controller/userInputController.lua +++ b/src/controller/userInputController.lua @@ -1,5 +1,6 @@ local class = require('util.class') require("util.key") +require("util.view") require("util.string.string") --- @param model UserInputModel @@ -131,7 +132,7 @@ function UserInputController:clear_error() self.model:clear_error() end ---- @param error string[]? +--- @param error string[]|Error[]? function UserInputController:set_error(error) self.model:set_error(error) end @@ -144,8 +145,9 @@ end --- @return boolean --- @return Error[] function UserInputController:evaluate() + local ok, res = self.model:handle(true) self:update_view() - return self.model:handle(true) + return ok, res end function UserInputController:cancel() diff --git a/src/examples/tixy/main.lua b/src/examples/tixy/main.lua index da687455..993a50fb 100644 --- a/src/examples/tixy/main.lua +++ b/src/examples/tixy/main.lua @@ -3,7 +3,7 @@ math.randomseed(os.time()) cw, ch = gfx.getDimensions() midx = cw / 2 -require("math") +require("mathlib") examples = require("examples") size = 28 diff --git a/src/examples/tixy/math.lua b/src/examples/tixy/mathlib.lua similarity index 100% rename from src/examples/tixy/math.lua rename to src/examples/tixy/mathlib.lua diff --git a/src/harmony/scenarios/examples.lua b/src/harmony/scenarios/examples.lua index d4645658..5671c815 100644 --- a/src/harmony/scenarios/examples.lua +++ b/src/harmony/scenarios/examples.lua @@ -23,8 +23,8 @@ local function examples() wait(.1) -- wait(.1) h.love_key('return') - wait(.1) - assert(love.state.app_state == 'running') + wait(.3) + assert(love.state.app_state == 'running') h.screenshot('turtl') wait(.3) h.love_key('space') diff --git a/src/lib/metalua b/src/lib/metalua index 72d242fe..26eaa786 160000 --- a/src/lib/metalua +++ b/src/lib/metalua @@ -1 +1 @@ -Subproject commit 72d242fe90279ac80a4847f0720f9cb3226b003b +Subproject commit 26eaa786a330dc4ffc08be7c58c8a189d56e8950 diff --git a/src/main.lua b/src/main.lua index aaf88408..5591a00d 100644 --- a/src/main.lua +++ b/src/main.lua @@ -10,6 +10,7 @@ require("view.consoleView") local colors = require("conf.colors") local hostconf = prequire('host') +require("util.lua") require("util.key") require("util.debug") local FS = require("util.filesystem") @@ -81,7 +82,12 @@ local config_view = function(flags) end local drawableChars = math.floor(drawableWidth / fw) - if love.DEBUG then drawableChars = drawableChars - 3 end + + if love.DEBUG then + drawableChars = parse_int(os.getenv("COMPY_WRAP")) + or drawableChars - 3 + lines = parse_int(os.getenv("COMPY_LINES")) or lines + end return { font = font_main, diff --git a/src/model/editor/bufferModel.lua b/src/model/editor/bufferModel.lua index 75e51a07..207d063a 100644 --- a/src/model/editor/bufferModel.lua +++ b/src/model/editor/bufferModel.lua @@ -20,7 +20,6 @@ local function render_blocks(blocks) ret:append_all(v.lines) end end - ret:append('') return ret end @@ -51,7 +50,10 @@ local function new( local function plaintext() ct = 'plain' _content = Dequeue(lines, 'string') - sel = #_content + 1 + if _content:last() ~= '' then + _content:push('') + end + sel = #_content end --- only passing this around so the linter shuts up about nil --- @param chk function @@ -60,7 +62,7 @@ local function new( local ok, blocks = chk(lines) if ok then local len = #blocks - sel = len + 1 + sel = len else readonly = true sel = 1 @@ -153,7 +155,7 @@ function BufferModel:rechunk() end function BufferModel:save() - self:highlight() + self:_text_change() local text = self:get_text_content() self:analyze() return self.save_file(text) @@ -207,7 +209,7 @@ function BufferModel:move_selection(dir, by, warp, move) local l = self:get_content_length() local last = (function() if move then return l end - return l + 1 + return l end)() if warp then if dir == 'up' then @@ -303,9 +305,13 @@ function BufferModel:_text_change(rechunk) self:rechunk() self:rechunk() end - end - if self.content_type == 'md' then + else self:highlight() + local ll = self.content:last() + if ll ~= '' then + -- Log.warn('asd') + self.content:push('') + end end end @@ -315,13 +321,8 @@ function BufferModel:delete_selected_text() local sb = self.content[sel] if not sb then return end - local l = sb.pos:len() self.content:remove(sel) - for i = sel, self:get_content_length() do - local b = self.content[i] - local r = b.pos - b.pos = r:translate(-l) - end + self:rechunk() else self.content:remove(sel) end @@ -329,21 +330,23 @@ function BufferModel:delete_selected_text() end --- @param t string[]|Block[] +--- @param coord integer? --- @return boolean insert --- @return integer? inserted_lines -function BufferModel:replace_selected_text(t) +function BufferModel:replace_content(t, coord) if self.content_type == 'lua' then local chunks = t local n = #chunks if n == 0 then return false end - local sel = self.selection + local blocknum = coord or self:get_selection() + local rechunk = false --- content start and original length local cs, ol = (function() - local current = self.content[sel] + local current = self.content[blocknum] if current then - return current.pos.start, self.content[sel].pos:len() + return current.pos.start, self.content[blocknum].pos:len() end local last = self.content:last() if last then @@ -357,33 +360,34 @@ function BufferModel:replace_selected_text(t) local c = chunks[1] local nr = c.pos:translate(cs - 1) c.pos = nr - self.content[sel] = chunks[1] + self.content[blocknum] = chunks[1] else --- remove old chunk - self.content:remove(sel) + self.content:remove(blocknum) --- insert new version of the chunk(s) for i = #chunks, 1, -1 do local c = chunks[i] local nr = c.pos:translate(cs - 1) c.pos = nr - self.content:insert(c, sel) + self.content:insert(c, blocknum) end + rechunk = true end --- move subsequent chunks down local diff = chunks[n].pos:len() - ol if diff ~= 0 then - for i = sel + 1, self:get_content_length() do + for i = blocknum + 1, self:get_content_length() do local b = self.content[i] b.pos = b.pos:translate(diff) end end - self:_text_change() + self:_text_change(rechunk) return true, n else - local sel = self.selection + local linenum = coord or self:get_selection() local clen = #(self.content) - local ti = sel + local ti = linenum if #t == 1 then self.content[ti] = t[1] if ti > clen then @@ -403,26 +407,92 @@ function BufferModel:replace_selected_text(t) end --- Insert a new line or empty block _before_ the selection +--- Returns true if a block/line was inserted --- @param i integer? +--- @return boolean? function BufferModel:insert_newline(i) --- block or line number local bln = i or self:get_selection() if self.content_type == 'lua' then - local sb = self.content[bln] - if not sb then return end + local b = self.content[bln] + if not b then return end + local prev_b = self.content[bln - 1] -- disallow consecutive empties local prev_empty = prev_b and prev_b:is_empty() - local sel_empty = sb:is_empty() + local sel_empty = b:is_empty() local cons = prev_empty or sel_empty if cons then return end local ln = self:get_selection_start_line() self.content:insert(Empty(ln), bln) self:_text_change(true) + return true else self.content:insert('', bln) self:_text_change() + return true + end +end + +--- Insert new lines or blocks +--- Returns success flag and number of inserted +--- @param t string[]|Block[] +--- @param lbn integer? +--- @return boolean? +--- @return integer? inserted_lines +function BufferModel:insert_content(t, lbn) + local num = lbn or self:get_selection() + local len = self:get_content_length() + 1 + if lbn < 1 or lbn > len then + return false + end + if self.content_type == 'lua' then + local chunks = t + local n = #chunks + if n == 0 then + return false + end + --- content start and original length + local cs, ol = (function() + local current = self.content[num] + if current then + return current.pos.start, + self.content[num].pos:len() + end + local last = self.content:last() + if last then + return self.content:last().pos.fin + 1, 0 + else --- empty file + return 1, 0 + end + end)() + + for i = #chunks, 1, -1 do + local c = chunks[i] + local nr = c.pos:translate(cs - 1) + c.pos = nr + self.content:insert(c, num) + end + + --- move subsequent chunks down + local diff = chunks[n].pos:len() - ol + if diff ~= 0 then + for i = num + 1, self:get_content_length() do + local b = self.content[i] + b.pos = b.pos:translate(diff) + end + end + + self:_text_change(true) + return true, n + else + local ti = num + for i = #t, 1, -1 do + self.content:insert(t[i], ti) + end + self:_text_change() + return true, #t end end diff --git a/src/model/editor/content.lua b/src/model/editor/content.lua index e07586a2..0beb0d22 100644 --- a/src/model/editor/content.lua +++ b/src/model/editor/content.lua @@ -30,6 +30,7 @@ end Chunk = class.create() --- @param lines str +--- @param pos Range --- @return Chunk function Chunk.new(lines, pos) local ls = (function() diff --git a/src/model/editor/editorModel.lua b/src/model/editor/editorModel.lua index c9fb4cbe..e5e80ca6 100644 --- a/src/model/editor/editorModel.lua +++ b/src/model/editor/editorModel.lua @@ -17,3 +17,16 @@ EditorModel = class.create(function(cfg) cfg = cfg, } end) + +--- @return {name: string, content: string[]}[] +function EditorModel:get_buffers_content() + local ret = {} + for _, buf in ipairs(self.buffers) do + local b = { + name = buf.name, + content = buf:get_text_content(), + } + table.insert(ret, b) + end + return ret +end diff --git a/src/model/input/history.lua b/src/model/input/history.lua index d40e2284..cd02fc95 100644 --- a/src/model/input/history.lua +++ b/src/model/input/history.lua @@ -26,6 +26,14 @@ function History:remember(input) if not self.index then self:append(input) return true + else + local entry = self:get(self.index) + local new = input or {} + local hist = string.unlines(entry) + local new_s = string.unlines(new) + if hist ~= new_s then + self:append(input) + end end end return false @@ -97,8 +105,9 @@ function History:_get_entries() end --- For debug purposes, log content +--- @private --- @param f function -function History:_dump(f) +function History:_dbgprint(f) local log = f or Log.debug local h = self:items() local t = table.map(h, function(t) @@ -106,5 +115,5 @@ function History:_dump(f) end) local i = self.index or '-' local l = ' [' .. self:length() .. '] ' - log(i .. l .. Debug.text_table(t, false, nil, 64) ) + log(i .. l .. Debug.text_table(t, false, nil, 64)) end diff --git a/src/model/input/userInputModel.lua b/src/model/input/userInputModel.lua index badb427b..97d0d537 100644 --- a/src/model/input/userInputModel.lua +++ b/src/model/input/userInputModel.lua @@ -820,8 +820,11 @@ function UserInputModel:handle(eval) end end else + --- @TODO fix local perr = result[1] + -- Log.debug(Debug.terse_t(perr, nil, nil, true)) if perr then + --- @TODO check line len and move to next if at end if perr.c then local c = perr.c if c > 1 then c = c + 1 end @@ -848,20 +851,29 @@ end --- @return string[]? function UserInputModel:get_wrapped_error() if self.error then + local e = table.map(self.error, function(er) + return tostring(er) + end) local we = string.wrap_array( - self.error, + e, self.visible.wrap_w) - table.insert(we, 1, 'Errors:') + table.insert(we, 1, 'Errors:') return we end end +--- @TODO support error[] and string[] --- @param errors Error[]? function UserInputModel:set_error(errors) - self.error = {} + self.error = nil if type(errors) == "table" then - for _, e in ipairs(errors) do - table.insert(self.error, tostring(e)) + if type(errors[1] == "string") then + self.error = errors + else + self.error = {} + for _, e in ipairs(errors) do + table.insert(self.error, tostring(e)) + end end end end diff --git a/src/model/lang/lua/parser.lua b/src/model/lang/lua/parser.lua index ad3cf448..714612ca 100644 --- a/src/model/lang/lua/parser.lua +++ b/src/model/lang/lua/parser.lua @@ -381,6 +381,8 @@ return function(lib) end end + --- multiline empty collapse offset + local of = 0 for _, v in ipairs(r) do has_lines = true local li = v.lineinfo @@ -389,14 +391,15 @@ return function(lib) local comments = ast_extract_comments(v, {}, wrap) get_comments(comments, 'first') - - -- account for empty lines, including the zeroth + --- account for empty lines, including the zeroth if fl > last + 1 then - ret:insert(Empty(last + 1), idx) + ret:insert(Empty(last + 1 - of), idx) idx = idx + 1 + last = last + 1 + of = of + (fl - last - 1) end local tex = table.slice(text or {}, fl, ll) - local chunk = Chunk(tex, Range(fl, ll)) + local chunk = Chunk(tex, Range(fl - of, ll - of)) ret:insert(chunk, idx) idx = idx + 1 last = ll @@ -410,13 +413,23 @@ return function(lib) get_comments(single_comment, 'first') end + if + ret:last().tag ~= 'empty' and ( + --- no empty line at EOF + not single + --- there is an empty line at the end + or single and (#string.lines(text) > last) + ) + then + ret:push_back(Empty(last + 1 - of)) + end return true, ret, r else --- content is not valid lua return false, Dequeue(text, 'string'), r end else - return true, Dequeue(Empty(1), 'block'), r + return true, Dequeue({ Empty(1) }, 'block'), r end end diff --git a/src/model/project/project.lua b/src/model/project/project.lua index 12ea4f40..97afdb1f 100644 --- a/src/model/project/project.lua +++ b/src/model/project/project.lua @@ -113,14 +113,18 @@ function Project:load_file(filename) end end +--- @param get_env fun(): LuaEnv --- @return function -function Project:get_loader() +function Project:get_loader(get_env) --- @param modname string --- @return unknown|string return function(modname) local fn = modname .. '.lua' local f = self:load_file(fn) + Log.debug(string.format("%s loader, loading %s", + self.name, modname)) if f then + setfenv(f, get_env()) return assert(f) else return string.format( @@ -289,11 +293,11 @@ function ProjectService:opreate(name, play) local ok = self:open('play', true) return ok, false else - local ook, _ = self:open(name) + local ook, _ = self:open(name, false) if ook then return ook, false else - local cok, c_err = self:create(name) + local cok, c_err = self:create(name, false) if cok then self:open(name) return false, cok @@ -362,7 +366,7 @@ function ProjectService:clone(old, new) end --- @param name string ---- @param env table +--- @param env LuaEnv --- @return function? --- @return string? error --- @return string? path diff --git a/src/util/audio.lua b/src/util/audio.lua new file mode 100644 index 00000000..7b005ce1 --- /dev/null +++ b/src/util/audio.lua @@ -0,0 +1,35 @@ +-- Table to become compy.audio +local audio = { } + +-- Add noises to the audio table +local names = { + "beep", + "blast", + "boom", + "correct", + "gameover", + "hyperjump", + "jump", + "knock", + "lose", + "pew", + "ping", + "punch", + "rawr", + "shot", + "sword", + "toggle", + "win", + "wow", + "wrong" +} +for _,name in pairs(names) do + local filename = "assets/sounds/"..name..".ogg" + local source = love.audio.newSource(filename, "static") + audio[name] = function() + love.audio.stop(source) + love.audio.play(source) + end +end + +return audio diff --git a/src/util/debug.lua b/src/util/debug.lua index 1209f057..4e22aa38 100644 --- a/src/util/debug.lua +++ b/src/util/debug.lua @@ -150,12 +150,12 @@ local function terse_array(a, skip) end end ---- @alias dumpstyle +--- @alias tablestyle --- | 'lua' --- | 'json5' --- @param ast token[]? --- @param skip_lineinfo boolean? ---- @param style dumpstyle? +--- @param style tablestyle? --- @return string local function terse_ast(ast, skip_lineinfo, style) if type(ast) ~= 'table' then @@ -165,7 +165,7 @@ local function terse_ast(ast, skip_lineinfo, style) --- @param t table? --- @param omit any[]? - --- @param style dumpstyle? + --- @param style tablestyle? --- @param level integer? --- @param prev_seen table? --- @return string @@ -320,7 +320,7 @@ Debug = { if type(t) == 'table' then local start = math.max(1, skip or 0) for i = start, #t do - local l = t[i] + local l = t[i] or '' local line = (function() if not no_ln then return string.format("#%02d: %s\n", i, text(l)) @@ -495,6 +495,7 @@ local error = function(...) printer(s) end local debug = function(...) + if love and not love.DEBUG then return end local args = { ... } local ts = string.format("%.3f ", os.clock()) local s = annot(ts .. 'DEBUG ', @@ -502,7 +503,7 @@ local debug = function(...) printer(s) end local once = function(...) - if not love.DEBUG then return end + if not love or not love.DEBUG then return end local args = { ... } local key = love.debug.once .. string.join(args, '') local kh = hash(key) diff --git a/src/util/lua.lua b/src/util/lua.lua index de1572fa..35407a05 100644 --- a/src/util/lua.lua +++ b/src/util/lua.lua @@ -34,6 +34,9 @@ local t = { b2s = function(b) return b and '#t' or '#f' end, + parse_int = function(s) + return tonumber(s or '', 10) + end } for k, v in pairs(t) do diff --git a/src/view/editor/bufferView.lua b/src/view/editor/bufferView.lua index 4f43eb64..1926a2f3 100644 --- a/src/view/editor/bufferView.lua +++ b/src/view/editor/bufferView.lua @@ -84,7 +84,6 @@ function BufferView:open(buffer) local ir = self:_get_end_range() self:_update_visible(ir) - if off > 0 then self:scroll('down', 1) end end --- @private @@ -205,7 +204,7 @@ end --- @param ln integer function BufferView:scroll_to_line(ln) - local off = self.content.wrap_forward[ln][1] + local off = self.content.wrap_forward[ln][1] or 0 self:scroll_to(off) end diff --git a/src/view/editor/editorView.lua b/src/view/editor/editorView.lua index a9d1cba0..8d122dc5 100644 --- a/src/view/editor/editorView.lua +++ b/src/view/editor/editorView.lua @@ -8,7 +8,7 @@ local class = require('util.class') --- @param cfg ViewConfig --- @param ctrl EditorController local function new(cfg, ctrl) - local ev = { + local self = { cfg = cfg, controller = ctrl, input = UserInputView(cfg, ctrl.input), @@ -16,8 +16,8 @@ local function new(cfg, ctrl) search = SearchView(cfg, ctrl.search), } --- hook the view in the controller - ctrl:init_view(ev) - return ev + ctrl:init_view(self) + return self end --- @class EditorView : ViewBase diff --git a/src/view/input/customStatus.lua b/src/view/input/customStatus.lua index 19e2d32c..72f423de 100644 --- a/src/view/input/customStatus.lua +++ b/src/view/input/customStatus.lua @@ -1,6 +1,7 @@ local class = require('util.class') --- @class CustomStatus table +--- @field name string --- @field content_type ContentType --- @field buflen integer --- @field buffer_more More @@ -8,8 +9,9 @@ local class = require('util.class') --- @field mode EditorMode --- @field range Range? CustomStatus = class.create( - function(ct, len, more, sel, mode, range) + function(name, ct, len, more, sel, mode, range) return { + name = name, content_type = ct, buflen = len, buffer_more = more, diff --git a/src/view/input/statusline.lua b/src/view/input/statusline.lua index 46742572..71fabfd2 100644 --- a/src/view/input/statusline.lua +++ b/src/view/input/statusline.lua @@ -78,8 +78,9 @@ function Statusline:draw(status, start_y) midX - (8 * cf.fw), start_text.y + corr) end - local lw = font:getWidth(state) / 2 - gfx.print((state or '???'), + local sl = state or '???' + local lw = font:getWidth(sl) / 2 + gfx.print((sl), midX - lw, start_text.y) gfx.setColor(colors.fg) end @@ -101,15 +102,19 @@ function Statusline:draw(status, start_y) end local more_b = morelabel(custom.buffer_more) .. ' ' local more_i = morelabel(status.input_more) .. ' ' + local name = custom.name .. ' ' gfx.setColor(colors.fg) - local w_il = gfx.getFont():getWidth(" 999:9999") - local w_br = gfx.getFont():getWidth("B999 L999-999(99)") - local w_mb = gfx.getFont():getWidth(" ↕↕ ") - local w_mi = gfx.getFont():getWidth(" ↕↕ ") + local font = gfx.getFont() + local w_il = font:getWidth(" 999:9999") + local w_br = font:getWidth("B999 L999-999(99)") + local w_mb = font:getWidth(" ↕↕ ") + local w_mi = font:getWidth(" ↕↕ ") local s_mb = endTextX - w_br - w_il - w_mi - w_mb - local cw_p = gfx.getFont():getWidth(t_blp) - local cw_il = gfx.getFont():getWidth(t_ic) + local w_n = font:getWidth(name) + local s_n = s_mb - w_n - 5 + local cw_p = font:getWidth(t_blp) + local cw_il = font:getWidth(t_ic) local sxl = endTextX - (cw_p + w_il + w_mi) local s_mi = endTextX - w_il @@ -146,6 +151,9 @@ function Statusline:draw(status, start_y) --- buffer more gfx.setColor(colors.fg) gfx.print(more_b, s_mb, start_text.y) + -- filename + gfx.setColor(Color[Color.white]) + gfx.print(custom.name, s_n, start_text.y) else --- normal statusline local pos_c = ':' .. c.c diff --git a/tests/brief_output.lua b/tests/brief_output.lua new file mode 100644 index 00000000..c73a892b --- /dev/null +++ b/tests/brief_output.lua @@ -0,0 +1,234 @@ +local s = require 'say' +local pretty = require 'pl.pretty' +local term = require 'term' +local luassert = require 'luassert' +local io = io +local type = type +local string_format = string.format +local string_gsub = string.gsub +local io_write = io.write +local io_flush = io.flush +local pairs = pairs +local colors + +local isatty = io.type(io.stdout) == 'file' and term.isatty(io.stdout) + +return function(options) + local busted = require 'busted' + local handler = require 'busted.outputHandlers.base' () + local cli = require 'cliargs' + local args = options.arguments + + cli:set_name('utfTerminal output handler') + cli:flag('--color', 'force use of color') + cli:flag('--plain', 'force use of no color') + + local cliArgs, err = cli:parse(args) + if not cliArgs and err then + io.stderr:write(string.format('%s: %s\n\n', cli.name, err)) + io.stderr:write(cli.printer.generate_help_and_usage() .. '\n') + os.exit(1) + end + + if cliArgs.plain then + colors = setmetatable({}, { + __index = function() + return function(s) return s end + end + }) + luassert:set_parameter("TableErrorHighlightColor", "none") + elseif cliArgs.color then + colors = require 'term.colors' + luassert:set_parameter("TableErrorHighlightColor", "red") + else + if package.config:sub(1, 1) == '\\' and not os.getenv("ANSICON") or not isatty then + -- Disable colors on Windows. + colors = setmetatable({}, { __index = function() return function(s) return s end end }) + luassert:set_parameter("TableErrorHighlightColor", "none") + else + colors = require 'term.colors' + luassert:set_parameter("TableErrorHighlightColor", "red") + end + end + + local successDot = colors.green('\226\151\143') -- '\226\151\143' = '●' = utf8.char(9679) + local failureDot = colors.red('\226\151\188') -- '\226\151\188' = '◼' = utf8.char(9724) + local errorDot = colors.magenta('\226\156\177') -- '\226\156\177' = '✱' = utf8.char(10033) + local pendingDot = colors.yellow('\226\151\140') -- '\226\151\140' = '◌' = utf8.char(9676) + + local pendingDescription = function(pending) + local name = pending.name + + -- '\226\134\146' = '→' = utf8.char('8594') + local string = colors.yellow(s('output.pending')) .. ' \226\134\146 ' .. + colors.cyan(pending.trace.short_src) .. ' @ ' .. + colors.cyan(pending.trace.currentline) .. + '\n' .. colors.bright(name) + + if type(pending.message) == 'string' then + string = string .. '\n' .. pending.message + elseif pending.message ~= nil then + string = string .. '\n' .. pretty.write(pending.message) + end + + return string + end + + local failureMessage = function(failure) + local string = failure.randomseed + and ('Random seed: ' .. failure.randomseed .. '\n') + or '' + if type(failure.message) == 'string' then + string = string .. failure.message + elseif failure.message == nil then + string = string .. 'Nil error' + else + string = string .. pretty.write(failure.message) + end + + return string + end + + local failureDescription = function(failure, isError) + -- '\226\134\146' = '→' = utf8.char(8594) + local string = colors.red(s('output.failure')) .. ' \226\134\146 ' + if isError then + string = colors.magenta(s('output.error')) .. ' \226\134\146 ' + end + + if not failure.element.trace + or not failure.element.trace.short_src + then + string = string .. + colors.cyan(failureMessage(failure)) .. '\n' .. + colors.bright(failure.name) + else + string = string .. + colors.cyan(failure.element.trace.short_src) .. ' @ ' .. + colors.cyan(failure.element.trace.currentline) + -- .. '\n' + -- .. colors.bright(failure.name) .. '\n' + .. ' ' ..string.lines( failureMessage(failure) )[1] + end + + if options.verbose and failure.trace and failure.trace.traceback then + string = string .. '\n' .. failure.trace.traceback + end + + return string + end + + local statusString = function() + local successString = s('output.success_plural') + local failureString = s('output.failure_plural') + local pendingString = s('output.pending_plural') + local errorString = s('output.error_plural') + + local sec = handler.getDuration() + local successes = handler.successesCount + local pendings = handler.pendingsCount + local failures = handler.failuresCount + local errors = handler.errorsCount + + if successes == 0 then + successString = s('output.success_zero') + elseif successes == 1 then + successString = s('output.success_single') + end + + if failures == 0 then + failureString = s('output.failure_zero') + elseif failures == 1 then + failureString = s('output.failure_single') + end + + if pendings == 0 then + pendingString = s('output.pending_zero') + elseif pendings == 1 then + pendingString = s('output.pending_single') + end + + if errors == 0 then + errorString = s('output.error_zero') + elseif errors == 1 then + errorString = s('output.error_single') + end + + local formattedTime = string_gsub(string_format('%.6f', sec), '([0-9])0+$', '%1') + + return colors.green(successes) .. ' ' .. successString .. ' / ' .. + colors.red(failures) .. ' ' .. failureString .. ' / ' .. + colors.magenta(errors) .. ' ' .. errorString .. ' / ' .. + colors.yellow(pendings) .. ' ' .. pendingString .. ' : ' .. + colors.bright(formattedTime) .. ' ' .. s('output.seconds') + end + + handler.testEnd = function(element, parent, status, debug) + if not options.deferPrint then + local string = successDot + + if status == 'pending' then + string = pendingDot + elseif status == 'failure' then + string = failureDot + elseif status == 'error' then + string = errorDot + end + + io_write(string) + io_flush() + end + + return nil, true + end + + handler.suiteStart = function(suite, count, total) + local runString = (total > 1 and '\nRepeating all tests (run %u of %u) . . .\n\n' or '') + io_write(string_format(runString, count, total)) + io_flush() + + return nil, true + end + + handler.suiteEnd = function() + -- io_write('\n') + io_write(statusString() .. '\n') + + for _, pending in pairs(handler.pendings) do + -- io_write('\n') + -- io_write(pendingDescription(pending)..'\n') + end + + for _, err in pairs(handler.failures) do + -- io_write('\n') + io_write(failureDescription(err) .. '\n') + end + + for _, err in pairs(handler.errors) do + -- io_write('\n') + -- io_write(failureDescription(err, true)..'\n') + end + + io_write(statusString() .. '\n') + + return nil, true + end + + handler.error = function(element, parent, message, debug) + io_write(errorDot) + io_flush() + + return nil, true + end + + busted.subscribe({ 'test', 'end' }, handler.testEnd, + { predicate = handler.cancelOnPending }) + busted.subscribe({ 'suite', 'start' }, handler.suiteStart) + busted.subscribe({ 'suite', 'end' }, handler.suiteEnd) + -- busted.subscribe({ 'error', 'file' }, handler.error) + -- busted.subscribe({ 'failure', 'file' }, handler.error) + -- busted.subscribe({ 'error', 'describe' }, handler.error) + -- busted.subscribe({ 'failure', 'describe' }, handler.error) + + return handler +end diff --git a/tests/editor/buffer_spec.lua b/tests/editor/buffer_spec.lua index a61ece0d..c5823874 100644 --- a/tests/editor/buffer_spec.lua +++ b/tests/editor/buffer_spec.lua @@ -3,13 +3,13 @@ local parser = require('model.lang.lua.parser')() require('util.table') - describe('Buffer #editor', function() local w = 64 local chunker = function(t, single) return parser.chunker(t, w, single) end - -- local chunker = parser.chunker + + local printer = parser.pprint local hl = parser.highlighter local noop = function() end @@ -23,11 +23,12 @@ describe('Buffer #editor', function() local cbuffer = BufferModel('untitled', tst, noop) local bc = cbuffer:get_content() assert.same(cbuffer.content_type, 'plain') - assert.same(4, #bc) + assert.same(5, #bc) assert.same(l1, bc[1]) assert.same(l2, bc[2]) assert.same(l3, bc[3]) assert.same(l4, bc[4]) + assert.same('', bc[5]) end) it('renders lua', function() @@ -39,7 +40,7 @@ describe('Buffer #editor', function() local cbuffer = BufferModel('untitled.lua', tst, noop, chunker, hl) local bc = cbuffer:get_content() assert.same(cbuffer.content_type, 'lua') - assert.same(4, #bc) + assert.same(5, #bc) assert.same({ l1 }, bc[1].lines) assert.same({ l2 }, bc[2].lines) assert.same({ l3 }, bc[3].lines) @@ -47,6 +48,7 @@ describe('Buffer #editor', function() end) describe('setup', function() + local buffer, bufcon local meat = [[function sierpinski(depth) lines = { '*' } for i = 2, depth + 1 do @@ -65,14 +67,18 @@ end]] print(sierpinski(4))]]) - local buffer = BufferModel('test.lua', txt, noop, chunker, hl) + + lazy_setup(function() + buffer = BufferModel('test.lua', txt, + noop, chunker, hl) + bufcon = buffer:get_content() + end) it('sets name', function() assert.same('test.lua', buffer.name) end) - local bufcon = buffer:get_content() it('sets content', function() assert.same('block', bufcon:type()) - assert.same(4, #bufcon) + assert.same(5, #bufcon) assert.same({ '--- @param depth integer' }, bufcon[1].lines) assert.same(string.lines(meat), bufcon[2].lines) assert.is_true(table.is_instance(bufcon[3], 'empty')) @@ -81,113 +87,155 @@ print(sierpinski(4))]]) end) describe('modifications', function() + local trtl = + 'Turtle graphics game inspired the LOGO family of languages.' + local turtle_doc = { + '', + trtl, + } + local turtle = { + '--- @diagnostic disable', + 'width, height = gfx.getDimensions()', + 'midx = width / 2', + 'midy = height / 2', + 'incr = 5', + '', + 'tx, ty = midx, midy', + 'debug = false', + 'debugColor = Color.yellow', + '', + 'bg_color = Color.black', + '', + 'local function drawHelp()', + ' gfx.setColor(Color[Color.white])', + ' gfx.print("Press [I] to open console", 20, 20)', + ' gfx.print("Enter \'forward\', \'back\', \'left\', or \'right\' to move the turtle!", 20, 40)', + 'end', + '', + 'local function drawDebuginfo()', + ' gfx.setColor(Color[debugColor])', + ' local label = string.format("Turtle position: (%d, %d)", tx, ty)', + ' gfx.print(label, width - 200, 20)', + 'end', + '', + 'function love.draw()', + ' drawBackground()', + ' drawHelp()', + ' drawTurtle(tx, ty)', + ' if debug then drawDebuginfo() end', + 'end', + '', + 'function love.keypressed(key)', + ' if love.keyboard.isDown("lshift", "rshift") then', + ' if key == \'r\' then', + ' tx, ty = midx, midy', + ' end', + ' end', + ' if key == \'space\' then', + ' debug = not debug', + ' end', + ' if key == \'pause\' then', + ' stop()', + ' end', + 'end', + '', + 'function love.keyreleased(key)', + ' if key == \'i\' then', + ' input_text(r)', + ' end', + ' if key == \'return\' then', + ' eval()', + ' end', + '', + ' if love.keyboard.isDown("lctrl", "rctrl") then', + ' if key == "escape" then', + ' love.event.quit()', + ' end', + ' end', + 'end', + '', + 'local t = 0', + 'function love.update(dt)', + ' t = t + dt', + ' if ty > midy then', + ' debugColor = Color.red', + ' end', + 'end', + '', + } + local buffer describe('plaintext', function() - local turtle_doc = { + lazy_setup(function() + buffer = BufferModel('README', turtle_doc) + end) + local qed = 'qed.' + local new = { + '', + trtl, '', - 'Turtle graphics game inspired the LOGO family of languages.', + qed, + '' } - local buffer = BufferModel('README', turtle_doc) it('insert newline', function() assert.same(#turtle_doc + 1, buffer:get_selection()) - local qed = 'qed.' - buffer:replace_selected_text({ qed }) + buffer:replace_content({ qed }) assert.same(#turtle_doc + 1, buffer:get_selection()) assert.same(qed, buffer:get_selected_text()) buffer:insert_newline() - local new = { - '', - 'Turtle graphics game inspired the LOGO family of languages.', - '', - qed, - } + assert.same(new, buffer:get_text_content()) end) + + describe('insert', function() + local addbuf + lazy_setup(function() + addbuf = BufferModel('README', turtle_doc) + end) + it('one', function() + local newlines = { 'test' } + addbuf:insert_content(newlines, 2) + local exp = { + '', + 'test', + 'Turtle graphics game inspired the LOGO family of languages.', + '', + } + local res = addbuf:get_text_content() + assert.same(exp, res) + end) + + it('multiple', function() + local newlines = { 'line1', 'line2', } + addbuf:insert_content(newlines, 1) + local exp = { + 'line1', + 'line2', + '', + 'test', + 'Turtle graphics game inspired the LOGO family of languages.', + '', + } + local res = addbuf:get_text_content() + assert.same(exp, res) + end) + end) end) describe('lua', function() - local turtle = { - '--- @diagnostic disable', - 'width, height = gfx.getDimensions()', - 'midx = width / 2', - 'midy = height / 2', - 'incr = 5', - '', - 'tx, ty = midx, midy', - 'debug = false', - 'debugColor = Color.yellow', - '', - 'bg_color = Color.black', - '', - 'local function drawHelp()', - ' gfx.setColor(Color[Color.white])', - ' gfx.print("Press [I] to open console", 20, 20)', - ' gfx.print("Enter \'forward\', \'back\', \'left\', or \'right\' to move the turtle!", 20, 40)', - 'end', - '', - 'local function drawDebuginfo()', - ' gfx.setColor(Color[debugColor])', - ' local label = string.format("Turtle position: (%d, %d)", tx, ty)', - ' gfx.print(label, width - 200, 20)', - 'end', - '', - 'function love.draw()', - ' drawBackground()', - ' drawHelp()', - ' drawTurtle(tx, ty)', - ' if debug then drawDebuginfo() end', - 'end', - '', - 'function love.keypressed(key)', - ' if love.keyboard.isDown("lshift", "rshift") then', - ' if key == \'r\' then', - ' tx, ty = midx, midy', - ' end', - ' end', - ' if key == \'space\' then', - ' debug = not debug', - ' end', - ' if key == \'pause\' then', - ' stop()', - ' end', - 'end', - '', - 'function love.keyreleased(key)', - ' if key == \'i\' then', - ' input_text(r)', - ' end', - ' if key == \'return\' then', - ' eval()', - ' end', - '', - ' if love.keyboard.isDown("lctrl", "rctrl") then', - ' if key == "escape" then', - ' love.event.quit()', - ' end', - ' end', - 'end', - '', - 'local t = 0', - 'function love.update(dt)', - ' t = t + dt', - ' if ty > midy then', - ' debugColor = Color.red', - ' end', - 'end', - '', - } - local text = turtle - - local buffer = BufferModel('main.lua', text, - noop, chunker, hl) - local bc = buffer:get_content() - local n_blocks = 24 + local bc + lazy_setup(function() + buffer = BufferModel('main.lua', turtle, + noop, chunker, hl) + bc = buffer:get_content() + end) + local n_blocks = 25 it('invariants', function() assert.same(n_blocks, #bc) assert.same(n_blocks, buffer:get_content_length()) - assert.same(text, buffer:get_text_content()) + assert.same(turtle, buffer:get_text_content()) - assert.same(n_blocks + 1, buffer:get_selection()) + assert.same(n_blocks, buffer:get_selection()) local ln = buffer:get_selection_start_line() assert.same(68, ln) end) @@ -201,7 +249,7 @@ print(sierpinski(4))]]) delbuf:delete_selected_text() assert.same(n_blocks - 2, delbuf:get_content_length()) assert.same(1, delbuf:get_selection()) - assert.same({ text[3] }, delbuf:get_selected_text()) + assert.same({ turtle[3] }, delbuf:get_selected_text()) end) it('insert empty', function() @@ -242,13 +290,14 @@ print(sierpinski(4))]]) local ln = replbuf:get_selection_start_line() assert.same(61, ln) assert.same({ 'local t = 0' }, replbuf:get_selected_text()) - assert.same({ text[ln] }, replbuf:get_selected_text()) + assert.same({ turtle[ln] }, replbuf:get_selected_text()) local empty = Empty(ln) - local ins, n = replbuf:replace_selected_text({ empty }) + local ins, n = replbuf:replace_content({ empty }) assert.truthy(ins) assert.same(1, n) end) + it('replacing block with empty', function() local replbuf = table.clone(buffer) replbuf:move_selection('down', nil, true) @@ -265,10 +314,11 @@ print(sierpinski(4))]]) replbuf:get_selected_text()) local empty = Empty(ln) - local ins, n = replbuf:replace_selected_text({ empty }) + local ins, n = replbuf:replace_content({ empty }) assert.truthy(ins) assert.same(1, n) end) + it('replacing middle block with empty', function() local replbuf = table.clone(buffer) replbuf:move_selection('down', nil, true) @@ -295,7 +345,7 @@ print(sierpinski(4))]]) replbuf:get_selected_text()) local empty = Empty(ln) - local ins, n = replbuf:replace_selected_text({ empty }) + local ins, n = replbuf:replace_content({ empty }) assert.truthy(ins) assert.same(1, n) @@ -322,7 +372,7 @@ print(sierpinski(4))]]) local ok, chunks = chunker(new, true) assert.is_true(ok) - local _, n = replbuf:replace_selected_text(chunks) + local _, n = replbuf:replace_content(chunks) assert.same(1, n) end) it('breaking line in block', function() @@ -343,20 +393,24 @@ print(sierpinski(4))]]) local ok, chunks = chunker(new, true) assert.is_true(ok) - local _, n = replbuf:replace_selected_text(chunks) + local _, n = replbuf:replace_content(chunks) assert.same(1, n) assert.same(24, replbuf:get_selection()) end) - describe('lua', function() - local addbuf = table.clone(buffer) - local orig_b = { 'function love.update(dt)', - ' t = t + dt', - ' if ty > midy then', - ' debugColor = Color.red', - ' end', - 'end' } + describe('lua newline', function() + local addbuf + local orig_b + lazy_setup(function() + addbuf = table.clone(buffer) + orig_b = { 'function love.update(dt)', + ' t = t + dt', + ' if ty > midy then', + ' debugColor = Color.red', + ' end', + 'end' } + end) it('introducing a new line', function() addbuf:move_selection('down', nil, true) @@ -369,7 +423,7 @@ print(sierpinski(4))]]) local ok, chunks = chunker(new, true) assert.is_true(ok) - local _, n = addbuf:replace_selected_text(chunks) + local _, n = addbuf:replace_content(chunks) assert.same(2, n) assert.same(23, addbuf:get_selection()) @@ -383,14 +437,88 @@ print(sierpinski(4))]]) local ok, chunks = chunker(new, true) assert.is_true(ok) - local _, n = addbuf:replace_selected_text(chunks) + local _, n = addbuf:replace_content(chunks) assert.same(1, n) assert.same(25, addbuf:get_selection()) end) end) + describe('lua insert', function() + local addbuf + lazy_setup(function() + addbuf = table.clone(buffer) + end) + it('insert two lines', function() + local lines = { 'x = 1; y =2' } + local pretty = printer(lines) + local _, newblocks = chunker(pretty, true) + + addbuf:insert_content(newblocks, 4) + local exp = { + '--- @diagnostic disable', + 'width, height = gfx.getDimensions()', + 'midx = width / 2', + 'x = 1', + 'y = 2', + 'midy = height / 2', + 'incr = 5', + } + local res = table.slice( + addbuf:get_text_content(), 1, 7 + ) + assert.same(exp, res) + end) + it('insert after empty lines', function() + local lines = { 'string = "empty"' } + local pretty = printer(lines) + local _, newblocks = chunker(pretty, true) + + addbuf:insert_content(newblocks, 9) + local exp = { + 'midx = width / 2', + 'x = 1', + 'y = 2', + 'midy = height / 2', + 'incr = 5', + '', + 'string = "empty"', + 'tx, ty = midx, midy' + } + local res = table.slice( + addbuf:get_text_content(), 3, 10 + ) + assert.same(exp, res) + end) + it('insert three lines', function() + local lines = { + 'a1 = 1', + ' a2 = 2', + 'z="a"', + } + local pretty = printer(lines) + local _, newblocks = chunker(pretty, true) + + addbuf:insert_content(newblocks, 4) + local exp = { + '--- @diagnostic disable', + 'width, height = gfx.getDimensions()', + 'midx = width / 2', + 'a1 = 1', + 'a2 = 2', + 'z = "a"', + 'x = 1', + 'y = 2', + 'midy = height / 2', + 'incr = 5', + } + local res = table.slice( + addbuf:get_text_content(), 1, 10 + ) + assert.same(exp, res) + end) + end) --- end --- end) end) diff --git a/tests/editor/chunker_inputs.lua b/tests/editor/chunker_inputs.lua new file mode 100644 index 00000000..339834b1 --- /dev/null +++ b/tests/editor/chunker_inputs.lua @@ -0,0 +1,204 @@ +require('model.editor.content') + +--- @param s str +--- @param r Block[] +local prep = function(s, r) + return { + string.lines(s), + r + } +end + +local sierp_code = [[function sierpinski(depth) + lines = { '*' } + for i = 2, depth + 1 do + sp = string.rep(' ', 2 ^ (i - 2)) + tmp = {} -- comment + for idx, line in ipairs(lines) do + tmp[idx] = sp .. line .. sp + tmp[idx + #lines] = line .. ' ' .. line + end + lines = tmp + end + return table.concat(lines, '\n') +end + +print(sierpinski(4))]] +local sierp_code_2 = [[function sierpinski(depth) + lines = { '*' } + for i = 2, depth + 1 do + sp = string.rep(' ', 2 ^ (i - 2)) + tmp = {} -- comment + for idx, line in ipairs(lines) do + tmp[idx] = sp .. line .. sp + tmp[idx + #lines] = line .. ' ' .. line + end + lines = tmp + end + return table.concat(lines, '\n') +end + + +print(sierpinski(4))]] +local sierp_res = { + Chunk(string.lines([[function sierpinski(depth) + lines = { '*' } + for i = 2, depth + 1 do + sp = string.rep(' ', 2 ^ (i - 2)) + tmp = {} -- comment + for idx, line in ipairs(lines) do + tmp[idx] = sp .. line .. sp + tmp[idx + #lines] = line .. ' ' .. line + end + lines = tmp + end + return table.concat(lines, '\n') +end]]), Range(1, 13)), + Empty(14), + Chunk({ 'print(sierpinski(4))' }, Range.singleton(15)), + Empty(16) +} + +local chonk_1 = Chunk({ + 'function chonky()', + ' return {"big", "chungus"}', + 'end' + }, + Range(1, 3) +) +local chonk_res = { + chonk_1, + Empty(4), + Chunk({ 'print(string.unlines(chonky()))' }, + Range.singleton(5)), + Empty(6), +} + +return { + prep( + "local x = 1", + { Chunk({ 'local x = 1' }, Range.singleton(1)), + Empty(2) } + ), + prep({ '' }, { Empty(1) }), + prep({ '', '' }, { Empty(1) }), + prep({ ' ', '' }, { Empty(1) }), + + prep( + "\nlocal x = 1", + { Empty(1), + Chunk({ 'local x = 1' }, Range.singleton(2)), + Empty(3) } + ), + prep( + "local x = 1\n", + { Chunk({ 'local x = 1' }, Range.singleton(1)), + Empty(2) } + ), + prep( + "local x = 1\n\n\n\n", + { Chunk({ 'local x = 1' }, Range.singleton(1)), + Empty(2) } + ), + prep( + "\nlocal x = 1\n", + { Empty(1), + Chunk({ 'local x = 1' }, Range.singleton(2)), + Empty(3) } + ), + prep( + "\n\n\nlocal x = 1\n\n\n", + { Empty(1), + Chunk({ 'local x = 1' }, Range.singleton(2)), + Empty(3) } + ), + + prep(sierp_code, sierp_res), + prep(sierp_code_2, sierp_res), + + prep([[function chonky() + return {"big", "chungus"} +end + +print(string.unlines(chonky()))]], chonk_res), + prep([[function chonky() + return {"big", "chungus"} +end +print(string.unlines(chonky()))]], + { + chonk_1, + Chunk({ 'print(string.unlines(chonky()))' }, + Range.singleton(4)), + Empty(5), + } + ), + + --- here comes the fun + prep([[function chonky() + return {"big", "chungus"} +end + + +print(string.unlines(chonky()))]], chonk_res), + + prep([[function chonky() + return {"big", "chungus"} +end + + +print(string.unlines(chonky())) +print(1)]], { + chonk_1, + Empty(4), + Chunk({ 'print(string.unlines(chonky()))' }, + Range.singleton(5)), + Chunk({ 'print(1)' }, + Range.singleton(6)), + Empty(7), + }), + + prep([[function chonky() + return {"big", "chungus"} +end + + +print(string.unlines(chonky())) +print(1) + +x = 1 +y = 2]], { + chonk_1, + Empty(4), + Chunk({ 'print(string.unlines(chonky()))' }, + Range.singleton(5)), + Chunk({ 'print(1)' }, Range.singleton(6)), + Empty(7), + Chunk({ 'x = 1' }, Range.singleton(8)), + Chunk({ 'y = 2' }, Range.singleton(9)), + Empty(10), + }), + + prep([[function chonky() + return {"big", "chungus"} +end + + +print(string.unlines(chonky())) +print(1) + +x = 1 + +y = 2]], { + chonk_1, + Empty(4), + Chunk({ 'print(string.unlines(chonky()))' }, + Range.singleton(5)), + Chunk({ 'print(1)' }, Range.singleton(6)), + Empty(7), + Chunk({ 'x = 1' }, Range.singleton(8)), + Empty(9), + Chunk({ 'y = 2' }, Range.singleton(10)), + Empty(11), + }), + +} diff --git a/tests/editor/chunker_spec.lua b/tests/editor/chunker_spec.lua new file mode 100644 index 00000000..d6d9b9ed --- /dev/null +++ b/tests/editor/chunker_spec.lua @@ -0,0 +1,28 @@ +local parser = require('model.lang.lua.parser')() + +require('util.table') + +local inputs = require('tests.editor.chunker_inputs') +local TU = require('tests.testutil') + + +describe('parser.chunker #chunk', function() + local w = TU.wrap + local chunker = function(t, single) + return parser.chunker(t, w, single) + end + + describe('produces blocks', function() + for i, test in ipairs(inputs) do + local str = test[1] + local blk = test[2] + + local ok, output = chunker(str) + it('matches ' .. i, function() + assert.is_true(ok) + assert.same(blk, output) + end) + end + end) + +end) diff --git a/tests/editor/editor_spec.lua b/tests/editor/editor_spec.lua index 2e983e33..2c93f036 100644 --- a/tests/editor/editor_spec.lua +++ b/tests/editor/editor_spec.lua @@ -20,9 +20,12 @@ describe('Editor #editor', function() mock.mock_love(love) end) + local trtl = + 'Turtle graphics game inspired the LOGO family of languages.' + local turtle_doc = { '', - 'Turtle graphics game inspired the LOGO family of languages.', + trtl, '', } @@ -78,9 +81,9 @@ describe('Editor #editor', function() local sel = buffer:get_selection() local sel_t = buffer:get_selected_text() --- default selection is at the end - assert.same(#turtle_doc + 1, sel) + assert.same(#turtle_doc, sel) --- and it's an empty line, of course - assert.same({}, sel_t) + assert.same('', sel_t) end) end) @@ -96,7 +99,7 @@ describe('Editor #editor', function() controller:open('turtle', turtle_doc, save) local buffer = controller:get_active_buffer() - local start_sel = #turtle_doc + 1 + local start_sel = #turtle_doc it('opens', function() local bc = buffer:get_content() @@ -109,15 +112,13 @@ describe('Editor #editor', function() --- default selection is at the end assert.same(start_sel, sel) --- and it's an empty line, of course - assert.same({}, sel_t) + assert.same('', sel_t) end) it('interacts', function() --- select middle line mock.keystroke('up', press) assert.same(start_sel - 1, buffer:get_selection()) - mock.keystroke('up', press) - assert.same(start_sel - 2, buffer:get_selection()) assert.same(turtle_doc[2], buffer:get_selected_text()) --- load it local input = function() @@ -127,7 +128,7 @@ describe('Editor #editor', function() assert.same({ turtle_doc[2] }, input()) mock.keystroke('end', press) mock.keystroke('down', press) - assert.same(start_sel - 1, buffer:get_selection()) + assert.same(start_sel, buffer:get_selection()) -- load the empty mock.keystroke('escape', press) assert.same({ '' }, input()) @@ -144,17 +145,18 @@ describe('Editor #editor', function() mock.keystroke('return', press) local new = { '', - 'Turtle graphics game inspired the LOGO family of languages.', + trtl, '-- test', + '' } assert.same(new, buffer:get_text_content()) --- input clears assert.same({ '' }, input()) --- highlight moves down - assert.same(start_sel, buffer:get_selection()) + assert.same(start_sel + 1, buffer:get_selection()) mock.keystroke('up', press) - assert.same(start_sel - 1, buffer:get_selection()) + assert.same(start_sel, buffer:get_selection()) --- replace controller:textinput('i') controller:textinput('n') @@ -219,7 +221,7 @@ describe('Editor #editor', function() end) it('bottoms out', function() local limit = #sierpinski + visible.overscroll - assert.same(Range(limit - l + 1, limit), visible.range) + -- assert.same(Range(limit - l + 2, limit), visible.range) end) end) @@ -244,8 +246,8 @@ describe('Editor #editor', function() local scroll = bv.SCROLL_BY local clen = visible:get_content_length() - local off = clen - l + 1 - local start_range = Range(off + 1, clen + 1) + local off = clen - l + local start_range = Range(off + 1, clen) it('loads', function() --- inital scroll is at EOF, meaning last l lines are visible --- plus the phantom line @@ -295,19 +297,21 @@ describe('Editor #editor', function() --- default selection is at the end assert.same(#sierpinski + 1, sel) --- and it's an empty line, of course - assert.same({}, sel_t) + assert.same('', sel_t) it('from below', function() mock.keystroke('pageup', press) mock.keystroke('up', press) - --- it's now one above the starting range, the phantom line not visible - --- assert.same(start_range:translate(-1), visible.range) + --- it's now one above the starting range, the + --- phantom line not visible + -- assert.same(start_range:translate(-1), visible.range) mock.keystroke('pageup', press) mock.keystroke('down', press) - --- after scrolling up and moving the sel back, we are back to the start + --- after scrolling up and moving the sel back, we + --- are back to the start --- TODO + assert.same(Range(19, 24), visible.range) -- assert.same(start_range, visible.range) - assert.same(Range(18, 23), visible.range) end) it('to above', function() local srs = visible.range.start @@ -319,10 +323,11 @@ describe('Editor #editor', function() local d = cs - srs --- TODO -- assert.same(start_range:translate(d), visible.range) - assert.same(start_range:translate(d + 2), visible.range) + assert.same(start_range:translate(d + 3), + visible.range) mock.keystroke('up', press) -- assert.same(start_range:translate(d - 1), visible.range) - assert.same(start_range:translate(d + 1), visible.range) + assert.same(start_range:translate(d + 2), visible.range) end) it('tops out', function() --- move up to the first line @@ -388,7 +393,7 @@ describe('Editor #editor', function() --- warps to bottom --- TODO -- assert.same(start_range, visible.range) - assert.same(Range(18, 23), visible.range) + assert.same(Range(19, 24), visible.range) assert.is_not.same(sel, buffer:get_selection()) end) it('to top', function() @@ -413,7 +418,7 @@ describe('Editor #editor', function() assert.same({ '' }, inter:get_text()) end) it('inserts', function() - mock.keystroke('up', press) + -- mock.keystroke('up', press) local prefix = 'asd ' local selected = buffer:get_selected_text() inter:add_text(prefix) @@ -440,7 +445,7 @@ describe('Editor #editor', function() assert.same('lua', buffer.content_type) it('length is correct', function() assert.same('block', cont:type()) - assert.same(3, buffer:get_content_length()) + assert.same(4, buffer:get_content_length()) end) it('changing single line', function() local modified = table.clone(sierpinski) diff --git a/tests/editor/visible_structured_content_spec.lua b/tests/editor/visible_structured_content_spec.lua new file mode 100644 index 00000000..ad254db8 --- /dev/null +++ b/tests/editor/visible_structured_content_spec.lua @@ -0,0 +1,72 @@ +require('model.editor.bufferModel') +local parser = require('model.lang.lua.parser')() +require("view.editor.visibleStructuredContent") + +require("util.string.string") +TU = require('tests.testutil') + +local lua_ex = +[[local x, y + +function eval(input) + local f = actions[input] + if f then + f() + end +end + +function love.draw() + gfx.setFont(font) + drawBackground() + drawHelp() + drawTurtle(tx, ty) + if debug then + drawDebuginfo() + end +end + +function love.update() + if ty > midy then + debug_color = Color.red + end + if not r:is_empty() then + eval(r()) + end +end]] + +local chunker = function(t, single) + return parser.chunker(t, TU.w, single) +end +-- local chunker = parser.chunker +local hl = parser.highlighter + +describe('VisibleStructuredContent #visible', function() + local buffer, vsc + local lines = string.lines(lua_ex) + lazy_setup(function () + buffer = BufferModel('main.lua', + lines, TU.noop, chunker, hl) + vsc = VisibleStructuredContent({ + wrap_w = TU.w, + overscroll_max = TU.SCROLL_BY, + size_max = TU.LINES, + view_config = TU.mock_view_cfg + }, + buffer:get_content(), + buffer.highlighter, + buffer.truncer + ) + end) + + it('invariants', function() + assert.same(TU.LINES, vsc.opts.size_max) + assert.same(TU.SCROLL_BY, vsc.opts.overscroll_max) + end) + + it('scrolls', function() + assert.same(27, #lines) + -- assert.same(11, vsc.offset) + vsc:move_range(-50) + assert.same(Range(1, TU.LINES), vsc.range) + end) +end) diff --git a/tests/interpreter/ast_spec.lua b/tests/interpreter/ast_spec.lua index 0f58e524..67fd1d34 100644 --- a/tests/interpreter/ast_spec.lua +++ b/tests/interpreter/ast_spec.lua @@ -47,8 +47,7 @@ describe('parser #ast', function() io.write(term.reset) end if show_ast then - -- Log.info(parser.pprint(v, { hide_lineinfo = false })) - -- Log.debug(Debug.terse_hash(v, nil, nil, true)) + Log.debug(Debug.terse_ast(ast, true)) end return code, seen_comments end