1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View 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.

View 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`.

View File

@ -0,0 +1 @@
require('cmp').register_source('buffer', require('cmp_buffer'))

View File

@ -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

View File

@ -0,0 +1 @@
return require('cmp_buffer.source').new()

View File

@ -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

View File

@ -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