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:
parent
9794a2eb97
commit
84e39a65c9
16
README.md
16
README.md
@ -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
75
crates/nil/src/config.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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 = ¶ms.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();
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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
45
docs/configuration.md
Normal 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": [],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user