Skip to content

Commit

Permalink
implement proper lsp-workspace support
Browse files Browse the repository at this point in the history
fix typo

Co-authored-by: LeoniePhiline <[email protected]>
  • Loading branch information
pascalkuthe and LeoniePhiline committed Mar 7, 2023
1 parent 8e8c85a commit 772e161
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 100 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

61 changes: 32 additions & 29 deletions helix-loader/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,38 @@ pub fn default_lang_config() -> toml::Value {

/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
let config = [crate::config_dir(), crate::find_workspace().join(".helix")]
.into_iter()
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read_to_string(file)
.map(|config| toml::from_str(&config))
.ok()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(default_lang_config(), |a, b| {
// combines for example
// b:
// [[language]]
// name = "toml"
// language-server = { command = "taplo", args = ["lsp", "stdio"] }
//
// a:
// [[language]]
// language-server = { command = "/usr/bin/taplo" }
//
// into:
// [[language]]
// name = "toml"
// language-server = { command = "/usr/bin/taplo" }
//
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(a, b, 3)
});
let config = [
crate::config_dir(),
crate::find_workspace().0.join(".helix"),
]
.into_iter()
.map(|path| path.join("languages.toml"))
.filter_map(|file| {
std::fs::read_to_string(file)
.map(|config| toml::from_str(&config))
.ok()
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(default_lang_config(), |a, b| {
// combines for example
// b:
// [[language]]
// name = "toml"
// language-server = { command = "taplo", args = ["lsp", "stdio"] }
//
// a:
// [[language]]
// language-server = { command = "/usr/bin/taplo" }
//
// into:
// [[language]]
// name = "toml"
// language-server = { command = "/usr/bin/taplo" }
//
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(a, b, 3)
});

Ok(config)
}
15 changes: 10 additions & 5 deletions helix-loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub fn config_file() -> PathBuf {
}

pub fn workspace_config_file() -> PathBuf {
find_workspace().join(".helix").join("config.toml")
find_workspace().0.join(".helix").join("config.toml")
}

pub fn lang_config_file() -> PathBuf {
Expand Down Expand Up @@ -230,14 +230,19 @@ mod merge_toml_tests {
}

/// Finds the current workspace folder.
/// Used as a ceiling dir for root resolve, for the filepicker and other related
pub fn find_workspace() -> PathBuf {
/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root
///
/// This function starts searching the FS upward from the CWD
/// and returns the first directory that contains either `.git` or `.helix`.
/// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return ancestor.to_owned();
return (ancestor.to_owned(), false);
}
}

current_dir
(current_dir, true)
}
1 change: 1 addition & 0 deletions helix-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ thiserror = "1.0"
tokio = { version = "1.26", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.12"
which = "4.4"
parking_lot = "0.12.1"
183 changes: 160 additions & 23 deletions helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
use crate::{
find_root, jsonrpc,
find_lsp_workspace, jsonrpc,
transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result,
};

use helix_core::{ChangeSet, Rope};
use helix_core::{find_workspace, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::PositionEncodingKind;
use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp;
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::future::Future;
Expand All @@ -26,6 +30,17 @@ use tokio::{
},
};

fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
lsp::WorkspaceFolder {
name: uri
.path_segments()
.and_then(|segments| segments.last())
.map(|basename| basename.to_string())
.unwrap_or_default(),
uri,
}
}

#[derive(Debug)]
pub struct Client {
id: usize,
Expand All @@ -36,11 +51,126 @@ pub struct Client {
config: Option<Value>,
root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>,
workspace_folders: Vec<lsp::WorkspaceFolder>,
workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
initalize_notify: Arc<Notify>,
/// workspace folders added while the server is still initalizing
req_timeout: u64,
}

impl Client {
pub fn try_add_doc(
self: &Arc<Self>,
root_markers: &[String],
manual_roots: &[PathBuf],
doc_path: Option<&std::path::PathBuf>,
may_support_workspace: bool,
) -> bool {
let (workspace, workspace_is_cwd) = find_workspace();
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
&workspace,
workspace_is_cwd,
);
let root_uri = root
.as_ref()
.and_then(|root| lsp::Url::from_file_path(root).ok());

if self.root_path == root.unwrap_or(workspace)
|| root_uri.as_ref().map_or(false, |root_uri| {
self.workspace_folders
.lock()
.iter()
.any(|workspace| &workspace.uri == root_uri)
})
{
// workspace URI is already register we can use this client
return true;
}

// this server definitly doesn't support muiltiple workspace, no need to check capabilities
if !may_support_workspace {
return false;
}

// TODO refactor to use let .. else when msrv is raised to 1.65
let capabilities = if let Some(capabilities) = self.capabilities.get() {
capabilities
} else {
let client = Arc::clone(self);
// initalization hasn't finished yet, deal with this new root later
// TODO: In the edgecase that a **new root** is added
// for an LSP that **doesn't support workspace_folders** before initaliation is finished
// the new roots are ignored.
// That particular edgecase would require retroactively spawning new LSP
// clients and therefore also require us to retroactively update the corresponding
// documents LSP client handle. It's doable but a pretty weird edgecase so let's
// wait and see if anyone ever runs into it.
tokio::spawn(async move {
client.initalize_notify.notified().await;
if let Some(workspace_folders_caps) = client
.capabilities()
.workspace
.as_ref()
.and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false))
{
client.add_workspace_folder(
root_uri,
&workspace_folders_caps.change_notifications,
);
}
});
return true;
};

if let Some(workspace_folders_caps) = capabilities
.workspace
.as_ref()
.and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false))
{
self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
true
} else {
// the server doesn't support multi workspaces, we need a new client
false
}
}

fn add_workspace_folder(
&self,
root_uri: Option<lsp::Url>,
change_notifications: &Option<OneOf<bool, String>>,
) {
// root_uri is None just means that there isn't really any LSP workspace
// associated with this file. For servers that support multiple workspaces
// there is just one server so we can always just use that shared instance.
// No need to add a new workspace root here as there is no logical root for this file
// let the server deal with this
// TODO: Refactor to use let .. else once MSRV reaches 1.65
let root_uri = if let Some(root_uri) = root_uri {
root_uri
} else {
return;
};

// server supports workspace folders, let's add the new root to the list
self.workspace_folders
.lock()
.push(workspace_for_uri(root_uri.clone()));
if &Some(OneOf::Left(false)) == change_notifications {
// server specifically opted out of DidWorkspaceChange notifications
// let's assume the server will request the workspace folders himself
// and that we can therefore reuse the client (but are done now)
return;
}
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
}

#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
pub fn start(
Expand Down Expand Up @@ -76,30 +206,25 @@ impl Client {

let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id);

let root_path = find_root(
let (workspace, workspace_is_cwd) = find_workspace();
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
&workspace,
workspace_is_cwd,
);

let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());

// TODO: support multiple workspace folders
let workspace_folders = root_uri
.clone()
.map(|root| {
vec![lsp::WorkspaceFolder {
name: root
.path_segments()
.and_then(|segments| segments.last())
.map(|basename| basename.to_string())
.unwrap_or_default(),
uri: root,
}]
})
.map(|root| vec![workspace_for_uri(root)])
.unwrap_or_default();

let client = Self {
Expand All @@ -110,10 +235,10 @@ impl Client {
capabilities: OnceCell::new(),
config,
req_timeout,

root_path,
root_uri,
workspace_folders,
workspace_folders: Mutex::new(workspace_folders),
initalize_notify: initialize_notify.clone(),
};

Ok((client, server_rx, initialize_notify))
Expand Down Expand Up @@ -169,8 +294,10 @@ impl Client {
self.config.as_ref()
}

pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] {
&self.workspace_folders
pub async fn workspace_folders(
&self,
) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> {
self.workspace_folders.lock()
}

/// Execute a RPC request on the language server.
Expand Down Expand Up @@ -298,7 +425,7 @@ impl Client {
#[allow(deprecated)]
let params = lsp::InitializeParams {
process_id: Some(std::process::id()),
workspace_folders: Some(self.workspace_folders.clone()),
workspace_folders: Some(self.workspace_folders.lock().clone()),
// root_path is obsolete, but some clients like pyright still use it so we specify both.
// clients will prefer _uri if possible
root_path: self.root_path.to_str().map(|path| path.to_owned()),
Expand Down Expand Up @@ -450,6 +577,16 @@ impl Client {
)
}

pub fn did_change_workspace(
&self,
added: Vec<WorkspaceFolder>,
removed: Vec<WorkspaceFolder>,
) -> impl Future<Output = Result<()>> {
self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
event: WorkspaceFoldersChangeEvent { added, removed },
})
}

// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 772e161

Please sign in to comment.