use crate::{ LocationLink, lsp_command::{ LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto, location_link_to_proto, location_links_from_lsp, location_links_from_proto, location_links_to_proto, }, lsp_store::LspStore, make_lsp_text_document_position, 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, serialize_anchor}, }; use lsp::{AdapterServerCapabilities, LanguageServer, LanguageServerId}; use rpc::proto::{self, PeerId}; use serde::{Deserialize, Serialize}; use std::{ path::{Path, PathBuf}, sync::Arc, }; use task::TaskTemplate; use text::{BufferId, PointUtf16, ToPointUtf16}; pub enum LspExtExpandMacro {} impl lsp::request::Request for LspExtExpandMacro { type Params = ExpandMacroParams; type Result = Option; const METHOD: &'static str = "rust-analyzer/expandMacro"; } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ExpandMacroParams { pub text_document: lsp::TextDocumentIdentifier, pub position: lsp::Position, } #[derive(Default, Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ExpandedMacro { pub name: String, pub expansion: String, } impl ExpandedMacro { pub fn is_empty(&self) -> bool { self.name.is_empty() && self.expansion.is_empty() } } #[derive(Debug)] pub struct ExpandMacro { pub position: PointUtf16, } #[async_trait(?Send)] impl LspCommand for ExpandMacro { type Response = ExpandedMacro; type LspRequest = LspExtExpandMacro; type ProtoRequest = proto::LspExtExpandMacro; fn display_name(&self) -> &str { "Expand macro" } fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { true } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { Ok(ExpandMacroParams { text_document: make_text_document_identifier(path)?, position: point_to_lsp(self.position), }) } async fn response_from_lsp( self, message: Option, _: Entity, _: Entity, _: LanguageServerId, _: AsyncApp, ) -> anyhow::Result { Ok(message .map(|message| ExpandedMacro { name: message.name, expansion: message.expansion, }) .unwrap_or_default()) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro { proto::LspExtExpandMacro { project_id, buffer_id: buffer.remote_id().into(), position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), } } async fn from_proto( message: Self::ProtoRequest, _: Entity, buffer: Entity, mut cx: AsyncApp, ) -> anyhow::Result { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: ExpandedMacro, _: &mut LspStore, _: PeerId, _: &clock::Global, _: &mut App, ) -> proto::LspExtExpandMacroResponse { proto::LspExtExpandMacroResponse { name: response.name, expansion: response.expansion, } } async fn response_from_proto( self, message: proto::LspExtExpandMacroResponse, _: Entity, _: Entity, _: AsyncApp, ) -> anyhow::Result { Ok(ExpandedMacro { name: message.name, expansion: message.expansion, }) } fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> Result { BufferId::new(message.buffer_id) } } pub enum LspOpenDocs {} impl lsp::request::Request for LspOpenDocs { type Params = OpenDocsParams; type Result = Option; const METHOD: &'static str = "experimental/externalDocs"; } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct OpenDocsParams { pub text_document: lsp::TextDocumentIdentifier, pub position: lsp::Position, } #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct DocsUrls { pub web: Option, pub local: Option, } impl DocsUrls { pub fn is_empty(&self) -> bool { self.web.is_none() && self.local.is_none() } } #[derive(Debug)] pub struct OpenDocs { pub position: PointUtf16, } #[async_trait(?Send)] impl LspCommand for OpenDocs { type Response = DocsUrls; type LspRequest = LspOpenDocs; type ProtoRequest = proto::LspExtOpenDocs; fn display_name(&self) -> &str { "Open docs" } fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { true } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { Ok(OpenDocsParams { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, position: point_to_lsp(self.position), }) } async fn response_from_lsp( self, message: Option, _: Entity, _: Entity, _: LanguageServerId, _: AsyncApp, ) -> anyhow::Result { Ok(message .map(|message| DocsUrls { web: message.web, local: message.local, }) .unwrap_or_default()) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs { proto::LspExtOpenDocs { project_id, buffer_id: buffer.remote_id().into(), position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), } } async fn from_proto( message: Self::ProtoRequest, _: Entity, buffer: Entity, mut cx: AsyncApp, ) -> anyhow::Result { let position = message .position .and_then(deserialize_anchor) .context("invalid position")?; Ok(Self { position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( response: DocsUrls, _: &mut LspStore, _: PeerId, _: &clock::Global, _: &mut App, ) -> proto::LspExtOpenDocsResponse { proto::LspExtOpenDocsResponse { web: response.web, local: response.local, } } async fn response_from_proto( self, message: proto::LspExtOpenDocsResponse, _: Entity, _: Entity, _: AsyncApp, ) -> anyhow::Result { Ok(DocsUrls { web: message.web, local: message.local, }) } fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result { BufferId::new(message.buffer_id) } } pub enum LspSwitchSourceHeader {} impl lsp::request::Request for LspSwitchSourceHeader { type Params = SwitchSourceHeaderParams; type Result = Option; const METHOD: &'static str = "textDocument/switchSourceHeader"; } #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier); #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "camelCase")] pub struct SwitchSourceHeaderResult(pub String); #[derive(Default, Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct SwitchSourceHeader; #[derive(Debug)] pub struct GoToParentModule { pub position: PointUtf16, } pub struct LspGoToParentModule {} impl lsp::request::Request for LspGoToParentModule { type Params = lsp::TextDocumentPositionParams; type Result = Option>; const METHOD: &'static str = "experimental/parentModule"; } #[async_trait(?Send)] impl LspCommand for SwitchSourceHeader { type Response = SwitchSourceHeaderResult; type LspRequest = LspSwitchSourceHeader; type ProtoRequest = proto::LspExtSwitchSourceHeader; fn display_name(&self) -> &str { "Switch source header" } fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { true } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { Ok(SwitchSourceHeaderParams(make_text_document_identifier( path, )?)) } async fn response_from_lsp( self, message: Option, _: Entity, _: Entity, _: LanguageServerId, _: AsyncApp, ) -> anyhow::Result { Ok(message .map(|message| SwitchSourceHeaderResult(message.0)) .unwrap_or_default()) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader { proto::LspExtSwitchSourceHeader { project_id, buffer_id: buffer.remote_id().into(), } } async fn from_proto( _: Self::ProtoRequest, _: Entity, _: Entity, _: AsyncApp, ) -> anyhow::Result { Ok(Self {}) } fn response_to_proto( response: SwitchSourceHeaderResult, _: &mut LspStore, _: PeerId, _: &clock::Global, _: &mut App, ) -> proto::LspExtSwitchSourceHeaderResponse { proto::LspExtSwitchSourceHeaderResponse { target_file: response.0, } } async fn response_from_proto( self, message: proto::LspExtSwitchSourceHeaderResponse, _: Entity, _: Entity, _: AsyncApp, ) -> anyhow::Result { Ok(SwitchSourceHeaderResult(message.target_file)) } fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result { BufferId::new(message.buffer_id) } } #[async_trait(?Send)] impl LspCommand for GoToParentModule { type Response = Vec; type LspRequest = LspGoToParentModule; type ProtoRequest = proto::LspExtGoToParentModule; fn display_name(&self) -> &str { "Go to parent module" } fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { true } fn to_lsp( &self, path: &Path, _: &Buffer, _: &Arc, _: &App, ) -> Result { make_lsp_text_document_position(path, self.position) } async fn response_from_lsp( self, links: Option>, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, cx: AsyncApp, ) -> anyhow::Result> { location_links_from_lsp( links.map(lsp::GotoDefinitionResponse::Link), lsp_store, buffer, server_id, cx, ) .await } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtGoToParentModule { proto::LspExtGoToParentModule { project_id, buffer_id: buffer.remote_id().to_proto(), position: Some(language::proto::serialize_anchor( &buffer.anchor_before(self.position), )), } } async fn from_proto( request: Self::ProtoRequest, _: Entity, buffer: Entity, mut cx: AsyncApp, ) -> anyhow::Result { let position = request .position .and_then(deserialize_anchor) .context("bad request with bad position")?; Ok(Self { position: buffer.read_with(&mut cx, |buffer, _| position.to_point_utf16(buffer))?, }) } fn response_to_proto( links: Vec, lsp_store: &mut LspStore, peer_id: PeerId, _: &clock::Global, cx: &mut App, ) -> proto::LspExtGoToParentModuleResponse { proto::LspExtGoToParentModuleResponse { links: location_links_to_proto(links, lsp_store, peer_id, cx), } } async fn response_from_proto( self, message: proto::LspExtGoToParentModuleResponse, lsp_store: Entity, _: Entity, cx: AsyncApp, ) -> anyhow::Result> { location_links_from_proto(message.links, lsp_store, cx).await } fn buffer_id_from_proto(message: &proto::LspExtGoToParentModule) -> Result { 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; const METHOD: &'static str = "experimental/runnables"; } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct RunnablesParams { pub text_document: lsp::TextDocumentIdentifier, #[serde(default)] pub position: Option, } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct Runnable { pub label: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub location: Option, 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(default, skip_serializing_if = "HashMap::is_empty")] pub environment: HashMap, pub cwd: PathBuf, /// Command to be executed instead of cargo #[serde(default)] pub override_cargo: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub workspace_root: Option, // command, --package and --lib stuff #[serde(default)] pub cargo_args: Vec, // stuff after -- #[serde(default)] pub executable_args: Vec, } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct ShellRunnableArgs { #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub environment: HashMap, pub cwd: PathBuf, pub program: String, #[serde(default)] pub args: Vec, } #[derive(Debug)] pub struct GetLspRunnables { pub buffer_id: BufferId, pub position: Option, } #[derive(Debug, Default)] pub struct LspRunnables { pub runnables: Vec<(Option, 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 check_capabilities(&self, _: AdapterServerCapabilities) -> bool { true } fn to_lsp( &self, path: &Path, buffer: &Buffer, _: &Arc, _: &App, ) -> Result { let url = file_path_to_lsp_url(path)?; 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, lsp_store: Entity, buffer: Entity, server_id: LanguageServerId, mut cx: AsyncApp, ) -> Result { 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 .into_iter() // rust-analyzer's doctest data may be smth. like // ``` // command: "cargo", // args: [ // "test", // "--doc", // "--package", // "cargo-output-parser", // "--", // "X::new", // "--show-output", // ], // ``` // and `X::new` will cause troubles if not escaped properly, as later // the task runs as `$SHELL -i -c "cargo test ..."`. // // We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`. // That bit is not auto-expanded when using single quotes. // Escape extra cargo args unconditionally as those are unlikely to contain `~`. .flat_map(|extra_arg| { shlex::try_quote(&extra_arg).ok().map(|s| s.to_string()) }), ); } } 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, _: Entity, _: AsyncApp, ) -> Result { 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, _: Entity, mut cx: AsyncApp, ) -> Result { 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.clone(), &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::new(message.buffer_id) } } #[derive(Debug)] pub struct LspExtCancelFlycheck {} #[derive(Debug)] pub struct LspExtRunFlycheck {} #[derive(Debug)] pub struct LspExtClearFlycheck {} impl lsp::notification::Notification for LspExtCancelFlycheck { type Params = (); const METHOD: &'static str = "rust-analyzer/cancelFlycheck"; } impl lsp::notification::Notification for LspExtRunFlycheck { type Params = RunFlycheckParams; const METHOD: &'static str = "rust-analyzer/runFlycheck"; } #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct RunFlycheckParams { pub text_document: Option, } impl lsp::notification::Notification for LspExtClearFlycheck { type Params = (); const METHOD: &'static str = "rust-analyzer/clearFlycheck"; }