@@ -90,16 +90,23 @@ public String getDescription() {
9090 - 'directory' (medium)
9191 - 'project' (slowest, scans up to 500 files)
9292
93+ SYMBOLS FILTERING (action='symbols' only):
94+ - kind: filter by symbol kind (class, method, field, constructor, constant, etc.)
95+ - brief: true for compact output (name + line only, no signatures/tokens)
96+ - Combine both for minimal output: kind='method', brief=true
97+
9398 PERFORMANCE TIPS:
9499 - Use scope='file' first, expand if needed
95100 - Prefer line/column over symbol name when possible
96101 - If a later edit depends on navigation, prefer strict=true to reject fallback cursor resolution
97102 - Large projects (1000+ files): expect delays for scope=project
103+ - For large files (100+ symbols): use kind filter or brief=true to reduce output
98104
99105 EXAMPLES:
100106 {"action":"definition", "path":"User.java", "line":42, "column":15}
101107 {"action":"references", "path":"User.java", "symbol":"getUserById", "scope":"file"}
102108 {"action":"symbols", "path":"User.java"}
109+ {"action":"symbols", "path":"User.java", "kind":"method", "brief":true}
103110 {"action":"hover", "path":"User.java", "line":10}
104111
105112 LANGUAGES: Java, Kotlin, JS/TS/TSX, Python, Go, Rust, C/C++, C#, PHP, HTML
@@ -144,6 +151,13 @@ public JsonNode getInputSchema() {
144151 props .putObject ("strict" ).put ("type" , "boolean" ).put ("description" ,
145152 "For definition/references by position: require exact cursor resolution. If fallback or enclosing-symbol logic would be used, return no result." );
146153
154+ props .putObject ("kind" ).put ("type" , "string" ).put ("description" ,
155+ "For 'symbols': filter by symbol kind. Values: class, interface, method, function, field, " +
156+ "variable, constructor, constant, enum, property. Omit to show all symbols." );
157+ props .putObject ("brief" ).put ("type" , "boolean" ).put ("description" ,
158+ "For 'symbols': compact output — name and line only, no signatures, types, or tokens. " +
159+ "Reduces output size for large files. Default: false." );
160+
147161 var required = schema .putArray ("required" );
148162 required .add ("action" );
149163 required .add ("path" );
@@ -178,7 +192,7 @@ public JsonNode execute(JsonNode params) throws Exception {
178192 case "definition" -> executeDefinition (path , params );
179193 case "references" -> executeReferences (path , params );
180194 case "hover" -> executeHover (path , params );
181- case "symbols" -> executeSymbols (path );
195+ case "symbols" -> executeSymbols (path , params );
182196 default -> throw new NtsException (NtsErrorCode .PARAM_INVALID , "action" , action );
183197 };
184198 }
@@ -292,7 +306,11 @@ private JsonNode executeReferences(Path path, JsonNode params) throws IOExceptio
292306 if (symbolName != null && !symbolName .isEmpty ()) {
293307 resolution = resolver .resolveDefinitionByName (path , symbolName );
294308 if ("ambiguous" .equals (resolution .status ())) {
295- return createAmbiguousResponse ("references" , resolution );
309+ // Auto-resolve: if one candidate is CLASS and others are CONSTRUCTOR, pick the CLASS
310+ resolution = tryAutoResolveClassVsConstructor (resolution );
311+ if ("ambiguous" .equals (resolution .status ())) {
312+ return createAmbiguousResponse ("references" , resolution );
313+ }
296314 }
297315 references = resolution .target () != null
298316 ? resolver .findReferencesByHandle (resolution .target (), scope , includeDeclaration )
@@ -306,7 +324,10 @@ private JsonNode executeReferences(Path path, JsonNode params) throws IOExceptio
306324 resolution );
307325 }
308326 if ("ambiguous" .equals (resolution .status ())) {
309- return createAmbiguousResponse ("references" , resolution );
327+ resolution = tryAutoResolveClassVsConstructor (resolution );
328+ if ("ambiguous" .equals (resolution .status ())) {
329+ return createAmbiguousResponse ("references" , resolution );
330+ }
310331 }
311332 references = resolution .target () != null
312333 ? resolver .findReferencesByHandle (resolution .target (), scope , includeDeclaration )
@@ -394,7 +415,10 @@ private JsonNode executeHover(Path path, JsonNode params) throws IOException {
394415 if (symbolName != null && !symbolName .isEmpty ()) {
395416 resolution = resolver .resolveDefinitionByName (path , symbolName );
396417 if ("ambiguous" .equals (resolution .status ())) {
397- return createAmbiguousResponse ("hover" , resolution );
418+ resolution = tryAutoResolveClassVsConstructor (resolution );
419+ if ("ambiguous" .equals (resolution .status ())) {
420+ return createAmbiguousResponse ("hover" , resolution );
421+ }
398422 }
399423 symbol = resolver .hoverByName (path , symbolName );
400424 } else if (line > 0 ) {
@@ -451,17 +475,34 @@ private JsonNode executeHover(Path path, JsonNode params) throws IOException {
451475
452476 /**
453477 * List Symbols: все символы в файле.
478+ * Supports optional filtering by 'kind' and compact output via 'brief'.
454479 */
455- private JsonNode executeSymbols (Path path ) throws IOException {
480+ private JsonNode executeSymbols (Path path , JsonNode params ) throws IOException {
456481 List <SymbolInfo > symbols = resolver .listSymbols (path );
457482
458483 if (symbols .isEmpty ()) {
459484 return createTextResponse ("No symbols found in " + path .getFileName ());
460485 }
461486
487+ // Apply kind filter if specified
488+ String kindFilter = params .path ("kind" ).asText (null );
489+ if (kindFilter != null && !kindFilter .isEmpty ()) {
490+ symbols = symbols .stream ()
491+ .filter (s -> matchesSymbolKind (s .kind (), kindFilter ))
492+ .toList ();
493+ if (symbols .isEmpty ()) {
494+ return createTextResponse ("No " + kindFilter + " symbols found in " + path .getFileName ());
495+ }
496+ }
497+
498+ boolean brief = params .path ("brief" ).asBoolean (false );
499+
462500 StringBuilder sb = new StringBuilder ();
463- sb .append ("**Symbols in " ).append (path .getFileName ()).append ("** (" )
464- .append (symbols .size ()).append (" total)\n \n " );
501+ sb .append ("**Symbols in " ).append (path .getFileName ()).append ("**" );
502+ if (kindFilter != null ) {
503+ sb .append (" [kind=" ).append (kindFilter ).append ("]" );
504+ }
505+ sb .append (" (" ).append (symbols .size ()).append (" total)\n \n " );
465506
466507 // Группируем по типу
467508 Map <SymbolKind , List <SymbolInfo >> byKind = symbols .stream ()
@@ -475,18 +516,23 @@ private JsonNode executeSymbols(Path path) throws IOException {
475516
476517 for (SymbolInfo sym : syms ) {
477518 Location loc = sym .location ();
478- // Регистрируем токен ТОЛЬКО для строки сигнатуры (не для всего файла)
479- String token = registerAccessForRange (path , loc .startLine (), loc .startLine ());
480-
481519 String indent = sym .parentName () != null ? " " : "" ;
482- sb .append (indent ).append ("- `" ).append (sym .name ()).append ("`" );
483- if (sym .signature () != null ) {
484- sb .append (" — `" ).append (sym .signature ()).append ("`" );
485- } else if (sym .type () != null ) {
486- sb .append (": " ).append (sym .type ());
520+
521+ if (brief ) {
522+ // Compact: name + line only
523+ sb .append (indent ).append ("- `" ).append (sym .name ()).append ("` L" ).append (loc .startLine ()).append ("\n " );
524+ } else {
525+ // Full: name + signature/type + line + token
526+ String token = registerAccessForRange (path , loc .startLine (), loc .startLine ());
527+ sb .append (indent ).append ("- `" ).append (sym .name ()).append ("`" );
528+ if (sym .signature () != null ) {
529+ sb .append (" — `" ).append (sym .signature ()).append ("`" );
530+ } else if (sym .type () != null ) {
531+ sb .append (": " ).append (sym .type ());
532+ }
533+ sb .append (" (line " ).append (loc .startLine ());
534+ sb .append (" | TOKEN: `" ).append (token ).append ("`)\n " );
487535 }
488- sb .append (" (line " ).append (loc .startLine ());
489- sb .append (" | TOKEN: `" ).append (token ).append ("`)\n " );
490536 }
491537 sb .append ("\n " );
492538 }
@@ -497,6 +543,61 @@ private JsonNode executeSymbols(Path path) throws IOException {
497543 return createTextResponse (sb .toString ());
498544 }
499545
546+ /**
547+ * Auto-resolves ambiguity when candidates are a CLASS and its CONSTRUCTORs.
548+ * In Java/Kotlin, class name and constructor name are identical, causing false ambiguity.
549+ * Selects the CLASS candidate since that's what users almost always want.
550+ */
551+ private ResolutionResult tryAutoResolveClassVsConstructor (ResolutionResult resolution ) {
552+ if (resolution .candidates () == null || resolution .candidates ().size () < 2 ) {
553+ return resolution ;
554+ }
555+
556+ SymbolHandle classCandidate = null ;
557+ boolean allSameNameAndClassOrCtor = true ;
558+ String firstName = resolution .candidates ().get (0 ).name ();
559+
560+ for (SymbolHandle candidate : resolution .candidates ()) {
561+ if (!candidate .name ().equals (firstName )) {
562+ allSameNameAndClassOrCtor = false ;
563+ break ;
564+ }
565+ SymbolInfo .SymbolKind kind = candidate .kind ();
566+ if (kind == SymbolInfo .SymbolKind .CLASS || kind == SymbolInfo .SymbolKind .INTERFACE
567+ || kind == SymbolInfo .SymbolKind .ENUM ) {
568+ classCandidate = candidate ;
569+ } else if (kind != SymbolInfo .SymbolKind .CONSTRUCTOR ) {
570+ allSameNameAndClassOrCtor = false ;
571+ break ;
572+ }
573+ }
574+
575+ if (allSameNameAndClassOrCtor && classCandidate != null ) {
576+ return ResolutionResult .exact (classCandidate , "auto_resolved_class_vs_constructor" ,
577+ resolution .resolutionKind ());
578+ }
579+ return resolution ;
580+ }
581+
582+ /**
583+ * Checks if a SymbolKind matches the user-specified kind filter string.
584+ */
585+ private boolean matchesSymbolKind (SymbolKind kind , String filter ) {
586+ return switch (filter .toLowerCase ()) {
587+ case "class" -> kind == SymbolKind .CLASS ;
588+ case "interface" -> kind == SymbolKind .INTERFACE ;
589+ case "enum" -> kind == SymbolKind .ENUM ;
590+ case "method" -> kind == SymbolKind .METHOD ;
591+ case "function" -> kind == SymbolKind .FUNCTION ;
592+ case "constructor" -> kind == SymbolKind .CONSTRUCTOR ;
593+ case "field" -> kind == SymbolKind .FIELD ;
594+ case "variable" -> kind == SymbolKind .VARIABLE ;
595+ case "constant" -> kind == SymbolKind .CONSTANT ;
596+ case "property" -> kind == SymbolKind .PROPERTY ;
597+ default -> true ;
598+ };
599+ }
600+
500601 // ===================== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ =====================
501602
502603 /**
@@ -582,7 +683,7 @@ private List<int[]> groupLocationsIntoRanges(List<Location> locations) {
582683 return Collections .emptyList ();
583684 }
584685
585- List <int []> ranges = new ArrayList <>();
686+ List <int []> ranges = new it . unimi . dsi . fastutil . objects . ObjectArrayList <>();
586687 int [] current = null ;
587688
588689 for (Location loc : locations ) {
0 commit comments