Regenerate nvim config
This commit is contained in:
21
config/neovim/store/lazy-plugins/cmp-buffer/LICENSE
Normal file
21
config/neovim/store/lazy-plugins/cmp-buffer/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 hrsh7th
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
226
config/neovim/store/lazy-plugins/cmp-buffer/README.md
Normal file
226
config/neovim/store/lazy-plugins/cmp-buffer/README.md
Normal file
@ -0,0 +1,226 @@
|
||||
# cmp-buffer
|
||||
|
||||
nvim-cmp source for buffer words.
|
||||
|
||||
## Setup
|
||||
|
||||
```lua
|
||||
require('cmp').setup({
|
||||
sources = {
|
||||
{ name = 'buffer' },
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The below source configuration are available. To set any of these options, do:
|
||||
|
||||
```lua
|
||||
cmp.setup({
|
||||
sources = {
|
||||
{
|
||||
name = 'buffer',
|
||||
option = {
|
||||
-- Options go into this table
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### keyword_length (type: number)
|
||||
|
||||
_Default:_ `3`
|
||||
|
||||
The number of characters that need to be typed to trigger auto-completion.
|
||||
|
||||
|
||||
### keyword_pattern (type: string)
|
||||
|
||||
_Default:_ `[[\%(-\?\d\+\%(\.\d\+\)\?\|\h\w*\%([\-.]\w*\)*\)]]`
|
||||
|
||||
A vim's regular expression for creating a word list from buffer content.
|
||||
|
||||
You can set this to `[[\k\+]]` if you want to use the `iskeyword` option for recognizing words.
|
||||
Lua's `[[ ]]` string literals are particularly useful here to avoid escaping all of the backslash
|
||||
(`\`) characters used for writing regular expressions.
|
||||
|
||||
**NOTE:** Be careful with where you set this option! You must do this:
|
||||
|
||||
```lua
|
||||
cmp.setup({
|
||||
sources = {
|
||||
{
|
||||
name = 'buffer',
|
||||
-- Correct:
|
||||
option = {
|
||||
keyword_pattern = [[\k\+]],
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Instead of this:
|
||||
|
||||
```lua
|
||||
cmp.setup({
|
||||
sources = {
|
||||
{
|
||||
name = 'buffer',
|
||||
-- Wrong:
|
||||
keyword_pattern = [[\k\+]],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
The second notation is allowed by nvim-cmp (documented [here](https://github.com/hrsh7th/nvim-cmp#sourcesnumberkeyword_pattern-type-string)), but it is meant for a different purpose and will not be detected by this plugin as the pattern for searching words.
|
||||
|
||||
|
||||
### get_bufnrs (type: fun(): number[])
|
||||
|
||||
_Default:_ `function() return { vim.api.nvim_get_current_buf() } end`
|
||||
|
||||
A function that specifies the buffer numbers to complete.
|
||||
|
||||
You can use the following pre-defined recipes.
|
||||
|
||||
##### All buffers
|
||||
|
||||
```lua
|
||||
cmp.setup {
|
||||
sources = {
|
||||
{
|
||||
name = 'buffer',
|
||||
option = {
|
||||
get_bufnrs = function()
|
||||
return vim.api.nvim_list_bufs()
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Visible buffers
|
||||
|
||||
```lua
|
||||
cmp.setup {
|
||||
sources = {
|
||||
{
|
||||
name = 'buffer',
|
||||
option = {
|
||||
get_bufnrs = function()
|
||||
local bufs = {}
|
||||
for _, win in ipairs(vim.api.nvim_list_wins()) do
|
||||
bufs[vim.api.nvim_win_get_buf(win)] = true
|
||||
end
|
||||
return vim.tbl_keys(bufs)
|
||||
end
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
### indexing_interval (type: number)
|
||||
|
||||
_Default:_ `100`
|
||||
|
||||
Optimization option. See the section [Indexing](#indexing-and-how-to-optimize-it).
|
||||
|
||||
|
||||
### indexing_batch_size (type: number)
|
||||
|
||||
_Default:_ `1000`
|
||||
|
||||
Optimization option. See the section [Indexing](#indexing-and-how-to-optimize-it).
|
||||
|
||||
|
||||
### max_indexed_line_length (type: number)
|
||||
|
||||
_Default:_ `1024 * 40` (40 Kilobytes)
|
||||
|
||||
Optimization option. See the section [Indexing](#indexing-and-how-to-optimize-it).
|
||||
|
||||
|
||||
## Locality bonus comparator (distance-based sorting)
|
||||
|
||||
This source also provides a comparator function which uses information from the word indexer
|
||||
to sort completion results based on the distance of the word from the cursor line. It will also
|
||||
sort completion results coming from other sources, such as Language Servers, which might improve
|
||||
accuracy of their suggestions too. The usage is as follows:
|
||||
|
||||
```lua
|
||||
local cmp = require('cmp')
|
||||
local cmp_buffer = require('cmp_buffer')
|
||||
|
||||
cmp.setup({
|
||||
sources = {
|
||||
{ name = 'buffer' },
|
||||
-- The rest of your sources...
|
||||
},
|
||||
sorting = {
|
||||
comparators = {
|
||||
function(...) return cmp_buffer:compare_locality(...) end,
|
||||
-- The rest of your comparators...
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Indexing and how to optimize it
|
||||
|
||||
When a buffer is opened, this source first has to scan all lines in the buffer, match all words
|
||||
and store all of their occurrences. This process is called _indexing_. When actually editing the
|
||||
text in the buffer, the index of words is kept up-to-date with changes to the buffer's contents,
|
||||
this is called _watching_. It is done by re-running the indexer on just the changed lines.
|
||||
Indexing happens completely asynchronously in background, unlike watching, which must be performed
|
||||
synchronously to ensure that the index of words is kept perfectly in-sync with the lines in the
|
||||
buffer. However, most of the time this will not be a problem since many typical text edit
|
||||
operations affect only one or two lines, unless you are pasting a 1000-line snippet.
|
||||
|
||||
_Note that you can freely edit the buffer while it is being indexed_, the underlying algorithm is
|
||||
written in such a way that your changes will not break the index or cause errors. If a crash does
|
||||
happen - it is a bug, so please report it.
|
||||
|
||||
The speed of indexing is configurable with two options: `indexing_interval` and
|
||||
`indexing_batch_size`. Essentially, when indexing, a timer is started, which pulls a batch of
|
||||
`indexing_batch_size` lines from the buffer, scans them for words, and repeats after
|
||||
`indexing_interval` milliseconds. Decreasing interval and/or increasing the batch size will make
|
||||
the indexer faster, but at the expense of higher CPU usage and more lag when editing the file
|
||||
while indexing is still in progress. Setting `indexing_batch_size` to a negative value will switch
|
||||
the indexer to the "synchronous" mode: this will process all lines in one go, take less time in
|
||||
total (since no other code will be running on the Lua thread), but with the obvious downside that
|
||||
the editor UI will be blocked.
|
||||
|
||||
The option `max_indexed_line_length` controls plugin's behavior in files with very long lines.
|
||||
This is known to slow this source down significantly (see issue [#13](https://github.com/hrsh7th/cmp-buffer/issues/13)),
|
||||
so by default it will take only the first few kilobytes of the line it is currently on. In other
|
||||
words, very long lines are not ignored, but only a part of them is indexed.
|
||||
|
||||
### Performance on large text files
|
||||
|
||||
This source has been tested on code files of a few megabytes in size (5-10) and contains
|
||||
optimizations for them, however, the indexed words can still take up tens of megabytes of RAM if
|
||||
the file is large. So, if you wish to avoid accidentally running this source on big files, you
|
||||
can tweak `get_bufnrs`, for example like this:
|
||||
|
||||
```lua
|
||||
get_bufnrs = function()
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
local byte_size = vim.api.nvim_buf_get_offset(buf, vim.api.nvim_buf_line_count(buf))
|
||||
if byte_size > 1024 * 1024 then -- 1 Megabyte max
|
||||
return {}
|
||||
end
|
||||
return { buf }
|
||||
end
|
||||
```
|
||||
|
||||
Of course, this snippet can be combined with any other recipes for `get_bufnrs`.
|
||||
@ -0,0 +1 @@
|
||||
require('cmp').register_source('buffer', require('cmp_buffer'))
|
||||
@ -0,0 +1,380 @@
|
||||
local timer = require('cmp_buffer.timer')
|
||||
|
||||
local function clear_table(tbl)
|
||||
for k in pairs(tbl) do
|
||||
tbl[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@class cmp_buffer.Buffer
|
||||
---@field public bufnr number
|
||||
---@field public opts cmp_buffer.Options
|
||||
---@field public regex any
|
||||
---@field public timer cmp_buffer.Timer
|
||||
---@field public lines_count number
|
||||
---@field public timer_current_line number
|
||||
---@field public lines_words table<number, false|string[]>
|
||||
---@field public unique_words_curr_line table<string, boolean>
|
||||
---@field public unique_words_other_lines table<string, boolean>
|
||||
---@field public unique_words_curr_line_dirty boolean
|
||||
---@field public unique_words_other_lines_dirty boolean
|
||||
---@field public last_edit_first_line number
|
||||
---@field public last_edit_last_line number
|
||||
---@field public closed boolean
|
||||
---@field public on_close_cb fun()|nil
|
||||
---@field public words_distances table<string, number>
|
||||
---@field public words_distances_last_cursor_row number
|
||||
---@field public words_distances_dirty boolean
|
||||
local buffer = {}
|
||||
|
||||
-- For some reason requesting this much lines multiple times in chunks leads to
|
||||
-- much better memory usage than fetching the entire file in one go.
|
||||
buffer.GET_LINES_CHUNK_SIZE = 1000
|
||||
|
||||
---Create new buffer object
|
||||
---@param bufnr number
|
||||
---@param opts cmp_buffer.Options
|
||||
---@return cmp_buffer.Buffer
|
||||
function buffer.new(bufnr, opts)
|
||||
local self = setmetatable({}, { __index = buffer })
|
||||
|
||||
self.bufnr = bufnr
|
||||
self.timer = timer.new()
|
||||
self.closed = false
|
||||
self.on_close_cb = nil
|
||||
|
||||
self.opts = opts
|
||||
self.regex = vim.regex(self.opts.keyword_pattern)
|
||||
|
||||
self.lines_count = 0
|
||||
self.timer_current_line = -1
|
||||
self.lines_words = {}
|
||||
|
||||
self.unique_words_curr_line = {}
|
||||
self.unique_words_other_lines = {}
|
||||
self.unique_words_curr_line_dirty = true
|
||||
self.unique_words_other_lines_dirty = true
|
||||
self.last_edit_first_line = 0
|
||||
self.last_edit_last_line = 0
|
||||
|
||||
self.words_distances = {}
|
||||
self.words_distances_dirty = true
|
||||
self.words_distances_last_cursor_row = 0
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---Close buffer
|
||||
function buffer.close(self)
|
||||
self.closed = true
|
||||
self:stop_indexing_timer()
|
||||
self.timer:close()
|
||||
self.timer = nil
|
||||
|
||||
self.lines_count = 0
|
||||
self.timer_current_line = -1
|
||||
self.lines_words = {}
|
||||
|
||||
self.unique_words_curr_line = {}
|
||||
self.unique_words_other_lines = {}
|
||||
self.unique_words_curr_line_dirty = false
|
||||
self.unique_words_other_lines_dirty = false
|
||||
self.last_edit_first_line = 0
|
||||
self.last_edit_last_line = 0
|
||||
|
||||
self.words_distances = {}
|
||||
self.words_distances_dirty = false
|
||||
self.words_distances_last_cursor_row = 0
|
||||
|
||||
if self.on_close_cb then
|
||||
self.on_close_cb()
|
||||
end
|
||||
end
|
||||
|
||||
function buffer.stop_indexing_timer(self)
|
||||
self.timer:stop()
|
||||
self.timer_current_line = -1
|
||||
end
|
||||
|
||||
function buffer.mark_all_lines_dirty(self)
|
||||
self.unique_words_curr_line_dirty = true
|
||||
self.unique_words_other_lines_dirty = true
|
||||
self.last_edit_first_line = 0
|
||||
self.last_edit_last_line = 0
|
||||
self.words_distances_dirty = true
|
||||
end
|
||||
|
||||
--- Workaround for https://github.com/neovim/neovim/issues/16729
|
||||
function buffer.safe_buf_call(self, callback)
|
||||
if vim.api.nvim_get_current_buf() == self.bufnr then
|
||||
callback()
|
||||
else
|
||||
vim.api.nvim_buf_call(self.bufnr, callback)
|
||||
end
|
||||
end
|
||||
|
||||
function buffer.index_range(self, range_start, range_end, skip_already_indexed)
|
||||
self:safe_buf_call(function()
|
||||
local chunk_size = self.GET_LINES_CHUNK_SIZE
|
||||
local chunk_start = range_start
|
||||
while chunk_start < range_end do
|
||||
local chunk_end = math.min(chunk_start + chunk_size, range_end)
|
||||
local chunk_lines = vim.api.nvim_buf_get_lines(self.bufnr, chunk_start, chunk_end, true)
|
||||
for i, line in ipairs(chunk_lines) do
|
||||
if not skip_already_indexed or not self.lines_words[chunk_start + i] then
|
||||
self:index_line(chunk_start + i, line)
|
||||
end
|
||||
end
|
||||
chunk_start = chunk_end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function buffer.start_indexing_timer(self)
|
||||
self.lines_count = vim.api.nvim_buf_line_count(self.bufnr)
|
||||
self.timer_current_line = 0
|
||||
|
||||
-- Negative values result in an integer overflow in luv (vim.loop), and zero
|
||||
-- disables timer repeat, so only intervals larger than 1 are valid.
|
||||
local interval = math.max(1, self.opts.indexing_interval)
|
||||
self.timer:start(0, interval, function()
|
||||
if self.closed then
|
||||
self:stop_indexing_timer()
|
||||
return
|
||||
end
|
||||
|
||||
-- Note that the async indexer is designed to not break even if the user is
|
||||
-- editing the file while it is in the process of being indexed. Because
|
||||
-- the indexing in watcher must use the synchronous algorithm, we assume
|
||||
-- that the data already present in self.lines_words to be correct and
|
||||
-- doesn't need refreshing here because even if we do receive text from
|
||||
-- nvim_buf_get_lines different from what the watcher has seen so far, it
|
||||
-- (the watcher) will catch up on the next on_lines event.
|
||||
|
||||
-- Skip over the already indexed lines
|
||||
while self.lines_words[self.timer_current_line + 1] do
|
||||
self.timer_current_line = self.timer_current_line + 1
|
||||
end
|
||||
|
||||
local batch_start = self.timer_current_line
|
||||
local batch_size = self.opts.indexing_batch_size
|
||||
-- NOTE: self.lines_count may be modified by the indexer.
|
||||
local batch_end = batch_size >= 1 and math.min(batch_start + batch_size, self.lines_count) or self.lines_count
|
||||
if batch_end >= self.lines_count then
|
||||
self:stop_indexing_timer()
|
||||
end
|
||||
self.timer_current_line = batch_end
|
||||
self:mark_all_lines_dirty()
|
||||
|
||||
self:index_range(batch_start, batch_end, true)
|
||||
end)
|
||||
end
|
||||
|
||||
--- watch
|
||||
function buffer.watch(self)
|
||||
self.lines_count = vim.api.nvim_buf_line_count(self.bufnr)
|
||||
|
||||
-- NOTE: As far as I know, indexing in watching can't be done asynchronously
|
||||
-- because even built-in commands generate multiple consequent `on_lines`
|
||||
-- events, and I'm not even mentioning plugins here. To get accurate results
|
||||
-- we would have to either re-index the entire file on throttled events (slow
|
||||
-- and looses the benefit of on_lines watching), or put the events in a
|
||||
-- queue, which would complicate the plugin a lot. Plus, most changes which
|
||||
-- trigger this event will be from regular editing, and so 99% of the time
|
||||
-- they will affect only 1-2 lines.
|
||||
vim.api.nvim_buf_attach(self.bufnr, false, {
|
||||
-- NOTE: line indexes are 0-based and the last line is not inclusive.
|
||||
on_lines = function(_, _, _, first_line, old_last_line, new_last_line, _, _, _)
|
||||
if self.closed then
|
||||
return true
|
||||
end
|
||||
|
||||
if old_last_line == new_last_line and first_line == new_last_line then
|
||||
-- This condition is really intended as a workaround for
|
||||
-- https://github.com/hrsh7th/cmp-buffer/issues/28, but it will also
|
||||
-- protect us from completely empty text edits.
|
||||
return
|
||||
end
|
||||
|
||||
local delta = new_last_line - old_last_line
|
||||
local old_lines_count = self.lines_count
|
||||
local new_lines_count = old_lines_count + delta
|
||||
if new_lines_count == 0 then -- clear
|
||||
-- This branch protects against bugs after full-file deletion. If you
|
||||
-- do, for example, gdGG, the new_last_line of the event will be zero.
|
||||
-- Which is not true, a buffer always contains at least one empty line,
|
||||
-- only unloaded buffers contain zero lines.
|
||||
new_lines_count = 1
|
||||
for i = old_lines_count, 2, -1 do
|
||||
self.lines_words[i] = nil
|
||||
end
|
||||
self.lines_words[1] = {}
|
||||
elseif delta > 0 then -- append
|
||||
-- Explicitly reserve more slots in the array part of the lines table,
|
||||
-- all of them will be filled in the next loop, but in reverse order
|
||||
-- (which is why I am concerned about preallocation). Why is there no
|
||||
-- built-in function to do this in Lua???
|
||||
for i = old_lines_count + 1, new_lines_count do
|
||||
self.lines_words[i] = false
|
||||
end
|
||||
-- Move forwards the unchanged elements in the tail part.
|
||||
for i = old_lines_count, old_last_line + 1, -1 do
|
||||
self.lines_words[i + delta] = self.lines_words[i]
|
||||
end
|
||||
-- Fill in new tables for the added lines.
|
||||
for i = old_last_line + 1, new_last_line do
|
||||
self.lines_words[i] = {}
|
||||
end
|
||||
elseif delta < 0 then -- remove
|
||||
-- Move backwards the unchanged elements in the tail part.
|
||||
for i = old_last_line + 1, old_lines_count do
|
||||
self.lines_words[i + delta] = self.lines_words[i]
|
||||
end
|
||||
-- Remove (already copied) tables from the end, in reverse order, so
|
||||
-- that we don't make holes in the lines table.
|
||||
for i = old_lines_count, new_lines_count + 1, -1 do
|
||||
self.lines_words[i] = nil
|
||||
end
|
||||
end
|
||||
self.lines_count = new_lines_count
|
||||
|
||||
-- This branch is support code for handling cases when the user is
|
||||
-- editing the buffer while the async indexer is running. It solves the
|
||||
-- problem that if new lines are inserted or old lines are deleted, the
|
||||
-- indexes of each subsequent line will change, and so the indexer
|
||||
-- current position must be adjusted to not accidentally skip any lines.
|
||||
if self.timer:is_active() then
|
||||
if first_line <= self.timer_current_line and self.timer_current_line < old_last_line then
|
||||
-- The indexer was in the area of the current text edit. We will
|
||||
-- synchronously index this area it in a moment, so the indexer
|
||||
-- should resume from right after the edit range.
|
||||
self.timer_current_line = new_last_line
|
||||
elseif self.timer_current_line >= old_last_line then
|
||||
-- The indexer was somewhere past the current text edit. This means
|
||||
-- that the line numbers could have changed, and the indexing
|
||||
-- position must be adjusted accordingly.
|
||||
self.timer_current_line = self.timer_current_line + delta
|
||||
end
|
||||
end
|
||||
|
||||
if first_line == self.last_edit_first_line and old_last_line == self.last_edit_last_line and new_last_line == self.last_edit_last_line then
|
||||
self.unique_words_curr_line_dirty = true
|
||||
else
|
||||
self.unique_words_curr_line_dirty = true
|
||||
self.unique_words_other_lines_dirty = true
|
||||
end
|
||||
self.last_edit_first_line = first_line
|
||||
self.last_edit_last_line = new_last_line
|
||||
|
||||
self.words_distances_dirty = true
|
||||
|
||||
-- replace lines
|
||||
self:index_range(first_line, new_last_line)
|
||||
end,
|
||||
|
||||
on_reload = function(_, _)
|
||||
if self.closed then
|
||||
return true
|
||||
end
|
||||
|
||||
clear_table(self.lines_words)
|
||||
|
||||
self:stop_indexing_timer()
|
||||
self:start_indexing_timer()
|
||||
end,
|
||||
|
||||
on_detach = function(_, _)
|
||||
if self.closed then
|
||||
return true
|
||||
end
|
||||
self:close()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param linenr number
|
||||
---@param line string
|
||||
function buffer.index_line(self, linenr, line)
|
||||
local words = self.lines_words[linenr]
|
||||
if not words then
|
||||
words = {}
|
||||
self.lines_words[linenr] = words
|
||||
else
|
||||
clear_table(words)
|
||||
end
|
||||
local word_i = 1
|
||||
|
||||
local remaining = line
|
||||
-- The if statement checks the number of bytes in the line string, but slices
|
||||
-- it on the number of characters. This is not a problem because the number
|
||||
-- of characters is always equal to (if only ASCII characters are used) or
|
||||
-- smaller than (if multibyte Unicode characters are used) the number of bytes.
|
||||
-- In other words, if the line contains more characters than the max limit,
|
||||
-- then it will always contain more bytes than the same limit.
|
||||
-- This check is here because calling a Vimscript function is relatively slow.
|
||||
if #remaining > self.opts.max_indexed_line_length then
|
||||
remaining = vim.fn.strcharpart(line, 0, self.opts.max_indexed_line_length)
|
||||
end
|
||||
while #remaining > 0 do
|
||||
-- NOTE: Both start and end indexes here are 0-based (unlike Lua strings),
|
||||
-- and the end index is not inclusive.
|
||||
local match_start, match_end = self.regex:match_str(remaining)
|
||||
if match_start and match_end then
|
||||
local word = remaining:sub(match_start + 1, match_end)
|
||||
if #word >= self.opts.keyword_length then
|
||||
words[word_i] = word
|
||||
word_i = word_i + 1
|
||||
end
|
||||
remaining = remaining:sub(match_end + 1)
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function buffer.get_words(self)
|
||||
-- NOTE: unique_words are rebuilt on-demand because it is common for the
|
||||
-- watcher callback to be fired VERY frequently, and a rebuild needs to go
|
||||
-- over ALL lines, not just the changed ones.
|
||||
if self.unique_words_other_lines_dirty then
|
||||
clear_table(self.unique_words_other_lines)
|
||||
self:rebuild_unique_words(self.unique_words_other_lines, 0, self.last_edit_first_line)
|
||||
self:rebuild_unique_words(self.unique_words_other_lines, self.last_edit_last_line, self.lines_count)
|
||||
self.unique_words_other_lines_dirty = false
|
||||
end
|
||||
if self.unique_words_curr_line_dirty then
|
||||
clear_table(self.unique_words_curr_line)
|
||||
self:rebuild_unique_words(self.unique_words_curr_line, self.last_edit_first_line, self.last_edit_last_line)
|
||||
self.unique_words_curr_line_dirty = false
|
||||
end
|
||||
return { self.unique_words_other_lines, self.unique_words_curr_line }
|
||||
end
|
||||
|
||||
--- rebuild_unique_words
|
||||
function buffer.rebuild_unique_words(self, words_table, range_start, range_end)
|
||||
for i = range_start + 1, range_end do
|
||||
for _, w in ipairs(self.lines_words[i] or {}) do
|
||||
words_table[w] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param cursor_row number
|
||||
---@return table<string, number>
|
||||
function buffer.get_words_distances(self, cursor_row)
|
||||
if self.words_distances_dirty or cursor_row ~= self.words_distances_last_cursor_row then
|
||||
local distances = self.words_distances
|
||||
clear_table(distances)
|
||||
for i = 1, self.lines_count do
|
||||
for _, w in ipairs(self.lines_words[i] or {}) do
|
||||
local dist = math.abs(cursor_row - i)
|
||||
distances[w] = distances[w] and math.min(distances[w], dist) or dist
|
||||
end
|
||||
end
|
||||
self.words_distances_last_cursor_row = cursor_row
|
||||
self.words_distances_dirty = false
|
||||
end
|
||||
return self.words_distances
|
||||
end
|
||||
|
||||
return buffer
|
||||
@ -0,0 +1 @@
|
||||
return require('cmp_buffer.source').new()
|
||||
@ -0,0 +1,125 @@
|
||||
local buffer = require('cmp_buffer.buffer')
|
||||
|
||||
---@class cmp_buffer.Options
|
||||
---@field public keyword_length number
|
||||
---@field public keyword_pattern string
|
||||
---@field public get_bufnrs fun(): number[]
|
||||
---@field public indexing_batch_size number
|
||||
---@field public indexing_interval number
|
||||
---@field public max_indexed_line_length number
|
||||
|
||||
---@type cmp_buffer.Options
|
||||
local defaults = {
|
||||
keyword_length = 3,
|
||||
keyword_pattern = [[\%(-\?\d\+\%(\.\d\+\)\?\|\h\%(\w\|á\|Á\|é\|É\|í\|Í\|ó\|Ó\|ú\|Ú\)*\%(-\%(\w\|á\|Á\|é\|É\|í\|Í\|ó\|Ó\|ú\|Ú\)*\)*\)]],
|
||||
get_bufnrs = function()
|
||||
return { vim.api.nvim_get_current_buf() }
|
||||
end,
|
||||
indexing_batch_size = 1000,
|
||||
indexing_interval = 100,
|
||||
max_indexed_line_length = 1024 * 40,
|
||||
}
|
||||
|
||||
local source = {}
|
||||
|
||||
source.new = function()
|
||||
local self = setmetatable({}, { __index = source })
|
||||
self.buffers = {}
|
||||
return self
|
||||
end
|
||||
|
||||
---@return cmp_buffer.Options
|
||||
source._validate_options = function(_, params)
|
||||
local opts = vim.tbl_deep_extend('keep', params.option, defaults)
|
||||
vim.validate({
|
||||
keyword_length = { opts.keyword_length, 'number' },
|
||||
keyword_pattern = { opts.keyword_pattern, 'string' },
|
||||
get_bufnrs = { opts.get_bufnrs, 'function' },
|
||||
indexing_batch_size = { opts.indexing_batch_size, 'number' },
|
||||
indexing_interval = { opts.indexing_interval, 'number' },
|
||||
})
|
||||
return opts
|
||||
end
|
||||
|
||||
source.get_keyword_pattern = function(self, params)
|
||||
local opts = self:_validate_options(params)
|
||||
return opts.keyword_pattern
|
||||
end
|
||||
|
||||
source.complete = function(self, params, callback)
|
||||
local opts = self:_validate_options(params)
|
||||
|
||||
local processing = false
|
||||
local bufs = self:_get_buffers(opts)
|
||||
for _, buf in ipairs(bufs) do
|
||||
if buf.timer:is_active() then
|
||||
processing = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
vim.defer_fn(function()
|
||||
local input = string.sub(params.context.cursor_before_line, params.offset)
|
||||
local items = {}
|
||||
local words = {}
|
||||
for _, buf in ipairs(bufs) do
|
||||
for _, word_list in ipairs(buf:get_words()) do
|
||||
for word, _ in pairs(word_list) do
|
||||
if not words[word] and input ~= word then
|
||||
words[word] = true
|
||||
table.insert(items, {
|
||||
label = word,
|
||||
dup = 0,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
callback({
|
||||
items = items,
|
||||
isIncomplete = processing,
|
||||
})
|
||||
end, processing and 100 or 0)
|
||||
end
|
||||
|
||||
---@param opts cmp_buffer.Options
|
||||
---@return cmp_buffer.Buffer[]
|
||||
source._get_buffers = function(self, opts)
|
||||
local buffers = {}
|
||||
for _, bufnr in ipairs(opts.get_bufnrs()) do
|
||||
if not self.buffers[bufnr] then
|
||||
local new_buf = buffer.new(bufnr, opts)
|
||||
new_buf.on_close_cb = function()
|
||||
self.buffers[bufnr] = nil
|
||||
end
|
||||
new_buf:start_indexing_timer()
|
||||
new_buf:watch()
|
||||
self.buffers[bufnr] = new_buf
|
||||
end
|
||||
table.insert(buffers, self.buffers[bufnr])
|
||||
end
|
||||
|
||||
return buffers
|
||||
end
|
||||
|
||||
source._get_distance_from_entry = function(self, entry)
|
||||
local buf = self.buffers[entry.context.bufnr]
|
||||
if buf then
|
||||
local distances = buf:get_words_distances(entry.context.cursor.line + 1)
|
||||
return distances[entry.completion_item.filterText] or distances[entry.completion_item.label]
|
||||
end
|
||||
end
|
||||
|
||||
source.compare_locality = function(self, entry1, entry2)
|
||||
if entry1.context ~= entry2.context then
|
||||
return
|
||||
end
|
||||
local dist1 = self:_get_distance_from_entry(entry1) or math.huge
|
||||
local dist2 = self:_get_distance_from_entry(entry2) or math.huge
|
||||
if dist1 ~= dist2 then
|
||||
return dist1 < dist2
|
||||
end
|
||||
end
|
||||
|
||||
return source
|
||||
@ -0,0 +1,68 @@
|
||||
---This timer matches the semantics of setInterval and clearInterval of
|
||||
---Javascript. It provides a more reliable alternative to vim.loop.timer_start
|
||||
---with a callback wrapped into a vim.schedule call by addressing two problems:
|
||||
---1. Scheduled callbacks are invoked less frequently than a libuv timer with a
|
||||
--- small interval (1-5ms). This causes those callbacks to fill up the queue
|
||||
--- in the event loop, and so the callback function may get invoked multiple
|
||||
--- times on one event loop tick. In contrast, Javascript's setInterval
|
||||
--- guarantees that the callback is not invoked more frequently than the
|
||||
--- interval.
|
||||
---2. When a libuv timer is stopped with vim.loop.timer_stop, it doesn't affect
|
||||
--- the callbacks that have already been scheduled. So timer_stop will not
|
||||
--- immediately stop the timer, the actual callback function will run one
|
||||
--- more time until it is finally stopped. This implementation ensures that
|
||||
--- timer_stop prevents any subsequent invocations of the callback.
|
||||
---
|
||||
---@class cmp_buffer.Timer
|
||||
---@field public handle any
|
||||
---@field private callback_wrapper_instance fun()|nil
|
||||
local timer = {}
|
||||
|
||||
function timer.new()
|
||||
local self = setmetatable({}, { __index = timer })
|
||||
self.handle = vim.loop.new_timer()
|
||||
self.callback_wrapper_instance = nil
|
||||
return self
|
||||
end
|
||||
|
||||
---@param timeout_ms number
|
||||
---@param repeat_ms number
|
||||
---@param callback fun()
|
||||
function timer:start(timeout_ms, repeat_ms, callback)
|
||||
-- This is the flag that fixes problem 1.
|
||||
local scheduled = false
|
||||
-- Creating a function on every call to timer_start ensures that we can always
|
||||
-- detect when a different callback is set by calling timer_start and prevent
|
||||
-- the old one from being invoked.
|
||||
local function callback_wrapper()
|
||||
if scheduled then
|
||||
return
|
||||
end
|
||||
scheduled = true
|
||||
vim.schedule(function()
|
||||
scheduled = false
|
||||
-- Either a different callback was set, or the timer has been stopped.
|
||||
if self.callback_wrapper_instance ~= callback_wrapper then
|
||||
return
|
||||
end
|
||||
callback()
|
||||
end)
|
||||
end
|
||||
self.handle:start(timeout_ms, repeat_ms, callback_wrapper)
|
||||
self.callback_wrapper_instance = callback_wrapper
|
||||
end
|
||||
|
||||
function timer:stop()
|
||||
self.handle:stop()
|
||||
self.callback_wrapper_instance = nil
|
||||
end
|
||||
|
||||
function timer:is_active()
|
||||
return self.handle:is_active()
|
||||
end
|
||||
|
||||
function timer:close()
|
||||
self.handle:close()
|
||||
end
|
||||
|
||||
return timer
|
||||
Reference in New Issue
Block a user