Skip to content
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

File watcher implementation & LSP DidChangeWatchedFiles notification #2653

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
91 changes: 91 additions & 0 deletions Cargo.lock

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

12 changes: 12 additions & 0 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,9 @@ impl Client {
did_change_configuration: Some(lsp::DynamicRegistrationClientCapabilities {
dynamic_registration: Some(false),
}),
did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
}),
workspace_folders: Some(true),
..Default::default()
}),
Expand Down Expand Up @@ -397,6 +400,15 @@ impl Client {
)
}

pub fn did_change_watched_files(
&self,
changes: Vec<lsp::FileEvent>,
) -> impl Future<Output = Result<()>> {
self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
changes,
})
}

// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ pub enum MethodCall {
ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams),
WorkspaceFolders,
WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
}

impl MethodCall {
Expand All @@ -248,6 +250,14 @@ impl MethodCall {
let params: lsp::ConfigurationParams = params.parse()?;
Self::WorkspaceConfiguration(params)
}
lsp::request::RegisterCapability::METHOD => {
let params: lsp::RegistrationParams = params.parse()?;
Self::RegisterCapability(params)
}
lsp::request::UnregisterCapability::METHOD => {
let params: lsp::UnregistrationParams = params.parse()?;
Self::UnregisterCapability(params)
}
_ => {
return Err(Error::Unhandled);
}
Expand Down
122 changes: 119 additions & 3 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ use helix_core::{
config::{default_syntax_loader, user_syntax_loader},
pos_at_coords, syntax, Selection,
};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{align_view, editor::ConfigEvent, theme, Align, Editor};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_pos_to_pos,
LspProgressMap,
};
use helix_view::{align_view, editor::ConfigEvent, theme, watcher, Align, Editor};
use serde_json::json;

use crate::{
Expand All @@ -24,7 +28,7 @@ use std::{
time::{Duration, Instant},
};

use anyhow::Error;
use anyhow::{Context, Error};

use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
Expand Down Expand Up @@ -696,6 +700,35 @@ impl Application {
.collect();
Ok(json!(result))
}
MethodCall::RegisterCapability(params) => {
for reg in params.registrations {
if let Err(e) = self.register(reg, server_id).await {
log::warn!("failed LSP dynamic registration: {}", e);
}
}
Ok(serde_json::Value::Null)
}
MethodCall::UnregisterCapability(params) => {
params
.unregisterations // "unregisterations" is a known typo in the LSP spec
.into_iter()
.map(|unreg| match unreg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
// TODO: unregister tokens from watcher once they're actually
// being saved somewhere
Ok(serde_json::Value::Null)
}
_ => Err(anyhow::anyhow!(
"attempt to register unsupported capability"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should say "unregister"?

Suggested change
"attempt to register unsupported capability"
"attempt to unregister unsupported capability"

)),
})
.collect::<anyhow::Result<_>>()
.map_err(|e| helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::InvalidParams,
message: e.to_string(),
data: None,
})
}
};

let language_server = match self.editor.language_servers.get_by_id(server_id) {
Expand All @@ -712,6 +745,89 @@ impl Application {
}
}

async fn register(&mut self, reg: lsp::Registration, server_id: usize) -> anyhow::Result<()> {
use lsp::{FileChangeType as Change, WatchKind};
use watcher::notify_event::EventKind;

match reg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
let opts: lsp::DidChangeWatchedFilesRegistrationOptions = reg
.register_options
.context("missing register options")
.and_then(|v| Ok(serde_json::from_value(v)?))?;

// need lsp wrapped in `Arc`, so can't use `Registry::find_by_id`
let language_server = self
.editor
.language_servers
.iter_clients()
.find(|c| c.id() == server_id)
.map(Arc::clone)
.unwrap();

let watcher = self
.editor
.file_watcher()
.context("file watching is unavailable")?;

// TODO: save watch tokens somewhere so they can be deregistered
// in `UnregisterCapability`. This state seems to belong with
// the individual `lsp::Client` but the current API doesn't allow
// mutation on clients without adding internal mutability.
for watch in opts.watchers {
let language_server = Arc::clone(&language_server);

let workspace = language_server
.workspace_folders()
.get(0)
.and_then(|f| f.uri.to_file_path().ok())
.context("language server lacks valid workspace folder")?;

let cb = move |event: &watcher::Event| {
// If no `kind` is specified, defaults to CREATED | CHANGED | DELETED.
let has = |op| watch.kind.map(|kind| kind.contains(op)).unwrap_or(true);
let typ = match event.kind {
EventKind::Create(_) if has(WatchKind::Create) => Change::CREATED,
EventKind::Modify(_) if has(WatchKind::Change) => Change::CHANGED,
EventKind::Remove(_) if has(WatchKind::Delete) => Change::DELETED,
_ => return,
};
let uri = match event
.paths
.get(0)
.and_then(|p| lsp::Url::from_file_path(p).ok())
{
Some(p) => p,
None => {
log::warn!("file watch event lacks valid path: {:?}", event);
return;
}
};

let events = vec![lsp::FileEvent { uri, typ }];
tokio::spawn(language_server.did_change_watched_files(events));
};

if let Err(e) = watcher
.register_glob(workspace, &watch.glob_pattern, cb)
.await
{
log::warn!(
"failed to set watch on glob `{}`: {}",
watch.glob_pattern,
e
);
}
}

Ok(())
}
_ => Err(anyhow::anyhow!(
"attempt to register unsupported capability"
)),
}
}

async fn claim_term(&mut self) -> Result<(), Error> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
Expand Down
3 changes: 3 additions & 0 deletions helix-view/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ log = "~0.4"

which = "4.2"

notify = "5.0.0-pre.15"
globset = "0.4.8"

[target.'cfg(windows)'.dependencies]
clipboard-win = { version = "4.4", features = ["std"] }

Expand Down
Loading