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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
--- Marks functionality
--- - |:marks| command/completion
local api = vim.api
local util = require('vim._core.util')
local N_ = vim.fn.gettext
local M = {}
--- Names of the namespaces that have extmarks in the current buffer, sorted.
--- @return string[]
local function buf_namespaces()
local names = {} ---@type string[]
for name, id in pairs(api.nvim_get_namespaces()) do
if #api.nvim_buf_get_extmarks(0, id, 0, -1, { limit = 1 }) > 0 then
names[#names + 1] = name
end
end
table.sort(names)
return names
end
--- Completion for ":marks {arg}":
--- - set marks (buffer-local and global)
--- - extmark namespaces (buffer-local)
--- @return string[]
function M.complete()
local names = {} ---@type string[]
for _, list in ipairs({ vim.fn.getmarklist(api.nvim_get_current_buf()), vim.fn.getmarklist() }) do
for _, m in ipairs(list) do
names[#names + 1] = m.mark:sub(2) -- "'a" => "a"
end
end
table.sort(names)
return vim.list_extend(names, buf_namespaces())
end
--- ":marks {ns}": lists the extmarks of namespace {ns} in the current buffer.
--- @param ns string
--- @return boolean handled false: {ns} is not a namespace (it is a set of mark names)
function M.show(ns)
local id = api.nvim_get_namespaces()[ns]
if not id then
return false
end
local extmarks = api.nvim_buf_get_extmarks(0, id, 0, -1, {})
if #extmarks == 0 then
api.nvim_echo({ { ('No extmarks in this buffer for namespace "%s"'):format(ns) } }, false, {})
return true
end
--- @type [string, string?][]
local chunks = { { ('%6s %5s %4s %s'):format('id', 'line', 'col', 'text'), 'Title' } }
for _, m in ipairs(extmarks) do
local text = api.nvim_buf_get_lines(0, m[2], m[2] + 1, false)[1] or ''
chunks[#chunks + 1] = { ('\n%6d %5d %4d %s'):format(m[1], m[2] + 1, m[3], vim.trim(text)) }
end
api.nvim_echo(chunks, false, { kind = 'list_cmd' })
return true
end
--- The line at mark line {lnum}, sans leading whitespace, truncated to fit in the window
--- (cells), like C mark_line().
--- @param lnum integer
--- @return string
local function mark_line(lnum)
local line = lnum > 0 and api.nvim_buf_get_lines(0, lnum - 1, lnum, false)[1]
if not line then
return '-invalid-'
end
local text = line:gsub('^%s+', '')
local limit = vim.o.columns - 15
if vim.fn.strdisplaywidth(text) < limit then
return text
end
local keep = {} ---@type string[]
local width = 0
for _, c in ipairs(vim.fn.split(text, [[\zs]])) do
width = width + vim.fn.strdisplaywidth(c)
if width >= limit then
break
end
keep[#keep + 1] = c
end
return table.concat(keep)
end
--- ":marks [arg]" (|ex_cmds.lua| "marks" => C ex_marks() => this).
--- @param args vim._core.ExCmdArgs
function M.ex_marks(args)
local arg = args.args ~= '' and args.args or nil
-- ":marks {ns}": list namespace's extmarks, or fallthrough if unknown.
if arg and #arg > 1 and M.show(arg) then
return
end
local curbuf = api.nvim_get_current_buf()
local marks = {} ---@type table<string, vim.fn.getmarklist.ret.item>
for _, list in ipairs({ vim.fn.getmarklist(curbuf), vim.fn.getmarklist() }) do
for _, m in ipairs(list) do
marks[m.mark:sub(2)] = m
end
end
-- "<" and ">" are shown as where they will jump to: start before end.
local vs, ve = marks['<'], marks['>']
if vs and ve then
local s, e = vs.pos, ve.pos
if s[2] > e[2] or (s[2] == e[2] and s[3] > e[3]) then
vs.pos, ve.pos = e, s
end
elseif ve and not vs then
marks['<'], marks['>'] = ve, nil
end
--- The "file/text" column: the text at the mark if it is in the current buffer (highlighted
--- as "Directory", like C show_one_mark()), else the file name.
--- @param m vim.fn.getmarklist.ret.item
--- @return string text, string? hl
local function displayname(m)
if m.pos[1] == curbuf then
return mark_line(m.pos[2]), 'Directory'
elseif m.pos[1] == 0 then
return m.file or '', nil
end
return vim.fn.fnamemodify(vim.fn.bufname(m.pos[1]), ':~'), nil
end
-- Fixed :marks order.
local order = { "'" }
for _, range in ipairs({ { 'a', 'z' }, { 'A', 'Z' }, { '0', '9' } }) do
for b = range[1]:byte(), range[2]:byte() do
order[#order + 1] = string.char(b)
end
end
vim.list_extend(order, { '"', '[', ']', '^', '.', ':', '<', '>' })
local filtered = require('vim._core.ex_cmd').filter
local chunks = {} ---@type [string, string?][]
for _, name in ipairs(order) do
local m = marks[name]
if m and (not arg or arg:find(name, 1, true)) then
local text, hl = displayname(m)
if not filtered(args.smods.filter, text) then
chunks[#chunks + 1] = { ('\n %s %6d %4d '):format(name, m.pos[2], m.pos[3] - 1) }
if text ~= '' then
chunks[#chunks + 1] = { text, hl }
end
end
end
end
if #chunks == 0 then
if arg then
util.echo_err(N_('E283: No marks matching "%s"'):format(arg))
return
end
api.nvim_echo({ { N_('No marks set') } }, false, {})
else
table.insert(chunks, 1, { N_('\nmark line col file/text'), 'Title' })
api.nvim_echo(chunks, false, { kind = 'list_cmd' })
end
if not arg then
-- By default, list extmarks namespaces (but not their extmarks).
local names = buf_namespaces()
if #names > 0 then
local msg = ('Extmark namespaces (use ":marks {ns}"): %s'):format(table.concat(names, ', '))
api.nvim_echo({ { msg } }, false, {})
end
end
end
return M
|