|
5 | 5 |
|
6 | 6 | package software.amazon.smithy.lsp.language; |
7 | 7 |
|
| 8 | +import static software.amazon.smithy.lsp.protocol.LspAdapter.identRange; |
| 9 | + |
| 10 | +import java.util.ArrayList; |
| 11 | +import java.util.EnumSet; |
8 | 12 | import java.util.List; |
| 13 | +import java.util.ListIterator; |
9 | 14 | import java.util.function.Consumer; |
10 | 15 | import org.eclipse.lsp4j.DocumentSymbol; |
11 | | -import org.eclipse.lsp4j.Range; |
12 | 16 | import org.eclipse.lsp4j.SymbolInformation; |
13 | 17 | import org.eclipse.lsp4j.SymbolKind; |
14 | 18 | import org.eclipse.lsp4j.jsonrpc.messages.Either; |
15 | 19 | import software.amazon.smithy.lsp.document.Document; |
16 | | -import software.amazon.smithy.lsp.protocol.LspAdapter; |
17 | 20 | import software.amazon.smithy.lsp.syntax.Syntax; |
18 | 21 |
|
19 | 22 | public record DocumentSymbolHandler(Document document, List<Syntax.Statement> statements) { |
| 23 | + // Statement types that may appear before the start of a shape's members, which |
| 24 | + // we need to skip. |
| 25 | + private static final EnumSet<Syntax.Statement.Type> BEFORE_MEMBER_TYPES = EnumSet.of( |
| 26 | + Syntax.Statement.Type.ForResource, |
| 27 | + Syntax.Statement.Type.Mixins, |
| 28 | + Syntax.Statement.Type.Block |
| 29 | + ); |
| 30 | + |
20 | 31 | /** |
21 | 32 | * @return A list of DocumentSymbol |
22 | 33 | */ |
23 | 34 | public List<Either<SymbolInformation, DocumentSymbol>> handle() { |
24 | | - return statements.stream() |
25 | | - .mapMulti(this::addSymbols) |
26 | | - .toList(); |
| 35 | + List<Either<SymbolInformation, DocumentSymbol>> result = new ArrayList<>(); |
| 36 | + // Passing around the list would make the code super noisy, and we'd have |
| 37 | + // to do Either.forRight everywhere, so use a consumer. |
| 38 | + addSymbols((symbol) -> result.add(Either.forRight(symbol))); |
| 39 | + return result; |
27 | 40 | } |
28 | 41 |
|
29 | | - private void addSymbols(Syntax.Statement statement, Consumer<Either<SymbolInformation, DocumentSymbol>> consumer) { |
30 | | - switch (statement) { |
31 | | - case Syntax.Statement.TraitApplication app -> addSymbol(consumer, app.id(), SymbolKind.Class); |
| 42 | + private void addSymbols(Consumer<DocumentSymbol> consumer) { |
| 43 | + var listIterator = statements.listIterator(); |
| 44 | + while (listIterator.hasNext()) { |
| 45 | + var statement = listIterator.next(); |
| 46 | + if (statement instanceof Syntax.Statement.Namespace namespace) { |
| 47 | + consumer.accept(namespaceSymbol(namespace)); |
| 48 | + } else if (statement instanceof Syntax.Statement.ShapeDef shapeDef) { |
| 49 | + var symbol = rootSymbol(shapeDef); |
| 50 | + consumer.accept(symbol); |
| 51 | + addMemberSymbols(listIterator, symbol); |
| 52 | + } |
| 53 | + } |
| 54 | + } |
32 | 55 |
|
33 | | - case Syntax.Statement.ShapeDef def -> addSymbol(consumer, def.shapeName(), SymbolKind.Class); |
| 56 | + private void addMemberSymbols(ListIterator<Syntax.Statement> listIterator, DocumentSymbol parent) { |
| 57 | + // We only want to collect members within the block, so we can use Block's lastMemberIndex |
| 58 | + // to tell us when to stop. |
| 59 | + int lastMemberIndex = 0; |
| 60 | + while (listIterator.hasNext()) { |
| 61 | + var statement = listIterator.next(); |
34 | 62 |
|
35 | | - case Syntax.Statement.EnumMemberDef def -> addSymbol(consumer, def.name(), SymbolKind.Enum); |
| 63 | + if (!BEFORE_MEMBER_TYPES.contains(statement.type())) { |
| 64 | + // No members |
| 65 | + listIterator.previous(); |
| 66 | + return; |
| 67 | + } |
36 | 68 |
|
37 | | - case Syntax.Statement.ElidedMemberDef def -> addSymbol(consumer, def.name(), SymbolKind.Property); |
| 69 | + if (statement instanceof Syntax.Statement.Block block) { |
| 70 | + // Update the parent's range to cover all its members |
| 71 | + var blockEnd = document.positionAtIndex(block.end()); |
| 72 | + parent.getRange().setEnd(blockEnd); |
| 73 | + lastMemberIndex = block.lastStatementIndex(); |
| 74 | + break; |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + List<DocumentSymbol> children = new ArrayList<>(); |
38 | 79 |
|
39 | | - case Syntax.Statement.MemberDef def -> { |
40 | | - addSymbol(consumer, def.name(), SymbolKind.Property); |
41 | | - if (def.target() != null) { |
42 | | - addSymbol(consumer, def.target(), SymbolKind.Class); |
| 80 | + while (listIterator.nextIndex() <= lastMemberIndex) { |
| 81 | + var statement = listIterator.next(); |
| 82 | + |
| 83 | + switch (statement) { |
| 84 | + case Syntax.Statement.MemberDef memberDef -> { |
| 85 | + children.add(memberDefSymbol(memberDef)); |
| 86 | + } |
| 87 | + |
| 88 | + case Syntax.Statement.EnumMemberDef enumMemberDef -> { |
| 89 | + children.add(enumMemberDefSymbol(enumMemberDef)); |
| 90 | + } |
| 91 | + |
| 92 | + case Syntax.Statement.ElidedMemberDef elidedMemberDef -> { |
| 93 | + children.add(elidedMemberDefSymbol(elidedMemberDef)); |
| 94 | + } |
| 95 | + |
| 96 | + case Syntax.Statement.NodeMemberDef nodeMemberDef -> { |
| 97 | + children.add(nodeMemberDefSymbol(nodeMemberDef)); |
| 98 | + } |
| 99 | + |
| 100 | + case Syntax.Statement.InlineMemberDef inlineMemberDef -> { |
| 101 | + children.add(inlineMemberSymbol(listIterator, inlineMemberDef)); |
| 102 | + } |
| 103 | + |
| 104 | + default -> { |
43 | 105 | } |
44 | | - } |
45 | | - default -> { |
46 | 106 | } |
47 | 107 | } |
| 108 | + |
| 109 | + if (!children.isEmpty()) { |
| 110 | + parent.setChildren(children); |
| 111 | + } |
48 | 112 | } |
49 | 113 |
|
50 | | - private void addSymbol( |
51 | | - Consumer<Either<SymbolInformation, DocumentSymbol>> consumer, |
52 | | - Syntax.Ident ident, |
53 | | - SymbolKind symbolKind |
54 | | - ) { |
55 | | - Range range = LspAdapter.identRange(ident, document); |
56 | | - if (range == null) { |
57 | | - return; |
| 114 | + private DocumentSymbol namespaceSymbol(Syntax.Statement.Namespace namespace) { |
| 115 | + var range = document.rangeBetween(namespace.start(), namespace.end()); |
| 116 | + var selectionRange = identRange(namespace.namespace(), document); |
| 117 | + return new DocumentSymbol( |
| 118 | + namespace.namespace().stringValue(), |
| 119 | + SymbolKind.Namespace, |
| 120 | + range, |
| 121 | + selectionRange |
| 122 | + ); |
| 123 | + } |
| 124 | + |
| 125 | + private DocumentSymbol rootSymbol(Syntax.Statement.ShapeDef shapeDef) { |
| 126 | + var symbolKind = getSymbolKind(shapeDef); |
| 127 | + var range = document.rangeBetween(shapeDef.start(), shapeDef.end()); |
| 128 | + var selectionRange = identRange(shapeDef.shapeName(), document); |
| 129 | + return new DocumentSymbol( |
| 130 | + shapeDef.shapeName().stringValue(), |
| 131 | + symbolKind, |
| 132 | + range, |
| 133 | + selectionRange |
| 134 | + ); |
| 135 | + } |
| 136 | + |
| 137 | + private static SymbolKind getSymbolKind(Syntax.Statement.ShapeDef shapeDef) { |
| 138 | + return switch (shapeDef.shapeType().stringValue()) { |
| 139 | + case "enum", "intEnum" -> SymbolKind.Enum; |
| 140 | + default -> SymbolKind.Class; |
| 141 | + }; |
| 142 | + } |
| 143 | + |
| 144 | + private DocumentSymbol memberDefSymbol(Syntax.Statement.MemberDef memberDef) { |
| 145 | + var range = document.rangeBetween(memberDef.start(), memberDef.end()); |
| 146 | + var selectionRange = identRange(memberDef.name(), document); |
| 147 | + var detail = memberDef.target() == null |
| 148 | + ? null |
| 149 | + : memberDef.target().stringValue(); |
| 150 | + |
| 151 | + return new DocumentSymbol( |
| 152 | + memberDef.name().stringValue(), |
| 153 | + SymbolKind.Field, |
| 154 | + range, |
| 155 | + selectionRange, |
| 156 | + detail |
| 157 | + ); |
| 158 | + } |
| 159 | + |
| 160 | + private DocumentSymbol enumMemberDefSymbol(Syntax.Statement.EnumMemberDef enumMemberDef) { |
| 161 | + var range = document.rangeBetween(enumMemberDef.start(), enumMemberDef.end()); |
| 162 | + var selectionRange = identRange(enumMemberDef.name(), document); |
| 163 | + return new DocumentSymbol( |
| 164 | + enumMemberDef.name().stringValue(), |
| 165 | + SymbolKind.EnumMember, |
| 166 | + range, |
| 167 | + selectionRange |
| 168 | + ); |
| 169 | + } |
| 170 | + |
| 171 | + private DocumentSymbol elidedMemberDefSymbol(Syntax.Statement.ElidedMemberDef elidedMemberDef) { |
| 172 | + var range = document.rangeBetween(elidedMemberDef.start(), elidedMemberDef.end()); |
| 173 | + return new DocumentSymbol( |
| 174 | + "$" + elidedMemberDef.name().stringValue(), |
| 175 | + SymbolKind.Field, |
| 176 | + range, |
| 177 | + range |
| 178 | + ); |
| 179 | + } |
| 180 | + |
| 181 | + private DocumentSymbol nodeMemberDefSymbol(Syntax.Statement.NodeMemberDef nodeMemberDef) { |
| 182 | + var range = document.rangeBetween(nodeMemberDef.start(), nodeMemberDef.end()); |
| 183 | + var selectionRange = identRange(nodeMemberDef.name(), document); |
| 184 | + String detail = null; |
| 185 | + if (nodeMemberDef.value() instanceof Syntax.Ident ident) { |
| 186 | + detail = ident.stringValue(); |
58 | 187 | } |
59 | 188 |
|
60 | | - DocumentSymbol symbol = new DocumentSymbol(ident.stringValue(), symbolKind, range, range); |
61 | | - consumer.accept(Either.forRight(symbol)); |
| 189 | + return new DocumentSymbol( |
| 190 | + nodeMemberDef.name().stringValue(), |
| 191 | + SymbolKind.Property, |
| 192 | + range, |
| 193 | + selectionRange, |
| 194 | + detail |
| 195 | + ); |
| 196 | + } |
| 197 | + |
| 198 | + private DocumentSymbol inlineMemberSymbol( |
| 199 | + ListIterator<Syntax.Statement> listIterator, |
| 200 | + Syntax.Statement.InlineMemberDef inlineMemberDef |
| 201 | + ) { |
| 202 | + var range = document.rangeBetween(inlineMemberDef.start(), inlineMemberDef.end()); |
| 203 | + var selectionRange = identRange(inlineMemberDef.name(), document); |
| 204 | + var inlineSymbol = new DocumentSymbol( |
| 205 | + inlineMemberDef.name().stringValue(), |
| 206 | + SymbolKind.Property, |
| 207 | + range, |
| 208 | + selectionRange |
| 209 | + ); |
| 210 | + |
| 211 | + addMemberSymbols(listIterator, inlineSymbol); |
| 212 | + return inlineSymbol; |
62 | 213 | } |
63 | 214 | } |
0 commit comments