1
1
mirror of https://github.com/oxalica/nil.git synced 2024-10-27 04:19:40 +03:00

Watch flake.{nix,lock} for auto-reloading

This commit is contained in:
oxalica 2023-04-22 15:03:34 +08:00
parent c177bea181
commit 4a78a44f3e
3 changed files with 126 additions and 19 deletions

View File

@ -28,6 +28,18 @@ pub(crate) fn negotiate_capabilities(
.additional_properties_support
),
server_initiated_progress: test!(client_caps.window.work_done_progress),
watch_files: test!(
client_caps
.workspace
.did_change_watched_files
.dynamic_registration
),
watch_files_relative_pattern: test!(
client_caps
.workspace
.did_change_watched_files
.relative_pattern_support
),
};
let server_caps = ServerCapabilities {
@ -81,4 +93,6 @@ pub(crate) fn negotiate_capabilities(
pub(crate) struct NegotiatedCapabilities {
pub client_show_message_request: bool,
pub server_initiated_progress: bool,
pub watch_files: bool,
pub watch_files_relative_pattern: bool,
}

View File

@ -5,13 +5,17 @@ use anyhow::{bail, ensure, Context, Result};
use async_lsp::router::Router;
use async_lsp::{ClientSocket, ErrorCode, LanguageClient, ResponseError};
use ide::{Analysis, AnalysisHost, Cancelled, FlakeInfo, VfsPath};
use lsp_types::notification::Notification;
use lsp_types::request::{self as req, Request};
use lsp_types::{
notification as notif, ConfigurationItem, ConfigurationParams, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
DidChangeWatchedFilesRegistrationOptions, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, FileChangeType, FileEvent, FileSystemWatcher, GlobPattern,
InitializeParams, InitializeResult, InitializedParams, MessageActionItem,
MessageActionItemProperty, MessageType, NumberOrString, ProgressParams, ProgressParamsValue,
PublishDiagnosticsParams, ServerInfo, ShowMessageParams, ShowMessageRequestParams, Url,
MessageActionItemProperty, MessageType, NumberOrString, OneOf, ProgressParams,
ProgressParamsValue, PublishDiagnosticsParams, Registration, RegistrationParams,
RelativePattern, ServerInfo, ShowMessageParams, ShowMessageRequestParams, Url,
WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressCreateParams, WorkDoneProgressEnd,
WorkDoneProgressReport,
};
@ -24,6 +28,7 @@ use std::collections::HashMap;
use std::future::{ready, Future};
use std::ops::ControlFlow;
use std::panic::UnwindSafe;
use std::path::Path;
use std::pin::pin;
use std::sync::{Arc, Once, RwLock};
use std::time::Duration;
@ -89,10 +94,10 @@ impl Server {
.notification::<notif::DidCloseTextDocument>(Self::on_did_close)
.notification::<notif::DidChangeTextDocument>(Self::on_did_change)
.notification::<notif::DidChangeConfiguration>(Self::on_did_change_configuration)
// Workaround:
// NB. This handler is mandatory.
// > In former implementations clients pushed file events without the server actively asking for it.
// Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles
.notification::<notif::DidChangeWatchedFiles>(|_, _| ControlFlow::Continue(()))
.notification::<notif::DidChangeWatchedFiles>(Self::on_did_change_watched_files)
//// Requests ////
.request_snap::<req::GotoDefinition>(handler::goto_definition)
.request_snap::<req::References>(handler::references)
@ -112,7 +117,9 @@ impl Server {
//// Events ////
.event(Self::on_set_flake_info)
.event(Self::on_set_nixos_options)
.event(Self::on_update_config);
.event(Self::on_update_config)
// Loopback event.
.event(Self::on_did_change_watched_files);
router
}
@ -171,27 +178,78 @@ impl Server {
let _: Result<_, _> = self.client.show_message(msg);
}
// Always load flake.{nix,lock} for flake info.
for path in [
self.config.root_path.join(FLAKE_LOCK_FILE),
self.config.root_path.join(FLAKE_FILE),
] {
// TODO: Move file loading into a dedicated thread.
if let Ok(text) = std::fs::read_to_string(&path) {
let url = Url::from_vfs_path(&path.into());
self.set_vfs_file_content(&url, text);
}
}
// Load configurations before loading flake.
// The latter depends on `nix.binary`.
// FIXME: This is still racy since `on_did_open` can also trigger flake reloading and would
// read uninitialized configs.
self.spawn_reload_config();
// Make a virtual event to trigger loading of flake files for flake info.
let flake_files_changed_event = DidChangeWatchedFilesParams {
changes: [FLAKE_LOCK_FILE, FLAKE_FILE]
.into_iter()
.map(|name| {
let uri = Url::from_file_path(self.config.root_path.join(name))
.expect("Root must be absolute");
let typ = FileChangeType::CREATED;
FileEvent { uri, typ }
})
.collect(),
};
if self.capabilities.watch_files {
tokio::spawn({
let config = self.config.clone();
let caps = self.capabilities.clone();
let mut client = self.client.clone();
async move {
Self::register_watched_files(&config, &caps, &mut client).await;
let _: Result<_, _> = client.emit(flake_files_changed_event);
}
});
} else {
self.on_did_change_watched_files(flake_files_changed_event)?;
}
ControlFlow::Continue(())
}
async fn register_watched_files(
config: &Config,
caps: &NegotiatedCapabilities,
client: &mut ClientSocket,
) {
let to_watcher = |pat: &str| FileSystemWatcher {
glob_pattern: if caps.watch_files_relative_pattern {
let root_uri = Url::from_file_path(&config.root_path).expect("Must be absolute");
GlobPattern::Relative(RelativePattern {
base_uri: OneOf::Right(root_uri),
pattern: pat.into(),
})
} else {
GlobPattern::String(format!("{}/{}", config.root_path.display(), pat))
},
// All events.
kind: None,
};
let register_options = DidChangeWatchedFilesRegistrationOptions {
watchers: [FLAKE_LOCK_FILE, FLAKE_FILE].map(to_watcher).into(),
};
let params = RegistrationParams {
registrations: vec![Registration {
id: notif::DidChangeWatchedFiles::METHOD.into(),
method: notif::DidChangeWatchedFiles::METHOD.into(),
register_options: Some(serde_json::to_value(register_options).unwrap()),
}],
};
if let Err(err) = client.register_capability(params).await {
client.show_message_ext(
MessageType::ERROR,
format!("Failed to watch flake files: {err:#}"),
);
}
tracing::info!("Registered file watching for flake files");
}
fn on_did_open(&mut self, params: DidOpenTextDocumentParams) -> NotifyResult {
// Ignore the open event for unsupported files, thus all following interactions
// will error due to unopened files.
@ -286,6 +344,41 @@ impl Server {
ControlFlow::Continue(())
}
fn on_did_change_watched_files(&mut self, params: DidChangeWatchedFilesParams) -> NotifyResult {
tracing::debug!("Watched files changed: {params:?}");
let mut flake_files_changed = true;
for FileEvent { uri, typ } in &params.changes {
// Don't reload files maintained by the client.
if self.opened_files.contains_key(uri) {
continue;
}
let Ok(path) = uri.to_file_path() else { continue };
match *typ {
FileChangeType::CREATED | FileChangeType::CHANGED => {
if let Ok(text) = std::fs::read_to_string(&path) {
self.set_vfs_file_content(uri, text);
}
}
FileChangeType::DELETED => {
// TODO: Vfs file removal.
}
_ => continue,
}
if let Ok(relative) = path.strip_prefix(&self.config.root_path) {
if relative == Path::new(FLAKE_FILE) || relative == Path::new(FLAKE_LOCK_FILE) {
flake_files_changed = true;
}
}
}
if flake_files_changed {
self.spawn_load_flake_workspace();
}
ControlFlow::Continue(())
}
/// Spawn a task to (re)load the flake workspace via `flake.{nix,lock}`, including flake info,
/// NixOS options and outputs (TODO).
fn spawn_load_flake_workspace(&mut self) {
@ -650,7 +743,6 @@ impl Server {
// If this is the first load, load the flake workspace, which depends on `nix.binary`.
if !self.tried_flake_load {
self.tried_flake_load = true;
// TODO: Register file watcher for flake.lock.
self.spawn_load_flake_workspace();
}

View File

@ -110,6 +110,7 @@ rec {
# nix.out
nodejs
watchman # Required by coc.nvim for file watching.
jq
pre-commit