all repos

viye @ aac48460ab9c49d76dc3bf15746ee66308cd50a7

my shot at reimplementing xiki
6 files changed, 174 insertions(+), 306 deletions(-)
move away from xiki '/' separated command path to each node is it's own arg model
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-30 23:38:41 +0300
Authored at: 2026-07-30 21:58:23 +0300
Change ID: lrtuvzslqopvsktwvruuuyzttptnzqym
Parent: fa602cd
M internal/plumbing/config/config.go
···
        11
        11
         type Tool struct{}

      
        12
        12
         

      
        13
        13
         func (Tool) Name() string               { return "config" }

      
        14
        
        -func (Tool) Match(c *viye.Context) bool { return c.Path[0] == "config" }

      
        
        14
        +func (Tool) Match(c *viye.Context) bool { return c.Cmd == "config" }

      
        15
        15
         func (Tool) Execute(c *viye.Context) (string, error) {

      
        16
        16
         	cfg, err := config.Load()

      
        17
        17
         	if err != nil {

      
M internal/plumbing/files/files.go
···
        12
        12
         

      
        13
        13
         type Tool struct{}

      
        14
        14
         

      
        15
        
        -func (Tool) Name() string { return "files(~/..., /...)" }

      
        
        15
        +func (Tool) Name() string { return "files(~/..., /..., ./...)" }

      
        16
        16
         

      
        17
        17
         func (Tool) Match(ctx *viye.Context) bool {

      
        18
        
        -	p := ctx.Path[0]

      
        19
        
        -	if p == "~" || strings.HasPrefix(p, "/") {

      
        20
        
        -		return true

      
        21
        
        -	}

      
        22
        
        -	_, err := os.Stat(filepath.Join(ctx.Dir, p))

      
        23
        
        -	return err == nil

      
        
        18
        +	return strings.HasPrefix(ctx.Path[0], "/") || strings.HasPrefix(ctx.Path[0], "./") ||

      
        
        19
        +		strings.HasPrefix(ctx.Path[0], "~/")

      
        24
        20
         }

      
        25
        21
         

      
        26
        22
         func (Tool) Execute(ctx *viye.Context) (string, error) {

      
M internal/plumbing/files/files_test.go
···
        10
        10
         )

      
        11
        11
         

      
        12
        12
         func TestMatch(t *testing.T) {

      
        13
        
        -	dir := t.TempDir()

      
        14
        
        -	os.Mkdir(filepath.Join(dir, "sub"), 0o755)

      
        15
        
        -	os.WriteFile(filepath.Join(dir, "f"), nil, 0o644)

      
        16
        
        -

      
        17
        13
         	tests := []struct {

      
        18
        
        -		p, dir string

      
        19
        
        -		m      bool

      
        
        14
        +		p string

      
        
        15
        +		m bool

      
        20
        16
         	}{

      
        21
        
        -		{"~", ".", true},

      
        22
        
        -		{"/tmp", ".", true},

      
        23
        
        -		{dir, ".", true},

      
        24
        
        -		{"sub", dir, true},

      
        25
        
        -		{"f", dir, true},

      
        26
        
        -		{"nope", dir, false},

      
        27
        
        -		{"$ ls", dir, false},

      
        28
        
        -		{".", dir, true},

      
        29
        
        -		{"..", dir, true},

      
        
        17
        +		{"~/foo", true},

      
        
        18
        +		{"/tmp", true},

      
        
        19
        +		{"/tmp/foo", true},

      
        
        20
        +		{"./foo", true},

      
        
        21
        +		{"./foo/bar", true},

      
        
        22
        +

      
        
        23
        +		{"nope", false},

      
        
        24
        +		{"$ ls", false},

      
        
        25
        +		{"~", false},

      
        
        26
        +		{".", false},

      
        
        27
        +		{"..", false},

      
        30
        28
         	}

      
        31
        29
         	for _, tt := range tests {

      
        32
        
        -		got := (&Tool{}).Match(&viye.Context{Path: []string{tt.p}, Dir: tt.dir})

      
        
        30
        +		got := (&Tool{}).Match(&viye.Context{Path: []string{tt.p}})

      
        33
        31
         		if got != tt.m {

      
        34
        
        -			t.Errorf("Match(%q, dir=%q) = %v; want %v", tt.p, tt.dir, got, tt.m)

      
        
        32
        +			t.Errorf("Match(%q) = %v; want %v", tt.p, got, tt.m)

      
        35
        33
         		}

      
        36
        34
         	}

      
        37
        35
         }

      ···
        51
        49
         		}

      
        52
        50
         	})

      
        53
        51
         

      
        54
        
        -	t.Run("navigate into subdir", func(t *testing.T) {

      
        
        52
        +	t.Run("navigate into subdir via full path", func(t *testing.T) {

      
        55
        53
         		dir := t.TempDir()

      
        56
        54
         		sub := filepath.Join(dir, "sub")

      
        57
        55
         		os.Mkdir(sub, 0o755)

      ···
        60
        58
         		v := viye.New()

      
        61
        59
         		v.Register(&Tool{})

      
        62
        60
         		var out strings.Builder

      
        63
        
        -		if err := v.Run(&out, []string{"viye", dir, "sub"}); err != nil {

      
        
        61
        +		if err := v.Run(&out, []string{"viye", sub}); err != nil {

      
        64
        62
         			t.Fatal(err)

      
        65
        63
         		}

      
        66
        64
         		want := "| y\n"

      
M internal/viye/viye.go
···
        107
        107
         	return parts[0], parts[1:]

      
        108
        108
         }

      
        109
        109
         

      
        110
        
        -// splitArgs builds path by splitting each arg on "/" and body on "--".

      
        111
        
        -// Everything before "--" is the path (split into components).

      
        
        110
        +// splitArgs builds path and body from args.

      
        
        111
        +// Everything before "--" is the path (each arg is one component).

      
        112
        112
         // Everything after "--" is the body (kept as-is, one element per line).

      
        113
        113
         // If there's no "--", body is nil.

      
        114
        
        -// for absolute paths(/...), leading / is preserved as part of first component.

      
        115
        
        -// for home paths(~...), ~ is the first comonents

      
        116
        
        -// urls are kept as single component

      
        117
        114
         func splitArgs(args []string) (path []string, body []string) {

      
        118
        115
         	sawSep := false

      
        119
        116
         	for _, arg := range args {

      ···
        122
        119
         			continue

      
        123
        120
         		}

      
        124
        121
         		if !sawSep {

      
        125
        
        -			path = append(path, splitArg(arg)...)

      
        
        122
        +			path = append(path, arg)

      
        126
        123
         		} else {

      
        127
        124
         			body = append(body, arg)

      
        128
        125
         		}

      
        129
        126
         	}

      
        130
        127
         	return

      
        131
        
        -}

      
        132
        
        -

      
        133
        
        -func splitArg(arg string) []string {

      
        134
        
        -	switch {

      
        135
        
        -	case strings.HasPrefix(arg, "/"):

      
        136
        
        -		parts := strings.Split(arg, "/")

      
        137
        
        -		// parts[0] is always "" for absolute paths

      
        138
        
        -		i := 0

      
        139
        
        -		for i < len(parts) && parts[i] == "" {

      
        140
        
        -			i++

      
        141
        
        -		}

      
        142
        
        -		if i >= len(parts) {

      
        143
        
        -			return []string{"/"}

      
        144
        
        -		}

      
        145
        
        -		// parts[i] is the first non-empty segment → attach it to "/"

      
        146
        
        -		result := []string{"/" + parts[i]}

      
        147
        
        -		i++

      
        148
        
        -		for ; i < len(parts); i++ {

      
        149
        
        -			if parts[i] != "" {

      
        150
        
        -				result = append(result, parts[i])

      
        151
        
        -			}

      
        152
        
        -		}

      
        153
        
        -		return result

      
        154
        
        -	case strings.HasPrefix(arg, "~"):

      
        155
        
        -		parts := strings.Split(arg, "/")

      
        156
        
        -		var result []string

      
        157
        
        -		for _, p := range parts {

      
        158
        
        -			if p != "" {

      
        159
        
        -				result = append(result, p)

      
        160
        
        -			}

      
        161
        
        -		}

      
        162
        
        -		return result

      
        163
        
        -	case strings.HasPrefix(arg, "http://") || strings.HasPrefix(arg, "https://"):

      
        164
        
        -		return []string{arg}

      
        165
        
        -	default:

      
        166
        
        -		s := strings.TrimPrefix(arg, "./")

      
        167
        
        -		if s != arg {

      
        168
        
        -			if s == "" {

      
        169
        
        -				return []string{"."}

      
        170
        
        -			}

      
        171
        
        -			// No "/" left to split on — keep original (e.g. "./..." stays "./...")

      
        172
        
        -			if !strings.Contains(s, "/") {

      
        173
        
        -				return []string{arg}

      
        174
        
        -			}

      
        175
        
        -			return splitArg(s)

      
        176
        
        -		}

      
        177
        
        -		parts := strings.Split(s, "/")

      
        178
        
        -		var result []string

      
        179
        
        -		for _, p := range parts {

      
        180
        
        -			if p != "" {

      
        181
        
        -				result = append(result, p)

      
        182
        
        -			}

      
        183
        
        -		}

      
        184
        
        -		return result

      
        185
        
        -	}

      
        186
        128
         }

      
        187
        129
         

      
        188
        130
         func mustGetCwd() string {

      
M internal/viye/viye_test.go
···
        40
        40
         	}

      
        41
        41
         }

      
        42
        42
         

      
        43
        
        -func TestSplitArg(t *testing.T) {

      
        44
        
        -	tests := []struct {

      
        45
        
        -		arg  string

      
        46
        
        -		want []string

      
        47
        
        -	}{

      
        48
        
        -		{"foo", []string{"foo"}},

      
        49
        
        -		{"foo/bar", []string{"foo", "bar"}},

      
        50
        
        -		{"foo/bar/", []string{"foo", "bar"}},

      
        51
        
        -		{"a/b/c", []string{"a", "b", "c"}},

      
        52
        
        -

      
        53
        
        -		// absolute paths

      
        54
        
        -		{"/", []string{"/"}},

      
        55
        
        -		{"/tmp", []string{"/tmp"}},

      
        56
        
        -		{"/tmp/foo", []string{"/tmp", "foo"}},

      
        57
        
        -		{"/tmp/foo/", []string{"/tmp", "foo"}},

      
        58
        
        -		{"/a/b/c", []string{"/a", "b", "c"}},

      
        59
        
        -

      
        60
        
        -		// home paths

      
        61
        
        -		{"~", []string{"~"}},

      
        62
        
        -		{"~/foo", []string{"~", "foo"}},

      
        63
        
        -		{"~/a/b", []string{"~", "a", "b"}},

      
        64
        
        -

      
        65
        
        -		// ./ paths

      
        66
        
        -		{"./", []string{"."}},

      
        67
        
        -		{"./foo", []string{"./foo"}},

      
        68
        
        -		{"./foo/bar", []string{"foo", "bar"}},

      
        69
        
        -		{"././foo", []string{"./foo"}},

      
        70
        
        -		{"./././", []string{"."}},

      
        71
        
        -		{".", []string{"."}},

      
        72
        
        -		{"..", []string{".."}},

      
        73
        
        -		{"../foo", []string{"..", "foo"}},

      
        74
        
        -		{"./../foo", []string{"..", "foo"}},

      
        75
        
        -

      
        76
        
        -		// urls

      
        77
        
        -		{"http://example.com", []string{"http://example.com"}},

      
        78
        
        -		{"https://example.com/path", []string{"https://example.com/path"}},

      
        79
        
        -	}

      
        80
        
        -	for _, tt := range tests {

      
        81
        
        -		got := splitArg(tt.arg)

      
        82
        
        -		if len(got) != len(tt.want) {

      
        83
        
        -			t.Errorf("splitArg(%q) = %v; want %v", tt.arg, got, tt.want)

      
        84
        
        -			continue

      
        85
        
        -		}

      
        86
        
        -		for i := range got {

      
        87
        
        -			if got[i] != tt.want[i] {

      
        88
        
        -				t.Errorf("splitArg(%q) = %v; want %v", tt.arg, got, tt.want)

      
        89
        
        -				break

      
        90
        
        -			}

      
        91
        
        -		}

      
        92
        
        -	}

      
        93
        
        -}

      
        94
        
        -

      
        95
        43
         func TestSplitArgs(t *testing.T) {

      
        96
        44
         	tests := []struct {

      
        97
        
        -		args    []string

      
        98
        
        -		want    []string

      
        99
        
        -		wantB   []string

      
        
        45
        +		args  []string

      
        
        46
        +		want  []string

      
        
        47
        +		wantB []string

      
        100
        48
         	}{

      
        101
        
        -		{[]string{"/tmp/foo", "bar/baz"}, []string{"/tmp", "foo", "bar", "baz"}, nil},

      
        
        49
        +		{[]string{"/tmp/foo", "bar/baz"}, []string{"/tmp/foo", "bar/baz"}, nil},

      
        102
        50
         		{[]string{"./foo", "bar"}, []string{"./foo", "bar"}, nil},

      
        103
        
        -		{[]string{"./"}, []string{"."}, nil},

      
        104
        
        -		{[]string{"././foo", "./bar"}, []string{"./foo", "./bar"}, nil},

      
        
        51
        +		{[]string{"./"}, []string{"./"}, nil},

      
        
        52
        +		{[]string{"././foo", "./bar"}, []string{"././foo", "./bar"}, nil},

      
        105
        53
         		{[]string{"demo"}, []string{"demo"}, nil},

      
        106
        54
         		{[]string{"demo", "--", ": name: olex"}, []string{"demo"}, []string{": name: olex"}},

      
        107
        55
         		{[]string{"demo", "--", ": name: olex", ": age: 30"}, []string{"demo"}, []string{": name: olex", ": age: 30"}},

      
M lua/viye.lua
···
        1
        1
         local M = {}

      
        2
        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

      
        
        3
        +local function indent(text) return #text:match("^%s*") end

      
        
        4
        +local function is_data(line) return line:find("^%s*[|:]") ~= nil end

      
        6
        5
         

      
        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

      
        
        6
        +local function children_of(cmd_lnum)

      
        
        7
        +  local depth = indent(vim.fn.getline(cmd_lnum))

      
        
        8
        +  local first = cmd_lnum + 1

      
        
        9
        +  local last = nil

      
        
        10
        +  local i = first

      
        
        11
        +  while i <= vim.fn.line("$") do

      
        14
        12
             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

      
        
        13
        +    if l == "" then break end

      
        
        14
        +    if indent(l) <= depth then break end

      
        
        15
        +    last = i

      
        
        16
        +    i = i + 1

      
        25
        17
           end

      
        26
        
        -  return table.concat(parts, "/")

      
        
        18
        +  return first, last

      
        27
        19
         end

      
        28
        20
         

      
        29
        
        -local function exec()

      
        30
        
        -  local ok, err = pcall(function()

      
        31
        
        -    local line = vim.fn.getline "."

      
        
        21
        +local function parent_of(lnum)

      
        
        22
        +  local d = indent(vim.fn.getline(lnum))

      
        
        23
        +  while lnum >= 1 do

      
        
        24
        +    local l = vim.fn.getline(lnum)

      
        
        25
        +    if not is_data(l) and l:find("%S") and indent(l) < d then return lnum end

      
        
        26
        +    lnum = lnum - 1

      
        
        27
        +  end

      
        
        28
        +  return nil

      
        
        29
        +end

      
        32
        30
         

      
        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

      
        
        31
        +local function ancestors(cmd_lnum)

      
        
        32
        +  local parts = {}

      
        
        33
        +  local depth = math.huge

      
        
        34
        +  local i = cmd_lnum

      
        
        35
        +  while i >= 1 do

      
        
        36
        +    local l = vim.fn.getline(i)

      
        
        37
        +    if not is_data(l) and l:find("%S") then

      
        
        38
        +      local d = indent(l)

      
        
        39
        +      if d < depth then

      
        
        40
        +        local text = l:match("^%s*[+-] (.*)")

      
        
        41
        +        if not text then text = vim.trim(l) end

      
        
        42
        +        table.insert(parts, 1, text)

      
        
        43
        +        depth = d

      
        
        44
        +        if d == 0 then break end

      
        44
        45
               end

      
        
        46
        +    end

      
        
        47
        +    i = i - 1

      
        
        48
        +  end

      
        
        49
        +  return parts

      
        
        50
        +end

      
        45
        51
         

      
        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

      
        
        52
        +local function body_of(cmd_lnum)

      
        
        53
        +  local depth = indent(vim.fn.getline(cmd_lnum))

      
        
        54
        +  local body = {}

      
        
        55
        +  local i = cmd_lnum + 1

      
        
        56
        +  while i <= vim.fn.line("$") do

      
        
        57
        +    local l = vim.fn.getline(i)

      
        
        58
        +    if l == "" then break end

      
        
        59
        +    local d = indent(l)

      
        
        60
        +    if d <= depth then break end

      
        
        61
        +    if d == depth + 2 then

      
        
        62
        +      local val = l:match("^%s*: (.*)")

      
        
        63
        +      if val then

      
        
        64
        +        table.insert(body, ": " .. val)

      
        62
        65
               end

      
        
        66
        +    end

      
        
        67
        +    i = i + 1

      
        
        68
        +  end

      
        
        69
        +  return body

      
        
        70
        +end

      
        63
        71
         

      
        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

      
        
        72
        +local function run(cmd_lnum)

      
        
        73
        +  local parts = ancestors(cmd_lnum)

      
        
        74
        +  if #parts == 0 then return nil end

      
        70
        75
         

      
        71
        
        -      -- : line: re-execute with body (phase 2)

      
        72
        
        -      local path = build_path()

      
        73
        
        -      if path == "" then return end

      
        
        76
        +  local cmd = "viye"

      
        
        77
        +  for _, p in ipairs(parts) do

      
        
        78
        +    cmd = cmd .. " " .. vim.fn.shellescape(p)

      
        
        79
        +  end

      
        
        80
        +  local body = body_of(cmd_lnum)

      
        
        81
        +  if #body > 0 then

      
        
        82
        +    cmd = cmd .. " --"

      
        
        83
        +    for _, b in ipairs(body) do

      
        
        84
        +      cmd = cmd .. " " .. vim.fn.shellescape(b)

      
        
        85
        +    end

      
        
        86
        +  end

      
        74
        87
         

      
        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

      
        
        88
        +  vim.notify("ex'ing: " .. cmd, vim.log.levels.INFO)

      
        
        89
        +  local out = vim.fn.system(cmd)

      
        
        90
        +  if vim.v.shell_error ~= 0 then

      
        
        91
        +    vim.notify("viye: " .. (vim.trim(out or "")), vim.log.levels.ERROR)

      
        
        92
        +    return nil

      
        
        93
        +  end

      
        
        94
        +  return out

      
        
        95
        +end

      
        82
        96
         

      
        83
        
        -      local out = vim.fn.system(cmd)

      
        84
        
        -      if vim.v.shell_error ~= 0 then

      
        85
        
        -        vim.notify("viye: " .. (out:gsub("\n$", "")), vim.log.levels.ERROR)

      
        86
        
        -        return

      
        87
        
        -      end

      
        
        97
        +local function delete_range(first, last)

      
        
        98
        +  if last then vim.cmd(string.format("%d,%ddelete _", first, last)) end

      
        
        99
        +end

      
        88
        100
         

      
        89
        
        -      if last_child then vim.cmd(string.format("%d,%ddelete _", first_child, last_child)) end

      
        
        101
        +local function insert_output(cmd_lnum, out)

      
        
        102
        +  local pad = string.rep(" ", indent(vim.fn.getline(cmd_lnum)) + 2)

      
        
        103
        +  local lines = vim.split(out, "\n", { trimempty = true })

      
        
        104
        +  for i, l in ipairs(lines) do

      
        
        105
        +    lines[i] = pad .. l

      
        
        106
        +  end

      
        
        107
        +  vim.fn.append(cmd_lnum, lines)

      
        
        108
        +end

      
        90
        109
         

      
        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

      
        
        110
        +local function toggle_bullet(cmd_lnum, to)

      
        
        111
        +  vim.fn.setline(cmd_lnum, (vim.fn.getline(cmd_lnum):gsub("^[+-] ", to .. " ", 1)))

      
        
        112
        +end

      
        99
        113
         

      
        100
        
        -    -- Command line: toggle expand/collapse

      
        101
        
        -    local lnum = vim.fn.line "."

      
        102
        
        -    local depth = indent(vim.fn.getline(lnum))

      
        
        114
        +local function exec()

      
        
        115
        +  local lnum = vim.fn.line(".")

      
        
        116
        +  local line = vim.fn.getline(lnum)

      
        103
        117
         

      
        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

      
        
        118
        +  -- find the enclosing command line

      
        
        119
        +  local cmd_lnum

      
        
        120
        +  if is_data(line) then

      
        
        121
        +    cmd_lnum = parent_of(lnum)

      
        
        122
        +  else

      
        
        123
        +    cmd_lnum = lnum

      
        
        124
        +  end

      
        
        125
        +  if not cmd_lnum then return end

      
        115
        126
         

      
        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

      
        
        127
        +  local first, last = children_of(cmd_lnum)

      
        
        128
        +  local on_cmd = cmd_lnum == lnum

      
        
        129
        +  local has_bullet = line:find("^%s*[+-] ")

      
        122
        130
         

      
        123
        
        -    -- Collapsed: run CLI and insert output

      
        124
        
        -    local path = build_path()

      
        125
        
        -    if path == "" then return end

      
        
        131
        +  if on_cmd and last and has_bullet then

      
        
        132
        +    -- expanded togglable command -> collapse

      
        
        133
        +    delete_range(first, last)

      
        
        134
        +    toggle_bullet(cmd_lnum, "+")

      
        
        135
        +    return

      
        
        136
        +  end

      
        126
        137
         

      
        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

      
        
        138
        +  -- | output line: collapse without re-running

      
        
        139
        +  if line:find("^%s*|") then

      
        
        140
        +    if last then

      
        
        141
        +      delete_range(first, last)

      
        
        142
        +      toggle_bullet(cmd_lnum, "+")

      
        132
        143
             end

      
        
        144
        +    return

      
        
        145
        +  end

      
        133
        146
         

      
        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)

      
        
        147
        +  -- collapsed command, or Enter on a child: run and (re-)expand

      
        
        148
        +  local out = run(cmd_lnum)

      
        
        149
        +  if not out then return end

      
        
        150
        +

      
        
        151
        +  delete_range(first, last)

      
        
        152
        +  if out ~= "" then

      
        
        153
        +    insert_output(cmd_lnum, out)

      
        146
        154
           end

      
        
        155
        +  toggle_bullet(cmd_lnum, "-")

      
        147
        156
         end

      
        148
        157
         

      
        149
        158
         function M.setup()

      
        150
        
        -  vim.filetype.add { extension = { viye = "viye" } }

      
        
        159
        +  vim.filetype.add({ extension = { viye = "viye" } })

      
        
        160
        +  local group = vim.api.nvim_create_augroup("viye", { clear = true })

      
        151
        161
           vim.api.nvim_create_autocmd("FileType", {

      
        152
        162
             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,

      
        
        163
        +    group = group,

      
        
        164
        +    callback = function()

      
        
        165
        +      vim.keymap.set("n", "<CR>", exec, { buffer = true, silent = true })

      
        
        166
        +    end,

      
        155
        167
           })

      
        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

      
        
        168
        +  vim.api.nvim_create_autocmd("BufWinEnter", {

      
        
        169
        +    group = group,

      
        
        170
        +    callback = function()

      
        
        171
        +      if vim.fn.expand("%") == "" and vim.bo.buftype == "" then

      
        
        172
        +        vim.bo.filetype = "viye"

      
        
        173
        +      end

      
        
        174
        +    end,

      
        
        175
        +  })

      
        192
        176
         end

      
        193
        177
         

      
        194
        178
         return M