Skip to content

Commit c0c61fa

Browse files
authored
Merge pull request #8 from lydell/outline
Add go-to-symbol and open-symbol-by-name features
2 parents 2300412 + 0de9ce7 commit c0c61fa

File tree

13 files changed

+507
-60
lines changed

13 files changed

+507
-60
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ Visual Studio Code is available for Mac, Windows, and Linux.
1919
- Offline-friendly package docs
2020
- Module import autocomplete
2121
- Convert HTML to Elm
22+
- Go to symbol
23+
- Open symbol by name
2224

2325
__More documentation__
2426

2527
- 🧠 Learn more about [all of the features](https://github.com/elm-land/vscode/blob/main/docs/README.md#features)
2628
- 📊 View this plugin's [performance benchmarks](https://github.com/elm-land/vscode/blob/main/docs/README.md#performance-table)
27-
- 💖 Meet [the wonderful Elm folks](https://github.com/elm-land/vscode/blob/main/docs/README.md#thank-you-elm-community) that made this project possible
29+
- 💖 Meet [the wonderful Elm folks](https://github.com/elm-land/vscode/blob/main/docs/README.md#thank-you-elm-community) that made this project possible

docs/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
- [Offline package docs](#offline-package-docs)
1212
- [Module import autocomplete](#module-import-autocomplete)
1313
- [Convert HTML to Elm](#convert-html-to-elm)
14+
- [Go to symbol](#go-to-symbol)
15+
- [Open symbol by name](#open-symbol-by-name)
1416
- 📊 [Performance Table](#performance-table)
1517
- 💖 [Thank you, Elm community](#thank-you-elm-community)
1618
- 🛠️ [Want to contribute?](#want-to-contribute)
@@ -97,6 +99,26 @@ To help you convert HTML snippets to Elm code and help newcomers learn the synta
9799

98100
---
99101

102+
### __Go to symbol__
103+
104+
__Setting:__ `elmLand.feature.goToSymbol`
105+
106+
You can navigate symbols inside a file. This is helpful for quickly navigating among functions, values and types in a file. The Outline panel below the file tree in the sidebar also displays all functions, values and types in the file.
107+
108+
![Go to symbol](./go-to-symbol.gif)
109+
110+
---
111+
112+
### __Open symbol by name__
113+
114+
__Setting:__ `elmLand.feature.openSymbolByName`
115+
116+
You can navigate to any top-level declaration in any file, which is a quick way of getting to the right file.
117+
118+
![Open symbol by name](./open-symbol-by-name.gif)
119+
120+
---
121+
100122
## __Performance Table__
101123

102124
Elm's [editor plugins repo](https://github.com/elm/editor-plugins) recommends doing performance profiling to help others learn how different editors implement features, and also to help try to think of ways to bring down costs.

package.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,20 @@
113113
"type": "boolean",
114114
"default": true
115115
},
116-
"elmLand.compilerFilepath": {
116+
"elmLand.feature.goToSymbol": {
117117
"order": 7,
118+
"description": "Enable the 'Go-to-symbol' feature",
119+
"type": "boolean",
120+
"default": true
121+
},
122+
"elmLand.feature.openSymbolByName": {
123+
"order": 8,
124+
"description": "Enable the 'Open symbol by name' feature",
125+
"type": "boolean",
126+
"default": true
127+
},
128+
"elmLand.compilerFilepath": {
129+
"order": 9,
118130
"description": "The path to your Elm compiler",
119131
"type": "string",
120132
"default": "elm"
@@ -157,4 +169,4 @@
157169
"terser": "5.16.3",
158170
"typescript": "4.9.4"
159171
}
160-
}
172+
}

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import * as JumpToDefinition from "./features/jump-to-definition"
77
import * as OfflinePackageDocs from "./features/offline-package-docs"
88
import * as TypeDrivenAutocomplete from './features/type-driven-autocomplete'
99
import * as HtmlToElm from './features/html-to-elm'
10+
import * as GoToSymbol from "./features/go-to-symbol"
11+
import * as OpenSymbolByName from "./features/open-symbol-by-name"
1012

1113
export async function activate(context: vscode.ExtensionContext) {
1214
console.info("ACTIVATE")
@@ -32,6 +34,8 @@ export async function activate(context: vscode.ExtensionContext) {
3234
OfflinePackageDocs.feature({ globalState, context })
3335
TypeDrivenAutocomplete.feature({ globalState, context })
3436
HtmlToElm.feature({ globalState, context })
37+
GoToSymbol.feature({ globalState, context })
38+
OpenSymbolByName.feature({ globalState, context })
3539
}
3640

3741
export function deactivate() {

src/features/go-to-symbol.ts

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
import * as vscode from 'vscode'
2+
import sharedLogic, { Feature } from './shared/logic'
3+
import * as ElmToAst from './shared/elm-to-ast'
4+
import * as ElmSyntax from './shared/elm-to-ast/elm-syntax'
5+
6+
type Fallback = {
7+
fsPath: string
8+
symbols: vscode.DocumentSymbol[]
9+
}
10+
11+
export const feature: Feature = ({ context }) => {
12+
let fallback: Fallback | undefined = undefined
13+
14+
context.subscriptions.push(
15+
vscode.languages.registerDocumentSymbolProvider('elm', {
16+
async provideDocumentSymbols(doc: vscode.TextDocument, token: vscode.CancellationToken) {
17+
// Allow user to disable this feature
18+
const isEnabled: boolean = vscode.workspace.getConfiguration('elmLand').feature.goToSymbol
19+
if (!isEnabled) return
20+
21+
const start = Date.now()
22+
const text = doc.getText()
23+
const ast = await ElmToAst.run(text, token)
24+
25+
let symbols: vscode.DocumentSymbol[] = []
26+
if (ast) {
27+
symbols = ast.declarations.map(declarationToDocumentSymbol)
28+
fallback = {
29+
fsPath: doc.uri.fsPath,
30+
symbols,
31+
}
32+
} else if (fallback !== undefined && doc.uri.fsPath === fallback.fsPath) {
33+
// When you start editing code, it won’t have correct syntax straight away,
34+
// but VSCode will re-run this. If you have the Outline panel open in the sidebar,
35+
// it’s quite distracting if we return an empty list here – it will flash
36+
// between “no symbols” and all the symbols. So returning the symbols from last
37+
// time we got any improves the UX a little. Note: If you remove all text in the file,
38+
// the Outline view shows old stuff that isn’t available – until the file becomes
39+
// syntactically valid again – but I think it’s fine.
40+
symbols = fallback.symbols
41+
}
42+
43+
console.info('provideDocumentSymbol', `${symbols.length} results in ${Date.now() - start}ms`)
44+
return symbols
45+
}
46+
})
47+
)
48+
}
49+
50+
const declarationToDocumentSymbol = (declaration: ElmSyntax.Node<ElmSyntax.Declaration>): vscode.DocumentSymbol => {
51+
const symbol = (
52+
name: ElmSyntax.Node<string>,
53+
symbolKind: vscode.SymbolKind,
54+
fullRange: ElmSyntax.Range = declaration.range
55+
) => new vscode.DocumentSymbol(
56+
name.value,
57+
'',
58+
symbolKind,
59+
sharedLogic.fromElmRange(fullRange),
60+
sharedLogic.fromElmRange(name.range)
61+
)
62+
63+
const symbolWithChildren = (
64+
name: ElmSyntax.Node<string>,
65+
symbolKind: vscode.SymbolKind,
66+
children: vscode.DocumentSymbol[]
67+
) => {
68+
const documentSymbol = symbol(name, symbolKind)
69+
documentSymbol.children = children
70+
return documentSymbol
71+
}
72+
73+
switch (declaration.value.type) {
74+
case 'function':
75+
return symbolWithChildren(
76+
declaration.value.function.declaration.value.name,
77+
vscode.SymbolKind.Function,
78+
expressionToDocumentSymbols(declaration.value.function.declaration.value.expression.value)
79+
)
80+
81+
case 'destructuring':
82+
return symbolWithChildren(
83+
{
84+
value: patternToString(declaration.value.destructuring.pattern.value),
85+
range: declaration.value.destructuring.pattern.range
86+
},
87+
vscode.SymbolKind.Function,
88+
expressionToDocumentSymbols(declaration.value.destructuring.expression.value)
89+
)
90+
91+
case 'typeAlias':
92+
return symbol(
93+
declaration.value.typeAlias.name,
94+
typeAliasSymbolKind(declaration.value.typeAlias.typeAnnotation.value)
95+
)
96+
97+
case 'typedecl':
98+
return symbolWithChildren(
99+
declaration.value.typedecl.name,
100+
vscode.SymbolKind.Enum,
101+
declaration.value.typedecl.constructors.map(constructor =>
102+
symbol(
103+
constructor.value.name,
104+
vscode.SymbolKind.EnumMember,
105+
constructor.range
106+
)
107+
)
108+
)
109+
110+
case 'port':
111+
return symbol(
112+
declaration.value.port.name,
113+
vscode.SymbolKind.Function
114+
)
115+
116+
case 'infix':
117+
return symbol(
118+
declaration.value.infix.operator,
119+
vscode.SymbolKind.Operator
120+
)
121+
}
122+
}
123+
124+
const expressionToDocumentSymbols = (expression: ElmSyntax.Expression): vscode.DocumentSymbol[] => {
125+
switch (expression.type) {
126+
case 'unit':
127+
return []
128+
129+
case 'application':
130+
return expression.application.flatMap(node => expressionToDocumentSymbols(node.value))
131+
132+
case 'operatorapplication':
133+
return [
134+
...expressionToDocumentSymbols(expression.operatorapplication.left.value),
135+
...expressionToDocumentSymbols(expression.operatorapplication.right.value),
136+
]
137+
138+
case 'functionOrValue':
139+
return []
140+
141+
case 'ifBlock':
142+
return [
143+
...expressionToDocumentSymbols(expression.ifBlock.clause.value),
144+
...expressionToDocumentSymbols(expression.ifBlock.then.value),
145+
...expressionToDocumentSymbols(expression.ifBlock.else.value),
146+
]
147+
148+
case 'prefixoperator':
149+
return []
150+
151+
case 'operator':
152+
return []
153+
154+
case 'hex':
155+
return []
156+
157+
case 'integer':
158+
return []
159+
160+
case 'float':
161+
return []
162+
163+
case 'negation':
164+
return expressionToDocumentSymbols(expression.negation.value)
165+
166+
case 'literal':
167+
return []
168+
169+
case 'charLiteral':
170+
return []
171+
172+
case 'tupled':
173+
return expression.tupled.flatMap(node => expressionToDocumentSymbols(node.value))
174+
175+
case 'list':
176+
return expression.list.flatMap(node => expressionToDocumentSymbols(node.value))
177+
178+
case 'parenthesized':
179+
return expressionToDocumentSymbols(expression.parenthesized.value)
180+
181+
case 'let':
182+
return [
183+
...expression.let.declarations.map(declarationToDocumentSymbol),
184+
...expressionToDocumentSymbols(expression.let.expression.value),
185+
]
186+
187+
case 'case':
188+
return [
189+
...expressionToDocumentSymbols(expression.case.expression.value),
190+
...expression.case.cases.flatMap(node => expressionToDocumentSymbols(node.expression.value)),
191+
]
192+
193+
case 'lambda':
194+
return expressionToDocumentSymbols(expression.lambda.expression.value)
195+
196+
case 'recordAccess':
197+
return expressionToDocumentSymbols(expression.recordAccess.expression.value)
198+
199+
case 'recordAccessFunction':
200+
return []
201+
202+
case 'record':
203+
return expression.record.flatMap(item => expressionToDocumentSymbols(item.value.expression.value))
204+
205+
case 'recordUpdate':
206+
return expression.recordUpdate.updates.flatMap(item => expressionToDocumentSymbols(item.value.expression.value))
207+
208+
case 'glsl':
209+
return []
210+
}
211+
}
212+
213+
const patternToString = (pattern: ElmSyntax.Pattern): string => {
214+
switch (pattern.type) {
215+
case 'string': return 'STRING' // should not happen
216+
case 'all': return '_'
217+
case 'unit': return '()'
218+
case 'char': return 'CHAR' // should not happen
219+
case 'hex': return 'HEX' // should not happen
220+
case 'int': return 'INT' // should not happen
221+
case 'float': return 'FLOAT' // should not happen
222+
case 'tuple': return `( ${pattern.tuple.value.map(value => patternToString(value.value)).join(', ')} )`
223+
case 'record': return `{ ${pattern.record.value.map(node => node.value).join(', ')} }`
224+
case 'uncons': return 'UNCONS' // should not happen
225+
case 'list': return 'LIST' // should not happen
226+
case 'var': return pattern.var.value
227+
case 'named': return pattern.named.patterns.map(node => patternToString(node.value)).join(' ')
228+
case 'as': return pattern.as.name.value
229+
case 'parentisized': return patternToString(pattern.parentisized.value.value)
230+
}
231+
}
232+
233+
const typeAliasSymbolKind = (typeAnnotation: ElmSyntax.TypeAnnotation): vscode.SymbolKind => {
234+
switch (typeAnnotation.type) {
235+
// Note: In VSCode, TypeScript `type Foo =` gets `vscode.SymbolKind.Variable`.
236+
case 'function': return vscode.SymbolKind.Variable
237+
case 'generic': return vscode.SymbolKind.Variable
238+
case 'typed': return vscode.SymbolKind.Variable
239+
case 'unit': return vscode.SymbolKind.Variable
240+
case 'tupled': return vscode.SymbolKind.Variable
241+
// `vscode.SymbolKind.Object` gives a nice icon looking like this: {}
242+
case 'record': return vscode.SymbolKind.Object
243+
case 'genericRecord': return vscode.SymbolKind.Object
244+
}
245+
}

0 commit comments

Comments
 (0)