1
1
mirror of https://github.com/oxalica/nil.git synced 2024-11-25 18:41:40 +03:00

Allow disable diagnostics for some files

This commit is contained in:
oxalica 2022-11-25 23:41:09 +08:00
parent 9794a2eb97
commit 84e39a65c9
8 changed files with 238 additions and 86 deletions

View File

@ -78,7 +78,13 @@ Merge this setting into your `coc-settings.json`, which can be opened by `:CocCo
"nix": { "nix": {
"command": "nil", "command": "nil",
"filetypes": ["nix"], "filetypes": ["nix"],
"rootPatterns": ["flake.nix"] "rootPatterns": ["flake.nix"],
// Uncomment these to tweak settings.
// "settings": {
// "nil": {
// "formatting": { "command": ["nixpkgs-fmt"] }
// }
// }
} }
} }
} }
@ -127,9 +133,15 @@ Modify the extension's settings in your `settings.json`.
```jsonc ```jsonc
{ {
// ...
"nix.enableLanguageServer": true, // Enable LSP. "nix.enableLanguageServer": true, // Enable LSP.
"nix.serverPath": "nil" // The path to the LSP server executable. "nix.serverPath": "nil" // The path to the LSP server executable.
// Uncomment these to tweak settings.
// "nix.serverSettings": {
// "nil": {
// "formatting": { "command": ["nixpkgs-fmt"] }
// }
// }
} }
``` ```

75
crates/nil/src/config.rs Normal file
View File

@ -0,0 +1,75 @@
use lsp_types::Url;
use std::collections::HashSet;
use std::path::PathBuf;
pub const CONFIG_KEY: &str = "nil";
#[derive(Debug, Clone)]
pub struct Config {
pub root_path: PathBuf,
pub diagnostics_excluded_files: Vec<Url>,
pub diagnostics_ignored: HashSet<String>,
pub formatting_command: Option<Vec<String>>,
}
impl Config {
pub fn new(root_path: PathBuf) -> Self {
assert!(root_path.is_absolute());
Self {
root_path,
diagnostics_excluded_files: Vec::new(),
diagnostics_ignored: HashSet::new(),
formatting_command: None,
}
}
pub fn update(&mut self, mut value: serde_json::Value) -> (Vec<String>, bool) {
let mut errors = Vec::new();
let mut updated_diagnostics = false;
if let Some(v) = value.pointer_mut("/diagnostics/excludedFiles") {
match serde_json::from_value::<Vec<String>>(v.take()) {
Ok(v) => {
self.diagnostics_excluded_files = v
.into_iter()
.map(|path| {
Url::from_file_path(self.root_path.join(path))
.expect("Root path is absolute")
})
.collect();
updated_diagnostics = true;
}
Err(e) => {
errors.push(format!("Invalid value of `diagnostics.excludedFiles`: {e}"));
}
}
}
if let Some(v) = value.pointer_mut("/diagnostics/ignored") {
match serde_json::from_value(v.take()) {
Ok(v) => {
self.diagnostics_ignored = v;
updated_diagnostics = true;
}
Err(e) => {
errors.push(format!("Invalid value of `diagnostics.ignored`: {e}"));
}
}
}
if let Some(v) = value.pointer_mut("/formatting/command") {
match serde_json::from_value::<Option<Vec<String>>>(v.take()) {
Ok(Some(v)) if v.is_empty() => {
errors.push("`formatting.command` must not be an empty list".into());
}
Ok(v) => {
self.formatting_command = v;
}
Err(e) => {
errors.push(format!("Invalid value of `formatting.command`: {e}"));
}
}
}
(errors, updated_diagnostics)
}
}

View File

@ -1,4 +1,5 @@
mod capabilities; mod capabilities;
mod config;
mod convert; mod convert;
mod handler; mod handler;
mod semantic_tokens; mod semantic_tokens;
@ -36,7 +37,16 @@ pub fn main_loop(conn: Connection) -> Result<()> {
let init_params = serde_json::from_value::<InitializeParams>(init_params)?; let init_params = serde_json::from_value::<InitializeParams>(init_params)?;
let mut server = Server::new(conn.sender.clone()); let root_path = match init_params
.root_uri
.as_ref()
.and_then(|uri| uri.to_file_path().ok())
{
Some(path) => path,
None => std::env::current_dir()?,
};
let mut server = Server::new(conn.sender.clone(), root_path);
server.run(conn.receiver, init_params)?; server.run(conn.receiver, init_params)?;
tracing::info!("Leaving main loop"); tracing::info!("Leaving main loop");

View File

@ -1,3 +1,4 @@
use crate::config::{Config, CONFIG_KEY};
use crate::{convert, handler, Result, Vfs}; use crate::{convert, handler, Result, Vfs};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use ide::{Analysis, AnalysisHost, Cancelled}; use ide::{Analysis, AnalysisHost, Cancelled};
@ -8,28 +9,24 @@ use lsp_types::{
InitializeParams, MessageType, NumberOrString, PublishDiagnosticsParams, ShowMessageParams, InitializeParams, MessageType, NumberOrString, PublishDiagnosticsParams, ShowMessageParams,
Url, Url,
}; };
use serde::Deserialize;
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashSet; use std::collections::HashMap;
use std::panic::UnwindSafe; use std::panic::UnwindSafe;
use std::path::PathBuf;
use std::sync::{Arc, Once, RwLock}; use std::sync::{Arc, Once, RwLock};
use std::{panic, thread}; use std::{panic, thread};
const CONFIG_KEY: &str = "nil";
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Config {
pub(crate) diagnostics_ignored: HashSet<String>,
pub(crate) formatting_command: Option<Vec<String>>,
}
type ReqHandler = fn(&mut Server, Response); type ReqHandler = fn(&mut Server, Response);
type Task = Box<dyn FnOnce() -> Event + Send>; type Task = Box<dyn FnOnce() -> Event + Send>;
enum Event { enum Event {
Response(Response), Response(Response),
Diagnostics(Url, Vec<Diagnostic>), Diagnostics {
uri: Url,
version: u64,
diagnostics: Vec<Diagnostic>,
},
ClientExited, ClientExited,
} }
@ -38,9 +35,11 @@ pub struct Server {
/// This contains an internal RWLock and must not lock together with `vfs`. /// This contains an internal RWLock and must not lock together with `vfs`.
host: AnalysisHost, host: AnalysisHost,
vfs: Arc<RwLock<Vfs>>, vfs: Arc<RwLock<Vfs>>,
opened_files: HashSet<Url>, opened_files: HashMap<Url, FileData>,
config: Arc<Config>, config: Arc<Config>,
is_shutdown: bool, is_shutdown: bool,
/// Monotonic version counter for diagnostics calculation ordering.
version_counter: u64,
// Message passing. // Message passing.
req_queue: ReqQueue<(), ReqHandler>, req_queue: ReqQueue<(), ReqHandler>,
@ -50,8 +49,14 @@ pub struct Server {
event_rx: Receiver<Event>, event_rx: Receiver<Event>,
} }
#[derive(Debug, Default)]
struct FileData {
diagnostics_version: u64,
diagnostics: Vec<Diagnostic>,
}
impl Server { impl Server {
pub fn new(lsp_tx: Sender<Message>) -> Self { pub fn new(lsp_tx: Sender<Message>, root_path: PathBuf) -> Self {
let (task_tx, task_rx) = crossbeam_channel::unbounded(); let (task_tx, task_rx) = crossbeam_channel::unbounded();
let (event_tx, event_rx) = crossbeam_channel::unbounded(); let (event_tx, event_rx) = crossbeam_channel::unbounded();
let worker_cnt = thread::available_parallelism().map_or(1, |n| n.get()); let worker_cnt = thread::available_parallelism().map_or(1, |n| n.get());
@ -69,8 +74,9 @@ impl Server {
host: Default::default(), host: Default::default(),
vfs: Arc::new(RwLock::new(Vfs::new())), vfs: Arc::new(RwLock::new(Vfs::new())),
opened_files: Default::default(), opened_files: Default::default(),
config: Arc::new(Config::default()), config: Arc::new(Config::new(root_path)),
is_shutdown: false, is_shutdown: false,
version_counter: 0,
req_queue: ReqQueue::default(), req_queue: ReqQueue::default(),
lsp_tx, lsp_tx,
@ -164,13 +170,26 @@ impl Server {
self.lsp_tx.send(resp.into()).unwrap(); self.lsp_tx.send(resp.into()).unwrap();
} }
} }
Event::Diagnostics(uri, diagnostics) => { Event::Diagnostics {
self.send_notification::<notif::PublishDiagnostics>(PublishDiagnosticsParams { uri,
uri, version,
diagnostics, diagnostics,
version: None, } => match self.opened_files.get_mut(&uri) {
}); Some(f) if f.diagnostics_version < version => {
} f.diagnostics_version = version;
f.diagnostics = diagnostics.clone();
tracing::trace!(
"Push {} diagnostics of {uri}, version {version}",
diagnostics.len(),
);
self.send_notification::<notif::PublishDiagnostics>(PublishDiagnosticsParams {
uri,
diagnostics,
version: None,
});
}
_ => tracing::debug!("Ignore raced diagnostics of {uri}, version {version}"),
},
Event::ClientExited => { Event::ClientExited => {
return Err("The process initializing this server is exited. Stopping.".into()); return Err("The process initializing this server is exited. Stopping.".into());
} }
@ -224,7 +243,7 @@ impl Server {
})? })?
.on_sync_mut::<notif::DidOpenTextDocument>(|st, params| { .on_sync_mut::<notif::DidOpenTextDocument>(|st, params| {
let uri = &params.text_document.uri; let uri = &params.text_document.uri;
st.opened_files.insert(uri.clone()); st.opened_files.insert(uri.clone(), FileData::default());
st.set_vfs_file_content(uri, params.text_document.text)?; st.set_vfs_file_content(uri, params.text_document.text)?;
Ok(()) Ok(())
})? })?
@ -315,34 +334,9 @@ impl Server {
}); });
} }
fn update_config(&mut self, mut v: serde_json::Value) { fn update_config(&mut self, value: serde_json::Value) {
let mut updated_diagnostics = false;
let mut config = Config::clone(&self.config); let mut config = Config::clone(&self.config);
let mut errors = Vec::new(); let (errors, updated_diagnostics) = config.update(value);
if let Some(v) = v.pointer_mut("/diagnostics/ignored") {
match serde_json::from_value(v.take()) {
Ok(v) => {
config.diagnostics_ignored = v;
updated_diagnostics = true;
}
Err(e) => {
errors.push(format!("Invalid value of `diagnostics.ignored`: {e}"));
}
}
}
if let Some(v) = v.pointer_mut("/formatting/command") {
match serde_json::from_value::<Option<Vec<String>>>(v.take()) {
Ok(Some(v)) if v.is_empty() => {
errors.push("`formatting.command` must not be an empty list".into());
}
Ok(v) => {
config.formatting_command = v;
}
Err(e) => {
errors.push(format!("Invalid value of `formatting.command`: {e}"));
}
}
}
tracing::debug!("Updated config, errors: {errors:?}, config: {config:?}"); tracing::debug!("Updated config, errors: {errors:?}, config: {config:?}");
self.config = Arc::new(config); self.config = Arc::new(config);
@ -356,26 +350,41 @@ impl Server {
// Refresh all diagnostics since the filter may be changed. // Refresh all diagnostics since the filter may be changed.
if updated_diagnostics { if updated_diagnostics {
for uri in &self.opened_files { let version = self.next_version();
tracing::debug!("Recalculate diagnostics of {uri}"); for uri in self.opened_files.keys() {
self.update_diagnostics(uri.clone()); tracing::trace!("Recalculate diagnostics of {uri}, version {version}");
self.update_diagnostics(uri.clone(), version);
} }
} }
} }
fn update_diagnostics(&self, uri: Url) { fn update_diagnostics(&self, uri: Url, version: u64) {
let snap = self.snapshot(); let snap = self.snapshot();
let task = move || { let task = move || {
let diags = with_catch_unwind("diagnostics", || handler::diagnostics(snap, &uri)) // Return empty diagnostics for ignored files.
.unwrap_or_else(|err| { let diagnostics = (!snap.config.diagnostics_excluded_files.contains(&uri))
tracing::error!("Failed to calculate diagnostics: {err}"); .then(|| {
Vec::new() with_catch_unwind("diagnostics", || handler::diagnostics(snap, &uri))
}); .unwrap_or_else(|err| {
Event::Diagnostics(uri, diags) tracing::error!("Failed to calculate diagnostics: {err}");
Vec::new()
})
})
.unwrap_or_default();
Event::Diagnostics {
uri,
version,
diagnostics,
}
}; };
self.task_tx.send(Box::new(task)).unwrap(); self.task_tx.send(Box::new(task)).unwrap();
} }
fn next_version(&mut self) -> u64 {
self.version_counter += 1;
self.version_counter
}
fn snapshot(&self) -> StateSnapshot { fn snapshot(&self) -> StateSnapshot {
StateSnapshot { StateSnapshot {
analysis: self.host.snapshot(), analysis: self.host.snapshot(),
@ -399,20 +408,25 @@ impl Server {
// Must be called without holding the lock of `vfs`. // Must be called without holding the lock of `vfs`.
self.host.apply_change(changes); self.host.apply_change(changes);
let version = self.next_version();
let vfs = self.vfs.read().unwrap(); let vfs = self.vfs.read().unwrap();
for (file, text) in file_changes { for (file, text) in file_changes {
let uri = vfs.uri_for_file(file); let uri = vfs.uri_for_file(file);
if !self.opened_files.contains(&uri) { if !self.opened_files.contains_key(&uri) {
continue; continue;
} }
// FIXME: Removed or closed files are indistinguishable from empty files. // FIXME: Removed or closed files are indistinguishable from empty files.
if !text.is_empty() { if !text.is_empty() {
self.update_diagnostics(uri); self.update_diagnostics(uri, version);
} else { } else {
// Clear diagnostics. // Clear diagnostics.
self.event_tx self.event_tx
.send(Event::Diagnostics(uri, Vec::new())) .send(Event::Diagnostics {
uri,
version,
diagnostics: Vec::new(),
})
.unwrap(); .unwrap();
} }
} }

View File

@ -102,7 +102,7 @@ impl Vfs {
// This is not quite efficient, but we already do many O(n) traversals. // This is not quite efficient, but we already do many O(n) traversals.
let (new_text, line_map) = LineMap::normalize(new_text).ok_or("File too large")?; let (new_text, line_map) = LineMap::normalize(new_text).ok_or("File too large")?;
let new_text = <Arc<str>>::from(new_text); let new_text = <Arc<str>>::from(new_text);
log::debug!("File {:?} content changed: {:?}", file, new_text); log::trace!("File {:?} content changed: {:?}", file, new_text);
self.files[file.0 as usize] = (new_text.clone(), Arc::new(line_map)); self.files[file.0 as usize] = (new_text.clone(), Arc::new(line_map));
self.change.change_file(file, new_text); self.change.change_file(file, new_text);
Ok(()) Ok(())

View File

@ -94,6 +94,7 @@ let
settings.nil = { settings.nil = {
testSetting = 42; testSetting = 42;
formatting.command = [ "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt" ]; formatting.command = [ "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt" ];
diagnostics.excludedFiles = [ "generated.nix" ];
}; };
}; };
}; };

45
docs/configuration.md Normal file
View File

@ -0,0 +1,45 @@
## LSP Configuration
There are some tunable options and settings for `nil`.
They are retrieved via LSP and support runtine modification.
All settings are nested under a key `"nil"`.
For example, `formatting.command` means to write
`"nil": { "formatting": { "command": ["your-command"] } }`
in JSON, not `"nil.formatting.command": ["wrong"]`.
The place to write LSP configurations differs between clients.
Please check the documentation of your LSP client (usually the editor or editor plugins).
There are some examples for common editor/plugins in [README](../README.md).
### Reference
The values shown here are the default values.
```jsonc
{
"nil": {
"formatting": {
// External formatter command (with arguments).
// It should accepts file content in stdin and print the formatted code into stdout.
// Type: [string] | null
// Example: ["nixpkgs-fmt"]
"command": null,
},
"diagnostics": {
// Ignored diagnostic kinds.
// The kind identifier is a snake_cased_string usually shown together
// with the diagnostic message.
// Type: [string]
// Example: ["unused_binding", "unused_with"]
"ignored": [],
// Files to exclude from showing diagnostics. Useful for generated files.
// It accepts an array of paths. Relative paths are joint to the workspace root.
// Glob patterns are currently not supported.
// Type: [string]
// Example: ["Cargo.nix"]
"excludedFiles": [],
},
},
}
```

View File

@ -32,6 +32,7 @@ This incomplete list tracks noteble features currently implemented or planned.
- [ ] Attrset fields. - [ ] Attrset fields.
- [x] Diagnostics. `textDocument/publishDiagnostics` - [x] Diagnostics. `textDocument/publishDiagnostics`
- [x] Syntax errors. - [x] Syntax errors.
- [x] Hard semantic errors reported as parse errors by Nix, like duplicated keys in attrsets. - [x] Hard semantic errors reported as parse errors by Nix, like duplicated keys in attrsets.
- [x] Undefiend names. - [x] Undefiend names.
@ -39,13 +40,11 @@ This incomplete list tracks noteble features currently implemented or planned.
- [x] Warnings of unnecessary syntax. - [x] Warnings of unnecessary syntax.
- [x] Warnings of unused bindings, `with` and `rec`. - [x] Warnings of unused bindings, `with` and `rec`.
- [ ] Client pulled diagnostics. - [ ] Client pulled diagnostics.
- [x] Custom filter - [x] Custom filter on kinds.
- You can disable some diagnostic messages via LSP setting `diagnostics.ignored`, - [x] Exclude files.
which accepts an array of ignored diagnostic code strings,
eg. `["unused_binding","unused_with"]`.
The code of diagnostics is usually shows in parentheses together with the message.
See documentations of your editor about how to set LSP settings. You can disable some diagnostic kinds or for some (generated) files via LSP configuration.
See [docs/configuration.md](./configuration.md) for more information.
- [x] Expand selection. `textDocument/selectionRange` - [x] Expand selection. `textDocument/selectionRange`
- [x] Renaming. `textDocument/renamme`, `textDocument/prepareRename` - [x] Renaming. `textDocument/renamme`, `textDocument/prepareRename`
@ -80,22 +79,18 @@ This incomplete list tracks noteble features currently implemented or planned.
- [ ] Range formatting. - [ ] Range formatting.
- [ ] On-type formatting. - [ ] On-type formatting.
- [x] External formatter. - [x] External formatter.
- Currently, an external formatter must be configured via LSP setting
`formatting.command` to enable this functionality.
It accepts `null` for disabled, or an non-empty array for the formatting command,
eg. `["nixpkgs-fmt"]` for [nixpkgs-fmt].
The command must read Nix code from stdin and print the formatted code to stdout.
[nixpkgs-fmt]: https://github.com/nix-community/nixpkgs-fmt External formatter must be manually configured to work.
See [docs/configuration.md](./configuration.md) for more information.
You might need to set other editor settings to enable format-on-save. When formatter is configured, you can also enable format-on-save in your editor.
Like, for [`coc.nvim`], Like, for [`coc.nvim`],
```jsonc ```jsonc
// coc-settings.json // coc-settings.json
{ {
"coc.preferences.formatOnSaveFiletypes": ["nix"] "coc.preferences.formatOnSaveFiletypes": ["nix"]
} }
``` ```
- [ ] Cross-file analysis. - [ ] Cross-file analysis.
- [x] Multi-threaded. - [x] Multi-threaded.