aboutsummaryrefslogtreecommitdiff
path: root/src/nvim/lua/vim.lua
blob: ed435439a4c6900a829b145a78ce5b02444ea90a (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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
-- Nvim-Lua stdlib: the `vim` module (:help lua-stdlib)
--
-- Lua code lives in one of three places:
--    1. runtime/lua/vim/ (the runtime): For "nice to have" features, e.g. the
--       `inspect` and `lpeg` modules.
--    2. runtime/lua/vim/shared.lua: Code shared between Nvim and tests.
--    3. src/nvim/lua/: Compiled-into Nvim itself.
--
-- Guideline: "If in doubt, put it in the runtime".
--
-- Most functions should live directly in `vim.`, not in submodules.
-- The only "forbidden" names are those claimed by legacy `if_lua`:
--    $ vim
--    :lua for k,v in pairs(vim) do print(k) end
--    buffer
--    open
--    window
--    lastline
--    firstline
--    type
--    line
--    eval
--    dict
--    beep
--    list
--    command
--
-- Reference (#6580):
--    - https://github.com/luafun/luafun
--    - https://github.com/rxi/lume
--    - http://leafo.net/lapis/reference/utilities.html
--    - https://github.com/torch/paths
--    - https://github.com/bakpakin/Fennel (pretty print, repl)
--    - https://github.com/howl-editor/howl/tree/master/lib/howl/util

local vim = vim
assert(vim)

vim.inspect = package.loaded['vim.inspect']
assert(vim.inspect)

local pathtrails = {}
vim._so_trails = {}
for s in  (package.cpath..';'):gmatch('[^;]*;') do
    s = s:sub(1, -2)  -- Strip trailing semicolon
  -- Find out path patterns. pathtrail should contain something like
  -- /?.so, \?.dll. This allows not to bother determining what correct
  -- suffixes are.
  local pathtrail = s:match('[/\\][^/\\]*%?.*$')
  if pathtrail and not pathtrails[pathtrail] then
    pathtrails[pathtrail] = true
    table.insert(vim._so_trails, pathtrail)
  end
end

function vim._load_package(name)
  local basename = name:gsub('%.', '/')
  local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"}
  for _,path in ipairs(paths) do
    local found = vim.api.nvim_get_runtime_file(path, false)
    if #found > 0 then
      local f, err = loadfile(found[1])
      return f or error(err)
    end
  end

  for _,trail in ipairs(vim._so_trails) do
    local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash
    local found = vim.api.nvim_get_runtime_file(path, false)
    if #found > 0 then
      -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
      -- a) strip prefix up to and including the first dash, if any
      -- b) replace all dots by underscores
      -- c) prepend "luaopen_"
      -- So "foo-bar.baz" should result in "luaopen_bar_baz"
      local dash = name:find("-", 1, true)
      local modname = dash and name:sub(dash + 1) or name
      local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_"))
      return f or error(err)
    end
  end
  return nil
end

table.insert(package.loaders, 1, vim._load_package)

-- These are for loading runtime modules lazily since they aren't available in
-- the nvim binary as specified in executor.c
setmetatable(vim, {
  __index = function(t, key)
    if key == 'treesitter' then
      t.treesitter = require('vim.treesitter')
      return t.treesitter
    elseif key == 'F' then
      t.F = require('vim.F')
      return t.F
    elseif require('vim.uri')[key] ~= nil then
      -- Expose all `vim.uri` functions on the `vim` module.
      t[key] = require('vim.uri')[key]
      return t[key]
    elseif key == 'lsp' then
      t.lsp = require('vim.lsp')
      return t.lsp
    elseif key == 'highlight' then
      t.highlight = require('vim.highlight')
      return t.highlight
    end
  end
})

vim.log = {
  levels = {
    TRACE = 0;
    DEBUG = 1;
    INFO  = 2;
    WARN  = 3;
    ERROR = 4;
  }
}

-- Internal-only until comments in #8107 are addressed.
-- Returns:
--    {errcode}, {output}
function vim._system(cmd)
  local out = vim.fn.system(cmd)
  local err = vim.v.shell_error
  return err, out
end

-- Gets process info from the `ps` command.
-- Used by nvim_get_proc() as a fallback.
function vim._os_proc_info(pid)
  if pid == nil or pid <= 0 or type(pid) ~= 'number' then
    error('invalid pid')
  end
  local cmd = { 'ps', '-p', pid, '-o', 'comm=', }
  local err, name = vim._system(cmd)
  if 1 == err and vim.trim(name) == '' then
    return {}  -- Process not found.
  elseif 0 ~= err then
    error('command failed: '..vim.fn.string(cmd))
  end
  local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=', })
  -- Remove trailing whitespace.
  name = vim.trim(name):gsub('^.*/', '')
  ppid = tonumber(ppid) or -1
  return {
    name = name,
    pid = pid,
    ppid = ppid,
  }
end

-- Gets process children from the `pgrep` command.
-- Used by nvim_get_proc_children() as a fallback.
function vim._os_proc_children(ppid)
  if ppid == nil or ppid <= 0 or type(ppid) ~= 'number' then
    error('invalid ppid')
  end
  local cmd = { 'pgrep', '-P', ppid, }
  local err, rv = vim._system(cmd)
  if 1 == err and vim.trim(rv) == '' then
    return {}  -- Process not found.
  elseif 0 ~= err then
    error('command failed: '..vim.fn.string(cmd))
  end
  local children = {}
  for s in rv:gmatch('%S+') do
    local i = tonumber(s)
    if i ~= nil then
      table.insert(children, i)
    end
  end
  return children
end

-- TODO(ZyX-I): Create compatibility layer.

--- Return a human-readable representation of the given object.
---
---@see https://github.com/kikito/inspect.lua
---@see https://github.com/mpeterv/vinspect
local function inspect(object, options)  -- luacheck: no unused
  error(object, options)  -- Stub for gen_vimdoc.py
end

do
  local tdots, tick, got_line1 = 0, 0, false

  --- Paste handler, invoked by |nvim_paste()| when a conforming UI
  --- (such as the |TUI|) pastes text into the editor.
  ---
  --- Example: To remove ANSI color codes when pasting:
  --- <pre>
  --- vim.paste = (function(overridden)
  ---   return function(lines, phase)
  ---     for i,line in ipairs(lines) do
  ---       -- Scrub ANSI color codes from paste input.
  ---       lines[i] = line:gsub('\27%[[0-9;mK]+', '')
  ---     end
  ---     overridden(lines, phase)
  ---   end
  --- end)(vim.paste)
  --- </pre>
  ---
  ---@see |paste|
  ---
  ---@param lines  |readfile()|-style list of lines to paste. |channel-lines|
  ---@param phase  -1: "non-streaming" paste: the call contains all lines.
  ---              If paste is "streamed", `phase` indicates the stream state:
  ---                - 1: starts the paste (exactly once)
  ---                - 2: continues the paste (zero or more times)
  ---                - 3: ends the paste (exactly once)
  ---@returns false if client should cancel the paste.
  function vim.paste(lines, phase)
    local call = vim.api.nvim_call_function
    local now = vim.loop.now()
    local mode = call('mode', {}):sub(1,1)
    if phase < 2 then  -- Reset flags.
      tdots, tick, got_line1 = now, 0, false
    elseif mode ~= 'c' then
      vim.api.nvim_command('undojoin')
    end
    if mode == 'c' and not got_line1 then  -- cmdline-mode: paste only 1 line.
      got_line1 = (#lines > 1)
      vim.api.nvim_set_option('paste', true)  -- For nvim_input().
      local line1 = lines[1]:gsub('<', '<lt>'):gsub('[\r\n\012\027]', ' ')  -- Scrub.
      vim.api.nvim_input(line1)
      vim.api.nvim_set_option('paste', false)
    elseif mode ~= 'c' then
      if phase < 2 and mode:find('^[vV\22sS\19]') then
        vim.api.nvim_command([[exe "normal! \<Del>"]])
        vim.api.nvim_put(lines, 'c', false, true)
      elseif phase < 2 and not mode:find('^[iRt]') then
        vim.api.nvim_put(lines, 'c', true, true)
        -- XXX: Normal-mode: workaround bad cursor-placement after first chunk.
        vim.api.nvim_command('normal! a')
      elseif phase < 2 and mode == 'R' then
        local nchars = 0
        for _, line in ipairs(lines) do
            nchars = nchars + line:len()
        end
        local row, col = unpack(vim.api.nvim_win_get_cursor(0))
        local bufline = vim.api.nvim_buf_get_lines(0, row-1, row, true)[1]
        local firstline = lines[1]
        firstline = bufline:sub(1, col)..firstline
        lines[1] = firstline
        lines[#lines] = lines[#lines]..bufline:sub(col + nchars + 1, bufline:len())
        vim.api.nvim_buf_set_lines(0, row-1, row, false, lines)
      else
        vim.api.nvim_put(lines, 'c', false, true)
      end
    end
    if phase ~= -1 and (now - tdots >= 100) then
      local dots = ('.'):rep(tick % 4)
      tdots = now
      tick = tick + 1
      -- Use :echo because Lua print('') is a no-op, and we want to clear the
      -- message when there are zero dots.
      vim.api.nvim_command(('echo "%s"'):format(dots))
    end
    if phase == -1 or phase == 3 then
      vim.api.nvim_command('redraw'..(tick > 1 and '|echo ""' or ''))
    end
    return true  -- Paste will not continue if not returning `true`.
  end
end

--- Defers callback `cb` until the Nvim API is safe to call.
---
---@see |lua-loop-callbacks|
---@see |vim.schedule()|
---@see |vim.in_fast_event()|
function vim.schedule_wrap(cb)
  return (function (...)
    local args = {...}
    vim.schedule(function() cb(unpack(args)) end)
  end)
end

--- <Docs described in |vim.empty_dict()| >
---@private
function vim.empty_dict()
  return setmetatable({}, vim._empty_dict_mt)
end

-- vim.fn.{func}(...)
vim.fn = setmetatable({}, {
  __index = function(t, key)
    local _fn
    if vim.api[key] ~= nil then
      _fn = function()
        error(string.format("Tried to call API function with vim.fn: use vim.api.%s instead", key))
      end
    else
      _fn = function(...)
        return vim.call(key, ...)
      end
    end
    t[key] = _fn
    return _fn
  end
})

vim.funcref = function(viml_func_name)
  return vim.fn[viml_func_name]
end

-- An easier alias for commands.
vim.cmd = function(command)
  return vim.api.nvim_exec(command, false)
end

-- These are the vim.env/v/g/o/bo/wo variable magic accessors.
do
  local validate = vim.validate

  local function make_dict_accessor(scope)
    validate {
      scope = {scope, 's'};
    }
    local mt = {}
    function mt:__newindex(k, v)
      return vim._setvar(scope, 0, k, v)
    end
    function mt:__index(k)
      return vim._getvar(scope, 0, k)
    end
    return setmetatable({}, mt)
  end

  vim.g = make_dict_accessor('g')
  vim.v = make_dict_accessor('v')
  vim.b = make_dict_accessor('b')
  vim.w = make_dict_accessor('w')
  vim.t = make_dict_accessor('t')
end

--- Get a table of lines with start, end columns for a region marked by two points
---
---@param bufnr number of buffer
---@param pos1 (line, column) tuple marking beginning of region
---@param pos2 (line, column) tuple marking end of region
---@param regtype type of selection (:help setreg)
---@param inclusive boolean indicating whether the selection is end-inclusive
---@return region lua table of the form {linenr = {startcol,endcol}}
function vim.region(bufnr, pos1, pos2, regtype, inclusive)
  if not vim.api.nvim_buf_is_loaded(bufnr) then
    vim.fn.bufload(bufnr)
  end

  -- check that region falls within current buffer
  local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
  pos1[1] = math.min(pos1[1], buf_line_count - 1)
  pos2[1] = math.min(pos2[1], buf_line_count - 1)

  -- in case of block selection, columns need to be adjusted for non-ASCII characters
  -- TODO: handle double-width characters
  local bufline
  if regtype:byte() == 22 then
    bufline = vim.api.nvim_buf_get_lines(bufnr, pos1[1], pos1[1] + 1, true)[1]
    pos1[2] = vim.str_utfindex(bufline, pos1[2])
  end

  local region = {}
  for l = pos1[1], pos2[1] do
    local c1, c2
    if regtype:byte() == 22 then  -- block selection: take width from regtype
      c1 = pos1[2]
      c2 = c1 + regtype:sub(2)
      -- and adjust for non-ASCII characters
      bufline = vim.api.nvim_buf_get_lines(bufnr, l, l + 1, true)[1]
      if c1 < #bufline then
        c1 = vim.str_byteindex(bufline, c1)
      end
      if c2 < #bufline then
        c2 = vim.str_byteindex(bufline, c2)
      end
    else
      c1 = (l == pos1[1]) and (pos1[2]) or 0
      c2 = (l == pos2[1]) and (pos2[2] + (inclusive and 1 or 0)) or -1
    end
    table.insert(region, l, {c1, c2})
  end
  return region
end

--- Defers calling `fn` until `timeout` ms passes.
---
--- Use to do a one-shot timer that calls `fn`
--- Note: The {fn} is |schedule_wrap|ped automatically, so API functions are
--- safe to call.
---@param fn Callback to call once `timeout` expires
---@param timeout Number of milliseconds to wait before calling `fn`
---@return timer luv timer object
function vim.defer_fn(fn, timeout)
  vim.validate { fn = { fn, 'c', true}; }
  local timer = vim.loop.new_timer()
  timer:start(timeout, 0, vim.schedule_wrap(function()
    timer:stop()
    timer:close()

    fn()
  end))

  return timer
end


--- Notification provider
---
--- Without a runtime, writes to :Messages
---@see :help nvim_notify
---@param msg Content of the notification to show to the user
---@param log_level Optional log level
---@param opts Dictionary with optional options (timeout, etc)
function vim.notify(msg, log_level, _opts)

  if log_level == vim.log.levels.ERROR then
    vim.api.nvim_err_writeln(msg)
  elseif log_level == vim.log.levels.WARN then
    vim.api.nvim_echo({{msg, 'WarningMsg'}}, true, {})
  else
    vim.api.nvim_echo({{msg}}, true, {})
  end
end


local on_keystroke_callbacks = {}

--- Register a lua {fn} with an {id} to be run after every keystroke.
---
---@param fn function: Function to call. It should take one argument, which is a string.
---                   The string will contain the literal keys typed.
---                   See |i_CTRL-V|
---
---                   If {fn} is nil, it removes the callback for the associated {ns_id}
---@param ns_id number? Namespace ID. If not passed or 0, will generate and return a new
---                    namespace ID from |nvim_create_namesapce()|
---
---@return number Namespace ID associated with {fn}
---
---@note {fn} will be automatically removed if an error occurs while calling.
---     This is to prevent the annoying situation of every keystroke erroring
---     while trying to remove a broken callback.
---@note {fn} will not be cleared from |nvim_buf_clear_namespace()|
---@note {fn} will receive the keystrokes after mappings have been evaluated
function vim.register_keystroke_callback(fn, ns_id)
  vim.validate {
    fn = { fn, 'c', true},
    ns_id = { ns_id, 'n', true }
  }

  if ns_id == nil or ns_id == 0 then
    ns_id = vim.api.nvim_create_namespace('')
  end

  on_keystroke_callbacks[ns_id] = fn
  return ns_id
end

--- Function that executes the keystroke callbacks.
---@private
function vim._log_keystroke(char)
  local failed_ns_ids = {}
  local failed_messages = {}
  for k, v in pairs(on_keystroke_callbacks) do
    local ok, err_msg = pcall(v, char)
    if not ok then
      vim.register_keystroke_callback(nil, k)

      table.insert(failed_ns_ids, k)
      table.insert(failed_messages, err_msg)
    end
  end

  if failed_ns_ids[1] then
    error(string.format(
      "Error executing 'on_keystroke' with ns_ids of '%s'\n    With messages: %s",
      table.concat(failed_ns_ids, ", "),
      table.concat(failed_messages, "\n")))
  end
end

--- Generate a list of possible completions for the string.
--- String starts with ^ and then has the pattern.
---
---     1. Can we get it to just return things in the global namespace with that name prefix
---     2. Can we get it to return things from global namespace even with `print(` in front.
function vim._expand_pat(pat, env)
  env = env or _G

  pat = string.sub(pat, 2, #pat)

  if pat == '' then
    local result = vim.tbl_keys(env)
    table.sort(result)
    return result, 0
  end

  -- TODO: We can handle spaces in [] ONLY.
  --    We should probably do that at some point, just for cooler completion.
  -- TODO: We can suggest the variable names to go in []
  --    This would be difficult as well.
  --    Probably just need to do a smarter match than just `:match`

  -- Get the last part of the pattern
  local last_part = pat:match("[%w.:_%[%]'\"]+$")
  if not last_part then return {}, 0 end

  local parts, search_index = vim._expand_pat_get_parts(last_part)

  local match_part = string.sub(last_part, search_index, #last_part)
  local prefix_match_pat = string.sub(pat, 1, #pat - #match_part) or ''

  local final_env = env

  for _, part in ipairs(parts) do
    if type(final_env) ~= 'table' then
      return {}, 0
    end
    local key

    -- Normally, we just have a string
    -- Just attempt to get the string directly from the environment
    if type(part) == "string" then
      key = part
    else
      -- However, sometimes you want to use a variable, and complete on it
      --    With this, you have the power.

      -- MY_VAR = "api"
      -- vim[MY_VAR]
      -- -> _G[MY_VAR] -> "api"
      local result_key = part[1]
      if not result_key then
        return {}, 0
      end

      local result = rawget(env, result_key)

      if result == nil then
        return {}, 0
      end

      key = result
    end
    local field = rawget(final_env, key)
    if field == nil then
      local mt = getmetatable(final_env)
      if mt and type(mt.__index) == "table" then
        field = rawget(mt.__index, key)
      end
    end
    final_env = field

    if not final_env then
      return {}, 0
    end
  end

  local keys = {}
  local function insert_keys(obj)
    for k,_ in pairs(obj) do
      if type(k) == "string" and string.sub(k,1,string.len(match_part)) == match_part then
        table.insert(keys,k)
      end
    end
  end

  if type(final_env) == "table" then
    insert_keys(final_env)
  end
  local mt = getmetatable(final_env)
  if mt and type(mt.__index) == "table" then
    insert_keys(mt.__index)
  end

  table.sort(keys)

  return keys, #prefix_match_pat
end

vim._expand_pat_get_parts = function(lua_string)
  local parts = {}

  local accumulator, search_index = '', 1
  local in_brackets, bracket_end = false, -1
  local string_char = nil
  for idx = 1, #lua_string do
    local s = lua_string:sub(idx, idx)

    if not in_brackets and (s == "." or s == ":") then
      table.insert(parts, accumulator)
      accumulator = ''

      search_index = idx + 1
    elseif s == "[" then
      in_brackets = true

      table.insert(parts, accumulator)
      accumulator = ''

      search_index = idx + 1
    elseif in_brackets then
      if idx == bracket_end then
        in_brackets = false
        search_index = idx + 1

        if string_char == "VAR" then
          table.insert(parts, { accumulator })
          accumulator = ''

          string_char = nil
        end
      elseif not string_char then
        bracket_end = string.find(lua_string, ']', idx, true)

        if s == '"' or s == "'" then
          string_char = s
        elseif s ~= ' ' then
          string_char = "VAR"
          accumulator = s
        end
      elseif string_char then
        if string_char ~= s then
          accumulator = accumulator .. s
        else
          table.insert(parts, accumulator)
          accumulator = ''

          string_char = nil
        end
      end
    else
      accumulator = accumulator .. s
    end
  end

  parts = vim.tbl_filter(function(val) return #val > 0 end, parts)

  return parts, search_index
end

pcall(require, 'vim._meta')

return module