diff --git a/README.md b/README.md index 065df6f..d54aaee 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ return { ---Set filetype to jsonc when opening a file specified by `config_file_paths`, ---make sure you have the jsonc tree-sitter parser installed for highlighting jsonc_filetype = true, + ---Enable live reloading of settings when config files change; for servers that support it, + ---this is done via the `workspace/didChangeConfiguration` notification, otherwise the + ---server is restarted + live_reload = false, ---Provide your own root dir; can be a string or function returning a string. ---It should be/return the full absolute path to the root directory. ---If not set, defaults to `require('codesettings.util').get_root()` @@ -149,6 +153,7 @@ return { - Minimal API: one function you call per server setup, or with a global hook (see example above) - `jsonc` filetype for local config files +- Live reload: automatically reload settings when config files change (opt-in via `live_reload = true`) - Configure the `codesettings.nvim` plugin itself in local config JSON files - `jsonls` integration for schema-based completion of LSP settings in JSON(C) configuration files ![jsonls integration](https://github.com/user-attachments/assets/5d37f0bb-0e07-4c22-bc6b-16cf3e65e201) diff --git a/bench/run.lua b/bench/run.lua index 5a798ee..02c84ea 100644 --- a/bench/run.lua +++ b/bench/run.lua @@ -1,5 +1,5 @@ local Util = require('codesettings.util') -local Jsonls = require('codesettings.integrations.jsonls') +local Jsonls = require('codesettings.setup.jsonls') local Settings = require('codesettings.settings') local Benchmark = {} diff --git a/codesettings.json b/codesettings.json index dbfa1f6..ba51c0e 100644 --- a/codesettings.json +++ b/codesettings.json @@ -14,7 +14,8 @@ "workspace": { "library": [ "${3rd}/luassert/library", - "${3rd}/busted/library" + "${3rd}/busted/library", + "${3rd}/luv/library" ], "checkThirdParty": false }, @@ -22,4 +23,5 @@ "vim" ] }, + "codesettings.live_reload": true } diff --git a/lua/codesettings/build/config-schema.lua b/lua/codesettings/build/config-schema.lua index b2bf675..f5c7eb9 100644 --- a/lua/codesettings/build/config-schema.lua +++ b/lua/codesettings/build/config-schema.lua @@ -1,5 +1,5 @@ -local Util = require('codesettings.util') local ConfigSchema = require('codesettings.config.schema') +local Util = require('codesettings.util') local relpath = 'lua/codesettings/generated/codesettings-config-schema.lua' diff --git a/lua/codesettings/build/doc.lua b/lua/codesettings/build/doc.lua index 3a51df9..ac122c8 100644 --- a/lua/codesettings/build/doc.lua +++ b/lua/codesettings/build/doc.lua @@ -1,5 +1,5 @@ -local Util = require('codesettings.util') local Schemas = require('codesettings.build.schemas') +local Util = require('codesettings.util') local M = {} diff --git a/lua/codesettings/build/schemas.lua b/lua/codesettings/build/schemas.lua index bee5ef0..457c4fa 100644 --- a/lua/codesettings/build/schemas.lua +++ b/lua/codesettings/build/schemas.lua @@ -250,8 +250,8 @@ function M.clean() end for _, f in pairs(files) do Util.delete_file(f) - print('Deleted ' .. f) end + print('Deleted ' .. #files .. ' schema files from schemas/*') end return M diff --git a/lua/codesettings/config/init.lua b/lua/codesettings/config/init.lua index b1b39b6..5d9da64 100644 --- a/lua/codesettings/config/init.lua +++ b/lua/codesettings/config/init.lua @@ -25,16 +25,20 @@ function Config.setup(opts) options = vim.tbl_deep_extend('force', {}, options, plugin_config) if options.jsonls_integration then - require('codesettings.integrations.jsonls').setup() + require('codesettings.setup.jsonls').setup() end if options.jsonc_filetype then - require('codesettings.integrations.jsonc-filetype').setup() + require('codesettings.setup.jsonc-filetype').setup() end local lua_ls_integration = options.lua_ls_integration if lua_ls_integration == true or (type(lua_ls_integration) == 'function' and lua_ls_integration()) then - require('codesettings.integrations.lua_ls').setup() + require('codesettings.setup.lua_ls').setup() + end + + if options.live_reload then + require('codesettings.setup.live-reload').setup() end end diff --git a/lua/codesettings/config/schema.lua b/lua/codesettings/config/schema.lua index 4461bad..ae80452 100644 --- a/lua/codesettings/config/schema.lua +++ b/lua/codesettings/config/schema.lua @@ -75,6 +75,11 @@ M.properties = { description = 'Set filetype to jsonc for config files', default = true, }, + live_reload = { + type = 'boolean', + description = 'Enable live reloading of settings when config files change; for servers that support it, this is done via the `workspace/didChangeConfiguration` notification, otherwise the server is restarted', + default = false, + }, } ---Extract the default values from the schema diff --git a/lua/codesettings/generated/annotations.lua b/lua/codesettings/generated/annotations.lua index cb936ab..77cacd0 100644 --- a/lua/codesettings/generated/annotations.lua +++ b/lua/codesettings/generated/annotations.lua @@ -1,4 +1,3 @@ --- vim: ft=bigfile -- stylua: ignore ---@meta @@ -2306,6 +2305,7 @@ -- -- ```lua -- default = { +-- analyze_files = false, -- run_tests = false -- } -- ``` @@ -3173,7 +3173,7 @@ -- An array of language ids for which the extension should probe if support is installed. -- -- ```lua --- default = { "astro", "civet", "javascript", "javascriptreact", "typescript", "typescriptreact", "html", "mdx", "vue", "markdown", "json", "jsonc", "css", "glimmer-js", "glimmer-ts" } +-- default = { "astro", "civet", "javascript", "javascriptreact", "typescript", "typescriptreact", "html", "mdx", "vue", "markdown", "json", "jsonc", "css", "glimmer-js", "glimmer-ts", "svelte" } -- ``` ---@field probe string[]? ---@field problems lsp.eslint.Problems? @@ -10678,6 +10678,26 @@ -- ``` ---@field enable boolean? +---@class lsp.intelephense.InlayHint +-- Will show inlay hints for call argument parameter names if named arguments are not already in use. +-- +-- ```lua +-- default = true +-- ``` +---@field parameterNames boolean? +-- Will show inlay hints for anonymous function declaration parameter types if not already declared. +-- +-- ```lua +-- default = true +-- ``` +---@field parameterTypes boolean? +-- Will show an inlay hint for call declaration return type if not already declared. +-- +-- ```lua +-- default = true +-- ``` +---@field returnTypes boolean? + -- An object that describes the format of generated class/interface/trait phpdoc. The following snippet variables are available: SYMBOL_NAME; SYMBOL_KIND; SYMBOL_TYPE; SYMBOL_NAMESPACE. -- -- ```lua @@ -10809,6 +10829,7 @@ ---@field environment lsp.intelephense.Environment? ---@field files lsp.intelephense.Files? ---@field format lsp.intelephense.Format? +---@field inlayHint lsp.intelephense.InlayHint? -- DEPRECATED. Don't use this. Go to command palette and search for enter licence key. ---@field licenceKey string? -- Maximum memory (in MB) that the server should use. On some systems this may only have effect when runtime has been set. Minimum 256. @@ -24730,4 +24751,4 @@ ---@field zig_lib_path string? ---@class lsp.zls ----@field zls lsp.zls.Zls? \ No newline at end of file +---@field zls lsp.zls.Zls? diff --git a/lua/codesettings/generated/codesettings-config-schema.lua b/lua/codesettings/generated/codesettings-config-schema.lua index 4ea8107..d28b2b2 100644 --- a/lua/codesettings/generated/codesettings-config-schema.lua +++ b/lua/codesettings/generated/codesettings-config-schema.lua @@ -47,6 +47,8 @@ ---default = true ---``` ---@field jsonls_integration boolean +---Enable live reloading of settings when config files change; for servers that support it, this is done via the `workspace/didChangeConfiguration` notification, otherwise the server is restarted +---@field live_reload boolean ---Integrate with lua_ls for LSP settings completion; can be a function so that, for example, you can enable it only if editing your nvim config --- ---```lua diff --git a/lua/codesettings/health.lua b/lua/codesettings/health.lua index 311eea8..86e3bd5 100644 --- a/lua/codesettings/health.lua +++ b/lua/codesettings/health.lua @@ -1,3 +1,4 @@ +local Config = require('codesettings.config') local Util = require('codesettings.util') local health_start = vim.health.start or vim.health.report_start @@ -49,9 +50,35 @@ function M.check() end if pcall(vim.treesitter.get_string_parser, '', 'jsonc') then - ok('**jsonc** parser for tree-sitter is installed') + ok('`jsonc` parser for tree-sitter is installed') else - warn('**jsonc** parser for tree-sitter is not installed. Jsonc highlighting might be broken') + warn('`jsonc` parser for tree-sitter is not installed. Jsonc highlighting might be broken') + end + + if Config.live_reload then + ok(('Live reload is enabled for paths: %s'):format(vim.inspect(Config.config_file_paths))) + else + info( + 'Live reload is disabled. Enable with `live_reload = true` to automatically reload settings when config files change' + ) + end + + -- check LSP integration settings + if Config.jsonls_integration then + ok('`jsonls` integration is enabled') + else + info('`jsonls` integration is disabled') + end + + if Config.lua_ls_integration then + ok('`lua_ls` integration is enabled') + else + info('`lua_ls` integration is disabled') + end + + -- check loader extensions + if Config.loader_extensions and #Config.loader_extensions > 0 then + ok('Loader extensions configured: %d', #Config.loader_extensions) end end diff --git a/lua/codesettings/settings.lua b/lua/codesettings/settings.lua index 9174d99..c79a646 100644 --- a/lua/codesettings/settings.lua +++ b/lua/codesettings/settings.lua @@ -1,6 +1,6 @@ -local Util = require('codesettings.util') local Extensions = require('codesettings.extensions') local TerminalObjects = require('codesettings.generated.terminal-objects') +local Util = require('codesettings.util') local M = {} diff --git a/lua/codesettings/integrations/jsonc-filetype.lua b/lua/codesettings/setup/jsonc-filetype.lua similarity index 100% rename from lua/codesettings/integrations/jsonc-filetype.lua rename to lua/codesettings/setup/jsonc-filetype.lua diff --git a/lua/codesettings/integrations/jsonls/init.lua b/lua/codesettings/setup/jsonls/init.lua similarity index 92% rename from lua/codesettings/integrations/jsonls/init.lua rename to lua/codesettings/setup/jsonls/init.lua index 38f7345..1cb80b1 100644 --- a/lua/codesettings/integrations/jsonls/init.lua +++ b/lua/codesettings/setup/jsonls/init.lua @@ -1,5 +1,5 @@ +local Transformer = require('codesettings.setup.jsonls.transformer') local Util = require('codesettings.util') -local Transformer = require('codesettings.integrations.jsonls.transformer') local M = {} @@ -75,7 +75,9 @@ function M.setup() }) -- lazy loading; if jsonls is already active, restart it - Util.restart_lsp('jsonls') + vim.defer_fn(function() + Util.did_change_configuration('jsonls', vim.lsp.config.jsonls, true) + end, 500) end return M diff --git a/lua/codesettings/integrations/jsonls/transformer.lua b/lua/codesettings/setup/jsonls/transformer.lua similarity index 100% rename from lua/codesettings/integrations/jsonls/transformer.lua rename to lua/codesettings/setup/jsonls/transformer.lua diff --git a/lua/codesettings/setup/live-reload.lua b/lua/codesettings/setup/live-reload.lua new file mode 100644 index 0000000..e6e6bc0 --- /dev/null +++ b/lua/codesettings/setup/live-reload.lua @@ -0,0 +1,77 @@ +local Util = require('codesettings.util') + +local M = {} + +---@type number|nil +local augroup = nil + +---@type {[string]: uv_timer_t} +local debounce_timers = {} + +---Reload settings for all active LSP clients +---@param filepath string the config file that changed +local function reload_settings(filepath) + local Settings = require('codesettings.settings') + + local settings = Settings.new():load(filepath) + if not settings then + return + end + + local clients = vim.lsp.get_clients() + + if #clients == 0 then + Util.info('settings file changed but no LSP clients are running') + return + end + local updated_clients = false + + for _, client in ipairs(clients) do + local client_settings = settings:schema(client.name) + if client_settings and vim.tbl_count(client_settings:totable()) > 0 then + client.config.settings = vim.tbl_deep_extend('force', client.config.settings or {}, client_settings:totable()) + Util.did_change_configuration(client, client.config) + updated_clients = true + end + end + + if not updated_clients then + Util.info('settings file changed but no settings found for running LSP servers') + end +end + +function M.setup() + if augroup then + return + end + + augroup = vim.api.nvim_create_augroup('CodesettingsLiveReload', { clear = true }) + local config_files = Util.get_local_configs({ only_exists = false }) + vim.api.nvim_create_autocmd('BufWritePost', { + group = augroup, + pattern = config_files, + callback = function(args) + -- cancel existing timer for this file if any + if debounce_timers[args.file] then + debounce_timers[args.file]:stop() + debounce_timers[args.file]:close() + end + + -- create new timer that will fire after 100ms + debounce_timers[args.file] = vim.defer_fn(function() + reload_settings(args.file) + debounce_timers[args.file] = nil + end, 500) + end, + }) +end + +---Teardown live reload +function M.teardown() + if augroup then + vim.api.nvim_del_augroup_by_id(augroup) + augroup = nil + end +end + +return M diff --git a/lua/codesettings/integrations/lua_ls.lua b/lua/codesettings/setup/lua_ls.lua similarity index 83% rename from lua/codesettings/integrations/lua_ls.lua rename to lua/codesettings/setup/lua_ls.lua index 56382eb..da57798 100644 --- a/lua/codesettings/integrations/lua_ls.lua +++ b/lua/codesettings/setup/lua_ls.lua @@ -18,7 +18,9 @@ function M.setup() }) -- lazy loading; if lua_ls is already active, restart it - Util.restart_lsp('lua_ls') + vim.defer_fn(function() + Util.did_change_configuration('lua_ls', vim.lsp.config.lua_ls, true) + end, 500) end return M diff --git a/lua/codesettings/util.lua b/lua/codesettings/util.lua index 3303298..c6a9006 100644 --- a/lua/codesettings/util.lua +++ b/lua/codesettings/util.lua @@ -305,6 +305,12 @@ end local msg_prefix = '[codesettings] ' +---@param msg string +---@param ... any +function M.info(msg, ...) + vim.notify(('%s%s'):format(msg_prefix, msg:format(...)), vim.log.levels.INFO) +end + ---@param msg string ---@param ... any function M.warn(msg, ...) @@ -317,17 +323,70 @@ function M.error(msg, ...) vim.notify(('%s%s'):format(msg_prefix, msg:format(...)), vim.log.levels.ERROR) end ----Restart LSP client by name, if active ----@param name string -function M.restart_lsp(name) - vim.defer_fn(function() - if #vim.lsp.get_clients({ name = name }) > 0 then - vim.lsp.enable(name, false) +---Schedule a notification function call +---@param fn function notification function (M.info, M.warn, M.error) +---@param ... any arguments to pass to the function +local function schedule_notify(fn, ...) + local args = { ... } + vim.schedule(function() + fn(unpack(args)) + end) +end + +---Tell the specified LSP server that configuration has changed. +---For servers that support it, this is done by `workspace/didChangeConfiguration` notification, +---otherwise the server is restarted. +---@param client_or_name vim.lsp.Client|string LSP client or name of the client +---@param config vim.lsp.Config|vim.lsp.ClientConfig new settings to notify the client about +---@param silent boolean? if true, suppress info/warn messages +function M.did_change_configuration(client_or_name, config, silent) + ---@type vim.lsp.Client[] + local clients + if type(client_or_name) == 'table' then + -- best effort: try to make sure the table is actually a `vim.lsp.Client` + if not client_or_name.notify or not client_or_name.supports_method then + schedule_notify(M.error, 'Expected vim.lsp.Client or string') + return + end + clients = { client_or_name } + elseif type(client_or_name) == 'string' then + local active_clients = vim.lsp.get_clients({ name = client_or_name }) + if #active_clients == 0 then + if not silent then + schedule_notify(M.warn, 'No active LSP client named %s', client_or_name) + end + return + end + clients = active_clients + else + M.error('Expected vim.lsp.Client or string') + return + end + local restarted_by_name = {} + vim.iter(clients):each(function(client) + ---@cast client vim.lsp.Client + + if client:supports_method(vim.lsp.protocol.Methods.workspace_didChangeConfiguration) then + client:notify(vim.lsp.protocol.Methods.workspace_didChangeConfiguration, { + settings = config.settings, + }) + if not silent then + schedule_notify(M.info, '%s configuration reloaded', client.name) + end + else + if restarted_by_name[client.name] then + return + end + vim.lsp.enable(client.name, false) + restarted_by_name[client.name] = true vim.defer_fn(function() - vim.lsp.enable(name) + vim.lsp.enable(client.name) + if not silent then + schedule_notify(M.info, '%s configuration reloaded (server restarted)', client.name) + end end, 500) end - end, 500) + end) end return M diff --git a/schemas/jdtls.json b/schemas/jdtls.json index 60b8017..7c80b6c 100644 --- a/schemas/jdtls.json +++ b/schemas/jdtls.json @@ -425,9 +425,6 @@ "name": { "description": "Java Execution Environment name. Must be unique.", "enum": [ - "J2SE-1.5", - "JavaSE-1.6", - "JavaSE-1.7", "JavaSE-1.8", "JavaSE-9", "JavaSE-10", diff --git a/spec/config_spec.lua b/spec/config_spec.lua index c825cbe..d562db8 100644 --- a/spec/config_spec.lua +++ b/spec/config_spec.lua @@ -21,9 +21,9 @@ describe('codesettings.config', function() it('should not run integration setups when disabled in local config', function() local Codesettings = require('codesettings') - local jsonls_mod = require('codesettings.integrations.jsonls') - local lua_ls_mod = require('codesettings.integrations.lua_ls') - local jsonc_filetype_mod = require('codesettings.integrations.jsonc-filetype') + local jsonls_mod = require('codesettings.setup.jsonls') + local lua_ls_mod = require('codesettings.setup.lua_ls') + local jsonc_filetype_mod = require('codesettings.setup.jsonc-filetype') spy.on(jsonls_mod, 'setup') spy.on(lua_ls_mod, 'setup') spy.on(jsonc_filetype_mod, 'setup') diff --git a/spec/integration_spec.lua b/spec/integration_tests_spec.lua similarity index 100% rename from spec/integration_spec.lua rename to spec/integration_tests_spec.lua diff --git a/spec/jsonls_spec.lua b/spec/jsonls_spec.lua index b151c85..72a05d0 100644 --- a/spec/jsonls_spec.lua +++ b/spec/jsonls_spec.lua @@ -1,7 +1,7 @@ ---@module 'busted' describe('jsonls integration', function() - local Jsonls = require('codesettings.integrations.jsonls') + local Jsonls = require('codesettings.setup.jsonls') before_each(function() Jsonls.clear_cache() @@ -37,7 +37,7 @@ describe('jsonls integration', function() end) describe('expand_schema', function() - local expand_schema = require('codesettings.integrations.jsonls.transformer').expand_schema + local expand_schema = require('codesettings.setup.jsonls.transformer').expand_schema it('creates both dotted and nested forms for a single dotted key', function() local input = { diff --git a/spec/schema_spec.lua b/spec/schema_spec.lua index 2e47051..a265c06 100644 --- a/spec/schema_spec.lua +++ b/spec/schema_spec.lua @@ -48,7 +48,7 @@ describe('Schema loading and property enumeration', function() it('caches per lsp_name key', function() local a1 = Schema.load('lua_ls') local a2 = Schema.load('lua_ls') - assert.is_true(a1 == a2, 'Expected same cached Schema object for repeated loads with identical key') + assert.is_true(a1 == a2) -- Different key form (if it existed) would produce a distinct cache entry; rust analyzer has two forms. local r1 = Schema.load('rust_analyzer') @@ -82,7 +82,7 @@ describe('Schema loading and property enumeration', function() set[p] = true end -- Lua LS schema commonly includes Lua.runtime.version & Lua.workspace.library - assert.is_true(set['Lua.runtime.version'], 'Expected Lua.runtime.version in enumerated properties') - assert.is_true(set['Lua.workspace.library'], 'Expected Lua.workspace.library in enumerated properties') + assert.is_true(set['Lua.runtime.version']) + assert.is_true(set['Lua.workspace.library']) end) end) diff --git a/stylua.toml b/stylua.toml index 4a89f57..c4a3392 100644 --- a/stylua.toml +++ b/stylua.toml @@ -1,3 +1,5 @@ indent_type = "Spaces" indent_width = 2 quote_style = "AutoPreferSingle" +[sort_requires] +enabled = true