Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add go-to-symbol and open-symbol-by-name features #8

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ To help you convert HTML snippets to Elm code and help newcomers learn the synta

---

### __Go to symbol__

__Setting:__ `elmLand.feature.goToSymbol`

You can navigate symbols inside a file. This is helpful for quickly navigating among functions, values and types in a file.

![Go to symbol](./go-to-symbol.gif)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Create this gif

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Update the performance tables further below

(should probably be done on the same computer as the other features?)


---

## __Performance Table__

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.
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@
"description": "Enable the 'HTML to Elm' feature",
"type": "boolean",
"default": true
},
"elmLand.feature.goToSymbol": {
"order": 7,
"description": "Enable the 'Go-to-symbol' feature",
"type": "boolean",
"default": true
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as JumpToDefinition from "./features/jump-to-definition"
import * as OfflinePackageDocs from "./features/offline-package-docs"
import * as TypeDrivenAutocomplete from './features/type-driven-autocomplete'
import * as HtmlToElm from './features/html-to-elm'
import * as GoToSymbol from "./features/go-to-symbol"

export async function activate(context: vscode.ExtensionContext) {
console.info("ACTIVATE")
Expand All @@ -32,6 +33,7 @@ export async function activate(context: vscode.ExtensionContext) {
OfflinePackageDocs.feature({ globalState, context })
TypeDrivenAutocomplete.feature({ globalState, context })
HtmlToElm.feature({ globalState, context })
GoToSymbol.feature({ globalState, context })
}

export function deactivate() {
Expand Down
248 changes: 248 additions & 0 deletions src/features/go-to-symbol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import * as vscode from 'vscode'
import sharedLogic, { Feature } from './shared/logic'
import * as ElmToAst from './shared/elm-to-ast'
import * as ElmSyntax from './shared/elm-to-ast/elm-syntax'

type Fallback = {
fsPath: string
symbols: vscode.DocumentSymbol[]
}

export const feature: Feature = ({ context }) => {
let fallback: Fallback | undefined = undefined

context.subscriptions.push(
vscode.languages.registerDocumentSymbolProvider('elm', {
async provideDocumentSymbols(doc: vscode.TextDocument, token: vscode.CancellationToken) {
// Allow user to disable this feature
const isEnabled: boolean = vscode.workspace.getConfiguration('elmLand').feature.goToSymbol
if (!isEnabled) return

const start = Date.now()
const text = doc.getText()
const ast = await ElmToAst.run(text)

if (ast) {
const symbols = ast.declarations.map(declarationToDocumentSymbol)
fallback = {
fsPath: doc.uri.fsPath,
symbols,
}
console.info('provideDocumentSymbol', `${Date.now() - start}ms`)
return symbols
} else if (fallback !== undefined && doc.uri.fsPath === fallback.fsPath) {
// When you start editing code, it won’t have correct syntax straight away,
// but VSCode will re-run this. If you have the Outline panel in the sidebar,
// it’s quite distracting if we return an empty list here – the list will flash
// between “no symbols” and all the symbols. So returning the symbols from last
// time we got any improves the UX a little. Note: If you remove all text in the,
// the Outline view will show old stuff that isn’t available until the file becomes
// syntactically valid again – but I think it’s fine.
return fallback.symbols
}
}
})
)
}

const declarationToDocumentSymbol = (declaration: ElmSyntax.Node<ElmSyntax.Declaration>): vscode.DocumentSymbol => {
const symbol = (
name: ElmSyntax.Node<string>,
symbolKind: vscode.SymbolKind,
fullRange: ElmSyntax.Range = declaration.range
) => new vscode.DocumentSymbol(
name.value,
'',
symbolKind,
sharedLogic.fromElmRange(fullRange),
sharedLogic.fromElmRange(name.range)
)

const symbolWithChildren = (
name: ElmSyntax.Node<string>,
symbolKind: vscode.SymbolKind,
children: vscode.DocumentSymbol[]
) => {
const documentSymbol = new vscode.DocumentSymbol(
name.value,
'',
symbolKind,
sharedLogic.fromElmRange(declaration.range),
sharedLogic.fromElmRange(name.range)
)
documentSymbol.children = children
return documentSymbol
}

switch (declaration.value.type) {
case 'function':
return symbolWithChildren(
declaration.value.function.declaration.value.name,
vscode.SymbolKind.Function,
expressionToDocumentSymbols(declaration.value.function.declaration.value.expression.value)
)

case 'destructuring':
return symbolWithChildren(
{
value: patternToString(declaration.value.destructuring.pattern.value),
range: declaration.value.destructuring.pattern.range
},
vscode.SymbolKind.Function,
expressionToDocumentSymbols(declaration.value.destructuring.expression.value)
)

case 'typeAlias':
return symbol(
declaration.value.typeAlias.name,
typeAliasSymbolKind(declaration.value.typeAlias.typeAnnotation.value)
)

case 'typedecl':
return symbolWithChildren(
declaration.value.typedecl.name,
vscode.SymbolKind.Enum,
declaration.value.typedecl.constructors.map(constructor =>
symbol(
constructor.value.name,
vscode.SymbolKind.EnumMember,
constructor.range
)
)
)

case 'port':
return symbol(
declaration.value.port.name,
vscode.SymbolKind.Function
)

case 'infix':
return symbol(
declaration.value.infix.operator,
vscode.SymbolKind.Operator
)
}
}

const expressionToDocumentSymbols = (expression: ElmSyntax.Expression): vscode.DocumentSymbol[] => {
switch (expression.type) {
case 'unit':
return []

case 'application':
return expression.application.flatMap(node => expressionToDocumentSymbols(node.value))

case 'operatorapplication':
return [
...expressionToDocumentSymbols(expression.operatorapplication.left.value),
...expressionToDocumentSymbols(expression.operatorapplication.right.value),
]

case 'functionOrValue':
return []

case 'ifBlock':
return [
...expressionToDocumentSymbols(expression.ifBlock.clause.value),
...expressionToDocumentSymbols(expression.ifBlock.then.value),
...expressionToDocumentSymbols(expression.ifBlock.else.value),
]

case 'prefixoperator':
return []

case 'operator':
return []

case 'hex':
return []

case 'integer':
return []

case 'float':
return []

case 'negation':
return expressionToDocumentSymbols(expression.negation.value)

case 'literal':
return []

case 'charLiteral':
return []

case 'tupled':
return expression.tupled.flatMap(node => expressionToDocumentSymbols(node.value))

case 'list':
return expression.list.flatMap(node => expressionToDocumentSymbols(node.value))

case 'parenthesized':
return expressionToDocumentSymbols(expression.parenthesized.value)

case 'let':
return [
...expression.let.declarations.map(declarationToDocumentSymbol),
...expressionToDocumentSymbols(expression.let.expression.value),
]

case 'case':
return [
...expressionToDocumentSymbols(expression.case.expression.value),
...expression.case.cases.flatMap(node => expressionToDocumentSymbols(node.expression.value)),
]

case 'lambda':
return expressionToDocumentSymbols(expression.lambda.expression.value)

case 'recordAccess':
return expressionToDocumentSymbols(expression.recordAccess.expression.value)

case 'recordAccessFunction':
return []

case 'record':
return expression.record.flatMap(item => expressionToDocumentSymbols(item.value.expression.value))

case 'recordUpdate':
return expression.recordUpdate.updates.flatMap(item => expressionToDocumentSymbols(item.value.expression.value))

case 'glsl':
return []
}
}

const patternToString = (pattern: ElmSyntax.Pattern): string => {
switch (pattern.type) {
case 'string': return 'STRING' // should not happen
case 'all': return '_'
case 'unit': return '()'
case 'char': return 'CHAR' // should not happen
case 'hex': return 'HEX' // should not happen
case 'int': return 'INT' // should not happen
case 'float': return 'FLOAT' // should not happen
case 'tuple': return `( ${pattern.tuple.value.map(value => patternToString(value.value)).join(', ')} )`
case 'record': return `{ ${pattern.record.value.map(node => node.value).join(', ')} }`
case 'uncons': return 'UNCONS' // should not happen
case 'list': return 'LIST' // should not happen
case 'var': return pattern.var.value
case 'named': return pattern.named.patterns.map(node => patternToString(node.value)).join(' ')
case 'as': return pattern.as.name.value
case 'parentisized': return patternToString(pattern.parentisized.value.value)
}
}

const typeAliasSymbolKind = (typeAnnotation: ElmSyntax.TypeAnnotation): vscode.SymbolKind => {
switch (typeAnnotation.type) {
// Note: In VSCode, TypeScript `type Foo =` gets `vscode.SymbolKind.Variable`.
case 'function': return vscode.SymbolKind.Variable
case 'generic': return vscode.SymbolKind.Variable
case 'typed': return vscode.SymbolKind.Variable
case 'unit': return vscode.SymbolKind.Variable
case 'tupled': return vscode.SymbolKind.Variable
case 'record': return vscode.SymbolKind.Object
case 'genericRecord': return vscode.SymbolKind.Object
}
}