fix(tools): formatter bracket rules, LSP compiler diagnostics, vocabulary sync

- ludic-fmt: `rows[i]`, `new []int`, `s[a..b]`, `emit(`, `~x`, list-literal
  and query-tag braces stay tight; member calls hug their paren
  (`Date.new(`, `Prefab.spawn(`); `<< >> & | ^ ~` are operators and
  `&& ||` are not — mirrored in ludic_syntax.h, LudicLexer.kt and the
  TextMate grammar. 149 of 274 tracked sources failed --check before.
- ludic-lsp: `initializationOptions.compilerDiagnostics` / `compilerPath`
  are honoured — on save the document's compilation unit is compiled and
  `file:line: error: msg` is published as a "ludicc" diagnostic (the
  option had been documented but never implemented); the workspace scan
  file is per process; bracket codes spelled as char literals
- Overlay added to LUDIC_PHASES, LudicTokens.kt, ludic-mode.el and the
  grammar (the game uses it; check-vocabulary flagged the drift)
- editors: VS Code snippets/package.json/extension.js (`${workspaceFolder}`
  and `~` expansion, PATH lookup, Apache-2.0, real repo URL), Neovim root
  markers, Helix/Zed/Sublime bin/ paths, Emacs vocabulary, JetBrains
  comments; tools/editors/README.md no longer describes C tooling
- .forgejo workflows clone `${{ github.server_url }}/${{ github.repository }}`
  so a fork or mirror tests itself; the docs publish pushes the same way

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-05 01:12:26 +03:00
parent 8fed9add66
commit e3e1bc7784
26 changed files with 746 additions and 586 deletions

View file

@ -1,20 +1,18 @@
# Ludic in your editor
Everything here is built on one idea: **write the language knowledge once, in C,
and let every editor talk to it.** There is one lexer, one vocabulary, one
Everything here is built on one idea: **write the language knowledge once, in
Ludic, and let every editor talk to it.** There is one vocabulary, one
formatter and one language server. VS Code and JetBrains get first-class plugins
because those are the two that were asked for; every other editor gets the same
capabilities by pointing at the same binary.
```
tools/ludic-tools/ the actual language knowledge, in C, no dependencies
ludic_syntax.h lexer + the vocabulary — the source of truth
ludic_fmt.h comment-preserving formatter
ludic_index.h error-tolerant reader: declarations, bindings, scopes
ludic_workspace.h multi-file model: imports, compilation units, Markdown
ludic_json.h just enough JSON for LSP
ludic_fmt_main.c -> bin/ludic-fmt
ludic_lsp.c -> bin/ludic-lsp
tools/ludic-tools/ the actual language knowledge, in Ludic
ludic_syntax.h the vocabulary tables (keywords, operators, builtins)
that the formatter, the server and the vocabulary
check all read — the source of truth
fmt.ludic -> bin/ludic-fmt token-based, comment-preserving formatter
lsp.ludic -> bin/ludic-lsp the language server (LSP 3.17)
tools/editors/
shared/ TextMate grammar + Markdown injection + language config

View file

@ -10,19 +10,23 @@
;;; Code:
;; The vocabulary mirrors tools/ludic-tools/ludic_syntax.h; that header is the reference.
(defconst ludic--declaration-keywords
'("program" "import" "property" "struct" "model" "enum" "ui"
"const" "var" "fn" "extern" "handler" "entry"))
'("program" "import" "property" "model" "enum" "ui" "namespace"
"const" "var" "function" "extern" "handler" "entry" "event" "scene" "test" "prefab"))
(defconst ludic--clause-keywords
'("phase" "query" "reads" "writes"))
'("phase" "query" "on" "cancellable" "public" "layer" "start" "shows" "lasts" "loads" "then" "export" "internal"))
(defconst ludic--statement-keywords
'("let" "return" "if" "else" "when" "while" "for" "in" "spawn" "despawn"
"match" "machine" "state" "become" "enter" "where" "and" "or"))
'("let" "return" "if" "else" "while" "for" "in" "spawn" "despawn"
"enable" "disable" "attach" "detach" "match" "machine" "state" "become" "where"
"and" "or" "not" "break" "continue" "new" "emit" "cancel" "try"))
(defconst ludic--types '("int" "fixed" "bool" "entity" "str" "ptr" "void"))
(defconst ludic--phases '("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render"))
(defconst ludic--types
'("int" "long" "fixed" "countdown" "bool" "entity" "string" "pointer" "byte"
"words" "fixeds" "pointers" "Vector" "IVec2" "Rect" "void"))
(defconst ludic--phases '("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render" "Overlay"))
(defconst ludic-font-lock-keywords
(list
@ -31,7 +35,7 @@
(cons (regexp-opt ludic--statement-keywords 'symbols) 'font-lock-keyword-face)
(cons (regexp-opt ludic--types 'symbols) 'font-lock-type-face)
(cons (regexp-opt ludic--phases 'symbols) 'font-lock-builtin-face)
(cons "\\_<\\(true\\|false\\)\\_>" 'font-lock-constant-face)
(cons "\\_<\\(true\\|false\\|null\\)\\_>" 'font-lock-constant-face)
(cons "@[A-Za-z_][A-Za-z0-9_]*" 'font-lock-preprocessor-face)
;; components, archetypes, scenes and UI handles are Capitalised by convention
(cons "\\_<[A-Z][A-Za-z0-9_]*\\_>" 'font-lock-type-face)

View file

@ -10,7 +10,7 @@ comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-servers = ["ludic-lsp"]
auto-format = true
roots = ["build.sh", "LANGUAGE.md", ".git"]
roots = ["package.ludic", "LANGUAGE.md", ".git"]
[language-server.ludic-lsp]
command = "ludic-lsp"

View file

@ -35,7 +35,7 @@ class LudicBraceMatcher : PairedBraceMatcher {
private companion object {
// Structural braces only: a record literal shares the token type, so
// marking them all structural would make the "matched brace" gutter
// noisy inside `spawn Hero { Pos = { x = 1 } }`.
// noisy inside `spawn Hero { Pos { x: 1 } }`.
val PAIRS = arrayOf(
BracePair(LudicTokens.BRACE, LudicTokens.BRACE, true),
BracePair(LudicTokens.BRACKET, LudicTokens.BRACKET, false),

View file

@ -7,7 +7,7 @@ import com.intellij.psi.tree.IElementType
/**
* A hand-written lexer matching tools/ludic-tools/ludic_syntax.h token for
* token. Hand-written rather than generated because the grammar is small, the
* rules are the ones the C lexer already implements, and keeping the two
* rules are the ones the shared header already implements, and keeping the two
* readable side by side is worth more here than a JFlex spec.
*
* It must never throw and never fail to advance: the IDE lexes half-typed text
@ -142,7 +142,7 @@ class LudicLexer : LexerBase() {
}
private companion object {
val TWO_CHAR_OPS = setOf("->", "+=", "-=", "*=", "/=", "==", "!=", "<=", ">=", "&&", "||", "..", "=>")
const val SINGLE_CHAR_OPS = "+-*/%<>=:.!@;"
val TWO_CHAR_OPS = setOf("->", "+=", "-=", "*=", "/=", "==", "!=", "<=", ">=", "<<", ">>", "..", "=>")
const val SINGLE_CHAR_OPS = "+-*/%<>=:.!@;&|^~"
}
}

View file

@ -17,7 +17,7 @@ import java.io.File
/**
* Where to find the toolchain. Both paths may be left blank, in which case the
* plugin looks under the project root for what tools/build-tools.sh produces —
* plugin looks under the project root for what `bin/x tools` produces —
* which is the case for anyone working in the Ludic repository itself.
*/
@Service(Service.Level.PROJECT)
@ -110,7 +110,7 @@ class LudicConnectionProvider(private val project: Project) : ProcessStreamConne
}
append(" and on PATH.\n")
}
append("\nBuild it with ./tools/build-tools.sh from the repository root, ")
append("\nBuild it with `bin/x tools` from the repository root, ")
append("or set the path in Settings | Languages & Frameworks | Ludic. ")
append("Syntax highlighting works without it; completion and diagnostics do not.")
}

View file

@ -20,8 +20,8 @@ object LudicTokens {
val IDENTIFIER = LudicTokenType("IDENTIFIER")
val TYPE_NAME = LudicTokenType("TYPE_NAME") // Capitalised: component, archetype, scene
val KEYWORD = LudicTokenType("KEYWORD")
val DECL_KEYWORD = LudicTokenType("DECL_KEYWORD") // game, component, system, function, ...
val CLAUSE = LudicTokenType("CLAUSE") // phase, query, reads, writes, ...
val DECL_KEYWORD = LudicTokenType("DECL_KEYWORD") // program, property, model, handler, function, ...
val CLAUSE = LudicTokenType("CLAUSE") // phase, query, on, layer, ...
val PRIMITIVE = LudicTokenType("PRIMITIVE") // int, fixed, bool, entity, string, pointer, void
val PHASE = LudicTokenType("PHASE") // Start, Update, Render, ...
val BOOLEAN = LudicTokenType("BOOLEAN")
@ -53,7 +53,7 @@ object LudicVocabulary {
"and", "or", "not", "break", "continue", "new", "emit", "cancel", "try"
)
val PRIMITIVES = setOf("int", "long", "fixed", "countdown", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "pointers", "Vector", "IVec2", "Rect", "void")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render")
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render", "Overlay")
val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer")
val BUILTINS = setOf(

View file

@ -67,7 +67,7 @@
name="Ludic"
factoryClass="io.ludic.ide.LudicLanguageServerFactory">
<description><![CDATA[
The Ludic language server. Built by <code>tools/build-tools.sh</code>.
The Ludic language server. Built by <code>bin/x tools</code>.
]]></description>
</server>
<languageMapping language="Ludic" serverId="ludic"/>

View file

@ -3,7 +3,7 @@
-- :lua require('ludic').setup()
-- -- or, from init.lua:
-- vim.opt.runtimepath:append('/path/to/gpp/tools/editors/neovim')
-- require('ludic').setup({ server = '/path/to/gpp/build/ludic-lsp' })
-- require('ludic').setup({ server = '/path/to/gpp/bin/ludic-lsp' })
--
-- Highlighting here comes from the language server's semantic tokens, which
-- Neovim applies natively -- there is no tree-sitter grammar to install and no
@ -43,7 +43,7 @@ function M.setup(opts)
end,
})
local root_markers = { 'build.sh', 'LANGUAGE.md', '.git' }
local root_markers = { 'package.ludic', 'LANGUAGE.md', '.git' }
local function root_dir(fname)
local found = vim.fs.find(root_markers, { path = vim.fs.dirname(fname), upward = true })[1]
@ -57,10 +57,10 @@ function M.setup(opts)
callback = function(args)
local root = root_dir(vim.api.nvim_buf_get_name(args.buf))
local server = opts.server or find_executable(
{ 'ludic-lsp' }, { root }, { 'build', 'tools/build', 'bin', '.' })
{ 'ludic-lsp' }, { root }, { 'bin', 'build', '.' })
if not server then return end
local compiler = opts.compiler or find_executable(
{ 'ludicc' }, { root }, { 'build', 'bin', '.' }) or ''
{ 'ludicc' }, { root }, { 'bin', 'build', '.' }) or ''
vim.lsp.start({
name = 'ludic-lsp',

View file

@ -12,7 +12,7 @@
],
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
"indentationRules": {
"increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
"decreaseIndentPattern": "^\\s*[}\\])].*$"
},
"onEnterRules": [

View file

@ -133,12 +133,20 @@
}
},
{
"match": "\\b(enter)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(become)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "keyword.control.enter.ludic" },
"1": { "name": "keyword.control.become.ludic" },
"2": { "name": "entity.name.type.scene.ludic" }
}
},
{
"match": "\\b(scene|prefab|event)\\s+(cancellable\\s+)?([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.ludic" },
"2": { "name": "keyword.other.clause.ludic" },
"3": { "name": "entity.name.type.ludic" }
}
},
{
"match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
@ -172,29 +180,29 @@
},
"keyword": {
"patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|prefab|become|return|spawn|despawn|enable|disable|where|break|continue|new|try)\\b" },
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|namespace|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|handler|entry|event|scene|state|test)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
]
},
"widget": {
"patterns": [
{
"comment": "widget types, recognised by the props or the child block that follow them",
"comment": "widget types, recognised by the `key: value` 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*=|$))"
"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_]*)(=)(?!=)",
"comment": "a widget prop or a named argument: `id: Root`, `x: 10`",
"match": "\\b([a-z][A-Za-z0-9_]*)(:)(?=\\s)",
"captures": {
"1": { "name": "variable.parameter.widget.ludic" },
"2": { "name": "keyword.operator.assignment.ludic" }
"1": { "name": "variable.parameter.ludic" },
"2": { "name": "punctuation.separator.ludic" }
}
}
]

View file

@ -20,10 +20,10 @@ Sublime reads TextMate grammars directly, so the shared grammar works as-is.
"clients": {
"ludic": {
"enabled": true,
"command": ["/path/to/gpp/build/ludic-lsp", "--stdio"],
"command": ["/path/to/gpp/bin/ludic-lsp", "--stdio"],
"selector": "source.ludic | text.html.markdown",
"initializationOptions": {
"compilerPath": "/path/to/gpp/build/ludicc",
"compilerPath": "/path/to/gpp/bin/ludicc",
"compilerDiagnostics": true,
"indentSize": 2
}

View file

@ -37,9 +37,9 @@ Format on save:
## Settings
- `ludic.server.path` — where `ludic-lsp` lives. Empty = search `bin/`, then `PATH`.
- `ludic.server.path` — where `ludic-lsp` lives (`~` and `${workspaceFolder}` expand). Empty = search `bin/`, then `PATH`.
- `ludic.server.enabled` — turn the server off and keep only static highlighting.
- `ludic.compilerPath` — where `ludicc` lives, for on-save diagnostics.
- `ludic.compilerPath` — where `ludicc` lives, for on-save diagnostics (`~` and `${workspaceFolder}` expand).
- `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`).

View file

@ -12,7 +12,7 @@
],
"surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]],
"indentationRules": {
"increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
"increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$",
"decreaseIndentPattern": "^\\s*[}\\])].*$"
},
"onEnterRules": [

View file

@ -4,7 +4,7 @@
"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",
"license": "Apache-2.0",
"engines": {
"vscode": "^1.75.0"
},
@ -90,7 +90,7 @@
"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`."
"markdownDescription": "Path to the `ludic-lsp` binary; `~` and `${workspaceFolder}` are expanded. Empty means: look in `bin/ludic-lsp` under each workspace folder, then on `PATH`."
},
"ludic.server.enabled": {
"type": "boolean",
@ -100,7 +100,7 @@
"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."
"markdownDescription": "Path to `ludicc`; `~` and `${workspaceFolder}` are expanded. The server runs it on save for the compiler's own diagnostics. Empty means `bin/ludicc` under the workspace, then `PATH`."
},
"ludic.compilerDiagnostics": {
"type": "boolean",
@ -153,6 +153,6 @@
},
"repository": {
"type": "git",
"url": "https://example.invalid/ludic"
"url": "https://git.workshopsoft.io/workshopsoft/ludic"
}
}

View file

@ -1,53 +1,52 @@
{
"game": {
"prefix": "game",
"body": ["game ${1:Name} {", "\t$0", "}"],
"description": "A game unit — compiles to an executable."
"program": {
"prefix": "program",
"body": ["program ${1:Name} {", "\t$0", "}"],
"description": "A program: one block of declarations that 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."
"property": {
"prefix": "property",
"body": ["property ${1:Name} { ${2:x}: ${3:int} = ${4:0} }"],
"description": "A property: typed fields with defaults — a per-entity component, or a record made with `new`."
},
"component": {
"prefix": "component",
"body": ["component ${1:Name} { ${2:x}: ${3:int} = ${4:0} }"],
"description": "A component: typed per-entity fields."
"model": {
"prefix": "model",
"body": ["model ${1:Name} { ${2:Position} }"],
"description": "An entity kind bundling a fixed set of properties."
},
"archetype": {
"prefix": "archetype",
"body": ["archetype ${1:Name} {", "\t${2:Pos}", "}"],
"description": "An entity kind bundling a fixed set of components."
"prefab": {
"prefix": "prefab",
"body": ["prefab ${1:Name}: ${2:Model} { ${3:Stats} { ${4:hp}: ${5:30} } }"],
"description": "A model with preset property fields; `spawn` adds only what differs."
},
"system": {
"prefix": "system",
"handler": {
"prefix": "handler",
"body": [
"system ${1:Name} phase ${2|Start,Input,FixedUpdate,Update,LateUpdate,Render|} {",
"handler ${1:Name} phase ${2|Start,Input,FixedUpdate,Update,LateUpdate,Render,Overlay|} {",
"\t$0",
"}"
],
"description": "A system that runs once per tick in its phase."
"description": "A handler that runs once per tick in its phase."
},
"system with query": {
"prefix": "systemq",
"handler with query": {
"prefix": "handlerq",
"body": [
"system ${1:Name} phase ${2|Update,FixedUpdate,LateUpdate,Render,Input|}",
" query (${3:p}) [${4:Pos}]",
"{",
"@Queries(these: [${1:Position}])",
"handler ${2:Name} phase ${3|Update,FixedUpdate,LateUpdate,Render,Input|} {",
"\t$0",
"}"
],
"description": "A system whose body runs once per matching entity."
"description": "A handler whose body runs once per matching entity, each property bound by name."
},
"for query": {
"prefix": "forq",
"body": ["for (${1:p}) in query [${2:Pos}] {", "\t$0", "}"],
"body": ["for (${1:p}) in query [${2:Position}] {", "\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."
"body": ["spawn ${1:Kind} { ${2:Position} { ${3:x}: ${4:0} } }"],
"description": "Create an entity of a model or prefab."
},
"scene": {
"prefix": "scene",
@ -59,18 +58,18 @@
"\t}",
"}"
],
"description": "A scene with lifecycle hooks and one layer of systems."
"description": "A scene with lifecycle hooks and one layer of handlers."
},
"ui": {
"prefix": "ui",
"body": [
"ui ${1:Name} {",
"\tpanel id=${2:Root} pad=16 gap=6 align=center {",
"\tpanel id: ${2:Root} pad: 16 gap: 6 align: center {",
"\t\t$0",
"\t}",
"}"
],
"description": "A retained UI tree."
"description": "A retained UI tree, declared as data."
},
"match": {
"prefix": "match",
@ -80,17 +79,32 @@
"machine": {
"prefix": "machine",
"body": [
"machine reg(${1:R_PHASE}) {",
"\tstate ${2:Idle} = 0 {",
"machine ${1:state} {",
"\tstate ${2:Idle} {",
"\t\t$0",
"\t}",
"}"
],
"description": "A state machine over a register."
"description": "A state machine over an enum-typed variable; `become` transitions."
},
"extern fn": {
"event": {
"prefix": "event",
"body": ["event ${1:Name} { ${2:amount}: int = 0 }"],
"description": "An event payload; fire it with `emit`, listen with `@On`."
},
"listener": {
"prefix": "on",
"body": ["@On(${1:Event}) handler ${2:Name} { $0 }"],
"description": "A listener on an event, its payload fields bound by name."
},
"namespace": {
"prefix": "namespace",
"body": ["namespace ${1:Name} {", "\texport function ${2:method}() -> ${3:void} { $0 }", "}"],
"description": "A block of functions callable as Name.method(); `internal` keeps a helper private."
},
"extern function": {
"prefix": "extern",
"body": ["extern fn ${1:name}(${2:a}: ${3:int}) -> ${4:int} = \"${5:c_symbol}\""],
"body": ["extern function ${1:name}(${2:a}: ${3:int}) -> ${4:int} = \"${5:c_symbol}\""],
"description": "Bind a C symbol through the FFI."
}
}

View file

@ -19,7 +19,7 @@ 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);
if (configured) return expand(configured, folders);
const names = ['ludic-lsp', 'ludic-lsp.exe'];
for (const folder of folders) {
@ -43,18 +43,26 @@ function resolveServer(folders) {
function resolveCompiler(folders) {
const configured = vscode.workspace.getConfiguration('ludic').get('compilerPath');
if (configured) return expand(configured);
if (configured) return expand(configured, folders);
for (const folder of folders) {
for (const rel of ['bin', 'build']) {
const p = path.join(folder.uri.fsPath, rel, 'ludicc');
if (isExecutable(p)) return p;
}
}
for (const dir of (process.env.PATH || '').split(path.delimiter)) {
const p = path.join(dir, 'ludicc');
if (isExecutable(p)) return p;
}
return '';
}
function expand(p) {
return p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p;
/** A configured path may use `~` and `${workspaceFolder}`, so a project can point
* at a sibling checkout without hard-coding one machine's home directory. */
function expand(p, folders) {
const root = folders && folders.length ? folders[0].uri.fsPath : '';
const withRoot = p.replace(/\$\{workspaceFolder\}/g, root);
return withRoot.startsWith('~') ? path.join(os.homedir(), withRoot.slice(1)) : withRoot;
}
function isExecutable(p) {

View file

@ -133,12 +133,20 @@
}
},
{
"match": "\\b(enter)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"match": "\\b(become)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "keyword.control.enter.ludic" },
"1": { "name": "keyword.control.become.ludic" },
"2": { "name": "entity.name.type.scene.ludic" }
}
},
{
"match": "\\b(scene|prefab|event)\\s+(cancellable\\s+)?([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
"1": { "name": "storage.type.ludic" },
"2": { "name": "keyword.other.clause.ludic" },
"3": { "name": "entity.name.type.ludic" }
}
},
{
"match": "\\b(spawn)\\s+([A-Za-z_][A-Za-z0-9_]*)",
"captures": {
@ -172,29 +180,29 @@
},
"keyword": {
"patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|prefab|become|return|spawn|despawn|enable|disable|where|break|continue|new|try)\\b" },
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|namespace|enum|ui|const|var|let|function|handler|entry|state|test)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|handler|entry|event|scene|state|test)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|countdown|bool|entity|string|pointer|byte|words|fixeds|pointers|Vector|IVec2|Rect|void)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b" }
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
]
},
"widget": {
"patterns": [
{
"comment": "widget types, recognised by the props or the child block that follow them",
"comment": "widget types, recognised by the `key: value` 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*=|$))"
"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_]*)(=)(?!=)",
"comment": "a widget prop or a named argument: `id: Root`, `x: 10`",
"match": "\\b([a-z][A-Za-z0-9_]*)(:)(?=\\s)",
"captures": {
"1": { "name": "variable.parameter.widget.ludic" },
"2": { "name": "keyword.operator.assignment.ludic" }
"1": { "name": "variable.parameter.ludic" },
"2": { "name": "punctuation.separator.ludic" }
}
}
]

View file

@ -1,9 +1,9 @@
# Ludic for Zed
Zed extensions want a Tree-sitter grammar for highlighting, which this toolchain
deliberately does not ship — the vocabulary lives in one C header and is exposed
through the language server's semantic tokens instead, so there is no second
grammar to keep honest.
deliberately does not ship — the vocabulary lives in one shared header and is
exposed through the language server's semantic tokens instead, so there is no
second grammar to keep honest.
That is enough for Zed: point it at `ludic-lsp` and highlighting, completion,
diagnostics and formatting all arrive over LSP.
@ -14,9 +14,9 @@ Add to `~/.config/zed/settings.json`:
{
"lsp": {
"ludic-lsp": {
"binary": { "path": "/path/to/gpp/build/ludic-lsp", "arguments": ["--stdio"] },
"binary": { "path": "/path/to/gpp/bin/ludic-lsp", "arguments": ["--stdio"] },
"initialization_options": {
"compilerPath": "/path/to/gpp/build/ludicc",
"compilerPath": "/path/to/gpp/bin/ludicc",
"compilerDiagnostics": true,
"indentSize": 2
}