├── .gitignore ├── .stylua.toml ├── lua └── vsrocq │ ├── tagged_lines.lua │ ├── util.lua │ ├── lsp_options.lua │ ├── init.lua │ ├── render.lua │ ├── config.lua │ ├── _meta.lua │ ├── pp.lua │ └── client.lua ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | _opam 2 | .lia.cache 3 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 100 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 2 5 | quote_style = "AutoPreferSingle" 6 | call_parentheses = "NoSingleTable" 7 | -------------------------------------------------------------------------------- /lua/vsrocq/tagged_lines.lua: -------------------------------------------------------------------------------- 1 | ---@class vsrocq.TaggedLines 2 | ---@field [1] string[] 3 | ---@field [2] vsrocq.Tag[] 4 | local TaggedLines = {} 5 | TaggedLines.__index = TaggedLines 6 | 7 | ---@param lines? string[] 8 | ---@param tags? vsrocq.Tag[] 9 | ---@return vsrocq.TaggedLines 10 | function TaggedLines.new(lines, tags) 11 | return setmetatable({ lines or {}, tags or {} }, TaggedLines) 12 | end 13 | 14 | ---@param line string 15 | function TaggedLines:add_line(line) 16 | self[1][#self[1] + 1] = line 17 | end 18 | 19 | ---@param tl vsrocq.TaggedLines 20 | function TaggedLines:append(tl) 21 | local offset = #self[1] 22 | for _, tag in ipairs(tl[2]) do 23 | self[2][#self[2] + 1] = { 24 | offset + tag[1], 25 | tag[2], 26 | offset + tag[3], 27 | tag[4], 28 | [0] = tag[0], 29 | } 30 | end 31 | for _, line in ipairs(tl[1]) do 32 | self:add_line(line) 33 | end 34 | end 35 | 36 | return TaggedLines 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jaehwang Jung 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lua/vsrocq/util.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- TODO: The position sent by the server may be no longer valid in the current buffer text 4 | ---@param bufnr buffer 5 | ---@param position lsp.Position 6 | ---@param offset_encoding lsp.PositionEncodingKind 7 | ---@return APIPosition 8 | function M.position_lsp_to_api(bufnr, position, offset_encoding) 9 | local idx = vim.lsp.util._get_line_byte_from_position( 10 | bufnr, 11 | { line = position.line, character = position.character }, 12 | offset_encoding 13 | ) 14 | return { position.line, idx } 15 | end 16 | 17 | ---@param position APIPosition 18 | ---@return MarkPosition 19 | function M.position_api_to_mark(position) 20 | return { position[1] + 1, position[2] } 21 | end 22 | 23 | ---@param p1 APIPosition 24 | ---@param p2 APIPosition 25 | ---@return boolean 26 | function M.api_position_lt(p1, p2) 27 | if p1[1] < p2[1] then 28 | return true 29 | elseif p1[1] == p2[1] then 30 | return p1[2] < p2[2] 31 | else 32 | return false 33 | end 34 | end 35 | 36 | local str_utfindex = vim.fn.has('nvim-0.11') == 1 and function(s, encoding, index) 37 | return vim.str_utfindex(s, encoding, index, false) 38 | end or function(s, encoding, index) 39 | return vim.lsp.util._str_utfindex_enc(s, index, encoding) 40 | end 41 | 42 | ---@param bufnr buffer 43 | ---@param position MarkPosition 44 | ---@param offset_encoding lsp.PositionEncodingKind 45 | ---@return lsp.Position 46 | function M.make_position_params(bufnr, position, offset_encoding) 47 | local row, col = unpack(position) 48 | row = row - 1 49 | local line = vim.api.nvim_buf_get_lines(bufnr, row, row + 1, true)[1] 50 | if not line then 51 | return { line = 0, character = 0 } 52 | end 53 | 54 | col = str_utfindex(line, offset_encoding, col) 55 | 56 | return { line = row, character = col } 57 | end 58 | 59 | -- TODO: version is not used in the server, but errors if not included. 60 | -- file an issue to use TextDocumentIdentifier. 61 | ---@param bufnr buffer 62 | ---@return lsp.VersionedTextDocumentIdentifier 63 | function M.make_versioned_text_document_params(bufnr) 64 | return { 65 | uri = vim.uri_from_bufnr(bufnr), 66 | version = vim.lsp.util.buf_versions[bufnr], 67 | } 68 | end 69 | 70 | ---@param bufnr buffer 71 | ---@return MarkPosition 72 | function M.guess_position(bufnr) 73 | local win = vim.api.nvim_get_current_win() 74 | if vim.api.nvim_win_get_buf(win) ~= bufnr then 75 | error("can't guess position") 76 | end 77 | return vim.api.nvim_win_get_cursor(win) 78 | end 79 | 80 | ---@param client vim.lsp.Client 81 | ---@param bufnr integer 82 | ---@param method string 83 | ---@param params table 84 | ---@param handler? lsp.Handler 85 | ---@return fun()|nil cancel function to cancel the request 86 | function M.request_async(client, bufnr, method, params, handler) 87 | local request_success, request_id = client:request(method, params, handler, bufnr) 88 | if request_success then 89 | return function() 90 | client:cancel_request(assert(request_id)) 91 | end 92 | end 93 | end 94 | 95 | return M 96 | -------------------------------------------------------------------------------- /lua/vsrocq/lsp_options.lua: -------------------------------------------------------------------------------- 1 | -- ## Lsp Options 2 | -- Configuration for the language server 3 | -- (is separate to *Config* because this is what's actually sent to the server) 4 | -- is following setting of vsrocqtop 5 | --- https://github.com/rocq-prover/vsrocq/blob/main/language-server/protocol/settings.ml 6 | 7 | ---@class vsrocq.LspOptions 8 | local LspOptions = { 9 | memory = { 10 | ---@type integer 11 | limit = 4, 12 | }, 13 | 14 | goals = { 15 | diff = { 16 | ---@type "off"|"on"|"removed" 17 | mode = 'off', 18 | }, 19 | 20 | messages = { 21 | ---@type boolean 22 | full = true, 23 | }, 24 | 25 | ---@type "String"|"Pp" 26 | ppmode = "Pp", 27 | }, 28 | 29 | proof = { 30 | ---@enum 31 | ---|0 # Manual 32 | ---|1 # Continuous 33 | mode = 0, 34 | 35 | ---@enum 36 | ---|0 # Cursor 37 | ---|1 # NextCommand 38 | pointInterpretationMode = 0, 39 | 40 | ---@type "None"|"Skip"|"Delegate" 41 | delegation = 'None', 42 | 43 | ---@type integer 44 | workers = 1, 45 | 46 | ---@type boolean 47 | block = true, 48 | }, 49 | 50 | completion = { 51 | ---@type boolean 52 | enable = false, 53 | 54 | ---@type integer 55 | unificationLimit = 100, 56 | 57 | ---@enum 58 | ---|0 # StructuredSplitUnification 59 | ---|1 # SplitTypeIntersection 60 | algorithm = 1, 61 | 62 | -- atomicFactor = 5.0, 63 | -- sizeFactor = 1.0, 64 | }, 65 | 66 | diagnostics = { 67 | ---@type boolean 68 | full = false, 69 | }, 70 | } 71 | 72 | LspOptions.__index = LspOptions 73 | 74 | -- table to convert string to LspOptions 75 | local completion_algorithm_table = { 76 | StructuredSplitUnification = 0, 77 | SplitTypeIntersection = 1, 78 | } 79 | local proof_mode_table = { 80 | Manual = 0, 81 | Continuous = 1, 82 | } 83 | local proof_pointInterpretationMode_table = { 84 | Cursor = 0, 85 | NextCommand = 1, 86 | } 87 | 88 | ---@param config vsrocq.Config 89 | ---@return vsrocq.LspOptions 90 | function LspOptions:new(config) 91 | local lsp_opts = { 92 | memory = vim.deepcopy(config.memory), 93 | goals = { 94 | diff = vim.deepcopy(config.goals.diff), 95 | messages = vim.deepcopy(config.goals.messages), 96 | ppmode = config.goals.ppmode, 97 | }, 98 | proof = { 99 | mode = proof_mode_table[config.proof.mode], 100 | pointInterpretationMode = proof_pointInterpretationMode_table[config.proof.pointInterpretationMode], 101 | delegation = config.proof.delegation, 102 | workers = config.proof.workers, 103 | block = config.proof.block, 104 | }, 105 | completion = { 106 | enable = config.completion.enable, 107 | unificationLimit = config.completion.unificationLimit, 108 | algorithm = completion_algorithm_table[config.completion.algorithm], 109 | }, 110 | diagnostics = vim.deepcopy(config.diagnostics), 111 | } 112 | setmetatable(lsp_opts, self) 113 | return lsp_opts 114 | end 115 | 116 | return LspOptions 117 | -------------------------------------------------------------------------------- /lua/vsrocq/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | -- Some options values are used both in the server and the client, e.g., 4 | -- vsrocq.proof.mode is used in "check_mode" variable in server, and "goalsHook" in client. 5 | -- 6 | -- The vscode client forwards the entire config as `initializationOptions` to the server. 7 | -- The server itself doesn't have default config values, 8 | -- so we should forward the config as `init_options`, not `settings`. 9 | -- 10 | -- https://github.com/rocq-prover/vsrocq/blob/main/client/package.json 11 | -- https://github.com/rocq-prover/vsrocq/blob/main/language-server/protocol/settings.ml 12 | -- https://github.com/rocq-prover/vsrocq/blob/main/docs/protocol.md#configuration 13 | -- 14 | -- The "Rocq configuration" (vsrocq.trace.server, ...) are low-level client-only config handled by vim.lsp.start_client(). 15 | 16 | local Config = require('vsrocq.config') 17 | 18 | ---@class vsrocq.Config 19 | M.default_config = Config 20 | 21 | ---@type table map from client id 22 | M.clients = {} 23 | 24 | ---@param config vsrocq.Config 25 | ---@return fun(client: vim.lsp.Client, initialize_result: lsp.InitializeResult) 26 | local function make_on_init(user_on_init, config) 27 | return function(client, initialize_result) 28 | local ok, VSRocqNvim = pcall(require, 'vsrocq.client') 29 | if not ok then 30 | vim.print('[vsrocq.nvim] on_init failed', VSRocqNvim) 31 | return 32 | end 33 | M.clients[client.id] = VSRocqNvim:new(client, config) 34 | M.clients[client.id]:panels() 35 | if user_on_init then 36 | user_on_init(client, initialize_result) 37 | end 38 | end 39 | end 40 | 41 | ---@param user_on_attach? fun(client: vim.lsp.Client, bufnr: buffer) 42 | ---@return fun(client: vim.lsp.Client, bufnr: buffer) 43 | local function make_on_attach(user_on_attach) 44 | return function(client, bufnr) 45 | if not M.clients[client.id].buffers[bufnr] then 46 | M.clients[client.id]:attach(bufnr) 47 | end 48 | if user_on_attach then 49 | user_on_attach(client, bufnr) 50 | end 51 | end 52 | end 53 | 54 | local function make_on_exit(user_on_exit) 55 | return function(code, signal, client_id) 56 | if user_on_exit then 57 | user_on_exit(code, signal, client_id) 58 | end 59 | -- NOTE: on_exit runs in_fast_event 60 | vim.schedule(function() 61 | M.clients[client_id]:on_exit() 62 | M.clients[client_id] = nil 63 | end) 64 | end 65 | end 66 | 67 | ---@type lsp.Handler 68 | local function updateHighlights_notification_handler(_, result, ctx, _) 69 | M.clients[ctx.client_id]:updateHighlights(result) 70 | end 71 | 72 | ---@type lsp.Handler 73 | local function moveCursor_notification_handler(_, result, ctx, _) 74 | M.clients[ctx.client_id]:moveCursor(result) 75 | end 76 | 77 | ---@type lsp.Handler 78 | local function searchResult_notification_handler(_, result, ctx, _) 79 | M.clients[ctx.client_id]:searchResult(result) 80 | end 81 | 82 | ---@type lsp.Handler 83 | local function proofView_notification_handler(_, result, ctx, _) 84 | M.clients[ctx.client_id]:proofView(result) 85 | end 86 | 87 | local vscoqtop_config = { 88 | cmd = { 'vsrocqtop' }, 89 | filetypes = { 'coq' }, 90 | root_markers = { '_RocqProject', '_CoqProject', '.git' }, 91 | } 92 | 93 | -- TODO: don't use custom setup and use lspconfig's add_hook_before? 94 | ---@param opts { vsrocq?: table, lsp?: table } 95 | function M.setup(opts) 96 | opts = opts or {} 97 | opts.lsp = opts.lsp or {} 98 | opts.vsrocq = Config:new(opts.vsrocq or {}) 99 | 100 | opts.lsp.handlers = vim.tbl_extend('keep', opts.lsp.handlers or {}, { 101 | ['prover/updateHighlights'] = updateHighlights_notification_handler, 102 | ['prover/moveCursor'] = moveCursor_notification_handler, 103 | ['prover/searchResult'] = searchResult_notification_handler, 104 | ['prover/proofView'] = proofView_notification_handler, 105 | }) 106 | local user_on_init = opts.lsp.on_init 107 | opts.lsp.on_init = make_on_init(user_on_init, opts.vsrocq) 108 | local user_on_attach = opts.lsp.on_attach 109 | opts.lsp.on_attach = make_on_attach(user_on_attach) 110 | local user_on_exit = opts.lsp.on_exit 111 | opts.lsp.on_exit = make_on_exit(user_on_exit) 112 | assert( 113 | opts.lsp.init_options == nil and opts.lsp.settings == nil, 114 | "[vsrocq.nvim] settings must be passed via 'vsrocq' field" 115 | ) 116 | opts.lsp.init_options = opts.vsrocq:to_lsp_options() 117 | vim.lsp.config('vscoqtop', vim.tbl_deep_extend('force', vscoqtop_config, opts.lsp)) 118 | vim.lsp.enable('vscoqtop') 119 | end 120 | 121 | return M 122 | -------------------------------------------------------------------------------- /lua/vsrocq/render.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local pp = require('vsrocq.pp') 4 | local TaggedLines = require('vsrocq.tagged_lines') 5 | 6 | ---@param goal vsrocq.Goal 7 | ---@param i integer 8 | ---@param n integer 9 | ---@return vsrocq.TaggedLines 10 | function M.goal(i, n, goal) 11 | local tl = TaggedLines.new() 12 | tl:add_line(string.format('Goal %d (%d / %d)', goal.id, i, n)) 13 | for _, hyp in ipairs(goal.hypotheses) do 14 | tl:append(pp(hyp)) 15 | end 16 | tl:add_line('') 17 | tl:add_line('========================================') 18 | tl:add_line('') 19 | tl:append(pp(goal.goal)) 20 | return tl 21 | end 22 | 23 | ---@param goals vsrocq.Goal[] 24 | ---@return string[] 25 | function M.goals(goals) 26 | local tl = TaggedLines.new() 27 | for i, goal in ipairs(goals) do 28 | if i > 1 then 29 | tl:add_line('') 30 | tl:add_line('') 31 | tl:add_line( 32 | '────────────────────────────────────────────────────────────' 33 | ) 34 | tl:add_line('') 35 | end 36 | tl:append(M.goal(i, #goals, goal)) 37 | end 38 | return tl 39 | end 40 | 41 | vim.cmd([[ 42 | hi def link VsRocqError DiagnosticError 43 | hi def link VsRocqWarn DiagnosticWarn 44 | hi def link VsRocqInfo DiagnosticInfo 45 | hi def link VsRocqHint DiagnosticHint 46 | ]]) 47 | 48 | -- NOTE 49 | -- * no severity tag in pp 50 | -- * output of `info_eauto` is multiple messages 51 | ---@param messages vsrocq.RocqMessage[] 52 | ---@return vsrocq.TaggedLines 53 | function M.RocqMessages(messages) 54 | local tl = TaggedLines.new() 55 | for _, message in ipairs(messages) do 56 | tl:append(pp { 57 | 'Ppcmd_tag', 58 | ({ 'VsRocqError', 'VsRocqWarn', 'VsRocqInfo', 'VsRocqHint' })[message[1]], 59 | message[2], 60 | }) 61 | end 62 | return tl 63 | end 64 | 65 | ---@param proofView vsrocq.ProofViewNotification 66 | ---@param items ('goals'|'messages'|'shelvedGoals'|'givenUpGoals')[] 67 | ---@return vsrocq.TaggedLines 68 | function M.proofView(proofView, items) 69 | local tl = TaggedLines.new() 70 | 71 | if type(proofView.proof) == "table" then 72 | local stat = {} 73 | if #proofView.proof.goals > 0 then 74 | stat[#stat + 1] = #proofView.proof.goals .. ' goals' 75 | end 76 | if #proofView.proof.shelvedGoals > 0 then 77 | stat[#stat + 1] = #proofView.proof.shelvedGoals .. ' shelved' 78 | end 79 | if #proofView.proof.givenUpGoals > 0 then 80 | stat[#stat + 1] = #proofView.proof.givenUpGoals .. ' admitted' 81 | end 82 | if #stat > 0 then 83 | tl:add_line(table.concat(stat, ', ')) 84 | end 85 | end 86 | 87 | for i, item in ipairs(items) do 88 | local function padding() 89 | if i > 1 then 90 | tl:add_line('') 91 | tl:add_line('') 92 | end 93 | end 94 | if type(proofView.proof) == "table" then 95 | if item == 'goals' then 96 | if #proofView.proof.goals > 0 then 97 | tl:append(M.goals(proofView.proof.goals)) 98 | elseif #proofView.proof.unfocusedGoals > 0 then 99 | tl:add_line('This subproof is complete.') 100 | tl:add_line('') 101 | tl:add_line( 102 | 'Next unfocused subgoals ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' 103 | ) 104 | tl:add_line('') 105 | tl:append(M.goals(proofView.proof.unfocusedGoals)) 106 | elseif #proofView.proof.givenUpGoals > 0 then 107 | tl:add_line('No more subgoals, but there are some admitted subgoals.') 108 | tl:add_line('Go back and solve them, or use `Admitted.`') 109 | elseif #proofView.proof.shelvedGoals > 0 then 110 | tl:add_line('No more subgoals, but there are some shelved subgoals. Try `Unshelve.`') 111 | else 112 | tl:add_line('There are no more subgoals') 113 | end 114 | elseif item == 'shelvedGoals' then 115 | padding() 116 | tl:add_line( 117 | 'Shelved ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' 118 | ) 119 | tl:add_line('') 120 | tl:append(M.goals(proofView.proof.shelvedGoals)) 121 | elseif item == 'givenUpGoals' then 122 | padding() 123 | tl:add_line( 124 | 'Given Up ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━' 125 | ) 126 | tl:add_line('') 127 | tl:append(M.goals(proofView.proof.givenUpGoals)) 128 | end 129 | end 130 | end 131 | 132 | return tl 133 | end 134 | 135 | ---@param result vsrocq.SearchRocqResult 136 | ---@return vsrocq.TaggedLines 137 | function M.searchRocqResult(result) 138 | local tl = TaggedLines.new() 139 | tl:append(pp(result.name)) 140 | tl:append(pp(result.statement)) 141 | tl:add_line('') 142 | return tl 143 | end 144 | 145 | return M 146 | -------------------------------------------------------------------------------- /lua/vsrocq/config.lua: -------------------------------------------------------------------------------- 1 | ---@class vsrocq.Config 2 | local Config = { 3 | memory = { 4 | ---@type integer 5 | limit = 4, 6 | }, 7 | 8 | goals = { 9 | diff = { 10 | ---@type "off"|"on"|"removed" 11 | mode = 'off', 12 | }, 13 | 14 | messages = { 15 | ---@type boolean 16 | full = true, 17 | }, 18 | 19 | ---@type "String"|"Pp" 20 | ppmode = "Pp", 21 | 22 | ---@type integer 23 | maxDepth = 17, 24 | }, 25 | 26 | proof = { 27 | ---@type "Manual"|"Continuous" 28 | mode = 'Manual', 29 | 30 | ---@type "Cursor"|"NextCommand" 31 | pointInterpretationMode = 'Cursor', 32 | 33 | cursor = { 34 | ---@type boolean 35 | sticky = true, 36 | }, 37 | 38 | ---@type "None"|"Skip"|"Delegate" 39 | delegation = 'None', 40 | 41 | ---@type integer 42 | workers = 1, 43 | 44 | ---@type boolean 45 | block = true, 46 | }, 47 | 48 | completion = { 49 | ---@type boolean 50 | enable = false, 51 | 52 | ---@type integer 53 | unificationLimit = 100, 54 | 55 | ---@type "StructuredSplitUnification"|"SplitTypeIntersection" 56 | algorithm = 'SplitTypeIntersection', 57 | }, 58 | 59 | diagnostics = { 60 | ---@type boolean 61 | full = false, 62 | }, 63 | } 64 | 65 | Config.__index = Config 66 | 67 | -- keys in config 68 | local goals_diff_keys = { 'off', 'on', 'removed' } 69 | local goals_ppmode_keys = { 'Pp', 'String' } 70 | local proof_delegation_keys = { 'None', 'Skip', 'Delegate' } 71 | local proof_mode_keys = { 'Manual', 'Continuous' } 72 | local proof_pointInterpretationMode_keys = { 'Cursor', 'NextCommand' } 73 | local completion_algorithm_keys = { 'StructuredSplitUnification', 'SplitTypeIntersection' } 74 | 75 | ---@param opts table 76 | ---@return vsrocq.Config 77 | function Config:new(opts) 78 | local config = vim.tbl_deep_extend('keep', opts, self) 79 | vim.validate { 80 | ['vsrocq'] = { config, 'table' }, 81 | ['vsrocq.memory'] = { config.memory, 'table' }, 82 | ['vsrocq.memory.limit'] = { 83 | config.memory.limit, 84 | function(x) 85 | return type(x) == 'number' and x > 0 86 | end, 87 | 'positive number', 88 | }, 89 | ['vsrocq.goals'] = { config.goals, 'table' }, 90 | ['vsrocq.goals.diff'] = { config.goals.diff, 'table' }, 91 | ['vsrocq.goals.diff.mode'] = { 92 | config.goals.diff.mode, 93 | function(x) 94 | return type(x) == 'string' and vim.list_contains(goals_diff_keys, x) 95 | end, 96 | 'one of ' .. table.concat(goals_diff_keys, ', '), 97 | }, 98 | ['vsrocq.goals.messages'] = { config.goals.messages, 'table' }, 99 | ['vsrocq.goals.messages.full'] = { config.goals.messages.full, 'boolean' }, 100 | ['vsrocq.goals.maxDepth'] = { config.goals.maxDepth, 'number' }, 101 | ['vsrocq.goals.ppmode'] = { 102 | config.goals.ppmode, 103 | function(x) return type(x) == 'string' and vim.list_contains(goals_ppmode_keys, x) end, 104 | 'ome of ' .. table.concat(goals_ppmode_keys, ', '), 105 | }, 106 | ['vsrocq.proof'] = { config.proof, 'table' }, 107 | ['vsrocq.proof.mode'] = { 108 | config.proof.mode, 109 | function(x) 110 | return type(x) == 'string' and vim.list_contains(proof_mode_keys, x) 111 | end, 112 | 'one of ' .. table.concat(proof_mode_keys, ', '), 113 | }, 114 | ['vsrocq.proof.pointInterpretationMode'] = { 115 | config.proof.pointInterpretationMode, 116 | function(x) 117 | return type(x) == 'string' and vim.list_contains(proof_pointInterpretationMode_keys, x) 118 | end, 119 | 'one of ' .. table.concat(proof_pointInterpretationMode_keys, ', '), 120 | }, 121 | ['vsrocq.proof.cursor'] = { config.proof.cursor, 'table' }, 122 | ['vsrocq.proof.cursor.sticky'] = { config.proof.cursor.sticky, 'boolean' }, 123 | ['vsrocq.proof.delegation'] = { 124 | config.proof.delegation, 125 | function(x) 126 | return type(x) == 'string' and vim.list_contains(proof_delegation_keys, x) 127 | end, 128 | 'one of ' .. table.concat(proof_delegation_keys, ', '), 129 | }, 130 | ['vsrocq.proof.workers'] = { config.proof.workers, 'number' }, 131 | ['vsrocq.proof.block'] = { config.proof.block, 'boolean' }, 132 | ['vsrocq.completion'] = { config.completion, 'table' }, 133 | ['vsrocq.completion.enable'] = { config.completion.enable, 'boolean' }, 134 | ['vsrocq.completion.unificationLimit'] = { config.completion.unificationLimit, 'number' }, 135 | ['vsrocq.completion.algorithm'] = { 136 | config.completion.algorithm, 137 | function(x) 138 | return type(x) == 'string' and vim.list_contains(completion_algorithm_keys, x) 139 | end, 140 | 'one of ' .. table.concat(completion_algorithm_keys, ', '), 141 | }, 142 | ['vsrocq.diagnostics'] = { config.diagnostics, 'table' }, 143 | ['vsrocq.diagnostics.full'] = { config.diagnostics.full, 'boolean' }, 144 | } 145 | setmetatable(config, self) 146 | return config 147 | end 148 | 149 | ---@return vsrocq.LspOptions 150 | function Config:to_lsp_options() 151 | local LspConfig = require('vsrocq.lsp_options') 152 | return LspConfig:new(self) 153 | end 154 | 155 | return Config 156 | -------------------------------------------------------------------------------- /lua/vsrocq/_meta.lua: -------------------------------------------------------------------------------- 1 | ---@alias buffer integer 2 | ---@alias window integer 3 | 4 | ---Position for indexing used by most API functions (0-based line, 0-based column) (:h api-indexing). 5 | ---@class APIPosition: { [1]: integer, [2]: integer } 6 | 7 | ---Position for "mark-like" indexing (1-based line, 0-based column) (:h api-indexing). 8 | ---@class MarkPosition: { [1]: integer, [2]: integer } 9 | 10 | -- https://github.com/rocq-prover/vsrocq/blob/main/docs/protocol.md 11 | 12 | -- # Configuration 13 | 14 | -- # Highlights 15 | 16 | ---"vsrocq/UpdateHighlights" notification (server → client) parameter. 17 | ---@class vsrocq.UpdateHighlightsNotification 18 | ---@field uri lsp.DocumentUri 19 | ---@field processingRange lsp.Range[] 20 | ---@field processedRange lsp.Range[] 21 | 22 | -- # Goal view 23 | 24 | ---@alias vsrocq.PpTag string 25 | 26 | ---@alias vsrocq.BlockType 27 | ---| vsrocq.BlockType.Pp_hbox 28 | ---| vsrocq.BlockType.Pp_vbox 29 | ---| vsrocq.BlockType.Pp_hvbox 30 | ---| vsrocq.BlockType.Pp_hovbox 31 | 32 | ---@class vsrocq.BlockType.Pp_hbox 33 | ---@field [1] "Pp_hbox" 34 | 35 | ---@class vsrocq.BlockType.Pp_vbox 36 | ---@field [1] "Pp_vbox" 37 | ---@field [2] integer 38 | 39 | ---@class vsrocq.BlockType.Pp_hvbox 40 | ---@field [1] "Pp_hvbox" 41 | ---@field [2] integer 42 | 43 | ---@class vsrocq.BlockType.Pp_hovbox 44 | ---@field [1] "Pp_hovbox" 45 | ---@field [2] integer new lines in this box adds this amount of indent 46 | 47 | ---@alias vsrocq.PpString 48 | ---| vsrocq.PpString.Ppcmd_empty 49 | ---| vsrocq.PpString.Ppcmd_string 50 | ---| vsrocq.PpString.Ppcmd_glue 51 | ---| vsrocq.PpString.Ppcmd_box 52 | ---| vsrocq.PpString.Ppcmd_tag 53 | ---| vsrocq.PpString.Ppcmd_print_break 54 | ---| vsrocq.PpString.Ppcmd_force_newline 55 | ---| vsrocq.PpString.Ppcmd_comment 56 | 57 | ---@class vsrocq.PpString.Ppcmd_empty 58 | ---@field [1] "Ppcmd_empty" 59 | ---@field size integer 60 | 61 | ---@class vsrocq.PpString.Ppcmd_string 62 | ---@field [1] "Ppcmd_string" 63 | ---@field [2] string 64 | ---@field size? integer 65 | 66 | ---@class vsrocq.PpString.Ppcmd_glue 67 | ---@field [1] "Ppcmd_glue" 68 | ---@field [2] (vsrocq.PpString)[] 69 | ---@field size? integer 70 | 71 | ---@class vsrocq.PpString.Ppcmd_box 72 | ---@field [1] "Ppcmd_box" 73 | ---@field [2] vsrocq.BlockType 74 | ---@field [3] vsrocq.PpString 75 | ---@field size? integer 76 | 77 | ---@class vsrocq.PpString.Ppcmd_tag 78 | ---@field [1] "Ppcmd_tag" 79 | ---@field [2] vsrocq.PpTag 80 | ---@field [3] vsrocq.PpString 81 | ---@field size? integer 82 | 83 | ---@class vsrocq.PpString.Ppcmd_print_break 84 | ---@field [1] "Ppcmd_print_break" 85 | ---@field [2] integer number of spaces when this break is not line break 86 | ---@field [3] integer additional indent of the new lines (added to box's indent) 87 | ---@field size? integer 88 | 89 | ---@class vsrocq.PpString.Ppcmd_force_newline 90 | ---@field [1] "Ppcmd_force_newline" 91 | ---@field size? integer 92 | 93 | ---@class vsrocq.PpString.Ppcmd_comment 94 | ---@field [1] "Ppcmd_comment" 95 | ---@field [2] string[] 96 | ---@field size? integer 97 | 98 | --[[ 99 | if pp[1] == 'Ppcmd_empty' then 100 | ---@cast pp vsrocq.PpString.Ppcmd_empty 101 | elseif pp[1] == 'Ppcmd_string' then 102 | ---@cast pp vsrocq.PpString.Ppcmd_string 103 | elseif pp[1] == 'Ppcmd_glue' then 104 | ---@cast pp vsrocq.PpString.Ppcmd_glue 105 | elseif pp[1] == 'Ppcmd_box' then 106 | ---@cast pp vsrocq.PpString.Ppcmd_box 107 | elseif pp[1] == 'Ppcmd_tag' then 108 | ---@cast pp vsrocq.PpString.Ppcmd_tag 109 | elseif pp[1] == 'Ppcmd_print_break' then 110 | ---@cast pp vsrocq.PpString.Ppcmd_print_break 111 | elseif pp[1] == 'Ppcmd_force_newline' then 112 | ---@cast pp vsrocq.PpString.Ppcmd_force_newline 113 | elseif pp[1] == 'Ppcmd_comment' then 114 | ---@cast pp vsrocq.PpString.Ppcmd_comment 115 | end 116 | --]] 117 | 118 | ---@class vsrocq.Goal 119 | ---@field id integer 120 | ---@field goal vsrocq.PpString 121 | ---@field hypotheses (vsrocq.PpString)[] 122 | 123 | ---@class vsrocq.ProofViewGoals 124 | ---@field goals vsrocq.Goal[] 125 | ---@field shelvedGoals vsrocq.Goal[] 126 | ---@field givenUpGoals vsrocq.Goal[] 127 | ---@field unfocusedGoals vsrocq.Goal[] 128 | 129 | ---@enum vsrocq.MessageSeverity 130 | ---|1 # error 131 | ---|2 # warning 132 | ---|3 # information 133 | ---|4 # hint 134 | 135 | ---@alias vsrocq.RocqMessage {[1]: vsrocq.MessageSeverity, [2]: vsrocq.PpString} 136 | 137 | ---"vsrocq/proofView" notification (server → client) parameter. 138 | ---@class vsrocq.ProofViewNotification 139 | ---@field proof vsrocq.ProofViewGoals|vim.NIL|nil 140 | ---@field messages vsrocq.RocqMessage[] 141 | 142 | ---"vsrocq/moveCursor" notification (server → client) parameter. 143 | ---Sent as response to "vsrocq/stepForward" and "vsrocq/stepBack" notifications. 144 | ---@class vsrocq.MoveCursorNotification 145 | ---@field uri lsp.DocumentUri 146 | ---@field range lsp.Range 147 | 148 | -- # Query panel 149 | 150 | -- TODO: query response does not contain appropriate line breaks for window width (unlike rocqide) 151 | 152 | ---"vsrocq/search" request parameter. 153 | ---@class vsrocq.SearchRocqRequest 154 | ---@field id string this doesn't need to be an actual UUID 155 | ---@field textDocument lsp.VersionedTextDocumentIdentifier 156 | ---@field pattern string 157 | ---@field position lsp.Position 158 | 159 | ---"vsrocq/search" response parameter. 160 | ---@class vsrocq.SearchRocqHandshake 161 | ---@field id string 162 | 163 | ---"vsrocq/searchResult" notification parameter. 164 | ---@class vsrocq.SearchRocqResult 165 | ---@field id string 166 | ---@field name vsrocq.PpString 167 | ---@field statement vsrocq.PpString 168 | 169 | ---Request parameter for "vsrocq/about", "vsrocq/check", "vsrocq/print", "vsrocq/locate" 170 | ---@class vsrocq.SimpleRocqRequest 171 | ---@field textDocument lsp.VersionedTextDocumentIdentifier 172 | ---@field pattern string 173 | ---@field position lsp.Position 174 | 175 | ---Response parameter for "vsrocq/about", "vsrocq/check", "vsrocq/print", "vsrocq/locate" 176 | ---@alias vsrocq.SimpleRocqReponse vsrocq.PpString 177 | 178 | ---Request parameter for "vsrocq/resetRocq" 179 | ---@class vsrocq.ResetRocqRequest 180 | ---@field textDocument lsp.VersionedTextDocumentIdentifier 181 | -------------------------------------------------------------------------------- /lua/vsrocq/pp.lua: -------------------------------------------------------------------------------- 1 | -- Pretty printing PpString, based on Oppen's algorithm. 2 | -- (http://i.stanford.edu/pub/cstr/reports/cs/tr/79/770/CS-TR-79-770.pdf) 3 | -- 1. Compute the size of each token. 4 | -- - string: length of string 5 | -- - block open: the distance to the matching close 6 | -- - break: distance to the next break or block close 7 | -- - block close: zero 8 | -- - In the paper, this is called Scan(). We don't implement the optimized 9 | -- algorithm since the full PpString is already in memory. 10 | -- 2. Print 11 | -- - string: print 12 | -- - block open: check breaking mode, ... push stack 13 | -- - block close: pop satck 14 | -- - break: break or space 15 | -- 16 | -- See also 17 | -- * https://ocaml.org/manual/5.2/api/Format_tutorial.html 18 | -- * vsrocq's implementation: https://github.com/rocq-prover/vsrocq/pull/900 19 | -- * related: https://www.reddit.com/r/ProgrammingLanguages/comments/vzp7td/pretty_printing_which_paper/ 20 | 21 | local TaggedLines = require('vsrocq.tagged_lines') 22 | 23 | ---@alias Enter 0 24 | local Enter = 0 25 | ---@alias Leave 1 26 | local Leave = 1 27 | 28 | -- Iterative traversal of PpString 29 | ---@param pp_root vsrocq.PpString 30 | ---@return fun(): Enter|Leave?, vsrocq.PpString 31 | local function PpString_iter(pp_root) 32 | ---@type {pp: vsrocq.PpString, i?: integer}[] 33 | local stack = { { pp = pp_root } } 34 | 35 | local function iter() 36 | while #stack > 0 do 37 | local frame = stack[#stack] 38 | local pp = frame.pp 39 | if not frame.i then 40 | frame.i = 1 41 | return Enter, pp 42 | end 43 | 44 | local child ---@type vsrocq.PpString? 45 | if pp[1] == 'Ppcmd_glue' then 46 | ---@cast pp vsrocq.PpString.Ppcmd_glue 47 | child = pp[2][frame.i] 48 | elseif pp[1] == 'Ppcmd_box' then 49 | ---@cast pp vsrocq.PpString.Ppcmd_box 50 | if frame.i == 1 then 51 | child = pp[3] 52 | end 53 | elseif pp[1] == 'Ppcmd_tag' then 54 | ---@cast pp vsrocq.PpString.Ppcmd_tag 55 | if frame.i == 1 then 56 | child = pp[3] 57 | end 58 | end 59 | if not child then 60 | table.remove(stack) 61 | return Leave, pp 62 | end 63 | 64 | frame.i = frame.i + 1 65 | table.insert(stack, { pp = child }) 66 | end 67 | 68 | return nil 69 | end 70 | 71 | return iter 72 | end 73 | 74 | -- TODO: use window width. should take account of columns 75 | local LINE_SIZE = 80 76 | 77 | ---Populates the `size` field in each PpString. 78 | ---The defintion of size follows the Oppen's algorithm. 79 | ---@param pp_root vsrocq.PpString 80 | local function PpString_compute_sizes(pp_root) 81 | -- first pass: size of tokens other than break. 82 | -- Initially, the size of break is set to the number of spaces. 83 | -- This gives the "righttotal" stuff in Oppen's algorithm. 84 | for cmd, pp in PpString_iter(pp_root) do 85 | if cmd == Leave then 86 | if pp[1] == 'Ppcmd_empty' then 87 | ---@cast pp vsrocq.PpString.Ppcmd_empty 88 | pp.size = 0 89 | elseif pp[1] == 'Ppcmd_string' then 90 | ---@cast pp vsrocq.PpString.Ppcmd_string 91 | pp.size = vim.fn.strdisplaywidth(pp[2]) 92 | elseif pp[1] == 'Ppcmd_glue' then 93 | ---@cast pp vsrocq.PpString.Ppcmd_glue 94 | pp.size = 0 95 | for _, child in ipairs(pp[2]) do 96 | pp.size = pp.size + child.size 97 | end 98 | elseif pp[1] == 'Ppcmd_box' then 99 | ---@cast pp vsrocq.PpString.Ppcmd_box 100 | pp.size = pp[3].size 101 | elseif pp[1] == 'Ppcmd_tag' then 102 | ---@cast pp vsrocq.PpString.Ppcmd_tag 103 | pp.size = pp[3].size 104 | elseif pp[1] == 'Ppcmd_print_break' then 105 | ---@cast pp vsrocq.PpString.Ppcmd_print_break 106 | pp.size = pp[2] 107 | elseif pp[1] == 'Ppcmd_force_newline' then 108 | ---@cast pp vsrocq.PpString.Ppcmd_force_newline 109 | pp.size = LINE_SIZE 110 | elseif pp[1] == 'Ppcmd_comment' then 111 | ---@cast pp vsrocq.PpString.Ppcmd_comment 112 | pp.size = 0 113 | end 114 | end 115 | end 116 | 117 | -- second pass: size of breaks, i.e., distance to the next break/close 118 | for cmd, pp in PpString_iter(pp_root) do 119 | if cmd == Leave and pp[1] == 'Ppcmd_glue' then 120 | ---@cast pp vsrocq.PpString.Ppcmd_glue 121 | local last_break ---@type vsrocq.PpString.Ppcmd_print_break? 122 | for _, child in ipairs(pp[2]) do 123 | if child[1] == 'Ppcmd_print_break' then 124 | ---@cast child vsrocq.PpString.Ppcmd_print_break 125 | last_break = child 126 | last_break.size = 0 127 | elseif last_break then 128 | last_break.size = last_break.size + child.size 129 | end 130 | end 131 | end 132 | end 133 | end 134 | 135 | ---@class vsrocq.Tag 136 | ---@field [1] integer 0-indexed offset of the start line 137 | ---@field [2] integer start col 138 | ---@field [3] integer end line offset 139 | ---@field [4] integer end col 140 | ---@field [0] string 141 | 142 | ---@param pp_root vsrocq.PpString 143 | ---@return vsrocq.TaggedLines 144 | local function PpString(pp_root) 145 | if not pp_root.size then 146 | PpString_compute_sizes(pp_root) 147 | end 148 | 149 | local lines = {} ---@type string[] 150 | local cur_line = {} ---@type string[] 151 | local tags = {} ---@type vsrocq.Tag[] 152 | local cursor = 0 ---@type integer the 0-indexed position (strdisplaywidth) of the next output 153 | local cursor_byte = 0 --- like `cursor`, but with byte length 154 | ---@type {indent: integer, mode: 0|1|2}[] 0: no break. 1: break as needed. 2: break all 155 | local box_stack = {} 156 | ---@type vsrocq.Tag[] 157 | local tag_stack = {} 158 | 159 | local function output(str, size) 160 | cur_line[#cur_line + 1] = str 161 | cursor = cursor + size 162 | cursor_byte = cursor_byte + #str 163 | end 164 | 165 | for cmd, pp in PpString_iter(pp_root) do 166 | if cmd == Enter then 167 | if pp[1] == 'Ppcmd_string' then 168 | ---@cast pp vsrocq.PpString.Ppcmd_string 169 | for i, s in ipairs(vim.split(pp[2], '\n')) do 170 | -- handle multi-line string. no indent. 171 | if i > 1 then 172 | cursor, cursor_byte = 0, 0 173 | lines[#lines + 1] = table.concat(cur_line) 174 | cur_line = {} 175 | end 176 | output(s, vim.fn.strdisplaywidth(s)) 177 | end 178 | elseif pp[1] == 'Ppcmd_box' then 179 | ---@cast pp vsrocq.PpString.Ppcmd_box 180 | local mode 181 | if pp[2][1] == 'Pp_hbox' then 182 | mode = 0 183 | elseif pp[2][1] == 'Pp_vbox' then 184 | mode = 2 185 | elseif pp[2][1] == 'Pp_hvbox' then 186 | mode = cursor + pp.size > LINE_SIZE and 2 or 0 187 | elseif pp[2][1] == 'Pp_hovbox' then 188 | mode = 1 189 | end 190 | table.insert(box_stack, { indent = cursor + (pp[2][2] or 0), mode = mode }) 191 | elseif pp[1] == 'Ppcmd_tag' then 192 | ---@cast pp vsrocq.PpString.Ppcmd_tag 193 | table.insert(tag_stack, { #lines, cursor_byte, [0] = pp[2] }) 194 | elseif pp[1] == 'Ppcmd_print_break' then 195 | ---@cast pp vsrocq.PpString.Ppcmd_print_break 196 | -- NOTE: RocqMessage contains breaks without enclosing box. 197 | -- This behaves like regular text wrapping. 198 | local top = #box_stack > 0 and box_stack[#box_stack] or { mode = 1, indent = 0 } 199 | if top.mode > 0 and (cursor + pp.size > LINE_SIZE or top.mode == 2) then 200 | cursor = top.indent + pp[3] 201 | cursor_byte = cursor 202 | lines[#lines + 1] = table.concat(cur_line) 203 | cur_line = { string.rep(' ', cursor) } 204 | else 205 | output(string.rep(' ', pp[2]), pp[2]) 206 | end 207 | elseif pp[1] == 'Ppcmd_force_newline' then 208 | ---@cast pp vsrocq.PpString.Ppcmd_force_newline 209 | local top = #box_stack > 0 and box_stack[#box_stack] or { mode = 1, indent = 0 } 210 | cursor = top.indent 211 | cursor_byte = cursor 212 | lines[#lines + 1] = table.concat(cur_line) 213 | cur_line = { string.rep(' ', cursor) } 214 | end 215 | else 216 | if pp[1] == 'Ppcmd_box' then 217 | table.remove(box_stack) 218 | elseif pp[1] == 'Ppcmd_tag' then 219 | local tag = table.remove(tag_stack) ---@type vsrocq.Tag 220 | tag[3] = #lines 221 | tag[4] = cursor_byte 222 | table.insert(tags, tag) 223 | end 224 | end 225 | end 226 | 227 | if #cur_line > 0 then 228 | lines[#lines + 1] = table.concat(cur_line) 229 | end 230 | 231 | return TaggedLines.new(lines, tags) 232 | end 233 | 234 | return PpString 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vsrocq.nvim 2 | A Neovim client for [VsRocq `vsroqtop`](https://github.com/rocq-prover/vsrocq). 3 | 4 | ## Prerequisites 5 | * [Latest stable version of Neovim](https://github.com/neovim/neovim/releases/tag/stable) 6 | * [`vsrocqtop`](https://github.com/rocq-prover/vsrocq#installing-the-language-server) 7 | 8 | ## Setup 9 | ### [vim-plug](https://github.com/junegunn/vim-plug) 10 | ```vim 11 | Plug 'whonore/Coqtail' " for ftdetect, syntax, basic ftplugin, etc 12 | Plug 'tomtomjhj/vsrocq.nvim' 13 | 14 | ... 15 | 16 | " Don't load Coqtail 17 | let g:loaded_coqtail = 1 18 | let g:coqtail#supported = 0 19 | 20 | " Setup vsrocq.nvim 21 | lua require'vsrocq'.setup() 22 | ``` 23 | 24 | ### [lazy.nvim](https://github.com/folke/lazy.nvim) 25 | ```lua 26 | { 27 | 'whonore/Coqtail', 28 | init = function() 29 | vim.g.loaded_coqtail = 1 30 | vim.g["coqtail#supported"] = 0 31 | end, 32 | }, 33 | { 34 | 'tomtomjhj/vsrocq.nvim', 35 | filetypes = 'coq', 36 | dependecies = { 37 | 'whonore/Coqtail', 38 | }, 39 | opts = { 40 | vsrocq = { ... } 41 | lsp = { ... } 42 | }, 43 | }, 44 | ``` 45 | 46 | ## Interface 47 | * vsrocq.nvim uses Neovim's built-in LSP client and nvim-lspconfig. 48 | See [kickstart.nvim](https://github.com/nvim-lua/kickstart.nvim/) 49 | for basic example configurations for working with LSP. 50 | * `:VsRocq` command 51 | * `:VsRocq continuous`: Use the "Continuous" proof mode. It shows goals for the cursor position. 52 | * `:VsRocq manual`: Use the "Manual" proof mode (default), where the following four commands are used for navigation. 53 | * `:VsRocq stepForward` 54 | * `:VsRocq stepBackward` 55 | * `:VsRocq interpretToEnd` 56 | * `:VsRocq interpretToPoint` 57 | * `:VsRocq panels`: Open the proofview panel and query panel. 58 | * Queries 59 | * `:VsRocq search {pattern}` 60 | * `:VsRocq about {pattern}` 61 | * `:VsRocq check {pattern}` 62 | * `:VsRocq print {pattern}` 63 | * `:VsRocq locate {pattern}` 64 | * Proofview 65 | * `:VsRocq admitted`: Show the admitted goals. 66 | * `:VsRocq shelved`: Show the shelved goals. 67 | * `:VsRocq goals`: Show the normal goals and messages (default). 68 | * etc 69 | * `:VsRocq jumpToEnd`: Jump to the end of the checked region. 70 | 71 | ## Configurations 72 | The `setup()` function takes a table with the followings keys: 73 | * `vsrocq`: Settings specific to VsRocq, used in both the client and the server. 74 | This corresponds to the `"configuration"` key in VsRocq's [package.json][]. 75 | * `lsp`: The settings forwarded to `:help lspconfig-setup`. `:help vim.lsp.ClientConfig`. 76 | 77 | ### Basic LSP configuration 78 | 79 | Some settings in VsRocq's [package.json][] should be configured in nvim's LSP client configuration: 80 | * `"vsrocq.path"` and `"vsrocq.args"` → `lsp.cmd` 81 | * `"vsrocq.trace.server"` → `lsp.trace` 82 | 83 | | Key | Type | Default value | Description | 84 | | ------------------ | ---------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | 85 | | `lsp.cmd` | `string[]` | `{ "vsrocqtop" }` | Path to `vsrocqtop` (e.g. `path/to/vsrocq/bin/vsrocqtop`) and arguments passed | 86 | | `lsp.trace` | `"off" \| "messages" \| "verbose"` | `"off"` | Toggles the tracing of communications between the server and client | 87 | 88 | ### Memory management (since >= vsrocq 2.1.7) 89 | 90 | | Key | Type | Default value | Description | 91 | | -------------------- | ----- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | 92 | | `vsrocq.memory.limit` | `int` | 4 | specifies the memory limit (in Gb) over which when a user closes a tab, the corresponding document state is discarded in the server to free up memory | 93 | 94 | ### Goal and info view panel 95 | 96 | | Key | Type | Default value | Description | 97 | | --------------------------- | ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------- | 98 | | `vsrocq.goals.diff.mode` | `"on" \| "off" \| "removed"` | `"off"` | Toggles diff mode. If set to `removed`, only removed characters are shown | 99 | | `vsrocq.goals.messages.full` | `bool` | `false` | A toggle to include warnings and errors in the proof view | 100 | | `vsrocq.goals.maxDepth` | `int` | `17` | A setting to determine at which point the goal display starts eliding (since version >= 2.1.7 of `vsrocqtop`) | 101 | 102 | ### Proof checking 103 | | Key | Type | Default value | Description | 104 | | ------------------------------------- | -------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 105 | | `vsrocq.proof.mode` | `"Continuous" \| "Manual"` | `"Manual"` | Decide whether documents should checked continuously or using the classic navigation commmands (defaults to `Manual`) | 106 | | `vsrocq.proof.pointInterpretationMode` | `"Cursor" \| "NextCommand"` | `"Cursor"` | Determines the point to which the proof should be check to when using the 'Interpret to point' command | 107 | | `vsrocq.proof.cursor.sticky` | `bool` | `true` | A toggle to specify whether the cursor should move as Rocq interactively navigates a document (step forward, backward, etc...) | 108 | | `vsrocq.proof.delegation` | `"None" \| "Skip" \| "Delegate"` | `"None"` | Decides which delegation strategy should be used by the server. `Skip` allows to skip proofs which are out of focus and should be used in manual mode. `Delegate` allocates a settable amount of workers to delegate proofs | 109 | | `vsrocq.proof.workers` | `int` | `1` | Determines how many workers should be used for proof checking | 110 | | `vsrocq.proof.block` | `bool` | `true` | Determines if the the execution of a document should halt on first error (since version >= 2.1.7 of `vsrocqtop`) | 111 | 112 | ### Code completion (experimental) 113 | | Key | Type | Default value | Description | 114 | | ----------------------------------- | --------------------------------------------------------- | ------------------------ | ------------------------------------------------------------- | 115 | | `vsrocq.completion.enable` | `bool` | `false` | Toggle code completion | 116 | | `vsrocq.completion.algorithm` | `"StructuredSplitUnification" \| "SplitTypeIntersection"` | `"SplitTypeIntersection"`| Which completion algorithm to use | 117 | | `vsrocq.completion.unificationLimit` | `int` (minimum 0) | `100` | Sets the limit for how many theorems unification is attempted | 118 | 119 | ### Diagnostics 120 | | Key | Type | Default value | Description | 121 | | ------------------------ | ------ | ------------- | ------------------------------------------------ | 122 | | `vsrocq.diagnostics.full` | `bool` | `false` | Toggles the printing of `Info` level diagnostics | 123 | 124 | ### Example: 125 | ```lua 126 | require'vsrocq'.setup { 127 | vsrocq = { 128 | proof = { 129 | -- In manual mode, don't move the cursor when stepping forward/backward a command 130 | cursor = { sticky = false }, 131 | }, 132 | }, 133 | lsp = { 134 | on_attach = function(client, bufnr) 135 | -- your mappings, etc 136 | 137 | -- In manual mode, use ctrl-alt-{j,k,l} to step. 138 | vim.keymap.set({ 'n', 'i' }, '', 'VsRocq stepForward', { buffer = bufnr, desc='VsRocq step forward' }) 139 | vim.keymap.set({ 'n', 'i' }, '', 'VsRocq stepBackward', { buffer = bufnr, desc='VsRocq step backward' }) 140 | vim.keymap.set({ 'n', 'i' }, '', 'VsRocq interpretToPoint', { buffer = bufnr, desc='VsRocq interpret to point' }) 141 | vim.keymap.set({ 'n', 'i' }, '', 'VsRocq interpretToEnd', { buffer = bufnr, desc = 'VsRocq interpret to end' }) 142 | end, 143 | -- autostart = false, -- use this if you want to manually `:LspStart vscoqtop`. 144 | -- cmd = { 'vsrocqtop', '-bt', '-vsrocq-d', 'all' }, -- for debugging the server 145 | }, 146 | } 147 | ``` 148 | 149 | NOTE: 150 | Do not call `lspconfig.vscoqtop.setup()` yourself. 151 | `require'vsrocq'.setup` does it for you. 152 | 153 | ## Features not implemented yet 154 | * Fancy proofview rendering 155 | * proof diff highlights 156 | * Make lspconfig optional 157 | 158 | ## See also 159 | * [coq.ctags](https://github.com/tomtomjhj/coq.ctags) for go-to-definition. 160 | * [coq-lsp.nvim](https://github.com/tomtomjhj/coq-lsp.nvim) for `coq-lsp` client. 161 | 162 | [package.json]: https://github.com/rocq-prover/vsrocq/blob/main/client/package.json 163 | -------------------------------------------------------------------------------- /lua/vsrocq/client.lua: -------------------------------------------------------------------------------- 1 | local util = require('vsrocq.util') 2 | local pp = require('vsrocq.pp') 3 | local render = require('vsrocq.render') 4 | 5 | ---@class VSRocqNvim 6 | ---@field lc vim.lsp.Client 7 | ---@field config vsrocq.Config the current configuration 8 | -- TODO: Since proofView notification doesn't send which document it is for, 9 | -- for now we have a single proofview panel. 10 | -- Once fixed, make config for single/multi proofview. 11 | -- ---@field buffers table 12 | ---@field buffers table 13 | ---@field proofview_panel buffer 14 | ---@field proofview_content? vsrocq.ProofViewNotification 15 | ---@field query_panel buffer 16 | ---@field query_id integer latest query id. Only the latest query result is displayed. 17 | ---@field debounce_timer uv.uv_timer_t 18 | ---@field highlight_ns integer 19 | ---@field tag_ns integer 20 | ---@field ag integer 21 | local VSRocqNvim = {} 22 | VSRocqNvim.__index = VSRocqNvim 23 | 24 | ---@type string[] command names 25 | local commands = {} 26 | 27 | ---@param client vim.lsp.Client 28 | ---@param config vsrocq.Config 29 | ---@return VSRocqNvim 30 | function VSRocqNvim:new(client, config) 31 | local new = { 32 | lc = client, 33 | config = vim.deepcopy(config), 34 | buffers = {}, 35 | proofview_panel = -1, 36 | query_panel = -1, 37 | query_id = 0, 38 | debounce_timer = assert(vim.uv.new_timer(), 'Could not create timer'), 39 | highlight_ns = vim.api.nvim_create_namespace('vsrocq-progress-' .. client.id), 40 | tag_ns = vim.api.nvim_create_namespace('vsrocq-tag-' .. client.id), 41 | ag = vim.api.nvim_create_augroup('vsrocq-' .. client.id, { clear = true }), 42 | } 43 | setmetatable(new, self) 44 | new:ensure_proofview_panel() 45 | new:ensure_query_panel() 46 | return new 47 | end 48 | 49 | ---change config and send notification 50 | function VSRocqNvim:update_lsp_config() 51 | self.lc:notify('workspace/didChangeConfiguration', { settings = self.config:to_lsp_options() }) 52 | end 53 | 54 | function VSRocqNvim:manual() 55 | self.config.proof.mode = 'Manual' 56 | self:update_lsp_config() 57 | end 58 | function VSRocqNvim:continuous() 59 | self.config.proof.mode = 'Continuous' 60 | self:update_lsp_config() 61 | end 62 | commands[#commands + 1] = 'manual' 63 | commands[#commands + 1] = 'continuous' 64 | 65 | ---@param highlights vsrocq.UpdateHighlightsNotification 66 | function VSRocqNvim:updateHighlights(highlights) 67 | local bufnr = vim.uri_to_bufnr(highlights.uri) 68 | vim.api.nvim_buf_clear_namespace(bufnr, self.highlight_ns, 0, -1) 69 | self.buffers[bufnr].highlights = highlights 70 | -- for _, range in ipairs(highlights.processingRange) do 71 | for _, range in ipairs(highlights.processedRange) do 72 | vim.hl.range( 73 | bufnr, 74 | self.highlight_ns, 75 | 'CoqtailChecked', 76 | util.position_lsp_to_api(bufnr, range['start'], self.lc.offset_encoding), 77 | util.position_lsp_to_api(bufnr, range['end'], self.lc.offset_encoding), 78 | { priority = vim.hl.priorities.user + 1 } 79 | ) 80 | end 81 | end 82 | 83 | function VSRocqNvim:jumpToEnd() 84 | local win = vim.api.nvim_get_current_win() 85 | local bufnr = vim.api.nvim_win_get_buf(win) 86 | if not self.buffers[bufnr] then 87 | return 88 | end 89 | 90 | local max_end 91 | for _, range in ipairs(self.buffers[bufnr].highlights.processedRange) do 92 | local end_ = util.position_lsp_to_api(bufnr, range['end'], self.lc.offset_encoding) 93 | if max_end == nil or util.api_position_lt(max_end, end_) then 94 | max_end = end_ 95 | end 96 | end 97 | if max_end then 98 | vim.api.nvim_win_set_cursor(win, util.position_api_to_mark(max_end)) 99 | end 100 | end 101 | 102 | commands[#commands + 1] = 'jumpToEnd' 103 | 104 | ---@param target vsrocq.MoveCursorNotification 105 | function VSRocqNvim:moveCursor(target) 106 | local bufnr = vim.uri_to_bufnr(target.uri) 107 | local wins = vim.fn.win_findbuf(bufnr) or {} 108 | if self.config.proof.mode == 'Manual' and self.config.proof.cursor.sticky then 109 | local position = util.position_api_to_mark( 110 | util.position_lsp_to_api(bufnr, target.range['end'], self.lc.offset_encoding) 111 | ) 112 | for _, win in ipairs(wins) do 113 | vim.api.nvim_win_set_cursor(win, position) 114 | end 115 | end 116 | end 117 | 118 | ---@param proofView vsrocq.ProofViewNotification 119 | function VSRocqNvim:proofView(proofView) 120 | self.proofview_content = proofView 121 | self:show_proofView { 'goals', 'messages' } 122 | end 123 | 124 | ---@param items ('goals'|'messages'|'shelvedGoals'|'givenUpGoals')[] 125 | function VSRocqNvim:show_proofView(items) 126 | assert(self.proofview_content) 127 | 128 | self:ensure_proofview_panel() 129 | 130 | -- TODO: smarter view? relative position? always focus on the first goal? 131 | local wins = {} ---@type table 132 | for _, win in ipairs(vim.fn.win_findbuf(self.proofview_panel) or {}) do 133 | vim.api.nvim_win_call(win, function() 134 | wins[win] = vim.fn.winsaveview() 135 | end) 136 | end 137 | 138 | local tl = render.proofView(self.proofview_content, items) 139 | vim.api.nvim_buf_set_lines(self.proofview_panel, 0, -1, false, tl[1]) 140 | 141 | self:ensure_query_panel() 142 | if #self.proofview_content.messages > 0 then 143 | local msg_tl = render.RocqMessages(self.proofview_content.messages) 144 | vim.bo[self.query_panel].undolevels = vim.bo[self.query_panel].undolevels 145 | vim.api.nvim_buf_set_lines(self.query_panel, 0, -1, false, msg_tl[1]) 146 | for _, tag in ipairs(msg_tl[2]) do 147 | vim.api.nvim_buf_set_extmark(self.query_panel, self.tag_ns, tag[1], tag[2], { 148 | end_row = tag[3], 149 | end_col = tag[4], 150 | hl_group = tag[0], 151 | }) 152 | end 153 | elseif 154 | not ( 155 | vim.api.nvim_buf_line_count(self.query_panel) == 1 156 | and vim.api.nvim_buf_get_lines(self.query_panel, 0, -1, false)[1] == '' 157 | ) 158 | then 159 | vim.api.nvim_buf_clear_namespace(self.query_panel, self.tag_ns, 0, -1) 160 | vim.bo[self.query_panel].undolevels = vim.bo[self.query_panel].undolevels 161 | vim.api.nvim_buf_set_lines(self.query_panel, 0, -1, false, {}) 162 | end 163 | 164 | for win, view in pairs(wins) do 165 | vim.api.nvim_win_call(win, function() 166 | vim.fn.winrestview(view) 167 | end) 168 | end 169 | end 170 | 171 | function VSRocqNvim:shelved() 172 | if self.proofview_content then 173 | self:show_proofView { 'shelvedGoals' } 174 | end 175 | end 176 | function VSRocqNvim:admitted() 177 | if self.proofview_content then 178 | self:show_proofView { 'givenUpGoals' } 179 | end 180 | end 181 | function VSRocqNvim:goals() 182 | if self.proofview_content then 183 | self:show_proofView { 'goals', 'messages' } 184 | end 185 | end 186 | commands[#commands + 1] = 'shelved' 187 | commands[#commands + 1] = 'admitted' 188 | commands[#commands + 1] = 'goals' 189 | 190 | -- TODO: commands in panels 191 | function VSRocqNvim:ensure_proofview_panel() 192 | if vim.api.nvim_buf_is_valid(self.proofview_panel) then 193 | if not vim.api.nvim_buf_is_loaded(self.proofview_panel) then 194 | vim.fn.bufload(self.proofview_panel) 195 | end 196 | return 197 | end 198 | self.proofview_panel = vim.api.nvim_create_buf(false, true) 199 | vim.bo[self.proofview_panel].filetype = 'coq-goals' 200 | end 201 | 202 | function VSRocqNvim:ensure_query_panel() 203 | if vim.api.nvim_buf_is_valid(self.query_panel) then 204 | if not vim.api.nvim_buf_is_loaded(self.query_panel) then 205 | vim.fn.bufload(self.query_panel) 206 | end 207 | return 208 | end 209 | self.query_panel = vim.api.nvim_create_buf(false, true) 210 | vim.bo[self.query_panel].filetype = 'coq-infos' 211 | end 212 | 213 | function VSRocqNvim:panels() 214 | self:ensure_proofview_panel() 215 | self:ensure_query_panel() 216 | local win = vim.api.nvim_get_current_win() 217 | 218 | if vim.fn.bufwinid(self.proofview_panel) == -1 then 219 | vim.cmd.sbuffer { 220 | args = { self.proofview_panel }, 221 | -- See `:h nvim_parse_cmd`. Note that the "split size" is `range`. 222 | mods = { keepjumps = true, keepalt = true, vertical = true, split = 'belowright' }, 223 | } 224 | vim.cmd.clearjumps() 225 | end 226 | 227 | if vim.fn.bufwinid(self.query_panel) == -1 then 228 | vim.api.nvim_set_current_win(assert(vim.fn.bufwinid(self.proofview_panel))) 229 | vim.cmd.sbuffer { 230 | args = { self.query_panel }, 231 | mods = { keepjumps = true, keepalt = true, split = 'belowright' }, 232 | } 233 | vim.cmd.clearjumps() 234 | end 235 | 236 | vim.api.nvim_set_current_win(win) 237 | end 238 | 239 | commands[#commands + 1] = 'panels' 240 | 241 | ---@param bufnr? buffer 242 | ---@param position? MarkPosition 243 | function VSRocqNvim:interpretToPoint(bufnr, position) 244 | bufnr = bufnr or vim.api.nvim_get_current_buf() 245 | position = position or util.guess_position(bufnr) 246 | local params = { 247 | textDocument = util.make_versioned_text_document_params(bufnr), 248 | position = util.make_position_params(bufnr, position, self.lc.offset_encoding), 249 | } 250 | return self.lc:notify('prover/interpretToPoint', params) 251 | end 252 | commands[#commands + 1] = 'interpretToPoint' 253 | 254 | ---@param method "vsrocq/stepForward"|"vsrocq/stepBackward"|"vsrocq/interpretToEnd" 255 | ---@param bufnr? buffer 256 | function VSRocqNvim:step(method, bufnr) 257 | bufnr = bufnr or vim.api.nvim_get_current_buf() 258 | local params = { 259 | textDocument = util.make_versioned_text_document_params(bufnr), 260 | } 261 | return self.lc:notify(method, params) 262 | end 263 | 264 | function VSRocqNvim:stepForward() 265 | return self:step('prover/stepForward') 266 | end 267 | function VSRocqNvim:stepBackward() 268 | return self:step('prover/stepBackward') 269 | end 270 | function VSRocqNvim:interpretToEnd() 271 | return self:step('prover/interpretToEnd') 272 | end 273 | commands[#commands + 1] = 'stepForward' 274 | commands[#commands + 1] = 'stepBackward' 275 | commands[#commands + 1] = 'interpretToEnd' 276 | 277 | ---@param pattern string 278 | ---@param bufnr? buffer 279 | ---@param position? MarkPosition 280 | function VSRocqNvim:search(pattern, bufnr, position) 281 | bufnr = bufnr or vim.api.nvim_get_current_buf() 282 | position = position or util.guess_position(bufnr) 283 | self.query_id = self.query_id + 1 284 | ---@type vsrocq.SearchRocqRequest 285 | local params = { 286 | id = tostring(self.query_id), 287 | textDocument = util.make_versioned_text_document_params(bufnr), 288 | position = util.make_position_params(bufnr, position, self.lc.offset_encoding), 289 | pattern = pattern, 290 | } 291 | util.request_async(self.lc, bufnr, 'prover/search', params, function(err) 292 | if err then 293 | vim.notify( 294 | ('[vsrocq.nvim] vsrocq/search error:\nparam:\n%s\nerror:%s\n'):format( 295 | vim.inspect(params), 296 | vim.inspect(err) 297 | ), 298 | vim.log.levels.ERROR 299 | ) 300 | return 301 | end 302 | self:ensure_query_panel() 303 | -- :h undo-break 304 | vim.bo[self.query_panel].undolevels = vim.bo[self.query_panel].undolevels 305 | vim.api.nvim_buf_set_lines(self.query_panel, 0, -1, false, {}) 306 | end) 307 | end 308 | commands[#commands + 1] = 'search' 309 | 310 | ---@param result vsrocq.SearchRocqResult 311 | function VSRocqNvim:searchResult(result) 312 | if tonumber(result.id) < self.query_id then 313 | return 314 | end 315 | self:ensure_query_panel() 316 | -- NOTE: Each notification sends a single item. 317 | -- Because of that, the panel should maintain TaggedLines to which the new items are appended. 318 | -- But it turns out there is no reason for search to be implemented that way. 319 | -- So let's not care about it and wait for the fix. 320 | -- https://github.com/rocq-prover/vsrocq/issues/906#issuecomment-2353000748 321 | local tl = render.searchRocqResult(result) 322 | vim.api.nvim_buf_set_lines(self.query_panel, -1, -1, false, tl[1]) 323 | end 324 | 325 | ---@param method "vsrocq/about"|"vsrocq/check"|"vsrocq/print"|"vsrocq/locate" 326 | ---@param pattern string 327 | ---@param bufnr? buffer 328 | ---@param position? MarkPosition 329 | function VSRocqNvim:simple_query(method, pattern, bufnr, position) 330 | bufnr = bufnr or vim.api.nvim_get_current_buf() 331 | position = position or util.guess_position(bufnr) 332 | self.query_id = self.query_id + 1 333 | ---@type vsrocq.SimpleRocqRequest 334 | local params = { 335 | textDocument = util.make_versioned_text_document_params(bufnr), 336 | position = util.make_position_params(bufnr, position, self.lc.offset_encoding), 337 | pattern = pattern, 338 | } 339 | util.request_async( 340 | self.lc, 341 | bufnr, 342 | method, 343 | params, 344 | ---@param result vsrocq.PpString 345 | function(err, result) 346 | if err then 347 | vim.notify( 348 | ('[vsrocq.nvim] %s error:\nparam:\n%s\nerror:%s\n'):format( 349 | method, 350 | vim.inspect(params), 351 | vim.inspect(err) 352 | ), 353 | vim.log.levels.ERROR 354 | ) 355 | return 356 | end 357 | self:ensure_query_panel() 358 | -- :h undo-break 359 | vim.bo[self.query_panel].undolevels = vim.bo[self.query_panel].undolevels 360 | local tl = pp(result) 361 | vim.api.nvim_buf_set_lines(self.query_panel, 0, -1, false, tl[1]) 362 | end 363 | ) 364 | end 365 | 366 | function VSRocqNvim:about(pattern) 367 | self:simple_query('prover/about', pattern) 368 | end 369 | function VSRocqNvim:check(pattern) 370 | self:simple_query('prover/check', pattern) 371 | end 372 | function VSRocqNvim:print(pattern) 373 | self:simple_query('prover/print', pattern) 374 | end 375 | function VSRocqNvim:locate(pattern) 376 | self:simple_query('prover/locate', pattern) 377 | end 378 | commands[#commands + 1] = 'about' 379 | commands[#commands + 1] = 'check' 380 | commands[#commands + 1] = 'print' 381 | commands[#commands + 1] = 'locate' 382 | 383 | ---@param bufnr? buffer 384 | function VSRocqNvim:resetRocq(bufnr) 385 | bufnr = bufnr or vim.api.nvim_get_current_buf() 386 | ---@type vsrocq.ResetRocqRequest 387 | local params = { 388 | textDocument = util.make_versioned_text_document_params(bufnr), 389 | } 390 | util.request_async(self.lc, bufnr, 'prover/resetRocq', params, function(err) 391 | if err then 392 | vim.notify( 393 | ('[vsrocq.nvim] resetRocq error:\nparam:\n%s\nerror:%s\n'):format( 394 | vim.inspect(params), 395 | vim.inspect(err) 396 | ), 397 | vim.log.levels.ERROR 398 | ) 399 | return 400 | end 401 | vim.api.nvim_buf_set_lines(self.proofview_panel, 0, -1, false, {}) 402 | end) 403 | end 404 | commands[#commands + 1] = 'resetRocq' 405 | 406 | function VSRocqNvim:on_CursorMoved() 407 | if self.config.proof.mode == 'Continuous' then 408 | -- TODO: debounce_timer 409 | assert(self:interpretToPoint()) 410 | end 411 | end 412 | 413 | ---@param bufnr buffer 414 | function VSRocqNvim:detach(bufnr) 415 | assert(self.buffers[bufnr]) 416 | vim.api.nvim_buf_clear_namespace(bufnr, self.highlight_ns, 0, -1) 417 | vim.api.nvim_clear_autocmds { group = self.ag, buffer = bufnr } 418 | vim.api.nvim_buf_del_user_command(bufnr, 'VsRocq') 419 | self.buffers[bufnr] = nil 420 | end 421 | 422 | ---@param bufnr buffer 423 | function VSRocqNvim:attach(bufnr) 424 | assert(self.buffers[bufnr] == nil) 425 | self.buffers[bufnr] = {} 426 | 427 | vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { 428 | group = self.ag, 429 | buffer = bufnr, 430 | callback = function() 431 | self:on_CursorMoved() 432 | end, 433 | }) 434 | vim.api.nvim_create_autocmd({ 'BufDelete', 'LspDetach' }, { 435 | group = self.ag, 436 | buffer = bufnr, 437 | desc = 'Unregister deleted/detached buffer', 438 | callback = function(ev) 439 | self:detach(ev.buf) 440 | end, 441 | }) 442 | 443 | vim.api.nvim_buf_create_user_command(bufnr, 'VsRocq', function(opts) 444 | self:command(opts.args) 445 | end, { 446 | bang = true, 447 | nargs = 1, 448 | complete = function(arglead, _, _) 449 | return vim.tbl_filter(function(command) 450 | return command:find(arglead) ~= nil 451 | end, commands) 452 | end, 453 | }) 454 | 455 | if self.config.proof.mode == 'Continuous' then 456 | self:interpretToPoint(bufnr) 457 | end 458 | end 459 | 460 | ---@param args string 461 | function VSRocqNvim:command(args) 462 | local _, to, subcommand = args:find('(%w+)%s*') 463 | if not vim.tbl_contains(commands, subcommand) then 464 | error(('"%s" is not a valid VsRocq command'):format(subcommand)) 465 | end 466 | args = args:sub(to + 1) 467 | -- TODO: check validity of args? maybe add some spec to commands 468 | VSRocqNvim[subcommand](self, #args > 0 and args or nil) 469 | end 470 | 471 | function VSRocqNvim:on_exit() 472 | self.debounce_timer:stop() 473 | self.debounce_timer:close() 474 | for bufnr, _ in pairs(self.buffers) do 475 | self:detach(bufnr) 476 | end 477 | vim.api.nvim_buf_delete(self.proofview_panel, { force = true }) 478 | vim.api.nvim_buf_delete(self.query_panel, { force = true }) 479 | vim.api.nvim_clear_autocmds { group = self.ag } 480 | end 481 | 482 | return VSRocqNvim 483 | --------------------------------------------------------------------------------