From 058c1268a2a6a42bc0724d14e362a2334ae768d2 Mon Sep 17 00:00:00 2001 From: Jannik Buhr <17450586+jmbuhr@users.noreply.github.com> Date: Sun, 18 Dec 2022 19:50:41 +0100 Subject: [PATCH] Feat lsp (#11) * configure debug function * add hidden buffers for language and autoclose them if the qmd is closed * handle multiple languages * delete language buffers on qmd close * add diagnostics * improve diagnostics autocommand * add hover functionality --- README.md | 60 ++++++++++---- examples/example.qmd | 16 +++- ftplugin/quarto.lua | 1 - lua/quarto/init.lua | 189 ++++++++++++++++++++++++++++--------------- plugin/quarto.lua | 10 +++ 5 files changed, 191 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 7224b80..d54236a 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,43 @@ vim.keymap.set('n', 'qp', quarto.quartoPreview, {silent = true, noremap Then use the keyboard shortcut to open `quarto preview` for the current file or project in the active working directory in the neovim integrated terminal in a new tab. -### Language support (WIP) +## Configure + +You can pass a lua table with options to the setup function. +It will be merged with the default options, which are shown below in the example +(i.e. if you are fine with the defaults you don't have to call the setup function). + +```lua +require'quarto'.setup{ + debug = false, + closePreviewOnExit = true, + lspFeatures = { + enabled = true, + languages = { 'r', 'python', 'julia' } + }, + keymap = { + hover = 'K', +} +``` + +## Language support (WIP) + +This might need quite a few resources, especially for multi-language docuemnts, +as it maintains hidden buffers for all the embedded languages in your quarto document (R, python and julia) and +talks to language servers attached to each. + +Configure quarto-nvim's lsp features by configuring it with + +```lua +require'quarto'.setup{ + lspFeatures = { + enabled = true, + languages = { 'r', 'python', 'julia' } + } +} +``` -Language support is very buggy for now, so it is not enabled by default. -Enable code diagnostics for embedded languages with +Or explicitly run ```vim QuartoDiagnostics @@ -55,20 +88,17 @@ or lua require'quarto'.enableDiagnostics ``` -## Configure +After enabling the language features, you can open the hover documentation +for R, python and julia code chunks with `K` (or configure a different shortcut). -You can pass a lua table with options to the setup function. -It will be merged with the default options, which are shown below in the example -(i.e. if you are fine with the defaults you don't have to call the setup function). +## Available Commnds -```lua -require'quarto'.setup{ - closePreviewOnExit = true, -- close preview terminal on closing of qmd file buffer - diagnostics = { - enabled = false, -- enable diagnostics for embedded languages - languages = {'r', 'python', 'julia'} - } -} +```vim +QuartoPreview +QuartoClosePreview +QuartoHelp ... +QuartoDiagnostics +QuartoHover ``` ## Recommended Plugins diff --git a/examples/example.qmd b/examples/example.qmd index e1db494..c2f436d 100644 --- a/examples/example.qmd +++ b/examples/example.qmd @@ -7,20 +7,28 @@ format: html This is some python code, in which we define a function `hello`: - + ```{python} def hello(): print("Hello") ``` -Now, we use the function in the next code chunk -to highlight the necessity of having all code -chunks in the same hidden document for the language server: +This is how we call it: ```{python} hello() ``` +And this function is not found because we have a typo: + +```{python} +helo() +``` + +Now, we use the function in the next code chunk +to highlight the necessity of having all code +chunks in the same hidden document for the language server: + Let's make this work! # Furthermore diff --git a/ftplugin/quarto.lua b/ftplugin/quarto.lua index ccb54ea..13548ad 100644 --- a/ftplugin/quarto.lua +++ b/ftplugin/quarto.lua @@ -1,5 +1,4 @@ -- set filetype to markdown for now, -- until we have our own e.g. treesitter grammar vim.bo.filetype = 'markdown' - vim.b.slime_cell_delimiter = "```" diff --git a/lua/quarto/init.lua b/lua/quarto/init.lua index 5dd29aa..bc60af2 100644 --- a/lua/quarto/init.lua +++ b/lua/quarto/init.lua @@ -3,18 +3,20 @@ local a = vim.api local q = vim.treesitter.query local util = require "lspconfig.util" - local defaultConfig = { + debug = false, closePreviewOnExit = true, lspFeatures = { enabled = false, languages = { 'r', 'python', 'julia' } + }, + keymap = { + hover = 'K', } } M.config = defaultConfig - local function contains(list, x) for _, v in pairs(list) do if v == x then return true end @@ -95,7 +97,7 @@ local function spaces(n) return s end -local function get_language_content(bufnr, language) +local function get_language_content(bufnr) -- get and parse AST local language_tree = vim.treesitter.get_parser(bufnr, 'markdown') local syntax_tree = language_tree:parse() @@ -103,92 +105,139 @@ local function get_language_content(bufnr, language) -- create capture local query = vim.treesitter.parse_query('markdown', - string.gsub([[ - (fenced_code_block + [[ + (fenced_code_block (info_string (language) @lang - (#eq? @lang $language) ) (code_fence_content) @code (#offset! @code) - ) - ]] , "%$(%w+)", { language = language }) + ) + ]] ) -- get text ranges local results = {} - for _, captures, metadata in query:iter_matches(root, bufnr) do - local text = q.get_node_text(captures[2], bufnr) - -- line numbers start at 0 - -- {start line, col, end line, col} - local result = { - range = metadata.content[1], - -- text = lines(text) - text = lines(text) - } - table.insert(results, result) + for pattern, match, metadata in query:iter_matches(root, bufnr) do + local lang + for id, node in pairs(match) do + local name = query.captures[id] + local text = q.get_node_text(node, 0) + if name == 'lang' then + lang = text + end + if name == 'code' then + local row1, col1, row2, col2 = node:range() -- range of the capture + local result = { + range = { from = { row1, col1 }, to = { row2, col2 } }, + lang = lang, + text = lines(text) + } + if results[lang] == nil then + results[lang] = {} + end + table.insert(results[lang], result) + end + end end return results end -local function update_language_buffer(qmd_bufnr, language) - local language_lines = get_language_content(qmd_bufnr, language) - if next(language_lines) == nil then - return - end +local function update_language_buffers(qmd_bufnr) + local language_content = get_language_content(qmd_bufnr) + local bufnrs = {} + for _, lang in ipairs(M.config.lspFeatures.languages) do + local language_lines = language_content[lang] + if language_lines ~= nil then + local postfix + if lang == 'python' then + postfix = '.py' + elseif lang == 'r' then + postfix = '.R' + elseif lang == 'julia' then + postfix = '.jl' + end - local nmax = language_lines[#language_lines].range[3] -- last code line - local qmd_path = a.nvim_buf_get_name(qmd_bufnr) - local postfix - if language == 'python' then - postfix = '.py' - elseif language == 'r' then - postfix = '.R' - end + local nmax = language_lines[#language_lines].range['to'][1] -- last code line + local qmd_path = a.nvim_buf_get_name(qmd_bufnr) + + -- create buffer filled with spaces + local bufname_lang = qmd_path .. '-tmp' .. postfix + local bufuri_lang = 'file://' .. bufname_lang + local bufnr_lang = vim.uri_to_bufnr(bufuri_lang) + table.insert(bufnrs, bufnr_lang) + a.nvim_buf_set_name(bufnr_lang, bufname_lang) + a.nvim_buf_set_option(bufnr_lang, 'filetype', lang) + a.nvim_buf_set_lines(bufnr_lang, 0, -1, false, {}) + a.nvim_buf_set_lines(bufnr_lang, 0, nmax, false, spaces(nmax)) - -- create buffer filled with spaces - local bufname_lang = qmd_path .. postfix - local bufuri_lang = 'file://' .. bufname_lang - local bufnr_lang = vim.uri_to_bufnr(bufuri_lang) - a.nvim_buf_set_name(bufnr_lang, bufname_lang) - a.nvim_buf_set_option(bufnr_lang, 'filetype', language) - a.nvim_buf_set_lines(bufnr_lang, 0, -1, false, {}) - a.nvim_buf_set_lines(bufnr_lang, 0, nmax, false, spaces(nmax)) - - -- write language lines - for _, t in ipairs(language_lines) do - a.nvim_buf_set_lines(bufnr_lang, t.range[1], t.range[3], false, t.text) + -- write language lines + for _, t in ipairs(language_lines) do + a.nvim_buf_set_lines(bufnr_lang, t.range['from'][1], t.range['to'][1], false, t.text) + end + end end - return bufnr_lang + return bufnrs end -local function enable_language_diagnostics(lang) - local augroup = a.nvim_create_augroup("quartoUpdate" .. lang, {}) - - a.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, { - -- buffer = qmd_buf, - pattern = '*.qmd', - group = augroup, - callback = function(args) - local ns = a.nvim_create_namespace('quarto' .. lang) - local buf = update_language_buffer(0, lang) - local diag = vim.diagnostic.get(buf) - vim.diagnostic.reset(ns, 0) - vim.diagnostic.set(ns, 0, diag, {}) +M.enableDiagnostics = function() + local qmdbufnr = a.nvim_get_current_buf() + local bufnrs = update_language_buffers(qmdbufnr) + + -- auto-close language files on qmd file close + a.nvim_create_autocmd({ "QuitPre", "WinClosed" }, { + buffer = qmdbufnr, + group = a.nvim_create_augroup("quartoAutoclose", {}), + callback = function(_, _) + for _, bufnr in ipairs(bufnrs) do + if a.nvim_buf_is_loaded(bufnr) then + -- delete tmp file + local path = a.nvim_buf_get_name(bufnr) + vim.fn.delete(path) + -- remove buffer + a.nvim_buf_delete(bufnr, { force = true }) + end + end end }) - a.nvim_exec_autocmds('TextChanged', {}) -end -M.enableDiagnostics = function() - if M.config.lspFeatures.enabled then - for _, lang in ipairs(M.config.lspFeatures.languages) do - enable_language_diagnostics(lang) + -- update hidden buffers on changes + a.nvim_create_autocmd({ "CursorHold", "TextChanged" }, { + buffer = qmdbufnr, + group = a.nvim_create_augroup("quartoLSPDiagnositcs", { clear = false }), + callback = function(_, _) + local bufs = update_language_buffers(0) + for _, bufnr in ipairs(bufs) do + local diag = vim.diagnostic.get(bufnr) + local ns = a.nvim_create_namespace('quarto-lang-' .. bufnr) + vim.diagnostic.reset(ns, 0) + vim.diagnostic.set(ns, 0, diag, {}) + end end + }) + + local key = M.config.keymap.hover + vim.api.nvim_set_keymap('n', key, ":lua require'quarto'.quartoHover()", {silent = true}) +end + +M.quartoHover = function() + local qmdbufnr = a.nvim_get_current_buf() + local bufnrs = update_language_buffers(qmdbufnr) + for _, bufnr in ipairs(bufnrs) do + local uri = vim.uri_from_bufnr(bufnr) + local position_params = vim.lsp.util.make_position_params() + position_params.textDocument = { + uri = uri + } + vim.lsp.buf_request(bufnr, "textDocument/hover", position_params, function(err, response, method, ...) + if response ~= nil then + vim.lsp.handlers["textDocument/hover"](err, response, method, ...) + end + end) end - a.nvim_exec_autocmds({ 'TextChangedI', 'TextChanged' }, {}) end + M.searchHelp = function(cmd_input) local topic = cmd_input.args local url = 'https://quarto.org/?q=' .. topic .. '&show-results=1' @@ -211,8 +260,18 @@ M.setup = function(opt) end M.debug = function() + package.loaded['quarto'] = nil quarto = require 'quarto' - print(quarto.config) + quarto.setup { + debug = true, + closePreviewOnExit = true, + lspFeatures = { + enabled = true, + languages = { 'python', 'r', 'julia' }, + -- languages = { 'python' }, + } + } + quarto.enableDiagnostics() end diff --git a/plugin/quarto.lua b/plugin/quarto.lua index 36879cb..07c506e 100644 --- a/plugin/quarto.lua +++ b/plugin/quarto.lua @@ -5,4 +5,14 @@ a.nvim_create_user_command('QuartoPreview', quarto.quartoPreview, {}) a.nvim_create_user_command('QuartoClosePreview', quarto.quartoClosePreview, {}) a.nvim_create_user_command('QuartoDiagnostics', quarto.enableDiagnostics, {}) a.nvim_create_user_command('QuartoHelp', quarto.searchHelp, {nargs=1}) +a.nvim_create_user_command('QuartoHover', quarto.quartoHover, {}) +a.nvim_create_autocmd({"BufEnter"}, { + pattern = {"*.qmd"}, + callback = function () + quarto = require'quarto' + if quarto.config.lspFeatures.enabled then + quarto.enableDiagnostics() + end + end, +})