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 17 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
20 changes: 20 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,26 @@ 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. The Outline panel below the file tree in the sidebar also displays all functions, values and types in the 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?)


---

### __Open symbol by name__

__Setting:__ `elmLand.feature.openSymbolByName`

You can navigate to any top-level declaration in any file, which is a quick way of getting to the right file.

![Open symbol by name](./open-symbol-by-name.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.

And this one


---

## __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
12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@
"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
},
"elmLand.feature.openSymbolByName": {
"order": 8,
"description": "Enable the 'Open symbol by name' feature",
"type": "boolean",
"default": true
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ 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"
import * as OpenSymbolByName from "./features/open-symbol-by-name"

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

export function deactivate() {
Expand Down
245 changes: 245 additions & 0 deletions src/features/go-to-symbol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
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, token)

let symbols: vscode.DocumentSymbol[] = []
if (ast) {
symbols = ast.declarations.map(declarationToDocumentSymbol)
fallback = {
fsPath: doc.uri.fsPath,
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 open in the sidebar,
// it’s quite distracting if we return an empty list here – it 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 file,
// the Outline view shows old stuff that isn’t available – until the file becomes
// syntactically valid again – but I think it’s fine.
symbols = fallback.symbols
}

console.info('provideDocumentSymbol', `${symbols.length} results in ${Date.now() - start}ms`)
return 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 = symbol(name, symbolKind)
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
// `vscode.SymbolKind.Object` gives a nice icon looking like this: {}
case 'record': return vscode.SymbolKind.Object
case 'genericRecord': return vscode.SymbolKind.Object
}
}
Loading