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