Skip to content

Commit 0f36e0e

Browse files
da-r-kclaude
andcommitted
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 <noreply@anthropic.com>
1 parent 5bea6f1 commit 0f36e0e

1 file changed

Lines changed: 194 additions & 1 deletion

File tree

src/java.rs

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use zed_extension_api::{
1616
self as zed, CodeLabel, CodeLabelSpan, DebugAdapterBinary, DebugTaskDefinition, Extension,
1717
LanguageServerId, LanguageServerInstallationStatus, StartDebuggingRequestArguments,
1818
StartDebuggingRequestArgumentsRequest, Worktree,
19-
lsp::{Completion, CompletionKind},
19+
lsp::{Completion, CompletionKind, Symbol, SymbolKind},
2020
register_extension,
2121
serde_json::{Value, json},
2222
set_language_server_installation_status,
@@ -535,6 +535,199 @@ impl Extension for Java {
535535
_ => None,
536536
})
537537
}
538+
539+
fn label_for_symbol(
540+
&self,
541+
_language_server_id: &LanguageServerId,
542+
symbol: Symbol,
543+
) -> Option<CodeLabel> {
544+
let name = &symbol.name;
545+
546+
match symbol.kind {
547+
SymbolKind::Class => {
548+
// code: "class Name {}" → Tree-sitter: class_declaration
549+
// display: "class Name"
550+
let keyword = "class ";
551+
let code = format!("{keyword}{name} {{}}");
552+
553+
Some(CodeLabel {
554+
spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())],
555+
filter_range: (keyword.len()..keyword.len() + name.len()).into(),
556+
code,
557+
})
558+
}
559+
SymbolKind::Interface => {
560+
let keyword = "interface ";
561+
let code = format!("{keyword}{name} {{}}");
562+
563+
Some(CodeLabel {
564+
spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())],
565+
filter_range: (keyword.len()..keyword.len() + name.len()).into(),
566+
code,
567+
})
568+
}
569+
SymbolKind::Enum => {
570+
let keyword = "enum ";
571+
let code = format!("{keyword}{name} {{}}");
572+
573+
Some(CodeLabel {
574+
spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())],
575+
filter_range: (keyword.len()..keyword.len() + name.len()).into(),
576+
code,
577+
})
578+
}
579+
SymbolKind::Constructor => {
580+
// jdtls: "ClassName(Type, Type)"
581+
let ctor_name = name.split('(').next().unwrap_or(name);
582+
let rest = &name[ctor_name.len()..];
583+
// Wrap in matching class for constructor_declaration AST node
584+
let prefix = format!("class {ctor_name} {{ ");
585+
let code = format!("{prefix}{ctor_name}() {{}} }}");
586+
let ctor_start = prefix.len();
587+
588+
let mut spans = vec![
589+
CodeLabelSpan::code_range(ctor_start..ctor_start + ctor_name.len()),
590+
];
591+
if !rest.is_empty() {
592+
spans.push(CodeLabelSpan::literal(rest.to_string(), None));
593+
}
594+
595+
Some(CodeLabel {
596+
spans,
597+
filter_range: (0..name.len()).into(),
598+
code,
599+
})
600+
}
601+
SymbolKind::Method | SymbolKind::Function => {
602+
// jdtls: "methodName(Type, Type) : ReturnType" or "methodName(Type)"
603+
// display: "ReturnType methodName(Type, Type)" (Java declaration order)
604+
let method_name = name.split('(').next().unwrap_or(name);
605+
let after_name = &name[method_name.len()..];
606+
607+
let (params, return_type) = if let Some((p, r)) = after_name.split_once(" : ") {
608+
(p, Some(r))
609+
} else {
610+
(after_name, None)
611+
};
612+
613+
let ret = return_type.unwrap_or("void");
614+
let class_open = "class _ { ";
615+
let code = format!("{class_open}{ret} {method_name}() {{}} }}");
616+
617+
let ret_start = class_open.len();
618+
let name_start = ret_start + ret.len() + 1;
619+
620+
// Display: "void methodName(String, int)"
621+
let mut spans = vec![
622+
CodeLabelSpan::code_range(ret_start..ret_start + ret.len()),
623+
CodeLabelSpan::literal(" ".to_string(), None),
624+
CodeLabelSpan::code_range(name_start..name_start + method_name.len()),
625+
];
626+
if !params.is_empty() {
627+
spans.push(CodeLabelSpan::literal(params.to_string(), None));
628+
}
629+
630+
// filter on "methodName(params)" portion of displayed text
631+
let type_prefix_len = ret.len() + 1; // "void "
632+
let filter_end = type_prefix_len + method_name.len() + params.len();
633+
Some(CodeLabel {
634+
spans,
635+
filter_range: (type_prefix_len..filter_end).into(),
636+
code,
637+
})
638+
}
639+
SymbolKind::Field | SymbolKind::Property => {
640+
// jdtls: "fieldName : Type" or just "fieldName"
641+
// display: "Type fieldName" (Java declaration order)
642+
if let Some((field_name, field_type)) = name.split_once(" : ") {
643+
let class_open = "class _ { ";
644+
let code = format!("{class_open}{field_type} {field_name}; }}");
645+
646+
let type_start = class_open.len();
647+
let name_start = type_start + field_type.len() + 1;
648+
649+
// Display: "String fieldName"
650+
let spans = vec![
651+
CodeLabelSpan::code_range(type_start..type_start + field_type.len()),
652+
CodeLabelSpan::literal(" ".to_string(), None),
653+
CodeLabelSpan::code_range(name_start..name_start + field_name.len()),
654+
];
655+
656+
let type_prefix_len = field_type.len() + 1; // "String "
657+
Some(CodeLabel {
658+
spans,
659+
filter_range: (type_prefix_len..type_prefix_len + field_name.len()).into(),
660+
code,
661+
})
662+
} else {
663+
// No type info, just show the name
664+
let class_open = "class _ { int ";
665+
let code = format!("{class_open}{name}; }}");
666+
let name_start = class_open.len();
667+
668+
Some(CodeLabel {
669+
spans: vec![CodeLabelSpan::code_range(
670+
name_start..name_start + name.len(),
671+
)],
672+
filter_range: (0..name.len()).into(),
673+
code,
674+
})
675+
}
676+
}
677+
SymbolKind::Constant => {
678+
// Wrap in class; ALL_CAPS names get @constant from highlights.scm regex
679+
let class_open = "class _ { static final int ";
680+
let code = format!("{class_open}{name}; }}");
681+
let name_start = class_open.len();
682+
683+
Some(CodeLabel {
684+
spans: vec![CodeLabelSpan::code_range(
685+
name_start..name_start + name.len(),
686+
)],
687+
filter_range: (0..name.len()).into(),
688+
code,
689+
})
690+
}
691+
SymbolKind::EnumMember => {
692+
// Wrap in enum for enum_constant AST node → @constant highlight
693+
let prefix = "enum _ { ";
694+
let code = format!("{prefix}{name} }}");
695+
let name_start = prefix.len();
696+
697+
Some(CodeLabel {
698+
spans: vec![CodeLabelSpan::code_range(
699+
name_start..name_start + name.len(),
700+
)],
701+
filter_range: (0..name.len()).into(),
702+
code,
703+
})
704+
}
705+
SymbolKind::Variable => {
706+
let class_open = "class _ { int ";
707+
let code = format!("{class_open}{name}; }}");
708+
let name_start = class_open.len();
709+
710+
Some(CodeLabel {
711+
spans: vec![CodeLabelSpan::code_range(
712+
name_start..name_start + name.len(),
713+
)],
714+
filter_range: (0..name.len()).into(),
715+
code,
716+
})
717+
}
718+
SymbolKind::Package | SymbolKind::Module | SymbolKind::Namespace => {
719+
let keyword = "package ";
720+
let code = format!("{keyword}{name};");
721+
722+
Some(CodeLabel {
723+
spans: vec![CodeLabelSpan::code_range(0..keyword.len() + name.len())],
724+
filter_range: (keyword.len()..keyword.len() + name.len()).into(),
725+
code,
726+
})
727+
}
728+
_ => None,
729+
}
730+
}
538731
}
539732

540733
register_extension!(Java);

0 commit comments

Comments
 (0)