lsp: Support Goto Declaration (#15785)

Luis Cossío and Thorsten Ball created

Adds support for [Goto
Declaration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_declaration)
LSP command.

I am particularly interested in [this for Rust
projects](https://rust-analyzer.github.io/manual.html#go-to-declaration),
to be able to navigate to the place where a trait method is declared,
coming from a trait method implementation.

I noticed this was something I could do in VSCode before, but was
somehow missing is Zed. Thanks to the already existing infrastructure
for Goto Definition, I just followed and copy-paste-adapted it for Goto
Declaration.

As a bonus, I added `ctrl-F12` and `alt-ctrl-F12` as default macOS
keybindings for `GoToDeclaration` and `GoToDeclarationSplit`,
respectively. They are not keybindings from another editor, but I
figured they made sense to be grouped along with the other *F12
commands.

### Release Notes:

- Added "Go to declaration" editor action.
- vim: Breaking change to keybindings after introduction of the `Go to
declaration` editor action. The new keybindings are the following (and
can be found [here](https://zed.dev/docs/vim), alongside the other key
bindings):
  - `g d` - Go to definition
  - `g D` - Go to declaration
  - `g y` - Go to type definition
  - `g I` - Go to implementation




https://github.com/user-attachments/assets/ee5c10a8-94f0-4e50-afbb-6f71db540c1b

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

assets/keymaps/default-macos.json       |   4 
assets/keymaps/vim.json                 |   5 
crates/editor/src/actions.rs            |   2 
crates/editor/src/editor.rs             |  18 ++++
crates/editor/src/element.rs            |   6 +
crates/editor/src/mouse_context_menu.rs |   2 
crates/project/src/lsp_command.rs       | 104 +++++++++++++++++++++++++++
crates/project/src/project.rs           |  25 ++++++
crates/proto/proto/zed.proto            |  15 +++
crates/proto/src/proto.rs               |   4 +
crates/zed/src/zed/app_menus.rs         |   1 
docs/src/key-bindings.md                |   2 
docs/src/vim.md                         |   6 +
13 files changed, 188 insertions(+), 6 deletions(-)

Detailed changes

assets/keymaps/default-macos.json 🔗

@@ -127,7 +127,9 @@
       "cmd-'": "editor::ToggleHunkDiff",
       "cmd-\"": "editor::ExpandAllHunkDiffs",
       "cmd-alt-g b": "editor::ToggleGitBlame",
-      "cmd-i": "editor::ShowSignatureHelp"
+      "cmd-i": "editor::ShowSignatureHelp",
+      "ctrl-f12": "editor::GoToDeclaration",
+      "alt-ctrl-f12": "editor::GoToDeclarationSplit"
     }
   },
   {

assets/keymaps/vim.json 🔗

@@ -89,8 +89,9 @@
       "g t": "pane::ActivateNextItem",
       "g shift-t": "pane::ActivatePrevItem",
       "g d": "editor::GoToDefinition",
-      "g shift-d": "editor::GoToTypeDefinition",
-      "g cmd-d": "editor::GoToImplementation",
+      "g shift-d": "editor::GoToDeclaration",
+      "g y": "editor::GoToTypeDefinition",
+      "g shift-i": "editor::GoToImplementation",
       "g x": "editor::OpenUrl",
       "g n": "vim::SelectNextMatch",
       "g shift-n": "vim::SelectPreviousMatch",

crates/editor/src/actions.rs 🔗

@@ -210,6 +210,8 @@ gpui::actions!(
         Format,
         GoToDefinition,
         GoToDefinitionSplit,
+        GoToDeclaration,
+        GoToDeclarationSplit,
         GoToDiagnostic,
         GoToHunk,
         GoToImplementation,

crates/editor/src/editor.rs 🔗

@@ -1539,6 +1539,7 @@ pub(crate) struct NavigationData {
 
 enum GotoDefinitionKind {
     Symbol,
+    Declaration,
     Type,
     Implementation,
 }
@@ -8948,6 +8949,22 @@ impl Editor {
         self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx)
     }
 
+    pub fn go_to_declaration(
+        &mut self,
+        _: &GoToDeclaration,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<bool>> {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, cx)
+    }
+
+    pub fn go_to_declaration_split(
+        &mut self,
+        _: &GoToDeclaration,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<bool>> {
+        self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, cx)
+    }
+
     pub fn go_to_implementation(
         &mut self,
         _: &GoToImplementation,
@@ -9008,6 +9025,7 @@ impl Editor {
         let project = workspace.read(cx).project().clone();
         let definitions = project.update(cx, |project, cx| match kind {
             GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
+            GotoDefinitionKind::Declaration => project.declaration(&buffer, head, cx),
             GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
             GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
         });

crates/editor/src/element.rs 🔗

@@ -300,6 +300,12 @@ impl EditorElement {
         register_action(view, cx, |editor, a, cx| {
             editor.go_to_definition_split(a, cx).detach_and_log_err(cx);
         });
+        register_action(view, cx, |editor, a, cx| {
+            editor.go_to_declaration(a, cx).detach_and_log_err(cx);
+        });
+        register_action(view, cx, |editor, a, cx| {
+            editor.go_to_declaration_split(a, cx).detach_and_log_err(cx);
+        });
         register_action(view, cx, |editor, a, cx| {
             editor.go_to_implementation(a, cx).detach_and_log_err(cx);
         });

crates/editor/src/mouse_context_menu.rs 🔗

@@ -1,5 +1,6 @@
 use std::ops::Range;
 
+use crate::GoToDeclaration;
 use crate::{
     selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut, DisplayPoint,
     DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
@@ -163,6 +164,7 @@ pub fn deploy_context_menu(
                 .on_blur_subscription(Subscription::new(|| {}))
                 .action("Rename Symbol", Box::new(Rename))
                 .action("Go to Definition", Box::new(GoToDefinition))
+                .action("Go to Declaration", Box::new(GoToDeclaration))
                 .action("Go to Type Definition", Box::new(GoToTypeDefinition))
                 .action("Go to Implementation", Box::new(GoToImplementation))
                 .action("Find All References", Box::new(FindAllReferences))

crates/project/src/lsp_command.rs 🔗

@@ -117,6 +117,10 @@ pub struct GetDefinition {
     pub position: PointUtf16,
 }
 
+pub(crate) struct GetDeclaration {
+    pub position: PointUtf16,
+}
+
 pub(crate) struct GetTypeDefinition {
     pub position: PointUtf16,
 }
@@ -521,6 +525,106 @@ impl LspCommand for GetDefinition {
     }
 }
 
+#[async_trait(?Send)]
+impl LspCommand for GetDeclaration {
+    type Response = Vec<LocationLink>;
+    type LspRequest = lsp::request::GotoDeclaration;
+    type ProtoRequest = proto::GetDeclaration;
+
+    fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
+        capabilities
+            .server_capabilities
+            .declaration_provider
+            .is_some()
+    }
+
+    fn to_lsp(
+        &self,
+        path: &Path,
+        _: &Buffer,
+        _: &Arc<LanguageServer>,
+        _: &AppContext,
+    ) -> lsp::GotoDeclarationParams {
+        lsp::GotoDeclarationParams {
+            text_document_position_params: lsp::TextDocumentPositionParams {
+                text_document: lsp::TextDocumentIdentifier {
+                    uri: lsp::Url::from_file_path(path).unwrap(),
+                },
+                position: point_to_lsp(self.position),
+            },
+            work_done_progress_params: Default::default(),
+            partial_result_params: Default::default(),
+        }
+    }
+
+    async fn response_from_lsp(
+        self,
+        message: Option<lsp::GotoDeclarationResponse>,
+        project: Model<Project>,
+        buffer: Model<Buffer>,
+        server_id: LanguageServerId,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_lsp(message, project, buffer, server_id, cx).await
+    }
+
+    fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration {
+        proto::GetDeclaration {
+            project_id,
+            buffer_id: buffer.remote_id().into(),
+            position: Some(language::proto::serialize_anchor(
+                &buffer.anchor_before(self.position),
+            )),
+            version: serialize_version(&buffer.version()),
+        }
+    }
+
+    async fn from_proto(
+        message: proto::GetDeclaration,
+        _: Model<Project>,
+        buffer: Model<Buffer>,
+        mut cx: AsyncAppContext,
+    ) -> Result<Self> {
+        let position = message
+            .position
+            .and_then(deserialize_anchor)
+            .ok_or_else(|| anyhow!("invalid position"))?;
+        buffer
+            .update(&mut cx, |buffer, _| {
+                buffer.wait_for_version(deserialize_version(&message.version))
+            })?
+            .await?;
+        Ok(Self {
+            position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
+        })
+    }
+
+    fn response_to_proto(
+        response: Vec<LocationLink>,
+        project: &mut Project,
+        peer_id: PeerId,
+        _: &clock::Global,
+        cx: &mut AppContext,
+    ) -> proto::GetDeclarationResponse {
+        let links = location_links_to_proto(response, project, peer_id, cx);
+        proto::GetDeclarationResponse { links }
+    }
+
+    async fn response_from_proto(
+        self,
+        message: proto::GetDeclarationResponse,
+        project: Model<Project>,
+        _: Model<Buffer>,
+        cx: AsyncAppContext,
+    ) -> Result<Vec<LocationLink>> {
+        location_links_from_proto(message.links, project, cx).await
+    }
+
+    fn buffer_id_from_proto(message: &proto::GetDeclaration) -> Result<BufferId> {
+        BufferId::new(message.buffer_id)
+    }
+}
+
 #[async_trait(?Send)]
 impl LspCommand for GetImplementation {
     type Response = Vec<LocationLink>;

crates/project/src/project.rs 🔗

@@ -714,6 +714,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
+        client.add_model_request_handler(Self::handle_lsp_command::<GetDeclaration>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
         client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
@@ -5463,6 +5464,30 @@ impl Project {
         self.definition_impl(buffer, position, cx)
     }
 
+    fn declaration_impl(
+        &self,
+        buffer: &Model<Buffer>,
+        position: PointUtf16,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<LocationLink>>> {
+        self.request_lsp(
+            buffer.clone(),
+            LanguageServerToQuery::Primary,
+            GetDeclaration { position },
+            cx,
+        )
+    }
+
+    pub fn declaration<T: ToPointUtf16>(
+        &self,
+        buffer: &Model<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<LocationLink>>> {
+        let position = position.to_point_utf16(buffer.read(cx));
+        self.declaration_impl(buffer, position, cx)
+    }
+
     fn type_definition_impl(
         &self,
         buffer: &Model<Buffer>,

crates/proto/proto/zed.proto 🔗

@@ -48,6 +48,8 @@ message Envelope {
 
         GetDefinition get_definition = 32;
         GetDefinitionResponse get_definition_response = 33;
+        GetDeclaration get_declaration = 237;
+        GetDeclarationResponse get_declaration_response = 238; // current max
         GetTypeDefinition get_type_definition = 34;
         GetTypeDefinitionResponse get_type_definition_response = 35;
 
@@ -696,12 +698,23 @@ message GetDefinition {
      uint64 buffer_id = 2;
      Anchor position = 3;
      repeated VectorClockEntry version = 4;
- }
+}
 
 message GetDefinitionResponse {
     repeated LocationLink links = 1;
 }
 
+message GetDeclaration {
+     uint64 project_id = 1;
+     uint64 buffer_id = 2;
+     Anchor position = 3;
+     repeated VectorClockEntry version = 4;
+}
+
+message GetDeclarationResponse {
+    repeated LocationLink links = 1;
+}
+
 message GetTypeDefinition {
      uint64 project_id = 1;
      uint64 buffer_id = 2;

crates/proto/src/proto.rs 🔗

@@ -239,6 +239,8 @@ messages!(
     (GetCompletionsResponse, Background),
     (GetDefinition, Background),
     (GetDefinitionResponse, Background),
+    (GetDeclaration, Background),
+    (GetDeclarationResponse, Background),
     (GetDocumentHighlights, Background),
     (GetDocumentHighlightsResponse, Background),
     (GetHover, Background),
@@ -437,6 +439,7 @@ request_messages!(
     (GetCodeActions, GetCodeActionsResponse),
     (GetCompletions, GetCompletionsResponse),
     (GetDefinition, GetDefinitionResponse),
+    (GetDeclaration, GetDeclarationResponse),
     (GetImplementation, GetImplementationResponse),
     (GetDocumentHighlights, GetDocumentHighlightsResponse),
     (GetHover, GetHoverResponse),
@@ -551,6 +554,7 @@ entity_messages!(
     GetCodeActions,
     GetCompletions,
     GetDefinition,
+    GetDeclaration,
     GetImplementation,
     GetDocumentHighlights,
     GetHover,

crates/zed/src/zed/app_menus.rs 🔗

@@ -145,6 +145,7 @@ pub fn app_menus() -> Vec<Menu> {
                 MenuItem::action("Go to Line/Column...", editor::actions::ToggleGoToLine),
                 MenuItem::separator(),
                 MenuItem::action("Go to Definition", editor::actions::GoToDefinition),
+                MenuItem::action("Go to Declaration", editor::actions::GoToDeclaration),
                 MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition),
                 MenuItem::action("Find All References", editor::actions::FindAllReferences),
                 MenuItem::separator(),

docs/src/key-bindings.md 🔗

@@ -218,6 +218,8 @@ See the [tasks documentation](/docs/tasks#custom-keybindings-for-tasks) for more
 | Format                           | Editor     | `⌘ + Shift + I`                 |
 | Go to definition                 | Editor     | `F12`                           |
 | Go to definition split           | Editor     | `Alt + F12`                     |
+| Go to declaration                | Editor     | `Ctrl + F12`                    |
+| Go to declaration split          | Editor     | `Alt + Ctrl + F12`              |
 | Go to diagnostic                 | Editor     | `F8`                            |
 | Go to implementation             | Editor     | `Shift + F12`                   |
 | Go to prev diagnostic            | Editor     | `Shift + F8`                    |

docs/src/vim.md 🔗

@@ -17,8 +17,10 @@ Vim mode has several "core Zed" key bindings, that will help you make the most o
 ```
 # Language server
 g d     Go to definition
-g D     Go to type definition
-g cmd-d Go to implementation
+g D     Go to declaration
+g y     Go to type definition
+g I     Go to implementation
+
 c d     Rename (change definition)
 g A     Go to All references to the current word