aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/_core/marks.lua
blob: 0d34dc6b4c164ed9c0070622d4bcf12702c41773 (plain) (blame)
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