Compare commits
No commits in common. "ea01a93563a88bed3374ea0b6c613dcaa69773df" and "7229f4afd044b8d2907b18b3f642a1cc73bcca1c" have entirely different histories.
ea01a93563
...
7229f4afd0
4
vscode-extension/.gitignore
vendored
4
vscode-extension/.gitignore
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
node_modules
|
|
||||||
client/dist
|
|
||||||
server/dist
|
|
||||||
*.vsix
|
|
||||||
19
vscode-extension/.vscode/launch.json
vendored
19
vscode-extension/.vscode/launch.json
vendored
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"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
18
vscode-extension/.vscode/tasks.json
vendored
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
.vscode/**
|
|
||||||
src/**
|
|
||||||
tsconfig.json
|
|
||||||
node_modules/**
|
|
||||||
*.map
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
# 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)
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
{
|
|
||||||
"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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
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() {}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
# 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
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 270 KiB |
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,145 +0,0 @@
|
||||||
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 }
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,221 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,150 +0,0 @@
|
||||||
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, '')}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"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