diff --git a/README.md b/README.md index 47b6244..de06c77 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,11 @@ Visual Studio Code is available for Mac, Windows, and Linux. - Offline-friendly package docs - Module import autocomplete - Convert HTML to Elm +- Go to symbol +- Open symbol by name __More documentation__ - 🧠 Learn more about [all of the features](https://github.com/elm-land/vscode/blob/main/docs/README.md#features) - 📊 View this plugin's [performance benchmarks](https://github.com/elm-land/vscode/blob/main/docs/README.md#performance-table) -- 💖 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 \ No newline at end of file +- 💖 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 diff --git a/docs/README.md b/docs/README.md index e442f1e..b0f286a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,8 @@ - [Offline package docs](#offline-package-docs) - [Module import autocomplete](#module-import-autocomplete) - [Convert HTML to Elm](#convert-html-to-elm) + - [Go to symbol](#go-to-symbol) + - [Open symbol by name](#open-symbol-by-name) - 📊 [Performance Table](#performance-table) - 💖 [Thank you, Elm community](#thank-you-elm-community) - 🛠️ [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 --- +### __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) + +--- + +### __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) + +--- + ## __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. diff --git a/package.json b/package.json index e1adeb1..6eb4f63 100644 --- a/package.json +++ b/package.json @@ -113,8 +113,20 @@ "type": "boolean", "default": true }, - "elmLand.compilerFilepath": { + "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 + }, + "elmLand.compilerFilepath": { + "order": 9, "description": "The path to your Elm compiler", "type": "string", "default": "elm" @@ -157,4 +169,4 @@ "terser": "5.16.3", "typescript": "4.9.4" } -} \ No newline at end of file +} diff --git a/src/extension.ts b/src/extension.ts index 6072d3e..7cf65d0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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") @@ -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() { diff --git a/src/features/go-to-symbol.ts b/src/features/go-to-symbol.ts new file mode 100644 index 0000000..a1d5fce --- /dev/null +++ b/src/features/go-to-symbol.ts @@ -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): vscode.DocumentSymbol => { + const symbol = ( + name: ElmSyntax.Node, + 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, + 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 + } +} \ No newline at end of file diff --git a/src/features/jump-to-definition.ts b/src/features/jump-to-definition.ts index 013cbbb..6772a8b 100644 --- a/src/features/jump-to-definition.ts +++ b/src/features/jump-to-definition.ts @@ -48,10 +48,11 @@ const provider = (globalState: GlobalState) => { position: vscode.Position ast: ElmSyntax.Ast elmJsonFile: ElmJsonFile + token: vscode.CancellationToken } const handleJumpToLinksForImports = - async ({ position, ast, elmJsonFile }: HandleJumpToLinksForImportsInput) + async ({ position, ast, elmJsonFile, token }: HandleJumpToLinksForImportsInput) : Promise => { for (let import_ of ast.imports) { @@ -65,7 +66,7 @@ const provider = (globalState: GlobalState) => { let fileUri = await findLocalProjectFileUri(elmJsonFile, moduleName) if (fileUri) { let otherDocument = await vscode.workspace.openTextDocument(fileUri) - let otherAst = await ElmToAst.run(otherDocument.getText()) + let otherAst = await ElmToAst.run(otherDocument.getText(), token) if (otherAst) { const otherModuleData: ElmSyntax.ModuleData = ElmSyntax.toModuleData(otherAst) return new vscode.Location( @@ -92,7 +93,7 @@ const provider = (globalState: GlobalState) => { if (fileUri) { let otherDocument = await vscode.workspace.openTextDocument(fileUri) - let otherAst = await ElmToAst.run(otherDocument.getText()) + let otherAst = await ElmToAst.run(otherDocument.getText(), token) if (otherAst) { const topOfFileRange = ElmSyntax.toModuleData(otherAst).moduleName.range @@ -126,9 +127,10 @@ const provider = (globalState: GlobalState) => { ast: ElmSyntax.Ast, doc: vscode.TextDocument elmJsonFile: ElmJsonFile + token: vscode.CancellationToken } - const handleJumpToLinksForDeclarations = async ({ position, ast, doc, elmJsonFile }: HandleJumpToLinksForDeclarationsInput): Promise => { + const handleJumpToLinksForDeclarations = async ({ position, ast, doc, elmJsonFile, token }: HandleJumpToLinksForDeclarationsInput): Promise => { let { aliasMappingToModuleNames, explicitExposingValuesForImports, @@ -143,17 +145,18 @@ const provider = (globalState: GlobalState) => { type FindLocationOfItemFromImportedFilesInput = { findItemWithName: (ast: ElmSyntax.Ast, moduleName: string) => ElmSyntax.Node | undefined isItemExposedFromModule: (ast: ElmSyntax.Ast, moduleName: string) => boolean + token: vscode.CancellationToken } const findLocationOfItemFromImportedFiles = - ({ findItemWithName, isItemExposedFromModule }: FindLocationOfItemFromImportedFilesInput) => + ({ findItemWithName, isItemExposedFromModule, token }: FindLocationOfItemFromImportedFilesInput) => async (importModuleNames: string[], moduleName: string) => { for (let importedModuleName of importModuleNames) { let importedModuleNameUri = await findLocalProjectFileUri(elmJsonFile, importedModuleName) if (importedModuleNameUri) { let importedDoc = await vscode.workspace.openTextDocument(importedModuleNameUri) - let importedAst = await ElmToAst.run(importedDoc.getText()) + let importedAst = await ElmToAst.run(importedDoc.getText(), token) if (importedAst) { let item = findItemWithName(importedAst, moduleName) @@ -271,12 +274,14 @@ const provider = (globalState: GlobalState) => { const findLocationOfDeclarationFromImportedFiles = findLocationOfItemFromImportedFiles({ findItemWithName: ElmSyntax.findDeclarationWithName, - isItemExposedFromModule: isDeclarationExposedFromModule + isItemExposedFromModule: isDeclarationExposedFromModule, + token }) const findLocationOfCustomTypeVariantFromImportedFiles = findLocationOfItemFromImportedFiles({ findItemWithName: ElmSyntax.findCustomTypeVariantWithName, - isItemExposedFromModule: isCustomTypeVariantExposedFromModule + isItemExposedFromModule: isCustomTypeVariantExposedFromModule, + token }) type FindLocationOfItemForModuleNameInput = { @@ -1004,7 +1009,7 @@ const provider = (globalState: GlobalState) => { const start = Date.now() const text = doc.getText() - const ast = await ElmToAst.run(text) + const ast = await ElmToAst.run(text, token) if (ast) { @@ -1019,14 +1024,14 @@ const provider = (globalState: GlobalState) => { } // Handle module imports - matchingLocation = await handleJumpToLinksForImports({ position, ast, elmJsonFile }) + matchingLocation = await handleJumpToLinksForImports({ position, ast, elmJsonFile, token }) if (matchingLocation) { console.info('provideDefinition', `${Date.now() - start}ms`) return matchingLocation } // Handle module declarations - matchingLocation = await handleJumpToLinksForDeclarations({ position, ast, doc, elmJsonFile }) + matchingLocation = await handleJumpToLinksForDeclarations({ position, ast, doc, elmJsonFile, token }) if (matchingLocation) { console.info('provideDefinition', `${Date.now() - start}ms`) return matchingLocation diff --git a/src/features/offline-package-docs.ts b/src/features/offline-package-docs.ts index c8a0943..9f4cbc9 100644 --- a/src/features/offline-package-docs.ts +++ b/src/features/offline-package-docs.ts @@ -22,7 +22,7 @@ export const feature: Feature = ({ globalState, context }) => { let start = Date.now() let text = document.getText() - let ast = await ElmToAst.run(text) + let ast = await ElmToAst.run(text, token) let uri = vscode.Uri.parse("command:elmLand.browsePackageDocs") let elmJsonFile = SharedLogic.findElmJsonFor(globalState, document.uri) diff --git a/src/features/open-symbol-by-name.ts b/src/features/open-symbol-by-name.ts new file mode 100644 index 0000000..b1b35b0 --- /dev/null +++ b/src/features/open-symbol-by-name.ts @@ -0,0 +1,78 @@ +import * as vscode from 'vscode' +import { Feature } from './shared/logic' + +// This finds all top-level declarations (type, type alias, values, functions, ports) +// with the following caveats: +// - Odd spacing or inline comments can cause declarations to be missed. +// - False positives might be found in multiline comments or multiline strings. +// But it’s a very fast way of computing an important feature! +// If there are a couple of false positives you can jump to, that’s not the end of the world. +// For example, if you comment out some code using multiline comments, you’ll still be able +// to jump to it, which is a bit unexpected but not super weird. +// Parts come from here: https://github.com/rtfeldman/node-test-runner/blob/eedf853fc9b45afd73a0db72decebdb856a69771/lib/Parser.js#L229-L234 +const topLevelDeclarationRegex = /\n|(?<=^type(?: +alias)? +)\p{Lu}[_\d\p{L}]*|^\p{Ll}[_\d\p{L}]*(?=.*=$)|^(?<=port +)\p{Ll}[_\d\p{L}]*(?= *:)/gmu + +export const feature: Feature = ({ context }) => { + context.subscriptions.push( + vscode.languages.registerWorkspaceSymbolProvider({ + async provideWorkspaceSymbols(query: string, token: vscode.CancellationToken) { + // Allow user to disable this feature + const isEnabled: boolean = vscode.workspace.getConfiguration('elmLand').feature.openSymbolByName + if (!isEnabled) return + + const start = Date.now() + const symbols: vscode.SymbolInformation[] = [] + const uris = await vscode.workspace.findFiles('**/*.elm', '**/{node_modules,elm-stuff}/**') + + for (const uri of uris) { + const buffer = await vscode.workspace.fs.readFile(uri) + if (token.isCancellationRequested) return + const fileContents = Buffer.from(buffer).toString('utf8') + let line = 0 + let lineIndex = 0 + for (const match of fileContents.matchAll(topLevelDeclarationRegex)) { + const { 0: name, index: matchIndex = 0 } = match + if (name === '\n') { + line++ + lineIndex = matchIndex + } else if (nameMatchesQuery(name, query)) { + const firstLetter = name.slice(0, 1) + const character = matchIndex - lineIndex - 1 + symbols.push( + new vscode.SymbolInformation( + name, + firstLetter.toUpperCase() === firstLetter ? vscode.SymbolKind.Variable : vscode.SymbolKind.Function, + '', + new vscode.Location( + uri, + new vscode.Range( + new vscode.Position(line, character), + new vscode.Position(line, character + name.length) + ) + ) + ) + ) + } + } + } + + console.info('provideWorkspaceSymbol', `${symbols.length} results in ${Date.now() - start}ms`) + return symbols + } + }) + ) +} + +// Checks that the characters of `query` appear in their order in a candidate symbol, +// as documented here: https://code.visualstudio.com/api/references/vscode-api#WorkspaceSymbolProvider +const nameMatchesQuery = (name: string, query: string): boolean => { + const nameChars = Array.from(name) + let nameIndex = 0 + outer: for (const char of query) { + for (; nameIndex < nameChars.length; nameIndex++) { + if (nameChars[nameIndex] === char) continue outer + } + return false + } + return true +} \ No newline at end of file diff --git a/src/features/shared/elm-to-ast/README.md b/src/features/shared/elm-to-ast/README.md index f4c3d9e..e3eb477 100644 --- a/src/features/shared/elm-to-ast/README.md +++ b/src/features/shared/elm-to-ast/README.md @@ -1,8 +1,6 @@ # Elm to AST > A script that turns raw Elm source code into a structured JSON value -This script is intentionally commited to source control so there's no extra build step needed to run this plugin. - ## Building from scratch 1. Install these NPM terminal commands diff --git a/src/features/shared/elm-to-ast/index.ts b/src/features/shared/elm-to-ast/index.ts index 57ce7af..1cb5300 100644 --- a/src/features/shared/elm-to-ast/index.ts +++ b/src/features/shared/elm-to-ast/index.ts @@ -1,36 +1,64 @@ +import * as path from 'path' +import * as vscode from 'vscode' +import * as WorkerThreads from 'worker_threads' import * as ElmSyntax from './elm-syntax' -type CompiledElmFile = { - Worker: { - init: (args: { flags: string }) => ElmApp - } +type WorkerState = + | { tag: 'Idle' } + | { tag: 'Busy', resolve: (result: ElmSyntax.Ast | undefined) => void, queue: QueueItem[] } + +type QueueItem = { + rawElmSource: string + resolve: (result: ElmSyntax.Ast | undefined) => void, } -type ElmApp = { - ports: { - onSuccess: { subscribe: (fn: (ast: ElmSyntax.Ast) => void) => void } - onFailure: { subscribe: (fn: (reason: string) => void) => void } +let worker: WorkerThreads.Worker +let workerState: WorkerState = { tag: 'Idle' } + +const initWorker = () => { + worker = new WorkerThreads.Worker(path.join(__dirname, './worker.js')) + + const finish = (result: ElmSyntax.Ast | undefined) => { + if (workerState.tag === 'Busy') { + workerState.resolve(result) + const next = workerState.queue.shift() + if (next === undefined) { + workerState = { tag: 'Idle' } + } else { + workerState.resolve = next.resolve + worker.postMessage(next.rawElmSource) + } + } } + + worker.on('exit', () => { + // Most likely, we terminated a busy worker. + initWorker() + finish(undefined) + }) + + worker.on('message', finish) } -export const run = async (rawElmSource: string): Promise => { - // Attempt to load the compiled Elm worker - try { - // @ts-ignore - let Elm: CompiledElmFile = require('./worker.min.js').Elm - return new Promise((resolve) => { - // Start the Elm worker, and subscribe to - const app = Elm.Worker.init({ - flags: rawElmSource || '' - }) - - app.ports.onSuccess.subscribe(resolve) - app.ports.onFailure.subscribe((reason) => { - resolve(undefined) - }) +initWorker() + +export const run = async (rawElmSource: string, token: vscode.CancellationToken): Promise => { + return new Promise((resolve) => { + token.onCancellationRequested(() => { + if (workerState.tag === 'Busy' && workerState.resolve === resolve) { + worker.terminate() + } }) - } catch (_) { - console.error(`ElmToAst`, `Missing "worker.min.js"? Please follow the README section titled "Building from scratch"`) - return undefined - } + + switch (workerState.tag) { + case 'Idle': + workerState = { tag: 'Busy', resolve, queue: [] } + worker.postMessage(rawElmSource) + break + + case 'Busy': + workerState.queue.push({ rawElmSource, resolve }) + break + } + }) } \ No newline at end of file diff --git a/src/features/shared/elm-to-ast/src/Worker.elm b/src/features/shared/elm-to-ast/src/Worker.elm index 5855d95..4fd993d 100644 --- a/src/features/shared/elm-to-ast/src/Worker.elm +++ b/src/features/shared/elm-to-ast/src/Worker.elm @@ -6,28 +6,46 @@ import Json.Encode import Platform +port input : (String -> msg) -> Sub msg + + port onSuccess : Json.Encode.Value -> Cmd msg port onFailure : String -> Cmd msg -main : Program String () () +type alias Model = + () + + +type Msg + = GotInput String + + +main : Program () Model Msg main = Platform.worker - { init = init - , update = \_ model -> ( model, Cmd.none ) - , subscriptions = \_ -> Sub.none + { init = \() -> ( (), Cmd.none ) + , update = update + , subscriptions = subscriptions } -init : String -> ( (), Cmd () ) -init rawElmSource = - ( () - , case Elm.Parser.parse rawElmSource of - Ok rawFile -> - onSuccess (Elm.RawFile.encode rawFile) +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + GotInput rawElmSource -> + ( model + , case Elm.Parser.parse rawElmSource of + Ok rawFile -> + onSuccess (Elm.RawFile.encode rawFile) + + Err deadEnds -> + onFailure "Could not parse Elm file" + ) + - Err deadEnds -> - onFailure "Could not parse Elm file" - ) +subscriptions : Model -> Sub Msg +subscriptions _ = + input GotInput diff --git a/src/features/shared/elm-to-ast/worker.ts b/src/features/shared/elm-to-ast/worker.ts new file mode 100644 index 0000000..29e57d9 --- /dev/null +++ b/src/features/shared/elm-to-ast/worker.ts @@ -0,0 +1,34 @@ +import * as WorkerThreads from 'worker_threads' +import * as ElmSyntax from './elm-syntax' +const Elm: CompiledElmFile = require('./worker.min.js').Elm + +type CompiledElmFile = { + Worker: { + init: () => ElmApp + } +} + +type ElmApp = { + ports: { + input: { send: (rawElmSource: string) => void } + onSuccess: { subscribe: (fn: (ast: ElmSyntax.Ast) => void) => void } + onFailure: { subscribe: (fn: (reason: string) => void) => void } + } +} + +const {parentPort} = WorkerThreads +if (parentPort === null) { + throw new Error('parentPort is null') +} + +const app = Elm.Worker.init() +app.ports.onSuccess.subscribe((ast) => { + parentPort.postMessage(ast) +}) +app.ports.onFailure.subscribe((reason) => { + parentPort.postMessage(undefined) +}) + +parentPort.on('message', (rawElmSource: string) => { + app.ports.input.send(rawElmSource) +}) \ No newline at end of file diff --git a/src/features/type-driven-autocomplete.ts b/src/features/type-driven-autocomplete.ts index 7e80567..124c4b9 100644 --- a/src/features/type-driven-autocomplete.ts +++ b/src/features/type-driven-autocomplete.ts @@ -82,7 +82,7 @@ export const feature: Feature = ({ globalState, context }) => { let elmJsonFile = SharedLogic.findElmJsonFor(globalState, document.uri) if (elmJsonFile) { - let moduleDoc = await findLocalElmModuleDoc({ elmJsonFile, moduleName }) + let moduleDoc = await findLocalElmModuleDoc({ elmJsonFile, moduleName, token }) if (moduleDoc) { console.info(`autocomplete`, `${Date.now() - start}ms`) return toCompletionItems(moduleDoc, allModuleNames) @@ -108,7 +108,7 @@ export const feature: Feature = ({ globalState, context }) => { // SCANNING LOCAL PROJECT FILES const findLocalElmModuleDoc = - async ({ elmJsonFile, moduleName }: { elmJsonFile: ElmJsonFile, moduleName: string }): Promise => { + async ({ elmJsonFile, moduleName, token }: { elmJsonFile: ElmJsonFile, moduleName: string, token: vscode.CancellationToken }): Promise => { let matchingFilepaths = await SharedLogic.keepFilesThatExist(elmJsonFile.sourceDirectories .map(folder => path.join(folder, ...moduleName.split('.')) + '.elm')) @@ -119,7 +119,7 @@ const findLocalElmModuleDoc = let uri = vscode.Uri.file(matchingFilepath) let document = await vscode.workspace.openTextDocument(uri) if (document) { - let ast = await ElmToAst.run(document.getText()) + let ast = await ElmToAst.run(document.getText(), token) if (ast) { return toModuleDoc(ast) }