Oleksandr Smirnov
Oleksandr Smirnov
olexsmir@gmail.com files: support ./ paths; add gobin; add json, 2 months ago
olexsmir@gmail.com files: support ./ paths; add gobin; add json, 2 months ago
| 1 | local M = {} |
| 2 | |
| 3 | local function indent(text) return #text:match "^%s*" end |
| 4 | local function is_data(text) return text:find "^%s*[|:] " ~= nil end |
| 5 | local function strip_bullet(text) return (text:gsub("^[+-] ", "", 1)) end |
| 6 | |
| 7 | local function build_path() |
| 8 | local lnum = vim.fn.line "." |
| 9 | local line = vim.fn.getline(lnum) |
| 10 | local depth = indent(line) |
| 11 | local parts = { strip_bullet(vim.trim(line)) } |
| 12 | |
| 13 | local i = lnum - 1 |
| 14 | while i >= 1 do |
| 15 | local l = vim.fn.getline(i) |
| 16 | local d = indent(l) |
| 17 | if d == 0 and l:find "%S" and depth > 0 then |
| 18 | table.insert(parts, 1, strip_bullet(vim.trim(l))) |
| 19 | break |
| 20 | end |
| 21 | if d < depth and l:find "%S" then |
| 22 | table.insert(parts, 1, strip_bullet(vim.trim(l))) |
| 23 | depth = d |
| 24 | end |
| 25 | i = i - 1 |
| 26 | end |
| 27 | |
| 28 | for idx = 1, #parts - 1 do |
| 29 | parts[idx] = parts[idx]:gsub("/+$", "") |
| 30 | end |
| 31 | |
| 32 | return table.concat(parts, "/") |
| 33 | end |
| 34 | |
| 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 |
| 41 | |
| 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 |
| 61 | |
| 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 |
| 87 | |
| 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 |
| 100 | |
| 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 |
| 112 | |
| 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 |
| 120 | |
| 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 |
| 134 | end |
| 135 | i = i + 1 |
| 136 | end |
| 137 | return last |
| 138 | end |
| 139 | |
| 140 | local function exec(reenter) |
| 141 | local line = vim.fn.getline "." |
| 142 | if is_data(line) then |
| 143 | local lnum = vim.fn.line "." |
| 144 | local depth = indent(line) |
| 145 | local i = lnum - 1 |
| 146 | while i >= 1 do |
| 147 | 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 |
| 154 | end |
| 155 | return |
| 156 | end |
| 157 | |
| 158 | if not reenter and has_children() then |
| 159 | collapse() |
| 160 | toggle_bullet() |
| 161 | return |
| 162 | end |
| 163 | |
| 164 | local path = build_path() |
| 165 | if path == "" then return end |
| 166 | |
| 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) |
| 174 | end |
| 175 | end |
| 176 | vim.notify("viye: " .. cmd, vim.log.levels.INFO) |
| 177 | |
| 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() |
| 190 | end |
| 191 | end |
| 192 | end |
| 193 | |
| 194 | function M.setup() |
| 195 | vim.filetype.add { extension = { viye = "viye" } } |
| 196 | vim.api.nvim_create_autocmd("FileType", { |
| 197 | pattern = "viye", |
| 198 | group = vim.api.nvim_create_augroup("viye_plugin", { clear = true }), |
| 199 | callback = function() vim.keymap.set("n", "<CR>", exec, { buffer = true, silent = true }) end, |
| 200 | }) |
| 201 | end |
| 202 | |
| 203 | return M |