Merge pull request 'I have extended vscode with an extension' (#23) from vscode into main
Reviewed-on: #23
This commit is contained in:
commit
ea01a93563
4
vscode-extension/.gitignore
vendored
Normal file
4
vscode-extension/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
client/dist
|
||||||
|
server/dist
|
||||||
|
*.vsix
|
||||||
19
vscode-extension/.vscode/launch.json
vendored
Normal file
19
vscode-extension/.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run Extension",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": [
|
||||||
|
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||||
|
"--profile=Shrimp Dev"
|
||||||
|
],
|
||||||
|
"outFiles": [
|
||||||
|
"${workspaceFolder}/client/dist/**/*.js",
|
||||||
|
"${workspaceFolder}/server/dist/**/*.js"
|
||||||
|
],
|
||||||
|
"preLaunchTask": "bun: compile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
vscode-extension/.vscode/tasks.json
vendored
Normal file
18
vscode-extension/.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "shell",
|
||||||
|
"label": "bun: compile",
|
||||||
|
"command": "bun",
|
||||||
|
"args": ["run", "compile"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": "$tsc",
|
||||||
|
"group": {
|
||||||
|
"kind": "build",
|
||||||
|
"isDefault": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
vscode-extension/.vscodeignore
Normal file
5
vscode-extension/.vscodeignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.vscode/**
|
||||||
|
src/**
|
||||||
|
tsconfig.json
|
||||||
|
node_modules/**
|
||||||
|
*.map
|
||||||
49
vscode-extension/README.md
Normal file
49
vscode-extension/README.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Shrimp VSCode Extension
|
||||||
|
|
||||||
|
Language support for Shrimp in VSCode. This README is for probablycorey and defunkt.
|
||||||
|
|
||||||
|
**What it provides:**
|
||||||
|
|
||||||
|
- Syntax highlighting and semantic tokens
|
||||||
|
- Language server with error diagnostics
|
||||||
|
- Commands: "Show Parse Tree" (Alt+K Alt+I), "Show Bytecode" (Alt+K Alt+,), and "Run File" (Cmd+R)
|
||||||
|
- `.sh` file association
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
**Developing the extension:**
|
||||||
|
|
||||||
|
1. Open `vscode-extension/` in VSCode
|
||||||
|
2. Run `bun run watch` in a terminal (keeps it compiling as you make changes)
|
||||||
|
3. Use **Run > Start Debugging** to launch Extension Development Host
|
||||||
|
4. Make changes to the code
|
||||||
|
5. Press **Cmd+R** (or Ctrl+R) in the Extension Development Host window to reload
|
||||||
|
6. Repeat steps 4-5
|
||||||
|
|
||||||
|
The `.vscode/launch.json` is configured to compile before launching and use a separate "Shrimp Dev" profile. This means you can have the extension installed in your main VSCode while developing without conflicts.
|
||||||
|
|
||||||
|
**Installing for daily use:**
|
||||||
|
|
||||||
|
Run `bun run build-and-install` to build a VSIX and install it in your current VSCode profile. This lets you use the extension when working on Shrimp scripts outside of development mode.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
The extension has two parts: a **client** (`client/src/extension.ts`) that registers commands and starts the language server, and a **server** (`server/src/`) that implements the Language Server Protocol for diagnostics and semantic highlighting.
|
||||||
|
|
||||||
|
Both compile to their respective `dist/` folders.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**Autocomplete:**
|
||||||
|
|
||||||
|
- [ ] Identifiers in scope
|
||||||
|
- [ ] Globals from the prelude (including native functions)
|
||||||
|
- [ ] Imports
|
||||||
|
- [ ] Dot-get properties
|
||||||
|
- [ ] Function argument completion
|
||||||
|
|
||||||
|
**Other features:**
|
||||||
|
|
||||||
|
- [ ] Better syntax coloring
|
||||||
|
- [ ] REPL integration
|
||||||
|
- [ ] Bundle shrimp binary with extension (currently uses `shrimp.binaryPath` setting)
|
||||||
47
vscode-extension/bun.lock
Normal file
47
vscode-extension/bun.lock
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "shrimp",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^9.0.1",
|
||||||
|
"vscode-languageserver": "^9.0.1",
|
||||||
|
"vscode-languageserver-textdocument": "^1.0.12",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "22.x",
|
||||||
|
"@types/vscode": "^1.105.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/node": ["@types/node@22.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA=="],
|
||||||
|
|
||||||
|
"@types/vscode": ["@types/vscode@1.105.0", "", {}, "sha512-Lotk3CTFlGZN8ray4VxJE7axIyLZZETQJVWi/lYoUVQuqfRxlQhVOfoejsD2V3dVXPSbS15ov5ZyowMAzgUqcw=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
|
||||||
|
|
||||||
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||||
|
|
||||||
|
"vscode-languageclient": ["vscode-languageclient@9.0.1", "", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.5" } }, "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
100
vscode-extension/client/src/extension.ts
Normal file
100
vscode-extension/client/src/extension.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import {
|
||||||
|
LanguageClient,
|
||||||
|
LanguageClientOptions,
|
||||||
|
ServerOptions,
|
||||||
|
TransportKind,
|
||||||
|
} from 'vscode-languageclient/node'
|
||||||
|
import * as vscode from 'vscode'
|
||||||
|
|
||||||
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
const serverModule = context.asAbsolutePath('server/dist/server.js')
|
||||||
|
|
||||||
|
const serverOptions: ServerOptions = {
|
||||||
|
run: { module: serverModule, transport: TransportKind.ipc },
|
||||||
|
debug: { module: serverModule, transport: TransportKind.ipc },
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientOptions: LanguageClientOptions = {
|
||||||
|
documentSelector: [{ scheme: 'file', language: 'shrimp' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new LanguageClient(
|
||||||
|
'shrimpLanguageServer',
|
||||||
|
'Shrimp Language Server',
|
||||||
|
serverOptions,
|
||||||
|
clientOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
client.start()
|
||||||
|
context.subscriptions.push(client)
|
||||||
|
|
||||||
|
// Command: Show Parse Tree
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('shrimp.showParseTree', async () => {
|
||||||
|
const editor = vscode.window.activeTextEditor
|
||||||
|
if (!editor || editor.document.languageId !== 'shrimp') {
|
||||||
|
vscode.window.showErrorMessage('No active Shrimp file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.sendRequest<string>('shrimp/parseTree', {
|
||||||
|
uri: editor.document.uri.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const doc = await vscode.workspace.openTextDocument({
|
||||||
|
content: result,
|
||||||
|
language: 'text',
|
||||||
|
})
|
||||||
|
await vscode.window.showTextDocument(doc, { preview: false })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command: Show Bytecode
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('shrimp.showBytecode', async () => {
|
||||||
|
const editor = vscode.window.activeTextEditor
|
||||||
|
if (!editor || editor.document.languageId !== 'shrimp') {
|
||||||
|
vscode.window.showErrorMessage('No active Shrimp file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await client.sendRequest<string>('shrimp/bytecode', {
|
||||||
|
uri: editor.document.uri.toString(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const doc = await vscode.workspace.openTextDocument({
|
||||||
|
content: result,
|
||||||
|
language: 'text',
|
||||||
|
})
|
||||||
|
await vscode.window.showTextDocument(doc, { preview: false })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Command: Run File
|
||||||
|
context.subscriptions.push(
|
||||||
|
vscode.commands.registerCommand('shrimp.run', async () => {
|
||||||
|
const editor = vscode.window.activeTextEditor
|
||||||
|
if (!editor || editor.document.languageId !== 'shrimp') {
|
||||||
|
vscode.window.showErrorMessage('No active Shrimp file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save before running
|
||||||
|
await editor.document.save()
|
||||||
|
|
||||||
|
// Get binary path from settings
|
||||||
|
const config = vscode.workspace.getConfiguration('shrimp')
|
||||||
|
const binaryPath = config.get<string>('binaryPath', 'shrimp')
|
||||||
|
|
||||||
|
// Get the file path
|
||||||
|
const filePath = editor.document.uri.fsPath
|
||||||
|
|
||||||
|
// Create or show terminal
|
||||||
|
const terminal = vscode.window.createTerminal('Shrimp')
|
||||||
|
terminal.show()
|
||||||
|
terminal.sendText(`${binaryPath} "${filePath}"`)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deactivate() {}
|
||||||
25
vscode-extension/example.sh
Normal file
25
vscode-extension/example.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# This just has some stuff I use to make sure the extension is working!
|
||||||
|
|
||||||
|
like-a-function = do x y z:
|
||||||
|
echo 'This is a function with parameters: $x, $y, $z'
|
||||||
|
end
|
||||||
|
|
||||||
|
value = if true:
|
||||||
|
'This is true!'
|
||||||
|
else:
|
||||||
|
'This is false!'
|
||||||
|
end
|
||||||
|
|
||||||
|
echo 'value is $(value)'
|
||||||
|
|
||||||
|
html lang=en do:
|
||||||
|
head do:
|
||||||
|
meta charset='UTF-8'
|
||||||
|
meta name='viewport' content='width=device-width, initial-scale=1.0'
|
||||||
|
end
|
||||||
|
|
||||||
|
body do:
|
||||||
|
h1 'Hello, World!'
|
||||||
|
p 'This is a sample HTML generated by the extension.'
|
||||||
|
end
|
||||||
|
end
|
||||||
BIN
vscode-extension/icon.png
Normal file
BIN
vscode-extension/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
28
vscode-extension/language-configuration.json
Normal file
28
vscode-extension/language-configuration.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"comments": {
|
||||||
|
"lineComment": {
|
||||||
|
"comment": "#"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"brackets": [
|
||||||
|
["(", ")"],
|
||||||
|
["[", "]"]
|
||||||
|
],
|
||||||
|
"autoClosingPairs": [
|
||||||
|
{ "open": "(", "close": ")" },
|
||||||
|
{ "open": "[", "close": "]" },
|
||||||
|
{ "open": "'", "close": "'", "notIn": ["string"] },
|
||||||
|
{ "open": "\"", "close": "\"", "notIn": ["string"] }
|
||||||
|
],
|
||||||
|
"surroundingPairs": [
|
||||||
|
["(", ")"],
|
||||||
|
["[", "]"],
|
||||||
|
["'", "'"],
|
||||||
|
["\"", "\""]
|
||||||
|
],
|
||||||
|
"wordPattern": "([a-z][a-z0-9-]*)|(-?\\d+\\.?\\d*)",
|
||||||
|
"indentationRules": {
|
||||||
|
"increaseIndentPattern": ":\\s*$",
|
||||||
|
"decreaseIndentPattern": "^\\s*(end|else)\\b"
|
||||||
|
}
|
||||||
|
}
|
||||||
96
vscode-extension/package.json
Normal file
96
vscode-extension/package.json
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"name": "shrimp",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "./client/dist/extension.js",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/vscode": "^1.105.0",
|
||||||
|
"@types/node": "22.x",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"categories": [
|
||||||
|
"Programming Languages"
|
||||||
|
],
|
||||||
|
"contributes": {
|
||||||
|
"languages": [
|
||||||
|
{
|
||||||
|
"id": "shrimp",
|
||||||
|
"aliases": [
|
||||||
|
"Shrimp",
|
||||||
|
"shrimp"
|
||||||
|
],
|
||||||
|
"extensions": [
|
||||||
|
".sh"
|
||||||
|
],
|
||||||
|
"configuration": "./language-configuration.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configurationDefaults": {
|
||||||
|
"[shrimp]": {
|
||||||
|
"editor.semanticHighlighting.enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"title": "Shrimp",
|
||||||
|
"properties": {
|
||||||
|
"shrimp.binaryPath": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "shrimp",
|
||||||
|
"description": "Path to the shrimp binary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "shrimp.showParseTree",
|
||||||
|
"title": "Shrimp: Show Parse Tree"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shrimp.showBytecode",
|
||||||
|
"title": "Shrimp: Show Bytecode"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shrimp.run",
|
||||||
|
"title": "Shrimp: Run File"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"keybindings": [
|
||||||
|
{
|
||||||
|
"command": "shrimp.showParseTree",
|
||||||
|
"key": "alt+k alt+i",
|
||||||
|
"when": "editorLangId == shrimp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shrimp.showBytecode",
|
||||||
|
"key": "alt+k alt+,",
|
||||||
|
"when": "editorLangId == shrimp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "shrimp.run",
|
||||||
|
"key": "cmd+r",
|
||||||
|
"when": "editorLangId == shrimp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Language support for Shrimp shell scripting language",
|
||||||
|
"displayName": "Shrimp",
|
||||||
|
"engines": {
|
||||||
|
"vscode": "^1.105.0"
|
||||||
|
},
|
||||||
|
"icon": "icon.png",
|
||||||
|
"publisher": "shrimp-lang",
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": "bun run package",
|
||||||
|
"compile": "bun run compile:client && bun run compile:server",
|
||||||
|
"compile:client": "bun build client/src/extension.ts --outdir client/dist --target node --format cjs --external vscode",
|
||||||
|
"compile:server": "bun build server/src/server.ts --outdir server/dist --target node --format cjs",
|
||||||
|
"watch": "bun run compile:client --watch & bun run compile:server --watch",
|
||||||
|
"package": "bun run compile:client --minify && bun run compile:server --minify",
|
||||||
|
"check-types": "tsc --noEmit",
|
||||||
|
"build-and-install": "bun run package && bunx @vscode/vsce package --allow-missing-repository && code --install-extension shrimp-*.vsix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-languageclient": "^9.0.1",
|
||||||
|
"vscode-languageserver": "^9.0.1",
|
||||||
|
"vscode-languageserver-textdocument": "^1.0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
93
vscode-extension/server/src/diagnostics.ts
Normal file
93
vscode-extension/server/src/diagnostics.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { TextDocument, Position } from 'vscode-languageserver-textdocument'
|
||||||
|
import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver/node'
|
||||||
|
import { parser } from '../../../src/parser/shrimp'
|
||||||
|
import { Compiler } from '../../../src/compiler/compiler'
|
||||||
|
import { CompilerError } from '../../../src/compiler/compilerError'
|
||||||
|
|
||||||
|
export const buildDiagnostics = (textDocument: TextDocument): Diagnostic[] => {
|
||||||
|
const text = textDocument.getText()
|
||||||
|
const diagnostics = getParseErrors(textDocument)
|
||||||
|
|
||||||
|
if (diagnostics.length > 0) {
|
||||||
|
return diagnostics
|
||||||
|
}
|
||||||
|
const diagnostic = getCompilerError(text)
|
||||||
|
if (diagnostic) return [diagnostic]
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCompilerError = (text: string): Diagnostic | undefined => {
|
||||||
|
try {
|
||||||
|
new Compiler(text)
|
||||||
|
} catch (e) {
|
||||||
|
if (!(e instanceof CompilerError)) {
|
||||||
|
return unknownDiagnostic(getErrorMessage(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
const lineInfo = e.lineAtPosition(text)!
|
||||||
|
const cause = e.cause ? ` Cause: ${e.cause}` : ''
|
||||||
|
const message = e.message
|
||||||
|
|
||||||
|
if (!lineInfo) {
|
||||||
|
return unknownDiagnostic(message + cause)
|
||||||
|
}
|
||||||
|
|
||||||
|
const diagnostic: Diagnostic = {
|
||||||
|
severity: DiagnosticSeverity.Error,
|
||||||
|
range: {
|
||||||
|
start: { line: lineInfo.lineNumber, character: lineInfo.columnStart },
|
||||||
|
end: { line: lineInfo.lineNumber, character: lineInfo.columnEnd },
|
||||||
|
},
|
||||||
|
message: `Compiler error: ${message}${cause}`,
|
||||||
|
source: 'shrimp',
|
||||||
|
}
|
||||||
|
return diagnostic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unknownDiagnostic = (message: string): Diagnostic => {
|
||||||
|
const diagnostic: Diagnostic = {
|
||||||
|
severity: DiagnosticSeverity.Error,
|
||||||
|
range: {
|
||||||
|
start: { line: 0, character: 0 },
|
||||||
|
end: { line: -1, character: -1 },
|
||||||
|
},
|
||||||
|
message,
|
||||||
|
source: 'shrimp',
|
||||||
|
}
|
||||||
|
return diagnostic
|
||||||
|
}
|
||||||
|
|
||||||
|
const getParseErrors = (textDocument: TextDocument): Diagnostic[] => {
|
||||||
|
const tree = parser.parse(textDocument.getText())
|
||||||
|
|
||||||
|
const ranges: { start: Position; end: Position }[] = []
|
||||||
|
tree.iterate({
|
||||||
|
enter(n) {
|
||||||
|
if (n.type.isError) {
|
||||||
|
ranges.push({
|
||||||
|
start: textDocument.positionAt(n.from),
|
||||||
|
end: textDocument.positionAt(n.to),
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return ranges.map((range) => {
|
||||||
|
return {
|
||||||
|
range,
|
||||||
|
severity: DiagnosticSeverity.Error,
|
||||||
|
message: 'Parse error: Invalid syntax',
|
||||||
|
source: 'shrimp',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown): string => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
return String(error)
|
||||||
|
}
|
||||||
145
vscode-extension/server/src/scopeTracker.test.ts
Normal file
145
vscode-extension/server/src/scopeTracker.test.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
import { test, expect, describe } from 'bun:test'
|
||||||
|
import { ScopeTracker } from './scopeTracker'
|
||||||
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
|
import { parser } from '../../../src/parser/shrimp'
|
||||||
|
import * as Terms from '../../../src/parser/shrimp.terms'
|
||||||
|
|
||||||
|
describe('ScopeTracker', () => {
|
||||||
|
test('top-level assignment is in scope', () => {
|
||||||
|
const code = 'x = 5\necho x'
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find the 'x' identifier in 'echo x'
|
||||||
|
const identifiers: any[] = []
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier) {
|
||||||
|
identifiers.push(node.node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Second identifier should be the 'x' in 'echo x'
|
||||||
|
const xInEcho = identifiers[1]
|
||||||
|
expect(xInEcho).toBeDefined()
|
||||||
|
expect(tracker.isInScope('x', xInEcho)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('undeclared variable is not in scope', () => {
|
||||||
|
const code = 'echo x'
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find the 'x' identifier
|
||||||
|
let xNode: any = null
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier) {
|
||||||
|
xNode = node.node
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(xNode).toBeDefined()
|
||||||
|
expect(tracker.isInScope('x', xNode)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('function parameter is in scope inside function', () => {
|
||||||
|
const code = `greet = do name:
|
||||||
|
echo name
|
||||||
|
end`
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find all identifiers
|
||||||
|
const identifiers: any[] = []
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier) {
|
||||||
|
identifiers.push(node.node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find the 'name' in 'echo name' (should be last identifier)
|
||||||
|
const nameInEcho = identifiers[identifiers.length - 1]
|
||||||
|
expect(tracker.isInScope('name', nameInEcho)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assignment before usage is in scope', () => {
|
||||||
|
const code = `x = 5
|
||||||
|
y = 10
|
||||||
|
echo x y`
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find identifiers
|
||||||
|
const identifiers: any[] = []
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier) {
|
||||||
|
identifiers.push(node.node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Last two identifiers should be 'x' and 'y' in 'echo x y'
|
||||||
|
const xInEcho = identifiers[identifiers.length - 2]
|
||||||
|
const yInEcho = identifiers[identifiers.length - 1]
|
||||||
|
|
||||||
|
expect(tracker.isInScope('x', xInEcho)).toBe(true)
|
||||||
|
expect(tracker.isInScope('y', yInEcho)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('assignment after usage is not in scope', () => {
|
||||||
|
const code = `echo x
|
||||||
|
x = 5`
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find the first 'x' identifier (in echo)
|
||||||
|
let xNode: any = null
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier && !xNode) {
|
||||||
|
xNode = node.node
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(tracker.isInScope('x', xNode)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested function has access to outer scope', () => {
|
||||||
|
const code = `x = 5
|
||||||
|
greet = do:
|
||||||
|
echo x
|
||||||
|
end`
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find all identifiers
|
||||||
|
const identifiers: any[] = []
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier) {
|
||||||
|
identifiers.push(node.node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find the 'x' in 'echo x' (should be last identifier)
|
||||||
|
const xInEcho = identifiers[identifiers.length - 1]
|
||||||
|
expect(tracker.isInScope('x', xInEcho)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('inner function parameter shadows outer variable', () => {
|
||||||
|
const code = `x = 5
|
||||||
|
greet = do x:
|
||||||
|
echo x
|
||||||
|
end`
|
||||||
|
const { tree, tracker } = parseAndGetScope(code)
|
||||||
|
|
||||||
|
// Find all identifiers
|
||||||
|
const identifiers: any[] = []
|
||||||
|
tree.topNode.cursor().iterate((node: any) => {
|
||||||
|
if (node.type.id === Terms.Identifier) {
|
||||||
|
identifiers.push(node.node)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// The 'x' in 'echo x' should have 'x' in scope (from parameter)
|
||||||
|
const xInEcho = identifiers[identifiers.length - 1]
|
||||||
|
expect(tracker.isInScope('x', xInEcho)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const parseAndGetScope = (code: string) => {
|
||||||
|
const document = TextDocument.create('test://test.sh', 'shrimp', 1, code)
|
||||||
|
const tree = parser.parse(code)
|
||||||
|
const tracker = new ScopeTracker(document)
|
||||||
|
return { document, tree, tracker }
|
||||||
|
}
|
||||||
135
vscode-extension/server/src/scopeTracker.ts
Normal file
135
vscode-extension/server/src/scopeTracker.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { SyntaxNode } from '@lezer/common'
|
||||||
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
|
import * as Terms from '../../../src/parser/shrimp.terms'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks variables in scope at a given position in the parse tree.
|
||||||
|
* Used to distinguish identifiers (in scope) from words (not in scope).
|
||||||
|
*/
|
||||||
|
export class ScopeTracker {
|
||||||
|
private document: TextDocument
|
||||||
|
private scopeCache = new Map<number, Set<string>>()
|
||||||
|
|
||||||
|
constructor(document: TextDocument) {
|
||||||
|
this.document = document
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a name is in scope at the given node's position.
|
||||||
|
*/
|
||||||
|
isInScope(name: string, node: SyntaxNode): boolean {
|
||||||
|
const scope = this.getScopeAt(node)
|
||||||
|
return scope.has(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all variables in scope at the given node's position.
|
||||||
|
*/
|
||||||
|
private getScopeAt(node: SyntaxNode): Set<string> {
|
||||||
|
const position = node.from
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.scopeCache.has(position)) {
|
||||||
|
return this.scopeCache.get(position)!
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = new Set<string>()
|
||||||
|
|
||||||
|
// Find all containing function definitions
|
||||||
|
const containingFunctions = this.findContainingFunctions(node)
|
||||||
|
|
||||||
|
// Collect scope from each containing function (inner to outer)
|
||||||
|
for (const fnNode of containingFunctions) {
|
||||||
|
this.collectParams(fnNode, scope)
|
||||||
|
this.collectAssignments(fnNode, position, scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect top-level assignments
|
||||||
|
const root = this.getRoot(node)
|
||||||
|
this.collectAssignments(root, position, scope)
|
||||||
|
|
||||||
|
this.scopeCache.set(position, scope)
|
||||||
|
return scope
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all function definitions that contain the given node.
|
||||||
|
*/
|
||||||
|
private findContainingFunctions(node: SyntaxNode): SyntaxNode[] {
|
||||||
|
const functions: SyntaxNode[] = []
|
||||||
|
let current = node.parent
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
if (current.type.id === Terms.FunctionDef) {
|
||||||
|
functions.unshift(current) // Add to beginning for outer-to-inner order
|
||||||
|
}
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return functions
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the root node of the tree.
|
||||||
|
*/
|
||||||
|
private getRoot(node: SyntaxNode): SyntaxNode {
|
||||||
|
let current = node
|
||||||
|
while (current.parent) {
|
||||||
|
current = current.parent
|
||||||
|
}
|
||||||
|
return current
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect parameter names from a function definition.
|
||||||
|
*/
|
||||||
|
private collectParams(fnNode: SyntaxNode, scope: Set<string>) {
|
||||||
|
let child = fnNode.firstChild
|
||||||
|
while (child) {
|
||||||
|
if (child.type.id === Terms.Params) {
|
||||||
|
let param = child.firstChild
|
||||||
|
while (param) {
|
||||||
|
if (param.type.id === Terms.Identifier) {
|
||||||
|
const text = this.document.getText({
|
||||||
|
start: this.document.positionAt(param.from),
|
||||||
|
end: this.document.positionAt(param.to),
|
||||||
|
})
|
||||||
|
scope.add(text)
|
||||||
|
}
|
||||||
|
param = param.nextSibling
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect assignment names from a scope node that occur before the given position.
|
||||||
|
*/
|
||||||
|
private collectAssignments(scopeNode: SyntaxNode, beforePosition: number, scope: Set<string>) {
|
||||||
|
const cursor = scopeNode.cursor()
|
||||||
|
|
||||||
|
cursor.iterate((node) => {
|
||||||
|
// Stop if we've passed the position we're checking
|
||||||
|
if (node.from >= beforePosition) return false
|
||||||
|
|
||||||
|
if (node.type.id === Terms.Assign) {
|
||||||
|
const assignNode = node.node
|
||||||
|
const child = assignNode.firstChild
|
||||||
|
if (child?.type.id === Terms.AssignableIdentifier) {
|
||||||
|
const text = this.document.getText({
|
||||||
|
start: this.document.positionAt(child.from),
|
||||||
|
end: this.document.positionAt(child.to),
|
||||||
|
})
|
||||||
|
scope.add(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't descend into nested functions unless it's the current scope
|
||||||
|
if (node.type.id === Terms.FunctionDef && node.node !== scopeNode) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
221
vscode-extension/server/src/semanticTokens.ts
Normal file
221
vscode-extension/server/src/semanticTokens.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { parser } from '../../../src/parser/shrimp'
|
||||||
|
import * as Terms from '../../../src/parser/shrimp.terms'
|
||||||
|
import { SyntaxNode } from '@lezer/common'
|
||||||
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
|
import {
|
||||||
|
SemanticTokensBuilder,
|
||||||
|
SemanticTokenTypes,
|
||||||
|
SemanticTokenModifiers,
|
||||||
|
} from 'vscode-languageserver/node'
|
||||||
|
import { ScopeTracker } from './scopeTracker'
|
||||||
|
|
||||||
|
export const TOKEN_TYPES = [
|
||||||
|
SemanticTokenTypes.function,
|
||||||
|
SemanticTokenTypes.variable,
|
||||||
|
SemanticTokenTypes.string,
|
||||||
|
SemanticTokenTypes.number,
|
||||||
|
SemanticTokenTypes.operator,
|
||||||
|
SemanticTokenTypes.keyword,
|
||||||
|
SemanticTokenTypes.parameter,
|
||||||
|
SemanticTokenTypes.property,
|
||||||
|
SemanticTokenTypes.regexp,
|
||||||
|
SemanticTokenTypes.comment,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TOKEN_MODIFIERS = [
|
||||||
|
SemanticTokenModifiers.declaration,
|
||||||
|
SemanticTokenModifiers.modification,
|
||||||
|
SemanticTokenModifiers.readonly,
|
||||||
|
]
|
||||||
|
|
||||||
|
export function buildSemanticTokens(document: TextDocument): number[] {
|
||||||
|
const text = document.getText()
|
||||||
|
const tree = parser.parse(text)
|
||||||
|
const builder = new SemanticTokensBuilder()
|
||||||
|
const scopeTracker = new ScopeTracker(document)
|
||||||
|
|
||||||
|
walkTree(tree.topNode, document, builder, scopeTracker)
|
||||||
|
|
||||||
|
return builder.build().data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the tree and collect tokens
|
||||||
|
function walkTree(
|
||||||
|
node: SyntaxNode,
|
||||||
|
document: TextDocument,
|
||||||
|
builder: SemanticTokensBuilder,
|
||||||
|
scopeTracker: ScopeTracker
|
||||||
|
) {
|
||||||
|
const tokenInfo = getTokenType(node, document, scopeTracker)
|
||||||
|
|
||||||
|
if (tokenInfo !== undefined) {
|
||||||
|
const start = document.positionAt(node.from)
|
||||||
|
const length = node.to - node.from
|
||||||
|
builder.push(start.line, start.character, length, tokenInfo.type, tokenInfo.modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = node.firstChild
|
||||||
|
while (child) {
|
||||||
|
walkTree(child, document, builder, scopeTracker)
|
||||||
|
child = child.nextSibling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map Lezer node IDs to semantic token type indices and modifiers
|
||||||
|
type TokenInfo = { type: number; modifiers: number } | undefined
|
||||||
|
function getTokenType(
|
||||||
|
node: SyntaxNode,
|
||||||
|
document: TextDocument,
|
||||||
|
scopeTracker: ScopeTracker
|
||||||
|
): TokenInfo {
|
||||||
|
const nodeTypeId = node.type.id
|
||||||
|
const parentTypeId = node.parent?.type.id
|
||||||
|
|
||||||
|
// Special case for now, eventually keywords will go away
|
||||||
|
if (node.type.name === 'keyword') {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.keyword),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (nodeTypeId) {
|
||||||
|
case Terms.Identifier:
|
||||||
|
// Check parent to determine context
|
||||||
|
if (parentTypeId === Terms.FunctionCall) {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.function),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentTypeId === Terms.FunctionDef) {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.function),
|
||||||
|
modifiers: getModifierBits(SemanticTokenModifiers.declaration),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentTypeId === Terms.FunctionCallOrIdentifier) {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.function),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentTypeId === Terms.Params) {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.parameter),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parentTypeId === Terms.DotGet) {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.property),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: Identifier in PositionalArg - check scope
|
||||||
|
if (parentTypeId === Terms.PositionalArg) {
|
||||||
|
const identifierText = document.getText({
|
||||||
|
start: document.positionAt(node.from),
|
||||||
|
end: document.positionAt(node.to),
|
||||||
|
})
|
||||||
|
|
||||||
|
// If not in scope, treat as string (like a Word)
|
||||||
|
if (!scopeTracker.isInScope(identifierText, node)) {
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.string),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If in scope, fall through to treat as variable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it's a regular variable
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.variable),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.IdentifierBeforeDot:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.variable),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.NamedArgPrefix:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.property),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.AssignableIdentifier:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.variable),
|
||||||
|
modifiers: getModifierBits(SemanticTokenModifiers.modification),
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.String:
|
||||||
|
case Terms.StringFragment:
|
||||||
|
case Terms.Word:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.string),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.Number:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.number),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.Plus:
|
||||||
|
case Terms.Minus:
|
||||||
|
case Terms.Star:
|
||||||
|
case Terms.Slash:
|
||||||
|
case Terms.Eq:
|
||||||
|
case Terms.EqEq:
|
||||||
|
case Terms.Neq:
|
||||||
|
case Terms.Lt:
|
||||||
|
case Terms.Lte:
|
||||||
|
case Terms.Gt:
|
||||||
|
case Terms.Gte:
|
||||||
|
case Terms.Modulo:
|
||||||
|
case Terms.And:
|
||||||
|
case Terms.Or:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.operator),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.Do:
|
||||||
|
case Terms.colon:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.keyword),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.Regex:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.regexp),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terms.Comment:
|
||||||
|
return {
|
||||||
|
type: TOKEN_TYPES.indexOf(SemanticTokenTypes.comment),
|
||||||
|
modifiers: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModifierBits = (...modifiers: SemanticTokenModifiers[]): number => {
|
||||||
|
let bits = 0
|
||||||
|
for (const modifier of modifiers) {
|
||||||
|
const index = TOKEN_MODIFIERS.indexOf(modifier)
|
||||||
|
if (index !== -1) bits |= 1 << index
|
||||||
|
}
|
||||||
|
return bits
|
||||||
|
}
|
||||||
150
vscode-extension/server/src/server.ts
Normal file
150
vscode-extension/server/src/server.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
import { TextDocument } from 'vscode-languageserver-textdocument'
|
||||||
|
import { buildDiagnostics } from './diagnostics'
|
||||||
|
import { buildSemanticTokens, TOKEN_MODIFIERS, TOKEN_TYPES } from './semanticTokens'
|
||||||
|
import { parser } from '../../../src/parser/shrimp'
|
||||||
|
import { Compiler } from '../../../src/compiler/compiler'
|
||||||
|
import {
|
||||||
|
InitializeResult,
|
||||||
|
TextDocuments,
|
||||||
|
TextDocumentSyncKind,
|
||||||
|
createConnection,
|
||||||
|
ProposedFeatures,
|
||||||
|
CompletionItemKind,
|
||||||
|
} from 'vscode-languageserver/node'
|
||||||
|
|
||||||
|
const connection = createConnection(ProposedFeatures.all)
|
||||||
|
const documents = new TextDocuments(TextDocument)
|
||||||
|
documents.listen(connection)
|
||||||
|
|
||||||
|
// Server capabilities
|
||||||
|
connection.onInitialize(handleInitialize)
|
||||||
|
|
||||||
|
// Language features
|
||||||
|
connection.languages.semanticTokens.on(handleSemanticTokens)
|
||||||
|
documents.onDidChangeContent(handleDocumentChange)
|
||||||
|
connection.onCompletion(handleCompletion)
|
||||||
|
|
||||||
|
// Debug commands
|
||||||
|
connection.onRequest('shrimp/parseTree', handleParseTree)
|
||||||
|
connection.onRequest('shrimp/bytecode', handleBytecode)
|
||||||
|
|
||||||
|
// Start listening
|
||||||
|
connection.listen()
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Handler implementations
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function handleInitialize(): InitializeResult {
|
||||||
|
connection.console.log('🦐 Server initialized with capabilities')
|
||||||
|
const result: InitializeResult = {
|
||||||
|
capabilities: {
|
||||||
|
textDocumentSync: TextDocumentSyncKind.Full,
|
||||||
|
completionProvider: {
|
||||||
|
triggerCharacters: ['.'],
|
||||||
|
},
|
||||||
|
semanticTokensProvider: {
|
||||||
|
legend: {
|
||||||
|
tokenTypes: TOKEN_TYPES,
|
||||||
|
tokenModifiers: TOKEN_MODIFIERS,
|
||||||
|
},
|
||||||
|
full: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSemanticTokens(params: any) {
|
||||||
|
const document = documents.get(params.textDocument.uri)
|
||||||
|
if (!document) return { data: [] }
|
||||||
|
|
||||||
|
const data = buildSemanticTokens(document)
|
||||||
|
return { data }
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDocumentChange(change: any) {
|
||||||
|
const textDocument = change.document
|
||||||
|
const diagnostics = buildDiagnostics(textDocument)
|
||||||
|
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCompletion(params: any) {
|
||||||
|
const keywords = ['if', 'else', 'do', 'end', 'and', 'or', 'true', 'false', 'null']
|
||||||
|
|
||||||
|
return keywords.map((keyword) => ({
|
||||||
|
label: keyword,
|
||||||
|
kind: CompletionItemKind.Keyword,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleParseTree(params: { uri: string }) {
|
||||||
|
connection.console.log(`🦐 Parse tree requested for: ${params.uri}`)
|
||||||
|
const document = documents.get(params.uri)
|
||||||
|
if (!document) return 'Document not found'
|
||||||
|
|
||||||
|
const text = document.getText()
|
||||||
|
const tree = parser.parse(text)
|
||||||
|
const cursor = tree.cursor()
|
||||||
|
|
||||||
|
let formatted = ''
|
||||||
|
let depth = 0
|
||||||
|
|
||||||
|
const printNode = () => {
|
||||||
|
const nodeName = cursor.name
|
||||||
|
const nodeText = text.slice(cursor.from, cursor.to)
|
||||||
|
const indent = ' '.repeat(depth)
|
||||||
|
|
||||||
|
formatted += `${indent}${nodeName}`
|
||||||
|
if (nodeText) {
|
||||||
|
const escapedText = nodeText.replace(/\n/g, '\\n').replace(/\r/g, '\\r')
|
||||||
|
formatted += ` "${escapedText}"`
|
||||||
|
}
|
||||||
|
formatted += '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
const traverse = (): void => {
|
||||||
|
printNode()
|
||||||
|
|
||||||
|
if (cursor.firstChild()) {
|
||||||
|
depth++
|
||||||
|
do {
|
||||||
|
traverse()
|
||||||
|
} while (cursor.nextSibling())
|
||||||
|
cursor.parent()
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse()
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBytecode(params: { uri: string }) {
|
||||||
|
connection.console.log(`🦐 Bytecode requested for: ${params.uri}`)
|
||||||
|
const document = documents.get(params.uri)
|
||||||
|
if (!document) return 'Document not found'
|
||||||
|
|
||||||
|
try {
|
||||||
|
const text = document.getText()
|
||||||
|
const compiler = new Compiler(text)
|
||||||
|
|
||||||
|
// Format bytecode as readable string
|
||||||
|
let output = 'Bytecode:\n\n'
|
||||||
|
const bytecode = compiler.bytecode
|
||||||
|
|
||||||
|
output += bytecode.instructions
|
||||||
|
.map((op, i) => `${i.toString().padStart(4)}: ${JSON.stringify(op)}`)
|
||||||
|
.join('\n')
|
||||||
|
|
||||||
|
// Strip ANSI color codes
|
||||||
|
output = output.replace(/\x1b\[[0-9;]*m/g, '')
|
||||||
|
|
||||||
|
return output
|
||||||
|
} catch (error) {
|
||||||
|
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||||
|
// Strip ANSI color codes from error message too
|
||||||
|
return `Compilation failed: ${errorMsg.replace(/\x1b\[[0-9;]*m/g, '')}`
|
||||||
|
}
|
||||||
|
}
|
||||||
16
vscode-extension/tsconfig.json
Normal file
16
vscode-extension/tsconfig.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["client/src/**/*", "server/src/**/*", "../src/**/*"],
|
||||||
|
"exclude": ["node_modules", "client/dist", "server/dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user