Skip to content

Replace Tokio Thread File Watcher with LSP Client File Watcher #7147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 1 addition & 94 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,6 @@ mark-flaky-tests = "1.0"
mdbook = { version = "0.4", default-features = false }
minifier = "0.3"
normalize-path = "0.2"
notify = "6.1"
notify-debouncer-mini = "0.4"
num-bigint = { version = "0.4", features = ["serde"] }
num-traits = "0.2"
object = "0.36"
Expand Down
2 changes: 0 additions & 2 deletions sway-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ forc-pkg.workspace = true
forc-tracing.workspace = true
forc-util.workspace = true
lsp-types = { workspace = true, features = ["proposed"] }
notify.workspace = true
notify-debouncer-mini.workspace = true
parking_lot.workspace = true
proc-macro2.workspace = true
quote.workspace = true
Expand Down
8 changes: 1 addition & 7 deletions sway-lsp/src/core/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,17 +107,11 @@ impl Session {
self.sync.clone_manifest_dir_to_temp()?;
// iterate over the project dir, parse all sway files
let _ = self.store_sway_files(documents).await;
self.sync.watch_and_sync_manifest();
self.sync.sync_manifest();
self.sync.manifest_dir().map_err(Into::into)
}

pub fn shutdown(&self) {
// shutdown the thread watching the manifest file
let handle = self.sync.notify_join_handle.read();
if let Some(join_handle) = &*handle {
join_handle.abort();
}

// Delete the temporary directory.
self.sync.remove_temp_dir();
}
Expand Down
55 changes: 4 additions & 51 deletions sway-lsp/src/core/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,16 @@ use dashmap::DashMap;
use forc_pkg::manifest::{GenericManifestFile, ManifestFile};
use forc_pkg::PackageManifestFile;
use lsp_types::Url;
use notify::RecursiveMode;
use notify_debouncer_mini::new_debouncer;
use parking_lot::RwLock;
use std::{
fs,
path::{Path, PathBuf},
time::Duration,
};
use sway_types::{SourceEngine, Span};
use sway_utils::{
constants::{LOCK_FILE_NAME, MANIFEST_FILE_NAME},
SWAY_EXTENSION,
};
use tempfile::Builder;
use tokio::task::JoinHandle;
use tracing::error;

#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Directory {
Expand All @@ -32,7 +26,6 @@ pub enum Directory {
#[derive(Debug)]
pub struct SyncWorkspace {
pub directories: DashMap<Directory, PathBuf>,
pub notify_join_handle: RwLock<Option<JoinHandle<()>>>,
}

impl SyncWorkspace {
Expand All @@ -41,21 +34,14 @@ impl SyncWorkspace {
pub(crate) fn new() -> Self {
Self {
directories: DashMap::new(),
notify_join_handle: RwLock::new(None),
}
}

/// Overwrite the contents of the tmp/folder with everything in
/// the current workspace.
pub fn resync(&self) -> Result<(), LanguageServerError> {
self.clone_manifest_dir_to_temp()?;
if let (Ok(manifest_dir), Some(manifest_path), Some(temp_manifest_path)) = (
self.manifest_dir(),
self.manifest_path(),
self.temp_manifest_path(),
) {
edit_manifest_dependency_paths(&manifest_dir, &manifest_path, &temp_manifest_path)?;
}
self.sync_manifest();
Ok(())
}

Expand Down Expand Up @@ -178,8 +164,8 @@ impl SyncWorkspace {
.ok()
}

/// Watch the manifest directory and check for any save events on Forc.toml
pub(crate) fn watch_and_sync_manifest(&self) {
/// Read the Forc.toml and convert relative paths to absolute. Save into our temp directory.
pub(crate) fn sync_manifest(&self) {
if let (Ok(manifest_dir), Some(manifest_path), Some(temp_manifest_path)) = (
self.manifest_dir(),
self.manifest_path(),
Expand All @@ -188,40 +174,7 @@ impl SyncWorkspace {
if let Err(err) =
edit_manifest_dependency_paths(&manifest_dir, &manifest_path, &temp_manifest_path)
{
error!("Failed to edit manifest dependency paths: {}", err);
}

let handle = tokio::spawn(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel(10);
// Setup debouncer. No specific tickrate, max debounce time 500 milliseconds
let mut debouncer = new_debouncer(Duration::from_millis(500), move |event| {
if let Ok(e) = event {
let _ = tx.blocking_send(e);
}
})
.unwrap();

debouncer
.watcher()
.watch(&manifest_dir, RecursiveMode::NonRecursive)
.unwrap();
while let Some(_events) = rx.recv().await {
// Rescan the Forc.toml and convert
// relative paths to absolute. Save into our temp directory.
if let Err(err) = edit_manifest_dependency_paths(
&manifest_dir,
&manifest_path,
&temp_manifest_path,
) {
error!("Failed to edit manifest dependency paths: {}", err);
}
}
});

// Store the join handle so we can clean up the thread on shutdown
{
let mut join_handle = self.notify_join_handle.write();
*join_handle = Some(handle);
tracing::error!("Failed to edit manifest dependency paths: {}", err);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions sway-lsp/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ pub enum LanguageServerError {
ProgramsIsNone,
#[error("Unable to acquire a semaphore permit for parsing")]
UnableToAcquirePermit,
#[error("Client is not initialized")]
ClientNotInitialized,
#[error("Client request error: {0}")]
ClientRequestError(String),
}

#[derive(Debug, Error, PartialEq, Eq)]
Expand Down
21 changes: 17 additions & 4 deletions sway-lsp/src/handlers/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,23 @@ pub(crate) async fn handle_did_change_watched_files(
params: DidChangeWatchedFilesParams,
) -> Result<(), LanguageServerError> {
for event in params.changes {
let (uri, _) = state.uri_and_session_from_workspace(&event.uri).await?;
if let FileChangeType::DELETED = event.typ {
state.pid_locked_files.remove_dirty_flag(&event.uri)?;
let _ = state.documents.remove_document(&uri);
let (uri, session) = state.uri_and_session_from_workspace(&event.uri).await?;

match event.typ {
FileChangeType::CHANGED => {
if event.uri.to_string().contains("Forc.toml") {
session.sync.sync_manifest();
// TODO: Recompile the project | see https://github.com/FuelLabs/sway/issues/7103
}
}
FileChangeType::DELETED => {
state.pid_locked_files.remove_dirty_flag(&event.uri)?;
let _ = state.documents.remove_document(&uri);
}
FileChangeType::CREATED => {
// TODO: handle this case
}
_ => {}
}
}
Ok(())
Expand Down
5 changes: 4 additions & 1 deletion sway-lsp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ impl LanguageServer for ServerState {
}

async fn initialized(&self, _: InitializedParams) {
let _p = tracing::trace_span!("parse_text").entered();
// Register a file system watcher for Forc.toml files with the client.
if let Err(err) = self.register_forc_toml_watcher().await {
tracing::error!("Failed to register Forc.toml file watcher: {}", err);
}
tracing::info!("Sway Language Server Initialized");
}

Expand Down
34 changes: 33 additions & 1 deletion sway-lsp/src/server_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use crossbeam_channel::{Receiver, Sender};
use dashmap::{mapref::multiple::RefMulti, DashMap};
use forc_pkg::manifest::GenericManifestFile;
use forc_pkg::PackageManifestFile;
use lsp_types::{Diagnostic, Url};
use lsp_types::{
Diagnostic, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, GlobPattern,
Registration, Url, WatchKind,
};
use parking_lot::{Mutex, RwLock};
use std::{
collections::{BTreeMap, VecDeque},
Expand Down Expand Up @@ -115,6 +118,35 @@ impl ServerState {
}
}

/// Registers a file system watcher for Forc.toml files with the client.
pub async fn register_forc_toml_watcher(&self) -> Result<(), LanguageServerError> {
let client = self
.client
.as_ref()
.ok_or(LanguageServerError::ClientNotInitialized)?;

let watchers = vec![FileSystemWatcher {
glob_pattern: GlobPattern::String("**/Forc.toml".to_string()),
kind: Some(WatchKind::Create | WatchKind::Change),
}];
let registration_options = DidChangeWatchedFilesRegistrationOptions { watchers };
let registration = Registration {
id: "forc-toml-watcher".to_string(),
method: "workspace/didChangeWatchedFiles".to_string(),
register_options: Some(
serde_json::to_value(registration_options)
.expect("Failed to serialize registration options"),
),
};

client
.register_capability(vec![registration])
.await
.map_err(|err| LanguageServerError::ClientRequestError(err.to_string()))?;

Ok(())
}

/// Spawns a new thread dedicated to handling compilation tasks. This thread listens for
/// `TaskMessage` instances sent over a channel and processes them accordingly.
///
Expand Down
Loading
Loading