From 3599306f3499d8bd9d868d66d29e6f659db42b6b Mon Sep 17 00:00:00 2001 From: Mat Jones Date: Mon, 10 Nov 2025 20:40:03 -0500 Subject: [PATCH 1/5] chore: Rename `integrations` module to `setup` --- bench/run.lua | 2 +- lua/codesettings/build/schemas.lua | 2 +- lua/codesettings/config/init.lua | 6 ++--- lua/codesettings/config/schema.lua | 5 ++++ lua/codesettings/generated/annotations.lua | 24 ++++++++++++++++++- .../jsonc-filetype.lua | 0 .../{integrations => setup}/jsonls/init.lua | 2 +- .../jsonls/transformer.lua | 0 .../{integrations => setup}/lua_ls.lua | 0 spec/config_spec.lua | 6 ++--- spec/jsonls_spec.lua | 4 ++-- 11 files changed, 39 insertions(+), 12 deletions(-) rename lua/codesettings/{integrations => setup}/jsonc-filetype.lua (100%) rename lua/codesettings/{integrations => setup}/jsonls/init.lua (96%) rename lua/codesettings/{integrations => setup}/jsonls/transformer.lua (100%) rename lua/codesettings/{integrations => setup}/lua_ls.lua (100%) 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/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..b944ff9 100644 --- a/lua/codesettings/config/init.lua +++ b/lua/codesettings/config/init.lua @@ -25,16 +25,16 @@ 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 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..2141943 100644 --- a/lua/codesettings/generated/annotations.lua +++ b/lua/codesettings/generated/annotations.lua @@ -2306,6 +2306,7 @@ -- -- ```lua -- default = { +-- analyze_files = false, -- run_tests = false -- } -- ``` @@ -3173,7 +3174,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 +10679,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 +10830,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. 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 96% rename from lua/codesettings/integrations/jsonls/init.lua rename to lua/codesettings/setup/jsonls/init.lua index 38f7345..61451aa 100644 --- a/lua/codesettings/integrations/jsonls/init.lua +++ b/lua/codesettings/setup/jsonls/init.lua @@ -1,5 +1,5 @@ local Util = require('codesettings.util') -local Transformer = require('codesettings.integrations.jsonls.transformer') +local Transformer = require('codesettings.setup.jsonls.transformer') local 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/integrations/lua_ls.lua b/lua/codesettings/setup/lua_ls.lua similarity index 100% rename from lua/codesettings/integrations/lua_ls.lua rename to lua/codesettings/setup/lua_ls.lua 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/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 = { From c6d4c71d9315f78aeec254364329978e84738545 Mon Sep 17 00:00:00 2001 From: Mat Jones Date: Mon, 10 Nov 2025 20:43:04 -0500 Subject: [PATCH 2/5] feat(config): Live reload --- README.md | 5 + codesettings.json | 1 + lua/codesettings/config/init.lua | 4 + .../generated/codesettings-config-schema.lua | 2 + lua/codesettings/health.lua | 13 ++ lua/codesettings/setup/live-reload.lua | 178 ++++++++++++++++++ lua/codesettings/util.lua | 6 + schemas/jdtls.json | 3 - ...on_spec.lua => integration_tests_spec.lua} | 0 9 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 lua/codesettings/setup/live-reload.lua rename spec/{integration_spec.lua => integration_tests_spec.lua} (100%) 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/codesettings.json b/codesettings.json index dbfa1f6..ecfe331 100644 --- a/codesettings.json +++ b/codesettings.json @@ -22,4 +22,5 @@ "vim" ] }, + "codesettings.live_reload": true } diff --git a/lua/codesettings/config/init.lua b/lua/codesettings/config/init.lua index b944ff9..5d9da64 100644 --- a/lua/codesettings/config/init.lua +++ b/lua/codesettings/config/init.lua @@ -36,6 +36,10 @@ function Config.setup(opts) if lua_ls_integration == true or (type(lua_ls_integration) == 'function' and lua_ls_integration()) then require('codesettings.setup.lua_ls').setup() end + + if options.live_reload then + require('codesettings.setup.live-reload').setup() + end end ---Reset the configuration to defaults. 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..af25acc 100644 --- a/lua/codesettings/health.lua +++ b/lua/codesettings/health.lua @@ -53,6 +53,19 @@ function M.check() else warn('**jsonc** parser for tree-sitter is not installed. Jsonc highlighting might be broken') end + + local Config = require('codesettings.config') + if Config.live_reload then + local LiveReload = require('codesettings.setup.live-reload') + local watcher_count = LiveReload.count() + if watcher_count > 0 then + ok('Live reload is enabled and watching %d file(s)', watcher_count) + else + info('Live reload is enabled but no files are currently being watched') + end + else + info('Live reload is disabled. Enable with `live_reload = true` to automatically reload settings when config files change') + end end return M diff --git a/lua/codesettings/setup/live-reload.lua b/lua/codesettings/setup/live-reload.lua new file mode 100644 index 0000000..52b4818 --- /dev/null +++ b/lua/codesettings/setup/live-reload.lua @@ -0,0 +1,178 @@ +local Util = require('codesettings.util') +local Settings = require('codesettings.settings') +local Methods = vim.lsp.protocol.Methods + +local M = {} + +---@type table +local watchers = {} + +---@type number|nil +local augroup = nil + +---Reload settings for all active LSP clients +---@param filepath string the config file that changed +local function reload_settings(filepath) + local settings = Settings.new():load(filepath) + if not settings then + return + end + + local clients = vim.lsp.get_clients() + local updated_clients = {} + + 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()) + + if client:supports_method(Methods.workspace_didChangeConfiguration) then + client:notify(Methods.workspace_didChangeConfiguration, { + settings = client.config.settings, + }) + table.insert(updated_clients, client.name) + else + Util.restart_lsp(client.name) + table.insert(updated_clients, client.name .. ' (restarted)') + end + end + end + + if #updated_clients > 0 then + Util.info('reloaded settings for: %s', table.concat(updated_clients, ', ')) + else + Util.info('settings file changed but no settings found for running LSP servers') + end +end + +---Watch a file for changes +---@param path string +---@return boolean success +local function watch_file(path) + if not Util.exists(path) then + return false + end + + if watchers[path] then + watchers[path]:stop() + watchers[path]:close() + end + + local watcher = vim.uv.new_fs_event() + if not watcher then + return false + end + + local debounce_timer = nil + local debounce_ms = 150 + + local function handle_change(err, filepath, events) + if err then + vim.schedule(function() + Util.error('watcher error for %s: %s', vim.fn.fnamemodify(filepath, ':t'), err) + end) + return + end + + -- editors like nvim use safe-write (write to temp, then rename) + -- which triggers both change and rename events; we only care if the file still exists + if events.change or events.rename then + if debounce_timer then + debounce_timer:stop() + debounce_timer:close() + end + + debounce_timer = vim.defer_fn(function() + -- small delay to ensure file operations complete + -- before checking existence (async fs operations) + vim.defer_fn(function() + vim.schedule(function() + if Util.exists(filepath) then + reload_settings(filepath) + else + -- file was deleted, stop watching + Util.warn('file deleted: %s', vim.fn.fnamemodify(filepath, ':t')) + if watchers[filepath] then + watchers[filepath]:stop() + watchers[filepath]:close() + watchers[filepath] = nil + end + end + end) + end, 50) + debounce_timer = nil + end, debounce_ms) + end + end + + local ok = watcher:start(path, {}, handle_change) + + if ok ~= 0 then + watcher:close() + return false + end + + watchers[path] = watcher + return true +end + +---Stop all watchers +local function unwatch_all() + for _, watcher in pairs(watchers) do + if not watcher:is_closing() then + watcher:stop() + watcher:close() + end + end + watchers = {} +end + +---Watch all config files in the current project +local function watch_config_files() + unwatch_all() + + local config_files = Util.get_local_configs({ only_exists = true }) + + for _, filepath in ipairs(config_files) do + if not watch_file(filepath) then + Util.error('failed to watch %s', filepath) + end + end +end + +function M.setup() + if augroup then + return + end + + augroup = vim.api.nvim_create_augroup('CodesettingsLiveReload', { clear = true }) + + vim.api.nvim_create_autocmd({ 'DirChanged', 'VimEnter' }, { + group = augroup, + callback = watch_config_files, + }) + + vim.api.nvim_create_autocmd('VimLeavePre', { + group = augroup, + callback = M.teardown, + }) + + watch_config_files() +end + +---Get the number of active watchers +---@return number +function M.count() + return vim.tbl_count(watchers) +end + +---Teardown live reload and stop all watchers +function M.teardown() + if augroup then + vim.api.nvim_del_augroup_by_id(augroup) + augroup = nil + end + unwatch_all() +end + +return M diff --git a/lua/codesettings/util.lua b/lua/codesettings/util.lua index 3303298..e2948f2 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, ...) 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/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 From 5a5cca068158d4abab0a6ad6876278fd74151629 Mon Sep 17 00:00:00 2001 From: Mat Jones Date: Mon, 10 Nov 2025 21:34:44 -0500 Subject: [PATCH 3/5] chore(stylua): Enable sort_requires --- lua/codesettings/build/config-schema.lua | 2 +- lua/codesettings/build/doc.lua | 2 +- lua/codesettings/health.lua | 4 +++- lua/codesettings/settings.lua | 2 +- lua/codesettings/setup/jsonls/init.lua | 2 +- lua/codesettings/setup/live-reload.lua | 2 +- stylua.toml | 2 ++ 7 files changed, 10 insertions(+), 6 deletions(-) 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/health.lua b/lua/codesettings/health.lua index af25acc..7b29347 100644 --- a/lua/codesettings/health.lua +++ b/lua/codesettings/health.lua @@ -64,7 +64,9 @@ function M.check() info('Live reload is enabled but no files are currently being watched') end else - info('Live reload is disabled. Enable with `live_reload = true` to automatically reload settings when config files change') + info( + 'Live reload is disabled. Enable with `live_reload = true` to automatically reload settings when config files change' + ) 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/setup/jsonls/init.lua b/lua/codesettings/setup/jsonls/init.lua index 61451aa..2c5b31b 100644 --- a/lua/codesettings/setup/jsonls/init.lua +++ b/lua/codesettings/setup/jsonls/init.lua @@ -1,5 +1,5 @@ -local Util = require('codesettings.util') local Transformer = require('codesettings.setup.jsonls.transformer') +local Util = require('codesettings.util') local M = {} diff --git a/lua/codesettings/setup/live-reload.lua b/lua/codesettings/setup/live-reload.lua index 52b4818..4e4a9c8 100644 --- a/lua/codesettings/setup/live-reload.lua +++ b/lua/codesettings/setup/live-reload.lua @@ -1,5 +1,5 @@ -local Util = require('codesettings.util') local Settings = require('codesettings.settings') +local Util = require('codesettings.util') local Methods = vim.lsp.protocol.Methods local M = {} 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 From c40afba9ad99b95c8b1bc3865e1af3cad74c5e3e Mon Sep 17 00:00:00 2001 From: Mat Jones Date: Mon, 10 Nov 2025 21:46:13 -0500 Subject: [PATCH 4/5] fix(health): Update healthchecks --- lua/codesettings/health.lua | 43 ++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/lua/codesettings/health.lua b/lua/codesettings/health.lua index 7b29347..84e7f21 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,12 +50,11 @@ 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 - local Config = require('codesettings.config') if Config.live_reload then local LiveReload = require('codesettings.setup.live-reload') local watcher_count = LiveReload.count() @@ -68,6 +68,43 @@ function M.check() 'Live reload is disabled. Enable with `live_reload = true` to automatically reload settings when config files change' ) end + + -- check if fs_event is available for live reload + if Config.live_reload then + local is_ok, has_fs_watch = pcall(function() + local test_watcher = vim.uv.new_fs_event() + if test_watcher then + test_watcher:stop() + test_watcher:close() + return true + end + return false + end) + + if not is_ok or not has_fs_watch then + warn('File system events (`fs_event`) are not available; live reload may not work properly') + else + ok('File system events (`fs_event`) are available for live reload') + end + 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 return M From f7fa9734c3ded3e94d3842121932322c3db35cd5 Mon Sep 17 00:00:00 2001 From: Mat Jones Date: Tue, 11 Nov 2025 20:08:21 -0500 Subject: [PATCH 5/5] refactor live reload to use autocmd --- codesettings.json | 3 +- lua/codesettings/generated/annotations.lua | 3 +- lua/codesettings/health.lua | 27 +--- lua/codesettings/setup/jsonls/init.lua | 4 +- lua/codesettings/setup/live-reload.lua | 161 ++++----------------- lua/codesettings/setup/lua_ls.lua | 4 +- lua/codesettings/util.lua | 69 ++++++++- spec/schema_spec.lua | 6 +- 8 files changed, 104 insertions(+), 173 deletions(-) diff --git a/codesettings.json b/codesettings.json index ecfe331..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 }, diff --git a/lua/codesettings/generated/annotations.lua b/lua/codesettings/generated/annotations.lua index 2141943..77cacd0 100644 --- a/lua/codesettings/generated/annotations.lua +++ b/lua/codesettings/generated/annotations.lua @@ -1,4 +1,3 @@ --- vim: ft=bigfile -- stylua: ignore ---@meta @@ -24752,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/health.lua b/lua/codesettings/health.lua index 84e7f21..86e3bd5 100644 --- a/lua/codesettings/health.lua +++ b/lua/codesettings/health.lua @@ -56,38 +56,13 @@ function M.check() end if Config.live_reload then - local LiveReload = require('codesettings.setup.live-reload') - local watcher_count = LiveReload.count() - if watcher_count > 0 then - ok('Live reload is enabled and watching %d file(s)', watcher_count) - else - info('Live reload is enabled but no files are currently being watched') - end + 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 if fs_event is available for live reload - if Config.live_reload then - local is_ok, has_fs_watch = pcall(function() - local test_watcher = vim.uv.new_fs_event() - if test_watcher then - test_watcher:stop() - test_watcher:close() - return true - end - return false - end) - - if not is_ok or not has_fs_watch then - warn('File system events (`fs_event`) are not available; live reload may not work properly') - else - ok('File system events (`fs_event`) are available for live reload') - end - end - -- check LSP integration settings if Config.jsonls_integration then ok('`jsonls` integration is enabled') diff --git a/lua/codesettings/setup/jsonls/init.lua b/lua/codesettings/setup/jsonls/init.lua index 2c5b31b..1cb80b1 100644 --- a/lua/codesettings/setup/jsonls/init.lua +++ b/lua/codesettings/setup/jsonls/init.lua @@ -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/setup/live-reload.lua b/lua/codesettings/setup/live-reload.lua index 4e4a9c8..e6e6bc0 100644 --- a/lua/codesettings/setup/live-reload.lua +++ b/lua/codesettings/setup/live-reload.lua @@ -1,178 +1,77 @@ -local Settings = require('codesettings.settings') local Util = require('codesettings.util') -local Methods = vim.lsp.protocol.Methods local M = {} ----@type table -local watchers = {} - ---@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() - local updated_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()) - - if client:supports_method(Methods.workspace_didChangeConfiguration) then - client:notify(Methods.workspace_didChangeConfiguration, { - settings = client.config.settings, - }) - table.insert(updated_clients, client.name) - else - Util.restart_lsp(client.name) - table.insert(updated_clients, client.name .. ' (restarted)') - end + Util.did_change_configuration(client, client.config) + updated_clients = true end end - if #updated_clients > 0 then - Util.info('reloaded settings for: %s', table.concat(updated_clients, ', ')) - else + if not updated_clients then Util.info('settings file changed but no settings found for running LSP servers') end end ----Watch a file for changes ----@param path string ----@return boolean success -local function watch_file(path) - if not Util.exists(path) then - return false - end - - if watchers[path] then - watchers[path]:stop() - watchers[path]:close() - end - - local watcher = vim.uv.new_fs_event() - if not watcher then - return false - end - - local debounce_timer = nil - local debounce_ms = 150 - - local function handle_change(err, filepath, events) - if err then - vim.schedule(function() - Util.error('watcher error for %s: %s', vim.fn.fnamemodify(filepath, ':t'), err) - end) - return - end - - -- editors like nvim use safe-write (write to temp, then rename) - -- which triggers both change and rename events; we only care if the file still exists - if events.change or events.rename then - if debounce_timer then - debounce_timer:stop() - debounce_timer:close() - end - - debounce_timer = vim.defer_fn(function() - -- small delay to ensure file operations complete - -- before checking existence (async fs operations) - vim.defer_fn(function() - vim.schedule(function() - if Util.exists(filepath) then - reload_settings(filepath) - else - -- file was deleted, stop watching - Util.warn('file deleted: %s', vim.fn.fnamemodify(filepath, ':t')) - if watchers[filepath] then - watchers[filepath]:stop() - watchers[filepath]:close() - watchers[filepath] = nil - end - end - end) - end, 50) - debounce_timer = nil - end, debounce_ms) - end - end - - local ok = watcher:start(path, {}, handle_change) - - if ok ~= 0 then - watcher:close() - return false - end - - watchers[path] = watcher - return true -end - ----Stop all watchers -local function unwatch_all() - for _, watcher in pairs(watchers) do - if not watcher:is_closing() then - watcher:stop() - watcher:close() - end - end - watchers = {} -end - ----Watch all config files in the current project -local function watch_config_files() - unwatch_all() - - local config_files = Util.get_local_configs({ only_exists = true }) - - for _, filepath in ipairs(config_files) do - if not watch_file(filepath) then - Util.error('failed to watch %s', filepath) - end - end -end - function M.setup() if augroup then return end augroup = vim.api.nvim_create_augroup('CodesettingsLiveReload', { clear = true }) - - vim.api.nvim_create_autocmd({ 'DirChanged', 'VimEnter' }, { + local config_files = Util.get_local_configs({ only_exists = false }) + vim.api.nvim_create_autocmd('BufWritePost', { group = augroup, - callback = watch_config_files, - }) + 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 - vim.api.nvim_create_autocmd('VimLeavePre', { - group = augroup, - callback = M.teardown, + -- 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, }) - - watch_config_files() -end - ----Get the number of active watchers ----@return number -function M.count() - return vim.tbl_count(watchers) end ----Teardown live reload and stop all watchers +---Teardown live reload function M.teardown() if augroup then vim.api.nvim_del_augroup_by_id(augroup) augroup = nil end - unwatch_all() end return M diff --git a/lua/codesettings/setup/lua_ls.lua b/lua/codesettings/setup/lua_ls.lua index 56382eb..da57798 100644 --- a/lua/codesettings/setup/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 e2948f2..c6a9006 100644 --- a/lua/codesettings/util.lua +++ b/lua/codesettings/util.lua @@ -323,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/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)