Add required-root-patterns for situational lsp activation (#8696)
* Added required-root-patterns for situational lsp activation using globbing * Replaced filter_map with flatten * updated book to include required-root-patterns option * fixed wrong function name for path * Added globset to helix-core. Moved globset building to config parsing. * Normalize implements AsRef * cargo fmt * Revert "cargo fmt" This reverts commit ca8ce123e8d77d2ae8ed84d5273a9b554101b0db.
This commit is contained in:
parent
ac8d1f62a1
commit
6a90166d0a
5 changed files with 81 additions and 42 deletions
|
@ -122,13 +122,14 @@ languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT
|
|||
|
||||
These are the available options for a language server.
|
||||
|
||||
| Key | Description |
|
||||
| ---- | ----------- |
|
||||
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
|
||||
| `args` | A list of arguments to pass to the language server binary |
|
||||
| `config` | LSP initialization options |
|
||||
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
|
||||
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
|
||||
| Key | Description |
|
||||
| ---- | ----------- |
|
||||
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
|
||||
| `args` | A list of arguments to pass to the language server binary |
|
||||
| `config` | LSP initialization options |
|
||||
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
|
||||
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
|
||||
| `required-root-patterns` | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found. |
|
||||
|
||||
A `format` sub-table within `config` can be used to pass extra formatting options to
|
||||
[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
|
||||
|
|
|
@ -53,6 +53,7 @@ globset = "0.4.14"
|
|||
|
||||
nucleo.workspace = true
|
||||
parking_lot = "0.12"
|
||||
globset = "0.4.14"
|
||||
|
||||
[dev-dependencies]
|
||||
quickcheck = { version = "1", default-features = false }
|
||||
|
|
|
@ -10,6 +10,7 @@ use crate::{
|
|||
use ahash::RandomState;
|
||||
use arc_swap::{ArcSwap, Guard};
|
||||
use bitflags::bitflags;
|
||||
use globset::GlobSet;
|
||||
use hashbrown::raw::RawTable;
|
||||
use slotmap::{DefaultKey as LayerId, HopSlotMap};
|
||||
|
||||
|
@ -365,6 +366,22 @@ where
|
|||
serializer.end()
|
||||
}
|
||||
|
||||
fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let patterns = Vec::<String>::deserialize(deserializer)?;
|
||||
if patterns.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut builder = globset::GlobSetBuilder::new();
|
||||
for pattern in patterns {
|
||||
let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
|
||||
builder.add(glob);
|
||||
}
|
||||
builder.build().map(Some).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageServerConfiguration {
|
||||
|
@ -378,6 +395,12 @@ pub struct LanguageServerConfiguration {
|
|||
pub config: Option<serde_json::Value>,
|
||||
#[serde(default = "default_timeout")]
|
||||
pub timeout: u64,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing,
|
||||
deserialize_with = "deserialize_required_root_patterns"
|
||||
)]
|
||||
pub required_root_patterns: Option<GlobSet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
@ -177,12 +177,11 @@ impl Client {
|
|||
args: &[String],
|
||||
config: Option<Value>,
|
||||
server_environment: HashMap<String, String>,
|
||||
root_markers: &[String],
|
||||
manual_roots: &[PathBuf],
|
||||
root_path: PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
id: usize,
|
||||
name: String,
|
||||
req_timeout: u64,
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = helix_stdx::env::which(cmd)?;
|
||||
|
@ -206,22 +205,6 @@ impl Client {
|
|||
|
||||
let (server_rx, server_tx, initialize_notify) =
|
||||
Transport::start(reader, writer, stderr, id, name.clone());
|
||||
let (workspace, workspace_is_cwd) = find_workspace();
|
||||
let workspace = path::normalize(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,
|
||||
);
|
||||
|
||||
// `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());
|
||||
|
||||
let workspace_folders = root_uri
|
||||
.clone()
|
||||
|
|
|
@ -680,7 +680,7 @@ impl Registry {
|
|||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<Arc<Client>> {
|
||||
) -> Result<Option<Arc<Client>>> {
|
||||
let config = self
|
||||
.syn_loader
|
||||
.language_server_configs()
|
||||
|
@ -688,7 +688,7 @@ impl Registry {
|
|||
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
|
||||
let id = self.counter;
|
||||
self.counter += 1;
|
||||
let NewClient(client, incoming) = start_client(
|
||||
if let Some(NewClient(client, incoming)) = start_client(
|
||||
id,
|
||||
name,
|
||||
ls_config,
|
||||
|
@ -696,9 +696,12 @@ impl Registry {
|
|||
doc_path,
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
)?;
|
||||
self.incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
Ok(client)
|
||||
)? {
|
||||
self.incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
Ok(Some(client))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
|
||||
|
@ -723,8 +726,8 @@ impl Registry {
|
|||
root_dirs,
|
||||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => client,
|
||||
error => return Some(error),
|
||||
Ok(client) => client?,
|
||||
Err(error) => return Some(Err(error)),
|
||||
};
|
||||
let old_clients = self
|
||||
.inner
|
||||
|
@ -764,13 +767,13 @@ impl Registry {
|
|||
root_dirs: &'a [PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
|
||||
language_config.language_servers.iter().map(
|
||||
language_config.language_servers.iter().filter_map(
|
||||
move |LanguageServerFeatures { name, .. }| {
|
||||
if let Some(clients) = self.inner.get(name) {
|
||||
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
|
||||
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
|
||||
}) {
|
||||
return (name.to_owned(), Ok(client.clone()));
|
||||
return Some((name.to_owned(), Ok(client.clone())));
|
||||
}
|
||||
}
|
||||
match self.start_client(
|
||||
|
@ -781,13 +784,14 @@ impl Registry {
|
|||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => {
|
||||
let client = client?;
|
||||
self.inner
|
||||
.entry(name.to_owned())
|
||||
.or_default()
|
||||
.push(client.clone());
|
||||
(name.clone(), Ok(client))
|
||||
Some((name.clone(), Ok(client)))
|
||||
}
|
||||
Err(err) => (name.to_owned(), Err(err)),
|
||||
Err(err) => Some((name.to_owned(), Err(err))),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -888,18 +892,45 @@ fn start_client(
|
|||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<NewClient> {
|
||||
) -> Result<Option<NewClient>> {
|
||||
let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
|
||||
let workspace = path::normalize(workspace);
|
||||
let root = find_lsp_workspace(
|
||||
doc_path
|
||||
.and_then(|x| x.parent().and_then(|x| x.to_str()))
|
||||
.unwrap_or("."),
|
||||
&config.roots,
|
||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
||||
&workspace,
|
||||
workspace_is_cwd,
|
||||
);
|
||||
|
||||
// `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());
|
||||
|
||||
if let Some(globset) = &ls_config.required_root_patterns {
|
||||
if !root_path
|
||||
.read_dir()?
|
||||
.flatten()
|
||||
.map(|entry| entry.file_name())
|
||||
.any(|entry| globset.is_match(entry))
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
let (client, incoming, initialize_notify) = Client::start(
|
||||
&ls_config.command,
|
||||
&ls_config.args,
|
||||
ls_config.config.clone(),
|
||||
ls_config.environment.clone(),
|
||||
&config.roots,
|
||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
||||
root_path,
|
||||
root_uri,
|
||||
id,
|
||||
name,
|
||||
ls_config.timeout,
|
||||
doc_path,
|
||||
)?;
|
||||
|
||||
let client = Arc::new(client);
|
||||
|
@ -938,7 +969,7 @@ fn start_client(
|
|||
initialize_notify.notify_one();
|
||||
});
|
||||
|
||||
Ok(NewClient(client, incoming))
|
||||
Ok(Some(NewClient(client, incoming)))
|
||||
}
|
||||
|
||||
/// Find an LSP workspace of a file using the following mechanism:
|
||||
|
|
Loading…
Add table
Reference in a new issue