diff --git a/scip-java-proto/src/main/protobuf/scip.proto b/scip-java-proto/src/main/protobuf/scip.proto index df5bd7fd..4012309b 100644 --- a/scip-java-proto/src/main/protobuf/scip.proto +++ b/scip-java-proto/src/main/protobuf/scip.proto @@ -569,6 +569,59 @@ message Occurrence { SyntaxKind syntax_kind = 5; // Diagnostics that have been reported for this specific range. repeated Diagnostic diagnostics = 6; + // (optional) Using the same encoding as the sibling `range` field, half-open + // source range of the nearest non-trivial enclosing AST node. This range must + // enclose the `range` field. Example applications that make use of the + // enclosing_range field: + // + // - Call hierarchies: to determine what symbols are references from the body + // of a function + // - Symbol outline: to display breadcrumbs from the cursor position to the + // root of the file + // - Expand selection: to select the nearest enclosing AST node. + // - Highlight range: to indicate the AST expression that is associated with a + // hover popover + // + // For definition occurrences, the enclosing range should indicate the + // start/end bounds of the entire definition AST node, including + // documentation. + // ``` + // const n = 3 + // ^ range + // ^^^^^^^^^^^ enclosing_range + // + // /** Parses the string into something */ + // ^ enclosing_range start --------------------------------------| + // function parse(input string): string { | + // ^^^^^ range | + // return input.slice(n) | + // } | + // ^ enclosing_range end <---------------------------------------| + // ``` + // + // Any attributes/decorators/attached macros should also be part of the + // enclosing range. + // + // ```python + // @cache + // ^ enclosing_range start---------------------| + // def factorial(n): | + // return n * factorial(n-1) if n else 1 | + // < enclosing_range end-----------------------| + // + // ``` + // + // For reference occurrences, the enclosing range should indicate the start/end + // bounds of the parent expression. + // ``` + // const a = a.b + // ^ range + // ^^^ enclosing_range + // const b = a.b(41).f(42).g(43) + // ^ range + // ^^^^^^^^^^^^^ enclosing_range + // ``` + repeated int32 enclosing_range = 7; } // Represents a diagnostic, such as a compiler error or warning, which should be diff --git a/scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/ScipSemanticdb.java b/scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/ScipSemanticdb.java index 3bd4c67b..18372491 100644 --- a/scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/ScipSemanticdb.java +++ b/scip-semanticdb/src/main/java/com/sourcegraph/scip_semanticdb/ScipSemanticdb.java @@ -186,11 +186,30 @@ private void processTypedDocument( occ.getRange().getEndLine(), occ.getRange().getEndCharacter()); Package pkg = packages.packageForSymbol(occ.getSymbol()).orElse(Package.EMPTY); - tdoc.addOccurrences( + Scip.Occurrence.Builder occBuilder = Scip.Occurrence.newBuilder() .addAllRange(range) .setSymbol(typedSymbol(occ.getSymbol(), pkg)) - .setSymbolRoles(role)); + .setSymbolRoles(role); + // Add enclosing_range if it exists + if (occ.hasEnclosingRange()) { + Semanticdb.Range enclosingRange = occ.getEnclosingRange(); + boolean isEnclosingSingleLine = + enclosingRange.getStartLine() == enclosingRange.getEndLine(); + Iterable enclosingRangeInts = + isEnclosingSingleLine + ? Arrays.asList( + enclosingRange.getStartLine(), + enclosingRange.getStartCharacter(), + enclosingRange.getEndCharacter()) + : Arrays.asList( + enclosingRange.getStartLine(), + enclosingRange.getStartCharacter(), + enclosingRange.getEndLine(), + enclosingRange.getEndCharacter()); + occBuilder.addAllEnclosingRange(enclosingRangeInts); + } + tdoc.addOccurrences(occBuilder); } Symtab symtab = new Symtab(doc.semanticdb); for (SymbolInformation info : doc.semanticdb.getSymbolsList()) { diff --git a/semanticdb-java/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbBuilders.java b/semanticdb-java/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbBuilders.java index d592efaf..5fd2650a 100644 --- a/semanticdb-java/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbBuilders.java +++ b/semanticdb-java/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbBuilders.java @@ -52,12 +52,14 @@ public static Semanticdb.Signature signature(Semanticdb.TypeSignature.Builder si // SemanticDB Symbols public static Semanticdb.SymbolOccurrence symbolOccurrence( - String symbol, Semanticdb.Range range, Semanticdb.SymbolOccurrence.Role role) { - return Semanticdb.SymbolOccurrence.newBuilder() + String symbol, Semanticdb.Range range, Semanticdb.SymbolOccurrence.Role role, + java.util.Optional enclosingRange) { + Semanticdb.SymbolOccurrence.Builder builder = Semanticdb.SymbolOccurrence.newBuilder() .setSymbol(symbol) .setRange(range) - .setRole(role) - .build(); + .setRole(role); + enclosingRange.ifPresent(builder::setEnclosingRange); + return builder.build(); } public static Semanticdb.SymbolInformation.Builder symbolInformation(String symbol) { diff --git a/semanticdb-java/src/main/protobuf/semanticdb.proto b/semanticdb-java/src/main/protobuf/semanticdb.proto index 2e72263c..a671f468 100644 --- a/semanticdb-java/src/main/protobuf/semanticdb.proto +++ b/semanticdb-java/src/main/protobuf/semanticdb.proto @@ -182,6 +182,10 @@ message SymbolOccurrence { Range range = 1; string symbol = 2; Role role = 3; + // NOTE: this field does not exist in the upstream SemanticDB spec. + // It is added to support SCIP's enclosing_range field. + // This is the range of the nearest non-trivial enclosing AST node. + optional Range enclosing_range = 4; } message Scope { diff --git a/semanticdb-javac/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbVisitor.java b/semanticdb-javac/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbVisitor.java index abd02c55..5e8585f6 100644 --- a/semanticdb-javac/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbVisitor.java +++ b/semanticdb-javac/src/main/java/com/sourcegraph/semanticdb_javac/SemanticdbVisitor.java @@ -110,7 +110,8 @@ private Optional emitSymbolOccurrence( Element sym, Tree tree, Name name, Role role, CompilerRange kind) { if (sym == null || name == null) return Optional.empty(); Optional range = semanticdbRange(tree, kind, sym, name.toString()); - emitSymbolOccurrence(sym, range, role); + Optional enclosingRange = computeEnclosingRange(tree); + emitSymbolOccurrence(sym, range, role, enclosingRange); if (role == Role.DEFINITION) { // Only emit SymbolInformation for symbols that are defined in this compilation unit. emitSymbolInformation(sym, tree); @@ -118,9 +119,9 @@ private Optional emitSymbolOccurrence( return range; } - private void emitSymbolOccurrence(Element sym, Optional range, Role role) { + private void emitSymbolOccurrence(Element sym, Optional range, Role role, Optional enclosingRange) { if (sym == null) return; - Optional occ = semanticdbOccurrence(sym, range, role); + Optional occ = semanticdbOccurrence(sym, range, role, enclosingRange); occ.ifPresent(occurrences::add); } @@ -298,7 +299,7 @@ private void resolveVariableTree(VariableTree node, TreePath treePath) { if (sym.getKind() == ElementKind.ENUM_CONSTANT) { TreePath typeTreePath = nodes.get(node.getInitializer()); Element typeSym = trees.getElement(typeTreePath); - if (typeSym != null) emitSymbolOccurrence(typeSym, range, Role.REFERENCE); + if (typeSym != null) emitSymbolOccurrence(typeSym, range, Role.REFERENCE, Optional.empty()); } } } @@ -462,11 +463,11 @@ private Semanticdb.Range correctForTabs(Semanticdb.Range range, LineMap lineMap, } private Optional semanticdbOccurrence( - Element sym, Optional range, Role role) { + Element sym, Optional range, Role role, Optional enclosingRange) { if (range.isPresent()) { String ssym = semanticdbSymbol(sym); if (!ssym.equals(SemanticdbSymbols.NONE)) { - Semanticdb.SymbolOccurrence occ = symbolOccurrence(ssym, range.get(), role); + Semanticdb.SymbolOccurrence occ = symbolOccurrence(ssym, range.get(), role, enclosingRange); return Optional.of(occ); } else { return Optional.empty(); @@ -476,6 +477,51 @@ private Optional semanticdbOccurrence( } } + /** + * Computes the enclosing range for the given tree node. + * Returns the range of the nearest non-trivial enclosing AST node. + * For definition occurrences, this includes the entire definition including documentation. + * For reference occurrences, this includes the parent expression bounds. + */ + private Optional computeEnclosingRange(Tree tree) { + if (tree == null) return Optional.empty(); + + TreePath path = nodes.get(tree); + if (path == null) return Optional.empty(); + + // For method, class, and variable definitions, use the tree itself as the enclosing range + // since we're processing the definition node + Tree enclosingTree = tree; + if (!(tree instanceof MethodTree || tree instanceof ClassTree || tree instanceof VariableTree)) { + // For non-definition nodes (like references), use the parent + TreePath parentPath = path.getParentPath(); + if (parentPath == null) return Optional.empty(); + enclosingTree = parentPath.getLeaf(); + if (enclosingTree == null || enclosingTree == compUnitTree) return Optional.empty(); + } + + SourcePositions sourcePositions = trees.getSourcePositions(); + int start = (int) sourcePositions.getStartPosition(compUnitTree, enclosingTree); + int end = (int) sourcePositions.getEndPosition(compUnitTree, enclosingTree); + + if (start != Diagnostic.NOPOS && end != Diagnostic.NOPOS && end > start) { + LineMap lineMap = compUnitTree.getLineMap(); + Semanticdb.Range range = + Semanticdb.Range.newBuilder() + .setStartLine((int) lineMap.getLineNumber(start) - 1) + .setStartCharacter((int) lineMap.getColumnNumber(start) - 1) + .setEndLine((int) lineMap.getLineNumber(end) - 1) + .setEndCharacter((int) lineMap.getColumnNumber(end) - 1) + .build(); + + range = correctForTabs(range, lineMap, start); + + return Optional.of(range); + } + + return Optional.empty(); + } + private String semanticdbText() { if (source != null) return source; try {