From 96a224c2a8417bdeac63b4447c18c339472cc465 Mon Sep 17 00:00:00 2001 From: da-r-k Date: Mon, 9 Feb 2026 14:54:38 +0530 Subject: [PATCH 1/4] feat: add syntax highlighting for project symbol search Implement `label_for_symbol` to provide syntax-highlighted labels in the project symbol picker (cmd-t). Without this, Java symbols appear as plain grey text. Each symbol kind generates a Tree-sitter-parseable code snippet wrapped in the appropriate context (e.g. methods/fields inside `class _ { }`) so the Java grammar can produce correct AST nodes for highlighting. Supported symbols: - Class, Interface, Enum: keyword prefix + name - Method/Function: return type + name + params (Java declaration order) - Constructor: name + params - Field/Property: type + name (Java declaration order) - Constant, EnumMember, Variable: highlighted name - Package/Module/Namespace: keyword prefix + name Co-Authored-By: Claude Opus 4.6 --- src/java.rs | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/src/java.rs b/src/java.rs index 76dc8e2..927d242 100644 --- a/src/java.rs +++ b/src/java.rs @@ -16,7 +16,7 @@ use zed_extension_api::{ self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, Extension, LanguageServerId, LanguageServerInstallationStatus, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, Worktree, - lsp::{Completion, CompletionKind}, + lsp::{Completion, CompletionKind, Symbol, SymbolKind}, register_extension, serde_json::{Value, json}, set_language_server_installation_status, @@ -535,6 +535,199 @@ impl Extension for Java { _ => None, }) } + + fn label_for_symbol( + &self, + _language_server_id: &LanguageServerId, + symbol: Symbol, + ) -> Option { + let name = &symbol.name; + + match symbol.kind { + SymbolKind::Class => { + // code: "class Name {}" → Tree-sitter: class_declaration + // display: "class Name" + let keyword = "class "; + let code = format!("{keyword}{name} {{}}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + SymbolKind::Interface => { + let keyword = "interface "; + let code = format!("{keyword}{name} {{}}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + SymbolKind::Enum => { + let keyword = "enum "; + let code = format!("{keyword}{name} {{}}"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + SymbolKind::Constructor => { + // jdtls: "ClassName(Type, Type)" + let ctor_name = name.split('(').next().unwrap_or(name); + let rest = &name[ctor_name.len()..]; + // Wrap in matching class for constructor_declaration AST node + let prefix = format!("class {ctor_name} {{ "); + let code = format!("{prefix}{ctor_name}() {{}} }}"); + let ctor_start = prefix.len(); + + let mut spans = vec![ + CodeLabelSpan::code_range(ctor_start..ctor_start + ctor_name.len()), + ]; + if !rest.is_empty() { + spans.push(CodeLabelSpan::literal(rest.to_string(), None)); + } + + Some(CodeLabel { + spans, + filter_range: (0..name.len()).into(), + code, + }) + } + SymbolKind::Method | SymbolKind::Function => { + // jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)" + // display: "ReturnType methodName(Type, Type)" (Java declaration order) + let method_name = name.split('(').next().unwrap_or(name); + let after_name = &name[method_name.len()..]; + + let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") { + (p, Some(r)) + } else { + (after_name, None) + }; + + let ret = return_type.unwrap_or("void"); + let class_open = "class _ { "; + let code = format!("{class_open}{ret} {method_name}() {{}} }}"); + + let ret_start = class_open.len(); + let name_start = ret_start + ret.len() + 1; + + // Display: "void methodName(String, int)" + let mut spans = vec![ + CodeLabelSpan::code_range(ret_start..ret_start + ret.len()), + CodeLabelSpan::literal(" ".to_string(), None), + CodeLabelSpan::code_range(name_start..name_start + method_name.len()), + ]; + if !params.is_empty() { + spans.push(CodeLabelSpan::literal(params.to_string(), None)); + } + + // filter on "methodName(params)" portion of displayed text + let type_prefix_len = ret.len() + 1; // "void " + let filter_end = type_prefix_len + method_name.len() + params.len(); + Some(CodeLabel { + spans, + filter_range: (type_prefix_len..filter_end).into(), + code, + }) + } + SymbolKind::Field | SymbolKind::Property => { + // jdtls: "fieldName : Type" or just "fieldName" + // display: "Type fieldName" (Java declaration order) + if let Some((field_name, field_type)) = name.split_once(" : ") { + let class_open = "class _ { "; + let code = format!("{class_open}{field_type} {field_name}; }}"); + + let type_start = class_open.len(); + let name_start = type_start + field_type.len() + 1; + + // Display: "String fieldName" + let spans = vec![ + CodeLabelSpan::code_range(type_start..type_start + field_type.len()), + CodeLabelSpan::literal(" ".to_string(), None), + CodeLabelSpan::code_range(name_start..name_start + field_name.len()), + ]; + + let type_prefix_len = field_type.len() + 1; // "String " + Some(CodeLabel { + spans, + filter_range: (type_prefix_len..type_prefix_len + field_name.len()).into(), + code, + }) + } else { + // No type info, just show the name + let class_open = "class _ { int "; + let code = format!("{class_open}{name}; }}"); + let name_start = class_open.len(); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range( + name_start..name_start + name.len(), + )], + filter_range: (0..name.len()).into(), + code, + }) + } + } + SymbolKind::Constant => { + // Wrap in class; ALL_CAPS names get @constant from highlights.scm regex + let class_open = "class _ { static final int "; + let code = format!("{class_open}{name}; }}"); + let name_start = class_open.len(); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range( + name_start..name_start + name.len(), + )], + filter_range: (0..name.len()).into(), + code, + }) + } + SymbolKind::EnumMember => { + // Wrap in enum for enum_constant AST node → @constant highlight + let prefix = "enum _ { "; + let code = format!("{prefix}{name} }}"); + let name_start = prefix.len(); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range( + name_start..name_start + name.len(), + )], + filter_range: (0..name.len()).into(), + code, + }) + } + SymbolKind::Variable => { + let class_open = "class _ { int "; + let code = format!("{class_open}{name}; }}"); + let name_start = class_open.len(); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range( + name_start..name_start + name.len(), + )], + filter_range: (0..name.len()).into(), + code, + }) + } + SymbolKind::Package | SymbolKind::Module | SymbolKind::Namespace => { + let keyword = "package "; + let code = format!("{keyword}{name};"); + + Some(CodeLabel { + spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], + filter_range: (keyword.len()..keyword.len() + name.len()).into(), + code, + }) + } + _ => None, + } + } } register_extension!(Java); From a1dffd9eabe1cd1dbca93a4619a34a4cfd4dc137 Mon Sep 17 00:00:00 2001 From: da-r-k Date: Mon, 9 Feb 2026 23:48:45 +0530 Subject: [PATCH 2/4] style: formatted code with cargo fmt --all --- src/java.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/java.rs b/src/java.rs index 927d242..e3f6ba6 100644 --- a/src/java.rs +++ b/src/java.rs @@ -585,9 +585,9 @@ impl Extension for Java { let code = format!("{prefix}{ctor_name}() {{}} }}"); let ctor_start = prefix.len(); - let mut spans = vec![ - CodeLabelSpan::code_range(ctor_start..ctor_start + ctor_name.len()), - ]; + let mut spans = vec![CodeLabelSpan::code_range( + ctor_start..ctor_start + ctor_name.len(), + )]; if !rest.is_empty() { spans.push(CodeLabelSpan::literal(rest.to_string(), None)); } From 48c4a51363a2dfa89d956dbc9a73e26abfef5a65 Mon Sep 17 00:00:00 2001 From: da-r-k Date: Tue, 24 Feb 2026 02:11:30 +0530 Subject: [PATCH 3/4] refactor(symbol): clean up label_for_symbol to match JDTLS output Remove handlers for symbol kinds that JDTLS workspace/symbol never returns (Constructor, Field, Constant, EnumMember, Variable, Package). Deduplicate Class/Interface/Enum into a single match arm. Add README section documenting project symbol search and CamelCase fuzzy matching. JDTLS WorkspaceSymbolHandler only searches type names (Class, Interface, Enum, Annotation) and optionally method names when includeSourceMethodDeclarations is enabled: https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/main/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/WorkspaceSymbolHandler.java Zed only calls label_for_symbol for workspace/symbol results, not textDocument/documentSymbol: https://github.com/zed-industries/zed/blob/main/crates/language_extension/src/extension_lsp_adapter.rs --- README.md | 6 ++++++ src/java.rs | 51 +++++++++++---------------------------------------- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 62d7d06..139691e 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ Here is a common `settings.json` including the above mentioned configurations: } ``` +## Project Symbol Search + +The extension supports project-wide symbol search with syntax-highlighted results. This feature is powered by JDTLS and can be accessed via Zed's symbol search. + +JDTLS uses **CamelCase fuzzy matching** for symbol queries. For example, searching for `EmpMe` would match `EmptyMedia`. The pattern works like `Emp*Me*`, matching the capital letters of CamelCase names. + ## Debugger Debug support is enabled via our [Fork of Java Debug](https://github.com/zed-industries/java-debug), which the extension will automatically download and start for you. Please refer to the [Zed Documentation](https://zed.dev/docs/debugger#getting-started) for general information about how debugging works in Zed. diff --git a/src/java.rs b/src/java.rs index e3f6ba6..4c40077 100644 --- a/src/java.rs +++ b/src/java.rs @@ -544,30 +544,13 @@ impl Extension for Java { let name = &symbol.name; match symbol.kind { - SymbolKind::Class => { - // code: "class Name {}" → Tree-sitter: class_declaration - // display: "class Name" - let keyword = "class "; - let code = format!("{keyword}{name} {{}}"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], - filter_range: (keyword.len()..keyword.len() + name.len()).into(), - code, - }) - } - SymbolKind::Interface => { - let keyword = "interface "; - let code = format!("{keyword}{name} {{}}"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], - filter_range: (keyword.len()..keyword.len() + name.len()).into(), - code, - }) - } - SymbolKind::Enum => { - let keyword = "enum "; + SymbolKind::Class | SymbolKind::Interface | SymbolKind::Enum => { + let keyword = match symbol.kind { + SymbolKind::Class => "class ", + SymbolKind::Interface => "interface ", + SymbolKind::Enum => "enum ", + _ => unreachable!(), + }; let code = format!("{keyword}{name} {{}}"); Some(CodeLabel { @@ -660,8 +643,8 @@ impl Extension for Java { code, }) } else { - // No type info, just show the name - let class_open = "class _ { int "; + // No type info — use placeholder type for valid tree-sitter parse (not displayed) + let class_open = "class _ { Object "; let code = format!("{class_open}{name}; }}"); let name_start = class_open.len(); @@ -676,7 +659,8 @@ impl Extension for Java { } SymbolKind::Constant => { // Wrap in class; ALL_CAPS names get @constant from highlights.scm regex - let class_open = "class _ { static final int "; + // Placeholder type for valid tree-sitter parse (not displayed) + let class_open = "class _ { static final Object "; let code = format!("{class_open}{name}; }}"); let name_start = class_open.len(); @@ -702,19 +686,6 @@ impl Extension for Java { code, }) } - SymbolKind::Variable => { - let class_open = "class _ { int "; - let code = format!("{class_open}{name}; }}"); - let name_start = class_open.len(); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range( - name_start..name_start + name.len(), - )], - filter_range: (0..name.len()).into(), - code, - }) - } SymbolKind::Package | SymbolKind::Module | SymbolKind::Namespace => { let keyword = "package "; let code = format!("{keyword}{name};"); From 5236fb92575880d8c560d6d4cffcd29f050cc504 Mon Sep 17 00:00:00 2001 From: da-r-k Date: Tue, 24 Feb 2026 02:25:22 +0530 Subject: [PATCH 4/4] refactor(symbol): clean up label_for_symbol to match JDTLS output Remove handlers for symbol kinds that JDTLS workspace/symbol never returns (Constructor, Field, Constant, EnumMember, Variable, Package). Deduplicate Class/Interface/Enum into a single match arm. Add README section documenting project symbol search and CamelCase fuzzy matching. JDTLS WorkspaceSymbolHandler only searches type names (Class, Interface, Enum, Annotation) and optionally method names when includeSourceMethodDeclarations is enabled: https://github.com/eclipse-jdtls/eclipse.jdt.ls/blob/main/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/WorkspaceSymbolHandler.java Zed only calls label_for_symbol for workspace/symbol results, not textDocument/documentSymbol: https://github.com/zed-industries/zed/blob/main/crates/language_extension/src/extension_lsp_adapter.rs --- src/java.rs | 99 ----------------------------------------------------- 1 file changed, 99 deletions(-) diff --git a/src/java.rs b/src/java.rs index 4c40077..05f7438 100644 --- a/src/java.rs +++ b/src/java.rs @@ -559,28 +559,6 @@ impl Extension for Java { code, }) } - SymbolKind::Constructor => { - // jdtls: "ClassName(Type, Type)" - let ctor_name = name.split('(').next().unwrap_or(name); - let rest = &name[ctor_name.len()..]; - // Wrap in matching class for constructor_declaration AST node - let prefix = format!("class {ctor_name} {{ "); - let code = format!("{prefix}{ctor_name}() {{}} }}"); - let ctor_start = prefix.len(); - - let mut spans = vec![CodeLabelSpan::code_range( - ctor_start..ctor_start + ctor_name.len(), - )]; - if !rest.is_empty() { - spans.push(CodeLabelSpan::literal(rest.to_string(), None)); - } - - Some(CodeLabel { - spans, - filter_range: (0..name.len()).into(), - code, - }) - } SymbolKind::Method | SymbolKind::Function => { // jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)" // display: "ReturnType methodName(Type, Type)" (Java declaration order) @@ -619,83 +597,6 @@ impl Extension for Java { code, }) } - SymbolKind::Field | SymbolKind::Property => { - // jdtls: "fieldName : Type" or just "fieldName" - // display: "Type fieldName" (Java declaration order) - if let Some((field_name, field_type)) = name.split_once(" : ") { - let class_open = "class _ { "; - let code = format!("{class_open}{field_type} {field_name}; }}"); - - let type_start = class_open.len(); - let name_start = type_start + field_type.len() + 1; - - // Display: "String fieldName" - let spans = vec![ - CodeLabelSpan::code_range(type_start..type_start + field_type.len()), - CodeLabelSpan::literal(" ".to_string(), None), - CodeLabelSpan::code_range(name_start..name_start + field_name.len()), - ]; - - let type_prefix_len = field_type.len() + 1; // "String " - Some(CodeLabel { - spans, - filter_range: (type_prefix_len..type_prefix_len + field_name.len()).into(), - code, - }) - } else { - // No type info — use placeholder type for valid tree-sitter parse (not displayed) - let class_open = "class _ { Object "; - let code = format!("{class_open}{name}; }}"); - let name_start = class_open.len(); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range( - name_start..name_start + name.len(), - )], - filter_range: (0..name.len()).into(), - code, - }) - } - } - SymbolKind::Constant => { - // Wrap in class; ALL_CAPS names get @constant from highlights.scm regex - // Placeholder type for valid tree-sitter parse (not displayed) - let class_open = "class _ { static final Object "; - let code = format!("{class_open}{name}; }}"); - let name_start = class_open.len(); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range( - name_start..name_start + name.len(), - )], - filter_range: (0..name.len()).into(), - code, - }) - } - SymbolKind::EnumMember => { - // Wrap in enum for enum_constant AST node → @constant highlight - let prefix = "enum _ { "; - let code = format!("{prefix}{name} }}"); - let name_start = prefix.len(); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range( - name_start..name_start + name.len(), - )], - filter_range: (0..name.len()).into(), - code, - }) - } - SymbolKind::Package | SymbolKind::Module | SymbolKind::Namespace => { - let keyword = "package "; - let code = format!("{keyword}{name};"); - - Some(CodeLabel { - spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())], - filter_range: (keyword.len()..keyword.len() + name.len()).into(), - code, - }) - } _ => None, } }