aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/_core/util.lua
blob: 19a992c725c538122e40d35ea2a32e3b4f72d705 (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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
-- Nursery for random things that may later find their way into stdlib if they mature.

local M = {}

-- Generated from async.nvim/lua/async/_errors.lua: start
local nil_error = 'error(nil)'

--- Normalize a failed Lua operation for error slots where `nil` means success.
--- @param err any
--- @return any
--- @private
function M._normalize_error(err)
  return err == nil and nil_error or err
end

--- Convert an error to a string without letting its metamethod interrupt cleanup.
--- @param err any
--- @return string
--- @private
function M._stringify_error(err)
  local ok, message = pcall(tostring, err)
  return ok and message or '<unprintable error>'
end
-- Generated from async.nvim/lua/async/_errors.lua: end

--- Adds one or more blank lines above or below the cursor.
--- @param above? boolean Place blank line(s) above the cursor
local function add_blank(above)
  local offset = above and 1 or 0
  local repeated = vim.fn['repeat']({ '' }, vim.v.count1)
  local linenr = vim.api.nvim_win_get_cursor(0)[1]
  vim.api.nvim_buf_set_lines(0, linenr - offset, linenr - offset, true, repeated)
end

function M.space_above()
  add_blank(true)
end

function M.space_below()
  add_blank()
end

--- Gets a buffer by name
--- @param name string
--- @return integer?
function M.get_buf_by_name(name)
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    if vim.api.nvim_buf_get_name(buf) == name then
      return buf
    end
  end
end

--- Edit a file in a specific window
--- @param winnr integer
--- @param file string
--- @return number buffer number of the edited buffer
M.edit_in = function(winnr, file)
  --- @param path string?
  local function resolved_path(path)
    if not path or path == '' then
      return ''
    end
    return vim.fn.resolve(vim.fs.abspath(path))
  end

  return vim.api.nvim_win_call(winnr, function()
    local current_buf = vim.api.nvim_win_get_buf(winnr)
    local current = resolved_path(vim.api.nvim_buf_get_name(current_buf))

    -- Check if the current buffer is already the target file
    if current == resolved_path(file) then
      return current_buf
    end

    -- Read the file into the buffer
    vim.cmd.edit(vim.fn.fnameescape(file))
    return vim.api.nvim_get_current_buf()
  end)
end

--- :edit, but it respects commands like :hor, :vert, :tab, etc.
--- @param file string
--- @param mods string|vim.api.keyset.cmd_mods Modifier string ("vertical") or structured mods table.
function M.wrapped_edit(file, mods)
  if type(mods) == 'string' then
    mods = vim.api.nvim_parse_cmd(mods .. ' edit').mods
  end
  if (mods.tab or 0) > 0 or (mods.split or '') ~= '' or mods.horizontal or mods.vertical then
    local buf = M.get_buf_by_name(file)
    if buf == nil then
      buf = vim.api.nvim_create_buf(true, false)
    end
    vim.cmd.sbuffer { buf, mods = mods }
  end
  vim.cmd.edit { file }
end

--- Read a chunk of data from a file
--- @param file string
--- @param size integer
--- @return string? chunk or nil on error
function M.read_chunk(file, size)
  local fd = io.open(file, 'rb')
  if not fd then
    return nil
  end
  local chunk = fd:read(size)
  fd:close()
  return tostring(chunk)
end

--- Check if a range in a buffer is inside a Lua codeblock via treesitter injection.
--- Used by :source to detect Lua code in non-Lua files (e.g., vimdoc).
--- @param bufnr integer Buffer number
--- @param line1 integer Start line (1-indexed)
--- @param line2 integer End line (1-indexed)
--- @return boolean True if the range is in a Lua injection
function M.source_is_lua(bufnr, line1, line2)
  local parser = vim.treesitter.get_parser(bufnr)
  if not parser then
    return false
  end
  -- Parse from buffer start through one line past line2 to include injection closing markers
  local range = { line1 - 1, 0, line2 - 1, -1 }
  parser:parse({ 0, 0, line2, -1 })
  local lang_tree = parser:language_for_range(range)
  return lang_tree:lang() == 'lua'
end

--- Returns the exit code string for the current buffer, given:
--- - Channel is attached to the current buffer
--- - Current buffer is a terminal buffer
---
--- @return string
function M.term_exitcode()
  local chan_id = vim.bo.channel
  if chan_id == 0 or vim.bo.buftype ~= 'terminal' then
    return ''
  end

  local info = vim.api.nvim_get_chan_info(chan_id)
  if info.exitcode and info.exitcode >= 0 then
    -- use non-breaking space to avoid fillchar
    return string.format('[Exit:\226\128\175%d]', info.exitcode)
  end
  return ''
end

--- Compute a link to a target on a forge host
--- @param repo string URL of repo, usually "https://<domain>/<user>/<name>"
--- @param target string Identifier of a target, like commit hash or tag name
--- @param target_type "commit"|"tag"
--- @return string? # Example: <repo>/releases/tag/<target>
function M.get_forge_url(repo, target, target_type)
  -- The structure <host>/<middle>/<target> works for most forges. Like:
  -- - https://github.com/neovim/nvim-lspconfig/commit/e146efa
  -- - https://github.com/neovim/nvim-lspconfig/releases/tag/v2.8.0
  local ref_middles = {
    { pattern = '^https://github%.com/', commit = 'commit', tag = 'releases/tag' },
    { pattern = '^https://gitlab%.com/', commit = '-/commit', tag = '-/tags' },
    { pattern = '^https://git%.sr%.ht/', commit = 'commit', tag = 'refs' },
    { pattern = '^https://tangled%.org/', commit = 'commit', tag = 'tags' },
    { pattern = '^https://bitbucket%.org/', commit = 'commits', tag = 'src' },

    -- Fall back to Forgejo style since there is no fixed host
    { pattern = '^https://', commit = 'commit', tag = 'src/tag' },
  }

  local middle = ''
  for _, mid in ipairs(ref_middles) do
    if repo:match(mid.pattern) then
      middle = mid[target_type]
      if middle ~= '' then
        break
      end
    end
  end

  if middle == '' then
    return nil
  end
  repo = repo:gsub('/+$', '')
  return ('%s/%s/%s'):format(repo, middle, target)
end

--- Gets a scrubbed message from a pcall'd command error (drops Lua context/traceback):
--- "…/editor.lua:123: Vim(put):E484: xx" => "E484: xx"
---
--- @param err string
--- @return string
function M.cmd_errmsg(err)
  err = err:match('^[^\n]*') or err
  --- @type string
  err = err:match('Vim%b():%s*(.*)') or err:match('Vim:%s*(.*)') or (err:gsub('^.-:%d+:%s*', ''))
  return (err:gsub('^Lua:%s*', ''))
end

--- Display a Vim error code (EXX), or raise an error without editor APIs.
--- @param msg string
function M.echo_err(msg)
  if not vim.api then
    error(msg, 2)
  end
  vim.api.nvim_echo({ { msg } }, true, { err = true })
end

--- Shows a message from a builtin plugin, prefixed with the plugin name.
---
--- Scheduled, so it is safe to call from |api-fast| contexts.
--- @param name string Plugin name, e.g. "zip".
--- @param msg string
--- @param level? integer Level from |vim.log.levels|. Defaults to ERROR.
--- @param once? boolean Only show the message once.
function M.notify(name, msg, level, once)
  vim.schedule(function()
    local notify = once and vim.notify_once or vim.notify
    notify(('%s: %s'):format(name, msg), level or vim.log.levels.ERROR)
  end)
end

--- Define event-handlers (autocmds) ergonomically.
---
--- Examples:
--- ```lua
---   local nvim_on = require('vim._core.util').nvim_on
---   nvim_on('BufWritePost', group, function(ev) print(ev.file) end)
---   nvim_on({ 'BufRead', 'BufNew' }, group, { pattern = '*.lua' }, function(ev) end)
---   nvim_on('VimLeavePre', nil, function() end)
--- ```
---
--- @param events vim.api.keyset.events|vim.api.keyset.events[] Event(s) to watch. See |autocmd-events|.
--- @param group string|integer? Group name or id, or `nil`.
--- @param opts_or_fn vim.api.keyset.create_autocmd Options.
--- @param fn fun(ev: vim.api.keyset.create_autocmd.callback_args): boolean? Event handler.
--- @return integer # Autocmd id (see |nvim_create_autocmd()|).
--- @overload fun(events: vim.api.keyset.events|vim.api.keyset.events[], group: string|integer?, fn: fun(ev: vim.api.keyset.create_autocmd.callback_args): boolean?): integer
function M.nvim_on(events, group, opts_or_fn, fn)
  vim.validate('opts_or_fn', opts_or_fn, { 'function', 'table' })
  local opts --- @type vim.api.keyset.create_autocmd
  if type(opts_or_fn) == 'function' then
    fn, opts = opts_or_fn, {}
  else
    opts = opts_or_fn
  end
  opts.group = group
  opts.callback = fn
  return vim.api.nvim_create_autocmd(events, opts)
end

return M