Support tasks from rust-analyzer (#28359)

(and any other LSP server in theory, if it exposes any LSP-ext endpoint
for the same)

Closes https://github.com/zed-industries/zed/issues/16160

* adds a way to disable tree-sitter tasks (the ones from the plugins,
enabled by default) with
```json5
"languages": {
  "Rust": "tasks": {
      "enabled": false
    }
  }
}
```
language settings

* adds a way to disable LSP tasks (the ones from the rust-analyzer
language server, enabled by default) with
```json5
"lsp": {
  "rust-analyzer": {
    "enable_lsp_tasks": false,
  }
}
```

* adds rust-analyzer tasks into tasks modal and gutter:

<img width="1728" alt="modal"
src="https://github.com/user-attachments/assets/22b9cee1-4ffb-4c9e-b1f1-d01e80e72508"
/>

<img width="396" alt="gutter"
src="https://github.com/user-attachments/assets/bd818079-e247-4332-bdb5-1b7cb1cce768"
/>


Release Notes:

- Added tasks from rust-analyzer
This commit is contained in:
Kirill Bulatov 2025-04-08 15:07:56 -06:00 committed by GitHub
parent 763cc6dba3
commit 39c98ce882
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 882 additions and 201 deletions

View file

@ -1,12 +1,27 @@
use crate::{lsp_command::LspCommand, lsp_store::LspStore, make_text_document_identifier};
use crate::{
LocationLink,
lsp_command::{
LspCommand, location_link_from_lsp, location_link_from_proto, location_link_to_proto,
},
lsp_store::LspStore,
make_text_document_identifier,
};
use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity};
use language::{Buffer, point_to_lsp, proto::deserialize_anchor};
use language::{
Buffer, point_to_lsp,
proto::{deserialize_anchor, serialize_anchor},
};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use std::{path::Path, sync::Arc};
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use task::TaskTemplate;
use text::{BufferId, PointUtf16, ToPointUtf16};
pub enum LspExpandMacro {}
@ -363,3 +378,245 @@ impl LspCommand for SwitchSourceHeader {
BufferId::new(message.buffer_id)
}
}
// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables
// Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489
pub enum Runnables {}
impl lsp::request::Request for Runnables {
type Params = RunnablesParams;
type Result = Vec<Runnable>;
const METHOD: &'static str = "experimental/runnables";
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct RunnablesParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: Option<lsp::Position>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Runnable {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<lsp::LocationLink>,
pub kind: RunnableKind,
pub args: RunnableArgs,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[serde(untagged)]
pub enum RunnableArgs {
Cargo(CargoRunnableArgs),
Shell(ShellRunnableArgs),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RunnableKind {
Cargo,
Shell,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CargoRunnableArgs {
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
pub cwd: PathBuf,
/// Command to be executed instead of cargo
pub override_cargo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace_root: Option<PathBuf>,
// command, --package and --lib stuff
pub cargo_args: Vec<String>,
// stuff after --
pub executable_args: Vec<String>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ShellRunnableArgs {
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
pub cwd: PathBuf,
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug)]
pub struct GetLspRunnables {
pub buffer_id: BufferId,
pub position: Option<text::Anchor>,
}
#[derive(Debug, Default)]
pub struct LspRunnables {
pub runnables: Vec<(Option<LocationLink>, TaskTemplate)>,
}
#[async_trait(?Send)]
impl LspCommand for GetLspRunnables {
type Response = LspRunnables;
type LspRequest = Runnables;
type ProtoRequest = proto::LspExtRunnables;
fn display_name(&self) -> &str {
"LSP Runnables"
}
fn to_lsp(
&self,
path: &Path,
buffer: &Buffer,
_: &Arc<LanguageServer>,
_: &App,
) -> Result<RunnablesParams> {
let url = match lsp::Url::from_file_path(path) {
Ok(url) => url,
Err(()) => anyhow::bail!("Failed to parse path {path:?} as lsp::Url"),
};
Ok(RunnablesParams {
text_document: lsp::TextDocumentIdentifier::new(url),
position: self
.position
.map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))),
})
}
async fn response_from_lsp(
self,
lsp_runnables: Vec<Runnable>,
lsp_store: Entity<LspStore>,
buffer: Entity<Buffer>,
server_id: LanguageServerId,
mut cx: AsyncApp,
) -> Result<LspRunnables> {
let mut runnables = Vec::with_capacity(lsp_runnables.len());
for runnable in lsp_runnables {
let location = match runnable.location {
Some(location) => Some(
location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx)
.await?,
),
None => None,
};
let mut task_template = TaskTemplate::default();
task_template.label = runnable.label;
match runnable.args {
RunnableArgs::Cargo(cargo) => {
match cargo.override_cargo {
Some(override_cargo) => {
let mut override_parts =
override_cargo.split(" ").map(|s| s.to_string());
task_template.command = override_parts
.next()
.unwrap_or_else(|| override_cargo.clone());
task_template.args.extend(override_parts);
}
None => task_template.command = "cargo".to_string(),
};
task_template.env = cargo.environment;
task_template.cwd = Some(
cargo
.workspace_root
.unwrap_or(cargo.cwd)
.to_string_lossy()
.to_string(),
);
task_template.args.extend(cargo.cargo_args);
if !cargo.executable_args.is_empty() {
task_template.args.push("--".to_string());
task_template.args.extend(cargo.executable_args);
}
}
RunnableArgs::Shell(shell) => {
task_template.command = shell.program;
task_template.args = shell.args;
task_template.env = shell.environment;
task_template.cwd = Some(shell.cwd.to_string_lossy().to_string());
}
}
runnables.push((location, task_template));
}
Ok(LspRunnables { runnables })
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables {
proto::LspExtRunnables {
project_id,
buffer_id: buffer.remote_id().to_proto(),
position: self.position.as_ref().map(serialize_anchor),
}
}
async fn from_proto(
message: proto::LspExtRunnables,
_: Entity<LspStore>,
_: Entity<Buffer>,
_: AsyncApp,
) -> Result<Self> {
let buffer_id = Self::buffer_id_from_proto(&message)?;
let position = message.position.and_then(deserialize_anchor);
Ok(Self {
buffer_id,
position,
})
}
fn response_to_proto(
response: LspRunnables,
lsp_store: &mut LspStore,
peer_id: PeerId,
_: &clock::Global,
cx: &mut App,
) -> proto::LspExtRunnablesResponse {
proto::LspExtRunnablesResponse {
runnables: response
.runnables
.into_iter()
.map(|(location, task_template)| proto::LspRunnable {
location: location
.map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)),
task_template: serde_json::to_vec(&task_template).unwrap(),
})
.collect(),
}
}
async fn response_from_proto(
self,
message: proto::LspExtRunnablesResponse,
lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
mut cx: AsyncApp,
) -> Result<LspRunnables> {
let mut runnables = LspRunnables {
runnables: Vec::new(),
};
for lsp_runnable in message.runnables {
let location = match lsp_runnable.location {
Some(location) => {
Some(location_link_from_proto(location, &lsp_store, &mut cx).await?)
}
None => None,
};
let task_template = serde_json::from_slice(&lsp_runnable.task_template)
.context("deserializing task template from proto")?;
runnables.runnables.push((location, task_template));
}
Ok(runnables)
}
fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result<BufferId> {
BufferId::new(message.buffer_id)
}
}

View file

@ -8,7 +8,7 @@ pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
/// Experimental: Informs the end user about the state of the server
///
/// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status)
/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status)
#[derive(Debug)]
enum ServerStatus {}
@ -38,13 +38,10 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
let name = language_server.name();
let server_id = language_server.server_id();
let this = lsp_store;
language_server
.on_notification::<ServerStatus, _>({
let name = name.to_string();
move |params, cx| {
let this = this.clone();
let name = name.to_string();
if let Some(ref message) = params.message {
let message = message.trim();
@ -53,10 +50,10 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
"Language server {name} (id {server_id}) status update: {message}"
);
match params.health {
ServerHealthStatus::Ok => log::info!("{}", formatted_message),
ServerHealthStatus::Warning => log::warn!("{}", formatted_message),
ServerHealthStatus::Ok => log::info!("{formatted_message}"),
ServerHealthStatus::Warning => log::warn!("{formatted_message}"),
ServerHealthStatus::Error => {
log::error!("{}", formatted_message);
log::error!("{formatted_message}");
let (tx, _rx) = smol::channel::bounded(1);
let request = LanguageServerPromptRequest {
level: PromptLevel::Critical,
@ -65,7 +62,7 @@ pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server:
response_channel: tx,
lsp_name: name.clone(),
};
let _ = this
lsp_store
.update(cx, |_, cx| {
cx.emit(LspStoreEvent::LanguageServerPrompt(request));
})