diff --git a/.gitignore b/.gitignore index a53d198..35c79c5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ yarn.lock package-lock.json -.vscode \ No newline at end of file +test.whi + +out +.vscode-test \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..71e8d9a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "extensionHost", + "request": "launch", + "name": "Launch Client", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}" + ], + "outFiles": [ + "${workspaceRoot}/client/out/**/*.js" + ], + "preLaunchTask": { + "type": "npm", + "script": "watch" + }, + } + ], + "compounds": [ + { + "name": "Client + Server", + "configurations": [ + "Launch Client", + ] + } + ] + } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f129682 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "compile", + "group": "build", + "presentation": { + "panel": "dedicated", + "reveal": "never" + }, + "problemMatcher": [ + "$tsc" + ] + }, + { + "type": "npm", + "script": "watch", + "isBackground": true, + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "panel": "dedicated", + "reveal": "never" + }, + "problemMatcher": [ + "$tsc-watch" + ] + } + ] +} diff --git a/.vscodeignore b/.vscodeignore index f369b5e..c9506bb 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -2,3 +2,4 @@ .vscode-test/** .gitignore vsc-extension-quickstart.md +src \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6c6c57 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2022 the Whistle authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 481e88d..cadd989 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# whistle-lang vscode support \ No newline at end of file +# whistle-lang vscode support diff --git a/icons/icon_dark.svg b/icons/icon_dark.svg new file mode 100644 index 0000000..42a9968 --- /dev/null +++ b/icons/icon_dark.svg @@ -0,0 +1,44 @@ + + diff --git a/icons/icon_light.svg b/icons/icon_light.svg new file mode 100644 index 0000000..99eba95 --- /dev/null +++ b/icons/icon_light.svg @@ -0,0 +1,48 @@ + + diff --git a/package.json b/package.json index 0fc835b..8f8841f 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,11 @@ "author": "Whistle Team", "publisher": "whistle", "icon": "icons/icon128.png", + "license": "MIT", "description": "Support for the whistle language", - "version": "1.7.0", + "version": "1.8.4", "engines": { - "vscode": "^1.47.0" + "vscode": "1.82.0" }, "repository": { "type": "git", @@ -26,31 +27,90 @@ "languages": [ { "id": "whistle", + "icon": { + "light": "icons/icon_light.svg", + "dark": "icons/icon_dark.svg" + }, "aliases": [ "Whistle", "whistle" ], "extensions": [ - "whi" + ".whi" ], "configuration": "./language-configuration.json" } ], + "configuration": { + "type": "object", + "title": "whistle", + "properties": { + "whistle.trace.server": { + "type": "string", + "scope": "window", + "enum": [ + "off", + "messages", + "verbose" + ], + "enumDescriptions": [ + "No traces", + "Error only", + "Full log" + ], + "default": "off", + "description": "Traces the communication between VS Code and the language server." + }, + "whistle.whistlePath": { + "type": "string", + "default": "whistle", + "markdownDescription": "A path to the `whistle` executable. The extension looks for `whistle` in the `PATH` By default", + "scope": "window" + } + } + }, "grammars": [ { "language": "whistle", "scopeName": "source.whistle", "path": "./syntaxes/whistle.tmLanguage.json" } + ], + "commands": [ + { + "command": "whistle.restartServer", + "title": "Restart Whistle Server", + "category": "whistle" + } ] }, + "activationEvents": [ + "onDebugInitialConfigurations" + ], + "enabledApiProposals": [], + "main": "./out/extension", "devDependencies": { - "js-yaml": "^3.14.0", - "vsce": "^1.87.1" + "@types/mocha": "10.0.1", + "@types/node": "20.6.2", + "@types/semver": "7.5.2", + "@types/vscode": "1.82.0", + "js-yaml": "4.1.0", + "vsce": "2.15.0", + "vscode": "1.1.37", + "vscode-test": "1.6.1" + }, + "dependencies": { + "esbuild": "0.19.3", + "lodash-es": "4.17.21", + "lodash.debounce": "4.0.8", + "vscode-languageclient": "9.0.0" }, "scripts": { + "compile": "esbuild --bundle --sourcemap=external --minify --external:vscode src/extension.ts --outdir=out --platform=node --format=cjs", "build": "js-yaml syntaxes/whistle.tmLanguage.yml > syntaxes/whistle.tmLanguage.json && js-yaml snippets/whistle.yml > snippets/whistle.json", "package": "vsce package", - "package:npm": "vsce package --no-yarn" + "package:npm": "vsce package --no-yarn", + "test": "npm run compile && npm run package:npm && node ./node_modules/vscode/bin/test", + "all": "npm run compile && npm run build && npm run package:npm" } } diff --git a/snippets/whistle.yml b/snippets/whistle.yml index 0190c27..c56bd97 100644 --- a/snippets/whistle.yml +++ b/snippets/whistle.yml @@ -1,21 +1,9 @@ --- -Import external module: - prefix: import statement +Import module: + prefix: import body: - - import { $0 } from "${1:module}" - description: Import external module. -Print string to the console: - prefix: pstr - body: - - printString($1) - - "$0" - description: Print string to the console -Print int to the console: - prefix: pint - body: - - printInt($1) - - "$0" - description: Print int to the console + - import from "${0:module}" + description: Import a whistle file. Function Statement: prefix: fn body: @@ -23,6 +11,13 @@ Function Statement: - "\t$TM_SELECTED_TEXT$0" - "}" description: Function Statement +Extern module: + prefix: extern + body: + - extern "${0:namespace}" { + - "\tfn ${1:name}(${2:params}:${3:type}):${4:return_type}," + - "}" + description: Import an external function If Statement: prefix: if body: diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..aef31f5 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,187 @@ +// deno-lint-ignore-file require-await no-unused-vars +import { + CancellationToken, + commands, + EventEmitter, + ExtensionContext, + InlayHint, + InlayHintsProvider, + languages, + ProviderResult, + Range, + Selection, + TextDocument, + TextDocumentChangeEvent, + TextEdit, + Uri, + window, + workspace, + WorkspaceEdit, +} from "vscode"; + +import { + Disposable, + Executable, + LanguageClient, + LanguageClientOptions, + ServerOptions, +} from "vscode-languageclient/node"; + +let client: LanguageClient; + +function createLanguageClient() { + const command = workspace.getConfiguration("whistle").get("whistlePath"); + console.log(command); + const run: Executable = { + command, + args: ["lsp"], + options: { + env: { + ...process.env, + RUST_LOG: "debug", + }, + }, + }; + + const serverOptions: ServerOptions = { + run, + debug: run, + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: "file", language: "whistle" }], + synchronize: { + fileEvents: workspace.createFileSystemWatcher("**/.clientrc"), + }, + }; + + return new LanguageClient( + "whistle-language-server", + "whistle language server", + serverOptions, + clientOptions, + ); +} + +export async function activate(context: ExtensionContext) { + const restartCommand = commands.registerCommand( + "whistle.restartServer", + async () => { + if (!client) { + window.showErrorMessage("whistle client not found"); + return; + } + + try { + if (client.isRunning()) { + await client.restart(); + window.showInformationMessage("whistle server restarted."); + } else { + await client.start(); + } + } catch (err) { + client.error("Restarting client failed", err, "force"); + } + }, + ); + + context.subscriptions.push(restartCommand); + + client = createLanguageClient(); + activateInlayHints(context); + client.start(); +} + +export function deactivate() { + return client?.stop(); +} + +export function activateInlayHints(ctx: ExtensionContext) { + const maybeUpdater = { + hintsProvider: null as Disposable | null, + updateHintsEventEmitter: new EventEmitter(), + + async onConfigChange() { + this.dispose(); + + const event = this.updateHintsEventEmitter.event; + this.hintsProvider = languages.registerInlayHintsProvider( + { scheme: "file", language: "whistle" }, + new (class implements InlayHintsProvider { + onDidChangeInlayHints = event; + resolveInlayHint( + hint: InlayHint, + _token: CancellationToken, + ): ProviderResult { + const ret = { + label: hint.label, + ...hint, + }; + return ret; + } + + async provideInlayHints( + document: TextDocument, + _range: Range, + _token: CancellationToken, + ): Promise { + const hints = (await client + .sendRequest("custom/inlay_hint", { + path: document.uri.toString(), + }) + .catch((_err: unknown) => null)) as [number, number, string][]; + if (hints == null) { + return []; + } else { + return hints.map((item) => { + const [start, end, label] = item; + const _startPosition = document.positionAt(start); + const endPosition = document.positionAt(end); + return { + position: endPosition, + paddingLeft: true, + label: [ + { + value: `${label}`, + command: { + title: "hello world", + command: "helloworld.helloWorld", + arguments: [document.uri], + }, + }, + ], + }; + }); + } + } + })(), + ); + }, + + onDidChangeTextDocument( + { contentChanges, document }: TextDocumentChangeEvent, + ) { + // this.updateHintsEventEmitter.fire(); + }, + + dispose() { + this.hintsProvider?.dispose(); + this.hintsProvider = null; + this.updateHintsEventEmitter.dispose(); + }, + }; + + workspace.onDidChangeConfiguration( + maybeUpdater.onConfigChange, + maybeUpdater, + ctx.subscriptions, + ); + + workspace.onDidChangeTextDocument( + maybeUpdater.onDidChangeTextDocument, + maybeUpdater, + ctx.subscriptions, + ); + + maybeUpdater.onConfigChange().catch(console.error); +} diff --git a/syntaxes/whistle.tmLanguage.yml b/syntaxes/whistle.tmLanguage.yml index 3634955..a33c49d 100644 --- a/syntaxes/whistle.tmLanguage.yml +++ b/syntaxes/whistle.tmLanguage.yml @@ -173,7 +173,7 @@ repository: name: constant.other.caps.whistle match: "\\b[A-Z]{2}[A-Z0-9_]*\\b" - comment: constant declarations - match: "\\b(const)\\s+([A-Z][A-Za-z0-9_]*)\\b" + match: "\\b(val)\\s+([A-Z][A-Za-z0-9_]*)\\b" captures: '1': name: storage.type.whistle @@ -181,7 +181,7 @@ repository: name: constant.other.caps.whistle - comment: decimal integers and floats name: constant.numeric.decimal.whistle - match: "\\b\\d[\\d_]*(\\.?)[\\d_]*(?:(E)([+-])([\\d_]+))?(f32|f64|i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b" + match: "\\b\\d[\\d_]*(\\.?)[\\d_]*(?:(E)([+-])([\\d_]+))?(f32|f64|i8|i16|i32|i64|u8|u16|u32|u64|number|int)?\\b" captures: '1': name: punctuation.separator.dot.decimal.whistle @@ -195,19 +195,19 @@ repository: name: entity.name.type.numeric.whistle - comment: hexadecimal integers name: constant.numeric.hex.whistle - match: "\\b0x[\\da-fA-F_]+(i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b" + match: "\\b0x[\\da-fA-F_]+(i8|i16|i32|i64|u8|u16|u32|u64|number)?\\b" captures: '1': name: entity.name.type.numeric.whistle - comment: octal integers name: constant.numeric.oct.whistle - match: "\\b0o[0-7_]+(i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b" + match: "\\b0o[0-7_]+(i8|i16|i32|i64|u8|u16|u32|u64|number|int)?\\b" captures: '1': name: entity.name.type.numeric.whistle - comment: binary integers name: constant.numeric.bin.whistle - match: "\\b0b[01_]+(i128|i16|i32|i64|i8|isize|u128|u16|u32|u64|u8|usize)?\\b" + match: "\\b0b[01_]+(i8|i16|i32|i64|u8|u16|u32|u64|number|int)?\\b" captures: '1': name: entity.name.type.numeric.whistle @@ -328,10 +328,10 @@ repository: match: "\\b(await|break|continue|do|else|for|if|loop|match|return|try|while|yield)\\b" - comment: storage keywords name: keyword.other.whistle storage.type.whistle - match: "\\b(extern|let|var|val|macro|mod)\\b" + match: "\\b(extern|var|val)\\b" - comment: const keyword name: storage.modifier.whistle - match: "\\b(const)\\b" + match: "\\b(val)\\b" - comment: type keyword name: keyword.declaration.type.whistle storage.type.whistle match: "\\b(type)\\b" @@ -349,7 +349,7 @@ repository: match: "\\b(abstract|static)\\b" - comment: other keywords name: keyword.other.whistle - match: "\\b(as|async|import|export|from|become|box|dyn|move|final|impl|in|override|private|public|ref|typeof|union|unsafe|unsized|use|virtual|where)\\b" + match: "\\b(as|inline|async|import|export|extern|from|become|box|dyn|move|final|impl|in|override|private|public|ref|typeof|union|unsafe|unsized|virtual|where)\\b" - comment: fun name: keyword.other.fun.whistle match: "\\bfn\\b" @@ -475,7 +475,7 @@ repository: types: patterns: - comment: numeric types - match: "(?