all repos

viye @ 4e123d7

my shot at reimplementing xiki

viye/lua/viye.lua (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
update lua, 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
  local i = lnum - 1
13
  while i >= 1 do
14
    local l = vim.fn.getline(i)
15
    local d = indent(l)
16
    if d == 0 and l:find "%S" and depth > 0 then
17
      table.insert(parts, 1, strip_bullet(vim.trim(l)))
18
      break
19
    end
20
    if d < depth and l:find "%S" then
21
      table.insert(parts, 1, strip_bullet(vim.trim(l)))
22
      depth = d
23
    end
24
    i = i - 1
25
  end
26
  return table.concat(parts, "/")
27
end
28
29
local function exec()
30
  local ok, err = pcall(function()
31
    local line = vim.fn.getline "."
32
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
45
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
63
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
82
83
      if last_child then vim.cmd(string.format("%d,%ddelete _", first_child, last_child)) end
84
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
90
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
98
    end
99
100
    -- Command line: toggle expand/collapse
101
    local lnum = vim.fn.line "."
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
108
      local l = vim.fn.getline(i)
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
114
    end
115
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
122
123
    -- Collapsed: run CLI and insert output
124
    local path = build_path()
125
    if path == "" then return end
126
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
132
    end
133
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)))
142
    end
143
  end)
144
  if not ok then
145
    vim.notify("viye: " .. tostring(err), vim.log.levels.ERROR)
146
  end
147
end
148
149
function M.setup()
150
  vim.filetype.add { extension = { viye = "viye" } }
151
  vim.api.nvim_create_autocmd("FileType", {
152
    pattern = "viye",
153
    group = vim.api.nvim_create_augroup("viye_plugin", { clear = true }),
154
    callback = function() vim.keymap.set("n", "<CR>", exec, { buffer = true, silent = true }) end,
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
192
end
193
194
return M