summaryrefslogtreecommitdiff
path: root/config/essentials/vis/complete-line.lua
blob: 93728a395cd127704d82788ba4310e9f8ed44e96 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
-- SPDX-License-Identifier: GPL-3.0-or-later
-- © 2020 Georgi Kirilov
local progname = ...

local lpeg = require("lpeg")
local R, C, Cmt = lpeg.R, lpeg.C, lpeg.Cmt

-- copied from vis.h
local VIS_MOVE_NOP = 64

local cont = R("\128\191")

local charpattern = R("\0\127") + R("\194\223") * cont + R("\224\239") * cont *
                        cont + R("\240\244") * cont * cont * cont

local function concat_keys(tbl)
    local keys = {}
    for k in pairs(tbl) do table.insert(keys, k) end
    return table.concat(keys, "\n"), #keys
end

local function line_complete()
    local file = vis.win.file
    local sel = vis.win.selection
    local cur_line = file.lines[sel.line]
    local indent_patt = "^[ \t\v\f]+"
    local prefix = cur_line:sub(1, sel.col - 1):gsub(indent_patt, "")
    local candidates = {}
    for l in file:lines_iterator() do
        local unindented = l:gsub(indent_patt, "")
        local start, finish = unindented:find(prefix, 1, true)
        if start == 1 and finish < #unindented then
            candidates[unindented] = true
        end
    end
    local candidates_str, n = concat_keys(candidates)
    if n < 2 then
        if n == 1 then vis:insert(candidates_str:sub(#prefix + 1)) end
        return
    end
    -- XXX: with too many candidates this command will become longer that the shell can handle:
    local command = string.format("vis-menu -l %d <<'EOF'\n%s\nEOF\n",
                                  math.min(n, math.ceil(vis.win.height / 2)),
                                  candidates_str)
    local status, output = vis:pipe(nil, nil, command)
    if n > 0 and status == 0 then
        vis:insert(output:sub(#prefix + 1):gsub("\n$", ""))
    end
end

local function selection_by_pos(pos)
    for s in vis.win:selections_iterator() do
        if s.pos == pos then return s end
    end
end

local function charwidth(cells_so_far, char)
    if char == "\t" then
        local tw = vis.tabwidth or 8
        local trail = cells_so_far % tw
        return tw - trail
    else
        return 1
    end
end

local function virtcol(line, col)
    local ncells = 0
    local nchars = 0
    local function upto(_, _, char)
        if nchars < col - 1 then
            ncells = ncells + charwidth(ncells, char)
            nchars = nchars + 1
            return true
        end
    end
    (Cmt(C(charpattern), upto) ^ 0):match(line)
    return ncells + 1
end

local function neighbor(lines, ln, col, direction)
    local line = ln + direction > 0 and lines[ln + direction]
    if not line then return end
    local column = virtcol(lines[ln], col)
    local ncells = 0
    local function upto(_, _, char)
        ncells = ncells + charwidth(ncells, char)
        return ncells < column
    end
    return
        (Cmt(C(charpattern), upto) ^ (-column + 1) / 0 * C(charpattern)):match(
            line)
end

local function dup_neighbor(direction)
    return function(file, _, pos)
        local sel = selection_by_pos(pos)
        local char = neighbor(file.lines, sel.line, sel.col, direction)
        if not char then return pos end
        file:insert(pos, char)
        return pos + #char
    end
end

local function dup_neighbor_feedkeys(direction)
    local sel = vis.win.selection
    local file = vis.win.file
    local char = neighbor(file.lines, sel.line, sel.col, direction)
    if not char then return end
    vis:feedkeys(char)
end

local function operator(handler)
    local id = vis:operator_register(handler)
    return id >= 0 and function()
        vis:operator(id)
        vis:motion(VIS_MOVE_NOP)
    end
end

vis.events.subscribe(vis.events.INIT, function()
    local function h(msg) return string.format("|@%s| %s", progname, msg) end

    local function column_complete(direction)
        local binding = operator(dup_neighbor(direction))
        return function()
            if #vis.win.selections == 1 then
                dup_neighbor_feedkeys(direction)
            else
                return binding()
            end
        end
    end

    vis:map(vis.modes.INSERT, "<C-y>", column_complete(-1),
            h "Insert the character which is above the cursor")
    vis:map(vis.modes.INSERT, "<C-e>", column_complete(1),
            h "Insert the character which is below the cursor")
    vis:map(vis.modes.INSERT, "<C-x><C-l>", line_complete,
            h "Complete the current line")
end)