├── README.md ├── after └── plugin │ └── cmp_calc.lua └── lua └── cmp_calc └── init.lua /README.md: -------------------------------------------------------------------------------- 1 | # cmp-calc 2 | 3 | nvim-cmp source for math calculation. 4 | 5 | # Setup 6 | 7 | ```lua 8 | require'cmp'.setup { 9 | sources = { 10 | { name = 'calc' } 11 | } 12 | } 13 | ``` 14 | 15 | -------------------------------------------------------------------------------- /after/plugin/cmp_calc.lua: -------------------------------------------------------------------------------- 1 | require'cmp'.register_source('calc', require'cmp_calc'.new()) 2 | 3 | -------------------------------------------------------------------------------- /lua/cmp_calc/init.lua: -------------------------------------------------------------------------------- 1 | local math_keys = {} 2 | for key in pairs(math) do 3 | table.insert(math_keys, key) 4 | end 5 | 6 | local source = {} 7 | 8 | source.new = function() 9 | return setmetatable({}, { __index = source }) 10 | end 11 | 12 | source.get_position_encoding_kind = function() 13 | return 'utf-8' 14 | end 15 | 16 | source.get_trigger_characters = function() 17 | return source._trigger_chars 18 | end 19 | 20 | source.get_keyword_pattern = function() 21 | return source._keyptn 22 | end 23 | 24 | source.complete = function(self, request, callback) 25 | local input = string.sub(request.context.cursor_before_line, request.offset) 26 | 27 | -- Resolve math_keys 28 | for _, key in ipairs(math_keys) do 29 | input = string.gsub(input, vim.pesc(key), 'math.' .. key) 30 | end 31 | 32 | -- Analyze column count. 33 | local delta = self:_analyze(input) 34 | if not delta then 35 | return callback({ isIncomplete = true }) 36 | end 37 | while string.byte(input, delta + 1) == 32 do -- keep indent 38 | delta = delta + 1 39 | end 40 | local program = string.sub(input, delta + 1) 41 | 42 | -- Ignore if input has no math operators. 43 | if string.match(program, '^[ %d().]*$') ~= nil then 44 | return callback({ isIncomplete = true }) 45 | end 46 | 47 | -- Ignore if failed to interpret to Lua. 48 | local m = load('return ' .. program) 49 | if type(m) ~= 'function' then 50 | return callback({ isIncomplete = true }) 51 | end 52 | local status, value = pcall(m) 53 | 54 | -- Ignore if failed or not a number. 55 | if not status or type(value) ~= "number" then 56 | return callback({ isIncomplete = true }) 57 | else 58 | value = tostring(value) 59 | end 60 | 61 | callback({ 62 | items = { 63 | { 64 | word = string.sub(input, 1, delta) .. value, 65 | label = value, 66 | filterText = input, 67 | textEdit = { 68 | range = { 69 | start = { 70 | line = request.context.cursor.row - 1, 71 | character = delta + request.offset - 1, 72 | }, 73 | ['end'] = { 74 | line = request.context.cursor.row - 1, 75 | character = request.context.cursor.col - 1, 76 | }, 77 | }, 78 | newText = value, 79 | }, 80 | }, 81 | { 82 | word = input .. ' = ' .. value, 83 | label = program .. ' = ' .. value, 84 | filterText = input, 85 | textEdit = { 86 | range = { 87 | start = { 88 | line = request.context.cursor.row - 1, 89 | character = delta + request.offset - 1, 90 | }, 91 | ['end'] = { 92 | line = request.context.cursor.row - 1, 93 | character = request.context.cursor.col - 1, 94 | }, 95 | }, 96 | newText = program .. ' = ' .. value, 97 | }, 98 | } 99 | }, 100 | isIncomplete = true, 101 | }) 102 | end 103 | 104 | source._analyze = function(_, input) 105 | local unmatched_parens = 0 106 | local o = string.byte('(') 107 | local c = string.byte(')') 108 | for i = #input, 1, -1 do 109 | if string.byte(input, i) == c then 110 | unmatched_parens = unmatched_parens - 1 111 | elseif string.byte(input, i) == o then 112 | if unmatched_parens == 0 then 113 | -- going in reverse -> extra '(' won't get matched -> cut here 114 | return i 115 | end 116 | unmatched_parens = unmatched_parens + 1 117 | end 118 | end 119 | 120 | if unmatched_parens == 0 then 121 | return 0 122 | end 123 | 124 | for i = 1, #input do 125 | if string.byte(input, i) == c then 126 | unmatched_parens = unmatched_parens + 1 127 | if unmatched_parens == 0 then 128 | return i 129 | end 130 | elseif string.byte(input, i) == o then 131 | unmatched_parens = unmatched_parens - 1 132 | end 133 | end 134 | 135 | return nil -- expression ends with extra ')' 136 | end 137 | 138 | source._trigger_chars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ')' } 139 | 140 | -- Keyword matching pattern (vim regex) 141 | source._keyptn = [[\s*\zs\(\d\+\(\.\d\+\)\?\|[ ()^*/%+-]\|]] .. 142 | table.concat(math_keys, '\\|') .. [[\)\+]] 143 | 144 | return source 145 | --------------------------------------------------------------------------------