1
1
mirror of https://github.com/oxalica/nil.git synced 2024-11-22 11:22:46 +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": {
"command": "nil",
"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
{
// ...
"nix.enableLanguageServer": true, // Enable LSP.
"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 config;
mod convert;
mod handler;
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 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)?;
tracing::info!("Leaving main loop");

View File

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

View File

@ -102,7 +102,7 @@ impl Vfs {
// 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 = <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.change.change_file(file, new_text);
Ok(())

View File

@ -94,6 +94,7 @@ let
settings.nil = {
testSetting = 42;
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.
- [x] Diagnostics. `textDocument/publishDiagnostics`
- [x] Syntax errors.
- [x] Hard semantic errors reported as parse errors by Nix, like duplicated keys in attrsets.
- [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 unused bindings, `with` and `rec`.
- [ ] Client pulled diagnostics.
- [x] Custom filter
- You can disable some diagnostic messages via LSP setting `diagnostics.ignored`,
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.
- [x] Custom filter on kinds.
- [x] Exclude files.
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] Renaming. `textDocument/renamme`, `textDocument/prepareRename`
@ -80,22 +79,18 @@ This incomplete list tracks noteble features currently implemented or planned.
- [ ] Range formatting.
- [ ] On-type formatting.
- [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.
Like, for [`coc.nvim`],
```jsonc
// coc-settings.json
{
"coc.preferences.formatOnSaveFiletypes": ["nix"]
}
```
When formatter is configured, you can also enable format-on-save in your editor.
Like, for [`coc.nvim`],
```jsonc
// coc-settings.json
{
"coc.preferences.formatOnSaveFiletypes": ["nix"]
}
```
- [ ] Cross-file analysis.
- [x] Multi-threaded.