diff --git a/lua/null-ls/builtins/code_actions/cspell.lua b/lua/null-ls/builtins/code_actions/cspell.lua deleted file mode 100644 index 24398854..00000000 --- a/lua/null-ls/builtins/code_actions/cspell.lua +++ /dev/null @@ -1,190 +0,0 @@ -local make_builtin = require("null-ls.helpers").make_builtin -local methods = require("null-ls.methods") -local h = require("null-ls.helpers.cspell") - -local CODE_ACTION = methods.internal.CODE_ACTION - ----@class AddToDictionaryAction ----@field diagnostic Diagnostic ----@field word string ----@field params GeneratorParams ----@field cspell CSpellConfigInfo ----@field dictionary CSpellDictionary - ----@param opts AddToDictionaryAction ----@return CodeAction -local function make_add_to_dictionary_action(opts) - ---@type CSpellSourceConfig - local code_action_config = opts.params:get_config() - local on_success = code_action_config.on_success - - return { - title = 'Add "' .. opts.word .. '" to dictionary "' .. opts.dictionary.name .. '"', - action = function() - if opts.dictionary == nil then - return - end - local dictionary_path = vim.fn.expand(opts.dictionary.path) - local dictionary_ok, dictionary_body = pcall(vim.fn.readfile, dictionary_path) - if not dictionary_ok then - vim.notify("Can't read " .. dictionary_path, vim.log.levels.ERROR) - return - end - table.insert(dictionary_body, opts.word) - - vim.fn.writefile(dictionary_body, dictionary_path) - vim.notify('Added "' .. opts.word .. '" to ' .. opts.dictionary.path, vim.log.levels.INFO) - - -- replace word in buffer to trigger cspell to update diagnostics - h.set_word(opts.diagnostic, opts.word) - - if on_success then - on_success(opts.cspell.path, opts.params, "add_to_dictionary") - end - end, - } -end - ----@class AddToJSONAction ----@field diagnostic Diagnostic ----@field word string ----@field params GeneratorParams ----@field cspell CSpellConfigInfo|nil - ----@param opts AddToJSONAction ----@return CodeAction -local function make_add_to_json(opts) - ---@type CSpellSourceConfig - local code_action_config = opts.params:get_config() - local on_success = code_action_config.on_success - local encode_json = code_action_config.encode_json or vim.json.encode - - return { - title = 'Add "' .. opts.word .. '" to cspell json file', - action = function() - local cspell = opts.cspell or h.create_cspell_json(opts.params) - - if not cspell.config.words then - cspell.config.words = {} - end - - table.insert(cspell.config.words, opts.word) - - local encoded = encode_json(cspell.config) or "" - local lines = {} - for line in encoded:gmatch("[^\r\n]+") do - table.insert(lines, line) - end - - vim.fn.writefile(lines, cspell.path) - - -- replace word in buffer to trigger cspell to update diagnostics - h.set_word(opts.diagnostic, opts.word) - - if on_success then - on_success(cspell.path, opts.params, "add_to_json") - end - end, - } -end - ---- Filter diagnostics generated by the cspell built-in ----@param bufnr number ----@param lnum number ----@param cursor_col number ----@return table -local cspell_diagnostics = function(bufnr, lnum, cursor_col) - local diagnostics = {} - for _, diagnostic in ipairs(vim.diagnostic.get(bufnr, { lnum = lnum })) do - if diagnostic.source == "cspell" and cursor_col >= diagnostic.col and cursor_col < diagnostic.end_col then - table.insert(diagnostics, diagnostic) - end - end - return diagnostics -end - -return make_builtin({ - name = "cspell", - meta = { - url = "https://github.com/streetsidesoftware/cspell", - description = "Injects actions to fix typos found by `cspell`.", - notes = { - "This source depends on the `cspell` built-in diagnostics source, so make sure to register it, too.", - }, - usage = "local sources = { null_ls.builtins.diagnostics.cspell, null_ls.builtins.code_actions.cspell }", - }, - method = CODE_ACTION, - filetypes = {}, - generator = { - ---@param params GeneratorParams - ---@return table - fn = function(params) - params.cwd = params.cwd or vim.loop.cwd() - - ---@type table - local actions = {} - - local cspell = h.async_get_config_info(params) - - local diagnostics = cspell_diagnostics(params.bufnr, params.row - 1, params.col) - if vim.tbl_isempty(diagnostics) then - return actions - end - - for _, diagnostic in ipairs(diagnostics) do - -- replace word with a suggestion - for _, suggestion in ipairs(diagnostic.user_data.suggestions) do - table.insert(actions, { - title = string.format("Use %s", suggestion), - action = function() - h.set_word(diagnostic, suggestion) - - ---@type CSpellSourceConfig - local code_action_config = params:get_config() - local on_success = code_action_config.on_success - - if on_success then - on_success(cspell and cspell.path, params, "use_suggestion") - end - end, - }) - end - - local word = h.get_word(diagnostic) - - -- add word to "words" in cspell.json - table.insert( - actions, - make_add_to_json({ - diagnostic = diagnostic, - word = word, - params = params, - cspell = cspell, - }) - ) - - if cspell == nil then - break - end - - -- add word to a custom dictionary - for _, dictionary in ipairs(cspell.config.dictionaryDefinitions or {}) do - if dictionary ~= nil and dictionary.addWords then - table.insert( - actions, - make_add_to_dictionary_action({ - diagnostic = diagnostic, - word = word, - params = params, - cspell = cspell, - dictionary = dictionary, - }) - ) - end - end - end - - return actions - end, - }, -}) diff --git a/lua/null-ls/builtins/diagnostics/cspell.lua b/lua/null-ls/builtins/diagnostics/cspell.lua deleted file mode 100644 index e5aa3e6a..00000000 --- a/lua/null-ls/builtins/diagnostics/cspell.lua +++ /dev/null @@ -1,109 +0,0 @@ -local h = require("null-ls.helpers") -local methods = require("null-ls.methods") -local helpers = require("null-ls.helpers.cspell") - -local DIAGNOSTICS = methods.internal.DIAGNOSTICS - -local custom_user_data = { - user_data = function(entries, _) - if not entries then - return - end - - local suggestions = {} - for suggestion in string.gmatch(entries["_suggestions"], "[^, ]+") do - table.insert(suggestions, suggestion) - end - - return { - suggestions = suggestions, - misspelled = entries["_quote"], - } - end, -} - -local needs_warning = true - -return h.make_builtin({ - name = "cspell", - meta = { - url = "https://github.com/streetsidesoftware/cspell", - description = "cspell is a spell checker for code.", - }, - method = DIAGNOSTICS, - filetypes = {}, - generator_opts = { - command = "cspell", - ---@param params GeneratorParams - args = function(params) - params.cwd = params.cwd or vim.loop.cwd() - - local cspell_args = { - "lint", - "--language-id", - params.ft, - "stdin://" .. params.bufname, - } - - local config_path = helpers.get_config_path(params) - if config_path then - cspell_args = vim.list_extend({ "-c", config_path }, cspell_args) - end - - local code_action_source = require("null-ls.sources").get({ - name = "cspell", - method = methods.internal.CODE_ACTION, - })[1] - - if code_action_source ~= nil then - -- only enable suggestions when using the code actions built-in, since they slow down the command - cspell_args = vim.list_extend({ "--show-suggestions" }, cspell_args) - - local code_action_config = code_action_source.config or {} - local diagnostics_config = params and params:get_config() or {} - - if helpers.matching_configs(code_action_config, diagnostics_config) then - -- warm up the config cache so we have the config ready by the time we call the code action - helpers.async_get_config_info(params) - elseif needs_warning then - needs_warning = false - vim.notify( - "You should use the same config for both sources", - vim.log.levels.WARN, - { title = "cspell.nvim" } - ) - end - end - - return cspell_args - end, - to_stdin = true, - ignore_stderr = true, - format = "line", - check_exit_code = function(code) - return code <= 1 - end, - on_output = h.diagnostics.from_patterns({ - { - pattern = ".*:(%d+):(%d+)%s*-%s*(.*%((.*)%))%s*Suggestions:%s*%[(.*)%]", - groups = { "row", "col", "message", "_quote", "_suggestions" }, - overrides = { - adapters = { - h.diagnostics.adapters.end_col.from_quote, - custom_user_data, - }, - }, - }, - { - pattern = [[.*:(%d+):(%d+)%s*-%s*(.*%((.*)%))]], - groups = { "row", "col", "message", "_quote" }, - overrides = { - adapters = { - h.diagnostics.adapters.end_col.from_quote, - }, - }, - }, - }), - }, - factory = h.generator_factory, -}) diff --git a/lua/null-ls/helpers/cspell.lua b/lua/null-ls/helpers/cspell.lua deleted file mode 100644 index 6e0a3d47..00000000 --- a/lua/null-ls/helpers/cspell.lua +++ /dev/null @@ -1,245 +0,0 @@ -local Path = require("plenary.path") -local uv = vim.loop - -local M = {} - -local CSPELL_CONFIG_FILES = { - "cspell.json", - ".cspell.json", - "cSpell.json", - ".cspell.json", - ".cspell.config.json", -} - ----@type table -local CONFIG_INFO_BY_CWD = {} -local PATH_BY_CWD = {} - ---- create a bare minimum cspell.json file ----@param params GeneratorParams ----@return CSpellConfigInfo -M.create_cspell_json = function(params) - ---@type CSpellSourceConfig - local code_action_config = params:get_config() - local config_file_preferred_name = code_action_config.config_file_preferred_name or "cspell.json" - local encode_json = code_action_config.encode_json or vim.json.encode - - if not vim.tbl_contains(CSPELL_CONFIG_FILES, config_file_preferred_name) then - vim.notify( - "Invalid config_file_preferred_name for cspell json file: " - .. config_file_preferred_name - .. '. The name "cspell.json" will be used instead', - vim.log.levels.WARN - ) - config_file_preferred_name = "cspell.json" - end - - local cspell_json = { - version = "0.2", - language = "en", - words = {}, - flagWords = {}, - } - - local cspell_json_str = encode_json(cspell_json) - local cspell_json_file_path = require("null-ls.utils").path.join(params.cwd, config_file_preferred_name) - - Path:new(cspell_json_file_path):write(cspell_json_str, "w") - vim.notify("Created a new cspell.json file at " .. cspell_json_file_path, vim.log.levels.INFO) - - local info = { - config = cspell_json, - path = cspell_json_file_path, - } - - CONFIG_INFO_BY_CWD[params.cwd] = info - - return info -end - ----@param filename string ----@param cwd string ----@return string|nil -local function find_file(filename, cwd) - ---@type string|nil - local current_dir = cwd - local root_dir = "/" - - repeat - local file_path = current_dir .. "/" .. filename - local stat = uv.fs_stat(file_path) - if stat and stat.type == "file" then - return file_path - end - - current_dir = uv.fs_realpath(current_dir .. "/..") - until current_dir == root_dir - - return nil -end - ---- Find the first cspell.json file in the directory tree ----@param cwd string ----@return string|nil -local find_cspell_config_path = function(cwd) - for _, file in ipairs(CSPELL_CONFIG_FILES) do - local path = find_file(file, cwd or vim.loop.cwd()) - if path then - return path - end - end - return nil -end - ----@class GeneratorParams ----@field bufnr number ----@field bufname string ----@field ft string ----@field row number ----@field col number ----@field cwd string ----@field get_config function - ----@param params GeneratorParams ----@return CSpellConfigInfo|nil -M.get_cspell_config = function(params) - ---@type CSpellSourceConfig - local code_action_config = params:get_config() - local decode_json = code_action_config.decode_json or vim.json.decode - - local cspell_json_path = M.get_config_path(params) - - if cspell_json_path == nil or cspell_json_path == "" then - return - end - - local content = Path:new(cspell_json_path):read() - local ok, cspell_config = pcall(decode_json, content) - - if not ok then - vim.notify("\nCannot parse cspell json file as JSON.\n", vim.log.levels.ERROR) - return - end - - return { - config = cspell_config, - path = cspell_json_path, - } -end - ---- Non-blocking config parser ---- The first run is meant to be a cache warm up ----@param params GeneratorParams ----@return CSpellConfigInfo|nil -M.async_get_config_info = function(params) - ---@type uv_async_t|nil - local async - async = vim.loop.new_async(function() - if CONFIG_INFO_BY_CWD[params.cwd] == nil then - local config = M.get_cspell_config(params) - CONFIG_INFO_BY_CWD[params.cwd] = config - end - async:close() - end) - - async:send() - - return CONFIG_INFO_BY_CWD[params.cwd] -end - -M.get_config_path = function(params) - if PATH_BY_CWD[params.cwd] == nil then - local code_action_config = params:get_config() - local find_json = code_action_config.find_json or find_cspell_config_path - local cspell_json_path = find_json(params.cwd) - PATH_BY_CWD[params.cwd] = cspell_json_path - end - return PATH_BY_CWD[params.cwd] -end - ---- Checks that both sources use the same config ---- We need to do that so we can start reading and parsing the cspell ---- configuration asynchronously as soon as we get the first diagnostic. ----@param code_actions_config CSpellSourceConfig ----@param diagnostics_config CSpellSourceConfig -M.matching_configs = function(code_actions_config, diagnostics_config) - return (vim.tbl_isempty(code_actions_config) and vim.tbl_isempty(diagnostics_config)) - or code_actions_config == diagnostics_config -end - ---- Get the word associated with the diagnostic ----@param diagnostic Diagnostic ----@return string -M.get_word = function(diagnostic) - return vim.api.nvim_buf_get_text( - diagnostic.bufnr, - diagnostic.lnum, - diagnostic.col, - diagnostic.end_lnum, - diagnostic.end_col, - {} - )[1] -end - ---- Replace the diagnostic's word with a new word ----@param diagnostic Diagnostic ----@param new_word string -M.set_word = function(diagnostic, new_word) - vim.api.nvim_buf_set_text( - diagnostic.bufnr, - diagnostic.lnum, - diagnostic.col, - diagnostic.end_lnum, - diagnostic.end_col, - { new_word } - ) -end - -M.clear_cache = function() - PATH_BY_CWD = {} - CONFIG_INFO_BY_CWD = {} -end - -return M - ----@class Diagnostic ----@field bufnr number Buffer number ----@field lnum number The starting line of the diagnostic ----@field end_lnum number The final line of the diagnostic ----@field col number The starting column of the diagnostic ----@field end_col number The final column of the diagnostic ----@field severity number The severity of the diagnostic ----@field message string The diagnostic text ----@field source string The source of the diagnostic ----@field code number The diagnostic code ----@field user_data UserData - ----@class CodeAction ----@field title string ----@field action function - ----@class UserData ----@field suggestions table Suggested words for the diagnostic - ----@class CSpellConfigInfo ----@field config CSpellConfig ----@field path string - ----@class CSpellConfig ----@field flagWords table ----@field language string ----@field version string ----@field words table ----@field dictionaryDefinitions table|nil - ----@class CSpellDictionary ----@field name string ----@field path string ----@field addWords boolean|nil - ----@class CSpellSourceConfig ----@field config_file_preferred_name string|nil ----@field find_json function|nil ----@field decode_json function|nil ----@field encode_json function|nil ----@field on_success function|nil