diff --git a/crates/project_model/src/cargo_workspace.rs b/crates/project_model/src/cargo_workspace.rs index 1417ac2fa1..a55901a8f7 100644 --- a/crates/project_model/src/cargo_workspace.rs +++ b/crates/project_model/src/cargo_workspace.rs @@ -1,6 +1,6 @@ //! See [`CargoWorkspace`]. -use std::convert::TryInto; +use std::convert::{TryFrom, TryInto}; use std::iter; use std::path::PathBuf; use std::{ops, process::Command}; @@ -400,6 +400,39 @@ impl CargoWorkspace { } } + pub fn parent_manifests(&self, manifest_path: &ManifestPath) -> Option> { + let mut found = false; + let parent_manifests = self + .packages() + .filter_map(|pkg| { + if !found && &self[pkg].manifest == manifest_path { + found = true + } + self[pkg].dependencies.iter().find_map(|dep| { + if &self[dep.pkg].manifest == manifest_path { + return Some(self[pkg].manifest.clone()); + } + None + }) + }) + .collect::>(); + + // some packages has this pkg as dep. return their manifests + if parent_manifests.len() > 0 { + return Some(parent_manifests); + } + + // this pkg is inside this cargo workspace, fallback to workspace root + if found { + return Some(vec![ + ManifestPath::try_from(self.workspace_root().join("Cargo.toml")).ok()? + ]); + } + + // not in this workspace + None + } + fn is_unique(&self, name: &str) -> bool { self.packages.iter().filter(|(_, v)| v.name == name).count() == 1 } diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index c3583df713..c8352660bc 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -3,6 +3,7 @@ //! `ide` crate. use std::{ + convert::TryFrom, io::Write as _, process::{self, Stdio}, }; @@ -20,15 +21,17 @@ use lsp_types::{ CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem, CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams, CodeLens, CompletionItem, Diagnostic, DiagnosticTag, DocumentFormattingParams, FoldingRange, - FoldingRangeParams, HoverContents, Location, NumberOrString, Position, PrepareRenameResponse, - Range, RenameParams, SemanticTokensDeltaParams, SemanticTokensFullDeltaResult, - SemanticTokensParams, SemanticTokensRangeParams, SemanticTokensRangeResult, - SemanticTokensResult, SymbolInformation, SymbolTag, TextDocumentIdentifier, Url, WorkspaceEdit, + FoldingRangeParams, HoverContents, Location, LocationLink, NumberOrString, Position, + PrepareRenameResponse, Range, RenameParams, SemanticTokensDeltaParams, + SemanticTokensFullDeltaResult, SemanticTokensParams, SemanticTokensRangeParams, + SemanticTokensRangeResult, SemanticTokensResult, SymbolInformation, SymbolTag, + TextDocumentIdentifier, Url, WorkspaceEdit, }; -use project_model::TargetKind; +use project_model::{ManifestPath, ProjectWorkspace, TargetKind}; use serde_json::json; use stdx::{format_to, never}; use syntax::{algo, ast, AstNode, TextRange, TextSize, T}; +use vfs::AbsPathBuf; use crate::{ cargo_target_spec::CargoTargetSpec, @@ -601,6 +604,62 @@ pub(crate) fn handle_parent_module( params: lsp_types::TextDocumentPositionParams, ) -> Result> { let _p = profile::span("handle_parent_module"); + if let Ok(file_path) = ¶ms.text_document.uri.to_file_path() { + if file_path.file_name().unwrap_or_default() == "Cargo.toml" { + // search workspaces for parent packages or fallback to workspace root + let abs_path_buf = match AbsPathBuf::try_from(file_path.to_path_buf()).ok() { + Some(abs_path_buf) => abs_path_buf, + None => return Ok(None), + }; + + let manifest_path = match ManifestPath::try_from(abs_path_buf).ok() { + Some(manifest_path) => manifest_path, + None => return Ok(None), + }; + + let links: Vec = snap + .workspaces + .iter() + .filter_map(|ws| match ws { + ProjectWorkspace::Cargo { cargo, .. } => cargo.parent_manifests(&manifest_path), + _ => None, + }) + .flatten() + .map(|parent_manifest_path| LocationLink { + origin_selection_range: None, + target_uri: to_proto::url_from_abs_path(&parent_manifest_path), + target_range: Range::default(), + target_selection_range: Range::default(), + }) + .collect::<_>(); + return Ok(Some(links.into())); + } + + // check if invoked at the crate root + let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; + let crate_id = match snap.analysis.crate_for(file_id)?.first() { + Some(&crate_id) => crate_id, + None => return Ok(None), + }; + let cargo_spec = match CargoTargetSpec::for_file(&snap, file_id)? { + Some(it) => it, + None => return Ok(None), + }; + + if snap.analysis.crate_root(crate_id)? == file_id { + let cargo_toml_url = to_proto::url_from_abs_path(&cargo_spec.cargo_toml); + let res = vec![LocationLink { + origin_selection_range: None, + target_uri: cargo_toml_url, + target_range: Range::default(), + target_selection_range: Range::default(), + }] + .into(); + return Ok(Some(res)); + } + } + + // locate parent module by semantics let position = from_proto::file_position(&snap, params)?; let navs = snap.analysis.parent_module(position)?; let res = to_proto::goto_definition_response(&snap, None, navs)?; diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts index 0e08b60a93..c9385361f8 100644 --- a/editors/code/src/commands.ts +++ b/editors/code/src/commands.ts @@ -8,7 +8,7 @@ import { applySnippetWorkspaceEdit, applySnippetTextEdits } from './snippets'; import { spawnSync } from 'child_process'; import { RunnableQuickPick, selectRunnable, createTask, createArgs } from './run'; import { AstInspector } from './ast_inspector'; -import { isRustDocument, sleep, isRustEditor } from './util'; +import { isRustDocument, isCargoTomlDocument, sleep, isRustEditor } from './util'; import { startDebugSession, makeDebugConfig } from './debug'; import { LanguageClient } from 'vscode-languageclient/node'; @@ -185,9 +185,10 @@ export function onEnter(ctx: Ctx): Cmd { export function parentModule(ctx: Ctx): Cmd { return async () => { - const editor = ctx.activeRustEditor; + const editor = vscode.window.activeTextEditor; const client = ctx.client; if (!editor || !client) return; + if (!(isRustDocument(editor.document) || isCargoTomlDocument(editor.document))) return; const locations = await client.sendRequest(ra.parentModule, { textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document), @@ -195,6 +196,7 @@ export function parentModule(ctx: Ctx): Cmd { editor.selection.active, ), }); + if (!locations) return; if (locations.length === 1) { const loc = locations[0]; diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts index ac632a0156..90796e611e 100644 --- a/editors/code/src/lsp_ext.ts +++ b/editors/code/src/lsp_ext.ts @@ -62,7 +62,7 @@ export interface MatchingBraceParams { } export const matchingBrace = new lc.RequestType("experimental/matchingBrace"); -export const parentModule = new lc.RequestType("experimental/parentModule"); +export const parentModule = new lc.RequestType("experimental/parentModule"); export interface JoinLinesParams { textDocument: lc.TextDocumentIdentifier; diff --git a/editors/code/src/util.ts b/editors/code/src/util.ts index aa57081a5f..057a3d2e19 100644 --- a/editors/code/src/util.ts +++ b/editors/code/src/util.ts @@ -104,6 +104,11 @@ export function isRustDocument(document: vscode.TextDocument): document is RustD return document.languageId === 'rust' && document.uri.scheme === 'file'; } +export function isCargoTomlDocument(document: vscode.TextDocument): document is RustDocument { + // ideally `document.languageId` should be 'toml' but user maybe not have toml extension installed + return document.uri.scheme === 'file' && document.fileName.endsWith('Cargo.toml'); +} + export function isRustEditor(editor: vscode.TextEditor): editor is RustEditor { return isRustDocument(editor.document); }