on master help sign in
implement proper lsp-workspace support
5b3dd6aPascal Kuthe
Cargo.lock+1
@@ -1149,6 +1149,7 @@ dependencies = [
1149 "helix-parsec",
1149 "helix-parsec",
1150 "log",
1150 "log",
1151 "lsp-types",
1151 "lsp-types",
1152+ "parking_lot",
1152 "serde",
1153 "serde",
1153 "serde_json",
1154 "serde_json",
1154 "thiserror",
1155 "thiserror",
book/src/languages.md+1−1
@@ -64,7 +64,7 @@ These configuration keys are available:
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
64| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
65| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
65| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
66| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
66| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` |
67−| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | `` |
67+| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
68
68
69### File-type detection and the `file-types` key
69### File-type detection and the `file-types` key
70
70
helix-loader/src/config.rs+32−29
@@ -9,35 +9,38 @@ pub fn default_lang_config() -> toml::Value {
9
9
10/// User configured languages.toml file, merged with the default config.
10/// User configured languages.toml file, merged with the default config.
11pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
11pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
12+ let config = [
13+ crate::config_dir(),
12− let config = [crate::config_dir(), crate::find_workspace().join(".helix")]
14+ crate::find_workspace().0.join(".helix"),
15+ ]
13− .into_iter()
16+ .into_iter()
14− .map(|path| path.join("languages.toml"))
17+ .map(|path| path.join("languages.toml"))
15− .filter_map(|file| {
18+ .filter_map(|file| {
16− std::fs::read_to_string(file)
19+ std::fs::read_to_string(file)
17− .map(|config| toml::from_str(&config))
20+ .map(|config| toml::from_str(&config))
18− .ok()
21+ .ok()
19− })
22+ })
20− .collect::<Result<Vec<_>, _>>()?
23+ .collect::<Result<Vec<_>, _>>()?
21− .into_iter()
24+ .into_iter()
22− .fold(default_lang_config(), |a, b| {
25+ .fold(default_lang_config(), |a, b| {
23− // combines for example
26+ // combines for example
24− // b:
27+ // b:
25− // [[language]]
28+ // [[language]]
26− // name = "toml"
29+ // name = "toml"
27− // language-server = { command = "taplo", args = ["lsp", "stdio"] }
30+ // language-server = { command = "taplo", args = ["lsp", "stdio"] }
28− //
31+ //
29− // a:
32+ // a:
30− // [[language]]
33+ // [[language]]
31− // language-server = { command = "/usr/bin/taplo" }
34+ // language-server = { command = "/usr/bin/taplo" }
32− //
35+ //
33− // into:
36+ // into:
34− // [[language]]
37+ // [[language]]
35− // name = "toml"
38+ // name = "toml"
36− // language-server = { command = "/usr/bin/taplo" }
39+ // language-server = { command = "/usr/bin/taplo" }
37− //
40+ //
38− // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
41+ // thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
39− crate::merge_toml_values(a, b, 3)
42+ crate::merge_toml_values(a, b, 3)
40− });
43+ });
41
44
42 Ok(config)
45 Ok(config)
43}
46}
helix-loader/src/lib.rs+10−5
@@ -129,7 +129,7 @@ pub fn config_file() -> PathBuf {
129}
129}
130
130
131pub fn workspace_config_file() -> PathBuf {
131pub fn workspace_config_file() -> PathBuf {
132− find_workspace().join(".helix").join("config.toml")
132+ find_workspace().0.join(".helix").join("config.toml")
133}
133}
134
134
135pub fn lang_config_file() -> PathBuf {
135pub fn lang_config_file() -> PathBuf {
@@ -283,14 +283,19 @@ mod merge_toml_tests {
283}
283}
284
284
285/// Finds the current workspace folder.
285/// Finds the current workspace folder.
286−/// Used as a ceiling dir for root resolve, for the filepicker and other related
286+/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root
287+///
288+/// This function starts searching the FS upward from the CWD
289+/// and returns the first directory that contains either `.git` or `.helix`.
290+/// If no workspace was found returns (CWD, true).
291+/// Otherwise (workspace, false) is returned
287−pub fn find_workspace() -> PathBuf {
292+pub fn find_workspace() -> (PathBuf, bool) {
288 let current_dir = std::env::current_dir().expect("unable to determine current directory");
293 let current_dir = std::env::current_dir().expect("unable to determine current directory");
289 for ancestor in current_dir.ancestors() {
294 for ancestor in current_dir.ancestors() {
290 if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
295 if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
291− return ancestor.to_owned();
296+ return (ancestor.to_owned(), false);
292 }
297 }
293 }
298 }
294
299
295− current_dir
300+ (current_dir, true)
296}
301}
helix-lsp/Cargo.toml+1
@@ -27,3 +27,4 @@ thiserror = "1.0"
27tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
27tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
28tokio-stream = "0.1.12"
28tokio-stream = "0.1.12"
29which = "4.4"
29which = "4.4"
30+parking_lot = "0.12.1"
helix-lsp/src/client.rs+154−23
@@ -1,13 +1,17 @@
1use crate::{
1use crate::{
2− find_root, jsonrpc,
2+ find_lsp_workspace, jsonrpc,
3 transport::{Payload, Transport},
3 transport::{Payload, Transport},
4 Call, Error, OffsetEncoding, Result,
4 Call, Error, OffsetEncoding, Result,
5};
5};
6
6
7−use helix_core::{ChangeSet, Rope};
7+use helix_core::{find_workspace, ChangeSet, Rope};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
8use helix_loader::{self, VERSION_AND_GIT_HASH};
9+use lsp::{
10+ notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
9−use lsp::PositionEncodingKind;
11+ PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
12+};
10use lsp_types as lsp;
13use lsp_types as lsp;
14+use parking_lot::Mutex;
11use serde::Deserialize;
15use serde::Deserialize;
12use serde_json::Value;
16use serde_json::Value;
13use std::future::Future;
17use std::future::Future;
@@ -26,6 +30,17 @@ use tokio::{
26 },
30 },
27};
31};
28
32
33+fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
34+ lsp::WorkspaceFolder {
35+ name: uri
36+ .path_segments()
37+ .and_then(|segments| segments.last())
38+ .map(|basename| basename.to_string())
39+ .unwrap_or_default(),
40+ uri,
41+ }
42+}
43+
29#[derive(Debug)]
44#[derive(Debug)]
30pub struct Client {
45pub struct Client {
31 id: usize,
46 id: usize,
@@ -36,11 +51,120 @@ pub struct Client {
36 config: Option<Value>,
51 config: Option<Value>,
37 root_path: std::path::PathBuf,
52 root_path: std::path::PathBuf,
38 root_uri: Option<lsp::Url>,
53 root_uri: Option<lsp::Url>,
39− workspace_folders: Vec<lsp::WorkspaceFolder>,
54+ workspace_folders: Mutex<Vec<lsp::WorkspaceFolder>>,
55+ initalize_notify: Arc<Notify>,
56+ /// workspace folders added while the server is still initalizing
40 req_timeout: u64,
57 req_timeout: u64,
41}
58}
42
59
43impl Client {
60impl Client {
61+ pub fn try_add_doc(
62+ self: &Arc<Self>,
63+ root_markers: &[String],
64+ manual_roots: &[PathBuf],
65+ doc_path: Option<&std::path::PathBuf>,
66+ may_support_workspace: bool,
67+ ) -> bool {
68+ let (workspace, workspace_is_cwd) = find_workspace();
69+ let root = find_lsp_workspace(
70+ doc_path
71+ .and_then(|x| x.parent().and_then(|x| x.to_str()))
72+ .unwrap_or("."),
73+ root_markers,
74+ manual_roots,
75+ &workspace,
76+ workspace_is_cwd,
77+ );
78+ let root_uri = root
79+ .as_ref()
80+ .and_then(|root| lsp::Url::from_file_path(root).ok());
81+
82+ if self.root_path == root.unwrap_or(workspace)
83+ || root_uri.as_ref().map_or(false, |root_uri| {
84+ self.workspace_folders
85+ .lock()
86+ .iter()
87+ .any(|workspace| &workspace.uri == root_uri)
88+ })
89+ {
90+ // workspace URI is already registered so we can use this client
91+ return true;
92+ }
93+
94+ // this server definitly doesn't support multiple workspace, no need to check capabilities
95+ if !may_support_workspace {
96+ return false;
97+ }
98+
99+ let Some(capabilities) = self.capabilities.get() else {
100+ let client = Arc::clone(self);
101+ // initalization hasn't finished yet, deal with this new root later
102+ // TODO: In the edgecase that a **new root** is added
103+ // for an LSP that **doesn't support workspace_folders** before initaliation is finished
104+ // the new roots are ignored.
105+ // That particular edgecase would require retroactively spawning new LSP
106+ // clients and therefore also require us to retroactively update the corresponding
107+ // documents LSP client handle. It's doable but a pretty weird edgecase so let's
108+ // wait and see if anyone ever runs into it.
109+ tokio::spawn(async move {
110+ client.initalize_notify.notified().await;
111+ if let Some(workspace_folders_caps) = client
112+ .capabilities()
113+ .workspace
114+ .as_ref()
115+ .and_then(|cap| cap.workspace_folders.as_ref())
116+ .filter(|cap| cap.supported.unwrap_or(false))
117+ {
118+ client.add_workspace_folder(
119+ root_uri,
120+ &workspace_folders_caps.change_notifications,
121+ );
122+ }
123+ });
124+ return true;
125+ };
126+
127+ if let Some(workspace_folders_caps) = capabilities
128+ .workspace
129+ .as_ref()
130+ .and_then(|cap| cap.workspace_folders.as_ref())
131+ .filter(|cap| cap.supported.unwrap_or(false))
132+ {
133+ self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
134+ true
135+ } else {
136+ // the server doesn't support multi workspaces, we need a new client
137+ false
138+ }
139+ }
140+
141+ fn add_workspace_folder(
142+ &self,
143+ root_uri: Option<lsp::Url>,
144+ change_notifications: &Option<OneOf<bool, String>>,
145+ ) {
146+ // root_uri is None just means that there isn't really any LSP workspace
147+ // associated with this file. For servers that support multiple workspaces
148+ // there is just one server so we can always just use that shared instance.
149+ // No need to add a new workspace root here as there is no logical root for this file
150+ // let the server deal with this
151+ let Some(root_uri) = root_uri else {
152+ return;
153+ };
154+
155+ // server supports workspace folders, let's add the new root to the list
156+ self.workspace_folders
157+ .lock()
158+ .push(workspace_for_uri(root_uri.clone()));
159+ if &Some(OneOf::Left(false)) == change_notifications {
160+ // server specifically opted out of DidWorkspaceChange notifications
161+ // let's assume the server will request the workspace folders itself
162+ // and that we can therefore reuse the client (but are done now)
163+ return;
164+ }
165+ tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
166+ }
167+
44 #[allow(clippy::type_complexity)]
168 #[allow(clippy::type_complexity)]
45 #[allow(clippy::too_many_arguments)]
169 #[allow(clippy::too_many_arguments)]
46 pub fn start(
170 pub fn start(
@@ -76,30 +200,25 @@ impl Client {
76
200
77 let (server_rx, server_tx, initialize_notify) =
201 let (server_rx, server_tx, initialize_notify) =
78 Transport::start(reader, writer, stderr, id);
202 Transport::start(reader, writer, stderr, id);
79−
203+ let (workspace, workspace_is_cwd) = find_workspace();
80− let root_path = find_root(
204+ let root = find_lsp_workspace(
81 doc_path
205 doc_path
82 .and_then(|x| x.parent().and_then(|x| x.to_str()))
206 .and_then(|x| x.parent().and_then(|x| x.to_str()))
83 .unwrap_or("."),
207 .unwrap_or("."),
84 root_markers,
208 root_markers,
85 manual_roots,
209 manual_roots,
210+ &workspace,
211+ workspace_is_cwd,
86 );
212 );
87
213
214+ // `root_uri` and `workspace_folder` can be empty in case there is no workspace
215+ // `root_url` can not, use `workspace` as a fallback
216+ let root_path = root.clone().unwrap_or_else(|| workspace.clone());
88− let root_uri = lsp::Url::from_file_path(root_path.clone()).ok();
217+ let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
89
218
90− // TODO: support multiple workspace folders
91 let workspace_folders = root_uri
219 let workspace_folders = root_uri
92 .clone()
220 .clone()
93− .map(|root| {
221+ .map(|root| vec![workspace_for_uri(root)])
94− vec![lsp::WorkspaceFolder {
95− name: root
96− .path_segments()
97− .and_then(|segments| segments.last())
98− .map(|basename| basename.to_string())
99− .unwrap_or_default(),
100− uri: root,
101− }]
102− })
103 .unwrap_or_default();
222 .unwrap_or_default();
104
223
105 let client = Self {
224 let client = Self {
@@ -110,10 +229,10 @@ impl Client {
110 capabilities: OnceCell::new(),
229 capabilities: OnceCell::new(),
111 config,
230 config,
112 req_timeout,
231 req_timeout,
113−
114 root_path,
232 root_path,
115 root_uri,
233 root_uri,
116− workspace_folders,
234+ workspace_folders: Mutex::new(workspace_folders),
235+ initalize_notify: initialize_notify.clone(),
117 };
236 };
118
237
119 Ok((client, server_rx, initialize_notify))
238 Ok((client, server_rx, initialize_notify))
@@ -169,8 +288,10 @@ impl Client {
169 self.config.as_ref()
288 self.config.as_ref()
170 }
289 }
171
290
172− pub fn workspace_folders(&self) -> &[lsp::WorkspaceFolder] {
291+ pub async fn workspace_folders(
292+ &self,
293+ ) -> parking_lot::MutexGuard<'_, Vec<lsp::WorkspaceFolder>> {
173− &self.workspace_folders
294+ self.workspace_folders.lock()
174 }
295 }
175
296
176 /// Execute a RPC request on the language server.
297 /// Execute a RPC request on the language server.
@@ -298,7 +419,7 @@ impl Client {
298 #[allow(deprecated)]
419 #[allow(deprecated)]
299 let params = lsp::InitializeParams {
420 let params = lsp::InitializeParams {
300 process_id: Some(std::process::id()),
421 process_id: Some(std::process::id()),
301− workspace_folders: Some(self.workspace_folders.clone()),
422+ workspace_folders: Some(self.workspace_folders.lock().clone()),
302 // root_path is obsolete, but some clients like pyright still use it so we specify both.
423 // root_path is obsolete, but some clients like pyright still use it so we specify both.
303 // clients will prefer _uri if possible
424 // clients will prefer _uri if possible
304 root_path: self.root_path.to_str().map(|path| path.to_owned()),
425 root_path: self.root_path.to_str().map(|path| path.to_owned()),
@@ -469,6 +590,16 @@ impl Client {
469 )
590 )
470 }
591 }
471
592
593+ pub fn did_change_workspace(
594+ &self,
595+ added: Vec<WorkspaceFolder>,
596+ removed: Vec<WorkspaceFolder>,
597+ ) -> impl Future<Output = Result<()>> {
598+ self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
599+ event: WorkspaceFoldersChangeEvent { added, removed },
600+ })
601+ }
602+
472 // -------------------------------------------------------------------------------------------
603 // -------------------------------------------------------------------------------------------
473 // Text document
604 // Text document
474 // -------------------------------------------------------------------------------------------
605 // -------------------------------------------------------------------------------------------
helix-lsp/src/lib.rs+69−47
@@ -10,15 +10,12 @@ pub use lsp::{Position, Url};
10pub use lsp_types as lsp;
10pub use lsp_types as lsp;
11
11
12use futures_util::stream::select_all::SelectAll;
12use futures_util::stream::select_all::SelectAll;
13−use helix_core::{
14− find_workspace,
15− syntax::{LanguageConfiguration, LanguageServerConfiguration},
13+use helix_core::syntax::{LanguageConfiguration, LanguageServerConfiguration};
16−};
17use tokio::sync::mpsc::UnboundedReceiver;
14use tokio::sync::mpsc::UnboundedReceiver;
18
15
19use std::{
16use std::{
20 collections::{hash_map::Entry, HashMap},
17 collections::{hash_map::Entry, HashMap},
21− path::PathBuf,
18+ path::{Path, PathBuf},
22 sync::{
19 sync::{
23 atomic::{AtomicUsize, Ordering},
20 atomic::{AtomicUsize, Ordering},
24 Arc,
21 Arc,
@@ -609,7 +606,7 @@ impl Notification {
609
606
610#[derive(Debug)]
607#[derive(Debug)]
611pub struct Registry {
608pub struct Registry {
612− inner: HashMap<LanguageId, (usize, Arc<Client>)>,
609+ inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
613
610
614 counter: AtomicUsize,
611 counter: AtomicUsize,
615 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
612 pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
@@ -633,12 +630,16 @@ impl Registry {
633 pub fn get_by_id(&self, id: usize) -> Option<&Client> {
630 pub fn get_by_id(&self, id: usize) -> Option<&Client> {
634 self.inner
631 self.inner
635 .values()
632 .values()
633+ .flatten()
636 .find(|(client_id, _)| client_id == &id)
634 .find(|(client_id, _)| client_id == &id)
637 .map(|(_, client)| client.as_ref())
635 .map(|(_, client)| client.as_ref())
638 }
636 }
639
637
640 pub fn remove_by_id(&mut self, id: usize) {
638 pub fn remove_by_id(&mut self, id: usize) {
639+ self.inner.retain(|_, clients| {
641− self.inner.retain(|_, (client_id, _)| client_id != &id)
640+ clients.retain(|&(client_id, _)| client_id != id);
641+ !clients.is_empty()
642+ })
642 }
643 }
643
644
644 pub fn restart(
645 pub fn restart(
@@ -664,11 +665,13 @@ impl Registry {
664 start_client(id, language_config, config, doc_path, root_dirs)?;
665 start_client(id, language_config, config, doc_path, root_dirs)?;
665 self.incoming.push(UnboundedReceiverStream::new(incoming));
666 self.incoming.push(UnboundedReceiverStream::new(incoming));
666
667
667− let (_, old_client) = entry.insert((id, client.clone()));
668+ let old_clients = entry.insert(vec![(id, client.clone())]);
668
669
670+ for (_, old_client) in old_clients {
669− tokio::spawn(async move {
671+ tokio::spawn(async move {
670− let _ = old_client.force_shutdown().await;
672+ let _ = old_client.force_shutdown().await;
671− });
673+ });
674+ }
672
675
673 Ok(Some(client))
676 Ok(Some(client))
674 }
677 }
@@ -678,10 +681,12 @@ impl Registry {
678 pub fn stop(&mut self, language_config: &LanguageConfiguration) {
681 pub fn stop(&mut self, language_config: &LanguageConfiguration) {
679 let scope = language_config.scope.clone();
682 let scope = language_config.scope.clone();
680
683
681− if let Some((_, client)) = self.inner.remove(&scope) {
684+ if let Some(clients) = self.inner.remove(&scope) {
685+ for (_, client) in clients {
682− tokio::spawn(async move {
686+ tokio::spawn(async move {
683− let _ = client.force_shutdown().await;
687+ let _ = client.force_shutdown().await;
684− });
688+ });
689+ }
685 }
690 }
686 }
691 }
687
692
@@ -696,24 +701,25 @@ impl Registry {
696 None => return Ok(None),
701 None => return Ok(None),
697 };
702 };
698
703
699− match self.inner.entry(language_config.scope.clone()) {
704+ let clients = self.inner.entry(language_config.scope.clone()).or_default();
700− Entry::Occupied(entry) => Ok(Some(entry.get().1.clone())),
705+ // check if we already have a client for this documents root that we can reuse
701− Entry::Vacant(entry) => {
706+ if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| {
702− // initialize a new client
707+ client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
703− let id = self.counter.fetch_add(1, Ordering::Relaxed);
704−
705− let NewClientResult(client, incoming) =
706− start_client(id, language_config, config, doc_path, root_dirs)?;
707− self.incoming.push(UnboundedReceiverStream::new(incoming));
708−
709− entry.insert((id, client.clone()));
710− Ok(Some(client))
711− }
708+ }) {
709+ return Ok(Some(client.1.clone()));
712 }
710 }
711+ // initialize a new client
712+ let id = self.counter.fetch_add(1, Ordering::Relaxed);
713+
714+ let NewClientResult(client, incoming) =
715+ start_client(id, language_config, config, doc_path, root_dirs)?;
716+ clients.push((id, client.clone()));
717+ self.incoming.push(UnboundedReceiverStream::new(incoming));
718+ Ok(Some(client))
713 }
719 }
714
720
715 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
721 pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
716− self.inner.values().map(|(_, client)| client)
722+ self.inner.values().flatten().map(|(_, client)| client)
717 }
723 }
718}
724}
719
725
@@ -850,16 +856,23 @@ fn start_client(
850 Ok(NewClientResult(client, incoming))
856 Ok(NewClientResult(client, incoming))
851}
857}
852
858
853−/// Find an LSP root of a file using the following mechansim:
859+/// Find an LSP workspace of a file using the following mechanism:
854−/// * start at `file` (either an absolute path or relative to CWD)
860+/// * if the file is outside `workspace` return `None`
861+/// * start at `file` and search the file tree upward
862+/// * stop the search at the first `root_dirs` entry that contains `file`
863+/// * if no `root_dirs` matchs `file` stop at workspace
855−/// * find the top most directory containing a root_marker
864+/// * Returns the top most directory that contains a `root_marker`
856−/// * inside the current workspace
865+/// * If no root marker and we stopped at a `root_dirs` entry, return the directory we stopped at
857−/// * stop the search at the first root_dir that contains `file` or the workspace (obtained from `helix_core::find_workspace`)
866+/// * If we stopped at `workspace` instead and `workspace_is_cwd == false` return `None`
858−/// * root_dirs only apply inside the workspace. For files outside of the workspace they are ignored
867+/// * If we stopped at `workspace` instead and `workspace_is_cwd == true` return `workspace`
859−/// * outside the current workspace: keep searching to the top of the file hiearchy
868+pub fn find_lsp_workspace(
869+ file: &str,
860−pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) -> PathBuf {
870+ root_markers: &[String],
871+ root_dirs: &[PathBuf],
872+ workspace: &Path,
873+ workspace_is_cwd: bool,
874+) -> Option<PathBuf> {
861 let file = std::path::Path::new(file);
875 let file = std::path::Path::new(file);
862− let workspace = find_workspace();
863 let file = if file.is_absolute() {
876 let file = if file.is_absolute() {
864 file.to_path_buf()
877 file.to_path_buf()
865 } else {
878 } else {
@@ -867,7 +880,9 @@ pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) ->
867 current_dir.join(file)
880 current_dir.join(file)
868 };
881 };
869
882
870− let inside_workspace = file.strip_prefix(&workspace).is_ok();
883+ if !file.starts_with(workspace) {
884+ return None;
885+ }
871
886
872 let mut top_marker = None;
887 let mut top_marker = None;
873 for ancestor in file.ancestors() {
888 for ancestor in file.ancestors() {
@@ -878,18 +893,25 @@ pub fn find_root(file: &str, root_markers: &[String], root_dirs: &[PathBuf]) ->
878 top_marker = Some(ancestor);
893 top_marker = Some(ancestor);
879 }
894 }
880
895
881− if inside_workspace
882− && (ancestor == workspace
883− || root_dirs
896+ if root_dirs
884− .iter()
897+ .iter()
885− .any(|root_dir| root_dir == ancestor.strip_prefix(&workspace).unwrap()))
898+ .any(|root_dir| root_dir == ancestor.strip_prefix(workspace).unwrap())
886 {
899 {
900+ // if the worskapce is the cwd do not search any higher for workspaces
901+ // but specify
887− return top_marker.unwrap_or(ancestor).to_owned();
902+ return Some(top_marker.unwrap_or(workspace).to_owned());
903+ }
904+ if ancestor == workspace {
905+ // if the workspace is the CWD, let the LSP decide what the workspace
906+ // is
907+ return top_marker
908+ .or_else(|| (!workspace_is_cwd).then_some(workspace))
909+ .map(Path::to_owned);
888 }
910 }
889 }
911 }
890
912
891− // If no root was found use the workspace as a fallback
913+ debug_assert!(false, "workspace must be an ancestor of <file>");
892− workspace
914+ None
893}
915}
894
916
895#[cfg(test)]
917#[cfg(test)]
helix-term/src/application.rs+1−1
@@ -1018,7 +1018,7 @@ impl Application {
1018 let language_server =
1018 let language_server =
1019 self.editor.language_servers.get_by_id(server_id).unwrap();
1019 self.editor.language_servers.get_by_id(server_id).unwrap();
1020
1020
1021− Ok(json!(language_server.workspace_folders()))
1021+ Ok(json!(&*language_server.workspace_folders().await))
1022 }
1022 }
1023 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1023 Ok(MethodCall::WorkspaceConfiguration(params)) => {
1024 let result: Vec<_> = params
1024 let result: Vec<_> = params
helix-term/src/commands.rs+1−1
@@ -2419,7 +2419,7 @@ fn append_mode(cx: &mut Context) {
2419}
2419}
2420
2420
2421fn file_picker(cx: &mut Context) {
2421fn file_picker(cx: &mut Context) {
2422− let root = find_workspace();
2422+ let root = find_workspace().0;
2423 let picker = ui::file_picker(root, &cx.editor.config());
2423 let picker = ui::file_picker(root, &cx.editor.config());
2424 cx.push_layer(Box::new(overlayed(picker)));
2424 cx.push_layer(Box::new(overlayed(picker)));
2425}
2425}