Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete
Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling, and docs. Phase 1 of the syntax-redesign cohesion pass has landed: edge-system fix, signature-query, when-alias, and the documentation truth-pass. Suite green (14/14), C-free bootstrap fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
985f9ad8f2
418 changed files with 39065 additions and 0 deletions
12
tools/editors/vscode/.vscodeignore
Normal file
12
tools/editors/vscode/.vscodeignore
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.vscode/**
|
||||
node_modules/**
|
||||
!node_modules/vscode-languageclient/**
|
||||
!node_modules/vscode-jsonrpc/**
|
||||
!node_modules/vscode-languageserver-protocol/**
|
||||
!node_modules/vscode-languageserver-types/**
|
||||
!node_modules/semver/**
|
||||
!node_modules/balanced-match/**
|
||||
!node_modules/minimatch/**
|
||||
!node_modules/brace-expansion/**
|
||||
!node_modules/concat-map/**
|
||||
**/*.map
|
||||
54
tools/editors/vscode/README.md
Normal file
54
tools/editors/vscode/README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Ludic for VS Code
|
||||
|
||||
Syntax highlighting, formatting, and full language-server support for **Ludic**,
|
||||
the AI-first, ECS-native game language. See `LANGUAGE.md` in the Ludic repository
|
||||
for the language reference.
|
||||
|
||||
## What you get
|
||||
|
||||
| | Source |
|
||||
|---|---|
|
||||
| Syntax highlighting | TextMate grammar, works with no build |
|
||||
| Semantic highlighting | `ludic-lsp` — real name resolution, so a component reads differently from a local |
|
||||
| Diagnostics | structural errors as you type; `ludicc`'s own errors on save |
|
||||
| Completion | context-aware: fields after `.`, components inside `query [...]`, phases after `phase`, widgets inside `ui` |
|
||||
| Hover | signatures and docs for builtins, intrinsics, and everything you declared |
|
||||
| Go to definition / references / rename | across every file in the compilation unit, following `import` |
|
||||
| Formatting | format-on-save, comment-preserving |
|
||||
| Outline, folding, inlay hints, signature help | |
|
||||
| ` ```ludic ` fences in Markdown | highlighted, checked, and formatted |
|
||||
|
||||
## Setup
|
||||
|
||||
Build the toolchain once, from the repository root:
|
||||
|
||||
```bash
|
||||
./tools/build-tools.sh
|
||||
```
|
||||
|
||||
That produces `build/ludic-lsp` and `build/ludic-fmt`, which the extension finds
|
||||
on its own. If you keep them elsewhere, set `ludic.server.path`.
|
||||
|
||||
Format on save:
|
||||
|
||||
```json
|
||||
"[ludic]": { "editor.formatOnSave": true }
|
||||
```
|
||||
|
||||
## Settings
|
||||
|
||||
- `ludic.server.path` — where `ludic-lsp` lives. Empty = search `build/`, then `PATH`.
|
||||
- `ludic.server.enabled` — turn the server off and keep only static highlighting.
|
||||
- `ludic.compilerPath` — where `ludicc` lives, for on-save diagnostics.
|
||||
- `ludic.compilerDiagnostics` — whether to run the compiler at all.
|
||||
- `ludic.indentSize` — spaces per level for the formatter.
|
||||
- `ludic.trace.server` — log LSP traffic (`Ludic: Show Language Server Log`).
|
||||
|
||||
## Installing without publishing
|
||||
|
||||
```bash
|
||||
cd tools/editors/vscode
|
||||
npm install
|
||||
npx @vscode/vsce package
|
||||
code --install-extension ludic-1.0.0.vsix
|
||||
```
|
||||
7
tools/editors/vscode/icons/ludic.svg
Normal file
7
tools/editors/vscode/icons/ludic.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
|
||||
<rect width="32" height="32" rx="6" fill="#1a1a2c"/>
|
||||
<rect x="7" y="7" width="8" height="8" rx="1.5" fill="#ffe060"/>
|
||||
<rect x="17" y="7" width="8" height="8" rx="1.5" fill="#4a4a72"/>
|
||||
<rect x="7" y="17" width="8" height="8" rx="1.5" fill="#4a4a72"/>
|
||||
<rect x="17" y="17" width="8" height="8" rx="1.5" fill="#9aa0c8"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 418 B |
31
tools/editors/vscode/language-configuration.json
Normal file
31
tools/editors/vscode/language-configuration.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"comment": "Editor mechanics that are not highlighting and not the language server: comments, bracket pairs, auto-closing, indentation triggers. VS Code reads this file directly; the JetBrains plugin implements the same rules natively; other editors can transcribe it.",
|
||||
"comments": {
|
||||
"lineComment": "#"
|
||||
},
|
||||
"brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
|
||||
"autoClosingPairs": [
|
||||
{ "open": "{", "close": "}" },
|
||||
{ "open": "[", "close": "]" },
|
||||
{ "open": "(", "close": ")" },
|
||||
{ "open": "\"", "close": "\"", "notIn": ["string", "comment"] }
|
||||
],
|
||||
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
|
||||
"indentationRules": {
|
||||
"increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
|
||||
"decreaseIndentPattern": "^\\s*[}\\])].*$"
|
||||
},
|
||||
"onEnterRules": [
|
||||
{
|
||||
"beforeText": "^\\s*#.*$",
|
||||
"action": { "indent": "none", "appendText": "# " }
|
||||
}
|
||||
],
|
||||
"wordPattern": "[A-Za-z_][A-Za-z0-9_]*",
|
||||
"folding": {
|
||||
"markers": {
|
||||
"start": "^\\s*#\\s*region\\b",
|
||||
"end": "^\\s*#\\s*endregion\\b"
|
||||
}
|
||||
}
|
||||
}
|
||||
97
tools/editors/vscode/package-lock.json
generated
Normal file
97
tools/editors/vscode/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
{
|
||||
"name": "ludic",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ludic",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "5.1.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
|
||||
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageclient": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz",
|
||||
"integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0",
|
||||
"semver": "^7.3.7",
|
||||
"vscode-languageserver-protocol": "3.17.5"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.82.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-protocol": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vscode-jsonrpc": "8.2.0",
|
||||
"vscode-languageserver-types": "3.17.5"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-languageserver-types": {
|
||||
"version": "3.17.5",
|
||||
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
158
tools/editors/vscode/package.json
Normal file
158
tools/editors/vscode/package.json
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
{
|
||||
"name": "ludic",
|
||||
"displayName": "Ludic",
|
||||
"description": "Ludic language support: syntax highlighting, formatting, and the ludic-lsp language server. Also highlights ```ludic fences in Markdown.",
|
||||
"version": "1.0.0",
|
||||
"publisher": "ludic",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Formatters",
|
||||
"Linters"
|
||||
],
|
||||
"keywords": [
|
||||
"ludic",
|
||||
"ecs",
|
||||
"gamedev"
|
||||
],
|
||||
"main": "./src/extension.js",
|
||||
"activationEvents": [
|
||||
"onLanguage:ludic",
|
||||
"onLanguage:markdown",
|
||||
"workspaceContains:**/*.ludic"
|
||||
],
|
||||
"contributes": {
|
||||
"languages": [
|
||||
{
|
||||
"id": "ludic",
|
||||
"aliases": [
|
||||
"Ludic",
|
||||
"ludic"
|
||||
],
|
||||
"extensions": [
|
||||
".ludic"
|
||||
],
|
||||
"configuration": "./language-configuration.json",
|
||||
"icon": {
|
||||
"light": "./icons/ludic.svg",
|
||||
"dark": "./icons/ludic.svg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "ludic",
|
||||
"scopeName": "source.ludic",
|
||||
"path": "./syntaxes/ludic.tmLanguage.json"
|
||||
},
|
||||
{
|
||||
"scopeName": "markdown.ludic.codeblock",
|
||||
"path": "./syntaxes/ludic.markdown-injection.json",
|
||||
"injectTo": [
|
||||
"text.html.markdown"
|
||||
],
|
||||
"embeddedLanguages": {
|
||||
"meta.embedded.block.ludic": "ludic"
|
||||
}
|
||||
}
|
||||
],
|
||||
"semanticTokenScopes": [
|
||||
{
|
||||
"language": "ludic",
|
||||
"scopes": {
|
||||
"struct": [
|
||||
"entity.name.type.component.ludic"
|
||||
],
|
||||
"class": [
|
||||
"entity.name.type.archetype.ludic"
|
||||
],
|
||||
"namespace": [
|
||||
"entity.name.type.scene.ludic"
|
||||
],
|
||||
"function.defaultLibrary": [
|
||||
"support.function.builtin.ludic"
|
||||
],
|
||||
"enumMember.readonly.defaultLibrary": [
|
||||
"constant.language.phase.ludic"
|
||||
],
|
||||
"decorator": [
|
||||
"entity.name.function.decorator.ludic"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Ludic",
|
||||
"properties": {
|
||||
"ludic.server.path": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Path to the `ludic-lsp` binary. Empty means: look in `build/ludic-lsp` under each workspace folder, then on `PATH`."
|
||||
},
|
||||
"ludic.server.enabled": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Run the Ludic language server. Turn this off to keep only the static syntax highlighting."
|
||||
},
|
||||
"ludic.compilerPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"markdownDescription": "Path to `ludicc`. The server shells out to it for authoritative diagnostics on save. Empty means `build/ludicc` under the workspace."
|
||||
},
|
||||
"ludic.compilerDiagnostics": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Report errors from the Ludic compiler on save, in addition to the server's own structural checks."
|
||||
},
|
||||
"ludic.indentSize": {
|
||||
"type": "number",
|
||||
"default": 2,
|
||||
"description": "Spaces per indent level used by the formatter when the editor does not say otherwise."
|
||||
},
|
||||
"ludic.trace.server": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"off",
|
||||
"messages",
|
||||
"verbose"
|
||||
],
|
||||
"default": "off",
|
||||
"description": "Log the traffic between VS Code and the Ludic language server."
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "ludic.restartServer",
|
||||
"title": "Ludic: Restart Language Server"
|
||||
},
|
||||
{
|
||||
"command": "ludic.showOutput",
|
||||
"title": "Ludic: Show Language Server Log"
|
||||
}
|
||||
],
|
||||
"configurationDefaults": {
|
||||
"[ludic]": {
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.semanticHighlighting.enabled": true
|
||||
}
|
||||
},
|
||||
"snippets": [
|
||||
{
|
||||
"language": "ludic",
|
||||
"path": "./snippets/ludic.json"
|
||||
}
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://example.invalid/ludic"
|
||||
}
|
||||
}
|
||||
96
tools/editors/vscode/snippets/ludic.json
Normal file
96
tools/editors/vscode/snippets/ludic.json
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
{
|
||||
"game": {
|
||||
"prefix": "game",
|
||||
"body": ["game ${1:Name} {", "\t$0", "}"],
|
||||
"description": "A game unit — compiles to an executable."
|
||||
},
|
||||
"module": {
|
||||
"prefix": "module",
|
||||
"body": ["module ${1:Name} {", "\t$0", "}"],
|
||||
"description": "A library unit — compiles to a shared library, no frame loop."
|
||||
},
|
||||
"component": {
|
||||
"prefix": "component",
|
||||
"body": ["component ${1:Name} { ${2:x}: ${3:int} = ${4:0} }"],
|
||||
"description": "A component: typed per-entity fields."
|
||||
},
|
||||
"archetype": {
|
||||
"prefix": "archetype",
|
||||
"body": ["archetype ${1:Name} {", "\t${2:Pos}", "}"],
|
||||
"description": "An entity kind bundling a fixed set of components."
|
||||
},
|
||||
"system": {
|
||||
"prefix": "system",
|
||||
"body": [
|
||||
"system ${1:Name} phase ${2|Start,Input,FixedUpdate,Update,LateUpdate,Render|} {",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "A system that runs once per tick in its phase."
|
||||
},
|
||||
"system with query": {
|
||||
"prefix": "systemq",
|
||||
"body": [
|
||||
"system ${1:Name} phase ${2|Update,FixedUpdate,LateUpdate,Render,Input|}",
|
||||
" query (${3:p}) [${4:Pos}]",
|
||||
"{",
|
||||
"\t$0",
|
||||
"}"
|
||||
],
|
||||
"description": "A system whose body runs once per matching entity."
|
||||
},
|
||||
"for query": {
|
||||
"prefix": "forq",
|
||||
"body": ["for (${1:p}) in query [${2:Pos}] {", "\t$0", "}"],
|
||||
"description": "Iterate every entity matching a query."
|
||||
},
|
||||
"spawn": {
|
||||
"prefix": "spawn",
|
||||
"body": ["spawn ${1:Kind} {", "\t${2:Pos} = { ${3:x} = ${4:0} }", "}"],
|
||||
"description": "Create an entity of an archetype."
|
||||
},
|
||||
"scene": {
|
||||
"prefix": "scene",
|
||||
"body": [
|
||||
"scene ${1:Name} {",
|
||||
"\ton enter { $2 }",
|
||||
"\tlayer ${3:Main} {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"}"
|
||||
],
|
||||
"description": "A scene with lifecycle hooks and one layer of systems."
|
||||
},
|
||||
"ui": {
|
||||
"prefix": "ui",
|
||||
"body": [
|
||||
"ui ${1:Name} {",
|
||||
"\tpanel id=${2:Root} pad=16 gap=6 align=center {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"}"
|
||||
],
|
||||
"description": "A retained UI tree."
|
||||
},
|
||||
"match": {
|
||||
"prefix": "match",
|
||||
"body": ["match ${1:x} {", "\t${2:0} => { $3 }", "\t_ => { $0 }", "}"],
|
||||
"description": "Pattern match."
|
||||
},
|
||||
"machine": {
|
||||
"prefix": "machine",
|
||||
"body": [
|
||||
"machine reg(${1:R_PHASE}) {",
|
||||
"\tstate ${2:Idle} = 0 {",
|
||||
"\t\t$0",
|
||||
"\t}",
|
||||
"}"
|
||||
],
|
||||
"description": "A state machine over a register."
|
||||
},
|
||||
"extern fn": {
|
||||
"prefix": "extern",
|
||||
"body": ["extern fn ${1:name}(${2:a}: ${3:int}) -> ${4:int} = \"${5:c_symbol}\""],
|
||||
"description": "Bind a C symbol through the FFI."
|
||||
}
|
||||
}
|
||||
149
tools/editors/vscode/src/extension.js
Normal file
149
tools/editors/vscode/src/extension.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/* ============================================================================
|
||||
* The VS Code half of Ludic support.
|
||||
*
|
||||
* There is deliberately very little here. Highlighting comes from the TextMate
|
||||
* grammar in syntaxes/ (shared, unmodified, with every other editor), and
|
||||
* everything else — completion, hover, diagnostics, formatting, symbols —
|
||||
* comes from `ludic-lsp`, the same binary JetBrains and Neovim talk to. This
|
||||
* file's whole job is finding that binary and keeping it alive.
|
||||
* ==========================================================================*/
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const vscode = require('vscode');
|
||||
const { LanguageClient, TransportKind } = require('vscode-languageclient/node');
|
||||
|
||||
let client = null;
|
||||
let output = null;
|
||||
|
||||
/** Where the language server might be, in the order we should look. */
|
||||
function resolveServer(folders) {
|
||||
const configured = vscode.workspace.getConfiguration('ludic').get('server.path');
|
||||
if (configured) return expand(configured);
|
||||
|
||||
const names = ['ludic-lsp', 'ludic-lsp.exe'];
|
||||
for (const folder of folders) {
|
||||
const root = folder.uri.fsPath;
|
||||
for (const rel of ['build', 'tools/build', 'bin', '.']) {
|
||||
for (const name of names) {
|
||||
const p = path.join(root, rel, name);
|
||||
if (isExecutable(p)) return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall back to PATH; spawning by bare name lets the OS resolve it.
|
||||
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
|
||||
for (const name of names) {
|
||||
const p = path.join(dir, name);
|
||||
if (isExecutable(p)) return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveCompiler(folders) {
|
||||
const configured = vscode.workspace.getConfiguration('ludic').get('compilerPath');
|
||||
if (configured) return expand(configured);
|
||||
for (const folder of folders) {
|
||||
const p = path.join(folder.uri.fsPath, 'build', 'ludicc');
|
||||
if (isExecutable(p)) return p;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function expand(p) {
|
||||
return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
|
||||
}
|
||||
|
||||
function isExecutable(p) {
|
||||
try {
|
||||
fs.accessSync(p, fs.constants.X_OK);
|
||||
return fs.statSync(p).isFile();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function start(context) {
|
||||
const cfg = vscode.workspace.getConfiguration('ludic');
|
||||
if (!cfg.get('server.enabled')) return;
|
||||
|
||||
const folders = vscode.workspace.workspaceFolders || [];
|
||||
const server = resolveServer(folders);
|
||||
if (!server) {
|
||||
// Not an error worth a modal: the grammar still highlights, and plenty of
|
||||
// people open a .ludic file without having built the toolchain.
|
||||
output.appendLine(
|
||||
'ludic-lsp not found. Build it with tools/build-tools.sh, or set "ludic.server.path". ' +
|
||||
'Syntax highlighting works without it; completion and diagnostics do not.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
output.appendLine(`starting ${server}`);
|
||||
|
||||
const serverOptions = {
|
||||
run: { command: server, args: ['--stdio'], transport: TransportKind.stdio },
|
||||
debug: { command: server, args: ['--stdio'], transport: TransportKind.stdio }
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
// Markdown is included on purpose: ```ludic fences in a .md get the same
|
||||
// diagnostics, hover and semantic highlighting as a real source file.
|
||||
documentSelector: [
|
||||
{ scheme: 'file', language: 'ludic' },
|
||||
{ scheme: 'file', language: 'markdown' }
|
||||
],
|
||||
synchronize: {
|
||||
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.ludic'),
|
||||
configurationSection: 'ludic'
|
||||
},
|
||||
outputChannel: output,
|
||||
initializationOptions: {
|
||||
compilerPath: resolveCompiler(folders),
|
||||
compilerDiagnostics: cfg.get('compilerDiagnostics'),
|
||||
indentSize: cfg.get('indentSize')
|
||||
}
|
||||
};
|
||||
|
||||
client = new LanguageClient('ludic', 'Ludic Language Server', serverOptions, clientOptions);
|
||||
await client.start();
|
||||
context.subscriptions.push(client);
|
||||
}
|
||||
|
||||
async function stop() {
|
||||
if (!client) return;
|
||||
const c = client;
|
||||
client = null;
|
||||
await c.stop();
|
||||
}
|
||||
|
||||
async function activate(context) {
|
||||
output = vscode.window.createOutputChannel('Ludic');
|
||||
context.subscriptions.push(output);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('ludic.restartServer', async () => {
|
||||
await stop();
|
||||
await start(context);
|
||||
vscode.window.setStatusBarMessage('Ludic: language server restarted', 3000);
|
||||
}),
|
||||
vscode.commands.registerCommand('ludic.showOutput', () => output.show())
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.workspace.onDidChangeConfiguration(async (e) => {
|
||||
if (e.affectsConfiguration('ludic.server')) {
|
||||
await stop();
|
||||
await start(context);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await start(context);
|
||||
}
|
||||
|
||||
function deactivate() {
|
||||
return stop();
|
||||
}
|
||||
|
||||
module.exports = { activate, deactivate };
|
||||
27
tools/editors/vscode/syntaxes/ludic.markdown-injection.json
Normal file
27
tools/editors/vscode/syntaxes/ludic.markdown-injection.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"scopeName": "markdown.ludic.codeblock",
|
||||
"comment": "Injects Ludic highlighting into ```ludic fences in Markdown. Any TextMate-based editor (VS Code, Sublime, JetBrains' TextMate bundles, Zed) picks this up; JetBrains' native Markdown support gets the same thing for free from the registered `Ludic` language ID, and ludic-lsp will also serve semantic tokens and diagnostics for the fence bodies in a .md file.",
|
||||
"injectionSelector": "L:text.html.markdown",
|
||||
"patterns": [{ "include": "#ludic-code-block" }],
|
||||
"repository": {
|
||||
"ludic-code-block": {
|
||||
"begin": "(^|\\G)(\\s*)(`{3,}|~{3,})\\s*(?i:(ludic)((\\s+|:|,|\\{|\\?)[^`~]*)?)$",
|
||||
"name": "markup.fenced_code.block.markdown",
|
||||
"end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$",
|
||||
"beginCaptures": {
|
||||
"3": { "name": "punctuation.definition.markdown" },
|
||||
"4": { "name": "fenced_code.block.language.markdown" },
|
||||
"5": { "name": "fenced_code.block.language.attributes.markdown" }
|
||||
},
|
||||
"endCaptures": { "3": { "name": "punctuation.definition.markdown" } },
|
||||
"patterns": [
|
||||
{
|
||||
"begin": "(^|\\G)(\\s*)(.*)",
|
||||
"while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)",
|
||||
"contentName": "meta.embedded.block.ludic",
|
||||
"patterns": [{ "include": "source.ludic" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
241
tools/editors/vscode/syntaxes/ludic.tmLanguage.json
Normal file
241
tools/editors/vscode/syntaxes/ludic.tmLanguage.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
|
||||
"name": "Ludic",
|
||||
"scopeName": "source.ludic",
|
||||
"fileTypes": ["ludic"],
|
||||
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. Generated shapes are kept in sync with tools/ludic-tools/ludic_syntax.h.",
|
||||
"patterns": [
|
||||
{ "include": "#comment" },
|
||||
{ "include": "#declaration" },
|
||||
{ "include": "#query" },
|
||||
{ "include": "#string" },
|
||||
{ "include": "#number" },
|
||||
{ "include": "#annotation" },
|
||||
{ "include": "#keyword" },
|
||||
{ "include": "#widget" },
|
||||
{ "include": "#builtin" },
|
||||
{ "include": "#member" },
|
||||
{ "include": "#call" },
|
||||
{ "include": "#typename" },
|
||||
{ "include": "#operator" }
|
||||
],
|
||||
"repository": {
|
||||
"comment": {
|
||||
"match": "#.*$",
|
||||
"name": "comment.line.number-sign.ludic"
|
||||
},
|
||||
"string": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "string.quoted.double.ludic",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"beginCaptures": { "0": { "name": "punctuation.definition.string.begin.ludic" } },
|
||||
"endCaptures": { "0": { "name": "punctuation.definition.string.end.ludic" } },
|
||||
"patterns": [{ "name": "constant.character.escape.ludic", "match": "\\\\." }]
|
||||
},
|
||||
{
|
||||
"name": "string.quoted.single.ludic",
|
||||
"match": "'(\\\\.|[^'\\\\])'"
|
||||
}
|
||||
]
|
||||
},
|
||||
"number": {
|
||||
"patterns": [
|
||||
{ "name": "constant.numeric.hex.ludic", "match": "\\b0[xX][0-9a-fA-F]+\\b" },
|
||||
{ "name": "constant.numeric.fixed.ludic", "match": "\\b[0-9]+\\.[0-9]+\\b" },
|
||||
{ "name": "constant.numeric.integer.ludic", "match": "\\b[0-9]+\\b" }
|
||||
]
|
||||
},
|
||||
"annotation": {
|
||||
"name": "entity.name.function.decorator.ludic",
|
||||
"match": "@[A-Za-z_][A-Za-z0-9_]*"
|
||||
},
|
||||
"declaration": {
|
||||
"patterns": [
|
||||
{
|
||||
"match": "\\b(game|module)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.control.unit.ludic" },
|
||||
"2": { "name": "entity.name.type.unit.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(component)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.component.ludic" },
|
||||
"2": { "name": "entity.name.type.component.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(archetype)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.archetype.ludic" },
|
||||
"2": { "name": "entity.name.type.archetype.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(scene)\\s+([A-Za-z_][A-Za-z0-9_]*)(\\s+start)?",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.scene.ludic" },
|
||||
"2": { "name": "entity.name.type.scene.ludic" },
|
||||
"3": { "name": "keyword.other.start.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(layer|ui)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.ludic" },
|
||||
"2": { "name": "entity.name.type.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(edge\\s+)?(system)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.modifier.edge.ludic" },
|
||||
"2": { "name": "storage.type.system.ludic" },
|
||||
"3": { "name": "entity.name.function.system.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(export\\s+|pure\\s+)?(extern\\s+)?(fn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.modifier.ludic" },
|
||||
"2": { "name": "storage.modifier.extern.ludic" },
|
||||
"3": { "name": "storage.type.function.ludic" },
|
||||
"4": { "name": "entity.name.function.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(const|var|let)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "storage.type.ludic" },
|
||||
"2": { "name": "variable.other.definition.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(state)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.control.state.ludic" },
|
||||
"2": { "name": "entity.name.constant.state.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(import)\\s+(?=\")",
|
||||
"captures": { "1": { "name": "keyword.control.import.ludic" } }
|
||||
},
|
||||
{
|
||||
"match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.other.clause.ludic" },
|
||||
"2": { "name": "constant.language.phase.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(enter)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.control.enter.ludic" },
|
||||
"2": { "name": "entity.name.type.scene.ludic" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "keyword.control.spawn.ludic" },
|
||||
"2": { "name": "entity.name.type.archetype.ludic" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"query": {
|
||||
"comment": "Inside a query's brackets a bare word names a component and {Word} is a filter that does not bind.",
|
||||
"begin": "\\b(query)\\s*(\\[)",
|
||||
"beginCaptures": {
|
||||
"1": { "name": "keyword.other.query.ludic" },
|
||||
"2": { "name": "punctuation.section.brackets.begin.ludic" }
|
||||
},
|
||||
"end": "(\\])",
|
||||
"endCaptures": { "1": { "name": "punctuation.section.brackets.end.ludic" } },
|
||||
"patterns": [
|
||||
{
|
||||
"match": "(\\{)\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*(\\})",
|
||||
"captures": {
|
||||
"1": { "name": "punctuation.section.braces.begin.ludic" },
|
||||
"2": { "name": "entity.name.type.tag.ludic" },
|
||||
"3": { "name": "punctuation.section.braces.end.ludic" }
|
||||
}
|
||||
},
|
||||
{ "name": "entity.name.type.component.ludic", "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b" },
|
||||
{ "name": "punctuation.separator.ludic", "match": "," }
|
||||
]
|
||||
},
|
||||
"keyword": {
|
||||
"patterns": [
|
||||
{ "name": "keyword.control.ludic", "match": "\\b(if|else|when|while|for|in|match|machine|become|enter|return|spawn|despawn|where|break|continue|new)\\b" },
|
||||
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
|
||||
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|reads|writes|needs|uses|requires|ensures|invariant|effects|on)\\b" },
|
||||
{ "name": "keyword.other.ludic", "match": "\\b(import|export|pure|extern|start|edge)\\b" },
|
||||
{ "name": "storage.type.ludic", "match": "\\b(game|module|component|archetype|ui|scene|layer|const|var|let|fn|system|state)\\b" },
|
||||
{ "name": "support.type.primitive.ludic", "match": "\\b(int|fixed|bool|entity|str|ptr|void)\\b" },
|
||||
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false)\\b" },
|
||||
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
|
||||
]
|
||||
},
|
||||
"widget": {
|
||||
"patterns": [
|
||||
{
|
||||
"comment": "widget types, recognised by the props or the child block that follow them",
|
||||
"name": "support.class.widget.ludic",
|
||||
"match": "\\b(panel|col|row|label|button|image|spacer)\\b(?=\\s*(\\{|[a-z][A-Za-z0-9_]*\\s*=|$))"
|
||||
},
|
||||
{
|
||||
"comment": "widget props are written tight (id=Root); an assignment is written x = 1",
|
||||
"match": "\\b([a-z][A-Za-z0-9_]*)(=)(?!=)",
|
||||
"captures": {
|
||||
"1": { "name": "variable.parameter.widget.ludic" },
|
||||
"2": { "name": "keyword.operator.assignment.ludic" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"builtin": {
|
||||
"patterns": [
|
||||
{
|
||||
"comment": "the runtime surface — every name here resolves to rt_<name> in runtime/native",
|
||||
"name": "support.function.builtin.ludic",
|
||||
"match": "\\b(min|max|abs|clamp|seed|rng_range|rng_chance|fx|flr|map_size|map_row|tile|clear|present|fill_rect|frame_rect|put_px|text|text_int|font_load|text_ttf|text_w|text_h|image_load|draw_image|draw_image_scaled|draw_9slice|load_png|load_sprites|draw_sprite|draw_sprite_scaled|ui_build|ui_open|ui_tick|ui_render|ui_clicked|ui_set_text|ui_set_int|ui_focus|ui_focused|ui_visible|key|reg|setreg|self|save|load|status|print_int|quit)\\b(?=\\s*\\()"
|
||||
},
|
||||
{
|
||||
"comment": "compiler intrinsics — these lower straight to libc or the OS",
|
||||
"name": "support.function.intrinsic.ludic",
|
||||
"match": "\\b(mem_alloc|mem_realloc|mem_free|mem_copy|mem_set|peek8|poke8|peek32|poke32|peekp|pokep|peekf|pokef|ptr_add|ptr_null|ptr_is_null|as_fixed|as_int|file_open|file_read|file_write|file_seek|file_tell|file_close|read_byte|write_byte|print_str|str_len|shl|shr|band|bor|bxor|bnot|os_exit|os_time|os_argc|os_arg|file_stderr|is_windowed|game_title|win_open|win_poll|win_present|win_running|win_close)\\b(?=\\s*\\()"
|
||||
}
|
||||
]
|
||||
},
|
||||
"member": {
|
||||
"match": "(\\.)([A-Za-z_][A-Za-z0-9_]*)",
|
||||
"captures": {
|
||||
"1": { "name": "punctuation.accessor.ludic" },
|
||||
"2": { "name": "variable.other.property.ludic" }
|
||||
}
|
||||
},
|
||||
"call": {
|
||||
"match": "\\b([A-Za-z_][A-Za-z0-9_]*)\\s*(?=\\()",
|
||||
"name": "entity.name.function.call.ludic"
|
||||
},
|
||||
"typename": {
|
||||
"comment": "components, archetypes, scenes and UI handles are Capitalised by convention",
|
||||
"name": "entity.name.type.ludic",
|
||||
"match": "\\b[A-Z][A-Za-z0-9_]*\\b"
|
||||
},
|
||||
"operator": {
|
||||
"patterns": [
|
||||
{ "name": "keyword.operator.arrow.ludic", "match": "->|=>" },
|
||||
{ "name": "keyword.operator.range.ludic", "match": "\\.\\." },
|
||||
{ "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" },
|
||||
{ "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" },
|
||||
{ "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue