Files
oak-gpui/crates/languages/src/elixir.rs
T
Kirill Bulatov d1ad96782c Rework task modal (#10341)
New list (used tasks are above the separator line, sorted by the usage
recency), then all language tasks, then project-local and global tasks
are listed.
Note that there are two test tasks (for `test_name_1` and `test_name_2`
functions) that are created from the same task template:
<img width="563" alt="Screenshot 2024-04-10 at 01 00 46"
src="https://github.com/zed-industries/zed/assets/2690773/7455a82f-2af2-47bf-99bd-d9c5a36e64ab">

Tasks are deduplicated by labels, with the used tasks left in case of
the conflict with the new tasks from the template:
<img width="555" alt="Screenshot 2024-04-10 at 01 01 06"
src="https://github.com/zed-industries/zed/assets/2690773/8f5a249e-abec-46ef-a991-08c6d0348648">

Regular recent tasks can be now removed too:
<img width="565" alt="Screenshot 2024-04-10 at 01 00 55"
src="https://github.com/zed-industries/zed/assets/2690773/0976b8fe-b5d7-4d2a-953d-1d8b1f216192">

When the caret is in the place where no function symbol could be
retrieved, no cargo tests for function are listed in tasks:
<img width="556" alt="image"
src="https://github.com/zed-industries/zed/assets/2690773/df30feba-fe27-4645-8be9-02afc70f02da">


Part of https://github.com/zed-industries/zed/issues/10132
Reworks the task code to simplify it and enable proper task labels.

* removes `trait Task`, renames `Definition` into `TaskTemplate` and use
that instead of `Arc<dyn Task>` everywhere
* implement more generic `TaskId` generation that depends on the
`TaskContext` and `TaskTemplate`
* remove `TaskId` out of the template and only create it after
"resolving" the template into the `ResolvedTask`: this way, task
templates, task state (`TaskContext`) and task "result" (resolved state)
are clearly separated and are not mixed
* implement the logic for filtering out non-related language tasks and
tasks that have non-resolved Zed task variables
* rework Zed template-vs-resolved-task display in modal: now all reruns
and recently used tasks are resolved tasks with "fixed" context (unless
configured otherwise in the task json) that are always shown, and Zed
can add on top tasks with different context that are derived from the
same template as the used, resolved tasks
* sort the tasks list better, showing more specific and least recently
used tasks higher
* shows a separator between used and unused tasks, allow removing the
used tasks same as the oneshot ones
* remote the Oneshot task source as redundant: all oneshot tasks are now
stored in the inventory's history
* when reusing the tasks as query in the modal, paste the expanded task
label now, show trimmed resolved label in the modal
* adjusts Rust and Elixir task labels to be more descriptive and closer
to bash scripts

Release Notes:

- Improved task modal ordering, run and deletion capabilities
2024-04-11 02:02:04 +03:00

608 lines
19 KiB
Rust

use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::{AppContext, AsyncAppContext, Task};
pub use language::*;
use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
use project::project_settings::ProjectSettings;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use settings::{Settings, SettingsSources};
use smol::fs::{self, File};
use std::{
any::Any,
env::consts,
ops::Deref,
path::PathBuf,
sync::{
atomic::{AtomicBool, Ordering::SeqCst},
Arc,
},
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{
fs::remove_matching,
github::{latest_github_release, GitHubLspBinaryVersion},
maybe, ResultExt,
};
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
pub struct ElixirSettings {
pub lsp: ElixirLspSetting,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ElixirLspSetting {
ElixirLs,
NextLs,
Local {
path: String,
arguments: Vec<String>,
},
}
#[derive(Clone, Serialize, Default, Deserialize, JsonSchema)]
pub struct ElixirSettingsContent {
lsp: Option<ElixirLspSetting>,
}
impl Settings for ElixirSettings {
const KEY: Option<&'static str> = Some("elixir");
type FileContent = ElixirSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
pub struct ElixirLspAdapter;
#[async_trait(?Send)]
impl LspAdapter for ElixirLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("elixir-ls".into())
}
fn will_start_server(
&self,
delegate: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Option<Task<Result<()>>> {
static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false);
const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found.";
let delegate = delegate.clone();
Some(cx.spawn(|cx| async move {
let elixir_output = smol::process::Command::new("elixir")
.args(["--version"])
.output()
.await;
if elixir_output.is_err() {
if DID_SHOW_NOTIFICATION
.compare_exchange(false, true, SeqCst, SeqCst)
.is_ok()
{
cx.update(|cx| {
delegate.show_notification(NOTIFICATION_MESSAGE, cx);
})?
}
return Err(anyhow!("cannot run elixir-ls"));
}
Ok(())
}))
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let http = delegate.http_client();
let release = latest_github_release("elixir-lsp/elixir-ls", true, false, http).await?;
let asset_name = format!("elixir-ls-{}.zip", &release.tag_name);
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.ok_or_else(|| anyhow!("no asset found matching {asset_name:?}"))?;
let version = GitHubLspBinaryVersion {
name: release.tag_name.clone(),
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name));
let folder_path = container_dir.join("elixir-ls");
let binary_path = folder_path.join("language_server.sh");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.context("error downloading release")?;
let mut file = File::create(&zip_path)
.await
.with_context(|| format!("failed to create file {}", zip_path.display()))?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
fs::create_dir_all(&folder_path)
.await
.with_context(|| format!("failed to create directory {}", folder_path.display()))?;
let unzip_status = smol::process::Command::new("unzip")
.arg(&zip_path)
.arg("-d")
.arg(&folder_path)
.output()
.await?
.status;
if !unzip_status.success() {
Err(anyhow!("failed to unzip elixir-ls archive"))?;
}
remove_matching(&container_dir, |entry| entry != folder_path).await;
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec![],
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary_elixir_ls(container_dir).await
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary_elixir_ls(container_dir).await
}
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Arc<Language>,
) -> Option<CodeLabel> {
match completion.kind.zip(completion.detail.as_ref()) {
Some((_, detail)) if detail.starts_with("(function)") => {
let text = detail.strip_prefix("(function) ")?;
let filter_range = 0..text.find('(').unwrap_or(text.len());
let source = Rope::from(format!("def {text}").as_str());
let runs = language.highlight_text(&source, 4..4 + text.len());
return Some(CodeLabel {
text: text.to_string(),
runs,
filter_range,
});
}
Some((_, detail)) if detail.starts_with("(macro)") => {
let text = detail.strip_prefix("(macro) ")?;
let filter_range = 0..text.find('(').unwrap_or(text.len());
let source = Rope::from(format!("defmacro {text}").as_str());
let runs = language.highlight_text(&source, 9..9 + text.len());
return Some(CodeLabel {
text: text.to_string(),
runs,
filter_range,
});
}
Some((
CompletionItemKind::CLASS
| CompletionItemKind::MODULE
| CompletionItemKind::INTERFACE
| CompletionItemKind::STRUCT,
_,
)) => {
let filter_range = 0..completion
.label
.find(" (")
.unwrap_or(completion.label.len());
let text = &completion.label[filter_range.clone()];
let source = Rope::from(format!("defmodule {text}").as_str());
let runs = language.highlight_text(&source, 10..10 + text.len());
return Some(CodeLabel {
text: completion.label.clone(),
runs,
filter_range,
});
}
_ => {}
}
None
}
async fn label_for_symbol(
&self,
name: &str,
kind: SymbolKind,
language: &Arc<Language>,
) -> Option<CodeLabel> {
let (text, filter_range, display_range) = match kind {
SymbolKind::METHOD | SymbolKind::FUNCTION => {
let text = format!("def {}", name);
let filter_range = 4..4 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
SymbolKind::CLASS | SymbolKind::MODULE | SymbolKind::INTERFACE | SymbolKind::STRUCT => {
let text = format!("defmodule {}", name);
let filter_range = 10..10 + name.len();
let display_range = 0..filter_range.end;
(text, filter_range, display_range)
}
_ => return None,
};
Some(CodeLabel {
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
text: text[display_range].to_string(),
filter_range,
})
}
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let settings = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get("elixir-ls")
.and_then(|s| s.settings.clone())
.unwrap_or_default()
})?;
Ok(serde_json::json!({
"elixirLS": settings
}))
}
}
async fn get_cached_server_binary_elixir_ls(
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
let server_path = container_dir.join("elixir-ls/language_server.sh");
if server_path.exists() {
Some(LanguageServerBinary {
path: server_path,
env: None,
arguments: vec![],
})
} else {
log::error!("missing executable in directory {:?}", server_path);
None
}
}
pub struct NextLspAdapter;
#[async_trait(?Send)]
impl LspAdapter for NextLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("next-ls".into())
}
async fn fetch_latest_server_version(
&self,
delegate: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
let platform = match consts::ARCH {
"x86_64" => "darwin_amd64",
"aarch64" => "darwin_arm64",
other => bail!("Running on unsupported platform: {other}"),
};
let release =
latest_github_release("elixir-tools/next-ls", true, false, delegate.http_client())
.await?;
let version = release.tag_name;
let asset_name = format!("next_ls_{platform}");
let asset = release
.assets
.iter()
.find(|asset| asset.name == asset_name)
.with_context(|| format!("no asset found matching {asset_name:?}"))?;
let version = GitHubLspBinaryVersion {
name: version,
url: asset.browser_download_url.clone(),
};
Ok(Box::new(version) as Box<_>)
}
async fn fetch_server_binary(
&self,
version: Box<dyn 'static + Send + Any>,
container_dir: PathBuf,
delegate: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
let binary_path = container_dir.join("next-ls");
if fs::metadata(&binary_path).await.is_err() {
let mut response = delegate
.http_client()
.get(&version.url, Default::default(), true)
.await
.map_err(|err| anyhow!("error downloading release: {}", err))?;
let mut file = smol::fs::File::create(&binary_path).await?;
if !response.status().is_success() {
Err(anyhow!(
"download failed with status {}",
response.status().to_string()
))?;
}
futures::io::copy(response.body_mut(), &mut file).await?;
// todo("windows")
#[cfg(not(windows))]
{
fs::set_permissions(
&binary_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
}
}
Ok(LanguageServerBinary {
path: binary_path,
env: None,
arguments: vec!["--stdio".into()],
})
}
async fn cached_server_binary(
&self,
container_dir: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
get_cached_server_binary_next(container_dir)
.await
.map(|mut binary| {
binary.arguments = vec!["--stdio".into()];
binary
})
}
async fn installation_test_binary(
&self,
container_dir: PathBuf,
) -> Option<LanguageServerBinary> {
get_cached_server_binary_next(container_dir)
.await
.map(|mut binary| {
binary.arguments = vec!["--help".into()];
binary
})
}
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Arc<Language>,
) -> Option<CodeLabel> {
label_for_completion_elixir(completion, language)
}
async fn label_for_symbol(
&self,
name: &str,
symbol_kind: SymbolKind,
language: &Arc<Language>,
) -> Option<CodeLabel> {
label_for_symbol_elixir(name, symbol_kind, language)
}
}
async fn get_cached_server_binary_next(container_dir: PathBuf) -> Option<LanguageServerBinary> {
maybe!(async {
let mut last_binary_path = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_file()
&& entry
.file_name()
.to_str()
.map_or(false, |name| name == "next-ls")
{
last_binary_path = Some(entry.path());
}
}
if let Some(path) = last_binary_path {
Ok(LanguageServerBinary {
path,
env: None,
arguments: Vec::new(),
})
} else {
Err(anyhow!("no cached binary"))
}
})
.await
.log_err()
}
pub struct LocalLspAdapter {
pub path: String,
pub arguments: Vec<String>,
}
#[async_trait(?Send)]
impl LspAdapter for LocalLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("local-ls".into())
}
async fn fetch_latest_server_version(
&self,
_: &dyn LspAdapterDelegate,
) -> Result<Box<dyn 'static + Send + Any>> {
Ok(Box::new(()) as Box<_>)
}
async fn fetch_server_binary(
&self,
_: Box<dyn 'static + Send + Any>,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Result<LanguageServerBinary> {
let path = shellexpand::full(&self.path)?;
Ok(LanguageServerBinary {
path: PathBuf::from(path.deref()),
env: None,
arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
})
}
async fn cached_server_binary(
&self,
_: PathBuf,
_: &dyn LspAdapterDelegate,
) -> Option<LanguageServerBinary> {
let path = shellexpand::full(&self.path).ok()?;
Some(LanguageServerBinary {
path: PathBuf::from(path.deref()),
env: None,
arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
})
}
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
let path = shellexpand::full(&self.path).ok()?;
Some(LanguageServerBinary {
path: PathBuf::from(path.deref()),
env: None,
arguments: self.arguments.iter().map(|arg| arg.into()).collect(),
})
}
async fn label_for_completion(
&self,
completion: &lsp::CompletionItem,
language: &Arc<Language>,
) -> Option<CodeLabel> {
label_for_completion_elixir(completion, language)
}
async fn label_for_symbol(
&self,
name: &str,
symbol: SymbolKind,
language: &Arc<Language>,
) -> Option<CodeLabel> {
label_for_symbol_elixir(name, symbol, language)
}
}
fn label_for_completion_elixir(
completion: &lsp::CompletionItem,
language: &Arc<Language>,
) -> Option<CodeLabel> {
return Some(CodeLabel {
runs: language.highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
text: completion.label.clone(),
filter_range: 0..completion.label.len(),
});
}
fn label_for_symbol_elixir(
name: &str,
_: SymbolKind,
language: &Arc<Language>,
) -> Option<CodeLabel> {
Some(CodeLabel {
runs: language.highlight_text(&name.into(), 0..name.len()),
text: name.to_string(),
filter_range: 0..name.len(),
})
}
pub(super) fn elixir_task_context() -> ContextProviderWithTasks {
// Taken from https://gist.github.com/josevalim/2e4f60a14ccd52728e3256571259d493#gistcomment-4995881
ContextProviderWithTasks::new(TaskTemplates(vec![
TaskTemplate {
label: "mix test".to_owned(),
command: "mix".to_owned(),
args: vec!["test".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: "mix test --failed".to_owned(),
command: "mix".to_owned(),
args: vec!["test".to_owned(), "--failed".to_owned()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!("mix test {}", VariableName::Symbol.template_value()),
command: "mix".to_owned(),
args: vec!["test".to_owned(), VariableName::Symbol.template_value()],
..TaskTemplate::default()
},
TaskTemplate {
label: format!(
"mix test {}:{}",
VariableName::File.template_value(),
VariableName::Row.template_value()
),
command: "mix".to_owned(),
args: vec![
"test".to_owned(),
format!(
"{}:{}",
VariableName::File.template_value(),
VariableName::Row.template_value()
),
],
..TaskTemplate::default()
},
TaskTemplate {
label: "Elixir: break line".to_owned(),
command: "iex".to_owned(),
args: vec![
"-S".to_owned(),
"mix".to_owned(),
"test".to_owned(),
"-b".to_owned(),
format!(
"{}:{}",
VariableName::File.template_value(),
VariableName::Row.template_value()
),
],
..TaskTemplate::default()
},
]))
}