1 files changed,
137 insertions(+),
146 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-07-23 12:27:11 +0300
Authored at:
2026-07-22 17:38:21 +0300
Change ID:
lsqukyyvxxrwswrzqxmttmsqqxxmspro
Parent:
d764ee5
M
lua/viye.lua
··· 1 1 local M = {} 2 2 3 3 local function indent(text) return #text:match "^%s*" end 4 -local function is_data(text) return text:find "^%s*[|:] " ~= nil end 4 +local function is_data(text) return text:find "^%s*[|:]" ~= nil end 5 5 local function strip_bullet(text) return (text:gsub("^[+-] ", "", 1)) end 6 6 7 7 local function build_path() ··· 9 9 local line = vim.fn.getline(lnum) 10 10 local depth = indent(line) 11 11 local parts = { strip_bullet(vim.trim(line)) } 12 - 13 12 local i = lnum - 1 14 13 while i >= 1 do 15 14 local l = vim.fn.getline(i) ··· 24 23 end 25 24 i = i - 1 26 25 end 27 - 28 - for idx = 1, #parts - 1 do 29 - parts[idx] = parts[idx]:gsub("/+$", "") 30 - end 31 - 32 26 return table.concat(parts, "/") 33 27 end 34 28 35 -local function has_children() 36 - local lnum = vim.fn.line "." 37 - local depth = indent(vim.fn.getline(lnum)) 38 - local next_line = vim.fn.getline(lnum + 1) 39 - return next_line ~= "" and indent(next_line) > depth 40 -end 29 +local function exec() 30 + local ok, err = pcall(function() 31 + local line = vim.fn.getline "." 41 32 42 --- Collect : body lines under the current line (siblings at depth+2) 43 --- Returns list of stripped lines (with ": " prefix preserved) 44 -local function collect_body() 45 - local lnum = vim.fn.line "." 46 - local depth = indent(vim.fn.getline(lnum)) 47 - local body = {} 48 - local i = lnum + 1 49 - while i <= vim.fn.line "$" do 50 - local l = vim.fn.getline(i) 51 - if l == "" then break end 52 - local d = indent(l) 53 - if d <= depth then break end 54 - if d == depth + 2 and l:find("^%s*: ") then 55 - table.insert(body, vim.trim(l)) 56 - end 57 - i = i + 1 58 - end 59 - return body 60 -end 33 + -- Data line: | means collapse, : means re-execute with body 34 + if is_data(line) then 35 + local depth = indent(line) 36 + local i = vim.fn.line "." - 1 37 + while i >= 1 do 38 + local l = vim.fn.getline(i) 39 + if not is_data(l) and indent(l) < depth and l:find "%S" then 40 + vim.fn.cursor(i, 1) 41 + break 42 + end 43 + i = i - 1 44 + end 61 45 62 --- Remove | output lines under the current line (siblings at depth+2) 63 --- Returns the line number after the removed block, or nil if nothing removed 64 -local function remove_output() 65 - local lnum = vim.fn.line "." 66 - local depth = indent(vim.fn.getline(lnum)) 67 - local first = nil 68 - local last = nil 69 - local i = lnum + 1 70 - while i <= vim.fn.line "$" do 71 - local l = vim.fn.getline(i) 72 - if l == "" then break end 73 - local d = indent(l) 74 - if d <= depth then break end 75 - if d == depth + 2 and l:find("^%s*| ") then 76 - if first == nil then first = i end 77 - last = i 78 - end 79 - i = i + 1 80 - end 81 - if first then 82 - vim.cmd(string.format("%d,%ddelete _", first, last)) 83 - return first 84 - end 85 - return nil 86 -end 46 + local lnum = vim.fn.line "." 47 + local depth = indent(vim.fn.getline(lnum)) 48 + local first_child = lnum + 1 49 + local body = {} 50 + local last_child = nil 51 + local i = first_child 52 + while i <= vim.fn.line "$" do 53 + local l = vim.fn.getline(i) 54 + if l == "" then break end 55 + local d = indent(l) 56 + if d <= depth then break end 57 + if d == depth + 2 then 58 + if l:find "^%s*: " then table.insert(body, vim.trim(l)) end 59 + last_child = i 60 + end 61 + i = i + 1 62 + end 87 63 88 -local function collapse() 89 - local lnum = vim.fn.line "." 90 - local depth = indent(vim.fn.getline(lnum)) 91 - local s = lnum + 1 92 - local e = s 93 - while e <= vim.fn.line "$" do 94 - local l = vim.fn.getline(e) 95 - if l == "" or indent(l) <= depth then break end 96 - e = e + 1 97 - end 98 - if e > s then vim.cmd(string.format("%d,%ddelete _", s, e - 1)) end 99 -end 64 + if line:find "^%s*|" then 65 + -- | line: collapse (delete children, toggle to +) 66 + if last_child then vim.cmd(string.format("%d,%ddelete _", first_child, last_child)) end 67 + vim.fn.setline(lnum, (vim.fn.getline(lnum):gsub("^%- ", "+ ", 1))) 68 + return 69 + end 70 + 71 + -- : line: re-execute with body (phase 2) 72 + local path = build_path() 73 + if path == "" then return end 74 + 75 + local cmd = "viye " .. vim.fn.shellescape(path) 76 + if #body > 0 then 77 + cmd = cmd .. " --" 78 + for _, b in ipairs(body) do 79 + cmd = cmd .. " " .. vim.fn.shellescape(b) 80 + end 81 + end 100 82 101 -local function toggle_bullet() 102 - local lnum = vim.fn.line "." 103 - local line = vim.fn.getline(lnum) 104 - local toggled = line:gsub("^%+ ", "- ", 1) 105 - if toggled ~= line then 106 - vim.fn.setline(lnum, toggled) 107 - return 108 - end 109 - toggled = line:gsub("^%- ", "+ ", 1) 110 - if toggled ~= line then vim.fn.setline(lnum, toggled) end 111 -end 83 + if last_child then vim.cmd(string.format("%d,%ddelete _", first_child, last_child)) end 112 84 113 -local function insert_output(output, after_lnum) 114 - after_lnum = after_lnum or vim.fn.line "." 115 - local pad = string.rep(" ", indent(vim.fn.getline(after_lnum)) + 2) 116 - local lines = vim.split(output, "\n", { trimempty = true }) 117 - for i, line in ipairs(lines) do lines[i] = pad .. line end 118 - vim.fn.append(after_lnum, lines) 119 -end 85 + local out = vim.fn.system(cmd) 86 + if vim.v.shell_error ~= 0 then 87 + vim.notify("viye: " .. (out:gsub("\n$", "")), vim.log.levels.ERROR) 88 + return 89 + end 120 90 121 --- Find the last child line at depth+2 under the current line 122 -local function find_last_child() 123 - local lnum = vim.fn.line "." 124 - local depth = indent(vim.fn.getline(lnum)) 125 - local last = lnum 126 - local i = lnum + 1 127 - while i <= vim.fn.line "$" do 128 - local l = vim.fn.getline(i) 129 - if l == "" then break end 130 - local d = indent(l) 131 - if d <= depth then break end 132 - if d == depth + 2 then 133 - last = i 91 + if out ~= "" then 92 + local pad = string.rep(" ", depth + 2) 93 + local lines = vim.split(out, "\n", { trimempty = true }) 94 + for j, l in ipairs(lines) do lines[j] = pad .. l end 95 + vim.fn.append(lnum, lines) 96 + end 97 + return 134 98 end 135 - i = i + 1 136 - end 137 - return last 138 -end 139 99 140 -local function exec(reenter) 141 - local line = vim.fn.getline "." 142 - if is_data(line) then 100 + -- Command line: toggle expand/collapse 143 101 local lnum = vim.fn.line "." 144 - local depth = indent(line) 145 - local i = lnum - 1 146 - while i >= 1 do 102 + local depth = indent(vim.fn.getline(lnum)) 103 + 104 + local first_child = lnum + 1 105 + local last_child = nil 106 + local i = first_child 107 + while i <= vim.fn.line "$" do 147 108 local l = vim.fn.getline(i) 148 - if not is_data(l) and indent(l) < depth and l:find "%S" then 149 - vim.fn.cursor(i, 1) 150 - exec(true) 151 - return 152 - end 153 - i = i - 1 109 + if l == "" then break end 110 + local d = indent(l) 111 + if d <= depth then break end 112 + if d == depth + 2 then last_child = i end 113 + i = i + 1 154 114 end 155 - return 156 - end 157 115 158 - if not reenter and has_children() then 159 - collapse() 160 - toggle_bullet() 161 - return 162 - end 116 + if last_child then 117 + -- Expanded: collapse 118 + vim.cmd(string.format("%d,%ddelete _", first_child, last_child)) 119 + vim.fn.setline(lnum, (vim.fn.getline(lnum):gsub("^%- ", "+ ", 1))) 120 + return 121 + end 163 122 164 - local path = build_path() 165 - if path == "" then return end 123 + -- Collapsed: run CLI and insert output 124 + local path = build_path() 125 + if path == "" then return end 166 126 167 - local body = reenter and collect_body() or {} 168 - 169 - local cmd = "viye " .. vim.fn.shellescape(path) 170 - if #body > 0 then 171 - cmd = cmd .. " --" 172 - for _, line in ipairs(body) do 173 - cmd = cmd .. " " .. vim.fn.shellescape(line) 127 + local cmd = "viye " .. vim.fn.shellescape(path) 128 + local out = vim.fn.system(cmd) 129 + if vim.v.shell_error ~= 0 then 130 + vim.notify("viye: " .. (out:gsub("\n$", "")), vim.log.levels.ERROR) 131 + return 174 132 end 175 - end 176 - vim.notify("viye: " .. cmd, vim.log.levels.INFO) 177 133 178 - local out = vim.fn.system(cmd) 179 - if vim.v.shell_error ~= 0 then 180 - vim.notify("viye: " .. out:gsub("\n$", ""), vim.log.levels.ERROR) 181 - return 182 - end 183 - 184 - if out ~= "" then 185 - remove_output() 186 - local target = reenter and find_last_child() or vim.fn.line "." 187 - insert_output(out, target) 188 - if not reenter then 189 - toggle_bullet() 134 + if out ~= "" then 135 + local pad = string.rep(" ", depth + 2) 136 + local lines = vim.split(out, "\n", { trimempty = true }) 137 + for j, l in ipairs(lines) do lines[j] = pad .. l end 138 + vim.fn.append(lnum, lines) 139 + vim.fn.setline(lnum, (vim.fn.getline(lnum):gsub("^%+ ", "- ", 1))) 140 + else 141 + vim.fn.setline(lnum, (vim.fn.getline(lnum):gsub("^%- ", "+ ", 1))) 190 142 end 143 + end) 144 + if not ok then 145 + vim.notify("viye: " .. tostring(err), vim.log.levels.ERROR) 191 146 end 192 147 end 193 148 ··· 198 153 group = vim.api.nvim_create_augroup("viye_plugin", { clear = true }), 199 154 callback = function() vim.keymap.set("n", "<CR>", exec, { buffer = true, silent = true }) end, 200 155 }) 156 +end 157 + 158 +if false then -- tests 159 + local ok, fail = 0, 0 160 + local function check(got, want, msg) 161 + if got == want then 162 + ok = ok + 1 163 + else 164 + fail = fail + 1 165 + print("FAIL " .. msg .. ": got " .. vim.inspect(got) .. " want " .. vim.inspect(want)) 166 + end 167 + end 168 + 169 + check(indent "", 0, "indent empty") 170 + check(indent "foo", 0, "indent no indent") 171 + check(indent " foo", 2, "indent 2 spaces") 172 + check(indent " foo", 4, "indent 4 spaces") 173 + 174 + check(is_data ": hello", true, "is_data colon") 175 + check(is_data " : hello", true, "is_data colon indent") 176 + check(is_data "| hello", true, "is_data pipe") 177 + check(is_data " |", true, "is_data pipe alone") 178 + check(is_data "+ demo", false, "is_data not +") 179 + check(is_data "- demo", false, "is_data not -") 180 + check(is_data "hello", false, "is_data plain") 181 + 182 + check(strip_bullet "+ demo", "demo", "strip +") 183 + check(strip_bullet "- demo", "demo", "strip -") 184 + check(strip_bullet "demo", "demo", "strip none") 185 + check(strip_bullet "+ hello world", "hello world", "strip + multiword") 186 + 187 + if fail == 0 then 188 + print("viye.lua: " .. ok .. "/" .. (ok + fail) .. " tests passed") 189 + else 190 + print("viye.lua: " .. fail .. " failures out of " .. (ok + fail) .. " tests") 191 + end 201 192 end 202 193 203 194 return M