Skip to content

Commit

Permalink
feat: improvements to loading project-local settings (#290)
Browse files Browse the repository at this point in the history
  • Loading branch information
mrcjkb authored Mar 17, 2024
1 parent fd3a47f commit bb06512
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 28 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- [Experimental] Load rust-analyzer settings from `.vscode/settings.json`.
Can be enabled by setting `vim.g.rustaceanvim.server.load_vscode_settings`
to `true` [[#286](https://github.com/mrcjkb/rustaceanvim/issues/286)].

## [4.13.0] - 2024-03-15

### Added
Expand All @@ -14,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
The `server.settings` function can now take a `default_settings` tabe
to be merged.

### Fixed

- Loading settings from `rust-analyzer.json`:
Potential for duplicate lua config keys if json keys are of the format:
`"rust-analyzer.foo.bar"`

## [4.12.2] - 2024-03-11

### Fixed
Expand Down
11 changes: 6 additions & 5 deletions doc/rustaceanvim.txt
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,12 @@ RustcOpts *RustcOpts*
RustaceanLspClientOpts *RustaceanLspClientOpts*

Fields: ~
{auto_attach?} (boolean|fun(bufnr:integer):boolean) Whether to automatically attach the LSP client. Defaults to `true` if the `rust-analyzer` executable is found.
{cmd?} (string[]|fun():string[]) Command and arguments for starting rust-analyzer
{settings?} (table|fun(project_root:string|nil,default_settings:table):table) Setting passed to rust-analyzer. Defaults to a function that looks for a `rust-analyzer.json` file or returns an empty table. See https://rust-analyzer.github.io/manual.html#configuration.
{standalone?} (boolean) Standalone file support (enabled by default). Disabling it may improve rust-analyzer's startup time.
{logfile?} (string) The path to the rust-analyzer log file.
{auto_attach?} (boolean|fun(bufnr:integer):boolean) Whether to automatically attach the LSP client. Defaults to `true` if the `rust-analyzer` executable is found.
{cmd?} (string[]|fun():string[]) Command and arguments for starting rust-analyzer
{settings?} (table|fun(project_root:string|nil,default_settings:table):table) Setting passed to rust-analyzer. Defaults to a function that looks for a `rust-analyzer.json` file or returns an empty table. See https://rust-analyzer.github.io/manual.html#configuration.
{standalone?} (boolean) Standalone file support (enabled by default). Disabling it may improve rust-analyzer's startup time.
{logfile?} (string) The path to the rust-analyzer log file.
{load_vscode_settings?} (boolean) Whether to search (upward from the buffer) for rust-analyzer settings in .vscode/settings json. If found, loaded settings will override configured options. Default: false


RustaceanDapOpts *RustaceanDapOpts*
Expand Down
1 change: 1 addition & 0 deletions lua/rustaceanvim/config/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ vim.g.rustaceanvim = vim.g.rustaceanvim
---@field settings? table | fun(project_root:string|nil, default_settings: table):table Setting passed to rust-analyzer. Defaults to a function that looks for a `rust-analyzer.json` file or returns an empty table. See https://rust-analyzer.github.io/manual.html#configuration.
---@field standalone? boolean Standalone file support (enabled by default). Disabling it may improve rust-analyzer's startup time.
---@field logfile? string The path to the rust-analyzer log file.
---@field load_vscode_settings? boolean Whether to search (upward from the buffer) for rust-analyzer settings in .vscode/settings json. If found, loaded settings will override configured options. Default: false

---@class RustaceanDapOpts
--- @field autoload_configurations boolean Whether to autoload nvim-dap configurations when rust-analyzer has attached? Default: `true`.
Expand Down
2 changes: 2 additions & 0 deletions lua/rustaceanvim/config/internal.lua
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ local RustaceanDefaultConfig = {
--- @type table
['rust-analyzer'] = {},
},
---@type boolean Whether to search (upward from the buffer) for rust-analyzer settings in .vscode/settings json.
load_vscode_settings = false,
},

--- debugging stuff
Expand Down
50 changes: 50 additions & 0 deletions lua/rustaceanvim/config/json.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
local M = {}

local function tbl_set(tbl, keys, value)
local next = table.remove(keys, 1)
if #keys > 0 then
tbl[next] = tbl[next] or {}
tbl_set(tbl[next], keys, value)
else
tbl[next] = value
end
end

---@param tbl table
---@param json_key string e.g. "rust-analyzer.check.overrideCommand"
---@param json_value unknown
local function override_tbl_values(tbl, json_key, json_value)
local keys = vim.split(json_key, '%.')
tbl_set(tbl, keys, json_value)
end

---@param json_content string
---@return table
function M.silent_decode(json_content)
local ok, json_tbl = pcall(vim.json.decode, json_content)
if not ok or type(json_tbl) ~= 'table' then
return {}
end
return json_tbl
end

---@param tbl table
---@param json_tbl { [string]: unknown }
---@param key_predicate? fun(string): boolean
function M.override_with_json_keys(tbl, json_tbl, key_predicate)
for json_key, value in pairs(json_tbl) do
if not key_predicate or key_predicate(json_key) then
override_tbl_values(tbl, json_key, value)
end
end
end

---@param tbl table
---@param json_tbl { [string]: unknown }
function M.override_with_rust_analyzer_json_keys(tbl, json_tbl)
M.override_with_json_keys(tbl, json_tbl, function(key)
return vim.startswith(key, 'rust-analyzer')
end)
end

return M
37 changes: 16 additions & 21 deletions lua/rustaceanvim/config/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,6 @@

local server = {}

---Read the content of a file
---@param filename string
---@return string|nil content
local function read_file(filename)
local content
local f = io.open(filename, 'r')
if f then
content = f:read('*a')
f:close()
end
return content
end

---@class LoadRASettingsOpts
---@field settings_file_pattern string|nil File name or pattern to search for. Defaults to 'rust-analyzer.json'
---@field default_settings table|nil Default settings to merge the loaded settings into
Expand All @@ -28,6 +15,7 @@ end
function server.load_rust_analyzer_settings(project_root, opts)
local config = require('rustaceanvim.config.internal')
local compat = require('rustaceanvim.compat')
local os = require('rustaceanvim.os')

local default_opts = { settings_file_pattern = 'rust-analyzer.json' }
opts = vim.tbl_deep_extend('force', {}, default_opts, opts or {})
Expand All @@ -50,20 +38,27 @@ function server.load_rust_analyzer_settings(project_root, opts)
return default_settings
end
local config_json = results[1]
local content = read_file(config_json)
local success, rust_analyzer_settings = pcall(vim.json.decode, content)
if not success or not rust_analyzer_settings then
local msg = 'Could not decode ' .. config_json .. '. Falling back to default settings.'
vim.notify('rustaceanvim: ' .. msg, vim.log.levels.ERROR)
local content = os.read_file(config_json)
if not content then
vim.notify('Could not read ' .. config_json, vim.log.levels.WARNING)
return default_settings
end
local json = require('rustaceanvim.config.json')
local rust_analyzer_settings = json.silent_decode(content)
local ra_key = 'rust-analyzer'
if rust_analyzer_settings[ra_key] then
local has_ra_key = true
for key, _ in pairs(rust_analyzer_settings) do
if key:find(ra_key) ~= nil then
has_ra_key = true
break
end
end
if has_ra_key then
-- Settings json with "rust-analyzer" key
default_settings[ra_key] = rust_analyzer_settings[ra_key]
json.override_with_rust_analyzer_json_keys(default_settings, rust_analyzer_settings)
else
-- "rust-analyzer" settings are top level
default_settings[ra_key] = rust_analyzer_settings
json.override_with_json_keys(default_settings, rust_analyzer_settings)
end
return default_settings
end
Expand Down
30 changes: 28 additions & 2 deletions lua/rustaceanvim/lsp.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,25 @@ local function is_in_workspace(client, root_dir)
return false
end

---@Searches upward for a .vscode/settings.json that contains rust-analyzer
--- settings and applies them.
---@param bufname string
---@return table server_settings or an empty table if no settings were found
local function find_vscode_settings(bufname)
local settings = {}
local found_dirs = vim.fs.find({ '.vscode' }, { upward = true, path = vim.fs.dirname(bufname), type = 'directory' })
if vim.tbl_isempty(found_dirs) then
return settings
end
local vscode_dir = found_dirs[1]
local results = vim.fn.glob(compat.joinpath(vscode_dir, 'settings.json'), true, true)
if vim.tbl_isempty(results) then
return settings
end
local content = os.read_file(results[1])
return content and require('rustaceanvim.config.json').silent_decode(content) or {}
end

---@class LspStartConfig: RustaceanLspClientConfig
---@field root_dir string | nil
---@field init_options? table
Expand All @@ -53,23 +72,30 @@ end
---@return integer|nil client_id The LSP client ID
M.start = function(bufnr)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local bufname = vim.api.nvim_buf_get_name(bufnr)
local client_config = config.server
---@type LspStartConfig
local lsp_start_config = vim.tbl_deep_extend('force', {}, client_config)
local root_dir = cargo.get_root_dir(vim.api.nvim_buf_get_name(bufnr))
local root_dir = cargo.get_root_dir(bufname)
root_dir = root_dir and os.normalize_path_on_windows(root_dir)
lsp_start_config.root_dir = root_dir
if not root_dir then
--- No project root found. Start in detached/standalone mode.
lsp_start_config.init_options = { detachedFiles = { vim.api.nvim_buf_get_name(bufnr) } }
lsp_start_config.init_options = { detachedFiles = { bufname } }
end

local settings = client_config.settings
local evaluated_settings = type(settings) == 'function' and settings(root_dir, client_config.default_settings)
or settings

---@cast evaluated_settings table
lsp_start_config.settings = evaluated_settings

if config.server.load_vscode_settings then
local json_settings = find_vscode_settings(bufname)
require('rustaceanvim.config.json').override_with_rust_analyzer_json_keys(lsp_start_config.settings, json_settings)
end

-- Check if a client is already running and add the workspace folder if necessary.
for _, client in pairs(rust_analyzer.get_active_rustaceanvim_clients()) do
if root_dir and not is_in_workspace(client, root_dir) then
Expand Down
13 changes: 13 additions & 0 deletions lua/rustaceanvim/os.lua
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,17 @@ function os.is_valid_file_path(path)
return vim.startswith(normalized_path, '/')
end

---Read the content of a file
---@param filename string
---@return string|nil content
function os.read_file(filename)
local content
local f = io.open(filename, 'r')
if f then
content = f:read('*a')
f:close()
end
return content
end

return os
18 changes: 18 additions & 0 deletions spec/config_server_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
local config = require('rustaceanvim.config.server')
local compat = require('rustaceanvim.compat')
describe('Decode rust-analyzer settings from json', function()
local function expect_correct_loading(json_path)
local settings = config.load_rust_analyzer_settings(json_path)
assert.True(settings['rust-analyzer'].cargo.targetDir)
assert.same({ 'leptosfmt', '--stdin', '--rustfmt' }, settings['rust-analyzer'].rustfmt.overrideCommand)
assert.True(settings['rust-analyzer'].checkOnSave)
end
it('Fixture: rust-analyzer key', function()
local json_path = compat.joinpath(vim.fn.getcwd(), 'spec', 'fixtures', 'rust-analyzer-json')
expect_correct_loading(json_path)
end)
it('Fixture: top-level settings', function()
local json_path = compat.joinpath(vim.fn.getcwd(), 'spec', 'fixtures', 'rust-analyzer-json-top-level')
expect_correct_loading(json_path)
end)
end)
5 changes: 5 additions & 0 deletions spec/fixtures/rust-analyzer-json-top-level/rust-analyzer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cargo.targetDir": true,
"rustfmt.overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"],
"checkOnSave": true
}
5 changes: 5 additions & 0 deletions spec/fixtures/rust-analyzer-json/rust-analyzer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rust-analyzer.cargo.targetDir": true,
"rust-analyzer.rustfmt.overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"],
"rust-analyzer.checkOnSave": true
}
45 changes: 45 additions & 0 deletions spec/json_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
local json = require('rustaceanvim.config.json')
describe('Decode rust-analyzer settings from json', function()
it('can extract rust-analyzer key from index', function()
local json_content = [[
{
"rust-analyzer.check.overrideCommand": [
"cargo",
"check",
"-p",
"service_b",
"--message-format=json"
],
"rust-analyzer.foo.enable": true,
"rust-analyzer.foo.bar.enable": true,
"rust-analyzer.foo.bar.baz.bat": "something deeply nested",
"some-other-key.foo.bar.baz.bat": "should not be included"
}
]]
local tbl = {}
local json_tbl = json.silent_decode(json_content)
json.override_with_rust_analyzer_json_keys(tbl, json_tbl)
assert.same({
['rust-analyzer'] = {
check = {
overrideCommand = {
'cargo',
'check',
'-p',
'service_b',
'--message-format=json',
},
},
foo = {
enable = true,
bar = {
enable = true,
baz = {
bat = 'something deeply nested',
},
},
},
},
}, tbl)
end)
end)

0 comments on commit bb06512

Please sign in to comment.