diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a1a14ce..2e020464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,10 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-diffs-${{ matrix.nvim }} - path: tests/cases/**/diff-*.txt + path: | + tests/cases/**/diff-*.txt + tests/stdout.txt + tests/stderr.txt - name: Generate coverage report run: | diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 00000000..e9e69b30 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,47 @@ +{ + "runtime": { + "version": "LuaJIT", + "path": [ + "lua/?.lua", + "lua/?/init.lua" + ] + }, + "workspace": { + "checkThirdParty": false, + "library": [ + "${3rd}/luv/library", + "${3rd}/busted/library", + "${3rd}/neovim/runtime/lua" + ] + }, + "diagnostics": { + "globals": [ + "vim" + ], + "undefined-global": "Error", + "strict": true, + "type-check": true, + "unused-local": "Warning", + "unused-function": "Warning", + "unused-vararg": "Warning", + "redefined-local": "Warning", + "duplicate-set-field": "Warning", + "missing-parameter": "Warning", + "param-type-mismatch": "Warning", + "cast-local-type": "Warning", + "need-check-nil": "Warning" + }, + "hint": { + "enable": true, + "setType": true, + "paramType": true, + "paramName": "All", + "arrayIndex": "Auto" + }, + "format": { + "enable": false + }, + "telemetry": { + "enable": false + } +} \ No newline at end of file diff --git a/.stylua.toml b/.stylua.toml new file mode 100644 index 00000000..1eb29df9 --- /dev/null +++ b/.stylua.toml @@ -0,0 +1,2 @@ +# .stylua.toml +column_width = 80 diff --git a/docs/testing/embedded.md b/docs/testing/embedded.md index e01e1513..2e479eae 100644 --- a/docs/testing/embedded.md +++ b/docs/testing/embedded.md @@ -29,7 +29,6 @@ import traceback * 詳細は tir-embedded を参照のこと * まずカレント行から key を決定することを試みる。 -* 失敗した場合はファイル先頭から検索する。 * "|"を二つ以上含むこと * 行末が"|"であること * カレント行が条件を満たさない場合は先頭行から検索する @@ -81,8 +80,11 @@ import traceback | No | Preconditions | Action | Expected | Date | Notes | Commit Message | | --- | --- | --- | --- | --- | --- | --- | +| | | health | OK | 26/07/16 | | feat: support both legacy and new parser version interfaces | +| | edit table.txt | Tir toggle (row # 1) | show grid block | 26/07/18 | | Add grid display support for text files in :Tir toggle | +| | edit table.txt | Tir toggle (row # 1) 2 time | show flat | 26/07/19 | | | | | record.prefix=" #hoge# " | Tir _read_tir ./tests/data/simple.tir | #hoge# | | | | | | | Tir _write_tir /tmp/hoge.tir | record.prefix=" #hoge# " | | | | -| | record.prefix=" // " | " // " -> " // " | 戻る | | | | -| | record.prefix=" // " | " // " -> " " | 戻る | | | | +| | record.prefix=" // " | " // " -> " // " | 戻る | | | | +| | record.prefix=" // " | " // " -> " " | 戻る | | | | diff --git a/lua/tirenvi/app/auto_wrap.lua b/lua/tirenvi/app/auto_wrap.lua new file mode 100644 index 00000000..6d8b8fec --- /dev/null +++ b/lua/tirenvi/app/auto_wrap.lua @@ -0,0 +1,51 @@ +local api = vim.api -- Neovim + +local fn = vim.fn --Neovim + +local config = require("tirenvi.config") -- Root + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser + +local buf_state = require("tirenvi.io.buf_state") -- IO + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +local function apply_wrap(winid, should_wrap) + if vim.wo[winid].wrap ~= should_wrap then + vim.wo[winid].wrap = should_wrap + end +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +function M.auto_wrap(ctx) + if not config.ui.manage_wrap then + return + end + if not buf_state.is_allow_plain(ctx.bufnr) then + apply_wrap(ctx.winid, false) + return + end + -- Fast path for CursorMoved. + -- We only need the current line of the current window. + local line = api.nvim_get_current_line() + local line_width = fn.strdisplaywidth(line) + local win_span = buf_state.get_win_span(ctx.winid) + local is_over = win_span < line_width + local is_plain = not tir_buf.has_pipe({ line }) + if is_over then + apply_wrap(ctx.winid, is_plain) + end +end + +return M diff --git a/lua/tirenvi/app/common.lua b/lua/tirenvi/app/common.lua new file mode 100644 index 00000000..e885ce72 --- /dev/null +++ b/lua/tirenvi/app/common.lua @@ -0,0 +1,101 @@ +local width_layout = require("tirenvi.width.layout") -- Width + +local flat_parser = require("tirenvi.parser.flat_parser") -- Parser +local buf_parser = require("tirenvi.parser.buf_parser") + +local buf_state = require("tirenvi.io.buf_state") -- IO +local writer = require("tirenvi.io.writer") +local attr_store = require("tirenvi.io.attr_store") +local reader = require("tirenvi.io.reader") +local Request = require("tirenvi.io.request") + +local Document = require("tirenvi.core.document") -- Core +local Attrs = require("tirenvi.core.attrs") + +local util = require("tirenvi.util.util") -- Util +local Range = require("tirenvi.util.range") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param ctx Context +---@param tirdoc Document +local function apply_wrap_mode(ctx, tirdoc) + width_layout.compute(ctx.winid, tirdoc) +end + +---@param ctx Context +---@param tirdoc Document +---@param is_write_pre boolean|nil +local function tirdoc_to_flat(ctx, r_result, tirdoc, is_write_pre) + local parser = buf_state.get(ctx.bufnr, buf_state.IKEY.PARSER) + local fltlines = flat_parser.unparse(parser, tirdoc) + local req_w = Request.new_writer(r_result, fltlines, is_write_pre) + local attrs = vim.deepcopy(r_result.attrs) + if not is_write_pre then + Attrs.remove_range(attrs) + log.watch("ATTR", Attrs.debug_attrs(attrs, "[9]CHACHED:")) + buf_state.set_buffer_tirbuf(ctx.bufnr, false) + attr_store.write(ctx.bufnr, attrs) + end + writer.write(ctx, req_w) +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@param r_result ReadResult +---@return Document +function M.buflines_to_bufdoc_text_driven(ctx, r_result) + local opts = { first = r_result.range.first } + local bufdoc = buf_parser.parse(ctx, r_result, opts) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[1]DOC ATTR:")) + Document.apply_attrs(bufdoc, r_result.attrs) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[4]CACHED:")) + return bufdoc +end + +---@param ctx Context +---@param r_result ReadResult +---@param doc Document +---@param opts DocToBufLinesOpts|nil +function M.doc_to_buflines(ctx, r_result, doc, opts) + local no_undo = opts and opts.no_undo or false + local no_normalize = opts and opts.no_normalize or false + local tirdoc = doc + if not doc._tir then + tirdoc = Document.from_bufdoc(doc, no_normalize) + end + apply_wrap_mode(ctx, tirdoc) + local bufdoc = Document.to_bufdoc(tirdoc) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[9]DOC ATTR:")) + local attrs = Document.replace_attrs(bufdoc, r_result.range, r_result.attrs) + log.watch("ATTR", Attrs.debug_attrs(attrs, "[9]CHACHED:")) + buf_state.set_buffer_tirbuf(ctx.bufnr, Attrs.has_grid(attrs)) + attr_store.write(ctx.bufnr, attrs) + local buflines = buf_parser.unparse(bufdoc) + if not util.same_str_array(buflines, r_result.lines or "") then + local req_w = Request.new_writer(r_result, buflines, no_undo) + writer.write(ctx, req_w) + end +end + +---@param ctx Context +---@param is_write_pre boolean|nil +---@return ReadResult +function M.to_flat(ctx, is_write_pre) + local r_result = reader.read(ctx, Range.WHOLE) + local bufdoc = M.buflines_to_bufdoc_text_driven(ctx, r_result) + local tirdoc = Document.from_bufdoc(bufdoc) + tirdoc_to_flat(ctx, r_result, tirdoc, is_write_pre) + return r_result +end + +return M diff --git a/lua/tirenvi/app/context.lua b/lua/tirenvi/app/context.lua deleted file mode 100644 index fb2d9a15..00000000 --- a/lua/tirenvi/app/context.lua +++ /dev/null @@ -1,55 +0,0 @@ ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ - -local Parser = require("tirenvi.parser.parser") -local buffer = require("tirenvi.io.buffer") -local buffer_line_provider = require("tirenvi.io.buffer_line_provider") -local log = require("tirenvi.util.log") - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ - -local M = {} - -local api = vim.api - ----@class Context ----@field bufnr number ----@field winid number ----@field filetype string ----@field parser Parser|nil ----@field line_provider LineProvider - --- private helpers - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param bufnr number|nil ----@return Context -function M.from_buf(bufnr) - bufnr = bufnr or api.nvim_get_current_buf() - local winid = api.nvim_get_current_win() - local filetype = buffer.get(bufnr, buffer.IKEY.FILETYPE) - local parser = Parser.resolve_parser(filetype) - ---@type Context - return - { - bufnr = bufnr, - winid = winid, - filetype = filetype, - parser = parser, - line_provider = buffer_line_provider.new(bufnr) - } -end - ----@parma self Context ----@return boolean -function M:is_allow_plain() - return self.parser and (self.parser.allow_plain or false) or false -end - -return M diff --git a/lua/tirenvi/app/filetype.lua b/lua/tirenvi/app/filetype.lua new file mode 100644 index 00000000..e4614016 --- /dev/null +++ b/lua/tirenvi/app/filetype.lua @@ -0,0 +1,37 @@ +local bo = vim.bo -- Neovim + +local common = require("tirenvi.app.common") -- App + +local Parser = require("tirenvi.parser.parser") -- Parser + +local buf_state = require("tirenvi.io.buf_state") -- IO +local attr_store = require("tirenvi.io.attr_store") + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +-- Public API + +---@param ctx Context +function M.on_filetype(ctx) + local old_filetype = buf_state.get(ctx.bufnr, buf_state.IKEY.FILETYPE) + local new_filetype = bo[ctx.bufnr].filetype + -- log.debug("filetype %s -> %s", tostring(old_filetype), tostring(new_filetype)) + if old_filetype and old_filetype == new_filetype then + return + end + if old_filetype then + common.to_flat(ctx) + end + buf_state.set(ctx.bufnr, buf_state.IKEY.FILETYPE, new_filetype) + buf_state.set_buffer_tirbuf(ctx.bufnr, false) + attr_store.write(ctx.bufnr, nil) + local parser = Parser.resolve_parser(new_filetype) + buf_state.set(ctx.bufnr, buf_state.IKEY.PARSER, parser) +end + +return M diff --git a/lua/tirenvi/app/init.lua b/lua/tirenvi/app/init.lua new file mode 100644 index 00000000..2b7c173b --- /dev/null +++ b/lua/tirenvi/app/init.lua @@ -0,0 +1,115 @@ +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= +local M = {} + +-- ============================================================================= +-- Public API + +---@param ctx Context +function M.read_post(ctx) + require("tirenvi.app.parse").read_post(ctx) +end + +---@param ctx Context +function M.write_pre(ctx) + require("tirenvi.app.parse").write_pre(ctx) +end + +---@param ctx Context +function M.write_post(ctx) + require("tirenvi.app.parse").write_post(ctx) +end + +---@param ctx Context +---@param filename string +function M.debug_read_tir(ctx, filename) + require("tirenvi.app.parse").debug_read_tir(ctx, filename) +end + +---@param ctx Context +---@param filename string +function M.debug_write_tir(ctx, filename) + require("tirenvi.app.parse").debug_write_tir(ctx, filename) +end + +---@param ctx Context +function M.from_flat(ctx) + require("tirenvi.app.parse").from_flat(ctx) +end + +---@param ctx Context +---@param is_write_pre boolean|nil +---@return ReadResult +function M.to_flat(ctx, is_write_pre) + return require("tirenvi.app.parse").to_flat(ctx, is_write_pre) +end + +---@param ctx Context +function M.toggle(ctx) + return require("tirenvi.app.parse").toggle(ctx) +end + +---@param ctx Context +---@param width_op WidthOp +function M.cmd_width(ctx, width_op) + require("tirenvi.app.width").cmd_width(ctx, width_op) +end + +---@param ctx Context +---@param width_op WidthOp +function M.cmd_fit(ctx, width_op) + require("tirenvi.app.width").cmd_fit(ctx, width_op) +end + +---@param ctx Context +---@param width_op WidthOp +function M.cmd_wrap(ctx, width_op) + require("tirenvi.app.width").cmd_wrap(ctx, width_op) + M.cmd_redraw(ctx) +end + +---@param ctx Context +---@param opts DocToBufLinesOpts|nil +function M.cmd_redraw(ctx, opts) + require("tirenvi.app.redraw").cmd_redraw(ctx, opts) +end + +---@param ctx Context +---@param range3 Range3|nil +function M.check_and_repair(ctx, range3) + require("tirenvi.app.redraw").check_and_repair(ctx, range3) +end + +---@param ctx Context +---@param range3 Range3 +function M.on_lines(ctx, range3) + require("tirenvi.app.on_lines").on_lines(ctx, range3) +end + +---@param ctx Context +function M.auto_wrap(ctx) + require("tirenvi.app.auto_wrap").auto_wrap(ctx) +end + +---@param ctx Context +function M.on_filetype(ctx) + require("tirenvi.app.filetype").on_filetype(ctx) +end + +---@param ctx Context +function M.insert_char_in_newline(ctx) + require("tirenvi.app.insert").insert_char_in_newline(ctx) +end + +---@return string +function M.keymap_lf() + return require("tirenvi.app.insert").keymap_lf() +end + +---@return string +function M.keymap_tab() + return require("tirenvi.app.insert").keymap_tab() +end + +return M diff --git a/lua/tirenvi/app/insert.lua b/lua/tirenvi/app/insert.lua new file mode 100644 index 00000000..c6463fa0 --- /dev/null +++ b/lua/tirenvi/app/insert.lua @@ -0,0 +1,69 @@ +local fn = vim.fn -- Neovim +local bo = vim.bo + +local config = require("tirenvi.config") -- Root + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser + +local buf_lines = require("tirenvi.io.buf_lines") -- IO +local buf_state = require("tirenvi.io.buf_state") -- IO +local reader = require("tirenvi.io.reader") + +local util = require("tirenvi.util.util") -- Util +local Range = require("tirenvi.util.range") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +-- Public API + +---@param ctx Context +function M.insert_char_in_newline(ctx) + local cursor_buf = reader.cursor_buf(ctx) + local row_cur = cursor_buf.row_cur + local line_new = buf_lines.get_line(ctx.bufnr, row_cur) + if line_new ~= "" then + return + end + local line_prev, line_next = + buf_lines.get_lines_around(ctx.bufnr, Range.from_lua(row_cur, row_cur)) + local line_ref = line_prev + if not buf_state.is_allow_plain(ctx.bufnr) then + line_ref = line_ref or line_next + end + local pipe = tir_buf.get_pipe_char(line_ref) + if not pipe then + return + end + vim.v.char = pipe .. vim.v.char +end + +---@return string +function M.keymap_lf() + local col = fn.col(".") + local line = fn.getline(".") + if not tir_buf.get_pipe_char(line) then + return util.get_termcodes("") + end + if col == 1 or col > #line then + return util.get_termcodes("") + end + return config.marks.lf +end + +---@return string +function M.keymap_tab() + local line = fn.getline(".") + if not tir_buf.get_pipe_char(line) then + return util.get_termcodes("") + end + if bo.expandtab then + return util.get_termcodes("") + end + return config.marks.tab +end + +return M diff --git a/lua/tirenvi/app/on_lines.lua b/lua/tirenvi/app/on_lines.lua new file mode 100644 index 00000000..2969a733 --- /dev/null +++ b/lua/tirenvi/app/on_lines.lua @@ -0,0 +1,77 @@ +local dirty_range = require("tirenvi.parser.dirty_range") -- Parser +local buf_parser = require("tirenvi.parser.buf_parser") + +local LineProvider = require("tirenvi.io.buf_line_provider") -- IO +local attr_store = require("tirenvi.io.attr_store") +local reader = require("tirenvi.io.reader") +local dirty = require("tirenvi.io.dirty") + +local Document = require("tirenvi.core.document") -- Core +local Attrs = require("tirenvi.core.attrs") + +local Range3 = require("tirenvi.util.range3") -- Util +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param r_result ReadResult +---@param bufdoc Document +---@param range3 Range3 +---@return Attr[] +local function reconcile_attrs(r_result, bufdoc, range3) + Document.inherit_neighbor_attr(bufdoc, r_result.attrs, range3) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[2]NEIGHBOR:")) + Document.infer_consistent_attr(bufdoc) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[3]CONSISTENT:")) + Document.apply_attrs(bufdoc, r_result.attrs) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[4]CACHED:")) + Document.set_max_attr(bufdoc) + local attrs = Document.replace_attrs(bufdoc, r_result.range, r_result.attrs) + log.watch("ATTR", Attrs.debug_attrs(attrs, "[6]RESULT:")) + return attrs +end + +---@param bufnr number +---@param attrs Attr[] +---@param range3 Range3 +local function reconcile_dirty_ranges(bufnr, attrs, range3) + local prev_ranges = dirty.get_ranges(bufnr) + local line_provider = LineProvider.new(bufnr) + local inv_ranges = + dirty_range.reconcile(line_provider, prev_ranges, attrs, range3) + log.watch("INVD", inv_ranges) + dirty.set_ranges(bufnr, inv_ranges) +end + +---@param ctx Context +---@param range3 Range3 +---@param r_result ReadResult +local function update_attrs(ctx, range3, r_result) + r_result.attrs = Attrs.adjust(r_result.attrs, range3) + log.watch("ATTR", Attrs.debug_attrs(r_result.attrs, "[0]UPDATE CHACHED:")) + local opts = { range3 = range3, first = r_result.range.first } + local bufdoc = buf_parser.parse(ctx, r_result, opts) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[1]DOC ATTR:")) + local attrs = reconcile_attrs(r_result, bufdoc, range3) + reconcile_dirty_ranges(ctx.bufnr, attrs, range3) + attr_store.write(ctx.bufnr, attrs) +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@param range3 Range3 +function M.on_lines(ctx, range3) + local r_result = + reader.read(ctx, Range3.get_new_range(range3), { cursor = false }) + update_attrs(ctx, range3, r_result) +end + +return M diff --git a/lua/tirenvi/app/parse.lua b/lua/tirenvi/app/parse.lua new file mode 100644 index 00000000..7097860b --- /dev/null +++ b/lua/tirenvi/app/parse.lua @@ -0,0 +1,142 @@ +local fn = vim.fn -- Neovim + +local common = require("tirenvi.app.common") -- App + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser +local flat_parser = require("tirenvi.parser.flat_parser") +local Parser = require("tirenvi.parser.parser") + +local buf_state = require("tirenvi.io.buf_state") +local writer = require("tirenvi.io.writer") +local reader = require("tirenvi.io.reader") +local Request = require("tirenvi.io.request") +local ReadResult = require("tirenvi.io.read_result") + +local Document = require("tirenvi.core.document") -- Core + +local util = require("tirenvi.util.util") -- Util +local Range = require("tirenvi.util.range") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param ctx Context +---@param r_result ReadResult +---@return Document +local function fltlines_to_tirdoc(ctx, r_result) + local parser = buf_state.get(ctx.bufnr, buf_state.IKEY.PARSER) + local tirdoc = flat_parser.parse(parser, r_result) + log.watch("ATTR", Document.debug_attrs(tirdoc, "[1]DOC ATTR:")) + Document.apply_attrs(tirdoc, r_result.attrs) + log.watch("ATTR", Document.debug_attrs(tirdoc, "[4]CACHED:")) + return tirdoc +end + +local function embedded_on(ctx) + local parser = buf_state.get(ctx.bufnr, buf_state.IKEY.PARSER) + if parser then + return + end + parser = Parser.resolve_parser("*") + buf_state.set(ctx.bufnr, buf_state.IKEY.PARSER, parser) +end + +---@param ctx Context +local function embedded_off(ctx) + buf_state.set(ctx.bufnr, buf_state.IKEY.PARSER, nil) +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@return nil +function M.read_post(ctx) + local r_result = reader.read(ctx, Range.WHOLE) + if not tir_buf.has_pipe(r_result.lines) then + util.ensure_no_reserved_marks(r_result.lines) + local tirdoc = fltlines_to_tirdoc(ctx, r_result) + common.doc_to_buflines(ctx, r_result, tirdoc, { no_undo = true }) + else + local bufdoc = common.buflines_to_bufdoc_text_driven(ctx, r_result) + common.doc_to_buflines(ctx, r_result, bufdoc, { no_undo = true }) + end +end + +local backup_buffer +local backup_cursor +---@param ctx Context +function M.write_pre(ctx) + local r_result = M.to_flat(ctx, true) + backup_buffer = r_result.lines + backup_cursor = r_result.cursor_buf + backup_cursor.restore_mode = "buffer" +end + +---@param ctx Context +function M.write_post(ctx) + if backup_buffer then + local r_result = reader.read(ctx, Range.WHOLE) + r_result.cursor_buf = backup_cursor + local req = Request.new_writer(r_result, backup_buffer, true) + writer.write(ctx, req) + backup_buffer = nil + backup_cursor = nil + end +end + +---@param ctx Context +---@param filename string +function M.debug_read_tir(ctx, filename) + local r_result = ReadResult.new_reader(Range.WHOLE) + local jslines = fn.readfile(filename) + local tirdoc = flat_parser.from_jslines(ctx, jslines) + common.doc_to_buflines(ctx, r_result, tirdoc) +end + +---@param ctx Context +---@param filename string +function M.debug_write_tir(ctx, filename) + local r_result = reader.read(ctx, Range.WHOLE) + local bufdoc = common.buflines_to_bufdoc_text_driven(ctx, r_result) + local tirdoc = Document.from_bufdoc(bufdoc) + local jslines = flat_parser.to_jslines(tirdoc) + fn.writefile(jslines, filename) +end + +---@param ctx Context +function M.from_flat(ctx) + local r_result = reader.read(ctx, Range.WHOLE) + util.ensure_no_reserved_marks(r_result.lines) + local tirdoc = fltlines_to_tirdoc(ctx, r_result) + common.doc_to_buflines(ctx, r_result, tirdoc) +end + +---@param ctx Context +---@param is_write_pre boolean|nil +---@return ReadResult +function M.to_flat(ctx, is_write_pre) + return common.to_flat(ctx, is_write_pre) +end + +---@param ctx Context +function M.toggle(ctx) + embedded_on(ctx) + local is_flat = not buf_state.is_tirbuf(ctx.bufnr) + if is_flat then + M.from_flat(ctx) + if not buf_state.has_grid(ctx.bufnr) then + embedded_off(ctx) + end + elseif buf_state.has_grid(ctx.bufnr) then + M.to_flat(ctx) + end +end + +return M diff --git a/lua/tirenvi/app/pipeline.lua b/lua/tirenvi/app/pipeline.lua deleted file mode 100644 index 9df1b540..00000000 --- a/lua/tirenvi/app/pipeline.lua +++ /dev/null @@ -1,481 +0,0 @@ ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ -local ReadResult = require("tirenvi.app.read_result") -local Document = require("tirenvi.core.document") -local Attrs = require("tirenvi.core.attrs") -local Attr = require("tirenvi.core.attr") -local Cell = require("tirenvi.core.cell") -local Bufline = require("tirenvi.core.bufline") -local dirty_range = require("tirenvi.core.dirty_range") -local Request = require("tirenvi.app.request") -local width_layout = require("tirenvi.width.layout") -local flat_parser = require("tirenvi.parser.flat_parser") -local buf_parser = require("tirenvi.parser.buf_parser") -local LinProvider = require("tirenvi.io.buffer_line_provider") -local buffer = require("tirenvi.io.buffer") -local buf_state = require("tirenvi.io.buf_state") -local writer = require("tirenvi.io.writer") -local attr_store = require("tirenvi.io.attr_store") -local reader = require("tirenvi.io.reader") -local dirty = require("tirenvi.io.dirty") -local util = require("tirenvi.util.util") -local Range = require("tirenvi.util.range") -local Range3 = require("tirenvi.util.range3") -local log = require("tirenvi.util.log") - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ - -local M = {} - -local api = vim.api - --- private helpers - ----@param ctx Context ----@param r_result ReadResult ----@return Document -local function fltlines_to_tirdoc(ctx, r_result) - local tirdoc = flat_parser.parse(ctx, r_result) - log.watch("ATTR", Document.debug_attrs(tirdoc, "[1]DOC ATTR:")) - Document.apply_attrs(tirdoc, r_result.attrs) - log.watch("ATTR", Document.debug_attrs(tirdoc, "[4]CACHED:")) - return tirdoc -end - ----@param ctx Context ----@param r_result ReadResult ----@return Document -local function buflines_to_bufdoc_text_driven(ctx, r_result) - local opts = { first = r_result.range.first } - local bufdoc = buf_parser.parse(ctx, r_result, opts) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[1]DOC ATTR:")) - Document.apply_attrs(bufdoc, r_result.attrs) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[4]CACHED:")) - return bufdoc -end - ----@param ctx Context ----@param r_result ReadResult --- Prevents line count changes that would break put(); used for repair. ----@return Document -local function buflines_to_bufdoc_attrs_driven(ctx, r_result) - log.watch("ATTR", Attrs.debug_attrs(r_result.attrs, "CHACHED ATTRS:")) - local opts = { attrs = r_result.attrs } - local bufdoc = buf_parser.parse(ctx, r_result, opts) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[1]DOC ATTR:")) - Document.insert_empty_lines(bufdoc) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[7]INSERT EMPTY:")) - return bufdoc -end - ----@param ctx Context ----@param tirdoc Document -local function apply_wrap_mode(ctx, tirdoc) - width_layout.compute(ctx.winid, tirdoc) -end - ----@class DocToBufLinesOpts ----@field no_undo? boolean ----@field no_normalize? boolean - ----@param ctx Context ----@param r_result ReadResult ----@param doc Document ----@param opts DocToBufLinesOpts|nil -local function doc_to_buflines(ctx, r_result, doc, opts) - local no_undo = opts and opts.no_undo or false - local no_normalize = opts and opts.no_normalize or false - local tirdoc = doc - if not doc._tir then - tirdoc = Document.from_bufdoc(doc, no_normalize) - end - apply_wrap_mode(ctx, tirdoc) - local bufdoc = Document.to_bufdoc(tirdoc) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[9]DOC ATTR:")) - local attrs = Document.replace_attrs(bufdoc, r_result.range, r_result.attrs) - log.watch("ATTR", Attrs.debug_attrs(attrs, "[9]CHACHED:")) - buf_state.set_buffer_flat(ctx.bufnr, false) - attr_store.write(ctx, attrs) - local buf_lines = buf_parser.unparse(bufdoc) - if not util.same_str_array(buf_lines, r_result.lines or "") then - local req_w = Request.new_writer(r_result, buf_lines, no_undo) - writer.write(ctx, req_w) - end -end - ----@param ctx Context ----@param tirdoc Document ----@param is_write_pre boolean|nil -local function tirdoc_to_flat(ctx, r_result, tirdoc, is_write_pre) - local fltlines = flat_parser.unparse(ctx, tirdoc) - local req_w = Request.new_writer(r_result, fltlines, is_write_pre) - local attrs = vim.deepcopy(r_result.attrs) - if not is_write_pre then - Attrs.remove_range(attrs) - log.watch("ATTR", Attrs.debug_attrs(attrs, "[9]CHACHED:")) - buf_state.set_buffer_flat(ctx.bufnr, true) - attr_store.write(ctx, attrs) - end - writer.write(ctx, req_w) -end - ----@param ctx Context ----@param irow integer -local function expand_rect(ctx, irow) - local line_provider = LinProvider.new(ctx.bufnr) - local top = Bufline.get_block_top_nrow(ctx, line_provider, irow) - local bottom = Bufline.get_block_bottom_nrow(ctx, line_provider, irow) - return Range.from_lua(top, bottom) -end - ----@param r_result ReadResult ----@param bufdoc Document ----@param range3 Range3 ----@return Attr[] -local function reconcile_attrs(r_result, bufdoc, range3) - Document.inherit_neighbor_attr(bufdoc, r_result.attrs, range3) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[2]NEIGHBOR:")) - Document.infer_consistent_attr(bufdoc) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[3]CONSISTENT:")) - Document.apply_attrs(bufdoc, r_result.attrs) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[4]CACHED:")) - Document.set_max_attr(bufdoc) - local attrs = Document.replace_attrs(bufdoc, r_result.range, r_result.attrs) - log.watch("ATTR", Attrs.debug_attrs(attrs, "[6]RESULT:")) - return attrs -end - ----@param bufnr number ----@param attrs Attr[] ----@param range3 Range3 -local function reconcile_dirty_ranges(bufnr, attrs, range3) - local prev_ranges = dirty.get_ranges(bufnr) - local line_provider = LinProvider.new(bufnr) - local inv_ranges = dirty_range.reconcile(line_provider, prev_ranges, attrs, range3) - log.watch("INVD", inv_ranges) - dirty.set_ranges(bufnr, inv_ranges) -end - -local schedule_repair_flag = false ----@param ctx Context ----@param range3 Range3|nil -local function schedule_repair(ctx, range3) - if not schedule_repair_flag then - vim.schedule(function() - schedule_repair_flag = false - local no_normalize = range3 and Range3.get_delta(range3) == 0 or false - M.cmd_repair(ctx, { no_undo = true, no_normalize = no_normalize }) - end) - schedule_repair_flag = true - else - local dirty_ranges = dirty.get_ranges(ctx.bufnr) - log.watch("UNDO", ctx.bufnr, { "multi time on_lines", dirty_ranges }) - end -end - ----@param ctx Context ----@param range3 Range3|nil -local function repair_request(ctx, range3) - if buf_state.is_repair(ctx, range3) then - schedule_repair(ctx, range3) - end -end - ----@param ctx Context ----@return boolean -local function need_repair(ctx) - if buf_state.is_flat(ctx.bufnr) then - return false - end - if buf_state.has_grid(ctx) == false then - return false - end - -- repair must remove redundant padding, - -- so it does not check whether dirty exists. - return true -end - ----@param ctx Context ----@param range3 Range3 ----@param r_result ReadResult -local function update_attrs(ctx, range3, r_result) - r_result.attrs = Attrs.adjust(r_result.attrs, range3) - log.watch("ATTR", Attrs.debug_attrs(r_result.attrs, "[0]UPDATE CHACHED:")) - local opts = { range3 = range3, first = r_result.range.first } - local bufdoc = buf_parser.parse(ctx, r_result, opts) - log.watch("ATTR", Document.debug_attrs(bufdoc, "[1]DOC ATTR:")) - local attrs = reconcile_attrs(r_result, bufdoc, range3) - reconcile_dirty_ranges(ctx.bufnr, attrs, range3) - attr_store.write(ctx, attrs) -end - ----@param ctx Context ----@param width_op WidthOp -local function change_wrap_width(ctx, width_op) - local row_range = expand_rect(ctx, width_op.row_cur) - local r_result = reader.read(ctx, row_range) - local bufdoc = buflines_to_bufdoc_text_driven(ctx, r_result) - log.assert(#bufdoc.blocks == 1, "only one block") - local attr = bufdoc.blocks[1].attr - if Attr.is_plain(attr) then return end - local column = Attr.get(attr, width_op.col_disp) - column.width = width_op:apply(column.width) - attr.fit_span = Attr.get_fit_span(attr) - attr.wrap_mode = "wrap_width" - doc_to_buflines(ctx, r_result, bufdoc) -end - ----@param ctx Context ----@param width_op WidthOp -local function change_wrap_fit(ctx, width_op) - local row_range = expand_rect(ctx, width_op.row_cur) - local r_result = reader.read(ctx, row_range) - local bufdoc = buflines_to_bufdoc_text_driven(ctx, r_result) - log.assert(#bufdoc.blocks == 1, "only one block") - local attr = bufdoc.blocks[1].attr - if Attr.is_plain(attr) then return end - attr.fit_span = width_op:apply(Attr.get_fit_span(attr)) - attr.wrap_mode = "wrap_fit" - doc_to_buflines(ctx, r_result, bufdoc) -end - ----@param ctx Context ----@param width_op WidthOp -local function change_wrap_auto(ctx, width_op) - local row_range = expand_rect(ctx, width_op.row_cur) - local r_result = reader.read(ctx, row_range) - local bufdoc = buflines_to_bufdoc_text_driven(ctx, r_result) - log.assert(#bufdoc.blocks == 1, "only one block") - local attr = bufdoc.blocks[1].attr - if Attr.is_plain(attr) then return end - attr.fit_span = 0 - attr.wrap_mode = "wrap_auto" - doc_to_buflines(ctx, r_result, bufdoc) -end - -local MAX_HEAD = 5 ----@param bufnr number ----@param attr Attr ----@param icol integer ----@return string -local function get_head(bufnr, attr, icol) - local irow = attr.range.first - local line = buffer.get_line(bufnr, irow) or "" - local cells = Bufline.get_cells(line) - local head = Cell.remove_padding(cells[icol] or "") - local head_chars = util.utf8_chars(head) - if #head_chars > MAX_HEAD then - head = table.concat(vim.list_slice(head_chars, 1, MAX_HEAD)) .. ".." - end - return head -end - -local DELTA = 2 ----@param attr Attr ----@param logical CursorLogical ----@return string -local function get_col_info(attr, logical) - local widths = Attr.get_width_array(attr.columns) - ---@cast widths string[] - widths[logical.icol] = widths[logical.icol] .. "*" - local first = math.max(1, logical.icol - DELTA) - local last = math.min(#widths, logical.icol + DELTA) - local info = table.concat(widths, ",", first, last) - if first ~= 1 then - info = "..," .. info - end - if last ~= #widths then - info = info .. ",.." - end - return "[" .. info .. "]" -end - ----@param bufnr number ----@param attr Attr ----@param logical CursorLogical ----@return string -local function get_width_info(bufnr, attr, logical) - local mode = Attr.get_wrap_kind(attr) - local span = Attr.get_fit_span(attr) - local head = get_head(bufnr, attr, logical.icol) - local col_info = get_col_info(attr, logical) - return string.format("mode=%s span=%d col=%d/%d header=%q widths=%s", - mode, span, logical.icol, #attr.columns, head, col_info - ) -end - ----@param ctx Context ----@param width_op WidthOp -local function width_info(ctx, width_op) - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) - local logical = Attrs.to_logical(attrs, width_op.row_cur, width_op.col_disp) - local attr = attrs[logical.iblock] - if Attr.is_plain(attr) then - print("kind=plain") - else - print(get_width_info(ctx.bufnr, attr, logical)) - end -end - ----@param ctx Context ----@param width_op WidthOp -local function toggle_wrap_mode(ctx, width_op) - local attrs = attr_store.read(ctx.bufnr) - local attr = Attrs.get(attrs, width_op.row_cur) - Attr.toggle_wrap_mode(attr or {}) - attr_store.write(ctx, attrs) -end - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param ctx Context ----@return nil -function M.read_post(ctx) - local r_result = reader.read(ctx, Range.WHOLE) - if not Bufline.has_pipe(r_result.lines) then - util.ensure_no_reserved_marks(r_result.lines) - local tirdoc = fltlines_to_tirdoc(ctx, r_result) - doc_to_buflines(ctx, r_result, tirdoc, { no_undo = true }) - else - local bufdoc = buflines_to_bufdoc_text_driven(ctx, r_result) - doc_to_buflines(ctx, r_result, bufdoc, { no_undo = true }) - end -end - -local backup_buffer -local backup_cursor ----@param ctx Context -function M.write_pre(ctx) - local r_result = M.to_flat(ctx, true) - backup_buffer = r_result.lines - backup_cursor = r_result.cursor - backup_cursor.restore_mode = "buffer" -end - ----@param ctx Context -function M.write_post(ctx) - if backup_buffer then - local r_result = reader.read(ctx, Range.WHOLE) - r_result.cursor = backup_cursor - local req = Request.new_writer(r_result, backup_buffer, true) - writer.write(ctx, req) - backup_buffer = nil - backup_cursor = nil - end -end - ----@param ctx Context ----@param filename string -function M.debug_read_tir(ctx, filename) - local r_result = ReadResult.new_reader(Range.WHOLE) - local jslines = vim.fn.readfile(filename) - local tirdoc = flat_parser.from_jslines(ctx, jslines) - doc_to_buflines(ctx, r_result, tirdoc) -end - ----@param ctx Context ----@param filename string -function M.debug_write_tir(ctx, filename) - local r_result = reader.read(ctx, Range.WHOLE) - local bufdoc = buflines_to_bufdoc_text_driven(ctx, r_result) - local tirdoc = Document.from_bufdoc(bufdoc) - local jslines = flat_parser.to_jslines(tirdoc) - vim.fn.writefile(jslines, filename) -end - ----@param ctx Context ----@return nil -function M.from_flat(ctx) - local r_result = reader.read(ctx, Range.WHOLE) - util.ensure_no_reserved_marks(r_result.lines) - local tirdoc = fltlines_to_tirdoc(ctx, r_result) - doc_to_buflines(ctx, r_result, tirdoc) -end - ----@param ctx Context ----@param is_write_pre boolean|nil ----@return ReadResult -function M.to_flat(ctx, is_write_pre) - local r_result = reader.read(ctx, Range.WHOLE) - local bufdoc = buflines_to_bufdoc_text_driven(ctx, r_result) - local tirdoc = Document.from_bufdoc(bufdoc) - tirdoc_to_flat(ctx, r_result, tirdoc, is_write_pre) - return r_result -end - ----@param ctx Context ----@param width_op WidthOp -function M.cmd_width(ctx, width_op) - if width_op.operation == "info" then - width_info(ctx, width_op) - else - change_wrap_width(ctx, width_op) - end -end - ----@param ctx Context ----@param width_op WidthOp -function M.cmd_fit(ctx, width_op) - if width_op.operation == "auto" then - change_wrap_auto(ctx, width_op) - else - change_wrap_fit(ctx, width_op) - end -end - ----@param ctx Context ----@param width_op WidthOp -function M.cmd_wrap(ctx, width_op) - toggle_wrap_mode(ctx, width_op) - M.cmd_repair(ctx) -end - ----@param ctx Context ----@param opts DocToBufLinesOpts|nil -function M.cmd_repair(ctx, opts) - if not need_repair(ctx) then - return - end - log.debug("===+=== START ===+=== %s[#%d] ===", "REPAIR", ctx.bufnr) - local r_result = reader.read(ctx, Range.WHOLE) - log.watch("ATTR", Attrs.debug_attrs(r_result.attrs, "[88]MODE:")) - local bufdoc = buflines_to_bufdoc_attrs_driven(ctx, r_result) - doc_to_buflines(ctx, r_result, bufdoc, opts) -end - ----@param ctx Context ----@param range3 Range3 -function M.on_lines(ctx, range3) - local r_result = reader.read(ctx, Range3.get_new_range(range3), { cursor = false }) - update_attrs(ctx, range3, r_result) -end - ----@param ctx Context ----@param range3 Range3|nil -function M.check_and_repair(ctx, range3) - local bufnr = ctx.bufnr - vim.schedule(function() - if not api.nvim_buf_is_valid(bufnr) then - return - end - if api.nvim_get_current_buf() ~= bufnr then - return - end - local ok, err = xpcall( - function() - repair_request(ctx, range3) - end, - debug.traceback - ) - if not ok then - error(err) - end - end) -end - -return M diff --git a/lua/tirenvi/app/read_result.lua b/lua/tirenvi/app/read_result.lua deleted file mode 100644 index b7a8f8fc..00000000 --- a/lua/tirenvi/app/read_result.lua +++ /dev/null @@ -1,42 +0,0 @@ ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ - -local Range = require("tirenvi.util.range") -local Range3 = require("tirenvi.util.range3") -local log = require("tirenvi.util.log") - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ - -local M = {} - ----@class ReadResult ----@field range Range ----@field lines string[] ----@field attrs Attr[] ----@field cursor CursorBuf - --- private helpers - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param range Range ----@return ReadResult -function M.new_reader(range) - return { - range = range, - } -end - ----@param self ReadResult ----@return integer -- 1-based ----@return integer -- 1-based -function M:lua_range() - return Range.to_lua(self.range) -end - -return M diff --git a/lua/tirenvi/app/redraw.lua b/lua/tirenvi/app/redraw.lua new file mode 100644 index 00000000..9be61606 --- /dev/null +++ b/lua/tirenvi/app/redraw.lua @@ -0,0 +1,116 @@ +local api = vim.api -- Neovim + +local common = require("tirenvi.app.common") -- App + +local buf_parser = require("tirenvi.parser.buf_parser") -- Parse + +local buf_state = require("tirenvi.io.buf_state") -- IO +local reader = require("tirenvi.io.reader") +local dirty = require("tirenvi.io.dirty") + +local Document = require("tirenvi.core.document") -- Core +local Attrs = require("tirenvi.core.attrs") + +local Range = require("tirenvi.util.range") -- Util +local Range3 = require("tirenvi.util.range3") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param ctx Context +---@param r_result ReadResult +-- Prevents line count changes that would break put(); used for repair. +---@return Document +local function buflines_to_bufdoc_attrs_driven(ctx, r_result) + log.watch("ATTR", Attrs.debug_attrs(r_result.attrs, "CHACHED ATTRS:")) + local opts = { attrs = r_result.attrs } + local bufdoc = buf_parser.parse(ctx, r_result, opts) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[1]DOC ATTR:")) + Document.insert_empty_lines(bufdoc) + log.watch("ATTR", Document.debug_attrs(bufdoc, "[7]INSERT EMPTY:")) + return bufdoc +end + +local schedule_repair_flag = false +---@param ctx Context +---@param range3 Range3|nil +local function schedule_repair(ctx, range3) + if not schedule_repair_flag then + vim.schedule(function() + schedule_repair_flag = false + local no_normalize = range3 and Range3.get_delta(range3) == 0 + or false + M.cmd_redraw(ctx, { no_undo = true, no_normalize = no_normalize }) + end) + schedule_repair_flag = true + else + local dirty_ranges = dirty.get_ranges(ctx.bufnr) + log.watch("UNDO", ctx.bufnr, { "multi time on_lines", dirty_ranges }) + end +end + +---@param ctx Context +---@param range3 Range3|nil +local function repair_request(ctx, range3) + if buf_state.is_repair(ctx.bufnr, range3) then + schedule_repair(ctx, range3) + end +end + +---@param ctx Context +---@return boolean +local function need_repair(ctx) + if not buf_state.is_tirbuf(ctx.bufnr) then + return false + end + if not buf_state.has_grid(ctx.bufnr) then + return false + end + -- repair must remove redundant padding, + -- so it does not check whether dirty exists. + return true +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@param opts DocToBufLinesOpts|nil +function M.cmd_redraw(ctx, opts) + if not need_repair(ctx) then + return + end + log.debug("===+=== START ===+=== %s[#%d] ===", "REPAIR", ctx.bufnr) + local r_result = reader.read(ctx, Range.WHOLE) + log.watch("ATTR", Attrs.debug_attrs(r_result.attrs, "[88]MODE:")) + local bufdoc = buflines_to_bufdoc_attrs_driven(ctx, r_result) + common.doc_to_buflines(ctx, r_result, bufdoc, opts) +end + +---@param ctx Context +---@param range3 Range3|nil +function M.check_and_repair(ctx, range3) + local bufnr = ctx.bufnr + vim.schedule(function() + if not api.nvim_buf_is_valid(bufnr) then + return + end + if api.nvim_get_current_buf() ~= bufnr then + return + end + local ok, err = xpcall(function() + repair_request(ctx, range3) + end, debug.traceback) + if not ok then + error(err) + end + end) +end + +return M diff --git a/lua/tirenvi/app/request.lua b/lua/tirenvi/app/request.lua deleted file mode 100644 index e17a00d4..00000000 --- a/lua/tirenvi/app/request.lua +++ /dev/null @@ -1,54 +0,0 @@ ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ - -local Range = require("tirenvi.util.range") -local Range3 = require("tirenvi.util.range3") -local log = require("tirenvi.util.log") - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ - -local M = {} - ----@class Request ----@field range Range ----@field lines string[] ----@field no_undo boolean ----@field cursor CursorBuf - --- private helpers - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param r_req ReadResult ----@param lines string[] ----@param no_undo boolean|nil ----@return Request -function M.new_writer(r_req, lines, no_undo) - ---@type Request - return { - range = r_req.range, - lines = lines, - no_undo = no_undo or false, - cursor = r_req.cursor - } -end - ----@param self Request ----@return Range3 -function M:get_range3() - local first, last = Range.to_lua(self.range) - return Range3.new(first, last, first + #self.lines - 1) -end - ----@param self Request ----@return boolean -function M:is_no_undo() - return self.no_undo == true -end - -return M diff --git a/lua/tirenvi/app/width.lua b/lua/tirenvi/app/width.lua new file mode 100644 index 00000000..c642a2d5 --- /dev/null +++ b/lua/tirenvi/app/width.lua @@ -0,0 +1,216 @@ +local fn = vim.fn -- Neovim + +local common = require("tirenvi.app.common") -- App + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser +local Cursor = require("tirenvi.parser.cursor") + +local LinProvider = require("tirenvi.io.buf_line_provider") -- IO +local buf_lines = require("tirenvi.io.buf_lines") +local buf_state = require("tirenvi.io.buf_state") +local attr_store = require("tirenvi.io.attr_store") +local reader = require("tirenvi.io.reader") + +local Attrs = require("tirenvi.core.attrs") -- Core +local Attr = require("tirenvi.core.attr") +local Cell = require("tirenvi.core.cell") + +local util = require("tirenvi.util.util") -- Util +local Range = require("tirenvi.util.range") +local notify = require("tirenvi.util.notify") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@class DocToBufLinesOpts +---@field no_undo? boolean +---@field no_normalize? boolean + +---@param ctx Context +---@param irow integer +local function expand_rect(ctx, irow) + local line_provider = LinProvider.new(ctx.bufnr) + local top = tir_buf.get_block_top_nrow(ctx, line_provider, irow) + local bottom = tir_buf.get_block_bottom_nrow(ctx, line_provider, irow) + return Range.from_lua(top, bottom) +end + +---@param ctx Context +---@param width_op WidthOp +local function change_wrap_width(ctx, width_op) + local row_range = expand_rect(ctx, width_op.row_cur) + local r_result = reader.read(ctx, row_range) + local bufdoc = common.buflines_to_bufdoc_text_driven(ctx, r_result) + log.assert(#bufdoc.blocks == 1, "only one block") + local attr = bufdoc.blocks[1].attr + if Attr.is_plain(attr) then + return + end + local column = Attr.get(attr, width_op.col_disp) + column.width = width_op:apply(column.width) + attr.fit_span = Attr.get_fit_span(attr) + attr.wrap_mode = "wrap_width" + common.doc_to_buflines(ctx, r_result, bufdoc) +end + +---@param ctx Context +---@param width_op WidthOp +local function change_wrap_fit(ctx, width_op) + local row_range = expand_rect(ctx, width_op.row_cur) + local r_result = reader.read(ctx, row_range) + local bufdoc = common.buflines_to_bufdoc_text_driven(ctx, r_result) + log.assert(#bufdoc.blocks == 1, "only one block") + local attr = bufdoc.blocks[1].attr + if Attr.is_plain(attr) then + return + end + attr.fit_span = width_op:apply(Attr.get_fit_span(attr)) + attr.wrap_mode = "wrap_fit" + common.doc_to_buflines(ctx, r_result, bufdoc) +end + +---@param ctx Context +---@param width_op WidthOp +local function change_wrap_auto(ctx, width_op) + local row_range = expand_rect(ctx, width_op.row_cur) + local r_result = reader.read(ctx, row_range) + local bufdoc = common.buflines_to_bufdoc_text_driven(ctx, r_result) + log.assert(#bufdoc.blocks == 1, "only one block") + local attr = bufdoc.blocks[1].attr + if Attr.is_plain(attr) then + return + end + attr.fit_span = 0 + attr.wrap_mode = "wrap_auto" + common.doc_to_buflines(ctx, r_result, bufdoc) +end + +local MAX_HEAD = 5 +---@param bufnr number +---@param attr Attr +---@param icol integer +---@return string +local function get_head(bufnr, attr, icol) + local irow = attr.range.first + local line = buf_lines.get_line(bufnr, irow) or "" + local cells = tir_buf.get_cells(line) + local head = Cell.remove_padding(cells[icol] or "") + local head_chars = util.utf8_chars(head) + if #head_chars > MAX_HEAD then + head = table.concat(vim.list_slice(head_chars, 1, MAX_HEAD)) .. ".." + end + return head +end + +local DELTA = 2 +---@param attr Attr +---@param cursor_tir CursorTir +---@return string +local function get_col_info(attr, cursor_tir) + local widths = Attr.get_width_array(attr.columns) + ---@cast widths string[] + widths[cursor_tir.icol] = widths[cursor_tir.icol] .. "*" + local first = math.max(1, cursor_tir.icol - DELTA) + local last = math.min(#widths, cursor_tir.icol + DELTA) + local info = table.concat(widths, ",", first, last) + if first ~= 1 then + info = "..," .. info + end + if last ~= #widths then + info = info .. ",.." + end + return "[" .. info .. "]" +end + +---@param bufnr number +---@param attr Attr +---@param cursor_tir CursorTir +---@return string +local function get_width_info(bufnr, attr, cursor_tir) + local mode = Attr.get_wrap_kind(attr) + local span = Attr.get_fit_span(attr) + local head = get_head(bufnr, attr, cursor_tir.icol) + local col_info = get_col_info(attr, cursor_tir) + return string.format( + "mode=%s span=%d col=%d/%d header=%q widths=%s", + mode, + span, + cursor_tir.icol, + #attr.columns, + head, + col_info + ) +end + +---@param ctx Context +---@param width_op WidthOp +local function width_info(ctx, width_op) + local attrs = buf_state.get(ctx.bufnr, buf_state.IKEY.ATTRS) + local curosr_tir = Cursor.to_tir(attrs, width_op.row_cur, width_op.col_disp) + local attr = attrs[curosr_tir.iblock] + if Attr.is_plain(attr) then + print("kind=plain") + else + print(get_width_info(ctx.bufnr, attr, curosr_tir)) + end +end + +---@param ctx Context +---@param width_op WidthOp +local function toggle_wrap_mode(ctx, width_op) + local attrs = attr_store.read(ctx.bufnr) + local attr = Attrs.get(attrs, width_op.row_cur) + Attr.toggle_wrap_mode(attr or {}) + attr_store.write(ctx.bufnr, attrs) +end + +local warned = false +---@param command string +local function set_repeat(command) + local ok = pcall(function() + fn["repeat#set"](command) + end) + if not ok and not warned then + warned = true + notify.info("tirenvi: install 'tpope/vim-repeat' to enable '.' repeat") + end +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@param width_op WidthOp +function M.cmd_width(ctx, width_op) + if width_op.operation == "info" then + width_info(ctx, width_op) + else + change_wrap_width(ctx, width_op) + end + set_repeat(util.get_termcodes(width_op:to_cmd())) +end + +---@param ctx Context +---@param width_op WidthOp +function M.cmd_fit(ctx, width_op) + if width_op.operation == "auto" then + change_wrap_auto(ctx, width_op) + else + change_wrap_fit(ctx, width_op) + end + set_repeat(util.get_termcodes(width_op:to_cmd())) +end + +---@param ctx Context +---@param width_op WidthOp +function M.cmd_wrap(ctx, width_op) + toggle_wrap_mode(ctx, width_op) +end + +return M diff --git a/lua/tirenvi/config.lua b/lua/tirenvi/config.lua index 10806734..ba68f020 100644 --- a/lua/tirenvi/config.lua +++ b/lua/tirenvi/config.lua @@ -1,10 +1,6 @@ ---- Configuration management for tirenvi. +local levels = vim.log.levels -- Neovim -local levels = vim.log.levels - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ +-- ============================================================================= local M = {} @@ -15,10 +11,6 @@ local M = {} ---@field lf string ---@field tab string ------------------------------------------------------------------------ --- Defaults ------------------------------------------------------------------------ - local defaults = { ---@type Marks marks = { @@ -31,9 +23,25 @@ local defaults = { ---@type {[string]: Parser} parser_map = { csv = { executable = "tir-csv", required_version = "0.1.4" }, - tsv = { executable = "tir-csv", options = { "--delimiter", "\t" }, required_version = "0.1.4" }, - markdown = { executable = "tir-gfm-lite", allow_plain = true, required_version = "0.1.6" }, - pukiwiki = { executable = "tir-pukiwiki", allow_plain = true, required_version = "0.1.1" }, + tsv = { + executable = "tir-csv", + options = { "--delimiter", "\t" }, + required_version = "0.1.4", + }, + markdown = { + executable = "tir-gfm-lite", + allow_plain = true, + required_version = "0.1.6", + }, + pukiwiki = { + executable = "tir-pukiwiki", + allow_plain = true, + required_version = "0.1.1", + }, + ["*"] = { + executable = "tir-embedded", + allow_plain = true, + }, }, textobj = { column = "l", @@ -51,7 +59,7 @@ local defaults = { highlight = { line = "TirenviDirty", sign = "TirenviDirtySign", - } + }, }, log = { level = levels.WARN, @@ -65,9 +73,8 @@ local defaults = { }, } ------------------------------------------------------------------------ --- Initialize with defaults ------------------------------------------------------------------------ +-- ============================================================================= +--#region Private ---@param opts {[string]:any} local function apply(opts) @@ -79,13 +86,14 @@ end ---@param parser_map Parser[] local function parse_version(parser_map) for _, parser in pairs(parser_map) do - parser._required_version_int = M.version_to_integer(parser.required_version) + parser._required_version_int = + M.version_to_integer(parser.required_version) end end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param opts {[string]:any} function M.setup(opts) diff --git a/lua/tirenvi/constants.lua b/lua/tirenvi/constants.lua index 89bcf32e..f1750ecd 100644 --- a/lua/tirenvi/constants.lua +++ b/lua/tirenvi/constants.lua @@ -1,3 +1,5 @@ +-- ============================================================================= + local M = {} M.DOCUMENT_VERSION = "tir/0.1" diff --git a/lua/tirenvi/core/attr.lua b/lua/tirenvi/core/attr.lua index 554092c3..6abafb15 100644 --- a/lua/tirenvi/core/attr.lua +++ b/lua/tirenvi/core/attr.lua @@ -1,246 +1,260 @@ -local config = require("tirenvi.config") -local Record = require("tirenvi.core.record") +local config = require("tirenvi.config") -- Root + +local Record = require("tirenvi.core.record") -- Core local Cell = require("tirenvi.core.cell") -local Range = require("tirenvi.util.range") -local log = require("tirenvi.util.log") -local M = {} +local log = require("tirenvi.util.log") -- Util +-- ============================================================================= + +local M = {} M.plain = {} M.grid = {} --- constants / defaults - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= +--#region Private ---@param cells Cell[] ---@return Attr_column[] local function get_columns(cells) - local columns = {} - local widths = Cell.get_max_widths(cells) - for _, width in ipairs(widths) do - width = math.max(width, Cell.MIN_WIDTH) - columns[#columns + 1] = { width = width } - end - return columns + local columns = {} + local widths = Cell.get_max_widths(cells) + for _, width in ipairs(widths) do + width = math.max(width, Cell.MIN_WIDTH) + columns[#columns + 1] = { width = width } + end + return columns end ---@param records Record_grid[] ---@param icol integer ---@return integer local function get_max_width(records, icol) - local max_width = Cell.MIN_WIDTH - for _, record in ipairs(records) do - local width = Cell.get_max_width(record.row[icol]) - max_width = math.max(max_width, width) - end - return max_width + local max_width = Cell.MIN_WIDTH + for _, record in ipairs(records) do + local width = Cell.get_max_width(record.row[icol]) + max_width = math.max(max_width, width) + end + return max_width end ---@param columns Attr_column[]|nil ---@return Attr local function new_from_columns(columns) - local self = {} - self.columns = columns - self.fit_span = 0 - return self + local self = {} + self.columns = columns + self.fit_span = 0 + return self end ------------------------------------------------------------------------ +local short_map = { + plain = "", + nowrap = "n", + wrap_auto = "a", + wrap_fit = "f", + wrap_width = "w", +} +---@param self Attr +---@return string +local function get_mode_short(self) + local wrap_mode = M.get_wrap_mode(self) + return short_map[wrap_mode] +end + +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@return Attr function M.new() - return new_from_columns(nil) + return new_from_columns(nil) end ---@return Attr function M.plain.new() - return new_from_columns(nil) + return new_from_columns(nil) end ---@param record Record_grid|nil ---@return Attr function M.grid.new(record) - local self - if record then - self = new_from_columns(get_columns(record.row)) - else - self = new_from_columns({}) - end - return self + local self + if record then + self = new_from_columns(get_columns(record.row)) + else + self = new_from_columns({}) + end + return self end ---@self Attr ---@param records Record_grid[] ---@param force boolean|nil function M.grid:set_max_attr(records, force) - force = force or false - local ncol = #self.columns - if ncol == 0 then - ncol = Record.get_max_ncol(records) - M.set_ncol(self, ncol) - end - for icol, column in pairs(self.columns) do - if force or column.width <= 0 then - column.width = get_max_width(records, icol) - end - end + force = force or false + local ncol = #self.columns + if ncol == 0 then + ncol = Record.get_max_ncol(records) + M.set_ncol(self, ncol) + end + for icol, column in pairs(self.columns) do + if force or column.width <= 0 then + column.width = get_max_width(records, icol) + end + end end ---@param records Record_grid[] ---@return integer[] function M.grid.get_max_width(records) - local widths = {} - local ncol = Record.get_max_ncol(records) - for icol = 1, ncol do - widths[#widths + 1] = get_max_width(records, icol) - end - return widths + local widths = {} + local ncol = Record.get_max_ncol(records) + for icol = 1, ncol do + widths[#widths + 1] = get_max_width(records, icol) + end + return widths end ---@param self Attr ---@return string function M.get_attr_short(self) - local kind = M.is_plain(self) and "p" or "g" - local range_str = self.range and string.format("(%d,%d)", self.range.first, self.range.last) or "()" - return kind .. range_str -end - -local short_map = { plain = "", nowrap = "n", wrap_auto = "a", wrap_fit = "f", wrap_width = "w" } ----@param self Attr ----@return string -local function get_mode_short(self) - local wrap_mode = M.get_wrap_mode(self) - return short_map[wrap_mode] + local kind = M.is_plain(self) and "p" or "g" + local range_str = self.range + and string.format("(%d,%d)", self.range.first, self.range.last) + or "()" + local key + if self.prefix then + key = "'" .. vim.trim(self.prefix) .. "'" + else + key = "" + end + return kind .. key .. range_str end ---@param attr Attr ---@param ccol integer|nil ---@return string function M.get_attr_long(attr, ccol) - local widths = M.get_width_array(attr.columns) - ---@cast widths string[] - local long = "" - if ccol and M.is_plain(attr) then - long = "*" - else - if ccol then - widths[ccol] = widths[ccol] .. "*" - end - long = #widths > 0 and string.format("[%s]", table.concat(widths, ",")) or "" - end - local fit_span = attr.fit_span == 0 and "" or tostring(attr.fit_span) - return M.get_attr_short(attr) .. get_mode_short(attr) .. fit_span .. long + local widths = M.get_width_array(attr.columns) + ---@cast widths string[] + local long = "" + if ccol and M.is_plain(attr) then + long = "*" + else + if ccol then + widths[ccol] = widths[ccol] .. "*" + end + long = #widths > 0 and string.format("[%s]", table.concat(widths, ",")) + or "" + end + local fit_span = attr.fit_span == 0 and "" or tostring(attr.fit_span) + return M.get_attr_short(attr) .. get_mode_short(attr) .. fit_span .. long end ---@param columns Attr_column[] ---@return integer[] function M.get_width_array(columns) - if not columns then - return {} - end - local widths = {} - for _, column in ipairs(columns) do - widths[#widths + 1] = column.width - end - return widths + if not columns then + return {} + end + local widths = {} + for _, column in ipairs(columns) do + widths[#widths + 1] = column.width + end + return widths end ---@param source Attr ---@param target Attr ---@return boolean function M.is_same_ncol(source, target) - local columns1 = source.columns or {} - local columns2 = target.columns or {} - return #columns1 == #columns2 + local columns1 = source.columns or {} + local columns2 = target.columns or {} + return #columns1 == #columns2 end ---@param source Attr ---@param target Attr ---@return boolean function M.is_consistent(source, target) - if M.is_plain(source) or M.is_plain(target) then - return true - end - return M.is_same_ncol(source, target) + if M.is_plain(source) or M.is_plain(target) then + return true + end + return M.is_same_ncol(source, target) end ---@self Attr|nil ---@return boolean function M:is_plain() - return self and self.columns == nil + return self and self.columns == nil end ---@param self Attr|nil ---@return boolean function M:is_grid() - if not self then - return false - end - return not M.is_plain(self) + if not self then + return false + end + return not M.is_plain(self) end ---@param self Attr ---@param ncol integer|nil function M:set_ncol(ncol) - for icol = 1, ncol or 0 do - self.columns[icol] = { width = 0 } - end + for icol = 1, ncol or 0 do + self.columns[icol] = { width = 0 } + end end ---@param self Attr ---@param last_col integer|nil ---@return integer function M:get_total_width(last_col) - if M.is_plain(self) then - return 0 - end - last_col = last_col or #self.columns - last_col = math.min(last_col, #self.columns) - local total = 0 - for icol = 1, last_col do - total = total + self.columns[icol].width - end - return total + if M.is_plain(self) then + return 0 + end + last_col = last_col or #self.columns + last_col = math.min(last_col, #self.columns) + local total = 0 + for icol = 1, last_col do + total = total + self.columns[icol].width + end + return total end ---@param self Attr ---@return WrapMode function M:get_wrap_mode() - if M.is_plain(self) then - return "plain" - end - if not self.wrap_mode then - if config.table.wrap_mode == "nowrap" then - self.wrap_mode = "nowrap" - else - self.wrap_mode = "wrap_auto" - end - end - if self.wrap_mode == "wrap_fit" then - if self.fit_span == 0 then - self.wrap_mode = "wrap_auto" - end - end - return self.wrap_mode + if M.is_plain(self) then + return "plain" + end + if not self.wrap_mode then + if config.table.wrap_mode == "nowrap" then + self.wrap_mode = "nowrap" + else + self.wrap_mode = "wrap_auto" + end + end + if self.wrap_mode == "wrap_fit" then + if self.fit_span == 0 then + self.wrap_mode = "wrap_auto" + end + end + return self.wrap_mode end ---@param self Attr ---@return "wrap"|"nowrap"|"auto" function M:get_wrap_kind() - log.assert(not M.is_plain(self), "is not plain attr: %s", vim.inspect(self)) - local mode = M.get_wrap_mode(self) - if mode == "nowrap" then - return "nowrap" - elseif mode == "wrap_auto" then - return "auto" - else - return "wrap" - end + log.assert(not M.is_plain(self), "is not plain attr: %s", vim.inspect(self)) + local mode = M.get_wrap_mode(self) + if mode == "nowrap" then + return "nowrap" + elseif mode == "wrap_auto" then + return "auto" + else + return "wrap" + end end ---@param self Attr @@ -248,60 +262,60 @@ end ---@return integer ---@return integer function M:to_cell_col(col_disp) - if M.is_plain(self) then - return 0, 0 - end - local start = 1 - local last - for icol, column in ipairs(self.columns) do - last = start + column.width - if col_disp <= last then - return icol, col_disp - start - end - start = last + 1 - end - return #self.columns, self.columns[#self.columns].width + if M.is_plain(self) then + return 0, 0 + end + local start = 1 + local last + for icol, column in ipairs(self.columns) do + last = start + column.width + if col_disp <= last then + return icol, col_disp - start + end + start = last + 1 + end + return #self.columns, self.columns[#self.columns].width end ---@param self Attr ---@param col_byte integer function M:get(col_byte) - local icol = M.to_cell_col(self, col_byte) - return self.columns[icol] + local icol = M.to_cell_col(self, col_byte) + return self.columns[icol] end ---@param self Attr ---@param icol integer ---@return integer function M:get_start_pos(icol) - if M.is_plain(self) then - return 1 - end - local width = M.get_total_width(self, icol - 1) - return width + icol + 1 + if M.is_plain(self) then + return 1 + end + local width = M.get_total_width(self, icol - 1) + return width + icol + 1 end ---@param self Attr function M:toggle_wrap_mode() - if not self or M.is_plain(self) then - return - end - if M.get_wrap_mode(self) == "nowrap" then - if self.fit_span == 0 then - self.wrap_mode = "wrap_auto" - else - self.wrap_mode = "wrap_fit" - end - else - -- self.fit_span = M.get_fit_span(self) - self.wrap_mode = "nowrap" - end + if not self or M.is_plain(self) then + return + end + if M.get_wrap_mode(self) == "nowrap" then + if self.fit_span == 0 then + self.wrap_mode = "wrap_auto" + else + self.wrap_mode = "wrap_fit" + end + else + -- self.fit_span = M.get_fit_span(self) + self.wrap_mode = "nowrap" + end end ---@param self Attr ---@return integer function M.get_fit_span(self) - return M.get_total_width(self) + #self.columns + 1 + return M.get_total_width(self) + #self.columns + 1 end return M diff --git a/lua/tirenvi/core/attrs.lua b/lua/tirenvi/core/attrs.lua index 91e1d7d5..08da5b52 100644 --- a/lua/tirenvi/core/attrs.lua +++ b/lua/tirenvi/core/attrs.lua @@ -1,41 +1,41 @@ -local Attr = require("tirenvi.core.attr") -local CursorLogical = require("tirenvi.cursor.cursor_logical") -local Range = require("tirenvi.util.range") -local util = require("tirenvi.util.util") -local log = require("tirenvi.util.log") +local api = vim.api -- Neovim -local M = {} -local api = vim.api +local Attr = require("tirenvi.core.attr") -- Core --- constants / defaults +local Range = require("tirenvi.util.range") -- Util +local util = require("tirenvi.util.util") +local log = require("tirenvi.util.log") ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private ---@param self Attr[] ---@return Attr[] local function connect(self) - local attrs = { self[1] } - for iattr = 2, #self do - local attr = self[iattr] - if Attr.is_same_ncol(attrs[#attrs], attr) then - attrs[#attrs].range.last = attr.range.last - else - attrs[#attrs + 1] = attr - end - end - return attrs + local attrs = { self[1] } + for iattr = 2, #self do + local attr = self[iattr] + if Attr.is_same_ncol(attrs[#attrs], attr) then + attrs[#attrs].range.last = attr.range.last + else + attrs[#attrs + 1] = attr + end + end + return attrs end ---@param self Attr[] local function reset_range(self) - local last = self[1].range.last - for iattr = 2, #self do - local range = self[iattr].range - Range.move_to(range, last + 1) - last = range.last - end + local last = self[1].range.last + for iattr = 2, #self do + local range = self[iattr].range + Range.move_to(range, last + 1) + last = range.last + end end ---@param seq integer @@ -43,14 +43,14 @@ end ---@param size integer ---@param new_size integer local function get_new_seq(seq, range3, size, new_size) - local pos = seq - range3.first - local new_pos = pos - if 0 <= pos and pos < size then - new_pos = pos * new_size / size - elseif size <= pos then - new_pos = pos + new_size - size - end - return new_pos + range3.first + local pos = seq - range3.first + local new_pos = pos + if 0 <= pos and pos < size then + new_pos = pos * new_size / size + elseif size <= pos then + new_pos = pos + new_size - size + end + return new_pos + range3.first end local current_index = 1 @@ -58,68 +58,68 @@ local current_index = 1 ---@param row_cur integer ---@return integer|nil local function get_index(self, row_cur) - if not self or #self == 0 or not self[1].range then - return nil - end - if current_index > #self then - current_index = 1 - end - for _ = 1, #self do - if Range.contains(self[current_index].range, row_cur) then - return current_index - end - current_index = current_index % #self + 1 - end - return nil + if not self or #self == 0 or not self[1].range then + return nil + end + if current_index > #self then + current_index = 1 + end + for _ = 1, #self do + if Range.contains(self[current_index].range, row_cur) then + return current_index + end + current_index = current_index % #self + 1 + end + return nil end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param self Attr[]|nil ---@return table|nil function M.get_count(self) - local count = { plain = 0, grid = 0 } - if not self then - return nil - end - for _, attr in ipairs(self) do - if Attr.is_grid(attr) then - count.grid = count.grid + 1 - else - count.plain = count.plain + 1 - end - end - return count + local count = { plain = 0, grid = 0 } + if not self then + return nil + end + for _, attr in ipairs(self) do + if Attr.is_grid(attr) then + count.grid = count.grid + 1 + else + count.plain = count.plain + 1 + end + end + return count end ---@param self Attr[]|nil ----@return boolean|nil +---@return boolean function M.has_grid(self) - if not self then - return nil - end - local count = M.get_count(self) - for _, attr in ipairs(self) do - if Attr.is_grid(attr) then - return true - end - end - return count and count.grid > 0 or false + if not self then + return false + end + local count = M.get_count(self) + for _, attr in ipairs(self) do + if Attr.is_grid(attr) then + return true + end + end + return count and count.grid > 0 or false end ---@param self Attr[]|nil ---@return Attr[] function M.get_grid_attrs(self) - self = self or {} - local attrs = {} - for _, attr in ipairs(self) do - if Attr.is_grid(attr) then - attrs[#attrs + 1] = attr - end - end - return attrs + self = self or {} + local attrs = {} + for _, attr in ipairs(self) do + if Attr.is_grid(attr) then + attrs[#attrs + 1] = attr + end + end + return attrs end ---@param self Attr[] @@ -127,18 +127,21 @@ end ---@param doc_attrs Attr[] ---@return Attr[] function M:replace_attrs(range, doc_attrs) - local attrs1, _, attrs3 = Range.split(self, range) - local attrs = attrs1 - log.watch("ATTR", range) - log.watch("ATTR", M.debug_attrs(self, "MERGE ORIGIN:")) - log.watch("ATTR", M.debug_attrs(attrs1, "MERGE:") .. - M.debug_attrs(doc_attrs, " + ") .. - M.debug_attrs(attrs3, " + ")) - util.extend(attrs, doc_attrs) - util.extend(attrs, attrs3) - attrs = connect(attrs) - reset_range(attrs) - return attrs + local attrs1, _, attrs3 = Range.split(self, range) + local attrs = attrs1 + log.watch("ATTR", range) + log.watch("ATTR", M.debug_attrs(self, "MERGE ORIGIN:")) + log.watch( + "ATTR", + M.debug_attrs(attrs1, "MERGE:") + .. M.debug_attrs(doc_attrs, " + ") + .. M.debug_attrs(attrs3, " + ") + ) + util.extend(attrs, doc_attrs) + util.extend(attrs, attrs3) + attrs = connect(attrs) + reset_range(attrs) + return attrs end local nmax = 4 @@ -150,77 +153,77 @@ local nmax = 4 ---@param single boolean|nil ---@return string function M:debug_attrs(title, iblock, icol, single) - single = single or false - if not log.is_debug() and not vim.g.tirenvi_test_mode then - return "" - end - if not self then - return title .. " nil" - end - local strings = { title } - if single then - strings[1] = Attr.get_attr_long(self[iblock], icol) - else - for iattr = 1, math.min(#self, nmax) do - local ccol - if iblock and iattr == iblock then - ccol = icol - end - strings[#strings + 1] = Attr.get_attr_long(self[iattr], ccol) - end - end - return table.concat(strings, " ") + single = single or false + if not log.is_debug() and not vim.g.tirenvi_test_mode then + return "" + end + if not self then + return title .. " nil" + end + local strings = { title } + if single then + strings[1] = Attr.get_attr_long(self[iblock], icol) + else + for iattr = 1, math.min(#self, nmax) do + local ccol + if iblock and iattr == iblock then + ccol = icol + end + strings[#strings + 1] = Attr.get_attr_long(self[iattr], ccol) + end + end + return table.concat(strings, " ") end ---@param self Attr[] function M:remove_range() - for _, attr in ipairs(self) do - attr.range = nil - end + for _, attr in ipairs(self) do + attr.range = nil + end end ---@param attrs Attr[] ---@param range3 Range3 ---@return Attr[] function M.adjust(attrs, range3) - if #attrs == 0 then - return attrs - end - if not attrs[1].range then - return attrs - end - local size = range3.last - range3.first + 1 - local new_size = range3.new_last - range3.first + 1 - for _, attr in ipairs(attrs) do - attr.range.first = get_new_seq(attr.range.first, range3, size, new_size) - attr.range.last = get_new_seq(attr.range.last, range3, size, new_size) - end - log.watch("ATTR", M.debug_attrs(attrs, "UPDATE RANGE expand 1:")) - for iattr = 1, #attrs - 1 do - attrs[iattr].range.last = attrs[iattr + 1].range.first - 1 - end - attrs[1].range.first = 1 - attrs[#attrs].range.last = api.nvim_buf_line_count(0) - log.watch("ATTR", M.debug_attrs(attrs, "UPDATE RANGE expand 2:")) - local new_attrs = {} - for _, attr in ipairs(attrs) do - if attr.range.first <= attr.range.last then - new_attrs[#new_attrs + 1] = attr - end - end - return new_attrs + if #attrs == 0 then + return attrs + end + if not attrs[1].range then + return attrs + end + local size = range3.last - range3.first + 1 + local new_size = range3.new_last - range3.first + 1 + for _, attr in ipairs(attrs) do + attr.range.first = get_new_seq(attr.range.first, range3, size, new_size) + attr.range.last = get_new_seq(attr.range.last, range3, size, new_size) + end + log.watch("ATTR", M.debug_attrs(attrs, "UPDATE RANGE expand 1:")) + for iattr = 1, #attrs - 1 do + attrs[iattr].range.last = attrs[iattr + 1].range.first - 1 + end + attrs[1].range.first = 1 + attrs[#attrs].range.last = api.nvim_buf_line_count(0) + log.watch("ATTR", M.debug_attrs(attrs, "UPDATE RANGE expand 2:")) + local new_attrs = {} + for _, attr in ipairs(attrs) do + if attr.range.first <= attr.range.last then + new_attrs[#new_attrs + 1] = attr + end + end + return new_attrs end ---@param attrs Attr[] ---@param range Range ---@return Attr|nil function M.get_attr(attrs, range) - for _, attr in ipairs(attrs) do - if Range.intersects(attr.range, range) then - return attr - end - end - return nil + for _, attr in ipairs(attrs) do + if Range.intersects(attr.range, range) then + return attr + end + end + return nil end ---@param self Attr[] @@ -228,50 +231,34 @@ end ---@return Attr|nil ---@return integer|nil function M:get(row_cur) - local iblock = get_index(self, row_cur) - return self[iblock], iblock + local iblock = get_index(self, row_cur) + return self[iblock], iblock end ---@param self Attr[] ---@return Attr[] function M:get_invalid_attrs() - local invalid = {} - local prev = self[1] - for iattr = 2, #self do - local attr = self[iattr] - if not Attr.is_consistent(prev, attr) then - invalid[#invalid + 1] = attr - end - prev = attr - end - return invalid -end - ----@param self Attr[] ----@param row_cur integer ----@param col_disp integer ----@return CursorLogical -function M:to_logical(row_cur, col_disp) - local _, iblock = M.get(self, row_cur) - if not iblock then - return {} - end - local attr = self[iblock] - log.assert(attr, "invalid position %d", row_cur) - local irow = row_cur - attr.range.first + 1 - local icol, offset = Attr.to_cell_col(attr, col_disp) - return CursorLogical.new(iblock, irow, icol, offset) + local invalid = {} + local prev = self[1] + for iattr = 2, #self do + local attr = self[iattr] + if not Attr.is_consistent(prev, attr) then + invalid[#invalid + 1] = attr + end + prev = attr + end + return invalid end ---@param self Attr[] ----@param logical CursorLogical ----@return integer ----@return integer -function M:to_cursor(logical) - local attr = self[logical.iblock] - local row_cur = attr.range.first + logical.irow - 1 - local col_disp = Attr.get_start_pos(attr, logical.icol) - return row_cur, col_disp +---@return string|nil +function M:get_embedded_key() + for _, attr in ipairs(self) do + if Attr.is_grid(attr) then + return attr.prefix and vim.trim(attr.prefix) or nil + end + end + return nil end return M diff --git a/lua/tirenvi/core/block.lua b/lua/tirenvi/core/block.lua index 47ffe98e..4b81de5b 100644 --- a/lua/tirenvi/core/block.lua +++ b/lua/tirenvi/core/block.lua @@ -1,226 +1,243 @@ -local CONST = require("tirenvi.constants") -local Record = require("tirenvi.core.record") +local CONST = require("tirenvi.constants") -- Root local config = require("tirenvi.config") + +local Record = require("tirenvi.core.record") -- Core local Attr = require("tirenvi.core.attr") local Attrs = require("tirenvi.core.attrs") -local util = require("tirenvi.util.util") + +local util = require("tirenvi.util.util") -- Util local log = require("tirenvi.util.log") +-- ============================================================================= + local M = {} M.plain = {} M.grid = {} --- constants / defaults +-- ============================================================================= +--#region Private + +local function nop(...) end ---@param self Block ---@param method string ---@param ... unknown local function apply(self, method, ...) - for _, record in ipairs(self.records) do - local common = Record[method] - if common then - common(record, ...) - end - - local handler = Record[record.kind] - local specific = handler[method] - if specific then - specific(record, ...) - end - end + for _, record in ipairs(self.records) do + local common = Record[method] + if common then + common(record, ...) + end + + local handler = Record[record.kind] + local specific = handler[method] + if specific then + specific(record, ...) + end + end end ---@param map {[string]: string} ---@return {[string]: string} local function prepare_replace_map(map) - local out = {} - for key, value in pairs(map) do - out[vim.pesc(key)] = value - end - return out + local out = {} + for key, value in pairs(map) do + out[vim.pesc(key)] = value + end + return out end ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - -local function nop(...) end - ---@return {[string]: string} local function get_escape_map() - return prepare_replace_map({ - ["\n"] = config.marks.lf, - ["\t"] = config.marks.tab, - }) + return prepare_replace_map({ + ["\n"] = config.marks.lf, + ["\t"] = config.marks.tab, + }) end ---@return {[string]: string} local function get_un_escape_map() - return prepare_replace_map({ - [config.marks.lf] = "\n", - [config.marks.tab] = "\t", - }) + return prepare_replace_map({ + [config.marks.lf] = "\n", + [config.marks.tab] = "\t", + }) end ---@self Block ---@param kind Block_kind local function initialize(self, kind) - self.kind = kind - self.attr = Attr[self.kind].new() + self.kind = kind + self.attr = Attr[self.kind].new() end ---@param self Block ---@return Ndjson[] local function serialize_records(self) - ---@type Ndjson[] - local ndjsons = {} - for _, record in ipairs(self.records) do - ndjsons[#ndjsons + 1] = record - end - return ndjsons + ---@type Ndjson[] + local ndjsons = {} + for _, record in ipairs(self.records) do + ndjsons[#ndjsons + 1] = record + end + return ndjsons end ---@param self Block_grid local function wrap(self) - local records = {} - for _, record in ipairs(self.records) do - util.extend(records, Record.grid.wrap(record, self.attr.columns)) - end - records[#records]._has_continuation = false - self.records = records + local records = {} + for _, record in ipairs(self.records) do + util.extend(records, Record.grid.wrap(record, self.attr.columns)) + end + records[#records]._has_continuation = false + self.records = records end ---@param self Block_grid local function fill_padding(self) - apply(self, "fill_padding", self.attr.columns) + apply(self, "fill_padding", self.attr.columns) end ---@self Block local function remove_padding(self) - apply(self, "remove_padding") + apply(self, "remove_padding") end ---@self Block_grid local function unwrap(self) - local records = {} - ---@type Record_grid - local new_record = nil - local cont_prev = false - for _, record in ipairs(self.records) do - if not cont_prev then - new_record = Record.grid.new(record.row) - records[#records + 1] = new_record - else - Record.grid.concat(new_record, record) - end - cont_prev = record._has_continuation - new_record._has_continuation = cont_prev - end - self.records = records + local records = {} + ---@type Record_grid + local new_record = nil + local cont_prev = false + for _, record in ipairs(self.records) do + if not cont_prev then + new_record = vim.deepcopy(record) + records[#records + 1] = new_record + else + Record.grid.concat(new_record, record) + end + cont_prev = record._has_continuation + new_record._has_continuation = cont_prev + end + self.records = records end --- Normalize all rows in a grid block to have the same number of columns. ---@self Block_grid local function apply_column_count(self, ncol) - log.assert(ncol ~= 0, "column count must be greater than 0") - apply(self, "apply_column_count", ncol) + log.assert(ncol ~= 0, "column count must be greater than 0") + apply(self, "apply_column_count", ncol) end ---@self Block ---@self Block_grid ---@param replace {[string]:string} local function apply_replacements(self, replace) - for _, record in ipairs(self.records) do - log.assert(record.kind == CONST.KIND.GRID, "unexpected record kind") - for icol, cell in ipairs(record.row) do - for key, val in pairs(replace) do - cell = cell:gsub(key, val) - end - record.row[icol] = cell - end - end + for _, record in ipairs(self.records) do + log.assert(record.kind == CONST.KIND.GRID, "unexpected record kind") + for icol, cell in ipairs(record.row) do + for key, val in pairs(replace) do + cell = cell:gsub(key, val) + end + record.row[icol] = cell + end + end end ---@param self Block_grid local function set_consistent_ncol(self) - local ncol = Record.get_consistent_ncol(self.records) - Attr.set_ncol(self.attr, ncol) + local ncol = Record.get_consistent_ncol(self.records) + Attr.set_ncol(self.attr, ncol) end ---@param self Block_grid local function set_consistent_width(self) - local width_min = Attr.get_width_array(Attr.grid.new(self.records[1]).columns) - local width_max = vim.deepcopy(width_min) - for irec = 2, #self.records do - local widths = Attr.get_width_array(Attr.grid.new(self.records[irec]).columns) - for icol, width in ipairs(widths) do - width_min[icol] = width_min[icol] or 0 - width_max[icol] = width_max[icol] or math.huge - width_min[icol] = math.min(width_min[icol], width) - width_max[icol] = math.max(width_max[icol], width) - end - end - for icol = 1, math.min(#self.attr.columns, #width_max) do - if width_min[icol] == width_max[icol] then - self.attr.columns[icol].width = width_max[icol] - end - end -end - ------------------------------------------------------------------------ + local width_min = + Attr.get_width_array(Attr.grid.new(self.records[1]).columns) + local width_max = vim.deepcopy(width_min) + for irec = 2, #self.records do + local widths = + Attr.get_width_array(Attr.grid.new(self.records[irec]).columns) + for icol, width in ipairs(widths) do + width_min[icol] = width_min[icol] or 0 + width_max[icol] = width_max[icol] or math.huge + width_min[icol] = math.min(width_min[icol], width) + width_max[icol] = math.max(width_max[icol], width) + end + end + for icol = 1, math.min(#self.attr.columns, #width_max) do + if width_min[icol] == width_max[icol] then + self.attr.columns[icol].width = width_max[icol] + end + end +end + +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@return Block function M.new() - return { attr = Attr.new(), records = {} } + return { attr = Attr.new(), records = {} } end ---@self Block ---@param kind Block_kind function M:set_kind(kind) - if self.kind == kind then - return - end - log.assert(not self.kind, "Block kind already set") - initialize(self, kind) + if self.kind == kind then + return + end + log.assert(not self.kind, "Block kind already set") + initialize(self, kind) end ---@self Block ---@param record Record function M:add(record) - self.records[#self.records + 1] = record + self.records[#self.records + 1] = record end ----@self Block +---@self Block_plain ---@return Ndjson[] -function M:serialize() - return serialize_records(self) +function M.plain:serialize() + return serialize_records(self) +end + +---@self Block_grid +---@return Ndjson[] +function M.grid:serialize() + return serialize_records(self) +end + +---@self Block_grid +function M.grid:prefix_to_records() + local prefix = self.attr.prefix + for _, record in ipairs(self.records) do + --record.prefix = prefix + end end ---@self Block ---@param no_normalize boolean function M:from_buf(no_normalize) - remove_padding(self) - if self.kind == CONST.KIND.GRID and not no_normalize then - unwrap(self) - end + remove_padding(self) + if self.kind == CONST.KIND.GRID and not no_normalize then + unwrap(self) + end end function M.plain.new() - local self = M.new() - M.set_kind(self, CONST.KIND.PLAIN) - M.add(self, Record.plain.new_from_bufline("")) - return self + local self = M.new() + M.set_kind(self, CONST.KIND.PLAIN) + M.add(self, Record.plain.new("")) + return self end ---@self Block function M.plain:to_flat() - for _, record in ipairs(self.records) do - for key, val in pairs(get_un_escape_map()) do - record.line = record.line:gsub(key, val) - end - end + for _, record in ipairs(self.records) do + for key, val in pairs(get_un_escape_map()) do + record.line = record.line:gsub(key, val) + end + end end M.plain.inherit_neighbor_attr = nop @@ -228,91 +245,107 @@ M.plain.inherit_neighbor_attr = nop --- Normalize all rows in a grid block to have the same number of columns. ---@self Block_grid function M.grid:from_flat() - apply_replacements(self, get_escape_map()) + --self.attr.prefix = self.records[1].prefix + --self.records[1].prefix = nil + apply_replacements(self, get_escape_map()) end ---@self Block_grid function M.grid:to_flat() - apply_replacements(self, get_un_escape_map()) + --self.records[1].prefix = self.attr.prefix + apply_replacements(self, get_un_escape_map()) end --- Normalize all rows in a grid block to have the same number of columns. ---@self Block_grid function M.grid:to_bufdoc() - apply_column_count(self, #self.attr.columns) - wrap(self) - fill_padding(self) + apply_column_count(self, #self.attr.columns) + wrap(self) + fill_padding(self) end ---@self Block_grid function M.grid:infer_consistent_attr() - if #self.attr.columns ~= 0 then - return - end - set_consistent_ncol(self) - set_consistent_width(self) + if #self.attr.columns ~= 0 then + return + end + set_consistent_ncol(self) + set_consistent_width(self) end ---@param self Block_grid ---@param force boolean|nil function M.grid:set_max_attr(force) - Attr.grid.set_max_attr(self.attr, self.records, force) + Attr.grid.set_max_attr(self.attr, self.records, force) end ---@param self Block_grid ---@return integer[] function M.grid:get_max_width() - return Attr.grid.get_max_width(self.records) + return Attr.grid.get_max_width(self.records) end ---@self Block_grid ---@param attrs Attr[] function M.grid:apply_attrs_by_range(attrs) - local attr = Attrs.get_attr(attrs, self.attr.range) - if not attr or not Attr.is_grid(attr) then - return - end - self.attr.wrap_mode = attr.wrap_mode - self.attr.fit_span = attr.fit_span - if #self.attr.columns == 0 then - self.attr.columns = vim.deepcopy(attr.columns) - else - for icol, column in ipairs(self.attr.columns) do - if column.width <= 0 then - column.width = attr.columns[icol].width - end - end - end + local attr = Attrs.get_attr(attrs, self.attr.range) + if not attr or not Attr.is_grid(attr) then + return + end + self.attr.wrap_mode = attr.wrap_mode + self.attr.fit_span = attr.fit_span + if #self.attr.columns == 0 then + self.attr.columns = vim.deepcopy(attr.columns) + else + for icol, column in ipairs(self.attr.columns) do + if column.width <= 0 then + column.width = attr.columns[icol].width + end + end + end end ---@param self Block_grid ---@param attrs Attr[] ---@param row_cur integer function M.grid:inherit_neighbor_attr(attrs, row_cur) - local attr = Attrs.get(attrs, row_cur) - if not attr or not Attr.is_grid(attr) then - return - end - if #self.attr.columns == 0 then - self.attr.columns = vim.deepcopy(attr.columns) - end + local attr = Attrs.get(attrs, row_cur) + if not attr or not Attr.is_grid(attr) then + return + end + if #self.attr.columns == 0 then + self.attr.columns = vim.deepcopy(attr.columns) + end end ---@self Block_plain ---@return boolean function M.plain:has_width() - return true + return true end ---@self Block_grid ---@return boolean function M.grid:has_width() - for _, column in ipairs(self.attr.columns) do - if column.width <= 0 then - return false - end - end - return true + for _, column in ipairs(self.attr.columns) do + if column.width <= 0 then + return false + end + end + return true +end + +---@self Block_plain +---@return Attr +function M.plain:get_attr() + return self.attr +end + +---@self Block_grid +---@return Attr +function M.grid:get_attr() + self.attr.prefix = self.records[1].prefix + return self.attr end return M diff --git a/lua/tirenvi/core/blocks.lua b/lua/tirenvi/core/blocks.lua index 248c52a6..9e4cd343 100644 --- a/lua/tirenvi/core/blocks.lua +++ b/lua/tirenvi/core/blocks.lua @@ -1,27 +1,21 @@ ---- Utilities for handling NDJSON records and converting them into ---- plain/grid block structures. ---- ---- Design: ---- - Blocks are separated so that plain and grid records never mix. ---- - Column expansion is applied only to grid blocks. - -local CONST = require("tirenvi.constants") -local Attr = require("tirenvi.core.attr") +local CONST = require("tirenvi.constants") -- Root + +local Attr = require("tirenvi.core.attr") -- Core local Block = require("tirenvi.core.block") local Record = require("tirenvi.core.record") -local util = require("tirenvi.util.util") + +local util = require("tirenvi.util.util") -- Util local Range = require("tirenvi.util.range") local errors = require("tirenvi.util.errors") local notify = require("tirenvi.util.notify") local log = require("tirenvi.util.log") -local M = {} +-- ============================================================================= --- constants / defaults +local M = {} ------------------------------------------------------------------------ --- Utility ------------------------------------------------------------------------ +-- ============================================================================= +--#region Private ---@param self Blocks ---@param method string @@ -44,10 +38,6 @@ local function apply(self, method, ...) return result end ------------------------------------------------------------------------ --- Block construction ------------------------------------------------------------------------ - --- Split NDJSON records into plain/grid blocks. ---@param ndjsons Ndjson[] ---@return Blocks @@ -56,7 +46,7 @@ local function build_blocks_text_driven(ndjsons) ---@type Block local block = Block.new() local function flush_block() - if #(block.records) ~= 0 then + if #block.records ~= 0 then table.insert(self, block) end block = Block.new() @@ -87,7 +77,10 @@ local function build_blocks_attr_driven(ndjsons, attrs) local self = {} for iattr, attr in ipairs(attrs) do local block = Block.new() - Block.set_kind(block, Attr.is_plain(attr) and CONST.KIND.PLAIN or CONST.KIND.GRID) + Block.set_kind( + block, + Attr.is_plain(attr) and CONST.KIND.PLAIN or CONST.KIND.GRID + ) local has_continuation = false for irow = attr.range.first, attr.range.last do local record = ndjsons[irow] @@ -96,7 +89,10 @@ local function build_blocks_attr_driven(ndjsons, attrs) break end if record.kind == CONST.KIND.ATTR_FILE then - elseif record.kind == CONST.KIND.PLAIN or record.kind == CONST.KIND.GRID then + elseif + record.kind == CONST.KIND.PLAIN + or record.kind == CONST.KIND.GRID + then record = Record[record.kind].change_kind(record, block.kind) if record.kind == "grid" then Record.apply_column_count(record, #attr.columns) @@ -114,10 +110,6 @@ local function build_blocks_attr_driven(ndjsons, attrs) return self end ------------------------------------------------------------------------ --- Attribute handling ------------------------------------------------------------------------ - ---@alias RefAttrError ---| "conflict" ---| "grid in plain" @@ -130,13 +122,13 @@ local function rebuild_attr_range(bufblocks, first) block.attr = attr local last = first + #block.records - 1 attr.range = Range.from_lua(first, last) - first = last + 1 + first = last + 1 end end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@self Blocks ---@return Attr[] @@ -144,7 +136,7 @@ function M:collect_attrs() local attrs = {} for _, block in ipairs(self) do log.assert(block.attr, "block.attr is nil") - attrs[#attrs + 1] = block.attr + attrs[#attrs + 1] = Block[block.kind].get_attr(block) end return attrs end @@ -209,7 +201,7 @@ end function M.serialize(self) local ndjsons = {} for _, block in ipairs(self) do - util.extend(ndjsons, Block.serialize(block)) + util.extend(ndjsons, Block[block.kind].serialize(block)) end return ndjsons end diff --git a/lua/tirenvi/core/bufline.lua b/lua/tirenvi/core/bufline.lua deleted file mode 100644 index 5cd67b45..00000000 --- a/lua/tirenvi/core/bufline.lua +++ /dev/null @@ -1,252 +0,0 @@ -local Context = require("tirenvi.app.context") -local Cell = require("tirenvi.core.cell") -local config = require("tirenvi.config") -local buffer = require("tirenvi.io.buffer") -local CursorNvim = require("tirenvi.cursor.nvim") -local util = require("tirenvi.util.util") -local Range = require("tirenvi.util.range") -local log = require("tirenvi.util.log") - -local M = {} - -local api = vim.api --- private helpers - ----@param line string ----@return string -local function remove_start_pipe(line) - local pipen = config.marks.pipe - local pipec = config.marks.pipec - if util.start_with(line, pipen) then - line = line:sub(#pipen + 1) - elseif util.start_with(line, pipec) then - line = line:sub(#pipec + 1) - end - return line -end - ----@param line string ----@return string -local function remove_end_pipe(line) - local pipen = config.marks.pipe - local pipec = config.marks.pipec - if util.end_with(line, pipen) then - line = line:sub(1, - #pipen - 1) - elseif util.end_with(line, pipec) then - line = line:sub(1, - #pipec - 1) - end - return line -end - ----@param base_pipe boolean ----@param target string|nil ----@return boolean -local function is_block_boundary(base_pipe, target) - if not target then - return true - end - return base_pipe ~= (M.get_pipe_char(target) ~= nil) -end - ----@param provider LineProvider ----@param irow integer ----@param step integer -- -1 or 1 ----@return integer -local function find_block_edge(provider, irow, step) - local line = provider.get_line(irow) - local base_pipe = (M.get_pipe_char(line) ~= nil) - while true do - irow = irow + step - local line = provider.get_line(irow) - if is_block_boundary(base_pipe, line) then - return irow - step - end - end -end - ----@param line string ----@param pipe string ----@return integer[] -local function get_pipe_byte_position(line, pipe) - local indexes = {} - local index = 1 - while index <= #line do - if line:sub(index, index + #pipe - 1) == pipe then - indexes[#indexes + 1] = index - index = index + #pipe - else - index = index + 1 - end - end - if #indexes > 0 then - if indexes[1] ~= 1 then - table.insert(indexes, 1, 0) - end - end - return indexes -end - --- public API - ----@param line string ----@return integer[] -function M.get_pipe_byte_position(line) - local pipen = config.marks.pipe - local pipec = config.marks.pipec - local indexes = get_pipe_byte_position(line, pipen) - if #indexes == 0 then - indexes = get_pipe_byte_position(line, pipec) - end - return indexes -end - ----@param byte_pos integer[] ----@param icol integer ----@return integer|nil -function M.get_current_col_index(byte_pos, icol) - for index, ibyte in ipairs(byte_pos) do - if icol < ibyte then - return index - 1 - end - end - return nil -end - ----@param ctx Context ----@param line_provider LineProvider ----@param irow integer ----@return integer -function M.get_block_top_nrow(ctx, line_provider, irow) - if Context.is_allow_plain(ctx) then - return find_block_edge(line_provider, irow, -1) - else - return 1 - end -end - ----@param ctx Context ----@param line_provider LineProvider ----@param irow integer ----@return integer -function M.get_block_bottom_nrow(ctx, line_provider, irow) - if Context.is_allow_plain(ctx) then - return find_block_edge(line_provider, irow, 1) - else - return buffer.line_count(ctx.bufnr) - end -end - ----@param line string ----@return string[] -function M.get_cells(line) - local pipen = config.marks.pipe - local pipec = config.marks.pipec - line = remove_start_pipe(line) - line = remove_end_pipe(line) - line = line:gsub(vim.pesc(pipec), pipen) - return vim.split(line, pipen, { plain = true }) -end - ----@param line string ----@param pipe string ----@return boolean -function M.is_normal_grid(line, pipe) - if not util.start_with(line, pipe) then - return false - end - if not util.end_with(line, pipe) then - return false - end - return true -end - ----@param line string ----@return integer[] -function M.get_widths(line) - return M.get_max_widths(line, true) -end - ----@param line string ----@param no_wrap boolean|nil ----@return integer[] -function M.get_max_widths(line, no_wrap) - local cells = M.get_cells(line) - local widths = Cell.get_max_widths(cells, no_wrap) - return widths -end - ----@param line string|nil ----@return string|nil -function M.get_pipe_char(line) - local pipen = config.marks.pipe - local pipec = config.marks.pipec - if not line then - return nil - end - if line:find(pipen, 1, true) then - return pipen - end - if line:find(pipec, 1, true) then - return pipec - end - return nil -end - ----@param lines string[] ----@return boolean -function M.has_pipe(lines) - for _, line in ipairs(lines) do - if M.get_pipe_char(line) then - return true - end - end - return false -end - ----@param line string|nil ----@return boolean -function M.is_continue_line(line) - local pipec = config.marks.pipec - if not line then - return false - end - return M.get_pipe_char(line) == pipec -end - ----@param ctx Context ----@param count integer ----@param is_around boolean ----@return Rect|nil ----@return string[] -function M.get_block_rect(ctx, count, is_around) - local cursor = CursorNvim.capture(ctx) - local row_cur = cursor.row_cur - local col_byte = cursor.col_byte - local cline = ctx.line_provider.get_line(row_cur) or "" - local cbyte_pos = M.get_pipe_byte_position(cline) - if #cbyte_pos == 0 then - return nil, {} - end - local colIndex = M.get_current_col_index(cbyte_pos, col_byte) - if not colIndex then - return nil, {} - end - local trow = M.get_block_top_nrow(ctx, ctx.line_provider, row_cur) - local brow = M.get_block_bottom_nrow(ctx, ctx.line_provider, row_cur) - local lines = ctx.line_provider.get_lines(trow, brow) - local tline = lines[1] - local bline = lines[#lines] - local tbyte_pos = M.get_pipe_byte_position(tline) - local bbyte_pos = M.get_pipe_byte_position(bline) - local end_index = colIndex + count - end_index = math.min(end_index, #bbyte_pos) - local pipe = M.get_pipe_char(tline) - local rect = { - row = Range.from_lua(trow, brow), - col = Range.from_lua(tbyte_pos[colIndex] + (is_around and 0 or #pipe), - bbyte_pos[end_index] - 1), - } - return rect, lines -end - -return M diff --git a/lua/tirenvi/core/cell.lua b/lua/tirenvi/core/cell.lua index 9e7a94a1..4e1faf29 100644 --- a/lua/tirenvi/core/cell.lua +++ b/lua/tirenvi/core/cell.lua @@ -1,185 +1,188 @@ -local config = require("tirenvi.config") -local util = require("tirenvi.util.util") +local fn = vim.fn -- Neovim + +local config = require("tirenvi.config") -- Root + +local util = require("tirenvi.util.util") -- Util local log = require("tirenvi.util.log") +-- ============================================================================= + local M = {} --- constants / defaults M.MIN_WIDTH = 2 -local fn = vim.fn + +-- ============================================================================= +--#region Private + local padding = config.marks.padding local escaped_padding = vim.pesc(padding) local lf = config.marks.lf ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - ---@param self Cell ---@return integer local function display_width(self) - if not self:find("[\t\128-\255]") then - return #self - end - return fn.strdisplaywidth(self) + if not self:find("[\t\128-\255]") then + return #self + end + return fn.strdisplaywidth(self) end ---@param cells string[] ---@param ncol integer ---@return string[][] local function to_2d(cells, ncol) - local width = ncol + 1 - local result = {} - local row = {} - for _, cell in ipairs(cells) do - table.insert(row, cell) - if #row == width then - table.insert(result, row) - row = {} - end - end - if #row > 0 then - table.insert(result, row) - end - return result + local width = ncol + 1 + local result = {} + local row = {} + for _, cell in ipairs(cells) do + table.insert(row, cell) + if #row == width then + table.insert(result, row) + row = {} + end + end + if #row > 0 then + table.insert(result, row) + end + return result end local function get_delim_from_cell(cell) - if cell == "" then - return "" - elseif cell:match("^ +$") then - return " " - else - return nil - end + if cell == "" then + return "" + elseif cell:match("^ +$") then + return " " + else + return nil + end end ---@param cell2d string[][] ---@return string|nil local function get_delim(cell2d) - local ncol = #cell2d[1] - 1 - local ncolp1 = ncol + 1 - if #cell2d[#cell2d] ~= ncol then - return nil - end - local delm = get_delim_from_cell(cell2d[1][ncolp1]) - for irow = 2, #cell2d - 1 do - if delm ~= get_delim_from_cell(cell2d[irow][ncolp1]) then - return nil - end - end - return delm + local ncol = #cell2d[1] - 1 + local ncolp1 = ncol + 1 + if #cell2d[#cell2d] ~= ncol then + return nil + end + local delm = get_delim_from_cell(cell2d[1][ncolp1]) + for irow = 2, #cell2d - 1 do + if delm ~= get_delim_from_cell(cell2d[irow][ncolp1]) then + return nil + end + end + return delm end ---@param cells Cell[] ---@param ncol integer ---@return Cell[]|nil local function join(cells, ncol) - local cells2d = to_2d(cells, ncol) - local delim = get_delim(cells2d) - if not delim then - return nil - end - local join_row = vim.list_slice(cells2d[1], 1, ncol) - for irow = 2, #cells2d do - for icol = 1, ncol do - join_row[icol] = join_row[icol] .. delim .. cells2d[irow][icol] - end - end - return join_row + local cells2d = to_2d(cells, ncol) + local delim = get_delim(cells2d) + if not delim then + return nil + end + local join_row = vim.list_slice(cells2d[1], 1, ncol) + for irow = 2, #cells2d do + for icol = 1, ncol do + join_row[icol] = join_row[icol] .. delim .. cells2d[irow][icol] + end + end + return join_row end -local lf_len = display_width(lf) - ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ + +local lf_len = display_width(lf) ---@param cells Cell[] ---@param no_wrap boolean|nil ---@return integer[] function M.get_max_widths(cells, no_wrap) - local widths = {} - for _, cell in ipairs(cells) do - local width = M.get_max_width(cell, no_wrap) - widths[#widths + 1] = width - end - return widths + local widths = {} + for _, cell in ipairs(cells) do + local width = M.get_max_width(cell, no_wrap) + widths[#widths + 1] = width + end + return widths end ---@param self Cell ---@param no_wrap boolean|nil ---@return integer function M.get_max_width(self, no_wrap) - no_wrap = no_wrap or false - if not self then - return 0 - end - local cells = { self } - if not no_wrap then - if not util.end_with(self, config.marks.padding) then - cells = vim.split(self, lf) - end - end - local max_width = 0 - for icell, cell in pairs(cells) do - local width = display_width(cell) - if icell ~= #cells then - width = width + lf_len - end - max_width = math.max(max_width, width) - end - return max_width + no_wrap = no_wrap or false + if not self then + return 0 + end + local cells = { self } + if not no_wrap then + if not util.end_with(self, config.marks.padding) then + cells = vim.split(self, lf) + end + end + local max_width = 0 + for icell, cell in pairs(cells) do + local width = display_width(cell) + if icell ~= #cells then + width = width + lf_len + end + max_width = math.max(max_width, width) + end + return max_width end ---@param cells Cell[] ---@param ncol integer:w function M.normalize(cells, ncol) - for index = 1, ncol do - local cell = cells[index] - if cell == nil then - cells[index] = "" - elseif type(cell) ~= "string" then - cells[index] = tostring(cell) - end - end + for index = 1, ncol do + local cell = cells[index] + if cell == nil then + cells[index] = "" + elseif type(cell) ~= "string" then + cells[index] = tostring(cell) + end + end end ---@param cells Cell[] ---@param ncol integer ---@return Cell[] function M.merge_tail(cells, ncol) - if #cells <= ncol then - return cells - end - local join_cells = join(cells, ncol) - if join_cells then - return join_cells - else - cells[ncol] = table.concat(cells, " ", ncol) - return vim.list_slice(cells, 1, ncol) - end + if #cells <= ncol then + return cells + end + local join_cells = join(cells, ncol) + if join_cells then + return join_cells + else + cells[ncol] = table.concat(cells, " ", ncol) + return vim.list_slice(cells, 1, ncol) + end end ---@param self Cell ---@param target_width integer ---@return string function M:fill_padding(target_width) - if target_width == nil then - return self - end - local width = display_width(self) - local diff = target_width - width - if diff <= 0 then - return self - end - return self .. string.rep(padding, diff) + if target_width == nil then + return self + end + local width = display_width(self) + local diff = target_width - width + if diff <= 0 then + return self + end + return self .. string.rep(padding, diff) end ---@param self Cell ---@return string function M:remove_padding() - return (self:gsub(escaped_padding, "")) + return (self:gsub(escaped_padding, "")) end ---@param self Cell @@ -187,33 +190,33 @@ end ---@param _has_continuation boolean ---@return Cell[] function M:wrap(width, _has_continuation) - local cells = {} - if not self or self == "" or width <= 0 then - return { self } - end - local chars = util.utf8_chars(self) - local current = "" - local current_width = 0 - for _, char in ipairs(chars) do - local ch_width = display_width(char) - if current ~= "" and current_width + ch_width > width then - cells[#cells + 1] = current - current = char - current_width = ch_width - else - current = current .. char - current_width = current_width + ch_width - end - if char == lf then - cells[#cells + 1] = current - current = "" - current_width = 0 - end - end - if not (current == "" and _has_continuation) then - cells[#cells + 1] = current - end - return cells + local cells = {} + if not self or self == "" or width <= 0 then + return { self } + end + local chars = util.utf8_chars(self) + local current = "" + local current_width = 0 + for _, char in ipairs(chars) do + local ch_width = display_width(char) + if current ~= "" and current_width + ch_width > width then + cells[#cells + 1] = current + current = char + current_width = ch_width + else + current = current .. char + current_width = current_width + ch_width + end + if char == lf then + cells[#cells + 1] = current + current = "" + current_width = 0 + end + end + if not (current == "" and _has_continuation) then + cells[#cells + 1] = current + end + return cells end return M diff --git a/lua/tirenvi/core/cursor.lua b/lua/tirenvi/core/cursor.lua new file mode 100644 index 00000000..b4908fe7 --- /dev/null +++ b/lua/tirenvi/core/cursor.lua @@ -0,0 +1,30 @@ +-- ============================================================================= + +---@class CursorTir +---@field restore_mode "none"|"buffer"|"tir" +---@field iblock integer -- current block index (1-based) +---@field irow integer -- current row index (1-based) +---@field icol integer -- current column index (1-based) +---@field col_offset integer -- current column offset (1-based) +local M = {} + +-- ============================================================================= +-- Public API + +---@param iblock integer +---@param irow integer +---@param icol integer +---@param offset integer +---@return CursorTir +function M.new(iblock, irow, icol, offset) + ---@type CursorTir + return { + restore_mode = "none", + iblock = iblock, + irow = irow, + icol = icol, + col_offset = offset, + } +end + +return M diff --git a/lua/tirenvi/core/dirty_range.lua b/lua/tirenvi/core/dirty_range.lua deleted file mode 100644 index 8936acc7..00000000 --- a/lua/tirenvi/core/dirty_range.lua +++ /dev/null @@ -1,185 +0,0 @@ -local Attr = require("tirenvi.core.attr") -local Attrs = require("tirenvi.core.attrs") -local Bufline = require("tirenvi.core.bufline") -local Range3 = require("tirenvi.util.range3") -local Range = require("tirenvi.util.range") -local util = require("tirenvi.util.util") -local log = require("tirenvi.util.log") - -local M = {} - --- constants / defaults - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - ----@param line_provider LineProvider ----@param range Range -local function expand_continue_lines(line_provider, range) - if Range.is_empty(range) then - return - end - local first, last = Range.to_lua(range) - local lines = line_provider.get_lines(first, last) - local prev = range.first - 1 - local prev_line = line_provider.get_line(prev) - while Bufline.is_continue_line(prev_line) do - prev = prev - 1 - prev_line = line_provider.get_line(prev) - end - range.first = prev + 1 - ---@type string|nil - local last_line = lines[#lines] - local last = range.last - while Bufline.is_continue_line(last_line) do - last = last + 1 - last_line = line_provider.get_line(last) - end - range.last = last -end - ----@param line_provider LineProvider ----@param range3 Range3 ----@return Range -local function get_new_range(line_provider, range3) - local new_range = Range3.get_new_range(range3) - expand_continue_lines(line_provider, new_range) - return new_range -end - ----@param line_provider LineProvider ----@param prev_ranges Range[] ----@param range3 Range3 ----@return Range[] -local function adjust(line_provider, prev_ranges, range3) - local ranges1, _, ranges3 = Range.split(prev_ranges, Range.from_lua(range3.first, range3.last)) - Range.shift(ranges3, Range3.get_delta(range3)) - local range2 = get_new_range(line_provider, range3) - local new_ranges = ranges1 - if not Range.is_empty(range2) then - new_ranges[#new_ranges + 1] = range2 - end - util.extend(new_ranges, ranges3) - return new_ranges -end - ----@param attr Attr|nil ----@param line string|nil ----@return boolean -local function is_valid(attr, line) - if not line then - return true - end - if not attr then - return false - end - local pipe = Bufline.get_pipe_char(line) - if not pipe then - return Attr.is_plain(attr) - end - if not Bufline.is_normal_grid(line, pipe) then - return false - end - local widths = Bufline.get_widths(line) - if #attr.columns ~= #widths then - return false - end - for icol, width in ipairs(widths) do - if attr.columns[icol].width ~= width then - return false - end - end - return true -end - ----@param next_attr Attr|nil ----@param line string|nil ----@return boolean -local function is_valid_bound(next_attr, line) - if not line then - return true - end - if not next_attr or Attr.is_plain(next_attr) then - return true - end - local pipe = Bufline.get_pipe_char(line) - if not pipe then - return true - end - if not Bufline.is_normal_grid(line, pipe) then - return false - end - local widths = Bufline.get_widths(line) - if #next_attr.columns ~= #widths then - return false - end - for icol, width in ipairs(widths) do - if next_attr.columns[icol].width ~= width then - return false - end - end - return true -end - ----@param line_provider LineProvider ----@param range Range ----@param attrs Attr[] ----@return Range[] -local function check_dirty_1range(line_provider, range, attrs) - local inv_ranges = {} - for row_cur = range.first, range.last do - local attr = Attrs.get(attrs, row_cur) - local line = line_provider.get_line(row_cur) - if not is_valid(attr, line) then - Range.push(inv_ranges, row_cur) - elseif Attr.is_grid(attr) then - attr = Attrs.get(attrs, row_cur + 1) - if not is_valid_bound(attr, line) then - Range.push(inv_ranges, row_cur) - end - end - end - return inv_ranges -end - ----@param line_provider LineProvider ----@param new_ranges Range[] ----@param attrs Attr[] ----@return Range[] -local function check_dirty(line_provider, new_ranges, attrs) - local inv_ranges = {} - for _, range in ipairs(new_ranges) do - local ranges = check_dirty_1range(line_provider, range, attrs) - util.extend(inv_ranges, ranges) - end - return inv_ranges -end - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param line_provider LineProvider ----@param prev_ranges Range[] ----@param attrs Attr[] ----@param range3 Range3 ----@return Range[] -function M.reconcile(line_provider, prev_ranges, attrs, range3) - local new_ranges = adjust(line_provider, prev_ranges, range3) - local inv_ranges = check_dirty(line_provider, new_ranges, attrs) - return Range.merge(inv_ranges) -end - ----@param prev_ranges Range[] ----@param range3 Range3 ----@return Range[] -function M.remove(prev_ranges, range3) - local ranges1, _, ranges3 = Range.split(prev_ranges, Range.from_lua(range3.first, range3.last)) - Range.shift(ranges3, Range3.get_delta(range3)) - local new_ranges = ranges1 - util.extend(new_ranges, ranges3) - return Range.merge(new_ranges) -end - -return M diff --git a/lua/tirenvi/core/document.lua b/lua/tirenvi/core/document.lua index 559af6bd..6e2f9eea 100644 --- a/lua/tirenvi/core/document.lua +++ b/lua/tirenvi/core/document.lua @@ -1,20 +1,21 @@ -local CONST = require("tirenvi.constants") -local Attr = require("tirenvi.core.attr") +local CONST = require("tirenvi.constants") -- Root + +local Attr = require("tirenvi.core.attr") -- Core local Attrs = require("tirenvi.core.attrs") local Blocks = require("tirenvi.core.blocks") local Block = require("tirenvi.core.block") -local Range = require("tirenvi.util.range") + +local Range = require("tirenvi.util.range") -- Util local util = require("tirenvi.util.util") local log = require("tirenvi.util.log") -local M = {} - --- constants / defaults +-- ============================================================================= ---@class Document ---@field _tir boolean ---@field attr Attr_doc ---@field blocks Blocks +local M = {} ---@alias Blocks Block[] @@ -48,6 +49,7 @@ local M = {} ---@field wrap_mode WrapMode|nil ---@field fit_span integer ---@field columns Attr_column[] +---@field prefix string|nil ---@class Attr_column ---@field width integer display width (logical column width) @@ -60,6 +62,7 @@ local M = {} ---@class Record_grid ---@field kind "grid" +---@field prefix string|nil ---@field row Cell[] ---@field _has_continuation? boolean true if this record continues to the next row @@ -67,80 +70,87 @@ local M = {} ---@alias Ndjson Attr_file|Record -local VERSION = "tir/0.1" +-- ============================================================================= +--#region Private --- private helpers +local VERSION = "tir/0.1" ---@param ndjsons Record[] ---@param allow_plain boolean ---@param attrs Attr[]|nil ---@return Document local function new(ndjsons, allow_plain, attrs) - local self = {} - self.attr = { allow_plain = allow_plain } - self.blocks = Blocks.new_from_records(ndjsons, attrs) - return self + local self = {} + self.attr = { allow_plain = allow_plain } + self.blocks = Blocks.new_from_records(ndjsons, attrs) + return self end ---@return Ndjson local function new_attr_file() - return { kind = CONST.KIND.ATTR_FILE, version = VERSION } + return { kind = CONST.KIND.ATTR_FILE, version = VERSION } end ---@param self Document ---@return Attr[] local function collect_attrs(self) - return Blocks.collect_attrs(self.blocks) + return Blocks.collect_attrs(self.blocks) end ---@param bufdoc Document local function set_attr_range(bufdoc, first) - Blocks.set_attr_range(bufdoc.blocks, first) + Blocks.set_attr_range(bufdoc.blocks, first) end ---@param bufdoc Document ---@param attrs Attr[] local function apply_attrs_by_range(bufdoc, attrs) - log.assert(not bufdoc._tir, "apply_attrs_by_range should be called only for bufdoc") - Blocks.apply_attrs_by_range(bufdoc.blocks, attrs) + log.assert( + not bufdoc._tir, + "apply_attrs_by_range should be called only for bufdoc" + ) + Blocks.apply_attrs_by_range(bufdoc.blocks, attrs) end ---@param tirdoc Document ---@param c_attrs Attr[] local function apply_attrs_by_id(tirdoc, c_attrs) - log.assert(tirdoc._tir, "apply_attrs_by_id should be called only for tirdoc") - local attrs_grid = Attrs.get_grid_attrs(c_attrs) - local iblock = 0 - for _, block in ipairs(tirdoc.blocks) do - block.attr = Attr[block.kind].new() - if block.kind == CONST.KIND.GRID then - iblock = iblock + 1 - if attrs_grid[iblock] then - block.attr = attrs_grid[iblock] - end - end - end + log.assert( + tirdoc._tir, + "apply_attrs_by_id should be called only for tirdoc" + ) + local attrs_grid = Attrs.get_grid_attrs(c_attrs) + local iblock = 0 + for _, block in ipairs(tirdoc.blocks) do + block.attr = Attr[block.kind].new() + if block.kind == CONST.KIND.GRID then + iblock = iblock + 1 + if attrs_grid[iblock] then + block.attr = attrs_grid[iblock] + end + end + end end ---@param tirdoc Document local function to_flatdoc(tirdoc) - log.assert(tirdoc._tir, "to_flatdoc should be called only for tirdoc") - Blocks.to_flat(tirdoc.blocks) - tirdoc._tir = true + log.assert(tirdoc._tir, "to_flatdoc should be called only for tirdoc") + Blocks.to_flat(tirdoc.blocks) + tirdoc._tir = true end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param ndjsons Ndjson[] ---@param allow_plain boolean ---@return Document function M.new_tirdoc(ndjsons, allow_plain) - local tirdoc = new(ndjsons, allow_plain) - Blocks.from_flat(tirdoc.blocks) - tirdoc._tir = true - return tirdoc + local tirdoc = new(ndjsons, allow_plain) + Blocks.from_flat(tirdoc.blocks) + tirdoc._tir = true + return tirdoc end ---@param records Record[] @@ -149,10 +159,10 @@ end ---@param first integer|nil ---@return Document function M.new_bufdoc(records, allow_plain, attrs, first) - local bufdoc = new(records, allow_plain, attrs) - set_attr_range(bufdoc, first) - bufdoc._tir = false - return bufdoc + local bufdoc = new(records, allow_plain, attrs) + set_attr_range(bufdoc, first) + bufdoc._tir = false + return bufdoc end ---@param bufdoc Document @@ -160,39 +170,50 @@ end -- Prevents line count changes that would break put(); used for repair. ---@return Document function M.from_bufdoc(bufdoc, no_normalize) - log.assert(not bufdoc._tir, "from_bufdoc should be called only for bufdoc") - no_normalize = no_normalize or false - local tirdoc = vim.deepcopy(bufdoc) - Blocks.from_buf(tirdoc.blocks, no_normalize) - tirdoc._tir = true - return tirdoc + log.assert(not bufdoc._tir, "from_bufdoc should be called only for bufdoc") + no_normalize = no_normalize or false + local tirdoc = vim.deepcopy(bufdoc) + Blocks.from_buf(tirdoc.blocks, no_normalize) + tirdoc._tir = true + return tirdoc end ---@param tirdoc Document ---@return Document function M.to_bufdoc(tirdoc) - log.assert(tirdoc._tir, "to_bufdoc should be called only for tirdoc") - local bufdoc = vim.deepcopy(tirdoc) - Blocks.to_bufdoc(bufdoc.blocks) - bufdoc._tir = false - return bufdoc + log.assert(tirdoc._tir, "to_bufdoc should be called only for tirdoc") + local bufdoc = vim.deepcopy(tirdoc) + Blocks.to_bufdoc(bufdoc.blocks) + bufdoc._tir = false + return bufdoc +end + +---@param tirfdoc Document +function M.prefix_to_attrs(tirfdoc) + --Blocks.prefix_to_attrs(tirfdoc.blocks) end ---@param bufdoc Document ---@return Record[] function M.serialize_to_buf(bufdoc) - log.assert(not bufdoc._tir, "serialize_to_buf should be called only for bufdoc") - return Blocks.serialize(bufdoc.blocks) + log.assert( + not bufdoc._tir, + "serialize_to_buf should be called only for bufdoc" + ) + return Blocks.serialize(bufdoc.blocks) end ---@param tirdoc Document ---@return Ndjson[] function M.serialize_to_flat(tirdoc) - log.assert(tirdoc._tir, "serialize_to_flat should be called only for tirdoc") - to_flatdoc(tirdoc) - local ndjsons = { new_attr_file() } - util.extend(ndjsons, Blocks.serialize(tirdoc.blocks)) - return ndjsons + log.assert( + tirdoc._tir, + "serialize_to_flat should be called only for tirdoc" + ) + to_flatdoc(tirdoc) + local ndjsons = { new_attr_file() } + util.extend(ndjsons, Blocks.serialize(tirdoc.blocks)) + return ndjsons end ---@param bufdoc Document @@ -200,73 +221,99 @@ end ---@param chached_attrs Attr[] ---@return Attr[] function M.replace_attrs(bufdoc, range, chached_attrs) - log.assert(not bufdoc._tir, "replace_attrs should be called only for bufdoc") - local first = Range.to_lua(range) - set_attr_range(bufdoc, first) - local doc_attrs = Blocks.collect_attrs(bufdoc.blocks) - if not chached_attrs or Range.is_whole(range) then - return doc_attrs - end - return Attrs.replace_attrs(chached_attrs, range, doc_attrs) + log.assert( + not bufdoc._tir, + "replace_attrs should be called only for bufdoc" + ) + local first = Range.to_lua(range) + set_attr_range(bufdoc, first) + local doc_attrs = Blocks.collect_attrs(bufdoc.blocks) + if not chached_attrs or Range.is_whole(range) then + return doc_attrs + end + return Attrs.replace_attrs(chached_attrs, range, doc_attrs) end ---@param bufdoc Document function M.infer_consistent_attr(bufdoc) - log.assert(not bufdoc._tir, "infer_consistent_attr should be called only for bufdoc") - Blocks.infer_consistent_attr(bufdoc.blocks) + log.assert( + not bufdoc._tir, + "infer_consistent_attr should be called only for bufdoc" + ) + Blocks.infer_consistent_attr(bufdoc.blocks) end ---@param bufdoc Document function M.set_max_attr(bufdoc) - --log.assert(not bufdoc._tir, "set_auto_attr should be called only for bufdoc") - Blocks.set_max_attr(bufdoc.blocks) - log.watch("ATTR", M.debug_attrs(bufdoc, "[5]MAX ATTR:")) + --log.assert(not bufdoc._tir, "set_auto_attr should be called only for bufdoc") + Blocks.set_max_attr(bufdoc.blocks) + log.watch("ATTR", M.debug_attrs(bufdoc, "[5]MAX ATTR:")) end ---@param doc Document ---@param attrs Attr[] function M.apply_attrs(doc, attrs) - if doc._tir then - apply_attrs_by_id(doc, attrs) - else - apply_attrs_by_range(doc, attrs) - end + if not attrs or #attrs == 0 then + return + end + if doc._tir then + apply_attrs_by_id(doc, attrs) + else + apply_attrs_by_range(doc, attrs) + end end ---@param self Document function M:debug_attrs(title) - if not log.is_debug() then - return - end - local attrs = collect_attrs(self) - return Attrs.debug_attrs(attrs, title) + if not log.is_debug() then + return + end + local attrs = collect_attrs(self) + return Attrs.debug_attrs(attrs, title) end ---@param bufdoc Document ---@param attrs Attr[] ---@param range3 Range3 function M.inherit_neighbor_attr(bufdoc, attrs, range3) - log.assert(not bufdoc._tir, "inherit_neighbor_attr should be called only for bufdoc") - if #bufdoc.blocks == 0 then - return - end - local block = bufdoc.blocks[1] - Block[block.kind].inherit_neighbor_attr(block, attrs, range3.first - 1) - block = bufdoc.blocks[#bufdoc.blocks] - Block[block.kind].inherit_neighbor_attr(block, attrs, range3.last + 1) + log.assert( + not bufdoc._tir, + "inherit_neighbor_attr should be called only for bufdoc" + ) + if #bufdoc.blocks == 0 then + return + end + local block = bufdoc.blocks[1] + Block[block.kind].inherit_neighbor_attr(block, attrs, range3.first - 1) + block = bufdoc.blocks[#bufdoc.blocks] + Block[block.kind].inherit_neighbor_attr(block, attrs, range3.last + 1) end ---@param bufdoc Document function M.insert_empty_lines(bufdoc) - log.assert(not bufdoc._tir, "insert_empty_lines should be called only for bufdoc") - Blocks.insert_empty_lines(bufdoc.blocks) + log.assert( + not bufdoc._tir, + "insert_empty_lines should be called only for bufdoc" + ) + Blocks.insert_empty_lines(bufdoc.blocks) end ---@param bufdoc Document ---@return boolean function M.has_width(bufdoc) - log.assert(not bufdoc._tir, "has_width should be called only for bufdoc") - return Blocks.has_width(bufdoc.blocks) + log.assert(not bufdoc._tir, "has_width should be called only for bufdoc") + return Blocks.has_width(bufdoc.blocks) +end + +---@param self Document +---@return boolean +function M.has_grid(self) + for _, block in ipairs(self.blocks) do + if block.kind == CONST.KIND.GRID then + return true + end + end + return false end return M diff --git a/lua/tirenvi/core/record.lua b/lua/tirenvi/core/record.lua index 8c02b293..6cd1078d 100644 --- a/lua/tirenvi/core/record.lua +++ b/lua/tirenvi/core/record.lua @@ -1,198 +1,141 @@ -local config = require("tirenvi.config") -local CONST = require("tirenvi.constants") -local Cell = require("tirenvi.core.cell") -local Bufline = require("tirenvi.core.bufline") -local log = require("tirenvi.util.log") +local CONST = require("tirenvi.constants") -- Root -local M = {} -M.plain = {} -M.grid = {} +local Cell = require("tirenvi.core.cell") -- Core --- constants / defaults +local log = require("tirenvi.util.log") -- Util ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= ----@param bufline string ----@return Record -local function from_bufline(bufline) - local pipec = config.marks.pipec - local pipe = Bufline.get_pipe_char(bufline) - if pipe then - return M.grid.new_from_bufline(bufline, pipe == pipec) - else - return M.plain.new_from_bufline(bufline) - end -end +local M = {} +M.plain = {} +M.grid = {} ------------------------------------------------------------------------ +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param self Record_grid ---@param ncol integer function M:apply_column_count(ncol) - self.row = self.row or {} - Cell.normalize(self.row, ncol) - self.row = Cell.merge_tail(self.row, ncol) + self.row = self.row or {} + Cell.normalize(self.row, ncol) + self.row = Cell.merge_tail(self.row, ncol) end ---@param bufline string ---@return Record_plain -function M.plain.new_from_bufline(bufline) - return { kind = CONST.KIND.PLAIN, line = bufline } +function M.plain.new(bufline) + return { kind = CONST.KIND.PLAIN, line = bufline } end ---@param self Record_plain ---@return Record function M.plain:change_kind(kind) - if kind == CONST.KIND.PLAIN then - return self - else - return M.plain.to_grid(self) - end + if kind == CONST.KIND.PLAIN then + return self + else + return M.plain.to_grid(self) + end end ---@param self Record_plain ---@return Record_grid function M.plain:to_grid() - return M.grid.new({ self.line }) + return M.grid.new({ self.line }) end ---@param self Record_plain function M.plain:remove_padding() - self.line = Cell.remove_padding(self.line) + self.line = Cell.remove_padding(self.line) end ---@param cells Cell[]|nil ---@return Record_grid function M.grid.new(cells) - return { kind = CONST.KIND.GRID, row = cells or {} } -end - ----@param bufline string ----@param has_continuation boolean ----@return Record_grid -function M.grid.new_from_bufline(bufline, has_continuation) - bufline = bufline or "" - local cells = Bufline.get_cells(bufline) - local record = M.grid.new(cells) - record._has_continuation = has_continuation - return record + return { kind = CONST.KIND.GRID, row = cells or {} } end ---@param self Record_grid ---@return Record function M.grid:change_kind(kind) - if kind == CONST.KIND.PLAIN then - return { kind = CONST.KIND.PLAIN, line = table.concat(self.row, " ") } - else - return self - end + if kind == CONST.KIND.PLAIN then + return { kind = CONST.KIND.PLAIN, line = table.concat(self.row, " ") } + else + return self + end end ---@param self Record_grid ---@return Record_grid function M.grid:to_grid() - return self + return self end ---@param self Record_grid ---@param columns Attr_column function M.grid:fill_padding(columns) - for icol, cell in ipairs(self.row) do - self.row[icol] = Cell.fill_padding(cell, columns[icol].width) - end + for icol, cell in ipairs(self.row) do + self.row[icol] = Cell.fill_padding(cell, columns[icol].width) + end end ---@param self Record_grid function M.grid:remove_padding() - for icol, cell in ipairs(self.row) do - self.row[icol] = Cell.remove_padding(cell) - end + for icol, cell in ipairs(self.row) do + self.row[icol] = Cell.remove_padding(cell) + end end ---@param self Record_grid ---@param columns Attr_column[] ---@return Record_grid[] function M.grid:wrap(columns) - local records = {} - for icol, cell in ipairs(self.row) do - local cells = Cell.wrap(cell, columns[icol].width, self._has_continuation) - for irow, cell in ipairs(cells) do - records[irow] = records[irow] or M.grid.new() - records[irow].row[icol] = cell - end - end - local ncol = #self.row - for irow = 1, #records do - records[irow]._has_continuation = true - Cell.normalize(records[irow].row, ncol) - end - records[#records]._has_continuation = self._has_continuation - return records + local records = {} + for icol, cell in ipairs(self.row) do + local cells = + Cell.wrap(cell, columns[icol].width, self._has_continuation) + for irow, cell in ipairs(cells) do + records[irow] = records[irow] or M.grid.new() + records[irow].row[icol] = cell + records[irow].prefix = self.prefix + end + end + local ncol = #self.row + for irow = 1, #records do + records[irow]._has_continuation = true + Cell.normalize(records[irow].row, ncol) + end + records[#records]._has_continuation = self._has_continuation + return records end ---@param self Record_grid ---@param record Record_grid function M.grid:concat(record) - for icol, cell in ipairs(self.row) do - self.row[icol] = cell .. record.row[icol] - end + for icol, cell in ipairs(self.row) do + self.row[icol] = cell .. record.row[icol] + end end ---@param records Record_grid[] ---@return integer function M.get_max_ncol(records) - local max_col = 0 - for _, record in ipairs(records) do - max_col = math.max(max_col, #record.row) - end - return max_col -end - ----@param buflines string[] ----@return Record[] -function M.from_buflines(buflines) - local records = {} - for index = 1, #buflines do - records[index] = from_bufline(buflines[index]) - end - return records -end - ----@param records Record[] ----@return string[] -function M.to_buflines(records) - local pipec = config.marks.pipec - local pipen = config.marks.pipe - local buflines = {} - for _, record in ipairs(records) do - local kind = record.kind - if kind == CONST.KIND.PLAIN then - buflines[#buflines + 1] = record.line or "" - elseif kind == CONST.KIND.GRID then - local pipe = record._has_continuation and pipec or pipen - local row_items = record.row - local row = table.concat(row_items, pipe) - row = pipe .. row .. pipe - buflines[#buflines + 1] = row - end - end - return buflines + local max_col = 0 + for _, record in ipairs(records) do + max_col = math.max(max_col, #record.row) + end + return max_col end ---@param self Record_grid[] ---@return integer|nil function M.get_consistent_ncol(self) - local ncol = #self[1].row - for irecord = 2, #self do - if ncol ~= #self[irecord].row then - return nil - end - end - return ncol + local ncol = #self[1].row + for irecord = 2, #self do + if ncol ~= #self[irecord].row then + return nil + end + end + return ncol end return M diff --git a/lua/tirenvi/cursor/cursor_logical.lua b/lua/tirenvi/cursor/cursor_logical.lua deleted file mode 100644 index dfa70497..00000000 --- a/lua/tirenvi/cursor/cursor_logical.lua +++ /dev/null @@ -1,36 +0,0 @@ ----@class CursorLogical ----@field restore_mode "none"|"buffer"|"logical" ----@field iblock integer -- current block index (1-based) ----@field irow integer -- current row index (1-based) ----@field icol integer -- current column index (1-based) ----@field col_offset integer -- current column offset (1-based) - -local M = {} - --- constants / defaults - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param iblock integer ----@param irow integer ----@param icol integer ----@param offset integer ----@return CursorLogical -function M.new(iblock, irow, icol, offset) - ---@type CursorLogical - return { - restore_mode = "none", - iblock = iblock, - irow = irow, - icol = icol, - col_offset = offset, - } -end - -return M diff --git a/lua/tirenvi/cursor/nvim.lua b/lua/tirenvi/cursor/nvim.lua deleted file mode 100644 index ae12043c..00000000 --- a/lua/tirenvi/cursor/nvim.lua +++ /dev/null @@ -1,133 +0,0 @@ -local Cursor = require("tirenvi.cursor.cursor_buf") -local log = require("tirenvi.util.log") - -local M = {} - --- constants / defaults - -local api = vim.api -local fn = vim.fn -local bo = vim.bo -local b = vim.b - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - ----@param line string ----@param col_char integer ----@return integer -- byte index (1-based) -local function char_to_byte(line, col_char) - local nchar = vim.str_utfindex(line) + 1 - log.assert(col_char <= nchar, "col_char(%d) <= nchar(%d) : %s", col_char, nchar, line) - col_char = math.min(col_char, nchar) - -- str_byteindex(line, "utf-32", col_char - 1, false) - return vim.str_byteindex(line, col_char - 1) + 1 -end - ----@param cursor CursorBuf ----@param line string -local function complete(cursor, line) - cursor.line = line - cursor.col_char = vim.str_utfindex(line, cursor.col_byte - 1) + 1 - cursor.col_byte = char_to_byte(line, cursor.col_char) - cursor.char = fn.strcharpart(line, cursor.col_char - 1, 1) - local prefix = fn.strcharpart(line, 0, cursor.col_char - 1) - cursor.col_disp = fn.strdisplaywidth(prefix) + 1 -end - ----@param ctx Context ----@param row_cur integer ----@param col_byte integer ----@return CursorBuf -local function get_cursor(ctx, row_cur, col_byte) - local cursor = Cursor.new(row_cur, col_byte) - local line = ctx.line_provider.get_line(cursor.row_cur) or "" - complete(cursor, line) - return cursor -end - ----@param row_cur integer ----@param col_byte integer -local function restore_cursor(row_cur, col_byte) - local view = fn.winsaveview() - view.lnum = row_cur - view.col = col_byte - 1 - fn.winrestview(view) -end - ----@param col_disp integer ----@param line string ----@return integer -local function disp_to_byte(line, col_disp) - local nchar = vim.str_utfindex(line) - local disp = 1 - for ichar = 1, nchar do - local char = vim.fn.strcharpart(line, ichar - 1, 1) - local width = vim.fn.strdisplaywidth(char) - if col_disp < disp + width then - return char_to_byte(line, ichar) - end - disp = disp + width - end - return #line + 1 -end - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param ctx Context ----@return CursorBuf -function M.capture(ctx) - local row_cur, col_byte0 = unpack(api.nvim_win_get_cursor(ctx.winid)) - return get_cursor(ctx, row_cur, col_byte0 + 1) -end - ----@param line string ----@param row_cur integer ----@param col_disp integer ----@return CursorBuf -function M.from_col_disp(line, row_cur, col_disp) - local col_byte = disp_to_byte(line, col_disp) - return Cursor.new(row_cur, col_byte) -end - ----@param ctx Context -function M.reset(ctx) - local cursor = M.capture(ctx) - restore_cursor(cursor.row_cur, cursor.col_byte) -end - ---- Normal cursor movement. ---- Preserves window view and preferred column. ----@param ctx Context ----@param row_cur integer ----@param col_byte integer -function M.restore_byte(ctx, row_cur, col_byte) - local cursor = get_cursor(ctx, row_cur, col_byte) - restore_cursor(cursor.row_cur, cursor.col_byte) -end - ----@param ctx Context ----@param row_cur integer ----@param col_disp integer -function M.restore_disp(ctx, row_cur, col_disp) - local line = ctx.line_provider.get_line(row_cur) or "" - local cursor = M.from_col_disp(line, row_cur, col_disp) - M.restore_byte(ctx, row_cur, cursor.col_byte) -end - ---- Direct cursor update. ---- Use for Visual/Visual Block. ----@param ctx Context ----@param row_cur integer ----@param col_byte integer -function M.move_byte(ctx, row_cur, col_byte) - api.nvim_win_set_cursor(ctx.winid, { row_cur, math.max(0, col_byte - 1) }) -end - -function M.restore() -end - -return M diff --git a/lua/tirenvi/editor/autocmd.lua b/lua/tirenvi/editor/autocmd.lua index 2f558d73..308d07fb 100644 --- a/lua/tirenvi/editor/autocmd.lua +++ b/lua/tirenvi/editor/autocmd.lua @@ -1,31 +1,34 @@ --- dependencies -local config = require("tirenvi.config") -local Context = require("tirenvi.app.context") -local Bufline = require("tirenvi.core.bufline") -local reader = require("tirenvi.io.reader") -local buffer = require("tirenvi.io.buffer") -local init = require("tirenvi.init") -local buf_state = require("tirenvi.io.buf_state") +local api = vim.api -- Neovim + +local config = require("tirenvi.config") -- Root local ui = require("tirenvi.ui") -local guard = require("tirenvi.util.guard") -local Range3 = require("tirenvi.util.range3") + +local Debug = require("tirenvi.editor.debug") -- Editor +local guard = require("tirenvi.editor.guard") + +local app = require("tirenvi.app") -- App + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser + +local buf_lines = require("tirenvi.io.buf_lines") -- IO +local buf_state = require("tirenvi.io.buf_state") +local Context = require("tirenvi.io.context") + +local Range3 = require("tirenvi.util.range3") -- Util local Range = require("tirenvi.util.range") local log = require("tirenvi.util.log") -local Debug = require("tirenvi.editor.debug") --- module +-- ============================================================================= + local M = {} --- constants / defaults +-- ============================================================================= +--#region Private local GROUP_NAME = "tirenvi" -local api = vim.api - ----------------------------------------------------------------------- --- Event handlers (private) ----------------------------------------------------------------------- - +---@param bufnr number +---@return Context local function get_context(bufnr) return Context.from_buf(bufnr) end @@ -34,11 +37,11 @@ end local function recover_flat(bufnr, range3) -- local r_result = reader.read(get_context(bufnr), Range3.get_new_range(range3)) local first, last = Range.to_lua(Range3.get_new_range(range3)) - local lines = buffer.get_lines(bufnr, first, last) - if #lines ~= buffer.line_count(bufnr) then + local lines = buf_lines.get_lines(bufnr, first, last) + if #lines ~= buf_lines.line_count(bufnr) then return end - buf_state.set_buffer_flat(bufnr, not Bufline.has_pipe(lines)) + buf_state.set_buffer_tirbuf(bufnr, tir_buf.has_pipe(lines)) end ---@param _ string @@ -47,18 +50,21 @@ end ---@param range3 Range3 ---@param bytecount integer local function on_lines(_, bufnr, tick, range3, bytecount) - buffer.clear_cache() + buf_lines.clear_cache() recover_flat(bufnr, range3) - if buf_state.should_skip(bufnr) then return end + if buf_state.should_skip(bufnr) then + return + end local ctx = get_context(bufnr) Debug.ui_entry(bufnr, Range3.short(range3)) - init.on_lines(ctx, range3) + app.on_lines(ctx, range3) + app.check_and_repair(ctx, range3) Debug.ui_exit(bufnr, Range3.short(range3)) end ---@param bufnr number local function attach_on_lines(bufnr) - if buffer.get(bufnr, buffer.IKEY.ATTACHED) then + if buf_state.get(bufnr, buf_state.IKEY.ATTACHED) then return end log.debug("===+=== attach on_lines") @@ -68,48 +74,54 @@ local function attach_on_lines(bufnr) -- this handler. In this case, `on_detach` is NOT called automatically, so any -- state (e.g. ATTACHED flag) must be updated manually here. on_lines = function(_, bufnr, tick, first, last, new_last, bytecount) - if buffer.get(bufnr, buffer.IKEY.FILETYPE) == nil then + if buf_state.get(bufnr, buf_state.IKEY.PARSER) == nil then log.debug("===+=== auto detach (no filetype)") - buffer.set(bufnr, buffer.IKEY.ATTACHED, false) + buf_state.set(bufnr, buf_state.IKEY.ATTACHED, false) return true -- detach end - if buffer.get(bufnr, buffer.IKEY.PATCH_DEPTH) > 0 then + if buf_state.get(bufnr, buf_state.IKEY.PATCH_DEPTH) > 0 then return end - on_lines(_, bufnr, tick, Range3.new(first + 1, last, new_last), bytecount) + on_lines( + _, + bufnr, + tick, + Range3.new(first + 1, last, new_last), + bytecount + ) end, on_detach = function() log.debug("===+=== detach on_lines") - buffer.set(bufnr, buffer.IKEY.ATTACHED, false) + buf_state.set(bufnr, buf_state.IKEY.ATTACHED, false) end, }) - buffer.set(bufnr, buffer.IKEY.ATTACHED, true) + buf_state.set(bufnr, buf_state.IKEY.ATTACHED, true) end ---@param ctx Context local function on_insert_leave(ctx) - init.on_insert_leave(ctx) + app.check_and_repair(ctx) end ---@param ctx Context local function on_buf_read_post(ctx) - buffer.clear_buf_local(ctx.bufnr) - init.read_post(ctx) + buf_state.clear_buf_local(ctx.bufnr) + app.read_post(ctx) end ---@param ctx Context local function on_buf_write_pre(ctx) - init.write_pre(ctx) + app.write_pre(ctx) end ---@param ctx Context local function on_buf_write_post(ctx) - init.write_post(ctx) + app.write_post(ctx) end ---@param ctx Context local function on_insert_char_pre(ctx) - init.insert_char_in_newline(ctx) + app.insert_char_in_newline(ctx) end ---@param args table @@ -120,38 +132,43 @@ end ---@param args table local function on_cursor_moved(args) local ctx = Context.from_buf(args.buf) - Debug.show_attr_marks(ctx) - init.auto_wrap(ctx) + if vim.log.levels.DEBUG >= config.log.level then + Debug.show_attr_marks(ctx) + end + app.auto_wrap(ctx) end ---@param ctx Context local function on_filetype(ctx) - init.on_filetype(ctx) + app.on_filetype(ctx) end ---@param args table local function on_vim_leave(args) end ----------------------------------------------------------------------- --- Autocmd registration (private) ----------------------------------------------------------------------- - ----@param augroup integer ---@param bufnr number -local function clear_buffer_local_autocmds(augroup, bufnr) +local function clear_buffer_local_autocmds(bufnr) + buf_state.set(bufnr, buf_state.IKEY.AUTOCMD, false) + local augroup = api.nvim_create_augroup(GROUP_NAME, { clear = false }) api.nvim_clear_autocmds({ group = augroup, buffer = bufnr }) end ----@param augroup integer ---@param bufnr number -local function register_buffer_local_autocmds(augroup, bufnr) +local function register_buffer_local_autocmds(bufnr) + if buf_state.get(bufnr, buf_state.IKEY.AUTOCMD) then + return + end + buf_state.set(bufnr, buf_state.IKEY.AUTOCMD, true) + local augroup = api.nvim_create_augroup(GROUP_NAME, { clear = false }) attach_on_lines(bufnr) api.nvim_create_autocmd("BufWritePre", { group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf, { has_grid = true, }) then return end + if buf_state.should_skip(args.buf, { has_grid = true }) then + return + end Debug.ui_entry(args.buf, args.event) local ctx = get_context(args.buf) on_buf_write_pre(ctx) @@ -163,7 +180,7 @@ local function register_buffer_local_autocmds(augroup, bufnr) group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf, { is_tirbuf = false, }) then + if buf_state.should_skip(args.buf, { is_tirbuf = false }) then return end Debug.ui_entry(args.buf, args.event) @@ -177,7 +194,7 @@ local function register_buffer_local_autocmds(augroup, bufnr) group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf, { is_tirbuf = false, }) then + if buf_state.should_skip(args.buf, { is_tirbuf = false }) then return end on_cursor_hold(args) @@ -188,9 +205,6 @@ local function register_buffer_local_autocmds(augroup, bufnr) group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if vim.log.levels.DEBUG < config.log.level then - return - end if buf_state.should_skip(args.buf) then return end @@ -202,11 +216,15 @@ local function register_buffer_local_autocmds(augroup, bufnr) group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf) then return end + if buf_state.should_skip(args.buf) then + return + end Debug.ui_entry(args.buf, args.event) - log.assert(not buffer.get(args.buf, buffer.IKEY.INSERT_MODE), - "InsertEnter triggered while already in insert mode") - buffer.set(args.buf, buffer.IKEY.INSERT_MODE, true) + log.assert( + not buf_state.get(args.buf, buf_state.IKEY.INSERT_MODE), + "InsertEnter triggered while already in insert mode" + ) + buf_state.set(args.buf, buf_state.IKEY.INSERT_MODE, true) Debug.ui_exit(args.buf, args.event) end), }) @@ -215,13 +233,15 @@ local function register_buffer_local_autocmds(augroup, bufnr) group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf) then return end + if buf_state.should_skip(args.buf) then + return + end Debug.ui_entry(args.buf, args.event) local ctx = get_context(args.buf) -- InsertLeave may be triggered without a preceding InsertEnter -- due to the behavior of other plugins (e.g., Telescope). -- Do not assert insert_mode here. - buffer.set(args.buf, buffer.IKEY.INSERT_MODE, false) + buf_state.set(args.buf, buf_state.IKEY.INSERT_MODE, false) on_insert_leave(ctx) Debug.ui_exit(args.buf, args.event) end), @@ -231,7 +251,9 @@ local function register_buffer_local_autocmds(augroup, bufnr) group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf) then return end + if buf_state.should_skip(args.buf) then + return + end Debug.ui_entry(args.buf, args.event) local ctx = get_context(args.buf) on_insert_char_pre(ctx) @@ -239,11 +261,11 @@ local function register_buffer_local_autocmds(augroup, bufnr) end), }) - vim.api.nvim_create_autocmd({ "BufWinEnter", "WinEnter" }, { + api.nvim_create_autocmd({ "BufWinEnter", "WinEnter" }, { group = augroup, buffer = bufnr, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf, { is_tirbuf = false, }) then + if buf_state.should_skip(args.buf, { is_tirbuf = false }) then return end local ctx = Context.from_buf(bufnr) @@ -254,25 +276,26 @@ end local function register_autocmds() local augroup = api.nvim_create_augroup(GROUP_NAME, { clear = true }) - - vim.api.nvim_create_autocmd("FileType", { + api.nvim_create_autocmd("FileType", { group = augroup, callback = guard.guarded(function(args) local bufnr = args.buf - if buf_state.should_skip(bufnr, { + if + buf_state.should_skip(bufnr, { has_parser = false, is_tirbuf = false, - }) then + }) + then return end Debug.ui_entry(args.buf, args.event) local ctx = get_context(bufnr) on_filetype(ctx) - clear_buffer_local_autocmds(augroup, bufnr) - if buf_state.should_skip(args.buf, { is_tirbuf = false, }) then + clear_buffer_local_autocmds(bufnr) + if buf_state.should_skip(args.buf, { is_tirbuf = false }) then return end - register_buffer_local_autocmds(augroup, bufnr) + register_buffer_local_autocmds(bufnr) Debug.ui_exit(args.buf, args.event) end), }) @@ -280,7 +303,7 @@ local function register_autocmds() api.nvim_create_autocmd("BufReadPost", { group = augroup, callback = guard.guarded(function(args) - if buf_state.should_skip(args.buf, { is_tirbuf = false, }) then + if buf_state.should_skip(args.buf, { is_tirbuf = false }) then return end Debug.ui_entry(args.buf, args.event) @@ -290,7 +313,7 @@ local function register_autocmds() end), }) - vim.api.nvim_create_autocmd("WinClosed", { + api.nvim_create_autocmd("WinClosed", { group = augroup, callback = guard.guarded(function(args) local winid = tonumber(args.match) @@ -310,7 +333,8 @@ local function register_autocmds() if vim.g.tirenvi_test_mode == 1 then local ok, luacov = pcall(require, "luacov") if ok then - vim.api.nvim_create_autocmd("VimLeavePre", { + api.nvim_create_autocmd("VimLeavePre", { + group = augroup, callback = function(args) Debug.ui_entry(args.buf, args.event) luacov.save_stats() @@ -321,12 +345,20 @@ local function register_autocmds() end end ----------------------------------------------------------------------- +--#endregion +-- ============================================================================= -- Public API ----------------------------------------------------------------------- function M.setup() register_autocmds() end +function M.clear_buf_autocmds(bufnr) + clear_buffer_local_autocmds(bufnr) +end + +function M.register_buf_autocmd(bufnr) + register_buffer_local_autocmds(bufnr) +end + return M diff --git a/lua/tirenvi/editor/commands.lua b/lua/tirenvi/editor/commands.lua index 688f805c..7fe948bc 100644 --- a/lua/tirenvi/editor/commands.lua +++ b/lua/tirenvi/editor/commands.lua @@ -1,81 +1,109 @@ --- dependencies -local Context = require("tirenvi.app.context") -local WidthOp = require("tirenvi.width.op") -local buf_state = require("tirenvi.io.buf_state") -local buffer = require("tirenvi.io.buffer") -local init = require("tirenvi.init") -local ui = require("tirenvi.ui") -local guard = require("tirenvi.util.guard") -local notify = require("tirenvi.util.notify") +local api = vim.api -- Neovim + +local ui = require("tirenvi.ui") -- Root + +local autocmd = require("tirenvi.editor.autocmd") -- Editor +local Debug = require("tirenvi.editor.debug") +local guard = require("tirenvi.editor.guard") + +local app = require("tirenvi.app") -- App + +local WidthOp = require("tirenvi.width.op") -- Width + +local buf_state = require("tirenvi.io.buf_state") -- IO +local buf_lines = require("tirenvi.io.buf_lines") +local Context = require("tirenvi.io.context") + +local notify = require("tirenvi.util.notify") -- Util local errors = require("tirenvi.util.errors") local util = require("tirenvi.util.util") local log = require("tirenvi.util.log") -local Debug = require("tirenvi.editor.debug") --- module +-- ============================================================================= + local M = {} -local api = vim.api +-- ============================================================================= +--#region Private ---@param ctx Context ---@param opts {[string]:any} ---@return nil local function cmd_width(ctx, opts) - if buf_state.should_skip(ctx.bufnr, { has_grid = true, }) then return end + if buf_state.should_skip(ctx.bufnr, { has_grid = true }) then + return + end local width_op = WidthOp.new(opts) if not width_op then notify.error(errors.err_invalid_command(opts.args)) return end log.debug(width_op:to_string()) - init.width(ctx, width_op) + app.cmd_width(ctx, width_op) end ---@param ctx Context ---@param opts {[string]:any} ---@return nil local function cmd_fit(ctx, opts) - if buf_state.should_skip(ctx.bufnr, { has_grid = true, }) then return end + if buf_state.should_skip(ctx.bufnr, { has_grid = true }) then + return + end local width_op = WidthOp.new(opts) if not width_op then notify.error(errors.err_invalid_command(opts.args)) return end log.debug(width_op:to_string()) - init.fit(ctx, width_op) + app.cmd_fit(ctx, width_op) end ---@param ctx Context ---@param opts {[string]:any} ---@return nil local function cmd_wrap(ctx, opts) - if buf_state.should_skip(ctx.bufnr, { has_grid = true, }) then return end + if buf_state.should_skip(ctx.bufnr, { has_grid = true }) then + return + end local width_op = WidthOp.new(opts) if not width_op then notify.error(errors.err_invalid_command(opts.args)) return end log.debug(width_op:to_string()) - init.wrap(ctx, width_op) + app.cmd_wrap(ctx, width_op) end ---@param ctx Context ---@param opts {[string]:any} ---@return nil local function cmd_toggle(ctx, opts) - if buf_state.should_skip(ctx.bufnr, { is_tirbuf = false, has_grid = true, }) then + if + buf_state.should_skip(ctx.bufnr, { + is_tirbuf = false, + has_grid = false, + has_parser = false, + }) + then return end ui.special_apply(ctx.winid) - init.toggle(ctx) + app.toggle(ctx) + if buf_state.is_tirbuf(ctx.bufnr) then + autocmd.register_buf_autocmd(ctx.bufnr) + else + autocmd.clear_buf_autocmds(ctx.bufnr) + end end ---@param ctx Context ---@param opts {[string]:any} ---@return nil local function cmd_redraw(ctx, opts) - if buf_state.should_skip(ctx.bufnr) then return end - init.redraw(ctx) + if buf_state.should_skip(ctx.bufnr) then + return + end + app.cmd_redraw(ctx) end local warned = false @@ -83,27 +111,39 @@ local warned = false ---@param opts {[string]:any} ---@return nil local function cmd_repair(ctx, opts) - if buf_state.should_skip(ctx.bufnr) then return end + if buf_state.should_skip(ctx.bufnr) then + return + end local arg = opts.fargs[2] if arg == nil then if not warned then warned = true - notify.warn("Tir repair is deprecated and will be removed in v0.5. Use :Tir redraw") + notify.warn( + "Tir repair is deprecated and will be removed in v0.5. Use :Tir redraw" + ) end - init.redraw(ctx) + app.cmd_redraw(ctx) return elseif arg == "toggle" then - buffer.set_repair(ctx.bufnr, not buffer.get_repair(ctx.bufnr)) + buf_state.set_repair(ctx.bufnr, not buf_state.get_repair(ctx.bufnr)) elseif arg == "enable" then - buffer.set_repair(ctx.bufnr, true) + buf_state.set_repair(ctx.bufnr, true) elseif arg == "disable" then - buffer.set_repair(ctx.bufnr, false) + buf_state.set_repair(ctx.bufnr, false) else - notify.error("[Tirenvi] invalid argument: " .. arg .. " (expected: [enable|disable|toggle])") + notify.error( + "[Tirenvi] invalid argument: " + .. arg + .. " (expected: [enable|disable|toggle])" + ) return end - notify.info(string.format("[Tirenvi] repair:%s ", - buffer.get_repair(ctx.bufnr) and "enable" or "disable")) + notify.info( + string.format( + "[Tirenvi] repair:%s ", + buf_state.get_repair(ctx.bufnr) and "enable" or "disable" + ) + ) end ---@param ctx Context @@ -111,8 +151,8 @@ end ---@return nil local function cmd_debug_read_tir(ctx, opts) if buf_state.should_skip(ctx.bufnr, { - is_tirbuf = false, - }) then + is_tirbuf = false, + }) then return end local filename = opts.fargs[2] @@ -120,26 +160,24 @@ local function cmd_debug_read_tir(ctx, opts) notify.error("Tir _read_tir need filename") return end - init.debug_read_tir(ctx, filename) + app.debug_read_tir(ctx, filename) end ---@param ctx Context ---@param opts {[string]:any} ---@return nil local function cmd_debug_write_tir(ctx, opts) - if buf_state.should_skip(ctx.bufnr) then return end + if buf_state.should_skip(ctx.bufnr) then + return + end local filename = opts.fargs[2] if filename == nil then notify.error("Tir _write_tir need filename") return end - init.debug_write_tir(ctx, filename) + app.debug_write_tir(ctx, filename) end ----------------------------------------------------------------------- --- Registration (private) ----------------------------------------------------------------------- - local commands = { toggle = { func = cmd_toggle, sub = {} }, redraw = { func = cmd_redraw, sub = {} }, @@ -178,7 +216,8 @@ local function on_tir(opts) return end local ctx = Context.from_buf() - local debug_name = string.format("%s %s", opts.name, table.concat(opts.fargs, " ")) + local debug_name = + string.format("%s %s", opts.name, table.concat(opts.fargs, " ")) Debug.ui_entry(ctx.bufnr, debug_name) local command_name = sub:match("^[A-Za-z_]+") or "" local command = commands[command_name] @@ -214,7 +253,7 @@ local function register_user_command() nargs = "*", range = true, complete = complete_tir, - desc = build_desc() + desc = build_desc(), }) end @@ -233,36 +272,35 @@ local function register_keymaps() }) end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@return string function M.keymap_lf() local ctx = Context.from_buf() - buffer.clear_cache() + buf_lines.clear_cache() log.debug("===+===+===+===+=== keymap_lf %s ===+===+===+===+===", ctx.bufnr) if buf_state.should_skip(ctx.bufnr) then return util.get_termcodes("") end - return init.keymap_lf() + return app.keymap_lf() end ---@return string function M.keymap_tab() local ctx = Context.from_buf() - buffer.clear_cache() - log.debug("===+===+===+===+=== keymap_tab %s ===+===+===+===+===", ctx.bufnr) + buf_lines.clear_cache() + log.debug( + "===+===+===+===+=== keymap_tab %s ===+===+===+===+===", + ctx.bufnr + ) if buf_state.should_skip(ctx.bufnr) then return util.get_termcodes("") end - return init.keymap_tab() + return app.keymap_tab() end ----------------------------------------------------------------------- --- Setup ----------------------------------------------------------------------- - function M.setup() register_user_command() register_keymaps() diff --git a/lua/tirenvi/editor/debug.lua b/lua/tirenvi/editor/debug.lua index a1e47249..b2004edc 100644 --- a/lua/tirenvi/editor/debug.lua +++ b/lua/tirenvi/editor/debug.lua @@ -1,59 +1,72 @@ --- dependencies -local Context = require("tirenvi.app.context") -local Attrs = require("tirenvi.core.attrs") -local Attr = require("tirenvi.core.attr") -local CursorNvim = require("tirenvi.cursor.nvim") -local CursorLogical = require("tirenvi.cursor.cursor_logical") -local buffer = require("tirenvi.io.buffer") +local api = vim.api -- Neovim +local bo = vim.bo +local fn = vim.fn + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser +local Cursor = require("tirenvi.parser.cursor") + +local CursorTir = require("tirenvi.core.cursor") -- IO +local CursorNvim = require("tirenvi.io.cursor_nvim") +local buf_lines = require("tirenvi.io.buf_lines") local buf_state = require("tirenvi.io.buf_state") local reader = require("tirenvi.io.reader") local namespaces = require("tirenvi.io.namespaces") -local Range = require("tirenvi.util.range") +local Context = require("tirenvi.io.context") + +local Attrs = require("tirenvi.core.attrs") -- Core +local Attr = require("tirenvi.core.attr") + +local Range = require("tirenvi.util.range") -- Util local log = require("tirenvi.util.log") --- module +-- ============================================================================= + local M = {} --- constants / defaults +-- ============================================================================= +--#region Private local DELIMITER = " //" -local bo = vim.bo - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ ---@param bufnr number ---@param title string ---@param name string local function debug_trace(bufnr, title, name) - if not log.is_debug() then - return - end - local ctx = Context.from_buf() - local filetype = bo[ctx.bufnr].filetype - local state = buf_state.debug_state(ctx.bufnr) - log.debug("===+=== %s ===+=== %s[#%d] %s : %s ===", title, name or "", bufnr, filetype, state) + if not log.is_debug() then + return + end + local ctx = Context.from_buf() + local filetype = bo[ctx.bufnr].filetype + local state = buf_state.debug_state(ctx.bufnr) + log.debug( + "===+=== %s ===+=== %s[#%d] %s : %s ===", + title, + name or "", + bufnr, + filetype, + state + ) end ---@return string local function case_tag() - return vim.g.case_tag or "" + return vim.g.case_tag or "" end ----@param cursor CursorBuf ----@param logical CursorLogical +---@param cursor_buf CursorBuf +---@param cursor_tir CursorTir ---@return string -local function cursor_str(cursor, logical) - return string.format("cur(%d,%d) b%s:r%s:c%s%+d<%s>", - cursor.row_cur, - cursor.col_disp, - logical.iblock, - logical.irow, - logical.icol, - logical.col_offset or 0, - cursor.char - ) +local function cursor_str(cursor_buf, cursor_tir) + return string.format( + "cur(%d,%d) b%s:r%s:c%s%+d<%s>", + cursor_buf.row_cur, + cursor_buf.col_disp, + cursor_tir.iblock, + cursor_tir.irow, + cursor_tir.icol, + cursor_tir.col_offset or 0, + cursor_buf.char + ) end ---@param ctx Context @@ -61,89 +74,104 @@ end ---@param iattr integer ---@param icol integer local function show_attr_marks(ctx, attr, iattr, icol) - local start0 - if attr.range then - start0 = Range.to_vim(attr.range) - else - start0 = iattr - 1 - end - local highlight = "ErrorMsg" - if Attr.is_grid(attr) then - highlight = "Todo" - end - local attr_long = Attr.get_attr_long(attr, icol) - local opts = { - virt_text = { { tostring(attr_long), highlight } }, - virt_text_pos = "eol_right_align", -- eol - } - ---- NOTE: - -- virt_text screen position is not always stable and may differ - -- from the extmark's actual buffer position. - local nlines = buffer.line_count(ctx.bufnr) - start0 = math.min(start0, nlines - 1) - local ok = pcall(vim.api.nvim_buf_set_extmark, ctx.bufnr, namespaces.ATTR, start0, 0, opts) - if not ok then - opts = vim.deepcopy(opts) - opts.virt_text = nil - opts.virt_text_pos = nil - vim.api.nvim_buf_set_extmark(ctx.bufnr, namespaces.ATTR, start0, 0, opts) - end - + local start0 + if attr.range then + start0 = Range.to_vim(attr.range) + else + start0 = iattr - 1 + end + local highlight = "ErrorMsg" + if Attr.is_grid(attr) then + highlight = "Todo" + end + local attr_long = Attr.get_attr_long(attr, icol) + local opts = { + virt_text = { { tostring(attr_long), highlight } }, + virt_text_pos = "eol_right_align", -- eol + } + ---- NOTE: + -- virt_text screen position is not always stable and may differ + -- from the extmark's actual buffer position. + local nlines = buf_lines.line_count(ctx.bufnr) + start0 = math.min(start0, nlines - 1) + local ok = pcall( + api.nvim_buf_set_extmark, + ctx.bufnr, + namespaces.ATTR, + start0, + 0, + opts + ) + if not ok then + opts = vim.deepcopy(opts) + opts.virt_text = nil + opts.virt_text_pos = nil + api.nvim_buf_set_extmark(ctx.bufnr, namespaces.ATTR, start0, 0, opts) + end end ----------------------------------------------------------------------- +--#endregion +-- ============================================================================= -- Public API ----------------------------------------------------------------------- function M.ui_entry(bufnr, name) - debug_trace(bufnr, "ENTRY", name) + debug_trace(bufnr, "ENTRY", name) end function M.ui_exit(bufnr, name) - debug_trace(bufnr, "EXIT!", name) + debug_trace(bufnr, "EXIT!", name) end ---@param title string|nil ---@param single boolean|nil ---@return string function M.layout(title, single) - title = case_tag() .. (title or "") .. DELIMITER - local ctx = Context.from_buf() - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) or {} - local cursor = reader.cursor(ctx) - local logical = Attrs.to_logical(attrs, cursor.row_cur, cursor.col_disp) - local attr_str = Attrs.debug_attrs(attrs, "", logical.iblock, logical.icol, single) - return string.format("%s %s %s", title, cursor_str(cursor, logical), attr_str) + title = case_tag() .. (title or "") .. DELIMITER + local ctx = Context.from_buf() + local attrs = buf_state.get(ctx.bufnr, buf_state.IKEY.ATTRS) or {} + local cursor_buf = reader.cursor_buf(ctx) + local line = buf_lines.get_line(ctx.bufnr, cursor_buf.row_cur) or "" + local prefix = tir_buf.get_prefix_part(line) + local pre_disp = fn.strdisplaywidth(prefix) + local cursor_tir = + Cursor.to_tir(attrs, cursor_buf.row_cur, cursor_buf.col_disp - pre_disp) + local attr_str = + Attrs.debug_attrs(attrs, "", cursor_tir.iblock, cursor_tir.icol, single) + return string.format( + "%s %s %s", + title, + cursor_str(cursor_buf, cursor_tir), + attr_str + ) end ---@param iblock integer ---@param irow integer ---@param icol integer -function M.goto(iblock, irow, icol) - local ctx = Context.from_buf() - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) - local logical = CursorLogical.new(iblock, irow, icol, 0) - local row_cur, col_disp = Attrs.to_cursor(attrs, logical) - local line = buffer.get_line(ctx.bufnr, row_cur) or "" - local cursor = CursorNvim.from_col_disp(line, row_cur, col_disp) - CursorNvim.move_byte(ctx, row_cur, cursor.col_byte) +function M.goto_cell(iblock, irow, icol) + local ctx = Context.from_buf() + local attrs = buf_state.get(ctx.bufnr, buf_state.IKEY.ATTRS) + local cursor_tir = CursorTir.new(iblock, irow, icol, 0) + local cursor_buf = Cursor.to_buf(cursor_tir, attrs, ctx.line_provider) + CursorNvim.move_byte(ctx, cursor_buf.row_cur, cursor_buf.col_byte) end function M.show_attr_marks(ctx) - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) - vim.api.nvim_buf_clear_namespace(ctx.bufnr, namespaces.ATTR, 0, -1) - if not attrs then - return - end - local cursor = reader.cursor(ctx) - local pos = Attrs.to_logical(attrs, cursor.row_cur, cursor.col_disp) - for iattr, attr in ipairs(attrs) do - local icol - if pos.iblock == iattr then - icol = Attr.to_cell_col(attr, cursor.col_char) - end - show_attr_marks(ctx, attr, iattr, icol) - end + local attrs = buf_state.get(ctx.bufnr, buf_state.IKEY.ATTRS) + api.nvim_buf_clear_namespace(ctx.bufnr, namespaces.ATTR, 0, -1) + if not attrs then + return + end + local cursor_buf = reader.cursor_buf(ctx) + local cursor_tir = + Cursor.to_tir(attrs, cursor_buf.row_cur, cursor_buf.col_disp) + for iattr, attr in ipairs(attrs) do + local icol + if cursor_tir.iblock == iattr then + icol = Attr.to_cell_col(attr, cursor_buf.col_char) + end + show_attr_marks(ctx, attr, iattr, icol) + end end return M diff --git a/lua/tirenvi/editor/guard.lua b/lua/tirenvi/editor/guard.lua new file mode 100644 index 00000000..d203e147 --- /dev/null +++ b/lua/tirenvi/editor/guard.lua @@ -0,0 +1,53 @@ +local buf_state = require("tirenvi.io.buf_state") -- IO +local buf_lines = require("tirenvi.io.buf_lines") + +local errors = require("tirenvi.util.errors") -- Util +local notify = require("tirenvi.util.notify") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +-- Public API + +--- Wrap a function with guarded error handling. +---@param func fun(...) +---@param opts? { on_error?: fun(err, ...) } +---@return fun(...) +function M.guarded(func, opts) + opts = opts or {} + buf_lines.clear_cache() + + return function(...) + if buf_state.is_vscode() then + return + end + local args = { ... } + + local ok, result = xpcall(func, function(err) + if type(err) == "table" and err.tag == errors.DOMAIN_ERROR then + return err + end + return debug.traceback(err, 2) + end, ...) + + if not ok then + if opts.on_error then + opts.on_error(unpack(args), result) + end + + if + type(result) == "table" + and result.tag == errors.DOMAIN_ERROR + then + notify.error(result.message) + else + notify.error(result) + end + end + end +end + +return M diff --git a/lua/tirenvi/editor/init.lua b/lua/tirenvi/editor/init.lua new file mode 100644 index 00000000..8769f7ad --- /dev/null +++ b/lua/tirenvi/editor/init.lua @@ -0,0 +1,14 @@ +-- ============================================================================= + +local M = {} + +-- ============================================================================= +-- Public API + +function M.setup() + require("tirenvi.editor.autocmd").setup() + require("tirenvi.editor.commands").setup() + require("tirenvi.editor.textobj").setup() +end + +return M diff --git a/lua/tirenvi/editor/motion.lua b/lua/tirenvi/editor/motion.lua index b00d5f67..3eb0faec 100644 --- a/lua/tirenvi/editor/motion.lua +++ b/lua/tirenvi/editor/motion.lua @@ -1,18 +1,25 @@ -local Context = require("tirenvi.app.context") -local buffer = require("tirenvi.io.buffer") -local reader = require("tirenvi.io.reader") -local Attrs = require("tirenvi.core.attrs") -local Bufline = require("tirenvi.core.bufline") -local CursorNvim = require("tirenvi.cursor.nvim") -local log = require("tirenvi.util.log") +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser +local Cursor = require("tirenvi.parser.cursor") -local M = {} +local buf_state = require("tirenvi.io.buf_state") -- IO +local reader = require("tirenvi.io.reader") +local Context = require("tirenvi.io.context") +local CursorNvim = require("tirenvi.io.cursor_nvim") + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private ---@return string local function get_pipe() local ctx = Context.from_buf() - local cursor = reader.cursor(ctx) - return Bufline.get_pipe_char(cursor.line) or "" + local cursor_buf = reader.cursor_buf(ctx) + return tir_buf.get_pipe_char(cursor_buf.line) or "" end ---@param op string @@ -23,9 +30,9 @@ local function build_motion(op) end end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ M.f = build_motion("f") M.F = build_motion("F") @@ -33,21 +40,21 @@ M.t = build_motion("t") M.T = build_motion("T") function M.block_top() - local ctx = Context.from_buf() - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) - local cursor = reader.cursor(ctx) - local pos = Attrs.to_logical(attrs, cursor.row_cur, cursor.col_disp) + local ctx = Context.from_buf() + local attrs = buf_state.get(ctx.bufnr, buf_state.IKEY.ATTRS) + local cursor_buf = reader.cursor_buf(ctx) + local pos = Cursor.to_tir(attrs, cursor_buf.row_cur, cursor_buf.col_disp) local top_row = attrs[pos.iblock].range.first - CursorNvim.restore_disp(ctx, top_row, cursor.col_disp) + CursorNvim.restore_disp(ctx, top_row, cursor_buf.col_disp) end function M.block_bottom() - local ctx = Context.from_buf() - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) - local cursor = reader.cursor(ctx) - local pos = Attrs.to_logical(attrs, cursor.row_cur, cursor.col_disp) + local ctx = Context.from_buf() + local attrs = buf_state.get(ctx.bufnr, buf_state.IKEY.ATTRS) + local cursor_buf = reader.cursor_buf(ctx) + local pos = Cursor.to_tir(attrs, cursor_buf.row_cur, cursor_buf.col_disp) local bottom_row = attrs[pos.iblock].range.last - CursorNvim.restore_disp(ctx, bottom_row, cursor.col_disp) + CursorNvim.restore_disp(ctx, bottom_row, cursor_buf.col_disp) end return M diff --git a/lua/tirenvi/editor/textobj.lua b/lua/tirenvi/editor/textobj.lua index 0bcfc64c..29c00900 100644 --- a/lua/tirenvi/editor/textobj.lua +++ b/lua/tirenvi/editor/textobj.lua @@ -1,56 +1,62 @@ -local Context = require("tirenvi.app.context") -local Bufline = require("tirenvi.core.bufline") +local api = vim.api -- Neovim + +local config = require("tirenvi.config") -- Root + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser local buf_parser = require("tirenvi.parser.buf_parser") -local config = require("tirenvi.config") -local CursorNvim = require("tirenvi.cursor.nvim") -local LinProvider = require("tirenvi.io.buffer_line_provider") -local buffer = require("tirenvi.io.buffer") -local errors = require("tirenvi.util.errors") + +local Context = require("tirenvi.io.context") -- IO +local CursorNvim = require("tirenvi.io.cursor_nvim") + +local errors = require("tirenvi.util.errors") -- Util local notify = require("tirenvi.util.notify") local log = require("tirenvi.util.log") +-- ============================================================================= + local M = {} --- private helpers +-- ============================================================================= +--#region Private ---@param is_around boolean|nil local function setup_vl(is_around) - local ctx = Context.from_buf() - is_around = is_around or false - local count = vim.v.count1 - local rect, lines = Bufline.get_block_rect(ctx, count, is_around) - if not rect then - return - end - if not buf_parser.table_is_aligned(lines) then - notify.error(errors.ERR.TABLE_IS_NOT_ALIGNED) - return - end - CursorNvim.move_byte(ctx, rect.row.first, rect.col.first) - vim.api.nvim_feedkeys(vim.keycode(""), "n", false) - vim.cmd("normal! o") - CursorNvim.move_byte(ctx, rect.row.last, rect.col.last) + local ctx = Context.from_buf() + is_around = is_around or false + local count = vim.v.count1 + local rect, lines = tir_buf.get_block_rect(ctx, count, is_around) + if not rect then + return + end + if not buf_parser.table_is_aligned(lines) then + notify.error(errors.ERR.TABLE_IS_NOT_ALIGNED) + return + end + CursorNvim.move_byte(ctx, rect.row.first, rect.col.first) + api.nvim_feedkeys(vim.keycode(""), "n", false) + vim.cmd("normal! o") + CursorNvim.move_byte(ctx, rect.row.last, rect.col.last) end local function setup_vil() - setup_vl() + setup_vl() end local function setup_val() - setup_vl(true) + setup_vl(true) end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ function M.setup() - vim.keymap.set({ "x" }, "i" .. config.textobj.column, setup_vil, { - desc = "Inner column", - }) - vim.keymap.set({ "x" }, "a" .. config.textobj.column, setup_val, { - desc = "Around column", - }) + vim.keymap.set({ "x" }, "i" .. config.textobj.column, setup_vil, { + desc = "Inner column", + }) + vim.keymap.set({ "x" }, "a" .. config.textobj.column, setup_val, { + desc = "Around column", + }) end return M diff --git a/lua/tirenvi/health.lua b/lua/tirenvi/health.lua index 37978101..16c6bfe4 100644 --- a/lua/tirenvi/health.lua +++ b/lua/tirenvi/health.lua @@ -1,10 +1,20 @@ -local Parser = require("tirenvi.parser.parser") -local config = require("tirenvi.config") +local fn = vim.fn -- Neovim + +local health = vim.health or require("health") -- Neovim + +local config = require("tirenvi.config") -- Root local version = require("tirenvi.version") -local log = require("tirenvi.util.log") + +local Parser = require("tirenvi.parser.parser") -- Parser + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= local M = {} -local health = vim.health or require("health") + +-- ============================================================================= +--#region Private local function report(item) if item.status == "ok" then @@ -27,8 +37,9 @@ local function check_command(parser) if err == Parser.ERR.EXECUTABLE_NOT_FOUND then report({ status = "error", - message = parser.executable .. " not found.\nInstall it with:\n pip install " .. parser - .executable, + message = parser.executable + .. " not found.\nInstall it with:\n pip install " + .. parser.executable, }) return end @@ -45,7 +56,9 @@ local function check_command(parser) if err == Parser.ERR.VERSION_PARSE_FAILED then table.insert(results, { status = "warn", - message = "Could not parse " .. parser.executable .. " version string." + message = "Could not parse " + .. parser.executable + .. " version string.", }) end if err == Parser.ERR.VERSION_TOO_OLD then @@ -73,11 +86,15 @@ local function check_command(parser) end end +--#endregion +-- ============================================================================= +-- Public API + function M.check() health.start("tirenvi") health.info("version: " .. version.VERSION) - pcall(vim.fn["repeat#set"], "") - if vim.fn.exists("*repeat#set") == 1 then + pcall(fn["repeat#set"], "") + if fn.exists("*repeat#set") == 1 then vim.health.ok("vim-repeat is available") else vim.health.warn("vim-repeat not found ('.' repeat disabled)") @@ -95,13 +112,20 @@ function M.check() if parser.required_version and not parser._required_version_int then report({ status = "error", - message = "Could not parse " .. exe .. " version string: " .. parser.required_version, + message = "Could not parse " + .. exe + .. " version string: " + .. parser.required_version, }) elseif exe then if not command_requirements[exe] then command_requirements[exe] = parser - elseif Parser.is_old(parser._required_version_int, - command_requirements[exe]._required_version_int) then + elseif + Parser.is_old( + parser._required_version_int, + command_requirements[exe]._required_version_int + ) + then command_requirements[exe] = parser end end diff --git a/lua/tirenvi/init.lua b/lua/tirenvi/init.lua index a132c097..3769b317 100644 --- a/lua/tirenvi/init.lua +++ b/lua/tirenvi/init.lua @@ -1,46 +1,13 @@ ------ dependencies -local Context = require("tirenvi.app.context") -local pipeline = require("tirenvi.app.pipeline") -local config = require("tirenvi.config") -local buffer = require("tirenvi.io.buffer") -local reader = require("tirenvi.io.reader") -local buf_state = require("tirenvi.io.buf_state") -local attr_store = require("tirenvi.io.attr_store") -local Bufline = require("tirenvi.core.bufline") -local CursorNvim = require("tirenvi.cursor.nvim") -local Range = require("tirenvi.util.range") -local util = require("tirenvi.util.util") -local notify = require("tirenvi.util.notify") -local log = require("tirenvi.util.log") +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= --- module local M = {} -local api = vim.api -local fn = vim.fn -local bo = vim.bo --- constants / defaults M.motion = require("tirenvi.editor.motion") --- private helpers - -local warned = false ----@param command string -local function set_repeat(command) - local ok = pcall(function() - fn["repeat#set"](command) - end) - if not ok and not warned then - warned = true - notify.info( - "tirenvi: install 'tpope/vim-repeat' to enable '.' repeat" - ) - end -end - ------------------------------------------------------------------------ +-- ============================================================================= -- Public API ------------------------------------------------------------------------ --- Set up tirenvi plugin (load autocmds and commands) ---@param opts {[string]:any} @@ -50,185 +17,9 @@ function M.setup(opts) return end vim.g.tirenvi_initialized = true - config.setup(opts) - require("tirenvi.editor.autocmd").setup() - require("tirenvi.editor.commands").setup() - require("tirenvi.editor.textobj").setup() + require("tirenvi.config").setup(opts) require("tirenvi.ui").setup() -end - ---- Convert current buffer (or specified buffer) from plain format to tir-vim format ----@param ctx Context ----@return nil -function M.read_post(ctx) - pipeline.read_post(ctx) -end - ---- Convert current buffer (or specified buffer) from display format back to file format (tsv) ----@param ctx Context ----@return nil -function M.write_pre(ctx) - pipeline.write_pre(ctx) -end - ---- Convert current buffer (or specified buffer) from plain format to view format ----@param ctx Context ----@return nil -function M.write_post(ctx) - pipeline.write_post(ctx) -end - ----@param ctx Context ----@return nil -function M.toggle(ctx) - local is_flat = buf_state.is_flat(ctx.bufnr) - if is_flat == nil or is_flat then - pipeline.from_flat(ctx) - elseif buf_state.has_grid(ctx) then - pipeline.to_flat(ctx) - end -end - ----@param ctx Context -function M.redraw(ctx) - pipeline.cmd_repair(ctx) -end - ----@param ctx Context ----@param width_op WidthOp -function M.width(ctx, width_op) - pipeline.cmd_width(ctx, width_op) - set_repeat(util.get_termcodes(width_op:to_cmd())) -end - ----@param ctx Context ----@param width_op WidthOp -function M.fit(ctx, width_op) - pipeline.cmd_fit(ctx, width_op) - set_repeat(util.get_termcodes(width_op:to_cmd())) -end - ----@param ctx Context ----@param width_op WidthOp -function M.wrap(ctx, width_op) - pipeline.cmd_wrap(ctx, width_op) -end - ----@param ctx Context -function M.insert_char_in_newline(ctx) - local cursor = reader.cursor(ctx) - local row_cur = cursor.row_cur - local line_new = buffer.get_line(ctx.bufnr, row_cur) - if line_new ~= "" then - return - end - local line_prev, line_next = buffer.get_lines_around(ctx.bufnr, Range.from_lua(row_cur, row_cur)) - local line_ref = line_prev - if not Context.is_allow_plain(ctx) then - line_ref = line_ref or line_next - end - local pipe = Bufline.get_pipe_char(line_ref) - if not pipe then - return - end - vim.v.char = pipe .. vim.v.char -end - ----@return string -function M.keymap_lf() - local col = fn.col(".") - local line = fn.getline(".") - if not Bufline.get_pipe_char(line) then - return util.get_termcodes("") - end - if col == 1 or col > #line then - return util.get_termcodes("") - end - return config.marks.lf -end - ----@return string -function M.keymap_tab() - local line = fn.getline(".") - if not Bufline.get_pipe_char(line) then - return util.get_termcodes("") - end - if bo.expandtab then - return util.get_termcodes("") - end - return config.marks.tab -end - ----@param ctx Context ----@param range3 Range3 -function M.on_lines(ctx, range3) - pipeline.on_lines(ctx, range3) - pipeline.check_and_repair(ctx, range3) -end - ----@param ctx Context ----@param range3 Range3|nil -function M.on_insert_leave(ctx, range3) - pipeline.check_and_repair(ctx, range3) -end - -local function apply_wrap(winid, should_wrap) - if vim.wo[winid].wrap ~= should_wrap then - vim.wo[winid].wrap = should_wrap - end -end - ----@param ctx Context -function M.auto_wrap(ctx) - if not config.ui.manage_wrap then - return - end - if not ctx.parser.allow_plain then - apply_wrap(ctx.winid, false) - return - end - -- Fast path for CursorMoved. - -- We only need the current line of the current window. - local line = vim.api.nvim_get_current_line() - local line_width = fn.strdisplaywidth(line) - local win_span = buffer.get_win_span(ctx.winid) - local is_over = win_span < line_width - local is_plain = not Bufline.has_pipe({ line }) - if is_over then - apply_wrap(ctx.winid, is_plain) - end -end - ----@param ctx Context -function M.on_filetype(ctx) - local old_filetype = buffer.get(ctx.bufnr, buffer.IKEY.FILETYPE) - local new_filetype = bo[ctx.bufnr].filetype - -- log.debug("filetype %s -> %s", tostring(old_filetype), tostring(new_filetype)) - if old_filetype and old_filetype == new_filetype then - return - end - if old_filetype then - pipeline.to_flat(ctx) - end - buffer.set(ctx.bufnr, buffer.IKEY.FILETYPE, new_filetype) - buf_state.set_buffer_flat(ctx.bufnr, true) - attr_store.write(ctx, nil) - ctx = Context.from_buf(ctx.bufnr) - if not ctx.parser then - buffer.set(ctx.bufnr, buffer.IKEY.FILETYPE, nil) - end -end - ----@param ctx Context ----@param filename string -function M.debug_read_tir(ctx, filename) - pipeline.debug_read_tir(ctx, filename) -end - ----@param ctx Context ----@param filename string -function M.debug_write_tir(ctx, filename) - pipeline.debug_write_tir(ctx, filename) + require("tirenvi.editor").setup() end return M diff --git a/lua/tirenvi/io/attr_store.lua b/lua/tirenvi/io/attr_store.lua index 252a92e2..a9a2f12a 100644 --- a/lua/tirenvi/io/attr_store.lua +++ b/lua/tirenvi/io/attr_store.lua @@ -1,26 +1,24 @@ -local buffer = require("tirenvi.io.buffer") -local log = require("tirenvi.util.log") +local buf_state = require("tirenvi.io.buf_state") -local M = {} +local log = require("tirenvi.util.log") -- Util --- constants / defaults +-- ============================================================================= --- private helpers +local M = {} ------------------------------------------------------------------------ +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param bufnr number ---@return Attr[] function M.read(bufnr) - return buffer.get(bufnr, buffer.IKEY.ATTRS) or {} + return buf_state.get(bufnr, buf_state.IKEY.ATTRS) or {} end ----@param ctx Context +---@param bufnr number ---@param attrs Attr[]|nil -function M.write(ctx, attrs) - buffer.set(ctx.bufnr, buffer.IKEY.ATTRS, attrs) +function M.write(bufnr, attrs) + buf_state.set(bufnr, buf_state.IKEY.ATTRS, attrs) end return M diff --git a/lua/tirenvi/io/buf_line_provider.lua b/lua/tirenvi/io/buf_line_provider.lua new file mode 100644 index 00000000..d9a21a4e --- /dev/null +++ b/lua/tirenvi/io/buf_line_provider.lua @@ -0,0 +1,32 @@ +local buf_lines = require("tirenvi.io.buf_lines") -- IO + +-- ============================================================================= + +---@class BufferLineProvider : LineProvider +local M = {} + +-- ============================================================================= +-- Public API + +---@param bufnr number +---@return LineProvider +function M.new(bufnr) + return { + get_lines = function(first, last) + ---@diagnostic disable-next-line: redundant-parameter + return buf_lines.get_lines(bufnr, first, last) + end, + + get_line = function(row) + ---@diagnostic disable-next-line: redundant-parameter + return buf_lines.get_line(bufnr, row) + end, + + line_count = function() + ---@diagnostic disable-next-line: redundant-parameter + return buf_lines.line_count(bufnr) + end, + } +end + +return M diff --git a/lua/tirenvi/io/buf_lines.lua b/lua/tirenvi/io/buf_lines.lua new file mode 100644 index 00000000..6f15dfe3 --- /dev/null +++ b/lua/tirenvi/io/buf_lines.lua @@ -0,0 +1,228 @@ +local api = vim.api -- Neovim +local fn = vim.fn +local bo = vim.bo + +local buf_state = require("tirenvi.io.buf_state") -- IO +local CursorNvim = require("tirenvi.io.cursor_nvim") + +local Range = require("tirenvi.util.range") -- Util +local util = require("tirenvi.util.util") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +local cache = { bufnr = -1, start = -1, lines = {} } +local STEP = 25 + +-- ============================================================================= +--#region Private + +---@param ctx Context +---@param cursor_buf CursorBuf +local function reset_cursor_tir(ctx, cursor_buf) + -- CursorNvim.move(ctx, cursor.row_cur, cursor.col_byte) +end + +---@param ctx Context +---@param cursor_buf CursorBuf|nil +local function set_cursor_pos(ctx, cursor_buf) + if not cursor_buf then + CursorNvim.reset(ctx) + elseif cursor_buf.restore_mode == "buffer" then + CursorNvim.restore_byte(ctx, cursor_buf.row_cur, cursor_buf.col_byte) + elseif cursor_buf.restore_mode == "tir" then + reset_cursor_tir(ctx, cursor_buf) + else + CursorNvim.reset(ctx) + end +end + +---@param bufnr number +local function set_undo_tree_last(bufnr) + local next = fn.undotree(bufnr).seq_last + buf_state.set(bufnr, buf_state.IKEY.UNDO_TREE_LAST, next) +end + +---@param ctx Context +---@param range Range +---@param lines string[] +---@param no_undo boolean +---@param cursor_buf CursorBuf|nil +local function set_lines(ctx, range, lines, no_undo, cursor_buf) + M.clear_cache() + local bufnr = ctx.bufnr + local undolevels = bo[bufnr].undolevels + if no_undo then + local undotree = fn.undotree(bufnr) + if undotree.seq_last == 0 then + bo[bufnr].undolevels = -1 + else + pcall(vim.cmd, "undojoin") + end + end + local start0, end0 = Range.to_vim(range) + start0 = math.max(start0, 0) + api.nvim_buf_set_lines(bufnr, start0, end0, false, lines) + set_undo_tree_last(bufnr) + set_cursor_pos(ctx, cursor_buf) + bo[bufnr].undolevels = undolevels +end + +---@param bufnr number +---@param first integer -- 1-based +---@param last integer -- 1-based +local function get_lines_and_cache(bufnr, first, last) + local start = math.max(first - 1, 0) + local end0 = util.trim(last, start + 2 * STEP, M.line_count(bufnr)) + local start0 = util.trim(start, 0, end0 - 2 * STEP) + local lines = api.nvim_buf_get_lines(bufnr, start0, end0, false) + cache = { bufnr = bufnr, start = start0, lines = lines } + log.debug( + "=== cache[#%d] lines[%d]='%s'...[%d]='%s'", + cache.bufnr, + cache.start + 1, + tostring(cache.lines[1]), + cache.start + #cache.lines, + tostring(cache.lines[#cache.lines]) + ) +end + +---@param bufnr number +---@param iline integer -- 1-based +---@return string|nil +local function get_line_from_cache(bufnr, iline) + if cache.bufnr ~= bufnr then + return nil + end + return cache.lines[iline - cache.start] +end + +---@param bufnr number +---@param first integer -- 1-based +---@param last integer -- 1-based +---@return string[] +local function get_lines_from_cache(bufnr, first, last) + if cache.bufnr ~= bufnr then + return {} + end + local istart = first - cache.start + local ilast = last - cache.start + if istart < 1 or ilast > #cache.lines then + return {} + end + return vim.list_slice(cache.lines, istart, ilast) +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@param range Range +---@param lines string[] +---@param no_undo boolean|nil +---@param cursor_buf CursorBuf|nil +function M.set_lines(ctx, range, lines, no_undo, cursor_buf) + local bufnr = ctx.bufnr + buf_state.set( + bufnr, + buf_state.IKEY.PATCH_DEPTH, + buf_state.get(bufnr, buf_state.IKEY.PATCH_DEPTH) + 1 + ) + local before = fn.undotree(bufnr).seq_last + local ok, err = pcall(set_lines, ctx, range, lines, no_undo, cursor_buf) + local after = fn.undotree(bufnr).seq_last + local first, last = Range.to_lua(range) + log.watch( + "UNDO", + "%s[%d->%d]set_lines lines[%d]='%s'...[%d]='%s'", + tostring(no_undo), + before, + after, + first, + tostring(lines[1]), + last, + tostring(lines[#lines]) + ) + buf_state.set( + bufnr, + buf_state.IKEY.PATCH_DEPTH, + buf_state.get(bufnr, buf_state.IKEY.PATCH_DEPTH) - 1 + ) + log.assert( + buf_state.get(bufnr, buf_state.IKEY.PATCH_DEPTH) == 0, + "PATCH_DEPTH should be 0 after set_lines" + ) + if not ok then + error(err) + end +end + +function M.clear_cache() + cache = { bufnr = -1, start = -1, lines = {} } +end + +---@param bufnr number +---@param first integer -- 1-based +---@param last integer -- 1-based +---@return string[] +function M.get_lines(bufnr, first, last) + first = math.max(1, first) + local nline = M.line_count(bufnr) + if last == -1 then + last = nline + end + last = math.min(nline, last) + local lines = get_lines_from_cache(bufnr, first, last) + if #lines ~= 0 then + return lines + end + get_lines_and_cache(bufnr, first - STEP, last + STEP) + return get_lines_from_cache(bufnr, first, last) +end + +---@param bufnr number +---@param iline integer -- 1-based +---@return string|nil +function M.get_line(bufnr, iline) + local line = get_line_from_cache(bufnr, iline) + if line then + return line + end + if cache.bufnr ~= bufnr then + M.get_lines(bufnr, iline, iline) + elseif iline - 1 < cache.start then + if iline >= 1 then + get_lines_and_cache(bufnr, iline - 2 * STEP, iline) + end + else + if iline <= M.line_count(bufnr) then + get_lines_and_cache(bufnr, iline, iline + 2 * STEP - 1) + end + end + return get_line_from_cache(bufnr, iline) +end + +---@param bufnr number +---@return integer +function M.line_count(bufnr) + return api.nvim_buf_line_count(bufnr) +end + +---@param bufnr number +---@param range Range +---@return string|nil +---@return string|nil +function M.get_lines_around(bufnr, range) + M.get_lines(bufnr, range.first - 1, range.last + 1) + return M.get_line(bufnr, range.first - 1), M.get_line(bufnr, range.last + 1) +end + +---@param step integer +function M.set_step(step) + STEP = step +end + +return M diff --git a/lua/tirenvi/io/buf_state.lua b/lua/tirenvi/io/buf_state.lua index 4c589c21..339a32b6 100644 --- a/lua/tirenvi/io/buf_state.lua +++ b/lua/tirenvi/io/buf_state.lua @@ -1,15 +1,71 @@ -local Context = require("tirenvi.app.context") -local config = require("tirenvi.config") -local Range3 = require("tirenvi.util.range3") -local Attrs = require("tirenvi.core.attrs") -local buffer = require("tirenvi.io.buffer") +local api = vim.api -- Neovim +local fn = vim.fn +local bo = vim.bo +local b = vim.b + +local config = require("tirenvi.config") -- Root + +local Attrs = require("tirenvi.core.attrs") -- Core + +local Range3 = require("tirenvi.util.range3") -- Util local log = require("tirenvi.util.log") +-- ============================================================================= + local M = {} -local api = vim.api -local fn = vim.fn -local bo = vim.bo +-- Buffer-local flags. +M.IKEY = { + -- true when in insert mode + INSERT_MODE = "insert_mode", + + -- Set only when the on_lines callback is attached. + ATTACHED = "attached", + + -- Depth of patch recursion + PATCH_DEPTH = "patch_depth", + + -- create autocmd + AUTOCMD = "autocmd", + + -- fn.undotree().seq_last + UNDO_TREE_LAST = "undo_tree_last", + + -- bo[bufnr].filetype + FILETYPE = "filetype", + + -- parser + PARSER = "parser", + + -- repair flag + REPAIR = "repair", + + -- block attrs + ATTRS = "attrs", + + -- dirty row # + DIRTY = "dirty", + + -- buffer is flat or tir-buffer + TIRBUF = "tirbuf", +} + +local initial_value = { + [M.IKEY.INSERT_MODE] = false, + [M.IKEY.ATTACHED] = false, + [M.IKEY.PATCH_DEPTH] = 0, + [M.IKEY.AUTOCMD] = false, + [M.IKEY.UNDO_TREE_LAST] = -1, + [M.IKEY.FILETYPE] = nil, + [M.IKEY.PARSER] = nil, + [M.IKEY.REPAIR] = nil, + [M.IKEY.ATTRS] = nil, + [M.IKEY.DIRTY] = nil, + [M.IKEY.TIRBUF] = false, +} + +-- ============================================================================= +--#region Private ---@class Check_options ---@field supported? boolean @@ -30,27 +86,22 @@ local INSERT_MODE = "INSERT_MODE" local UNDO_REDO_MODE = "UNDO_REDO_MODE" local NORMAL_MODE = "NORMAL_MODE" - ---@param bufnr number ---@param message string ---@param range3 Range3|nil local function log_watch(bufnr, message, range3) range3 = range3 or Range3.new(0, 0, 0) - local pre = buffer.get(bufnr, buffer.IKEY.UNDO_TREE_LAST) + local pre = M.get(bufnr, M.IKEY.UNDO_TREE_LAST) local next = fn.undotree(bufnr).seq_last - local status = string.format( - "[tree:%d->%d]%s", - pre, - next, - Range3.short(range3) - ) + local status = + string.format("[tree:%d->%d]%s", pre, next, Range3.short(range3)) log.watch("UNDO", message .. status) end ---@param bufnr number ---@return boolean local function is_insert_mode(bufnr) - local mode = buffer.get(bufnr, buffer.IKEY.INSERT_MODE) == true + local mode = M.get(bufnr, M.IKEY.INSERT_MODE) == true if mode then log.debug("===-===-===-=== insert mode[%d] ===-===-===-===", bufnr) end @@ -60,30 +111,35 @@ end ---@param bufnr number ---@return boolean local function is_undo_mode(bufnr) - local pre = buffer.get(bufnr, buffer.IKEY.UNDO_TREE_LAST) + local pre = M.get(bufnr, M.IKEY.UNDO_TREE_LAST) local next = fn.undotree(bufnr).seq_last if pre == next then - log.debug("===-===-===-=== und/redo mode[%d] (%d, %d) ===-===-===-===", bufnr, pre, next) + log.debug( + "===-===-===-=== und/redo mode[%d] (%d, %d) ===-===-===-===", + bufnr, + pre, + next + ) return true end return false end ----@param ctx Context +---@param bufnr number ---@param range3 Range3|nil ---@return string -local function get_status(ctx, range3) - if buffer.get_repair(ctx.bufnr) == false then +local function get_status(bufnr, range3) + if M.get_repair(bufnr) == false then return REPAIR_OFF end if not range3 then return INSERT_LEAVE - elseif is_insert_mode(ctx.bufnr) then + elseif is_insert_mode(bufnr) then -- Modifying the buffer in insert mode may corrupt the undo node. -- Therefore, in insert mode, only record the dirty changed region -- and repair it when leaving insert mode. return INSERT_MODE - elseif is_undo_mode(ctx.bufnr) then + elseif is_undo_mode(bufnr) then -- Moving the cursor in insert mode may create an dirty table undo node. -- Therefore, when performing undo/redo, skip table validation. return UNDO_REDO_MODE @@ -98,16 +154,15 @@ local checks = { end, has_parser = function(bufnr) - return buffer.get(bufnr, buffer.IKEY.FILETYPE) ~= nil + return M.get(bufnr, M.IKEY.PARSER) ~= nil end, is_tirbuf = function(bufnr) - return M.is_flat(bufnr) ~= true + return M.is_tirbuf(bufnr) end, has_grid = function(bufnr) - local ctx = Context.from_buf(bufnr) - local has_grid = M.has_grid(ctx) + local has_grid = M.has_grid(bufnr) return has_grid == nil or has_grid == true end, @@ -116,9 +171,48 @@ local checks = { end, } ------------------------------------------------------------------------ +---@param bufnr number +---@return string +local function get_count(bufnr) + if not M.is_allow_plain(bufnr) then + return "P0G1" + end + local attrs = M.get(bufnr, M.IKEY.ATTRS) + local count = Attrs.get_count(attrs) + if not count then + return "NIL" + end + return string.format("P%dG%d", count.plain, count.grid) +end + +---@param bufnr number +---@return {[string]: boolean|integer|string|integer[][]|nil} +local function get_state(bufnr) + if not b[bufnr].tirenvi then + b[bufnr].tirenvi = initial_value + end + return b[bufnr].tirenvi +end + +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ + +---@param bufnr number +---@param key string +---@return any +function M.get(bufnr, key) + return get_state(bufnr)[key] +end + +---@param bufnr number +---@param key string +---@param val boolean|integer|string|integer[][]|nil +function M.set(bufnr, key, val) + local state = get_state(bufnr) + state[key] = val + b[bufnr].tirenvi = state +end ---@return boolean function M.is_vscode() @@ -146,12 +240,12 @@ function M.should_skip(bufnr, user_opts) return false end ----@param ctx Context +---@param bufnr number ---@param range3 Range3|nil ---@return boolean -function M.is_repair(ctx, range3) - local status = get_status(ctx, range3) - log_watch(ctx.bufnr, status, range3) +function M.is_repair(bufnr, range3) + local status = get_status(bufnr, range3) + log_watch(bufnr, status, range3) if status == INSERT_MODE then return false end @@ -166,22 +260,8 @@ end ---@param bufnr number ---@param value boolean -function M.set_buffer_flat(bufnr, value) - buffer.set(bufnr, buffer.IKEY.FLAT, value) -end - ----@param ctx Context ----@return string -local function get_count(ctx) - if not Context.is_allow_plain(ctx) then - return "P0G1" - end - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) - local count = Attrs.get_count(attrs) - if not count then - return "NIL" - end - return string.format("P%dG%d", count.plain, count.grid) +function M.set_buffer_tirbuf(bufnr, value) + M.set(bufnr, M.IKEY.TIRBUF, value) end ---@param bufnr number @@ -190,36 +270,73 @@ function M.debug_state(bufnr) if not log.is_debug() then return "" end - local ctx = Context.from_buf(bufnr) - local allow_plain = Context.is_allow_plain(ctx) - local flat - local is_flat = M.is_flat(bufnr) - if is_flat == nil then - flat = "NIL" - else - flat = is_flat and ",A," or "|A|" - end - local count = get_count(ctx) + local allow_plain = M.is_allow_plain(bufnr) + local form = M.is_tirbuf(bufnr) and "|A|" or ",A," + local count = get_count(bufnr) if not allow_plain then - log.assert(count == "P0G1", "grid must be enabled when plain is not allowed") + log.assert( + count == "P0G1", + "grid must be enabled when plain is not allowed" + ) end - return string.format("%s/%s/%s", allow_plain and "GFM" or "CSV", flat, count) + return string.format( + "%s/%s/%s", + allow_plain and "GFM" or "CSV", + form, + count + ) end ---@param bufnr number ----@return boolean|nil -function M.is_flat(bufnr) - return buffer.get(bufnr, buffer.IKEY.FLAT) +---@return boolean +function M.is_tirbuf(bufnr) + return M.get(bufnr, M.IKEY.TIRBUF) end ----@param ctx Context ----@return boolean|nil -function M.has_grid(ctx) - if not Context.is_allow_plain(ctx) then +---@param bufnr number +---@return boolean +function M.has_grid(bufnr) + if not M.is_allow_plain(bufnr) then return true end - local attrs = buffer.get(ctx.bufnr, buffer.IKEY.ATTRS) + local attrs = M.get(bufnr, M.IKEY.ATTRS) return Attrs.has_grid(attrs) end +---@param bufnr number +---@return boolean +function M.is_allow_plain(bufnr) + local parser = M.get(bufnr, M.IKEY.PARSER) + return parser and (parser.allow_plain or false) or false +end + +function M.clear_buf_local(bufnr) + M.set(bufnr, M.IKEY.ATTRS, nil) + M.set(bufnr, M.IKEY.DIRTY, nil) +end + +---@param bufnr number +---@param value boolean +function M.set_repair(bufnr, value) + M.set(bufnr, M.IKEY.REPAIR, value) +end + +---@param bufnr number +---@return boolean +function M.get_repair(bufnr) + local auto_repair = M.get(bufnr, M.IKEY.REPAIR) + if auto_repair == nil then + auto_repair = config.table.auto_reconcile + M.set_repair(bufnr, auto_repair) + end + return auto_repair +end + +---@param winid integer +---@return integer +function M.get_win_span(winid) + local info = fn.getwininfo(winid)[1] + return info.width - info.textoff +end + return M diff --git a/lua/tirenvi/io/buffer.lua b/lua/tirenvi/io/buffer.lua deleted file mode 100644 index e44b1382..00000000 --- a/lua/tirenvi/io/buffer.lua +++ /dev/null @@ -1,301 +0,0 @@ ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ ------ dependencies -local config = require("tirenvi.config") -local CursorNvim = require("tirenvi.cursor.nvim") -local Range = require("tirenvi.util.range") -local util = require("tirenvi.util.util") -local log = require("tirenvi.util.log") - -local M = {} - -local api = vim.api -local fn = vim.fn -local bo = vim.bo -local b = vim.b -local cache = { bufnr = -1, start = -1, lines = {}, } -local STEP = 25 - --- Buffer-local flags. -M.IKEY = { - -- true when in insert mode - INSERT_MODE = "insert_mode", - - -- Set only when the on_lines callback is attached. - ATTACHED = "attached", - - -- Depth of patch recursion - PATCH_DEPTH = "patch_depth", - - -- fn.undotree().seq_last - UNDO_TREE_LAST = "undo_tree_last", - - -- bo[bufnr].filetype - FILETYPE = "filetype", - - -- repair flag - REPAIR = "repair", - - -- block attrs - ATTRS = "attrs", - - -- dirty row # - DIRTY = "dirty", - - -- buffer is flat or tir-buffer - FLAT = "flat", -} - -local initial_value = { - [M.IKEY.INSERT_MODE] = false, - [M.IKEY.ATTACHED] = false, - [M.IKEY.PATCH_DEPTH] = 0, - [M.IKEY.UNDO_TREE_LAST] = -1, - [M.IKEY.FILETYPE] = nil, - [M.IKEY.REPAIR] = nil, - [M.IKEY.ATTRS] = nil, - [M.IKEY.DIRTY] = nil, - [M.IKEY.FLAT] = nil, -} - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - ----@param ctx Context ----@param cursor CursorBuf -local function reset_cursor_logical(ctx, cursor) - -- CursorNvim.move(ctx, cursor.row_cur, cursor.col_byte) -end - ----@param ctx Context ----@param cursor CursorBuf|nil -local function set_cursor_pos(ctx, cursor) - if not cursor then - CursorNvim.reset(ctx) - elseif cursor.restore_mode == "buffer" then - CursorNvim.restore_byte(ctx, cursor.row_cur, cursor.col_byte) - elseif cursor.restore_mode == "logical" then - reset_cursor_logical(ctx, cursor) - else - CursorNvim.reset(ctx) - end -end - ----@param bufnr number -local function set_undo_tree_last(bufnr) - local next = fn.undotree(bufnr).seq_last - M.set(bufnr, M.IKEY.UNDO_TREE_LAST, next) -end - ----@param ctx Context ----@param range Range ----@param lines string[] ----@param no_undo boolean ----@param cursor CursorBuf|nil -local function set_lines(ctx, range, lines, no_undo, cursor) - M.clear_cache() - local bufnr = ctx.bufnr - local undolevels = bo[bufnr].undolevels - if no_undo then - local undotree = fn.undotree(bufnr) - if undotree.seq_last == 0 then - bo[bufnr].undolevels = -1 - else - pcall(vim.cmd, "undojoin") - end - end - local start0, end0 = Range.to_vim(range) - start0 = math.max(start0, 0) - api.nvim_buf_set_lines(bufnr, start0, end0, false, lines) - set_undo_tree_last(bufnr) - set_cursor_pos(ctx, cursor) - bo[bufnr].undolevels = undolevels -end - ----@param bufnr number ----@param first integer -- 1-based ----@param last integer -- 1-based -local function get_lines_and_cache(bufnr, first, last) - local start = math.max(first - 1, 0) - local end0 = util.trim(last, start + 2 * STEP, M.line_count(bufnr)) - local start0 = util.trim(start, 0, end0 - 2 * STEP) - local lines = api.nvim_buf_get_lines(bufnr, start0, end0, false) - cache = { bufnr = bufnr, start = start0, lines = lines, } - log.debug("=== cache[#%d] lines[%d]='%s'...[%d]='%s'", cache.bufnr, - cache.start + 1, tostring(cache.lines[1]), - cache.start + #cache.lines, tostring(cache.lines[#cache.lines])) -end - ----@param bufnr number ----@param iline integer -- 1-based ----@return string|nil -local function get_line_from_cache(bufnr, iline) - if cache.bufnr ~= bufnr then - return nil - end - return cache.lines[iline - cache.start] -end - ----@param bufnr number ----@param first integer -- 1-based ----@param last integer -- 1-based ----@return string[] -local function get_lines_from_cache(bufnr, first, last) - if cache.bufnr ~= bufnr then - return {} - end - local istart = first - cache.start - local ilast = last - cache.start - if istart < 1 or ilast > #cache.lines then - return {} - end - return vim.list_slice(cache.lines, istart, ilast) -end - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ----@param bufnr number ----@return {[string]: boolean|integer|string|integer[][]|nil} -function M.get_state(bufnr) - if not b[bufnr].tirenvi then - b[bufnr].tirenvi = initial_value - end - return b[bufnr].tirenvi -end - ----@param bufnr number ----@param key string ----@return any -function M.get(bufnr, key) - return M.get_state(bufnr)[key] -end - ----@param bufnr number ----@param key string ----@param val boolean|integer|string|integer[][]|nil -function M.set(bufnr, key, val) - local state = M.get_state(bufnr) - state[key] = val - b[bufnr].tirenvi = state -end - ----@param ctx Context ----@param range Range ----@param lines string[] ----@param no_undo boolean|nil ----@param cursor CursorBuf|nil -function M.set_lines(ctx, range, lines, no_undo, cursor) - local bufnr = ctx.bufnr - M.set(bufnr, M.IKEY.PATCH_DEPTH, M.get(bufnr, M.IKEY.PATCH_DEPTH) + 1) - local before = fn.undotree(bufnr).seq_last - local ok, err = pcall(set_lines, ctx, range, lines, no_undo, cursor) - local after = fn.undotree(bufnr).seq_last - local first, last = Range.to_lua(range) - log.watch("UNDO", "%s[%d->%d]set_lines lines[%d]='%s'...[%d]='%s'", tostring(no_undo), - before, after, first, tostring(lines[1]), last, tostring(lines[#lines])) - M.set(bufnr, M.IKEY.PATCH_DEPTH, M.get(bufnr, M.IKEY.PATCH_DEPTH) - 1) - log.assert(M.get(bufnr, M.IKEY.PATCH_DEPTH) == 0, "PATCH_DEPTH should be 0 after set_lines") - if not ok then - error(err) - end -end - -function M.clear_buf_local(bufnr) - M.set(bufnr, M.IKEY.ATTRS, nil) - M.set(bufnr, M.IKEY.DIRTY, nil) -end - -function M.clear_cache() - cache = { bufnr = -1, start = -1, lines = {}, } -end - ----@param bufnr number ----@param first integer -- 1-based ----@param last integer -- 1-based ----@return string[] -function M.get_lines(bufnr, first, last) - first = math.max(1, first) - local nline = M.line_count(bufnr) - if last == -1 then - last = nline - end - last = math.min(nline, last) - local lines = get_lines_from_cache(bufnr, first, last) - if #lines ~= 0 then - return lines - end - get_lines_and_cache(bufnr, first - STEP, last + STEP) - return get_lines_from_cache(bufnr, first, last) -end - ----@param bufnr number ----@param iline integer -- 1-based ----@return string|nil -function M.get_line(bufnr, iline) - local line = get_line_from_cache(bufnr, iline) - if line then - return line - end - if cache.bufnr ~= bufnr then - M.get_lines(bufnr, iline, iline) - elseif iline - 1 < cache.start then - if iline >= 1 then - get_lines_and_cache(bufnr, iline - 2 * STEP, iline) - end - else - if iline <= M.line_count(bufnr) then - get_lines_and_cache(bufnr, iline, iline + 2 * STEP - 1) - end - end - return get_line_from_cache(bufnr, iline) -end - ----@param bufnr number ----@return integer -function M.line_count(bufnr) - return api.nvim_buf_line_count(bufnr) -end - ----@param bufnr number ----@param range Range ----@return string|nil ----@return string|nil -function M.get_lines_around(bufnr, range) - M.get_lines(bufnr, range.first - 1, range.last + 1) - return M.get_line(bufnr, range.first - 1), M.get_line(bufnr, range.last + 1) -end - ----@param bufnr number ----@param value boolean -function M.set_repair(bufnr, value) - M.set(bufnr, M.IKEY.REPAIR, value) -end - ----@param bufnr number ----@return boolean -function M.get_repair(bufnr) - local auto_repair = M.get(bufnr, M.IKEY.REPAIR) - if auto_repair == nil then - auto_repair = config.table.auto_reconcile - M.set_repair(bufnr, auto_repair) - end - return auto_repair -end - ----@param step integer -function M.set_step(step) - STEP = step -end - ----@param winid integer ----@return integer -function M.get_win_span(winid) - local info = fn.getwininfo(winid)[1] - return info.width - info.textoff -end - -return M diff --git a/lua/tirenvi/io/buffer_line_provider.lua b/lua/tirenvi/io/buffer_line_provider.lua deleted file mode 100644 index c738c44c..00000000 --- a/lua/tirenvi/io/buffer_line_provider.lua +++ /dev/null @@ -1,27 +0,0 @@ -local buffer = require("tirenvi.io.buffer") - ----@class BufferLineProvider : LineProvider -local M = {} - ----@param bufnr number ----@return BufferLineProvider -function M.new(bufnr) - return { - get_lines = function(first, last) - ---@diagnostic disable-next-line: redundant-parameter - return buffer.get_lines(bufnr, first, last) - end, - - get_line = function(row) - ---@diagnostic disable-next-line: redundant-parameter - return buffer.get_line(bufnr, row) - end, - - line_count = function() - ---@diagnostic disable-next-line: redundant-parameter - return buffer.line_count(bufnr) - end, - } -end - -return M diff --git a/lua/tirenvi/io/context.lua b/lua/tirenvi/io/context.lua new file mode 100644 index 00000000..4f4b9e46 --- /dev/null +++ b/lua/tirenvi/io/context.lua @@ -0,0 +1,30 @@ +local api = vim.api -- Neovim + +local LineProvider = require("tirenvi.io.buf_line_provider") -- IO + +local log = require("tirenvi.util.log") -- util + +-- ============================================================================= + +---@class Context +---@field bufnr number +---@field winid number +---@field line_provider LineProvider +local M = {} + +-- ============================================================================= +-- Public API + +---@param bufnr number|nil +---@return Context +function M.from_buf(bufnr) + local bufnr = bufnr or api.nvim_get_current_buf() + ---@type Context + return { + bufnr = bufnr, + winid = api.nvim_get_current_win(), + line_provider = LineProvider.new(bufnr), + } +end + +return M diff --git a/lua/tirenvi/cursor/cursor_buf.lua b/lua/tirenvi/io/cursor.lua similarity index 55% rename from lua/tirenvi/cursor/cursor_buf.lua rename to lua/tirenvi/io/cursor.lua index 8a8c446a..b15e40fe 100644 --- a/lua/tirenvi/cursor/cursor_buf.lua +++ b/lua/tirenvi/io/cursor.lua @@ -1,32 +1,26 @@ +-- ============================================================================= + ---@class CursorBuf ----@field restore_mode "none"|"buffer"|"logical" +---@field restore_mode "none"|"buffer"|"tir" ---@field row_cur integer -- current row (1-based) ---@field col_byte integer -- current column (1-based, byte index) ---@field col_char integer|nil -- current column (1-based, character index) ---@field col_disp integer|nil -- current column (1-based, screen index) ---@field line string|nil -- current line ---@field char string|nil -- char on cursor - local M = {} --- constants / defaults - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ - ------------------------------------------------------------------------ +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param row_cur integer ---@param col_byte integer ---@return CursorBuf function M.new(row_cur, col_byte) - return { - row_cur = row_cur, - col_byte = col_byte, - } + return { + row_cur = row_cur, + col_byte = col_byte, + } end return M diff --git a/lua/tirenvi/io/cursor_nvim.lua b/lua/tirenvi/io/cursor_nvim.lua new file mode 100644 index 00000000..80d10b0e --- /dev/null +++ b/lua/tirenvi/io/cursor_nvim.lua @@ -0,0 +1,136 @@ +local api = vim.api -- Neovim +local fn = vim.fn + +local CursorBuf = require("tirenvi.io.cursor") -- IO + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param line string +---@param col_char integer +---@return integer -- byte index (1-based) +local function char_to_byte(line, col_char) + local nchar = vim.str_utfindex(line) + 1 + log.assert( + col_char <= nchar, + "col_char(%d) <= nchar(%d) : %s", + col_char, + nchar, + line + ) + col_char = math.min(col_char, nchar) + -- str_byteindex(line, "utf-32", col_char - 1, false) + return vim.str_byteindex(line, col_char - 1) + 1 +end + +---@param cursor_buf CursorBuf +---@param line string +local function complete(cursor_buf, line) + cursor_buf.line = line + cursor_buf.col_char = vim.str_utfindex(line, cursor_buf.col_byte - 1) + 1 + cursor_buf.col_byte = char_to_byte(line, cursor_buf.col_char) + cursor_buf.char = fn.strcharpart(line, cursor_buf.col_char - 1, 1) + local prefix = fn.strcharpart(line, 0, cursor_buf.col_char - 1) + cursor_buf.col_disp = fn.strdisplaywidth(prefix) + 1 +end + +---@param ctx Context +---@param row_cur integer +---@param col_byte integer +---@return CursorBuf +local function get_cursor(ctx, row_cur, col_byte) + local cursor_buf = CursorBuf.new(row_cur, col_byte) + local line = ctx.line_provider.get_line(cursor_buf.row_cur) or "" + complete(cursor_buf, line) + return cursor_buf +end + +---@param row_cur integer +---@param col_byte integer +local function restore_cursor(row_cur, col_byte) + local view = fn.winsaveview() + view.lnum = row_cur + view.col = col_byte - 1 + fn.winrestview(view) +end + +---@param col_disp integer +---@param line string +---@return integer +local function disp_to_byte(line, col_disp) + local nchar = vim.str_utfindex(line) + local disp = 1 + for ichar = 1, nchar do + local char = fn.strcharpart(line, ichar - 1, 1) + local width = fn.strdisplaywidth(char) + if col_disp < disp + width then + return char_to_byte(line, ichar) + end + disp = disp + width + end + return #line + 1 +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param ctx Context +---@return CursorBuf +function M.capture(ctx) + local row_cur, col_byte0 = unpack(api.nvim_win_get_cursor(ctx.winid)) + return get_cursor(ctx, row_cur, col_byte0 + 1) +end + +---@param line string +---@param row_cur integer +---@param col_disp integer +---@return CursorBuf +function M.from_col_disp(line, row_cur, col_disp) + local col_byte = disp_to_byte(line, col_disp) + return CursorBuf.new(row_cur, col_byte) +end + +---@param ctx Context +function M.reset(ctx) + local cursor_buf = M.capture(ctx) + restore_cursor(cursor_buf.row_cur, cursor_buf.col_byte) +end + +--- Normal cursor movement. +--- Preserves window view and preferred column. +---@param ctx Context +---@param row_cur integer +---@param col_byte integer +function M.restore_byte(ctx, row_cur, col_byte) + local cursor_buf = get_cursor(ctx, row_cur, col_byte) + restore_cursor(cursor_buf.row_cur, cursor_buf.col_byte) +end + +---@param ctx Context +---@param row_cur integer +---@param col_disp integer +function M.restore_disp(ctx, row_cur, col_disp) + local line = ctx.line_provider.get_line(row_cur) or "" + local cursor_buf = M.from_col_disp(line, row_cur, col_disp) + M.restore_byte(ctx, row_cur, cursor_buf.col_byte) +end + +--- Direct cursor update. +--- Use for Visual/Visual Block. +---@param ctx Context +---@param row_cur integer +---@param col_byte integer +function M.move_byte(ctx, row_cur, col_byte) + api.nvim_win_set_cursor(ctx.winid, { row_cur, math.max(0, col_byte - 1) }) +end + +function M.restore() end + +return M diff --git a/lua/tirenvi/io/dirty.lua b/lua/tirenvi/io/dirty.lua index 01dc15da..958e9857 100644 --- a/lua/tirenvi/io/dirty.lua +++ b/lua/tirenvi/io/dirty.lua @@ -1,85 +1,95 @@ -local config = require("tirenvi.config") -local namespaces = require("tirenvi.io.namespaces") -local buffer = require("tirenvi.io.buffer") -local Range = require("tirenvi.util.range") +local api = vim.api -- Neovim + +local config = require("tirenvi.config") -- Root + +local Attrs = require("tirenvi.core.attrs") -- Core + +local namespaces = require("tirenvi.io.namespaces") -- IO +local buf_state = require("tirenvi.io.buf_state") + +local Range = require("tirenvi.util.range") -- Util local log = require("tirenvi.util.log") -local Attrs = require("tirenvi.core.attrs") + +-- ============================================================================= local M = {} +-- ============================================================================= +--#region Private + ---@param bufnr number ---@param range Range ---@param id integer|nil ---@param text string local function show_marks(bufnr, range, id, text) - local start0, end0 = Range.to_vim(range) - local opts = { - id = id, - end_row = end0 - 1, - end_col = 1000, - right_gravity = false, - end_right_gravity = true, - strict = false, - invalidate = false, - -- - hl_group = config.ui.highlight.line, - hl_eol = false, - sign_text = ".", - sign_hl_group = config.ui.highlight.sign, - } - if vim.log.levels.DEBUG >= config.log.level then - opts.virt_text = { { text, "Comment" } } - opts.virt_text_pos = "eol" - if id then - opts.sign_text = tostring(id):sub(-2) - end - end - vim.api.nvim_buf_set_extmark(bufnr, namespaces.DIRTY, start0, 0, opts) + local start0, end0 = Range.to_vim(range) + local opts = { + id = id, + end_row = end0 - 1, + end_col = 1000, + right_gravity = false, + end_right_gravity = true, + strict = false, + invalidate = false, + -- + hl_group = config.ui.highlight.line, + hl_eol = false, + sign_text = ".", + sign_hl_group = config.ui.highlight.sign, + } + if vim.log.levels.DEBUG >= config.log.level then + opts.virt_text = { { text, "Comment" } } + opts.virt_text_pos = "eol" + if id then + opts.sign_text = tostring(id):sub(-2) + end + end + api.nvim_buf_set_extmark(bufnr, namespaces.DIRTY, start0, 0, opts) end ---@param bufnr number ---@param ranges Range[] local function set_dirty_ranges(bufnr, ranges) - buffer.set(bufnr, buffer.IKEY.DIRTY, ranges) - log.watch("INVD", ranges) - for irange, range in ipairs(ranges) do - show_marks(bufnr, range, irange, "dirty") - end + buf_state.set(bufnr, buf_state.IKEY.DIRTY, ranges) + log.watch("INVD", ranges) + for irange, range in ipairs(ranges) do + show_marks(bufnr, range, irange, "dirty") + end end ---@param bufnr number local function set_dirty_attrs(bufnr) - local invalid_attrs = M.get_invalid_attrs(bufnr) - for _, attr in ipairs(invalid_attrs) do - local irow = attr.range.first - local range = Range.from_lua(irow, irow) - show_marks(bufnr, range, nil, "boundary") - end + local invalid_attrs = M.get_invalid_attrs(bufnr) + for _, attr in ipairs(invalid_attrs) do + local irow = attr.range.first + local range = Range.from_lua(irow, irow) + show_marks(bufnr, range, nil, "boundary") + end end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param bufnr number ---@param ranges Range[] function M.set_ranges(bufnr, ranges) - vim.api.nvim_buf_clear_namespace(bufnr, namespaces.DIRTY, 0, -1) - set_dirty_ranges(bufnr, ranges) - set_dirty_attrs(bufnr) + api.nvim_buf_clear_namespace(bufnr, namespaces.DIRTY, 0, -1) + set_dirty_ranges(bufnr, ranges) + set_dirty_attrs(bufnr) end ---@param bufnr number ---@return Range[] function M.get_ranges(bufnr) - return buffer.get(bufnr, buffer.IKEY.DIRTY) or {} + return buf_state.get(bufnr, buf_state.IKEY.DIRTY) or {} end ---@param bufnr number ---@return Attr[] function M.get_invalid_attrs(bufnr) - local attrs = buffer.get(bufnr, buffer.IKEY.ATTRS) or {} - return Attrs.get_invalid_attrs(attrs) + local attrs = buf_state.get(bufnr, buf_state.IKEY.ATTRS) or {} + return Attrs.get_invalid_attrs(attrs) end return M diff --git a/lua/tirenvi/io/namespaces.lua b/lua/tirenvi/io/namespaces.lua index 7b157fee..11549c44 100644 --- a/lua/tirenvi/io/namespaces.lua +++ b/lua/tirenvi/io/namespaces.lua @@ -1,6 +1,10 @@ +local api = vim.api -- Neovim + +-- ============================================================================= + local M = {} -M.ATTR = vim.api.nvim_create_namespace("tirenvi_attr") -M.DIRTY = vim.api.nvim_create_namespace("tirenvi_dirty") +M.ATTR = api.nvim_create_namespace("tirenvi_attr") +M.DIRTY = api.nvim_create_namespace("tirenvi_dirty") return M diff --git a/lua/tirenvi/io/read_result.lua b/lua/tirenvi/io/read_result.lua new file mode 100644 index 00000000..f9874b05 --- /dev/null +++ b/lua/tirenvi/io/read_result.lua @@ -0,0 +1,31 @@ +local Range = require("tirenvi.util.range") -- Util +local log = require("tirenvi.util.log") + +-- ============================================================================= + +---@class ReadResult +---@field range Range +---@field lines string[] +---@field attrs Attr[] +---@field cursor_buf CursorBuf +local M = {} + +-- ============================================================================= +-- Public API + +---@param range Range +---@return ReadResult +function M.new_reader(range) + return { + range = range, + } +end + +---@param self ReadResult +---@return integer -- 1-based +---@return integer -- 1-based +function M:lua_range() + return Range.to_lua(self.range) +end + +return M diff --git a/lua/tirenvi/io/reader.lua b/lua/tirenvi/io/reader.lua index c2f28c4c..5505e4ba 100644 --- a/lua/tirenvi/io/reader.lua +++ b/lua/tirenvi/io/reader.lua @@ -1,43 +1,42 @@ -local Attrs = require("tirenvi.core.attrs") -local buffer = require("tirenvi.io.buffer") +local buf_lines = require("tirenvi.io.buf_lines") -- IO local attr_store = require("tirenvi.io.attr_store") -local CursorNvim = require("tirenvi.cursor.nvim") -local ReadResult = require("tirenvi.app.read_result") -local log = require("tirenvi.util.log") +local ReadResult = require("tirenvi.io.read_result") +local CursorNvim = require("tirenvi.io.cursor_nvim") -local M = {} +local Attrs = require("tirenvi.core.attrs") -- Core + +local log = require("tirenvi.util.log") -- Util --- constants / defaults +-- ============================================================================= --- private helpers +local M = {} ------------------------------------------------------------------------ +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param ctx Context ---@param range Range ----@pram opts {restore_mode: "none"|"buffer"|"logical"|nil, curosr:boolean|nil} +---@pram opts {restore_mode: "none"|"buffer"|"tir"|nil, curosr:boolean|nil} ---@return ReadResult function M.read(ctx, range, opts) - opts = opts or {} - local restore_mode = opts.restore_mode - local result = ReadResult.new_reader(range) - result.attrs = attr_store.read(ctx.bufnr) - local first, last = ReadResult.lua_range(result) - result.lines = buffer.get_lines(ctx.bufnr, first, last) - if opts.cursor ~= false then - result.cursor = M.cursor(ctx) - result.cursor.restore_mode = restore_mode or "none" - end - log.watch("ATTR", Attrs.debug_attrs(result.attrs, "[0]CHACHED ATTRS:")) - return result + opts = opts or {} + local restore_mode = opts.restore_mode + local result = ReadResult.new_reader(range) + result.attrs = attr_store.read(ctx.bufnr) + local first, last = ReadResult.lua_range(result) + result.lines = buf_lines.get_lines(ctx.bufnr, first, last) + if opts.cursor ~= false then + result.cursor_buf = M.cursor_buf(ctx) + result.cursor_buf.restore_mode = restore_mode or "none" + end + log.watch("ATTR", Attrs.debug_attrs(result.attrs, "[0]CHACHED ATTRS:")) + return result end ---@param ctx Context ---@return CursorBuf -function M.cursor(ctx) - return CursorNvim.capture(ctx) +function M.cursor_buf(ctx) + return CursorNvim.capture(ctx) end return M diff --git a/lua/tirenvi/io/request.lua b/lua/tirenvi/io/request.lua new file mode 100644 index 00000000..66d111ce --- /dev/null +++ b/lua/tirenvi/io/request.lua @@ -0,0 +1,44 @@ +local Range = require("tirenvi.util.range") -- Util +local Range3 = require("tirenvi.util.range3") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +---@class Request +---@field range Range +---@field lines string[] +---@field no_undo boolean +---@field cursor_buf CursorBuf +local M = {} + +-- ============================================================================= +-- Public API + +---@param r_req ReadResult +---@param lines string[] +---@param no_undo boolean|nil +---@return Request +function M.new_writer(r_req, lines, no_undo) + ---@type Request + return { + range = r_req.range, + lines = lines, + no_undo = no_undo or false, + cursor_buf = r_req.cursor_buf, + } +end + +---@param self Request +---@return Range3 +function M:get_range3() + local first, last = Range.to_lua(self.range) + return Range3.new(first, last, first + #self.lines - 1) +end + +---@param self Request +---@return boolean +function M:is_no_undo() + return self.no_undo == true +end + +return M diff --git a/lua/tirenvi/io/writer.lua b/lua/tirenvi/io/writer.lua index 88e5ce8c..c79fdf8a 100644 --- a/lua/tirenvi/io/writer.lua +++ b/lua/tirenvi/io/writer.lua @@ -1,29 +1,32 @@ -local Request = require("tirenvi.app.request") -local dirty_range = require("tirenvi.core.dirty_range") -local buffer = require("tirenvi.io.buffer") -local dirty = require("tirenvi.io.dirty") -local log = require("tirenvi.util.log") +local dirty_range = require("tirenvi.parser.dirty_range") -- Parser -local M = {} +local buf_lines = require("tirenvi.io.buf_lines") -- IO +local dirty = require("tirenvi.io.dirty") +local Request = require("tirenvi.io.request") --- constants / defaults +local log = require("tirenvi.util.log") -- Util ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= ------------------------------------------------------------------------ +local M = {} + +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param ctx Context ---@param req Request function M.write(ctx, req) - buffer.set_lines(ctx, req.range, req.lines, Request.is_no_undo(req), req.cursor) - local range3 = Request.get_range3(req) - local prev_ranges = dirty.get_ranges(ctx.bufnr) - local new_ranges = dirty_range.remove(prev_ranges, range3) - dirty.set_ranges(ctx.bufnr, new_ranges) + buf_lines.set_lines( + ctx, + req.range, + req.lines, + Request.is_no_undo(req), + req.cursor_buf + ) + local range3 = Request.get_range3(req) + local prev_ranges = dirty.get_ranges(ctx.bufnr) + local new_ranges = dirty_range.remove(prev_ranges, range3) + dirty.set_ranges(ctx.bufnr, new_ranges) end return M diff --git a/lua/tirenvi/parser/buf_parser.lua b/lua/tirenvi/parser/buf_parser.lua index f89c043c..d8e85726 100644 --- a/lua/tirenvi/parser/buf_parser.lua +++ b/lua/tirenvi/parser/buf_parser.lua @@ -1,30 +1,67 @@ ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ - ------ dependencies -local CONST = require("tirenvi.constants") -local Request = require("tirenvi.app.request") -local Document = require("tirenvi.core.document") +local CONST = require("tirenvi.constants") -- Root +local config = require("tirenvi.config") + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser + +local buf_state = require("tirenvi.io.buf_state") -- IO + +local Document = require("tirenvi.core.document") -- Core local Record = require("tirenvi.core.record") local Attr = require("tirenvi.core.attr") local Attrs = require("tirenvi.core.attrs") -local Context = require("tirenvi.app.context") -local Range = require("tirenvi.util.range") + +local Range = require("tirenvi.util.range") -- Util local Range3 = require("tirenvi.util.range3") local log = require("tirenvi.util.log") +-- ============================================================================= + local M = {} --- constants / defaults +-- ============================================================================= +--#region Private ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +---@param bufline string +---@param pipe string +---@param embedded_key string|nil +---@return Record_grid +local function new_from_bufline(bufline, pipe, embedded_key) + local pos = string.find(bufline, pipe, 1, true) or 1 + local prefix = string.sub(bufline, 1, pos - 1) + if vim.trim(prefix) == embedded_key then + bufline = string.sub(bufline, pos) + end + local cells = tir_buf.get_cells(bufline) + local record = Record.grid.new(cells) + if vim.trim(prefix) == embedded_key then + record.prefix = prefix + end + record._has_continuation = pipe == config.marks.pipec + return record +end ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ +---@param bufline string +---@param embedded_key string|nil +---@return Record +local function bufline_to_records(bufline, embedded_key) + local pipe = tir_buf.get_pipe_char(bufline) + if pipe then + return new_from_bufline(bufline, pipe, embedded_key) + else + return Record.plain.new(bufline) + end +end + +---@param buflines string[] +---@param embedded_key string|nil +---@return Record[] +local function buflines_to_records(buflines, embedded_key) + local records = {} + for index = 1, #buflines do + records[index] = bufline_to_records(buflines[index], embedded_key) + end + return records +end ---@param records Record[] ---@param r_result ReadResult @@ -77,15 +114,43 @@ local function promote_empty_lines(records, r_result, allow_plain, range3) end end +---@param records Record[] +---@return string[] +local function to_buflines(records) + local pipec = config.marks.pipec + local pipen = config.marks.pipe + local buflines = {} + for _, record in ipairs(records) do + local kind = record.kind + if kind == CONST.KIND.PLAIN then + buflines[#buflines + 1] = record.line or "" + elseif kind == CONST.KIND.GRID then + local pipe = record._has_continuation and pipec or pipen + local row_items = record.row + local row = table.concat(row_items, pipe) + row = pipe .. row .. pipe + local line = (record.prefix or "") .. row + buflines[#buflines + 1] = line + end + end + return buflines +end + +--#endregion +-- ============================================================================= +-- Public API + ---@param ctx Context ---@param r_result ReadResult ---@param opts any ---@return Document function M.parse(ctx, r_result, opts) - local records = Record.from_buflines(r_result.lines) - local allow_plain = Context.is_allow_plain(ctx) + local embedded_key = Attrs.get_embedded_key(r_result.attrs) + local records = buflines_to_records(r_result.lines, embedded_key) + local allow_plain = buf_state.is_allow_plain(ctx.bufnr) promote_empty_lines(records, r_result, allow_plain, opts.range3) - local bufdoc = Document.new_bufdoc(records, allow_plain, opts.attrs, opts.first) + local bufdoc = + Document.new_bufdoc(records, allow_plain, opts.attrs, opts.first) return bufdoc end @@ -93,13 +158,13 @@ end ---@return string[] function M.unparse(bufdoc) local records = Document.serialize_to_buf(bufdoc) - return Record.to_buflines(records) + return to_buflines(records) end ---@param lines string[] ---@return boolean function M.table_is_aligned(lines) - local records = Record.from_buflines(lines) + local records = buflines_to_records(lines) local bufdoc = Document.new_bufdoc(records, false) Document.infer_consistent_attr(bufdoc) return Document.has_width(bufdoc) diff --git a/lua/tirenvi/parser/cursor.lua b/lua/tirenvi/parser/cursor.lua new file mode 100644 index 00000000..71a446e9 --- /dev/null +++ b/lua/tirenvi/parser/cursor.lua @@ -0,0 +1,60 @@ +local fn = vim.fn -- Neovim + +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser + +local Attrs = require("tirenvi.core.attrs") -- Core +local Attr = require("tirenvi.core.attr") +local CursorTir = require("tirenvi.core.cursor") + +local CursorNvim = require("tirenvi.io.cursor_nvim") -- IO + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +-- Public API + +---@param cursor_tir CursorTir +---@param attrs Attr[] +---@param line_provider LineProvider +---@return CursorBuf +function M.to_buf(cursor_tir, attrs, line_provider) + local row_cur, col_disp = M.to_cursor(attrs, cursor_tir) + local line = line_provider.get_line(row_cur) or "" + local prefix = tir_buf.get_prefix_part(line) + col_disp = col_disp + fn.strdisplaywidth(prefix) + local cursor_buf = CursorNvim.from_col_disp(line, row_cur, col_disp) + return cursor_buf +end + +---@param attrs Attr[] +---@param row_cur integer +---@param col_disp integer +---@return CursorTir +function M.to_tir(attrs, row_cur, col_disp) + local _, iblock = Attrs.get(attrs, row_cur) + if not iblock then + return {} + end + local attr = attrs[iblock] + log.assert(attr, "invalid position %d", row_cur) + local irow = row_cur - attr.range.first + 1 + local icol, offset = Attr.to_cell_col(attr, col_disp) + return CursorTir.new(iblock, irow, icol, offset) +end + +---@param attrs Attr[] +---@param cursor_tir CursorTir +---@return integer +---@return integer +function M.to_cursor(attrs, cursor_tir) + local attr = attrs[cursor_tir.iblock] + local row_cur = attr.range.first + cursor_tir.irow - 1 + local col_disp = Attr.get_start_pos(attr, cursor_tir.icol) + return row_cur, col_disp +end + +return M diff --git a/lua/tirenvi/parser/dirty_range.lua b/lua/tirenvi/parser/dirty_range.lua new file mode 100644 index 00000000..76e36546 --- /dev/null +++ b/lua/tirenvi/parser/dirty_range.lua @@ -0,0 +1,188 @@ +local tir_buf = require("tirenvi.parser.tir_buf") -- Parser + +local Attr = require("tirenvi.core.attr") -- Core +local Attrs = require("tirenvi.core.attrs") + +local Range3 = require("tirenvi.util.range3") -- Util +local Range = require("tirenvi.util.range") +local util = require("tirenvi.util.util") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param line_provider LineProvider +---@param range Range +local function expand_continue_lines(line_provider, range) + if Range.is_empty(range) then + return + end + local first, last = Range.to_lua(range) + local lines = line_provider.get_lines(first, last) + local prev = range.first - 1 + local prev_line = line_provider.get_line(prev) + while tir_buf.is_continue_line(prev_line) do + prev = prev - 1 + prev_line = line_provider.get_line(prev) + end + range.first = prev + 1 + ---@type string|nil + local last_line = lines[#lines] + local last = range.last + while tir_buf.is_continue_line(last_line) do + last = last + 1 + last_line = line_provider.get_line(last) + end + range.last = last +end + +---@param line_provider LineProvider +---@param range3 Range3 +---@return Range +local function get_new_range(line_provider, range3) + local new_range = Range3.get_new_range(range3) + expand_continue_lines(line_provider, new_range) + return new_range +end + +---@param line_provider LineProvider +---@param prev_ranges Range[] +---@param range3 Range3 +---@return Range[] +local function adjust(line_provider, prev_ranges, range3) + local ranges1, _, ranges3 = + Range.split(prev_ranges, Range.from_lua(range3.first, range3.last)) + Range.shift(ranges3, Range3.get_delta(range3)) + local range2 = get_new_range(line_provider, range3) + local new_ranges = ranges1 + if not Range.is_empty(range2) then + new_ranges[#new_ranges + 1] = range2 + end + util.extend(new_ranges, ranges3) + return new_ranges +end + +---@param attr Attr|nil +---@param line string|nil +---@return boolean +local function is_valid(attr, line) + if not line then + return true + end + if not attr then + return false + end + local pipe = tir_buf.get_pipe_char(line) + if not pipe then + return Attr.is_plain(attr) + end + if not tir_buf.is_normal_grid(line, pipe) then + return false + end + local widths = tir_buf.get_widths(line) + if #attr.columns ~= #widths then + return false + end + for icol, width in ipairs(widths) do + if attr.columns[icol].width ~= width then + return false + end + end + return true +end + +---@param next_attr Attr|nil +---@param line string|nil +---@return boolean +local function is_valid_bound(next_attr, line) + if not line then + return true + end + if not next_attr or Attr.is_plain(next_attr) then + return true + end + local pipe = tir_buf.get_pipe_char(line) + if not pipe then + return true + end + if not tir_buf.is_normal_grid(line, pipe) then + return false + end + local widths = tir_buf.get_widths(line) + if #next_attr.columns ~= #widths then + return false + end + for icol, width in ipairs(widths) do + if next_attr.columns[icol].width ~= width then + return false + end + end + return true +end + +---@param line_provider LineProvider +---@param range Range +---@param attrs Attr[] +---@return Range[] +local function check_dirty_1range(line_provider, range, attrs) + local inv_ranges = {} + for row_cur = range.first, range.last do + local attr = Attrs.get(attrs, row_cur) + local line = line_provider.get_line(row_cur) + if not is_valid(attr, line) then + Range.push(inv_ranges, row_cur) + elseif Attr.is_grid(attr) then + attr = Attrs.get(attrs, row_cur + 1) + if not is_valid_bound(attr, line) then + Range.push(inv_ranges, row_cur) + end + end + end + return inv_ranges +end + +---@param line_provider LineProvider +---@param new_ranges Range[] +---@param attrs Attr[] +---@return Range[] +local function check_dirty(line_provider, new_ranges, attrs) + local inv_ranges = {} + for _, range in ipairs(new_ranges) do + local ranges = check_dirty_1range(line_provider, range, attrs) + util.extend(inv_ranges, ranges) + end + return inv_ranges +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param line_provider LineProvider +---@param prev_ranges Range[] +---@param attrs Attr[] +---@param range3 Range3 +---@return Range[] +function M.reconcile(line_provider, prev_ranges, attrs, range3) + local new_ranges = adjust(line_provider, prev_ranges, range3) + local inv_ranges = check_dirty(line_provider, new_ranges, attrs) + return Range.merge(inv_ranges) +end + +---@param prev_ranges Range[] +---@param range3 Range3 +---@return Range[] +function M.remove(prev_ranges, range3) + local ranges1, _, ranges3 = + Range.split(prev_ranges, Range.from_lua(range3.first, range3.last)) + Range.shift(ranges3, Range3.get_delta(range3)) + local new_ranges = ranges1 + util.extend(new_ranges, ranges3) + return Range.merge(new_ranges) +end + +return M diff --git a/lua/tirenvi/parser/flat_parser.lua b/lua/tirenvi/parser/flat_parser.lua index d8cf8080..c5b7d372 100644 --- a/lua/tirenvi/parser/flat_parser.lua +++ b/lua/tirenvi/parser/flat_parser.lua @@ -1,39 +1,35 @@ ---- Utility for converting between flat format lines and NDJSON lines using external parsers. ---- ---- Purpose: ---- - parse: Convert flat lines to NDJSON via parser command. ---- - unparse: Convert NDJSON to flat lines via parser command. ---- ---- Notes: ---- - Errors from parser commands throw domain-specific errors (handled by guard.lua). ---- - Logging at debug level is only active in development mode. - ------ dependencies -local Document = require("tirenvi.core.document") -local Context = require("tirenvi.app.context") -local Parser = require("tirenvi.parser.parser") -local errors = require("tirenvi.util.errors") +local Parser = require("tirenvi.parser.parser") -- Parser + +local buf_state = require("tirenvi.io.buf_state") -- IO + +local Document = require("tirenvi.core.document") -- Core + +local errors = require("tirenvi.util.errors") -- Util local log = require("tirenvi.util.log") --- module +-- ============================================================================= local M = {} --- constants / defaults - ---@class Vim_system ---@field code integer ---@field signal? integer ---@field stdout? string ---@field stderr? string --- private helpers +-- ============================================================================= +--#region Private --- Convert flat lines to NDJSON lines ----@param fllines string[] ---@param parser Parser +---@param fllines string[] +---@param cursor_buf CursorBuf ---@return string[] NDJSON lines -local function flat_to_jslines(fllines, parser) - local js_string = Parser.run(parser, "parse", fllines) +local function flat_to_jslines(parser, fllines, cursor_buf) + local options = vim.deepcopy(parser.options) or {} + if parser.executable == "tir-embedded" then + options[#options + 1] = "--cursor-line=" .. cursor_buf.row_cur + end + local js_string = Parser.run(parser, "parse", options, fllines) return vim.split(js_string, "\n", { plain = true }) end @@ -45,7 +41,11 @@ local function jslines_to_ndjsons(jslines) if jsline ~= nil and jsline ~= "" then local ok, ndjson = pcall(vim.json.decode, jsline) if not ok then - error(errors.new_domain_error(errors.invalid_json_error(jsline, ndjson))) + error( + errors.new_domain_error( + errors.invalid_json_error(jsline, ndjson) + ) + ) end ndjsons[#ndjsons + 1] = ndjson end @@ -57,7 +57,13 @@ end ---@return string|nil local function ndjson_to_line(ndjson) local ok, line = pcall(vim.json.encode, ndjson) - log.assert(ok, ("tirenvi: internal JSON encode failure\n%s\nerror: %s"):format(vim.inspect(ndjson), line)) + log.assert( + ok, + ("tirenvi: internal JSON encode failure\n%s\nerror: %s"):format( + vim.inspect(ndjson), + line + ) + ) return line end @@ -79,32 +85,71 @@ end ---@param parser Parser ---@return string[] flat lines local function jslines_to_flat(jslines, parser) - local fl_string = Parser.run(parser, "unparse", jslines) + local fl_string = Parser.run(parser, "unparse", parser.options, jslines) local fllines = vim.split(fl_string, "\n") --log.debug(util.to_hex(table.concat(fllines, "\n")):sub(1, 80) .. " ") return fllines end --- public API +---@param ndjsons Ndjson[] +local function expand_grid_prefix(ndjsons) + local prefix + local is_first = true + for _, ndjson in ipairs(ndjsons) do + if ndjson.kind == "grid" then + if is_first then + prefix = ndjson.prefix + is_first = false + end + ndjson.prefix = prefix + else + prefix = nil + is_first = true + end + end +end + +---@param ndjsons Ndjson[] +local function collapse_grid_prefix(ndjsons) + local is_first = true + for _, ndjson in ipairs(ndjsons) do + if ndjson.kind == "grid" then + if is_first then + is_first = false + else + ndjson.prefix = nil + end + else + is_first = true + end + end +end + +--#endregion +-- ============================================================================= +-- Public API ---@param ctx Context ---@param jslines string[] ---@return Document function M.from_jslines(ctx, jslines) local ndjsons = jslines_to_ndjsons(jslines) - return Document.new_tirdoc(ndjsons, Context.is_allow_plain(ctx)) + return Document.new_tirdoc(ndjsons, buf_state.is_allow_plain(ctx.bufnr)) end ----@param ctx Context +---@param parser Parser ---@param r_result ReadResult ---@return Document -function M.parse(ctx, r_result) - local jslines = flat_to_jslines(r_result.lines, ctx.parser) +function M.parse(parser, r_result) + local jslines = flat_to_jslines(parser, r_result.lines, r_result.cursor_buf) local ndjsons = jslines_to_ndjsons(jslines) - return Document.new_tirdoc(ndjsons, Context.is_allow_plain(ctx)) + expand_grid_prefix(ndjsons) + local allow_plain = parser.allow_plain or false + local tirdoc = Document.new_tirdoc(ndjsons, allow_plain) + return tirdoc end ----@param tirdoc Document +---@param tirdoc Document ---@return string[] function M.to_jslines(tirdoc) local ndjsons = Document.serialize_to_flat(tirdoc) @@ -112,14 +157,21 @@ function M.to_jslines(tirdoc) end --- Convert display lines back to TSV format ----@param ctx Context ----@param tirdoc Document +---@param parser Parser +---@param tirdoc Document ---@return string[] -function M.unparse(ctx, tirdoc) +function M.unparse(parser, tirdoc) local ndjsons = Document.serialize_to_flat(tirdoc) + collapse_grid_prefix(ndjsons) local jslines = ndjsons_to_lines(ndjsons) - log.debug("[%d]='%s'...[%d]='%s'", 1, tostring(jslines[1]), #jslines, tostring(jslines[#jslines])) - return jslines_to_flat(jslines, ctx.parser) + log.debug( + "[%d]='%s'...[%d]='%s'", + 1, + tostring(jslines[1]), + #jslines, + tostring(jslines[#jslines]) + ) + return jslines_to_flat(jslines, parser) end return M diff --git a/lua/tirenvi/parser/parser.lua b/lua/tirenvi/parser/parser.lua index b9e74107..631a9839 100644 --- a/lua/tirenvi/parser/parser.lua +++ b/lua/tirenvi/parser/parser.lua @@ -1,16 +1,11 @@ ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ +local fn = vim.fn -- Neovim -local config = require("tirenvi.config") -local errors = require("tirenvi.util.errors") -local log = require("tirenvi.util.log") +local config = require("tirenvi.config") -- Root ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ +local errors = require("tirenvi.util.errors") -- Util +local log = require("tirenvi.util.log") -local M = {} +-- ============================================================================= ---@class Parser ---@field executable string Parser executable name @@ -21,58 +16,63 @@ local M = {} ---@field _installed_version? string installed version ---@field _err_code? string error code ---@field _checked? boolean is checked +local M = {} local ERR = { - EXECUTABLE_NOT_FOUND = "executable_not_found", - VERSION_COMMAND_FAILED = "version_command_failed", - VERSION_PARSE_FAILED = "version_parse_failed", - VERSION_TOO_OLD = "version_too_old", + EXECUTABLE_NOT_FOUND = "executable_not_found", + VERSION_COMMAND_FAILED = "version_command_failed", + VERSION_PARSE_FAILED = "version_parse_failed", + VERSION_TOO_OLD = "version_too_old", } M.ERR = ERR -local fn = vim.fn --- private helpers +-- ============================================================================= +--#region Private --- Get parser configuration for a file. ---@param filetype string|nil ---@return Parser|nil local function get_parser_for_filetype(filetype) - if not filetype then - return nil - end - local parser = config.parser_map[filetype] - if not parser or not parser.executable then - return nil - end - return parser + if not filetype then + return nil + end + local parser = config.parser_map[filetype] + if not parser or not parser.executable then + return nil + end + return parser end ---@param command string[] ---@param input string[] ---@return Vim_system local function vim_system(command, input) - log.debug("=== [exec] %s ===", table.concat(command, " ")) - local result = vim.system(command, { stdin = input }):wait() - if result.stdout and #result.stdout > 0 then - -- log.debug(util.to_hex(result.stdout):sub(1, 80) .. " ") - end - return result + log.debug("=== [exec] %s ===", table.concat(command, " ")) + local result = vim.system(command, { stdin = input }):wait() + if result.stdout and #result.stdout > 0 then + -- log.debug(util.to_hex(result.stdout):sub(1, 80) .. " ") + end + return result end ---@param self Parser local function ensure_parser(self) - if not self._err_code then - return - end - local message - if self._err_code == ERR.EXECUTABLE_NOT_FOUND then - message = errors.not_found_parser_error(self) - elseif self._err_code == ERR.VERSION_TOO_OLD then - message = errors.outdated_parser_error(self) - else - message = errors.no_parser_error() - end - error(errors.new_domain_error(message)) + if not self._err_code then + return + end + local message + if self._err_code == ERR.EXECUTABLE_NOT_FOUND then + message = errors.not_found_parser_error(self.executable) + elseif self._err_code == ERR.VERSION_TOO_OLD then + message = errors.outdated_parser_error( + self.executable, + self.required_version, + self._installed_version + ) + else + message = errors.no_parser_error() + end + error(errors.new_domain_error(message)) end --- run external parser command @@ -82,98 +82,105 @@ end ---@param lines string[] Input lines ---@return string stdout local function run_parser(executable, subcmd, options, lines) - local command = { executable, subcmd } - if options then - vim.list_extend(command, options) - end - local result = vim_system(command, lines) - if result.code ~= 0 then - error(errors.new_domain_error(errors.vim_system_error(result, command))) - end - return result.stdout + local command = { executable, subcmd } + if options then + vim.list_extend(command, options) + end + local result = vim_system(command, lines) + if result.code ~= 0 then + error(errors.new_domain_error(errors.vim_system_error(result, command))) + end + return result.stdout end ---@param self Parser ---@return string|nil local function get_string_version(self) - if fn.executable(self.executable) ~= 1 then - error({ code = ERR.EXECUTABLE_NOT_FOUND }) - end - self._installed_version = vim.trim(fn.system({ self.executable, "--version" })) - if vim.v.shell_error ~= 0 then - error({ code = ERR.VERSION_COMMAND_FAILED }) - end - return self._installed_version + if fn.executable(self.executable) ~= 1 then + error({ code = ERR.EXECUTABLE_NOT_FOUND }) + end + self._installed_version = + vim.trim(fn.system({ self.executable, "--version" })) + if vim.v.shell_error ~= 0 then + -- TODO + self._installed_version = + vim.trim(fn.system({ self.executable, "version" })) + if vim.v.shell_error ~= 0 then + error({ code = ERR.VERSION_COMMAND_FAILED }) + end + end + return self._installed_version end ---@param self Parser ---@return integer|nil local function get_int_version(self) - local str_ver = get_string_version(self) - return config.version_to_integer(str_ver) + local str_ver = get_string_version(self) + return config.version_to_integer(str_ver) end ---@return boolean local function is_available_version(self) - local iver = get_int_version(self) - if M.is_old(self._required_version_int, iver) then - error({ code = ERR.VERSION_TOO_OLD }) - end - return true + local iver = get_int_version(self) + if M.is_old(self._required_version_int, iver) then + error({ code = ERR.VERSION_TOO_OLD }) + end + return true end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param self Parser ---@param subcmd string Subcommand ("parse" or "unparse") +---@param options string[] ---@param lines string[] Input lines ---@return string stdout -function M.run(self, subcmd, lines) - ensure_parser(self) - return run_parser(self.executable, subcmd, self.options, lines) +function M.run(self, subcmd, options, lines) + ensure_parser(self) + return run_parser(self.executable, subcmd, options, lines) end ---@param self Parser ---@return boolean ---@return string|nil function M.check(self) - if self._checked then - return self._err_code == nil, self._err_code - end - local ok, err = pcall(is_available_version, self) - self._checked = true - local error_code - if not ok then - if type(err) == "table" and err.code then - error_code = err.code - else - error_code = tostring(err) - end - end - return ok, error_code + if self._checked then + return self._err_code == nil, self._err_code + end + local ok, err = pcall(is_available_version, self) + self._checked = true + local error_code + if not ok then + if type(err) == "table" and err.code then + error_code = err.code + else + error_code = tostring(err) + end + end + return ok, error_code end ---@param filetype string|nil ---@return Parser|nil function M.resolve_parser(filetype) - local parser = get_parser_for_filetype(filetype) - if not parser then - return nil - end - _, parser._err_code = M.check(parser) - return parser + local parser = get_parser_for_filetype(filetype) + if not parser then + return nil + end + _, parser._err_code = M.check(parser) + return parser end ---@param require integer|nil ---@param target integer|nil ---@return boolean function M.is_old(require, target) - if require == nil or target == nil then - return false - end - return require > target + if require == nil or target == nil then + return false + end + return require > target end return M diff --git a/lua/tirenvi/parser/tir_buf.lua b/lua/tirenvi/parser/tir_buf.lua new file mode 100644 index 00000000..5c25a5bb --- /dev/null +++ b/lua/tirenvi/parser/tir_buf.lua @@ -0,0 +1,271 @@ +local config = require("tirenvi.config") -- Root + +local buf_state = require("tirenvi.io.buf_state") -- IO +local CursorNvim = require("tirenvi.io.cursor_nvim") + +local Cell = require("tirenvi.core.cell") -- Core + +local util = require("tirenvi.util.util") -- Util +local Range = require("tirenvi.util.range") +local log = require("tirenvi.util.log") + +-- ============================================================================= + +local M = {} + +-- ============================================================================= +--#region Private + +---@param line string +---@return string +local function remove_start_pipe(line) + local pipen = config.marks.pipe + local pipec = config.marks.pipec + if util.start_with(line, pipen) then + line = line:sub(#pipen + 1) + elseif util.start_with(line, pipec) then + line = line:sub(#pipec + 1) + end + return line +end + +---@param line string +---@return string +local function remove_end_pipe(line) + local pipen = config.marks.pipe + local pipec = config.marks.pipec + if util.end_with(line, pipen) then + line = line:sub(1, -#pipen - 1) + elseif util.end_with(line, pipec) then + line = line:sub(1, -#pipec - 1) + end + return line +end + +---@param base_pipe boolean +---@param target string|nil +---@return boolean +local function is_block_boundary(base_pipe, target) + if not target then + return true + end + return base_pipe ~= (M.get_pipe_char(target) ~= nil) +end + +---@param provider LineProvider +---@param irow integer +---@param step integer -- -1 or 1 +---@return integer +local function find_block_edge(provider, irow, step) + local line = provider.get_line(irow) + local base_pipe = (M.get_pipe_char(line) ~= nil) + while true do + irow = irow + step + local line = provider.get_line(irow) + if is_block_boundary(base_pipe, line) then + return irow - step + end + end +end + +---@param line string +---@param pipe string +---@return integer[] +local function get_pipe_byte_positions(line, pipe) + local indexes = {} + local index = 1 + while index <= #line do + if line:sub(index, index + #pipe - 1) == pipe then + indexes[#indexes + 1] = index + index = index + #pipe + else + index = index + 1 + end + end + if #indexes > 0 then + if indexes[1] ~= 1 then + table.insert(indexes, 1, 0) + end + end + return indexes +end + +--#endregion +-- ============================================================================= +-- Public API + +---@param line string +---@return integer[] +function M.get_pipe_byte_position(line) + local pipen = config.marks.pipe + local indexes = get_pipe_byte_positions(line, pipen) + if #indexes == 0 then + local pipec = config.marks.pipec + indexes = get_pipe_byte_positions(line, pipec) + end + return indexes +end + +---@param byte_pos integer[] +---@param icol integer +---@return integer|nil +function M.get_current_col_index(byte_pos, icol) + for index, ibyte in ipairs(byte_pos) do + if icol < ibyte then + return index - 1 + end + end + return nil +end + +---@param ctx Context +---@param line_provider LineProvider +---@param irow integer +---@return integer +function M.get_block_top_nrow(ctx, line_provider, irow) + if buf_state.is_allow_plain(ctx.bufnr) then + return find_block_edge(line_provider, irow, -1) + else + return 1 + end +end + +---@param ctx Context +---@param line_provider LineProvider +---@param irow integer +---@return integer +function M.get_block_bottom_nrow(ctx, line_provider, irow) + if buf_state.is_allow_plain(ctx.bufnr) then + return find_block_edge(line_provider, irow, 1) + else + return line_provider.line_count() + end +end + +---@param line string +---@return string[] +function M.get_cells(line) + local pipen = config.marks.pipe + local pipec = config.marks.pipec + line = remove_start_pipe(line) + line = remove_end_pipe(line) + line = line:gsub(vim.pesc(pipec), pipen) + return vim.split(line, pipen, { plain = true }) +end + +---@param line string +---@param pipe string +---@return boolean +function M.is_normal_grid(line, pipe) + if not util.start_with(line, pipe) then + return false + end + if not util.end_with(line, pipe) then + return false + end + return true +end + +---@param line string +---@return integer[] +function M.get_widths(line) + return M.get_max_widths(line, true) +end + +---@param line string +---@param no_wrap boolean|nil +---@return integer[] +function M.get_max_widths(line, no_wrap) + local cells = M.get_cells(line) + local widths = Cell.get_max_widths(cells, no_wrap) + return widths +end + +---@param line string|nil +---@return string|nil +function M.get_pipe_char(line) + local pipen = config.marks.pipe + local pipec = config.marks.pipec + if not line then + return nil + end + if line:find(pipen, 1, true) then + return pipen + end + if line:find(pipec, 1, true) then + return pipec + end + return nil +end + +---@param lines string[] +---@return boolean +function M.has_pipe(lines) + for _, line in ipairs(lines) do + if M.get_pipe_char(line) then + return true + end + end + return false +end + +---@param line string|nil +---@return boolean +function M.is_continue_line(line) + local pipec = config.marks.pipec + if not line then + return false + end + return M.get_pipe_char(line) == pipec +end + +---@param ctx Context +---@param count integer +---@param is_around boolean +---@return Rect|nil +---@return string[] +function M.get_block_rect(ctx, count, is_around) + local cursor_buf = CursorNvim.capture(ctx) + local row_cur = cursor_buf.row_cur + local col_byte = cursor_buf.col_byte + local cline = ctx.line_provider.get_line(row_cur) or "" + local cbyte_pos = M.get_pipe_byte_position(cline) + if #cbyte_pos == 0 then + return nil, {} + end + local colIndex = M.get_current_col_index(cbyte_pos, col_byte) + if not colIndex then + return nil, {} + end + local trow = M.get_block_top_nrow(ctx, ctx.line_provider, row_cur) + local brow = M.get_block_bottom_nrow(ctx, ctx.line_provider, row_cur) + local lines = ctx.line_provider.get_lines(trow, brow) + local tline = lines[1] + local bline = lines[#lines] + local tbyte_pos = M.get_pipe_byte_position(tline) + local bbyte_pos = M.get_pipe_byte_position(bline) + local end_index = colIndex + count + end_index = math.min(end_index, #bbyte_pos) + local pipe = M.get_pipe_char(tline) + local rect = { + row = Range.from_lua(trow, brow), + col = Range.from_lua( + tbyte_pos[colIndex] + (is_around and 0 or #pipe), + bbyte_pos[end_index] - 1 + ), + } + return rect, lines +end + +---@param line string +---@return string +function M.get_prefix_part(line) + local pipe = M.get_pipe_char(line) + if not pipe then + return "" + end + local pos_byte = string.find(line, pipe, 1, true) or 1 + return string.sub(line, 1, pos_byte - 1) +end + +return M diff --git a/lua/tirenvi/ui.lua b/lua/tirenvi/ui.lua index 8dfdf215..aa0fca03 100644 --- a/lua/tirenvi/ui.lua +++ b/lua/tirenvi/ui.lua @@ -1,134 +1,141 @@ -local config = require("tirenvi.config") -local buffer = require("tirenvi.io.buffer") -local log = require("tirenvi.util.log") +local api = vim.api -- Neovim +local fn = vim.fn -local matches = {} +local config = require("tirenvi.config") -- Root + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= -local M = {} +local M = {} -local api = vim.api -local fn = vim.fn +local matches = {} ------------------------------------------------------------------------ --- utils ------------------------------------------------------------------------ +-- ============================================================================= +--#region Private ---@param targets string[] ---@return string local function get_safe_link_name(targets) - for _, target in ipairs(targets) do - local ok, hl = pcall(api.nvim_get_hl, 0, { name = target }) - if ok and hl and next(hl) ~= nil then - return target - end - end - return "Normal" + for _, target in ipairs(targets) do + local ok, hl = pcall(api.nvim_get_hl, 0, { name = target }) + if ok and hl and next(hl) ~= nil then + return target + end + end + return "Normal" end ---@param name string ---@param targets string[] local function safe_link_multi(name, targets) - local ns_id = 0 - local target = get_safe_link_name(targets) - api.nvim_set_hl(ns_id, name, { link = target }) + local ns_id = 0 + local target = get_safe_link_name(targets) + api.nvim_set_hl(ns_id, name, { link = target }) end local function diagnostic_setup() - local ns_id = 0 - fn.sign_define("TirenviSign", { text = "◆", texthl = "ErrorMsg" }) - api.nvim_set_hl(ns_id, "TirenviDebugLine", { bg = "#404000" }) - api.nvim_set_hl(ns_id, "TirenviDirty", { bg = "#2a2a1a", italic = true, }) - api.nvim_set_hl(ns_id, "TirenviDirtySign", { link = "DiagnosticWarn" }) + local ns_id = 0 + fn.sign_define("TirenviSign", { text = "◆", texthl = "ErrorMsg" }) + api.nvim_set_hl(ns_id, "TirenviDebugLine", { bg = "#404000" }) + api.nvim_set_hl(ns_id, "TirenviDirty", { bg = "#2a2a1a", italic = true }) + api.nvim_set_hl(ns_id, "TirenviDirtySign", { link = "DiagnosticWarn" }) end local function special_setup() - local ns_id = 0 - api.nvim_set_hl(ns_id, "TirenviPadding", {}) - local target = get_safe_link_name({ "@punctuation.special.markdown", "Delimiter", "Special", }) - local special = api.nvim_get_hl(ns_id, { name = target }) - api.nvim_set_hl(ns_id, "TirenviPipeNoHbar", { link = target }) - api.nvim_set_hl(ns_id, "TirenviPipeHbar", { - fg = special.fg, - bg = special.bg, - underline = true, - nocombine = true, - }) - api.nvim_set_hl(ns_id, "TirenviHbar", { - underline = true, - sp = special.fg, - nocombine = true, - }) - api.nvim_set_hl(ns_id, "Conceal", { link = "TirenviPipeNoHbar" }) - safe_link_multi("TirenviSpecialChar", { "NonText", }) + local ns_id = 0 + api.nvim_set_hl(ns_id, "TirenviPadding", {}) + local target = get_safe_link_name({ + "@punctuation.special.markdown", + "Delimiter", + "Special", + }) + local special = api.nvim_get_hl(ns_id, { name = target }) + api.nvim_set_hl(ns_id, "TirenviPipe", { + fg = special.fg, + bg = special.bg, + }) + api.nvim_set_hl(ns_id, "TirenviInnerText", { + underline = true, + sp = special.fg, + }) + api.nvim_set_hl(ns_id, "TirenviInnerPipe", { + fg = special.fg, + bg = special.bg, + underline = true, + }) + api.nvim_set_hl(ns_id, "Conceal", { link = "TirenviPipe" }) + safe_link_multi("TirenviSpecialChar", { "NonText" }) end ------------------------------------------------------------------------ --- special chars ------------------------------------------------------------------------ - ---@param winid integer ---@param group string ---@param pattern string ---@param priority integer local function add_match(winid, group, pattern, priority) - local id = fn.matchadd(group, pattern, priority) - matches[winid] = matches[winid] or {} - table.insert(matches[winid], id) + local id = fn.matchadd(group, pattern, priority) + matches[winid] = matches[winid] or {} + table.insert(matches[winid], id) end local function pat_v(s) - return "\\V" .. s + return "\\V" .. s end -local function pat_line_inner(pipe) - return "^" .. pipe .. "\\zs.*\\ze" .. pipe .. "$" +local function pat_inner_pipe(pipe) + return pipe .. "\\zs.*\\ze" .. pipe end -local function pat_line_start(pipe) - return "^" .. pipe +local function pat_inner_text(pipe) + return pipe .. "\\zs.\\{-}\\ze" .. pipe end -local function pat_line_end(pipe) - return pipe .. "$" -end +--#endregion +-- ============================================================================= +-- Public API function M.setup() - special_setup() - diagnostic_setup() + special_setup() + diagnostic_setup() end ---@param winid integer function M.special_clear(winid) - local ids = matches[winid] - if not ids then return end - for _, id in ipairs(ids) do - pcall(fn.matchdelete, id) - end - matches[winid] = nil + local ids = matches[winid] + if not ids then + return + end + for _, id in ipairs(ids) do + pcall(fn.matchdelete, id) + end + matches[winid] = nil end ---@param winid integer function M.special_apply(winid) - local pipen = config.marks.pipe - local pipec = config.marks.pipec - M.special_clear(winid) - add_match(winid, "TirenviPadding", pat_v(config.marks.padding), 10) - add_match(winid, "TirenviSpecialChar", pat_v(config.marks.lf), 20) - add_match(winid, "TirenviSpecialChar", pat_v(config.marks.tab), 20) - add_match(winid, "TirenviPipeHbar", pat_v(pipen), 30) - add_match(winid, "TirenviHbar", pat_line_inner(pipen), 20) - add_match(winid, "TirenviPipeNoHbar", pat_line_start(pipen), 40) - add_match(winid, "TirenviPipeNoHbar", pat_line_end(pipen), 40) - add_match(winid, "TirenviPipeNoHbar", pat_v(pipec), 30) - vim.opt_local.conceallevel = config.ui.conceal.level - vim.opt_local.concealcursor = config.ui.conceal.cursor - local pattern = fn.escape(pipec, [[/\]]) - local command = string.format([[syntax match TirPipeC /%s/ conceal cchar=%s]], pattern, pipen) - vim.cmd(command) + local pipen = config.marks.pipe + local pipec = config.marks.pipec + M.special_clear(winid) + add_match(winid, "TirenviPadding", pat_v(config.marks.padding), 10) + add_match(winid, "TirenviSpecialChar", pat_v(config.marks.lf), 20) + add_match(winid, "TirenviSpecialChar", pat_v(config.marks.tab), 20) + add_match(winid, "TirenviPipe", pat_v(pipec), 30) + add_match(winid, "TirenviPipe", pat_v(pipen), 30) + add_match(winid, "TirenviInnerPipe", pat_inner_pipe(pipen), 40) + add_match(winid, "TirenviInnerText", pat_inner_text(pipen), 50) + vim.opt_local.conceallevel = config.ui.conceal.level + vim.opt_local.concealcursor = config.ui.conceal.cursor + local pattern = fn.escape(pipec, [[/\]]) + local command = string.format( + [[syntax match TirPipeC /%s/ conceal cchar=%s]], + pattern, + pipen + ) + vim.cmd(command) end api.nvim_create_autocmd("ColorScheme", { - callback = M.setup + callback = M.setup, }) return M diff --git a/lua/tirenvi/util/errors.lua b/lua/tirenvi/util/errors.lua index 428776ec..ad35de56 100644 --- a/lua/tirenvi/util/errors.lua +++ b/lua/tirenvi/util/errors.lua @@ -1,37 +1,22 @@ ---- Centralized error definitions and error message builders for tirenvi. ---- ---- This module: ---- - Defines domain error tag ---- - Provides user-facing error message builders ---- - Centralizes all error strings ---- - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ +-- ============================================================================= local M = {} ------------------------------------------------------------------------ --- Domain error tag ------------------------------------------------------------------------ - --- Unique tag used to identify domain validation errors. M.DOMAIN_ERROR = {} ------------------------------------------------------------------------ --- Static error messages ------------------------------------------------------------------------ local PREFIX = "tirenvi: " M.ERR = { - INVALID_TABLE_MESSAGE = PREFIX .. "This change would break the table structure. Changes have been undone.", - ENSURE_TIRVIM_MODE = PREFIX .. "This command is only available in a tir-vim buffer.", - TABLE_IS_NOT_ALIGNED = PREFIX .. "Cannot select column: table is not aligned.", + INVALID_TABLE_MESSAGE = PREFIX + .. "This change would break the table structure. Changes have been undone.", + ENSURE_TIRVIM_MODE = PREFIX + .. "This command is only available in a tir-vim buffer.", + TABLE_IS_NOT_ALIGNED = PREFIX + .. "Cannot select column: table is not aligned.", } ------------------------------------------------------------------------ --- Error builders ------------------------------------------------------------------------ +-- ============================================================================= +-- Public API --- Create a domain error object. ---@param message string @@ -50,8 +35,8 @@ function M.err_no_usable_characters(missing) table.sort(missing) return string.format( PREFIX - .. "No usable characters found for marks: [%s].\n" - .. "Please configure alternative characters in tirenvi.setup().", + .. "No usable characters found for marks: [%s].\n" + .. "Please configure alternative characters in tirenvi.setup().", table.concat(missing, ", ") ) end @@ -74,7 +59,11 @@ function M.vim_system_error(system, command) end return string.format( - PREFIX .. "External command failed\n\n" .. "Command:\n %s\n\n" .. "Exit code: %d\n\n" .. "Error output:\n%s", + PREFIX + .. "External command failed\n\n" + .. "Command:\n %s\n\n" + .. "Exit code: %d\n\n" + .. "Error output:\n%s", table.concat(command, " "), system.code, stderr @@ -82,29 +71,38 @@ function M.vim_system_error(system, command) end --- Parser command not found in PATH. ----@param parser Parser +---@param executable string ---@return string -function M.not_found_parser_error(parser) +function M.not_found_parser_error(executable) return string.format( - PREFIX .. "Required command '%s' not found.\n\n" .. "Install it with:\n\n" .. " pip install %s", - parser.executable, - parser.executable + PREFIX + .. "Required command '%s' not found.\n\n" + .. "Install it with:\n\n" + .. " pip install %s", + executable, + executable ) end --- Parser version is too old. ----@param parser Parser +---@param executable string +---@param required_version string +---@param installed_version string ---@return string -function M.outdated_parser_error(parser) +function M.outdated_parser_error( + executable, + required_version, + installed_version +) return string.format( PREFIX - .. "Command '%s' version is too old.\n\n" - .. "Required version: %s\n" - .. "Installed version: %s\n\n" - .. "Use :checkhealth tirenvi for details.", - parser.executable, - parser.required_version or "unknown", - parser._installed_version or "unknown" + .. "Command '%s' version is too old.\n\n" + .. "Required version: %s\n" + .. "Installed version: %s\n\n" + .. "Use :checkhealth tirenvi for details.", + executable, + required_version or "unknown", + installed_version or "unknown" ) end @@ -112,13 +110,21 @@ end ---@param message string ---@return string function M.invalid_json_error(jsline, message) - return string.format(PREFIX .. "tirenvi: invalid JSON from parser\n%s\nerror: %s", jsline, message) + return string.format( + PREFIX .. "tirenvi: invalid JSON from parser\n%s\nerror: %s", + jsline, + message + ) end function M.table_merge_warning(irow) return string.format( - PREFIX .. "Tables were not merged: talble attribute differ in line %d-%d.\n" .. - "Align the table attribute to merge them.", irow, irow + 2) + PREFIX + .. "Tables were not merged: talble attribute differ in line %d-%d.\n" + .. "Align the table attribute to merge them.", + irow, + irow + 2 + ) end return M diff --git a/lua/tirenvi/util/guard.lua b/lua/tirenvi/util/guard.lua deleted file mode 100644 index 4b79c124..00000000 --- a/lua/tirenvi/util/guard.lua +++ /dev/null @@ -1,63 +0,0 @@ ---- Execution guard for user-facing commands. ---- ---- Wraps functions with error handling: ---- - Domain errors -> user notification only ---- - Unexpected errors -> traceback notification ---- - ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ - -local errors = require("tirenvi.util.errors") -local notify = require("tirenvi.util.notify") -local buf_state = require("tirenvi.io.buf_state") -local buffer = require("tirenvi.io.buffer") -local log = require("tirenvi.util.log") - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ - -local M = {} - ------------------------------------------------------------------------ --- Public API ------------------------------------------------------------------------ - ---- Wrap a function with guarded error handling. ----@param func fun(...) ----@param opts? { on_error?: fun(err, ...) } ----@return fun(...) -function M.guarded(func, opts) - opts = opts or {} - buffer.clear_cache() - - return function(...) - if buf_state.is_vscode() then - return - end - local args = { ... } - - local ok, result = xpcall(func, function(err) - if type(err) == "table" and err.tag == errors.DOMAIN_ERROR then - return err - end - return debug.traceback(err, 2) - end, ...) - - if not ok then - if opts.on_error then - opts.on_error(unpack(args), result) - end - - if type(result) == "table" and result.tag == errors.DOMAIN_ERROR then - notify.error(result.message) - else - notify.error(result) - end - end - end -end - -return M diff --git a/lua/tirenvi/util/log.lua b/lua/tirenvi/util/log.lua index a5f1fda3..56893927 100644 --- a/lua/tirenvi/util/log.lua +++ b/lua/tirenvi/util/log.lua @@ -1,12 +1,15 @@ -local config = require("tirenvi.config") -local notify = require("tirenvi.util.notify") - -local M = {} - -local api = vim.api +local api = vim.api -- Neovim local bo = vim.bo local fn = vim.fn +local config = require("tirenvi.config") -- Root + +local notify = require("tirenvi.util.notify") -- Util + +-- ============================================================================= + +local M = {} + local levels = vim.log.levels local level_names = {} @@ -25,6 +28,9 @@ local last_mem = 0 local last_time = uv.now() local monitoring = false +-- ============================================================================= +--#region Private + ---@return integer local function get_tick() local bufnr = api.nvim_get_current_buf() @@ -144,28 +150,36 @@ end local category_hl_map = {} local category_match_id = {} -vim.api.nvim_set_hl(0, "TirLog_Entry", { fg = "#55ffff", bold = true }) -vim.api.nvim_set_hl(0, "TirLog_Error", { fg = "#ffffff", bg = "#ff0000", bold = true }) -vim.api.nvim_set_hl(0, "TirLog_num", { fg = "#cc55cc", bold = true }) - +api.nvim_set_hl(0, "TirLog_Entry", { fg = "#55ffff", bold = true }) +api.nvim_set_hl( + 0, + "TirLog_Error", + { fg = "#ffffff", bg = "#ff0000", bold = true } +) +api.nvim_set_hl(0, "TirLog_num", { fg = "#cc55cc", bold = true }) + +---@param bufnr number local function apply_log_highlight(bufnr) - local winid = vim.fn.bufwinid(bufnr) - if winid == -1 then return end - vim.api.nvim_win_call(winid, function() - vim.fn.clearmatches() - vim.fn.matchadd("TirLog_Entry", "===") - --vim.fn.matchadd("TirLog_Error", [[\[[0-9,]\+\]]]) - vim.fn.matchadd("TirLog_num", "\\[[0-9,]\\+\\]") - vim.fn.matchadd("TirLog_Error", "\\[ERROR\\]") + local winid = fn.bufwinid(bufnr) + if winid == -1 then + return + end + api.nvim_win_call(winid, function() + fn.clearmatches() + fn.matchadd("TirLog_Entry", "===") + --fn.matchadd("TirLog_Error", [[\[[0-9,]\+\]]]) + fn.matchadd("TirLog_num", "\\[[0-9,]\\+\\]") + fn.matchadd("TirLog_Error", "\\[ERROR\\]") for cat, hl in pairs(category_hl_map) do - vim.fn.matchadd(hl, "\\[" .. cat .. "\\]") + fn.matchadd(hl, "\\[" .. cat .. "\\]") end end) end -local aug = vim.api.nvim_create_augroup("TirenviLogHL", { clear = true }) +local aug = api.nvim_create_augroup("TirenviLogHL", { clear = true }) +---@param bufnr number local function register_autocmds(bufnr) - vim.api.nvim_create_autocmd("BufWinEnter", { + api.nvim_create_autocmd("BufWinEnter", { group = aug, buffer = bufnr, callback = function(args) @@ -254,8 +268,12 @@ end local unpack = table.unpack or unpack -- Lua 5.1/5.2 compatibility local palette = { - "#ff5555", "#55aaaa", "#ffff55", - "#aaff55", "#aa55ff", "#ffaa55", + "#ff5555", + "#55aaaa", + "#ffff55", + "#aaff55", + "#aa55ff", + "#ffaa55", } local assigned = {} @@ -265,8 +283,8 @@ local function pick_color(cat) if assigned[cat] then return assigned[cat] end - local hash = vim.fn.sha256(cat) - local base = (vim.fn.str2nr(hash:sub(1, 8), 16) % #palette) + 1 + local hash = fn.sha256(cat) + local base = (fn.str2nr(hash:sub(1, 8), 16) % #palette) + 1 for i = 0, #palette - 1 do local idx = ((base + i - 1) % #palette) + 1 local color = palette[idx] @@ -289,7 +307,7 @@ local function ensure_category_highlight(category) local hl = "TirLog_" .. category local color = pick_color(category) - vim.api.nvim_set_hl(0, hl, { fg = color, bold = true }) + api.nvim_set_hl(0, hl, { fg = color, bold = true }) category_hl_map[category] = hl return hl @@ -300,9 +318,9 @@ local function ensure_match(bufnr, category, hl) return end - vim.api.nvim_buf_call(bufnr, function() + api.nvim_buf_call(bufnr, function() local pattern = "\\[" .. category .. "\\]" - local id = vim.fn.matchadd(hl, pattern) + local id = fn.matchadd(hl, pattern) category_match_id[category] = id end) end @@ -349,7 +367,16 @@ local function emit(force, level, opts, fmt, ...) elseif category then name = category end - local final = string.format("[%s]%s%s[%s][%s %d] %s", PREFIX, ts, mon, name, file, line, msg) + local final = string.format( + "[%s]%s%s[%s][%s %d] %s", + PREFIX, + ts, + mon, + name, + file, + line, + msg + ) if config.log.output == "buffer" then write_buffer(final) elseif config.log.output == "file" then @@ -361,6 +388,10 @@ local function emit(force, level, opts, fmt, ...) end end +--#endregion +-- ============================================================================= +-- Public API + ---@param ... unknown function M.debug(...) emit(false, levels.DEBUG, {}, ...) diff --git a/lua/tirenvi/util/notify.lua b/lua/tirenvi/util/notify.lua index 807213c0..6e02b855 100644 --- a/lua/tirenvi/util/notify.lua +++ b/lua/tirenvi/util/notify.lua @@ -1,17 +1,9 @@ ---- UI notification layer for tirenvi. ---- ---- Wraps vim.notify and ensures safe scheduling. ---- - ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ +-- ============================================================================= local M = {} ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= +--#region Private ---@param msg string ---@param level integer @@ -31,9 +23,9 @@ local function emit(msg, level) end end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param msg string function M.error(msg) diff --git a/lua/tirenvi/util/range.lua b/lua/tirenvi/util/range.lua index 8ce8a178..983c947f 100644 --- a/lua/tirenvi/util/range.lua +++ b/lua/tirenvi/util/range.lua @@ -1,182 +1,181 @@ +local api = vim.api -- Neovim + +local log = require("tirenvi.util.log") -- Util + +-- ============================================================================= + ---@class Range ---@field first integer ---@field last integer -local log = require("tirenvi.util.log") +local M = {} -local M = {} - -local api = vim.api +-- ============================================================================= +--#region Private ---@param first integer ---@param last integer ---@return Range local function new(first, last) - return { - first = first, - last = last, - } + return { + first = first, + last = last, + } end M.WHOLE = { first = nil, last = nil } ---@return Range[] local function sort(ranges) - table.sort(ranges, function(prev, next) - return prev.first < next.first - end) - return ranges + table.sort(ranges, function(prev, next) + return prev.first < next.first + end) + return ranges end ---@param prev Range ---@param next Range ---@return Range|nil local function union_range(prev, next) - if prev.last + 1 < next.first then - return nil - end - return new(math.min(prev.first, next.first), math.max(prev.last, next.last)) + if prev.last + 1 < next.first then + return nil + end + return new(math.min(prev.first, next.first), math.max(prev.last, next.last)) end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ +---@param self Range|Attr ---@return Range function M:get_range() - return self.range or self + return self.range or self end ---@param first0 integer ---@param last0 integer ---@return Range --- function M.from_vim(first0, last0) --- return new(first0 + 1, last0) --- end - ---@param first integer ---@param last integer ---@return Range function M.from_lua(first, last) - return new(first, last) + return new(first, last) end ---@param first integer ---@param last integer ---@return Range function M.from_lua_normal(first, last) - return { - first = math.min(first, last), - last = math.max(first, last), - } + return { + first = math.min(first, last), + last = math.max(first, last), + } end ---@return boolean function M:is_empty() - return self.first > self.last + return self.first > self.last end ---@return string function M:short() - return string.format("(%d,%d)", self.first, self.last) + return string.format("(%d,%d)", self.first, self.last) end ---@param self Range|nil ---@param target Range|nil ---@return boolean function M:intersects(target) - if not self or not target then - return false - end - if self.last < target.first then - return false - end - if target.last < self.first then - return false - end - return true + if not self or not target then + return false + end + if self.last < target.first then + return false + end + if target.last < self.first then + return false + end + return true end ---@param self Range ---@param index integer ---@return boolean function M:contains(index) - return self.first <= index and index <= self.last + return self.first <= index and index <= self.last end ---@param ranges Range[] ---@return Range[] function M.merge(ranges) - if #ranges == 0 then - return ranges - end - ranges = sort(ranges) - local unions = { ranges[1] } - for index = 2, #ranges do - local merged = union_range(unions[#unions], ranges[index]) - if merged then - unions[#unions] = merged - else - unions[#unions + 1] = ranges[index] - end - end - return unions + if #ranges == 0 then + return ranges + end + ranges = sort(ranges) + local unions = { ranges[1] } + for index = 2, #ranges do + local merged = union_range(unions[#unions], ranges[index]) + if merged then + unions[#unions] = merged + else + unions[#unions + 1] = ranges[index] + end + end + return unions end ---@param self Range ---@return integer ---@return integer function M:to_vim() - if M.is_whole(self) then - return 0, api.nvim_buf_line_count(0) - end - return self.first - 1, self.last + if M.is_whole(self) then + return 0, api.nvim_buf_line_count(0) + end + return self.first - 1, self.last end ---@param self Range ---@return integer ---@return integer function M:to_lua() - if M.is_whole(self) then - return 1, api.nvim_buf_line_count(0) - end - return self.first, self.last + if M.is_whole(self) then + return 1, api.nvim_buf_line_count(0) + end + return self.first, self.last end function M:is_whole() - return not self.first or not self.last + return not self.first or not self.last end ---@param self Range ---@param first integer function M:move_to(first) - local count = self.last - self.first + 1 - self.first = first - self.last = first + count - 1 + local count = self.last - self.first + 1 + self.first = first + self.last = first + count - 1 end ---@param self Range[] ---@param delta integer function M:shift(delta) - for _, range in ipairs(self) do - range.first = range.first + delta - range.last = range.last + delta - end + for _, range in ipairs(self) do + range.first = range.first + delta + range.last = range.last + delta + end end ---@param self Range[] ---@param irow integer function M:push(irow) - if #self == 0 then - self[1] = new(irow, irow) - else - local last = self[#self] - if last.last + 1 == irow then - last.last = irow - else - self[#self + 1] = new(irow, irow) - end - end + if #self == 0 then + self[1] = new(irow, irow) + else + local last = self[#self] + if last.last + 1 == irow then + last.last = irow + else + self[#self + 1] = new(irow, irow) + end + end end ---@generic T @@ -186,9 +185,9 @@ end ---@return T[] ---@return T[] function M.split(items, range) - local range1 = M.from_lua(1, range.first - 1) - local range2 = M.from_lua(range.last + 1, math.huge) - return M.slice(items, range1), M.slice(items, range), M.slice(items, range2) + local range1 = M.from_lua(1, range.first - 1) + local range2 = M.from_lua(range.last + 1, math.huge) + return M.slice(items, range1), M.slice(items, range), M.slice(items, range2) end ---@generic T @@ -196,20 +195,20 @@ end ---@param range Range ---@return T[] function M.slice(items, range) - local new_items = {} - if M.is_empty(range) then - return new_items - end - for _, item in ipairs(items) do - if M.intersects(M.get_range(item), range) then - local new_item = vim.deepcopy(item) - local item_range = M.get_range(new_item) - item_range.first = math.max(item_range.first, range.first) - item_range.last = math.min(item_range.last, range.last) - new_items[#new_items + 1] = new_item - end - end - return new_items + local new_items = {} + if M.is_empty(range) then + return new_items + end + for _, item in ipairs(items) do + if M.intersects(M.get_range(item), range) then + local new_item = vim.deepcopy(item) + local item_range = M.get_range(new_item) + item_range.first = math.max(item_range.first, range.first) + item_range.last = math.min(item_range.last, range.last) + new_items[#new_items + 1] = new_item + end + end + return new_items end return M diff --git a/lua/tirenvi/util/range3.lua b/lua/tirenvi/util/range3.lua index 5611b6a9..ee59f9b1 100644 --- a/lua/tirenvi/util/range3.lua +++ b/lua/tirenvi/util/range3.lua @@ -1,45 +1,50 @@ +local Range = require("tirenvi.util.range") -- Util + +-- ============================================================================= + ---@class Range3 ---@field first integer ---@field last integer ---@field new_last integer local Range3 = {} -local Range = require("tirenvi.util.range") +-- ============================================================================= +--#region Private ---@param self Range3 ---@return integer local function get_update(self) - return math.min(self.new_last, self.last) - self.first + 1 + return math.min(self.new_last, self.last) - self.first + 1 end ---@param self Range3 ---@return string local function get_add_str(self) - local delta = Range3.get_delta(self) - return delta > 0 and tostring(delta .. "A") or "" + local delta = Range3.get_delta(self) + return delta > 0 and tostring(delta .. "A") or "" end ---@param self Range3 ---@return string local function get_remove_str(self) - local delta = Range3.get_delta(self) - return delta < 0 and tostring(-delta .. "D") or "" + local delta = Range3.get_delta(self) + return delta < 0 and tostring(-delta .. "D") or "" end ---@param self Range3 ---@return string local function get_update_str(self) - return get_update(self) > 0 and tostring(get_update(self) .. "U") or "" + return get_update(self) > 0 and tostring(get_update(self) .. "U") or "" end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param self Range3 ---@return integer function Range3.get_delta(self) - return self.new_last - self.last + return self.new_last - self.last end ---@param first integer -- 1-based @@ -47,27 +52,34 @@ end ---@param new_last integer -- 1-based ---@return Range3 function Range3.new(first, last, new_last) - return { - first = first, - last = last, - new_last = new_last, - } + return { + first = first, + last = last, + new_last = new_last, + } end ---@return string function Range3:short() - return string.format("(%d,%d,%d)%s%s%s", self.first, self.last, self.new_last - , get_add_str(self), get_remove_str(self), get_update_str(self)) + return string.format( + "(%d,%d,%d)%s%s%s", + self.first, + self.last, + self.new_last, + get_add_str(self), + get_remove_str(self), + get_update_str(self) + ) end ---@param self Range3 ---@return Range function Range3:get_new_range() - return Range.from_lua(self.first, self.new_last) + return Range.from_lua(self.first, self.new_last) end function Range3:is_insert() - return Range3.get_delta(self) > 0 and get_update(self) == 0 + return Range3.get_delta(self) > 0 and get_update(self) == 0 end return Range3 diff --git a/lua/tirenvi/util/util.lua b/lua/tirenvi/util/util.lua index c27d7840..876ea0a1 100644 --- a/lua/tirenvi/util/util.lua +++ b/lua/tirenvi/util/util.lua @@ -1,28 +1,17 @@ ---- Small utility helpers used across tirenvi modules. ---- ---- Notes: ---- - This module contains only pure helper utilities. ---- - No side effects. - ------------------------------------------------------------------------ --- Dependencies ------------------------------------------------------------------------ - -local config = require("tirenvi.config") -local errors = require("tirenvi.util.errors") -local notify = require("tirenvi.util.notify") -local log = require("tirenvi.util.log") +local api = vim.api -- Neovim +local fn = vim.fn +local config = require("tirenvi.config") -- Root ------------------------------------------------------------------------ --- Module ------------------------------------------------------------------------ +local errors = require("tirenvi.util.errors") -- Util +local log = require("tirenvi.util.log") + +-- ============================================================================= local M = {} -local api = vim.api -local fn = vim.fn --- private helpers +-- ============================================================================= +--#region Private ---@return {[string]:string} local function collect_reserved_chars() @@ -55,9 +44,9 @@ local function find_reserved_marks(fllines) return result end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param str string ---@return string[] @@ -106,7 +95,7 @@ end ---@param str string ---@return boolean function M.end_with(line, str) - return line:sub(- #str) == str + return line:sub(-#str) == str end ---@param code string diff --git a/lua/tirenvi/version.lua b/lua/tirenvi/version.lua index 61de4671..daebaa04 100644 --- a/lua/tirenvi/version.lua +++ b/lua/tirenvi/version.lua @@ -1,3 +1,5 @@ +-- ============================================================================= + local M = {} M.VERSION = "0.4.0" diff --git a/lua/tirenvi/width/layout.lua b/lua/tirenvi/width/layout.lua index 91c89442..72371481 100644 --- a/lua/tirenvi/width/layout.lua +++ b/lua/tirenvi/width/layout.lua @@ -1,82 +1,81 @@ -local Document = require("tirenvi.core.document") -local Block = require("tirenvi.core.block") -local Cell = require("tirenvi.core.cell") -local Attr = require("tirenvi.core.attr") -local buffer = require("tirenvi.io.buffer") -local attr_store = require("tirenvi.io.attr_store") -local util = require("tirenvi.util.util") -local log = require("tirenvi.util.log") +local buf_state = require("tirenvi.io.buf_state") -local M = {} +local Block = require("tirenvi.core.block") -- Core +local Cell = require("tirenvi.core.cell") +local Attr = require("tirenvi.core.attr") --- constants / defaults +local util = require("tirenvi.util.util") -- Util +local log = require("tirenvi.util.log") ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= +local M = {} + +-- ============================================================================= +--#region Private ---@param widths integer[] ---@return integer[] local function sort_column_indices(widths) - local indices = {} - for index = 1, #widths do - indices[index] = index - end - table.sort(indices, function(index1, index2) - local width1 = widths[index1] - local width2 = widths[index2] - if width1 ~= width2 then - return width1 > width2 - end - return index1 > index2 - end) - return indices + local indices = {} + for index = 1, #widths do + indices[index] = index + end + table.sort(indices, function(index1, index2) + local width1 = widths[index1] + local width2 = widths[index2] + if width1 ~= width2 then + return width1 > width2 + end + return index1 > index2 + end) + return indices end ---@param ncol integer ---@param grid_span integer ---@return integer local function get_fit_width(ncol, grid_span) - return grid_span - ncol - 1 + return grid_span - ncol - 1 end ---@param widths integer[] ---@return number[] local function get_weight(widths) - local total = 0 - local logws = {} - for _, width in ipairs(widths) do - local logw = math.log(width) - logws[#logws + 1] = logw - total = total + logw - end - local weight = {} - for icol = 1, #widths do - weight[icol] = logws[icol] / total - end - return weight + local total = 0 + local logws = {} + for _, width in ipairs(widths) do + local logw = math.log(width) + logws[#logws + 1] = logw + total = total + logw + end + local weight = {} + for icol = 1, #widths do + weight[icol] = logws[icol] / total + end + return weight end ---@param fit_width integer ---@return integer[] local function get_fit_widths(org_widths, fit_width) - local weight = get_weight(org_widths) - local fit_widths = {} - for icol = 1, #org_widths do - fit_widths[icol] = math.max(math.ceil(fit_width * weight[icol]), Cell.MIN_WIDTH) - end - local indieces = sort_column_indices(fit_widths) - local over = math.min(util.sum(fit_widths) - fit_width, #fit_widths) - for index = 1, over do - local icol = indieces[index] - fit_widths[icol] = math.max(fit_widths[icol] - 1, Cell.MIN_WIDTH) - end - return fit_widths + local weight = get_weight(org_widths) + local fit_widths = {} + for icol = 1, #org_widths do + fit_widths[icol] = + math.max(math.ceil(fit_width * weight[icol]), Cell.MIN_WIDTH) + end + local indieces = sort_column_indices(fit_widths) + local over = math.min(util.sum(fit_widths) - fit_width, #fit_widths) + for index = 1, over do + local icol = indieces[index] + fit_widths[icol] = math.max(fit_widths[icol] - 1, Cell.MIN_WIDTH) + end + return fit_widths end ---@param block Block_grid local function nowrap(block) - Block.grid.set_max_attr(block, true) + Block.grid.set_max_attr(block, true) end local PADDING_RATE = 1 / 3 @@ -86,14 +85,16 @@ local PADDING_MAX = 6 ---@param fit_width integer ---@return integer[] local function shrink(max_widths, max_width, fit_width) - local plus_widths = get_fit_widths(max_widths, fit_width - max_width) - for icol = 1, #plus_widths do - local plus = math.min(plus_widths[icol], - math.ceil(max_widths[icol] * PADDING_RATE), - PADDING_MAX) - plus_widths[icol] = max_widths[icol] + plus - end - return plus_widths + local plus_widths = get_fit_widths(max_widths, fit_width - max_width) + for icol = 1, #plus_widths do + local plus = math.min( + plus_widths[icol], + math.ceil(max_widths[icol] * PADDING_RATE), + PADDING_MAX + ) + plus_widths[icol] = max_widths[icol] + plus + end + return plus_widths end local WIDTH_MIN = 6 @@ -101,92 +102,92 @@ local WIDTH_MIN = 6 ---@param fit_widths integer[] ---@return number local function get_expand_ratio(max_widths, fit_widths) - local ratio = 1 - for icol = 1, #fit_widths do - if fit_widths[icol] < max_widths[icol] then - ratio = math.max(ratio, WIDTH_MIN / fit_widths[icol]) - end - end - return ratio + local ratio = 1 + for icol = 1, #fit_widths do + if fit_widths[icol] < max_widths[icol] then + ratio = math.max(ratio, WIDTH_MIN / fit_widths[icol]) + end + end + return ratio end local function expand(max_widths, fit_width) - local fit_widths = get_fit_widths(max_widths, fit_width) - local ratio = get_expand_ratio(max_widths, fit_widths) - if ratio == 1 then - return fit_widths - end - return get_fit_widths(max_widths, math.ceil(fit_width * ratio)) + local fit_widths = get_fit_widths(max_widths, fit_width) + local ratio = get_expand_ratio(max_widths, fit_widths) + if ratio == 1 then + return fit_widths + end + return get_fit_widths(max_widths, math.ceil(fit_width * ratio)) end ---@param winid number ---@param block Block_grid local function wrap_auto(winid, block) - local max_widths = Block.grid.get_max_width(block) - local max_width = util.sum(max_widths) - local fit_span = buffer.get_win_span(winid) - local fit_width = get_fit_width(#max_widths, fit_span) - local fit_widths - if max_width < fit_width then - fit_widths = shrink(max_widths, max_width, fit_width) - else - fit_widths = expand(max_widths, fit_width) - end - for icol = 1, #fit_widths do - block.attr.columns[icol] = block.attr.columns[icol] or {} - block.attr.columns[icol].width = fit_widths[icol] - end + local max_widths = Block.grid.get_max_width(block) + local max_width = util.sum(max_widths) + local fit_span = buf_state.get_win_span(winid) + local fit_width = get_fit_width(#max_widths, fit_span) + local fit_widths + if max_width < fit_width then + fit_widths = shrink(max_widths, max_width, fit_width) + else + fit_widths = expand(max_widths, fit_width) + end + for icol = 1, #fit_widths do + block.attr.columns[icol] = block.attr.columns[icol] or {} + block.attr.columns[icol].width = fit_widths[icol] + end end ---@param winid number ---@param block Block_grid local function wrap_fit(winid, block) - local fit_span = block.attr.fit_span or buffer.get_win_span(winid) - local max_widths = Block.grid.get_max_width(block) - local fit_width = get_fit_width(#max_widths, fit_span) - local fit_widths = get_fit_widths(max_widths, fit_width) - for icol = 1, #fit_widths do - block.attr.columns[icol].width = fit_widths[icol] - end + local fit_span = block.attr.fit_span or buf_state.get_win_span(winid) + local max_widths = Block.grid.get_max_width(block) + local fit_width = get_fit_width(#max_widths, fit_span) + local fit_widths = get_fit_widths(max_widths, fit_width) + for icol = 1, #fit_widths do + block.attr.columns[icol].width = fit_widths[icol] + end end ---@param block Block_grid local function wrap_width(block) - Block.grid.set_max_attr(block) + Block.grid.set_max_attr(block) end ---@param winid number ---@param block Block_grid ---@param wrap_mode WrapMode local function wrap(winid, block, wrap_mode) - if wrap_mode == "wrap_auto" then - wrap_auto(winid, block) - elseif wrap_mode == "wrap_fit" then - wrap_fit(winid, block) - elseif wrap_mode == "wrap_width" then - wrap_width(block) - else - log.assert(false, "invalid mode %s", wrap_mode) - end + if wrap_mode == "wrap_auto" then + wrap_auto(winid, block) + elseif wrap_mode == "wrap_fit" then + wrap_fit(winid, block) + elseif wrap_mode == "wrap_width" then + wrap_width(block) + else + log.assert(false, "invalid mode %s", wrap_mode) + end end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param winid number ---@param tirdoc Document function M.compute(winid, tirdoc) - for _, block in ipairs(tirdoc.blocks) do - if block.kind == "grid" then - local wrap_mode = Attr.get_wrap_mode(block.attr) - if wrap_mode == "nowrap" then - nowrap(block) - elseif wrap_mode then - wrap(winid, block, wrap_mode) - end - end - end + for _, block in ipairs(tirdoc.blocks) do + if block.kind == "grid" then + local wrap_mode = Attr.get_wrap_mode(block.attr) + if wrap_mode == "nowrap" then + nowrap(block) + elseif wrap_mode then + wrap(winid, block, wrap_mode) + end + end + end end return M diff --git a/lua/tirenvi/width/op.lua b/lua/tirenvi/width/op.lua index d40a7ea6..d2fef6bd 100644 --- a/lua/tirenvi/width/op.lua +++ b/lua/tirenvi/width/op.lua @@ -1,3 +1,10 @@ +local fn = vim.fn -- Neovim + +local Cell = require("tirenvi.core.cell") -- Core + +local Range = require("tirenvi.util.range") -- Util +local log = require("tirenvi.util.log") + ---@alias WidthCommand ---| "width" ---| "fit" @@ -11,6 +18,7 @@ ---| "info" ---| "none" +-- ============================================================================= ---@class WidthOp ---@field args string ---@field command WidthCommand @@ -18,158 +26,159 @@ ---@field number integer ---@field row_cur integer ---@field col_disp integer -local WidthOp = {} +local WidthOp = {} WidthOp.__index = WidthOp -local Cell = require("tirenvi.core.cell") -local Range = require("tirenvi.util.range") -local log = require("tirenvi.util.log") - --- constants / defaults - ------------------------------------------------------------------------ --- Private helpers ------------------------------------------------------------------------ +-- ============================================================================= +--#region Private -local map = { - ["="] = "set", - ["+"] = "add", - ["-"] = "sub", - ["?"] = "info", +local map = { + ["="] = "set", + ["+"] = "add", + ["-"] = "sub", + ["?"] = "info", } ---@param opts {[string]:any} ---@return Rect local function get_selection(opts) - local row_range = Range.from_lua_normal(opts.line1, opts.line2) - local is_block = (vim.fn.visualmode() == "\22") - local col_disp_start, col_disp_end - if opts.range > 0 then - if is_block then - col_disp_start = vim.fn.virtcol("'<") - col_disp_end = vim.fn.virtcol("'>") - else - col_disp_start = 1 - col_disp_end = math.huge - end - else - local col = vim.fn.virtcol(".") - col_disp_start = col - col_disp_end = col - end - local col_range = Range.from_lua_normal(col_disp_start, col_disp_end) - return { - row = row_range, - col = col_range - } + local row_range = Range.from_lua_normal(opts.line1, opts.line2) + local is_block = (fn.visualmode() == "\22") + local col_disp_start, col_disp_end + if opts.range > 0 then + if is_block then + col_disp_start = fn.virtcol("'<") + col_disp_end = fn.virtcol("'>") + else + col_disp_start = 1 + col_disp_end = math.huge + end + else + local col = fn.virtcol(".") + col_disp_start = col + col_disp_end = col + end + local col_range = Range.from_lua_normal(col_disp_start, col_disp_end) + ---@type Rect + return { + row = row_range, + col = col_range, + } end ---@param str string ---@return integer local function get_number(str) - if not str or str == "" then - return 1 - end - local num = tonumber(str) - if not num or num < 0 then - error(string.format("%s is not positive number", str)) - end - return math.max(1, num) + if not str or str == "" then + return 1 + end + local num = tonumber(str) + if not num or num < 0 then + error(string.format("%s is not positive number", str)) + end + return math.max(1, num) end ---@param opts {[string]:any} ---@return WidthOperation|nil ---@return integer|nil local function get_operation(opts) - local sub = table.concat(opts.command.sub, "%") - local regex = string.format("^%s%%s*([%s])(.*)", opts.command_name, sub) - local op, value = opts.args:match(regex) - if not op then - error(string.format("%s need operator %s", opts.command_name, sub)) - end - if op == "?" and #value ~= 0 then - return nil, nil - end - local num = get_number(value) - if op == "=" and num <= 1 then - return "auto", 0 - end - return map[op], num + local sub = table.concat(opts.command.sub, "%") + local regex = string.format("^%s%%s*([%s])(.*)", opts.command_name, sub) + local op, value = opts.args:match(regex) + if not op then + error(string.format("%s need operator %s", opts.command_name, sub)) + end + if op == "?" and #value ~= 0 then + return nil, nil + end + local num = get_number(value) + if op == "=" and num <= 1 then + return "auto", 0 + end + return map[op], num end ---@param opts {[string]:any} ---@return WidthOp|nil local function try_new(opts) - local command_name = opts.command_name - ---@cast command_name WidthCommand - local rect = get_selection(opts) - local row_cur = rect.row.first - local col_disp = rect.col.first - local self = setmetatable({ - args = opts.args, - command = command_name, - operation = "none", - number = 0, - row_cur = row_cur, - col_disp = col_disp, - }, WidthOp) - if not opts.command.has_op then - if opts.args ~= command_name then - return nil - end - return self - end - local operation, number = get_operation(opts) - if not operation or not number then - return nil - end - self.operation = operation - self.number = number - return self + local command_name = opts.command_name + ---@cast command_name WidthCommand + local rect = get_selection(opts) + local row_cur = rect.row.first + local col_disp = rect.col.first + local self = setmetatable({ + args = opts.args, + command = command_name, + operation = "none", + number = 0, + row_cur = row_cur, + col_disp = col_disp, + }, WidthOp) + if not opts.command.has_op then + if opts.args ~= command_name then + return nil + end + return self + end + local operation, number = get_operation(opts) + if not operation or not number then + return nil + end + self.operation = operation + self.number = number + return self end ------------------------------------------------------------------------ +--#endregion +-- ============================================================================= -- Public API ------------------------------------------------------------------------ ---@param opts {[string]:any} ---@return WidthOp|nil function WidthOp.new(opts) - local ok, self = pcall(try_new, opts) - if not ok or not self then - return nil - end - return self + local ok, self = pcall(try_new, opts) + if not ok or not self then + return nil + end + return self end function WidthOp:to_cmd() - return string.format(":Tir %s", self.args) + return string.format(":Tir %s", self.args) end ---@param self WidthOp +---@return string function WidthOp:to_string() - return string.format("WidthOp %s %s (%d, %d) [%s] %s", - self.command, self.operation or "nil", - self.row_cur, self.col_disp, self.number or "nil", self:to_cmd()) + return string.format( + "WidthOp %s %s (%d, %d) [%s] %s", + self.command, + self.operation or "nil", + self.row_cur, + self.col_disp, + self.number or "nil", + self:to_cmd() + ) end ---@param self WidthOp ---@param current integer ---@return integer function WidthOp:apply(current) - local operation = self.operation - local count = self.number - if operation == "set" then - return math.max(count, Cell.MIN_WIDTH) - elseif operation == "add" then - return current + count - elseif operation == "sub" then - return math.max(current - count, Cell.MIN_WIDTH) - elseif operation == "auto" then - return 0 - else - return current - end + local operation = self.operation + local count = self.number + if operation == "set" then + return math.max(count, Cell.MIN_WIDTH) + elseif operation == "add" then + return current + count + elseif operation == "sub" then + return math.max(current - count, Cell.MIN_WIDTH) + elseif operation == "auto" then + return 0 + else + return current + end end return WidthOp diff --git a/tests/cases/check/format/out-expected.txt b/tests/cases/check/format/out-expected.txt new file mode 100644 index 00000000..e69de29b diff --git a/tests/cases/check/format/run.sh b/tests/cases/check/format/run.sh new file mode 100644 index 00000000..9975e0e3 --- /dev/null +++ b/tests/cases/check/format/run.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +stylua --check $TIRENVI_ROOT/lua $TIRENVI_ROOT/tests > out-actual.txt \ No newline at end of file diff --git a/tests/cases/check/format/skip-ci b/tests/cases/check/format/skip-ci new file mode 100644 index 00000000..e69de29b diff --git a/tests/cases/check/health/out-expected.txt b/tests/cases/check/health/out-expected.txt index 922aa519..ace22df5 100644 --- a/tests/cases/check/health/out-expected.txt +++ b/tests/cases/check/health/out-expected.txt @@ -9,6 +9,8 @@ tirenvi ~ - WARNING vim-repeat not found ('.' repeat disabled) - OK tir-csv found - OK tir-csv version 0.1.4 OK +- OK tir-embedded found +- OK tir-embedded version 0.1.0 OK - OK tir-gfm-lite found - OK tir-gfm-lite version 0.1.6 OK - OK tir-pukiwiki found @@ -29,6 +31,8 @@ tirenvi ~ pip install foo - OK tir-csv found - ERROR tir-csv >= 1.1.4 required, but 0.1.4 found +- OK tir-embedded found +- OK tir-embedded version 0.1.0 OK - OK tir-gfm-lite found - ERROR tir-gfm-lite >= 0.2 required, but 0.1.6 found diff --git a/tests/cases/check/health/run.vim b/tests/cases/check/health/run.vim index cf454f73..05a7e5d0 100644 --- a/tests/cases/check/health/run.vim +++ b/tests/cases/check/health/run.vim @@ -1,16 +1,10 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -M = require("tirenvi") -M.setup({ - parser_map = { - csv = { executable = "tir-csv", required_version = "0.1.4" }, - tsv = { executable = "tir-csv", options = { "--delimiter", "\t" }, required_version = "0.1.1" }, - markdown = { executable = "tir-gfm-lite", allow_plain = true, required_version = "0.1" }, - pukiwiki = { executable = "tir-pukiwiki", allow_plain = foo }, - }, -}) -EOF +lua require("tirenvi.config").parser_map.csv.required_version = "0.1.4" +lua require("tirenvi.config").parser_map.tsv.required_version = "0.1.1" +lua require("tirenvi.config").parser_map.markdown.required_version = "0.1" +lua require("tirenvi.config").parser_map.pukiwiki.allow_plain = foo +lua require("tirenvi.config").setup({}) " ===== CSV ===== edit $TIRENVI_ROOT/tests/data/simple.csv @@ -18,19 +12,13 @@ edit $TIRENVI_ROOT/tests/data/simple.csv call Snapshot({ 'nomessage': 'true', 'desc': 'checkhealth ok case' }) -lua << EOF -vim.g.tirenvi_initialized = false -M.setup({ - parser_map = { - csv = { executable = "tir-csv", required_version = "1.1.4" }, - tsv = { executable = "tir-csv", options = { "--delimiter", "\t" }, required_version = "0.2.4" }, - markdown = { executable = "tir-gfm-lite", allow_plain = true, required_version = "0.2" }, - pukiwiki = { executable = "tir-pukiwiki", allow_plain = true, required_version = "0." }, - foo = { executable = "foo", allow_plain = true, required_version = "0.1.1" }, - -- grep = { executable = "grep", allow_plain = true, required_version = "99999.99999.99999" }, - }, -}) -EOF +lua require("tirenvi.config").parser_map.csv.required_version = "1.1.4" +lua require("tirenvi.config").parser_map.tsv.required_version = "0.2.4" +lua require("tirenvi.config").parser_map.markdown.required_version = "0.2" +lua require("tirenvi.config").parser_map.pukiwiki.required_version = "0." +lua require("tirenvi.config").parser_map.foo = { executable = "foo", required_version = "0.1.1" } +lua require("tirenvi.config").setup({}) + " ===== TXT ===== edit $TIRENVI_ROOT/tests/data/empty.txt diff --git a/tests/cases/check/rg_ng_char/out-expected.txt b/tests/cases/check/rg_ng_char/out-expected.txt index b658ebc3..10fef8f2 100644 --- a/tests/cases/check/rg_ng_char/out-expected.txt +++ b/tests/cases/check/rg_ng_char/out-expected.txt @@ -3,7 +3,7 @@ /Users/ogawa/oss/tirenvi.nvim/lua/tirenvi/config.lua: pipec = "┊", -- 》⇥⇢⋯⋮︙›↠▶¬… /Users/ogawa/oss/tirenvi.nvim/lua/tirenvi/config.lua: lf = "↲", -- ⤶⏎↵↲⤷␤¶—↩️ /Users/ogawa/oss/tirenvi.nvim/lua/tirenvi/config.lua: tab = "⇥", -- »⇥→⇨▹▸▻►⇤␉》 -/Users/ogawa/oss/tirenvi.nvim/lua/tirenvi/ui.lua: fn.sign_define("TirenviSign", { text = "◆", texthl = "ErrorMsg" }) +/Users/ogawa/oss/tirenvi.nvim/lua/tirenvi/ui.lua: fn.sign_define("TirenviSign", { text = "◆", texthl = "ErrorMsg" }) /Users/ogawa/oss/tirenvi.nvim/lua/tirenvi/util/log.lua: name = "🟧PRB" /Users/ogawa/oss/tirenvi.nvim/tests/cases/check/health/run.sh: -e 's/❌ //g' \ /Users/ogawa/oss/tirenvi.nvim/tests/cases/check/health/run.sh: -e 's/✅ //g' \ diff --git a/tests/cases/edit/write/out-expected.txt b/tests/cases/edit/write/out-expected.txt index 2b673699..720cc221 100644 --- a/tests/cases/edit/write/out-expected.txt +++ b/tests/cases/edit/write/out-expected.txt @@ -1,8 +1,8 @@ --- write csv --- === MESSAGE === ---- CASE 16: simple csv --- -CASE 16 // cur(1,1) b1:r1:c1+0<│> g(1,7)a[3*,4,4] +--- CASE 9: simple csv --- +CASE 9 // cur(1,1) b1:r1:c1+0<│> g(1,7)a[3*,4,4] tirenvi: install 'tpope/vim-repeat' to enable '.' repeat === DISPLAY === @@ -66,21 +66,21 @@ b " --- edit save --- === MESSAGE === ---- CASE 30: complex csv --- -CASE 30 // cur(1,1) b1:r1:c1+0<│> g(1,114)a[20*,11,12,10,11,9] +--- CASE 23: complex csv --- +CASE 23 // cur(1,1) b1:r1:c1+0<│> g(1,114)a[20*,11,12,10,11,9] ---- CASE 39: simple md --- -CASE 39 // cur(1,1) b1:r1:c0+0 p(1,2)* g(3,8)a[7,4,15] p(9,11) +--- CASE 32: simple md --- +CASE 32 // cur(1,1) b1:r1:c0+0 p(1,2)* g(3,8)a[7,4,15] p(9,11) ---- CASE 45: complex md --- -CASE 45 // cur(1,1) b1:r1:c0+0<#> p(1,9)* g(10,14)a[7,4,8] p(15,22) g(23,27)a[6,8,7] +--- CASE 38: complex md --- +CASE 38 // cur(1,1) b1:r1:c0+0<#> p(1,9)* g(10,14)a[7,4,8] p(15,22) g(23,27)a[6,8,7] ---- CASE 53: empty txt --- -CASE 53 // cur(1,1) bnil:rnil:cnil+0<> +--- CASE 46: empty txt --- +CASE 46 // cur(1,1) bnil:rnil:cnil+0<> ---- CASE 58: tir-buf md --- -CASE 58 // cur(1,1) b1:r1:c0+0<-> p(1,1)* g(2,6)a[7,8] p(7,7) g(8,12)a[7,4,8] -CASE 58 // cur(1,1) bnil:rnil:cnil+0<-> p() g()a[7,8] p() g()a[7,4,8] +--- CASE 51: tir-buf md --- +CASE 51 // cur(1,1) b1:r1:c0+0<-> p(1,1)* g(2,6)a[7,8] p(7,7) g(8,12)a[7,4,8] +CASE 51 // cur(1,1) bnil:rnil:cnil+0<-> p() g()a[7,8] p() g()a[7,4,8] === DISPLAY === ----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 diff --git a/tests/cases/edit/write/run.vim b/tests/cases/edit/write/run.vim index 357bfeb3..7e999cd5 100644 --- a/tests/cases/edit/write/run.vim +++ b/tests/cases/edit/write/run.vim @@ -3,14 +3,7 @@ let outcsv = 'gen.csv' let outtsv = 'gen.tsv' let outxxx = 'gen.xxx' -lua << EOF -local M = require("tirenvi") -M.setup({ - table = { - wrap_mode = "wrap", - }, -}) -EOF +lua require("tirenvi.config").table.wrap_mode = "wrap" " ===== SIMPLE CSV ===== CASE simple csv diff --git a/tests/cases/embedded/auto/out-expected.txt b/tests/cases/embedded/auto/out-expected.txt new file mode 100644 index 00000000..f2164de0 --- /dev/null +++ b/tests/cases/embedded/auto/out-expected.txt @@ -0,0 +1,83 @@ +--- GFM --- +=== MESSAGE === + +--- CASE 6: initial cached attrs --- +CASE 6 // cur(1,1) b1:r1:c0+0<-> p(1,1)* g(2,6)n[5,6] p(7,7) g(8,12)n[5,3,6] + +--- CASE 9: fit= plain#1 --- +tirenvi: install 'tpope/vim-repeat' to enable '.' repeat +CASE 9 // cur(1,1) b1:r1:c0+0<-> p(1,1)* g(2,6)n[5,6] p(7,7) g(8,12)n[5,3,6] + +--- CASE 13: fit= grid#1 --- +CASE 13 // cur(2,1) b2:r1:c1+0<│> p(1,1) g(2,6)a[7*,8] p(7,7) g(8,12)n[5,3,6] + +--- CASE 17: fit= grid#1 logic A // within screen width: recommended width for an empty column is 3 --- +CASE 17 // cur(2,2) b2:r1:c1+1<⠀> p(1,1) g(2,6)a[3*,8] p(7,7) g(8,12)n[5,3,6] + +--- CASE 22: fit= grid#1 logic A // within screen width: recommended width for a 3-character column is 4 --- +CASE 22 // cur(2,6) b2:r1:c2+0<│> p(1,1) g(2,6)a[4,8*] p(7,7) g(8,12)n[5,3,6] + +--- CASE 26: fit= grid#1 logic A // within screen width: recommended width for a 4-character column is 6 --- +CASE 26 // cur(2,2) b2:r1:c1+1

p(1,1) g(2,6)a[6*,8] p(7,7) g(8,12)n[5,3,6] + +--- CASE 30: fit= grid#1 logic A // within screen width: recommended width for an 18-character column is 24 --- +CASE 30 // cur(3,19) b2:r2:c1+18 p(1,1) g(2,6)a[24*,8] p(7,7) g(8,12)n[5,3,6] + +--- CASE 34: fit= grid#1 logic A // within screen width: recommended width for a 19-character column is 25 --- +CASE 34 // cur(3,2) b2:r2:c1+1 p(1,1) g(2,6)a[25*,8] p(7,7) g(8,12)n[5,3,6] + +--- CASE 38: fit= grid#1 // max or fit --- +CASE 38 // cur(5,61) b2:r4:c1+60 p(1,1) g(2,6)a[68*,8] p(7,7) g(8,12)n[5,3,6] + +=== DISPLAY === +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +│Name⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│City⠀⠀⠀⠀│ +│----⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│----⠀⠀⠀⠀│ +│Alice⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│Tokyo⠀⠀⠀│ +│GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGBob⠀⠀⠀⠀⠀│Osaka⠀⠀⠀│ +│Carol⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│Nagoya⠀⠀│ +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +│Name⠀│Age│City⠀⠀│ +│----⠀│---│----⠀⠀│ +│Alice│23⠀│Tokyo⠀│ +│Bob⠀⠀│31⠀│Osaka⠀│ +│Carol│27⠀│Nagoya│ +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 + +--- Tir wrap --- +=== MESSAGE === + +--- CASE 50: wide CSV initial --- +CASE 50 // cur(1,1) b1:r1:c1+0<│> g(1,18)n[17*,9,10,7,17,9,10,7,8,7,17,9,10,7,8,7] + +--- CASE 53: fit= logic C // exceeds screen width --- +CASE 53 // cur(1,1) b1:r1:c1+0<│> g(1,21)a[9*,8,8,7,9,8,8,7,8,7,9,8,8,7,8,7] + +--- CASE 56: fit= logic B // fits within screen width --- +CASE 56 // cur(1,1) b1:r1:c1+0<┊> g(1,24)a[7*,7,6,8,6,6,6,6,6,8,6,6,6,6,6] + +=== DISPLAY === +┊B22⠀⠀⠀⠀┊C333⠀⠀⠀┊D4444⠀┊A1⠀⠀⠀⠀⠀⠀┊B22⠀⠀⠀┊C333⠀⠀┊D4444⠀┊E55555┊F66666┊A1⠀⠀⠀⠀⠀⠀┊B22⠀⠀⠀┊C333⠀⠀┊D4444⠀┊E55555┊F66666┊ +│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│6⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│6⠀⠀⠀⠀⠀│ +│b2⠀⠀⠀⠀⠀│c3⠀⠀⠀⠀⠀│d4⠀⠀⠀⠀│alpha1⠀⠀│b2⠀⠀⠀⠀│c3⠀⠀⠀⠀│d4⠀⠀⠀⠀│e5⠀⠀⠀⠀│f6⠀⠀⠀⠀│alpha1⠀⠀│b2⠀⠀⠀⠀│c3⠀⠀⠀⠀│d4⠀⠀⠀⠀│e5⠀⠀⠀⠀│f6⠀⠀⠀⠀│ +┊- next ┊ line⠀⠀┊ has⠀┊ ===⠀⠀⠀⠀┊- next┊ line⠀┊ has⠀┊ no⠀⠀⠀┊ value┊ ===⠀⠀⠀⠀┊- next┊ line⠀┊ has⠀┊ no⠀⠀⠀┊ value┊ +│-⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│ -⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ ⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│ -⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ ⠀⠀⠀⠀⠀│ +│⠀⠀⠀⠀⠀⠀⠀│y22⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│x1⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│y22⠀⠀⠀│⠀⠀⠀⠀⠀⠀│z333⠀⠀│⠀⠀⠀⠀⠀⠀│x1⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│y22⠀⠀⠀│⠀⠀⠀⠀⠀⠀│z333⠀⠀│⠀⠀⠀⠀⠀⠀│ +┊ - next┊ line⠀⠀┊ has⠀┊===⠀⠀⠀⠀⠀┊ - nex┊ line⠀┊ has⠀┊linefe┊ ⠀⠀⠀⠀┊===⠀⠀⠀⠀⠀┊ - nex┊ line⠀┊ has⠀┊linefe┊ ⠀⠀⠀⠀┊ +│ -⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│t -⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ed⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│t -⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ed⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +┊abc⠀⠀⠀⠀┊DEF123⠀┊↲⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀⠀⠀┊abc⠀⠀⠀┊DEF123┊↲⠀⠀⠀⠀⠀┊gh⠀⠀⠀⠀┊9⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀⠀⠀┊abc⠀⠀⠀┊DEF123┊↲⠀⠀⠀⠀⠀┊gh⠀⠀⠀⠀┊9⠀⠀⠀⠀⠀┊ +┊⠀⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊"1"2h", ┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊"1"2h", ┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊ +┊⠀⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊34567890┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊34567890┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊ +│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│a⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│a⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +┊⠀⠀⠀⠀⠀⠀⠀┊b↲⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊a⠀⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊b↲⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊c⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊a⠀⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊b↲⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊c⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊ +│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +┊mi⇥⇥d2⠀┊s⠀⠀⠀⠀⠀⠀┊tt⠀⠀⠀⠀┊⇥ l⇥ongt┊mi⇥⇥d2┊s⠀⠀⠀⠀⠀┊tt⠀⠀⠀⠀┊uuu⠀⠀⠀┊vvvv⠀⠀┊⇥ l⇥ongt┊mi⇥⇥d2┊s⠀⠀⠀⠀⠀┊tt⠀⠀⠀⠀┊uuu⠀⠀⠀┊vvvv⠀⠀┊ +│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ext01⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ext01⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +┊⠀⠀⠀⠀⠀⠀⠀┊⇥⇥⇥⇥⇥⇥⇥┊⇥⇥⇥⇥⇥⇥┊⠀⠀⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⇥⇥⇥⇥⇥⇥┊⇥⇥⇥⇥⇥⇥┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⇥⇥⇥⇥⇥⇥┊⇥⇥⇥⇥⇥⇥┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊ +│⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +┊いろ2⠀⠀┊ぼんさ⠀┊へを⠀⠀┊aaあい⠀⠀┊いろ2⠀┊ぼんさ┊へを⠀⠀┊こいた┊.!...┊aaあい⠀⠀┊いろ2⠀┊ぼんさ┊へを⠀⠀┊こいた┊.!...┊ +│⠀⠀⠀⠀⠀⠀⠀│んが⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│んが⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│んが⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +│h22⠀⠀⠀⠀│i333⠀⠀⠀│j4444⠀│g1⠀⠀⠀⠀⠀⠀│h22⠀⠀⠀│i333⠀⠀│j4444⠀│k55555│l6⠀⠀⠀⠀│g1⠀⠀⠀⠀⠀⠀│h22⠀⠀⠀│i333⠀⠀│j4444⠀│k55555│l6⠀⠀⠀⠀│ +┊↲⠀⠀⠀⠀⠀⠀┊n⇥ ⠀⠀⠀⠀┊⇥⇥ o33┊⇥⇥ ⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊n⇥ ⠀⠀⠀┊⇥⇥ o33┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊⇥⇥ ⠀⠀⠀⠀⠀┊↲⠀⠀⠀⠀⠀┊n⇥ ⠀⠀⠀┊⇥⇥ o33┊⠀⠀⠀⠀⠀⠀┊⠀⠀⠀⠀⠀⠀┊ +│⇥⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀│3⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│3⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀│⇥⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│3⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀│ +│uv⠀⠀⠀⠀⠀│wx1⠀⠀⠀⠀│yz22⠀⠀│pqrst⠀⠀⠀│uv⠀⠀⠀⠀│wx1⠀⠀⠀│yz22⠀⠀│⠀⠀⠀⠀⠀⠀│345⠀⠀⠀│pqrst⠀⠀⠀│uv⠀⠀⠀⠀│wx1⠀⠀⠀│yz22⠀⠀│⠀⠀⠀⠀⠀⠀│345⠀⠀⠀│ diff --git a/tests/cases/embedded/auto/run.vim b/tests/cases/embedded/auto/run.vim new file mode 100644 index 00000000..c1f0a046 --- /dev/null +++ b/tests/cases/embedded/auto/run.vim @@ -0,0 +1,66 @@ +source $TIRENVI_ROOT/tests/common.vim + +" ===== GFM ===== +edit $TIRENVI_ROOT/tests/data/table2.md + set ft=bar + 4,10s/^/ # # /g + Tir toggle + +CASE initial cached attrs + lua print(Debug.layout()) + +CASE fit= plain#1 + call At(1, 1, 1) + call Tir("fit=") + +CASE fit= grid#1 + call At(1, 2, 1) + call Tir("fit=") + +CASE fit= grid#1 logic A // within screen width: recommended width for an empty column is 3 + call feedkeys("vil", "x") + normal! d + sleep 1m | lua print(Debug.layout()) + +CASE fit= grid#1 logic A // within screen width: recommended width for a 3-character column is 4 + normal! 3aO + sleep 1m | lua print(Debug.layout()) + +CASE fit= grid#1 logic A // within screen width: recommended width for a 4-character column is 6 + normal! 0aP + sleep 1m | lua print(Debug.layout()) + +CASE fit= grid#1 logic A // within screen width: recommended width for an 18-character column is 24 + normal! j018aQ + sleep 1m | lua print(Debug.layout()) + +CASE fit= grid#1 logic A // within screen width: recommended width for a 19-character column is 25 + normal! 0aR + sleep 1m | lua print(Debug.layout()) + +CASE fit= grid#1 // max or fit + e! + call At(2, 4, 1) + Tir wrap + normal! 060aG + sleep 1m | lua print(Debug.layout()) + +call Snapshot({ 'desc': 'GFM' }) + +" ===== CSV ===== +edit $TIRENVI_ROOT/tests/data/wide.csv + set ft=bar + %s/^/ # /g + +CASE wide CSV initial + lua print(Debug.layout()) + +CASE fit= logic C // exceeds screen width + call Tir("fit=") + +CASE fit= logic B // fits within screen width + call feedkeys("val", "x") + normal! d + call Tir("fit=") + +call Snapshot({ 'desc': 'Tir wrap' }) \ No newline at end of file diff --git a/tests/cases/embedded/toggle/out-expected.txt b/tests/cases/embedded/toggle/out-expected.txt new file mode 100644 index 00000000..cab48f5e --- /dev/null +++ b/tests/cases/embedded/toggle/out-expected.txt @@ -0,0 +1,68 @@ +--- simple md -> tir --- +=== MESSAGE === + +--- CASE 4: embedded -> // --- +CASE 4 // cur(1,1) b1:r1:c0+0<-> p(1,1)* g'//'(2,7)n[7,6] p(8,14) g'//'(15,15)n[3,11,7] + +=== DISPLAY === +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +// │Name⠀⠀⠀│City⠀⠀│ +// │----⠀⠀⠀│----⠀⠀│ +// ┊Alice ↲┊⠀⠀⠀⠀⠀⠀┊ +// │ Tokyo⠀│⠀⠀⠀⠀⠀⠀│ +// │Bob⠀⠀⠀⠀│Osaka⠀│ +// │Carol⠀⠀│Nagoya│ +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +| 名前 | Age | City | +| Alice | 23
Tokyo | + Bob | 31 | 大阪 | +| Carol | 27 | Nagoya | +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 + + // │Key│Description│Example│ + // | `id` | **Primary key** | `user_001` + // │name│⠀⠀⠀⠀⠀⠀│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ + // ┊note┊Free ↲┊`a | b | c`┊ + // │⠀⠀⠀⠀│ text⠀│⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ + // + // │name│ + // + | 名前 | Age | City | + | Alice | 23
Tokyo | +| Bob | 31 | 大阪 | + | Carol | 27 | Nagoya | +--- simple md -> tir -> flat --- +=== MESSAGE === + +--- CASE 10: embedded -> "" --- +CASE 10 // cur(7,1) b1:r7:c0+0<-> p(1,7)* g''(8,11)n[5,6] p(12,12) g''(13,13)n[5,2,6] + +=== DISPLAY === +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +// | Name | City | +// | ---- | ---- | +// | Alice
Tokyo | | +// | Bob | Osaka | +// | Carol | Nagoya | +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +┊名前⠀┊Age Ci┊ +│⠀⠀⠀⠀⠀│ty⠀⠀⠀⠀│ +┊Alice┊23 ↲⠀⠀┊ +│⠀⠀⠀⠀⠀│ Tokyo│ + Bob | 31 | 大阪 | +│Carol│27│Nagoya│ +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 + + // | Key | Description | Example | + // | `id` | **Primary key** | `user_001` + // | name | | | + // | note | Free
text | `a \| b \| c` | + // + // | name | + // + │名前⠀│Age⠀⠀⠀│City⠀⠀│ + ┊Alice┊23 ↲⠀⠀┊⠀⠀⠀⠀⠀⠀┊ + │⠀⠀⠀⠀⠀│ Tokyo│⠀⠀⠀⠀⠀⠀│ + │Bob⠀⠀│31⠀⠀⠀⠀│大阪⠀⠀│ + │Carol│27⠀⠀⠀⠀│Nagoya│ + diff --git a/tests/cases/embedded/toggle/run.vim b/tests/cases/embedded/toggle/run.vim new file mode 100644 index 00000000..3007e08e --- /dev/null +++ b/tests/cases/embedded/toggle/run.vim @@ -0,0 +1,15 @@ +source $TIRENVI_ROOT/tests/common.vim + +" ===== EMBEDDED ===== +CASE embedded -> // +edit $TIRENVI_ROOT/tests/data/table.txt + Tir toggle + sleep 1m | lua print(Debug.layout()) +call Snapshot({ 'desc': 'simple md -> tir' }) + +CASE embedded -> "" + Tir toggle + normal! 7G + Tir toggle + sleep 1m | lua print(Debug.layout()) +call Snapshot({ 'desc': 'simple md -> tir -> flat' }) diff --git a/tests/cases/embedded/toggle/skip-ci b/tests/cases/embedded/toggle/skip-ci new file mode 100644 index 00000000..e69de29b diff --git a/tests/cases/embedded/width/out-expected.txt b/tests/cases/embedded/width/out-expected.txt new file mode 100644 index 00000000..0d044f2c --- /dev/null +++ b/tests/cases/embedded/width/out-expected.txt @@ -0,0 +1,19 @@ +--- simple md -> tir --- +=== MESSAGE === +7 substitutions on 7 lines + +--- CASE 11: embedded -> At(2, 3, 3) --- +CASE 11 // cur(6,18) b2:r3:c3+1<大> p(1,3) g'# #'(4,8)n[5,3,11*] p(9,11) + +=== DISPLAY === +gfm +----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 +| 名前 | Age | City | + # # │----⠀│---│----⠀⠀⠀⠀⠀⠀⠀│ + # # │Alice│23⠀│Tokyo⠀⠀⠀⠀⠀⠀│ + # # ┊Bob⠀⠀┊31⠀┊大阪↲⠀⠀⠀⠀⠀⠀┊ + # # │⠀⠀⠀⠀⠀│⠀⠀⠀│nipponbashi│ + # # │Carol│27⠀│名古屋⠀⠀⠀⠀⠀│ + # # ----+----1----+----2----+----3----+----4----+----5----+----6----+----7----+----8----+----9----+----0 + # # markdown + # # diff --git a/tests/cases/embedded/width/run.vim b/tests/cases/embedded/width/run.vim new file mode 100644 index 00000000..3df8fe08 --- /dev/null +++ b/tests/cases/embedded/width/run.vim @@ -0,0 +1,15 @@ +source $TIRENVI_ROOT/tests/common.vim + +edit $TIRENVI_ROOT/tests/data/simple.md + +" ===== EMBEDDED ===== + set ft=bar + 4,10s/^/ # # /g + normal! 4G + Tir toggle + +CASE embedded -> At(2, 3, 3) + call At(2, 3, 3) + sleep 1m | lua print(Debug.layout()) + +call Snapshot({ 'desc': 'simple md -> tir' }) diff --git a/tests/cases/error/invalid-table2/run.vim b/tests/cases/error/invalid-table2/run.vim index e757e7b5..71dee201 100644 --- a/tests/cases/error/invalid-table2/run.vim +++ b/tests/cases/error/invalid-table2/run.vim @@ -2,15 +2,8 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -local M = require("tirenvi") -M.setup({ - log = { - output = "file", -- "notify" | "buffer" | "print" | "file" - file_name = "/tmp/tirenvi.log", - }, -}) -EOF +lua require("tirenvi.config").log.output = "file" +lua require("tirenvi.config").log.file_name = "/tmp/tirenvi.log" " ===== CSV ===== edit $TIRENVI_ROOT/tests/data/simple.csv diff --git a/tests/cases/error/no-mark/run.vim b/tests/cases/error/no-mark/run.vim index 9f57e73e..c5de326e 100644 --- a/tests/cases/error/no-mark/run.vim +++ b/tests/cases/error/no-mark/run.vim @@ -2,18 +2,9 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -local M = require("tirenvi") -M.setup({ - marks = { - padding = "a" - }, - log = { - output = "buffer", -- "notify" | "buffer" | "print" | "file" - buffer_name = "tirenvi://log", - }, -}) -EOF +lua require("tirenvi.config").marks.padding = "a" +lua require("tirenvi.config").log.output = "buffer" +lua require("tirenvi.config").log.buffer_name = "tirenvi://log" " ===== CSV ===== try diff --git a/tests/cases/error/no-parser/run.vim b/tests/cases/error/no-parser/run.vim index 332e7b6c..e23525b7 100644 --- a/tests/cases/error/no-parser/run.vim +++ b/tests/cases/error/no-parser/run.vim @@ -2,15 +2,8 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -local M = require("tirenvi") -M.setup({ - parser_map = { - csv = { executable = "tir-my-csv" }, - }, - log = { level = vim.log.levels.WARN } -}) -EOF +lua require("tirenvi.config").parser_map.csv.executable = "tir-my-csv" +lua require("tirenvi.config").log.level = vim.log.levels.WARN " ===== CSV ===== try diff --git a/tests/cases/error/old-parse/run.vim b/tests/cases/error/old-parse/run.vim index 82316603..5fa75a52 100644 --- a/tests/cases/error/old-parse/run.vim +++ b/tests/cases/error/old-parse/run.vim @@ -2,20 +2,11 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -local M = require("tirenvi") -M.setup({ - parser_map = { - markdown = { executable = "tir-gfm-lite", allow_plain = true, required_version = "0.2.4" }, - }, - log = { level = vim.log.levels.WARN } -}) -EOF +lua require("tirenvi.config").parser_map.markdown.required_version = "0.2.4" +lua require("tirenvi.config").log.level = vim.log.levels.WARN +lua require("tirenvi.config").setup({}) " ===== GFM ===== -try - edit $TIRENVI_ROOT/tests/data/simple.md -catch -endtry +edit $TIRENVI_ROOT/tests/data/simple.md call Snapshot({}) \ No newline at end of file diff --git a/tests/cases/filetype/out-expected.txt b/tests/cases/filetype/out-expected.txt index 57512611..7dd81c39 100644 --- a/tests/cases/filetype/out-expected.txt +++ b/tests/cases/filetype/out-expected.txt @@ -14,7 +14,15 @@ CASE 18 // cur(1,1) b1:r1:c1+0<│> g(1,9)n[10*,2] --- CASE 25: filetype markdown -> bar --- v:true +markdown +tir-gfm-lite +---- +v:true +bar +---- false +bar +nil === DISPLAY === gfm diff --git a/tests/cases/filetype/run.vim b/tests/cases/filetype/run.vim index 15806de4..650466d9 100644 --- a/tests/cases/filetype/run.vim +++ b/tests/cases/filetype/run.vim @@ -26,9 +26,17 @@ CASE filetype markdown -> bar e! $TIRENVI_ROOT/tests/data/simple.md call At(2, 3, 1) normal! haADD + echomsg b:tirenvi.attached + echomsg b:tirenvi.filetype + echomsg b:tirenvi.parser.executable set filetype=bar + echomsg "----" echomsg b:tirenvi.attached + echomsg b:tirenvi.filetype normal! jdd + echomsg "----" lua print(vim.b.tirenvi.attached) + lua print(vim.b.tirenvi.filetype) + lua print(vim.b.tirenvi.parser) call Snapshot({}) \ No newline at end of file diff --git a/tests/cases/motion/cursor/out-expected.txt b/tests/cases/motion/cursor/out-expected.txt index 8a7aca0d..3a927000 100644 --- a/tests/cases/motion/cursor/out-expected.txt +++ b/tests/cases/motion/cursor/out-expected.txt @@ -22,7 +22,7 @@ false : grid false : gird-short false : grid ---- CASE 58: CSV confg --- +--- CASE 51: CSV confg --- true : gird-short true : grid false : gird-short diff --git a/tests/cases/motion/cursor/run.vim b/tests/cases/motion/cursor/run.vim index 46d17998..66938d69 100644 --- a/tests/cases/motion/cursor/run.vim +++ b/tests/cases/motion/cursor/run.vim @@ -2,9 +2,9 @@ source $TIRENVI_ROOT/tests/common.vim lua << EOF function print_wrap(title) - local Context = require("tirenvi.app.context") + local Context = require("tirenvi.io.context") ctx = Context.from_buf() - require("tirenvi.init").auto_wrap(ctx) + require("tirenvi.app").auto_wrap(ctx) print(tostring(vim.wo[0].wrap) .. " : " .. (title or "")) end EOF @@ -46,14 +46,7 @@ CASE CSV grid call At(1, 2, 1) | lua print_wrap("grid") " ===== CONFIG ===== -lua << EOF - local M = require("tirenvi") - M.setup({ - ui = { - manage_wrap = false - }, - }) -EOF +lua require("tirenvi.config").ui.manage_wrap = false CASE CSV confg lua vim.wo[0].wrap = true diff --git a/tests/cases/motion/ftg/run.vim b/tests/cases/motion/ftg/run.vim index 15eb4464..4ffad46e 100644 --- a/tests/cases/motion/ftg/run.vim +++ b/tests/cases/motion/ftg/run.vim @@ -8,11 +8,11 @@ CASE initial lua print(Debug.layout()) CASE block#2 bottom - lua require('tirenvi').motion.block_bottom() + lua require('tirenvi.editor.motion').block_bottom() lua print(Debug.layout()) CASE block#2 top - lua require('tirenvi').motion.block_top() + lua require('tirenvi.editor.motion').block_top() lua print(Debug.layout()) CASE next cell @@ -45,13 +45,13 @@ CASE prev cell edit $TIRENVI_ROOT/tests/data/simple.csv CASE CSV bottom -lua require('tirenvi').motion.block_bottom() +lua require('tirenvi.editor.motion').block_bottom() execute "normal! " . luaeval("require('tirenvi.editor.motion').t()") normal! 2; lua print(Debug.layout()) CASE CSV top - lua require('tirenvi').motion.block_top() + lua require('tirenvi.editor.motion').block_top() lua print(Debug.layout()) call Snapshot({ 'desc': 'motion f F t T g G' }) diff --git a/tests/cases/textobj/val/out-expected.txt b/tests/cases/textobj/val/out-expected.txt index dd05f314..ab664d3c 100644 --- a/tests/cases/textobj/val/out-expected.txt +++ b/tests/cases/textobj/val/out-expected.txt @@ -1,19 +1,19 @@ --- CSV --- === MESSAGE === ---- CASE 15: initial cached attrs --- -CASE 15 // cur(1,5) b1:r1:c2+1<2> g(1,7)n[2,3*,3] +--- CASE 8: initial cached attrs --- +CASE 8 // cur(1,5) b1:r1:c2+1<2> g(1,7)n[2,3*,3] ---- CASE 19: yank column and put --- +--- CASE 12: yank column and put --- block of 7 lines yanked -CASE 19 // cur(1,12) b1:r1:c4+0<│> g(1,7)n[2,3,3,3*] +CASE 12 // cur(1,12) b1:r1:c4+0<│> g(1,7)n[2,3,3,3*] ---- CASE 26: yank 2column and put --- -CASE 26 // cur(7,3) b1:r7:c1+2<⠀> g(1,7)n[2*,3,3,3] +--- CASE 19: yank 2column and put --- +CASE 19 // cur(7,3) b1:r7:c1+2<⠀> g(1,7)n[2*,3,3,3] block of 7 lines yanked -CASE 26 // cur(1,1) b1:r1:c1+0<│> g(1,7)n[2*,3,2,3,3,3] +CASE 19 // cur(1,1) b1:r1:c1+0<│> g(1,7)n[2*,3,2,3,3,3] ---- CASE 35: repair disable --- +--- CASE 28: repair disable --- [Tirenvi] repair:disable tirenvi: Cannot select column: table is not aligned. @@ -28,15 +28,15 @@ tirenvi: Cannot select column: table is not aligned. --- GFM --- === MESSAGE === ---- CASE 46: initial cached attrs --- -CASE 46 // cur(1,1) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) +--- CASE 39: initial cached attrs --- +CASE 39 // cur(1,1) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) ---- CASE 50: yank plain --- -CASE 50 // cur(1,3) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) +--- CASE 43: yank plain --- +CASE 43 // cur(1,3) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) ---- CASE 57: yank 2column and put --- +--- CASE 50: yank 2column and put --- block of 6 lines yanked -CASE 57 // cur(3,8) b2:r1:c2+1<│> p(1,2) g(3,8)n[5,2*,3,11,3,11] p(9,11) +CASE 50 // cur(3,8) b2:r1:c2+1<│> p(1,2) g(3,8)n[5,2*,3,11,3,11] p(9,11) === DISPLAY === gfgm diff --git a/tests/cases/textobj/val/run.vim b/tests/cases/textobj/val/run.vim index 97fbb5b0..cc9957ab 100644 --- a/tests/cases/textobj/val/run.vim +++ b/tests/cases/textobj/val/run.vim @@ -1,13 +1,6 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -local M = require("tirenvi") -M.setup({ - textobj = { - column = "h" - }, -}) -EOF +lua require("tirenvi").setup({ textobj = { column = "h" }, }) " ===== CSV ===== edit $TIRENVI_ROOT/tests/data/simple.csv diff --git a/tests/cases/textobj/vil/out-expected.txt b/tests/cases/textobj/vil/out-expected.txt index a9b61c19..2f1f6fa5 100644 --- a/tests/cases/textobj/vil/out-expected.txt +++ b/tests/cases/textobj/vil/out-expected.txt @@ -1,17 +1,17 @@ --- CSV --- === MESSAGE === ---- CASE 15: initial cached attrs --- -CASE 15 // cur(1,5) b1:r1:c2+1<2> g(1,7)n[2,3*,3] +--- CASE 8: initial cached attrs --- +CASE 8 // cur(1,5) b1:r1:c2+1<2> g(1,7)n[2,3*,3] ---- CASE 19: yank column and put --- -CASE 19 // cur(1,10) b1:r1:c3+3 g(1,7)n[2,2,3*,3] +--- CASE 12: yank column and put --- +CASE 12 // cur(1,10) b1:r1:c3+3 g(1,7)n[2,2,3*,3] ---- CASE 26: yank 2column and put --- -CASE 26 // cur(7,3) b1:r7:c1+2<⠀> g(1,7)n[2*,2,3,3] -CASE 26 // cur(1,1) b1:r1:c1+0<│> g(1,7)n[2*,2,2,3,3] +--- CASE 19: yank 2column and put --- +CASE 19 // cur(7,3) b1:r7:c1+2<⠀> g(1,7)n[2*,2,3,3] +CASE 19 // cur(1,1) b1:r1:c1+0<│> g(1,7)n[2*,2,2,3,3] ---- CASE 35: repair disable --- +--- CASE 28: repair disable --- [Tirenvi] repair:disable tirenvi: Cannot select column: table is not aligned. @@ -26,14 +26,14 @@ tirenvi: Cannot select column: table is not aligned. --- GFM --- === MESSAGE === ---- CASE 46: initial cached attrs --- -CASE 46 // cur(1,1) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) +--- CASE 39: initial cached attrs --- +CASE 39 // cur(1,1) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) ---- CASE 50: yank plain --- -CASE 50 // cur(1,2) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) +--- CASE 43: yank plain --- +CASE 43 // cur(1,2) b1:r1:c0+0 p(1,2)* g(3,8)n[5,3,11] p(9,11) ---- CASE 57: yank 2column and put --- -CASE 57 // cur(3,2) b2:r1:c1+1 p(1,2) g(3,8)n[3*,11,5] p(9,11) +--- CASE 50: yank 2column and put --- +CASE 50 // cur(3,2) b2:r1:c1+1 p(1,2) g(3,8)n[3*,11,5] p(9,11) === DISPLAY === fgm diff --git a/tests/cases/textobj/vil/run.vim b/tests/cases/textobj/vil/run.vim index dd0393c6..2c117ddf 100644 --- a/tests/cases/textobj/vil/run.vim +++ b/tests/cases/textobj/vil/run.vim @@ -1,13 +1,6 @@ source $TIRENVI_ROOT/tests/common.vim -lua << EOF -local M = require("tirenvi") -M.setup({ - textobj = { - column = "h" - }, -}) -EOF +lua require("tirenvi").setup({ textobj = { column = "h" }, }) " ===== CSV ===== edit $TIRENVI_ROOT/tests/data/simple.csv diff --git a/tests/cases/tir/toggle/out-expected.txt b/tests/cases/tir/toggle/out-expected.txt index 6093aad3..9d003e0e 100644 --- a/tests/cases/tir/toggle/out-expected.txt +++ b/tests/cases/tir/toggle/out-expected.txt @@ -33,7 +33,6 @@ CASE 37 // cur(1,1) b1:r1:c0+0 p(1,2)* --- CASE 43: NG case --- Usage: :Tir Usage: :Tir -Tir repair is deprecated and will be removed in v0.5. Use :Tir redraw Usage: :Tir CASE 43 // cur(1,1) b1:r1:c0+0 p(1,2)* diff --git a/tests/cases/undo/toggle/out-expected.txt b/tests/cases/undo/toggle/out-expected.txt index 7c80c77e..fc1d03b5 100644 --- a/tests/cases/undo/toggle/out-expected.txt +++ b/tests/cases/undo/toggle/out-expected.txt @@ -2,7 +2,7 @@ --- CASE 7: Tir toggle + undo --- 1 more line; before #2