mirror of https://github.com/helix-editor/helix
Added required-root-patterns for situational lsp activation using globbing
parent
a069b92897
commit
dfcd842726
|
@ -371,6 +371,9 @@ pub struct LanguageServerConfiguration {
|
||||||
pub config: Option<serde_json::Value>,
|
pub config: Option<serde_json::Value>,
|
||||||
#[serde(default = "default_timeout")]
|
#[serde(default = "default_timeout")]
|
||||||
pub timeout: u64,
|
pub timeout: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub required_root_patterns: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|
|
@ -174,12 +174,11 @@ impl Client {
|
||||||
args: &[String],
|
args: &[String],
|
||||||
config: Option<Value>,
|
config: Option<Value>,
|
||||||
server_environment: HashMap<String, String>,
|
server_environment: HashMap<String, String>,
|
||||||
root_markers: &[String],
|
root_path: PathBuf,
|
||||||
manual_roots: &[PathBuf],
|
root_uri: Option<lsp::Url>,
|
||||||
id: usize,
|
id: usize,
|
||||||
name: String,
|
name: String,
|
||||||
req_timeout: u64,
|
req_timeout: u64,
|
||||||
doc_path: Option<&std::path::PathBuf>,
|
|
||||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||||
// Resolve path to the binary
|
// Resolve path to the binary
|
||||||
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
|
||||||
|
@ -203,22 +202,6 @@ impl Client {
|
||||||
|
|
||||||
let (server_rx, server_tx, initialize_notify) =
|
let (server_rx, server_tx, initialize_notify) =
|
||||||
Transport::start(reader, writer, stderr, id, name.clone());
|
Transport::start(reader, writer, stderr, id, name.clone());
|
||||||
let (workspace, workspace_is_cwd) = find_workspace();
|
|
||||||
let workspace = path::get_normalized_path(&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
|
let workspace_folders = root_uri
|
||||||
.clone()
|
.clone()
|
||||||
|
|
|
@ -672,7 +672,7 @@ impl Registry {
|
||||||
doc_path: Option<&std::path::PathBuf>,
|
doc_path: Option<&std::path::PathBuf>,
|
||||||
root_dirs: &[PathBuf],
|
root_dirs: &[PathBuf],
|
||||||
enable_snippets: bool,
|
enable_snippets: bool,
|
||||||
) -> Result<Arc<Client>> {
|
) -> Result<Option<Arc<Client>>> {
|
||||||
let config = self
|
let config = self
|
||||||
.syn_loader
|
.syn_loader
|
||||||
.language_server_configs()
|
.language_server_configs()
|
||||||
|
@ -680,7 +680,7 @@ impl Registry {
|
||||||
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
|
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
|
||||||
let id = self.counter;
|
let id = self.counter;
|
||||||
self.counter += 1;
|
self.counter += 1;
|
||||||
let NewClient(client, incoming) = start_client(
|
if let Some(NewClient(client, incoming)) = start_client(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
ls_config,
|
ls_config,
|
||||||
|
@ -688,9 +688,12 @@ impl Registry {
|
||||||
doc_path,
|
doc_path,
|
||||||
root_dirs,
|
root_dirs,
|
||||||
enable_snippets,
|
enable_snippets,
|
||||||
)?;
|
)? {
|
||||||
self.incoming.push(UnboundedReceiverStream::new(incoming));
|
self.incoming.push(UnboundedReceiverStream::new(incoming));
|
||||||
Ok(client)
|
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,
|
/// 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,
|
||||||
|
@ -715,8 +718,8 @@ impl Registry {
|
||||||
root_dirs,
|
root_dirs,
|
||||||
enable_snippets,
|
enable_snippets,
|
||||||
) {
|
) {
|
||||||
Ok(client) => client,
|
Ok(client) => client?,
|
||||||
error => return Some(error),
|
Err(error) => return Some(Err(error)),
|
||||||
};
|
};
|
||||||
let old_clients = self
|
let old_clients = self
|
||||||
.inner
|
.inner
|
||||||
|
@ -756,13 +759,13 @@ impl Registry {
|
||||||
root_dirs: &'a [PathBuf],
|
root_dirs: &'a [PathBuf],
|
||||||
enable_snippets: bool,
|
enable_snippets: bool,
|
||||||
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
|
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
|
||||||
language_config.language_servers.iter().map(
|
language_config.language_servers.iter().filter_map(
|
||||||
move |LanguageServerFeatures { name, .. }| {
|
move |LanguageServerFeatures { name, .. }| {
|
||||||
if let Some(clients) = self.inner.get(name) {
|
if let Some(clients) = self.inner.get(name) {
|
||||||
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
|
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
|
||||||
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
|
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(
|
match self.start_client(
|
||||||
|
@ -773,13 +776,14 @@ impl Registry {
|
||||||
enable_snippets,
|
enable_snippets,
|
||||||
) {
|
) {
|
||||||
Ok(client) => {
|
Ok(client) => {
|
||||||
|
let client = client?;
|
||||||
self.inner
|
self.inner
|
||||||
.entry(name.to_owned())
|
.entry(name.to_owned())
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(client.clone());
|
.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))),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -880,18 +884,70 @@ fn start_client(
|
||||||
doc_path: Option<&std::path::PathBuf>,
|
doc_path: Option<&std::path::PathBuf>,
|
||||||
root_dirs: &[PathBuf],
|
root_dirs: &[PathBuf],
|
||||||
enable_snippets: bool,
|
enable_snippets: bool,
|
||||||
) -> Result<NewClient> {
|
) -> Result<Option<NewClient>> {
|
||||||
|
let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
|
||||||
|
let workspace = path::get_normalized_path(&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());
|
||||||
|
|
||||||
|
let mut globset = globset::GlobSetBuilder::new();
|
||||||
|
let required_root_patterns = &ls_config.required_root_patterns;
|
||||||
|
if !required_root_patterns.is_empty() {
|
||||||
|
for required_root_pattern in required_root_patterns {
|
||||||
|
match globset::Glob::new(required_root_pattern) {
|
||||||
|
Ok(glob) => {
|
||||||
|
globset.add(glob);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to build glob '{}' for language server '{}'",
|
||||||
|
required_root_pattern,
|
||||||
|
name
|
||||||
|
);
|
||||||
|
log::warn!("{}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match globset.build() {
|
||||||
|
Ok(glob) => {
|
||||||
|
if !root_path
|
||||||
|
.read_dir()?
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.file_name())
|
||||||
|
.any(|filename| glob.is_match(filename))
|
||||||
|
{
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("Failed to build globset for language server {name}");
|
||||||
|
log::warn!("{}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let (client, incoming, initialize_notify) = Client::start(
|
let (client, incoming, initialize_notify) = Client::start(
|
||||||
&ls_config.command,
|
&ls_config.command,
|
||||||
&ls_config.args,
|
&ls_config.args,
|
||||||
ls_config.config.clone(),
|
ls_config.config.clone(),
|
||||||
ls_config.environment.clone(),
|
ls_config.environment.clone(),
|
||||||
&config.roots,
|
root_path,
|
||||||
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
|
root_uri,
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
ls_config.timeout,
|
ls_config.timeout,
|
||||||
doc_path,
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let client = Arc::new(client);
|
let client = Arc::new(client);
|
||||||
|
@ -923,7 +979,7 @@ fn start_client(
|
||||||
initialize_notify.notify_one();
|
initialize_notify.notify_one();
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(NewClient(client, incoming))
|
Ok(Some(NewClient(client, incoming)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find an LSP workspace of a file using the following mechanism:
|
/// Find an LSP workspace of a file using the following mechanism:
|
||||||
|
|
Loading…
Reference in New Issue