diff --git a/.forgejo/workflows/bootstrap.yml b/.forgejo/workflows/bootstrap.yml index 7049ebba..cedd7096 100644 --- a/.forgejo/workflows/bootstrap.yml +++ b/.forgejo/workflows/bootstrap.yml @@ -25,10 +25,13 @@ jobs: clang-16 --version | head -1 - name: Check out the triggering commit + env: + # the repository that triggered the run, so a fork or a mirror tests itself + REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git run: | set -eu git config --global --add safe.directory '*' - git clone https://git.workshopsoft.io/workshopsoft/ludic.git . + git clone "$REPO_URL" . git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git log --oneline -1 # See ci.yml for why the Linux build injects the stdio shim via LUDIC_CC. diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index b09b91ba..4eb2b108 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -33,10 +33,13 @@ jobs: clang-16 --version | head -1 - name: Check out the triggering commit + env: + # the repository that triggered the run, so a fork or a mirror tests itself + REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git run: | set -eu git config --global --add safe.directory '*' - git clone https://git.workshopsoft.io/workshopsoft/ludic.git . + git clone "$REPO_URL" . git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" git log --oneline -1 # The toolchain is macOS-first; on this Linux runner it links against a diff --git a/.forgejo/workflows/commit-lint.yml b/.forgejo/workflows/commit-lint.yml index f5fbbc20..ff1c8c00 100644 --- a/.forgejo/workflows/commit-lint.yml +++ b/.forgejo/workflows/commit-lint.yml @@ -17,13 +17,12 @@ jobs: steps: - name: Check out with history env: - BEFORE: ${{ github.event.before }} - BASE: ${{ github.base_ref }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git run: | set -eu git config --global --add safe.directory '*' # Full clone so both endpoints of the range are present. - git clone https://git.workshopsoft.io/workshopsoft/ludic.git . + git clone "$REPO_URL" . git checkout "${GITHUB_SHA}" 2>/dev/null || git checkout "${GITHUB_REF_NAME:-main}" - name: Lint the new commits diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml index 2a5fb4ad..5061c2e5 100644 --- a/.forgejo/workflows/docs.yml +++ b/.forgejo/workflows/docs.yml @@ -41,11 +41,11 @@ jobs: - name: Generate the documentation site env: SOURCE_REF: ${{ github.ref_name }} + REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git run: | set -eu git config --global --add safe.directory '*' - git clone --depth 1 --branch "${SOURCE_REF:-main}" \ - https://git.workshopsoft.io/workshopsoft/ludic.git src + git clone --depth 1 --branch "${SOURCE_REF:-main}" "$REPO_URL" src cd src # The toolchain is macOS-first; on this Linux runner it links against a # tiny C-free IR shim supplying the Darwin stdout/stderr globals over @@ -67,6 +67,8 @@ jobs: PAGES_TOKEN: ${{ secrets.PAGES_TOKEN }} AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }} SOURCE_SHA: ${{ github.sha }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} run: | set -eu TOKEN="${PAGES_TOKEN:-${AUTO_TOKEN:-}}" @@ -80,5 +82,6 @@ jobs: git config user.email "docs@workshopsoft.io" git add -A git commit -q -m "docs: regenerate site from ${SOURCE_SHA}" - git push -f "https://ludic-docs-bot:${TOKEN}@git.workshopsoft.io/workshopsoft/ludic.git" pages + # the same server and repository the run came from, with the token spliced in + git push -f "${SERVER_URL%%://*}://ludic-docs-bot:${TOKEN}@${SERVER_URL#*://}/${REPO}.git" pages echo "published $(git rev-parse --short HEAD) to pages" diff --git a/tools/editors/README.md b/tools/editors/README.md index 24551939..234f507e 100644 --- a/tools/editors/README.md +++ b/tools/editors/README.md @@ -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 diff --git a/tools/editors/emacs/ludic-mode.el b/tools/editors/emacs/ludic-mode.el index 03394a92..08d9d0c7 100644 --- a/tools/editors/emacs/ludic-mode.el +++ b/tools/editors/emacs/ludic-mode.el @@ -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) diff --git a/tools/editors/helix/languages.toml b/tools/editors/helix/languages.toml index 47c8ea1e..1f24d54d 100644 --- a/tools/editors/helix/languages.toml +++ b/tools/editors/helix/languages.toml @@ -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" diff --git a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicEditorSupport.kt b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicEditorSupport.kt index 97195476..2513a688 100644 --- a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicEditorSupport.kt +++ b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicEditorSupport.kt @@ -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), diff --git a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicLexer.kt b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicLexer.kt index 1b18b173..23d31441 100644 --- a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicLexer.kt +++ b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicLexer.kt @@ -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 = "+-*/%<>=:.!@;&|^~" } } diff --git a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt index 1f99c8eb..642908df 100644 --- a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt +++ b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicServer.kt @@ -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.") } diff --git a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt index 67a547b6..74aed879 100644 --- a/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt +++ b/tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt @@ -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( diff --git a/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml b/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml index 95e31eb1..a1377298 100644 --- a/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml +++ b/tools/editors/jetbrains/src/main/resources/META-INF/plugin.xml @@ -67,7 +67,7 @@ name="Ludic" factoryClass="io.ludic.ide.LudicLanguageServerFactory"> tools/build-tools.sh. + The Ludic language server. Built by bin/x tools. ]]> diff --git a/tools/editors/neovim/lua/ludic.lua b/tools/editors/neovim/lua/ludic.lua index 67f12e38..bba41074 100644 --- a/tools/editors/neovim/lua/ludic.lua +++ b/tools/editors/neovim/lua/ludic.lua @@ -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', diff --git a/tools/editors/shared/language-configuration.json b/tools/editors/shared/language-configuration.json index 6fb5c01a..06f1fb7b 100644 --- a/tools/editors/shared/language-configuration.json +++ b/tools/editors/shared/language-configuration.json @@ -12,7 +12,7 @@ ], "surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]], "indentationRules": { - "increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$", + "increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$", "decreaseIndentPattern": "^\\s*[}\\])].*$" }, "onEnterRules": [ diff --git a/tools/editors/shared/ludic.tmLanguage.json b/tools/editors/shared/ludic.tmLanguage.json index 854c83b6..8333d319 100644 --- a/tools/editors/shared/ludic.tmLanguage.json +++ b/tools/editors/shared/ludic.tmLanguage.json @@ -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" } } } ] diff --git a/tools/editors/sublime/README.md b/tools/editors/sublime/README.md index 129a1eba..a2fd3edb 100644 --- a/tools/editors/sublime/README.md +++ b/tools/editors/sublime/README.md @@ -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 } diff --git a/tools/editors/vscode/README.md b/tools/editors/vscode/README.md index eb427231..31a650e9 100644 --- a/tools/editors/vscode/README.md +++ b/tools/editors/vscode/README.md @@ -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`). diff --git a/tools/editors/vscode/language-configuration.json b/tools/editors/vscode/language-configuration.json index 6fb5c01a..06f1fb7b 100644 --- a/tools/editors/vscode/language-configuration.json +++ b/tools/editors/vscode/language-configuration.json @@ -12,7 +12,7 @@ ], "surroundingPairs": [["{", "}"], ["[", "]"], ["(", ")"], ["\"", "\""]], "indentationRules": { - "increaseIndentPattern": "^((?!//).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$", + "increaseIndentPattern": "^((?!#).)*(\\{[^}\"']*|\\([^)\"']*|\\[[^\\]\"']*)$", "decreaseIndentPattern": "^\\s*[}\\])].*$" }, "onEnterRules": [ diff --git a/tools/editors/vscode/package.json b/tools/editors/vscode/package.json index 96d2273d..83323581 100644 --- a/tools/editors/vscode/package.json +++ b/tools/editors/vscode/package.json @@ -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" } } diff --git a/tools/editors/vscode/snippets/ludic.json b/tools/editors/vscode/snippets/ludic.json index 2bcd3a36..c300bd78 100644 --- a/tools/editors/vscode/snippets/ludic.json +++ b/tools/editors/vscode/snippets/ludic.json @@ -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." } } diff --git a/tools/editors/vscode/src/extension.js b/tools/editors/vscode/src/extension.js index b9207771..47f5c11b 100644 --- a/tools/editors/vscode/src/extension.js +++ b/tools/editors/vscode/src/extension.js @@ -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) { diff --git a/tools/editors/vscode/syntaxes/ludic.tmLanguage.json b/tools/editors/vscode/syntaxes/ludic.tmLanguage.json index 854c83b6..8333d319 100644 --- a/tools/editors/vscode/syntaxes/ludic.tmLanguage.json +++ b/tools/editors/vscode/syntaxes/ludic.tmLanguage.json @@ -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" } } } ] diff --git a/tools/editors/zed/README.md b/tools/editors/zed/README.md index 052796fa..e19f35ac 100644 --- a/tools/editors/zed/README.md +++ b/tools/editors/zed/README.md @@ -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 } diff --git a/tools/ludic-tools/fmt.ludic b/tools/ludic-tools/fmt.ludic index 618cee8b..80087513 100644 --- a/tools/ludic-tools/fmt.ludic +++ b/tools/ludic-tools/fmt.ludic @@ -40,27 +40,27 @@ program LudicFmt { file_close(f) return buf } - function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n } - function char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 } + function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n } + function char_is_digit(c: int) -> bool { return c >= '0' and c <= '9' } function char_is_alpha(c: int) -> bool { - if c >= 65 and c <= 90 { return true } - if c >= 97 and c <= 122 { return true } - return c == 95 + if c >= 'A' and c <= 'Z' { return true } + if c >= 'a' and c <= 'z' { return true } + return c == '_' } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } - function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) } + function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F') } function itoa(v: int) -> pointer { - if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z } + if v == 0 { let z = bytes(2); z[0] = '0'; z[1] = 0; return z } var neg = false; var x = v - if x < 0 { neg = true; x = 0 - x } + if x < 0 { neg = true; x = -x } let tmp = bytes(16); var n = 0 - while x > 0 { tmp[n] = 48 + x % 10; x = x / 10; n = n + 1 } + while x > 0 { tmp[n] = 48 + x % 10; x /= 10; n += 1 } var total = n - if neg { total = total + 1 } + if neg { total += 1 } let out = bytes(total + 1); var k = 0 - if neg { out[0] = 45; k = 1 } + if neg { out[0] = '-'; k = 1 } var i = 0 - while i < n { out[k + i] = tmp[n - 1 - i]; i = i + 1 } + while i < n { out[k + i] = tmp[n - 1 - i]; i += 1 } out[total] = 0 return out } @@ -70,15 +70,15 @@ program LudicFmt { function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b } function buf_ensure(b: Buf, extra: int) -> void { if b.len + extra + 1 <= b.cap { return } - while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 } + while b.len + extra + 1 > b.cap { b.cap *= 2 } b.data = resize(b.data, b.cap) } - function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 } - function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } } - function buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, 32); i = i + 1 } } + function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len += 1 } + function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i += 1 } } + function buf_indent(b: Buf, n: int) -> void { var i = 0; while i < n { buf_putc(b, ' '); i += 1 } } function buf_str(b: Buf) -> pointer { b.data[b.len] = 0; return b.data } # append src[a..b) raw - function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } } + function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k += 1 } } # ---- the token stream (parallel slices) ---- var src: pointer = null @@ -117,16 +117,19 @@ program LudicFmt { # ---- the lexer: keeps comments, newlines, byte spans; never exits on bad input ---- function is_op2(c0: int, c1: int) -> bool { - if c0 == 45 and c1 == 62 { return true } # -> - if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } # += -= *= /= == != <= >= - if c0 == 38 and c1 == 38 { return true } # && - if c0 == 124 and c1 == 124 { return true } # || - if c0 == 46 and c1 == 46 { return true } # .. - if c0 == 61 and c1 == 62 { return true } # => + if c0 == '-' and c1 == '>' { return true } # -> + if c1 == '=' and (c0 == '+' or c0 == '-' or c0 == '*' or c0 == '/' or c0 == '=' or c0 == '!' or c0 == '<' or c0 == '>') { return true } # += -= *= /= == != <= >= + if c0 == '<' and c1 == '<' { return true } # << + if c0 == '>' and c1 == '>' { return true } # >> + if c0 == '.' and c1 == '.' { return true } # .. + if c0 == '=' and c1 == '>' { return true } # => return false } function is_op1(c: int) -> bool { - return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59 + if c == '+' or c == '-' or c == '*' or c == '/' or c == '%' or c == '<' or c == '>' or c == '=' { return true } # + - * / % < > = + if c == '(' or c == ')' or c == '{' or c == '}' or c == '[' or c == ']' { return true } # ( ) { } [ ] + if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ; + return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~ } function lex(s: pointer) -> void { @@ -137,45 +140,45 @@ program LudicFmt { var i = 0; var line = 0 while s[i] != 0 { let c = s[i] - if c == 10 { push_tok(LT_NL, i, i + 1, line); i = i + 1; line = line + 1; push(linestart, i); continue } - if c == 32 or c == 9 or c == 13 { i = i + 1; continue } - if c == 35 { # '#' comment to end of line - let st = i; while s[i] != 0 and s[i] != 10 { i = i + 1 }; push_tok(LT_COMMENT, st, i, line); continue + if c == '\n' { push_tok(LT_NL, i, i + 1, line); i += 1; line += 1; push(linestart, i); continue } + if c == ' ' or c == '\t' or c == '\r' { i += 1; continue } + if c == '#' { # '#' comment to end of line + let st = i; while s[i] != 0 and s[i] != '\n' { i += 1 }; push_tok(LT_COMMENT, st, i, line); continue } - if c == 34 { # "string" - let st = i; i = i + 1 - while s[i] != 0 and s[i] != 34 and s[i] != 10 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } } - if s[i] == 34 { i = i + 1 } + if c == '"' { # "string" + let st = i; i += 1 + while s[i] != 0 and s[i] != '"' and s[i] != '\n' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } } + if s[i] == '"' { i += 1 } push_tok(LT_STR, st, i, line); continue } - if c == 96 { # `interpolated` - let st = i; i = i + 1 - while s[i] != 0 and s[i] != 96 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } } - if s[i] == 96 { i = i + 1 } + if c == '`' { # `interpolated` + let st = i; i += 1 + while s[i] != 0 and s[i] != '`' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } } + if s[i] == '`' { i += 1 } push_tok(LT_STR, st, i, line); continue } - if c == 39 { # 'c' - let st = i; i = i + 1 - if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { if s[i] != 0 and s[i] != 10 { i = i + 1 } } - if s[i] == 39 { i = i + 1 } + if c == '\'' { # 'c' + let st = i; i += 1 + if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { if s[i] != 0 and s[i] != '\n' { i += 1 } } + if s[i] == '\'' { i += 1 } push_tok(LT_CHAR, st, i, line); continue } if char_is_digit(c) { let st = i - if c == 48 and (s[i + 1] == 120 or s[i + 1] == 88) { # 0x hex - i = i + 2; while char_is_hex(s[i]) { i = i + 1 }; push_tok(LT_INT, st, i, line); continue + if c == '0' and (s[i + 1] == 'x' or s[i + 1] == 'X') { # 0x hex + i += 2; while char_is_hex(s[i]) { i += 1 }; push_tok(LT_INT, st, i, line); continue } - while char_is_digit(s[i]) { i = i + 1 } - if s[i] == 46 and char_is_digit(s[i + 1]) { - i = i + 1; while char_is_digit(s[i]) { i = i + 1 }; push_tok(LT_FLOAT, st, i, line); continue + while char_is_digit(s[i]) { i += 1 } + if s[i] == '.' and char_is_digit(s[i + 1]) { + i += 1; while char_is_digit(s[i]) { i += 1 }; push_tok(LT_FLOAT, st, i, line); continue } push_tok(LT_INT, st, i, line); continue } - if c == 64 and (char_is_alpha(s[i + 1]) or s[i + 1] == 95) { # @name annotation - let st = i; i = i + 1; while char_is_alnum(s[i]) { i = i + 1 }; push_tok(LT_ANNO, st, i, line); continue + if c == '@' and (char_is_alpha(s[i + 1]) or s[i + 1] == '_') { # @name annotation + let st = i; i += 1; while char_is_alnum(s[i]) { i += 1 }; push_tok(LT_ANNO, st, i, line); continue } if char_is_alpha(c) { - let st = i; while char_is_alnum(s[i]) { i = i + 1 } + let st = i; while char_is_alnum(s[i]) { i += 1 } let w = s[st..i] var k = LT_ID if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL } @@ -184,14 +187,14 @@ program LudicFmt { else { if is_keyword_word(w) { k = LT_KW } } } } push_tok(k, st, i, line); continue } - if is_op2(c, s[i + 1]) { push_tok(LT_OP, i, i + 2, line); i = i + 2; continue } - if is_op1(c) { push_tok(LT_OP, i, i + 1, line); i = i + 1; continue } + if is_op2(c, s[i + 1]) { push_tok(LT_OP, i, i + 2, line); i += 2; continue } + if is_op1(c) { push_tok(LT_OP, i, i + 1, line); i += 1; continue } # anything else: one UTF-8 character's worth as an LT_ERR token var ln = 1 if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } } var kk = 1 - while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk = kk + 1 } - push_tok(LT_ERR, i, i + ln, line); i = i + ln + while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk += 1 } + push_tok(LT_ERR, i, i + ln, line); i += ln } push_tok(LT_EOF, i, i, line) } @@ -199,36 +202,41 @@ program LudicFmt { # ---- token-stream helpers ---- function next_sig(i: int) -> int { var j = i + 1 - while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 } - return 0 - 1 + while j < ntok() { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j += 1 } + return -1 } function prev_sig(i: int) -> int { var j = i - 1 - while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 } - return 0 - 1 + while j >= 0 { let k = tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j -= 1 } + return -1 } function name_like(k: int) -> bool { return k == LT_ID or k == LT_KW or k == LT_TYPE or k == LT_PHASE or k == LT_BOOL } + # is token i written right after a '.', i.e. a member/method name? + function is_member(i: int) -> bool { + let p = prev_sig(i) + return p >= 0 and tk_kind[p] == LT_OP and tok_len(p) == 1 and src[tk_start[p]] == '.' + } # a '.' hugs an operand, a closing bracket, and another dot function dot_tight(t: int) -> bool { if name_like(tk_kind[t]) { return true } if tk_kind[t] != LT_OP { return false } let n = tok_len(t); let c0 = src[tk_start[t]] - if n == 1 and (c0 == 41 or c0 == 93 or c0 == 46) { return true } - if n == 2 and c0 == 46 and src[tk_start[t] + 1] == 46 { return true } + if n == 1 and (c0 == ')' or c0 == ']' or c0 == '.') { return true } + if n == 2 and c0 == '.' and src[tk_start[t] + 1] == 46 { return true } return false } - # is the token at index i a unary '-'/'!' rather than a binary operator? + # is the token at index i a unary '-' / '!' / '~' rather than a binary operator? function is_unary(i: int) -> bool { if tk_kind[i] != LT_OP { return false } let n = tok_len(i) - if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33)) { return false } + if not (n == 1 and (src[tk_start[i]] == 45 or src[tk_start[i]] == 33 or src[tk_start[i]] == 126)) { return false } let p = prev_sig(i) if p < 0 { return true } let pk = tk_kind[p] if pk == LT_ID or pk == LT_INT or pk == LT_FLOAT or pk == LT_STR or pk == LT_CHAR or pk == LT_BOOL or pk == LT_TYPE or pk == LT_PHASE { return false } if pk == LT_OP { let c = src[tk_start[p]] - return not (tok_len(p) == 1 and (c == 41 or c == 93 or c == 125)) # a closing bracket ends an operand + return not (tok_len(p) == 1 and (c == ')' or c == ']' or c == '}')) # a closing bracket ends an operand } return true # keyword/annotation/comment: operand starts here } @@ -241,14 +249,26 @@ program LudicFmt { let p0 = src[tk_start[prev]]; let c0 = src[tk_start[cur]] let p1 = (plen == 1); let c1 = (clen == 1) if pk == LT_ERR or ck == LT_ERR { return tk_start[cur] - tk_end[prev] } # preserve an error token's spacing - if c1 and (c0 == 41 or c0 == 93 or c0 == 44 or c0 == 58 or c0 == 59) { return 0 } # ) ] , : ; - if c1 and c0 == 46 and ck == LT_OP and dot_tight(prev) { return 0 } - if p1 and p0 == 46 and pk == LT_OP and dot_tight(cur) { return 0 } - if p1 and (p0 == 40 or p0 == 91) and pk == LT_OP { return 0 } # nothing hugs an opener from the right + if c1 and (c0 == ')' or c0 == ']' or c0 == ',' or c0 == ':' or c0 == ';') { return 0 } # ) ] , : ; + if c1 and c0 == '.' and ck == LT_OP and dot_tight(prev) { return 0 } + if p1 and p0 == '.' and pk == LT_OP and dot_tight(cur) { return 0 } + if p1 and (p0 == '(' or p0 == '[') and pk == LT_OP { return 0 } # nothing hugs an opener from the right + # an index or slice hugs its operand: a[i], f()[0], a[i][j]; `query [` and `= [` keep their space + if c1 and c0 == '[' and ck == LT_OP { + if pk == LT_ID or pk == LT_STR { return 0 } + if pk == LT_OP and p1 and (p0 == ')' or p0 == ']') { return 0 } + } + # the element type hugs an empty pair of brackets: `[]int`, `[]Node` + if p1 and p0 == ']' and pk == LT_OP and (ck == LT_ID or ck == LT_TYPE) { + let pp = prev_sig(prev) + if pp >= 0 and tk_kind[pp] == LT_OP and tok_len(pp) == 1 and src[tk_start[pp]] == 91 { return 0 } + } if pk == LT_OP and is_unary(prev) { return 0 } - if pk == LT_ANNO and c1 and c0 == 40 { return 0 } # @anno( - if c1 and c0 == 40 and ck == LT_OP { + if pk == LT_ANNO and c1 and c0 == '(' { return 0 } # @anno( + if c1 and c0 == '(' and ck == LT_OP { if pk == LT_ID or pk == LT_TYPE or pk == LT_PHASE { return 0 } # function move( / clear( + if pk == LT_KW and (tok_text(prev) == "emit") { return 0 } # emit(...) is a call, `emit E(...)` an event + if pk == LT_KW and is_member(prev) { return 0 } # Prefab.spawn( / Date.new( — a member name is never a keyword if pk == LT_OP { return 1 } # `= (`, `+ (`, `) (` keep a space return 1 } @@ -264,7 +284,7 @@ program LudicFmt { var cur = 0 var open = 0 var brack = 0 - var ui_depth = 0 - 1 + var ui_depth = -1 var pending_blank = 0 var wrote_any = 0 var prev_line_had_comment = 0 @@ -272,9 +292,9 @@ program LudicFmt { var i = 0 while i < N and tk_kind[i] != LT_EOF { let a = i - while i < N and tk_kind[i] != LT_NL and tk_kind[i] != LT_EOF { i = i + 1 } + while i < N and tk_kind[i] != LT_NL and tk_kind[i] != LT_EOF { i += 1 } let b = i - if i < N and tk_kind[i] == LT_NL { i = i + 1 } + if i < N and tk_kind[i] == LT_NL { i += 1 } if a == b { # a blank line if wrote_any == 1 { pending_blank = 1 } @@ -286,13 +306,13 @@ program LudicFmt { var tsp = sp var t = a while t < b and tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { # leading '}' - if tsp > 0 { tsp = tsp - 1; line_level = stack[tsp] } - t = t + 1 + if tsp > 0 { tsp -= 1; line_level = stack[tsp] } + t += 1 } # original column of this line var orig_ind = 0 var kk = linestart[tk_line[a]] - while kk < tk_start[a] { if src[kk] == 9 { orig_ind = orig_ind + 4 } else { orig_ind = orig_ind + 1 }; kk = kk + 1 } + while kk < tk_start[a] { if src[kk] == '\t' { orig_ind += 4 } else { orig_ind += 1 }; kk += 1 } # a comment continuing the previous comment line keeps the author's column var comment_run = 0 if tk_kind[a] == LT_COMMENT and b == a + 1 and prev_line_had_comment == 1 { comment_run = 1 } @@ -304,7 +324,7 @@ program LudicFmt { let ls = linestart[tk_line[a]] ind = 0 var k2 = ls - while k2 < tk_start[a] { if src[k2] == 9 { ind = ind + 4 } else { ind = ind + 1 }; k2 = k2 + 1 } + while k2 < tk_start[a] { if src[k2] == '\t' { ind += 4 } else { ind += 1 }; k2 += 1 } } else { var extra = 0 if is_clause_word(tok_text(a)) and tk_kind[a] == LT_KW { extra = indent_width } @@ -312,13 +332,14 @@ program LudicFmt { if comment_run == 1 and orig_ind > ind { ind = orig_ind } } - if pending_blank == 1 and wrote_any == 1 { buf_putc(o, 10) } + if pending_blank == 1 and wrote_any == 1 { buf_putc(o, '\n') } pending_blank = 0 buf_indent(o, ind) # emit the tokens - var prev = 0 - 1 + var prev = -1 var line_brack = brack + var in_tag = 0 # inside a query's {Tag} filter var line_ui_open = 0 if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 } t = a @@ -327,9 +348,18 @@ program LudicFmt { if tk_kind[t] == LT_COMMENT { if prev >= 0 { want = 2 } else { want = 0 } } else { want = space_before(prev, t) - if line_brack > 0 { # query {Tag} filter is one word - if tk_kind[t] == LT_OP and tok_len(t) == 1 and src[tk_start[t]] == 125 { want = 0 } - if prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and src[tk_start[prev]] == 123 { want = 0 } + # a query's {Tag} filter is one word: a `{` straight after `[` or `,` opens one + # (a record literal `new R { … }` inside a list keeps its spaces) + if line_brack > 0 and tk_kind[t] == LT_OP and tok_len(t) == 1 { + let c = src[tk_start[t]] + if c == '{' and prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and (src[tk_start[prev]] == '[' or src[tk_start[prev]] == ',') { in_tag = 1 } + if c == '}' and in_tag == 1 { want = 0; in_tag = 0 } + } + if in_tag == 1 and prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 1 and src[tk_start[prev]] == '{' { want = 0 } + # a slice range hugs its bounds, `s[a..b]`; a `for i in a .. b` range keeps its spaces + if line_brack > 0 { + if tk_kind[t] == LT_OP and tok_len(t) == 2 and src[tk_start[t]] == 46 and src[tk_start[t] + 1] == 46 { want = 0 } + if prev >= 0 and tk_kind[prev] == LT_OP and tok_len(prev) == 2 and src[tk_start[prev]] == 46 and src[tk_start[prev] + 1] == 46 { want = 0 } } if line_ui_open == 1 { # widget props are k=v var eq_here = 0 @@ -344,17 +374,17 @@ program LudicFmt { if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap } # hand alignment wins if want < 0 { want = 0 } var w2 = 0 - while w2 < want { buf_putc(o, 32); w2 = w2 + 1 } + while w2 < want { buf_putc(o, ' '); w2 += 1 } buf_addrange(o, src, tk_start[t], tk_end[t]) prev = t if tk_kind[t] == LT_OP and tok_len(t) == 1 { let c = src[tk_start[t]] - if c == 91 { line_brack = line_brack + 1 } - else { if c == 93 { line_brack = line_brack - 1; if line_brack < 0 { line_brack = 0 } } } + if c == '[' { line_brack += 1 } + else { if c == ']' { line_brack -= 1; if line_brack < 0 { line_brack = 0 } } } } - t = t + 1 + t += 1 } - buf_putc(o, 10) + buf_putc(o, '\n') wrote_any = 1 prev_line_had_comment = 0 if prev >= 0 and tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 } @@ -366,25 +396,25 @@ program LudicFmt { while t < b { if tk_kind[t] == LT_OP and tok_len(t) == 1 { let c = src[tk_start[t]] - if c == 123 { # '{' + if c == '{' { # '{' if sp < 512 { let nxt = next_sig(t) stack[sp] = line_level var h = 0 if nxt >= 0 and nxt < b { h = 1 } hang[sp] = h - sp = sp + 1 + sp += 1 } level = line_level + 1 } - else { if c == 125 { if sp > 0 { sp = sp - 1; level = stack[sp] } } - else { if c == 40 or c == 91 { open = open + 1; if c == 91 { brack = brack + 1 } } - else { if c == 41 or c == 93 { open = open - 1; if open < 0 { open = 0 }; if c == 93 { brack = brack - 1; if brack < 0 { brack = 0 } } } } } } + else { if c == '}' { if sp > 0 { sp -= 1; level = stack[sp] } } + else { if c == '(' or c == '[' { open += 1; if c == '[' { brack += 1 } } + else { if c == ')' or c == ']' { open -= 1; if open < 0 { open = 0 }; if c == ']' { brack -= 1; if brack < 0 { brack = 0 } } } } } } } - t = t + 1 + t += 1 } cur = level - if ui_depth >= 0 and sp <= ui_depth { ui_depth = 0 - 1 } + if ui_depth >= 0 and sp <= ui_depth { ui_depth = -1 } } return o } @@ -395,22 +425,22 @@ program LudicFmt { var g_marker: int = 0 # detect a fence at line offset i; sets g_flen/g_info/g_marker; returns bool function md_fence_at(s: pointer, i: int) -> bool { - var j = i; while s[j] == 32 { j = j + 1 } + var j = i; while s[j] == ' ' { j += 1 } let m = s[j] - if m != 96 and m != 126 { return false } # ` or ~ - var n = 0; while s[j] == m { j = j + 1; n = n + 1 } + if m != '`' and m != '~' { return false } # ` or ~ + var n = 0; while s[j] == m { j += 1; n += 1 } if n < 3 { return false } g_flen = n; g_info = j; g_marker = m return true } function md_info_is_ludic(s: pointer, at: int) -> bool { - var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 } + var a = at; while s[a] == ' ' or s[a] == '\t' { a += 1 } # case-insensitive "ludic" - if not (((s[a] == 108 or s[a] == 76)) and ((s[a + 1] == 117 or s[a + 1] == 85)) and ((s[a + 2] == 100 or s[a + 2] == 68)) and ((s[a + 3] == 105 or s[a + 3] == 73)) and ((s[a + 4] == 99 or s[a + 4] == 67))) { return false } + if not (((s[a] == 'l' or s[a] == 'L')) and ((s[a + 1] == 'u' or s[a + 1] == 'U')) and ((s[a + 2] == 'd' or s[a + 2] == 'D')) and ((s[a + 3] == 'i' or s[a + 3] == 'I')) and ((s[a + 4] == 'c' or s[a + 4] == 'C'))) { return false } let af = s[a + 5] - return af == 0 or af == 10 or af == 32 or af == 9 or af == 13 + return af == 0 or af == '\n' or af == ' ' or af == '\t' or af == '\r' } - function line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e } + function line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != '\n' { e += 1 }; return e } function format_markdown(s: pointer, indent_width: int) -> Buf { let o = buf_new() @@ -418,7 +448,7 @@ program LudicFmt { while s[i] != 0 { let ls = i let le = line_end(s, ls) - var indent = 0; while s[ls + indent] == 32 { indent = indent + 1 } + var indent = 0; while s[ls + indent] == ' ' { indent += 1 } if md_fence_at(s, ls) and md_info_is_ludic(s, g_info) { # copy the opening fence line verbatim (with its newline) var e0 = le; if s[le] != 0 { e0 = le + 1 } @@ -437,7 +467,7 @@ program LudicFmt { if g_marker == cmark and g_flen >= clen { var only = 1 var k = g_info - while k < pe { if s[k] != 32 and s[k] != 13 { only = 0; break }; k = k + 1 } + while k < pe { if s[k] != ' ' and s[k] != '\r' { only = 0; break }; k += 1 } if only == 1 { be = ps; break } } } @@ -450,9 +480,9 @@ program LudicFmt { while p < be { var q = line_end(s, p) var skip = 0 - while skip < indent and (p + skip) < q and s[p + skip] == 32 { skip = skip + 1 } + while skip < indent and (p + skip) < q and s[p + skip] == ' ' { skip += 1 } buf_addrange(body, s, p + skip, q) - buf_putc(body, 10) + buf_putc(body, '\n') if s[q] != 0 { p = q + 1 } else { p = q } } # feed the body through the ludic formatter @@ -461,10 +491,10 @@ program LudicFmt { let ftext = buf_str(f) var fp = 0 while ftext[fp] != 0 { - var fq = fp; while ftext[fq] != 0 and ftext[fq] != 10 { fq = fq + 1 } + var fq = fp; while ftext[fq] != 0 and ftext[fq] != '\n' { fq += 1 } if fq > fp { buf_indent(o, indent) } buf_addrange(o, ftext, fp, fq) - buf_putc(o, 10) + buf_putc(o, '\n') if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq } } i = be @@ -516,10 +546,10 @@ program LudicFmt { if (a == "-w") or (a == "--write") { write = true } else { if (a == "--check") or (a == "-l") { check = true } else { if (a == "-q") or (a == "--quiet") { quiet = true } - else { if (a == "--indent") { ai = ai + 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di = di + 1 } } } + else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } } else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source"); return } else { push(files, a) } } } } } - ai = ai + 1 + ai += 1 } if indent < 1 or indent > 8 { indent = 2 } @@ -555,7 +585,7 @@ program LudicFmt { file_write(file_stdout(), out, cstr_len(out)) } } } - fi = fi + 1 + fi += 1 } if failed { exit(2) } if check and changed { exit(1) } diff --git a/tools/ludic-tools/lsp.ludic b/tools/ludic-tools/lsp.ludic index eb21531c..af5736a7 100644 --- a/tools/ludic-tools/lsp.ludic +++ b/tools/ludic-tools/lsp.ludic @@ -84,36 +84,36 @@ program LudicLsp { file_close(f) return buf } - function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n = n + 1 }; return n } - function char_is_digit(c: int) -> bool { return c >= 48 and c <= 57 } + function cstr_len(s: pointer) -> int { var n = 0; while s[n] != 0 { n += 1 }; return n } + function char_is_digit(c: int) -> bool { return c >= '0' and c <= '9' } function char_is_alpha(c: int) -> bool { - if c >= 65 and c <= 90 { return true } - if c >= 97 and c <= 122 { return true } - return c == 95 + if c >= 'A' and c <= 'Z' { return true } + if c >= 'a' and c <= 'z' { return true } + return c == '_' } function char_is_alnum(c: int) -> bool { return char_is_alpha(c) or char_is_digit(c) } - function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 97 and c <= 102) or (c >= 65 and c <= 70) } + function char_is_hex(c: int) -> bool { return char_is_digit(c) or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F') } function itoa(v: int) -> pointer { - if v == 0 { let z = bytes(2); z[0] = 48; z[1] = 0; return z } + if v == 0 { let z = bytes(2); z[0] = '0'; z[1] = 0; return z } var neg = false; var x = v - if x < 0 { neg = true; x = 0 - x } + if x < 0 { neg = true; x = -x } let tmp = bytes(16); var n = 0 - while x > 0 { tmp[n] = 48 + x % 10; x = x / 10; n = n + 1 } + while x > 0 { tmp[n] = 48 + x % 10; x /= 10; n += 1 } var total = n - if neg { total = total + 1 } + if neg { total += 1 } let out = bytes(total + 1); var k = 0 - if neg { out[0] = 45; k = 1 } + if neg { out[0] = '-'; k = 1 } var i = 0 - while i < n { out[k + i] = tmp[n - 1 - i]; i = i + 1 } + while i < n { out[k + i] = tmp[n - 1 - i]; i += 1 } out[total] = 0 return out } function atoi(s: pointer) -> int { var n = 0; var i = 0; var neg = false - while s[i] == 32 or s[i] == 9 { i = i + 1 } - if s[i] == 45 { neg = true; i = i + 1 } - while char_is_digit(s[i]) { n = n * 10 + (s[i] - 48); i = i + 1 } - if neg { return 0 - n } + while s[i] == ' ' or s[i] == '\t' { i += 1 } + if s[i] == '-' { neg = true; i += 1 } + while char_is_digit(s[i]) { n = n * 10 + (s[i] - 48); i += 1 } + if neg { return -n } return n } @@ -122,13 +122,13 @@ program LudicLsp { function buf_new() -> Buf { let b = new Buf; b.cap = 256; b.data = bytes(b.cap); b.len = 0; return b } function buf_ensure(b: Buf, extra: int) -> void { if b.len + extra + 1 <= b.cap { return } - while b.len + extra + 1 > b.cap { b.cap = b.cap * 2 } + while b.len + extra + 1 > b.cap { b.cap *= 2 } b.data = resize(b.data, b.cap) } - function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len = b.len + 1 } - function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i = i + 1 } } + function buf_putc(b: Buf, c: int) -> void { buf_ensure(b, 1); b.data[b.len] = c; b.len += 1 } + function buf_puts(b: Buf, s: pointer) -> void { var i = 0; while s[i] != 0 { buf_putc(b, s[i]); i += 1 } } function buf_puti(b: Buf, n: int) -> void { buf_puts(b, itoa(n)) } - function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k = k + 1 } } + function buf_addrange(b: Buf, s: pointer, a: int, e: int) -> void { var k = a; while k < e { buf_putc(b, s[k]); k += 1 } } function buf_str(b: Buf) -> pointer { b.data[b.len] = 0; return b.data } # ---- string helpers ---- @@ -184,24 +184,32 @@ program LudicLsp { had_diags: int = 0 isopen: int = 0 version: int = 0 + cc_line: int = 0 # the compiler's last error on this file (1-based line), or 0 + cc_msg: pointer = null } var g_docs: []Doc var g_root: pointer = null var g_shutdown: int = 0 + # initializationOptions.compilerPath — the ludicc to run on every save when + # compilerDiagnostics is on (default: the ludicc beside this binary). null = off. + var g_compiler: pointer = null # ---- the lexer, over a Doc ---- function is_op2(c0: int, c1: int) -> bool { - if c0 == 45 and c1 == 62 { return true } - if c1 == 61 and (c0 == 43 or c0 == 45 or c0 == 42 or c0 == 47 or c0 == 61 or c0 == 33 or c0 == 60 or c0 == 62) { return true } - if c0 == 38 and c1 == 38 { return true } - if c0 == 124 and c1 == 124 { return true } - if c0 == 46 and c1 == 46 { return true } - if c0 == 61 and c1 == 62 { return true } + if c0 == '-' and c1 == '>' { return true } + if c1 == '=' and (c0 == '+' or c0 == '-' or c0 == '*' or c0 == '/' or c0 == '=' or c0 == '!' or c0 == '<' or c0 == '>') { return true } + if c0 == '<' and c1 == '<' { return true } # << + if c0 == '>' and c1 == '>' { return true } # >> + if c0 == '.' and c1 == '.' { return true } + if c0 == '=' and c1 == '>' { return true } return false } function is_op1(c: int) -> bool { - return c == 43 or c == 45 or c == 42 or c == 47 or c == 37 or c == 60 or c == 62 or c == 61 or c == 40 or c == 41 or c == 123 or c == 125 or c == 91 or c == 93 or c == 44 or c == 58 or c == 46 or c == 33 or c == 64 or c == 59 + if c == '+' or c == '-' or c == '*' or c == '/' or c == '%' or c == '<' or c == '>' or c == '=' { return true } # + - * / % < > = + if c == '(' or c == ')' or c == '{' or c == '}' or c == '[' or c == ']' { return true } # ( ) { } [ ] + if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ; + return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~ } function is_type_word(w: pointer) -> bool { return (w == "int") or (w == "long") or (w == "fixed") or (w == "countdown") or (w == "bool") or (w == "entity") or (w == "string") or (w == "pointer") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "pointers") or (w == "Vector") or (w == "IVec2") or (w == "Rect") or (w == "void") @@ -230,22 +238,22 @@ program LudicLsp { var i = 0; var line = 0 while s[i] != 0 { let c = s[i] - if c == 10 { push(D.tk_kind, LT_NL); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i = i + 1; line = line + 1; push(D.linestart, i); continue } - if c == 32 or c == 9 or c == 13 { i = i + 1; continue } + if c == '\n' { push(D.tk_kind, LT_NL); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i += 1; line += 1; push(D.linestart, i); continue } + if c == ' ' or c == '\t' or c == '\r' { i += 1; continue } var st = i - if c == 35 { while s[i] != 0 and s[i] != 10 { i = i + 1 }; push(D.tk_kind, LT_COMMENT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } - if c == 34 { i = i + 1; while s[i] != 0 and s[i] != 34 and s[i] != 10 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } }; if s[i] == 34 { i = i + 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } - if c == 96 { i = i + 1; while s[i] != 0 and s[i] != 96 { if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { i = i + 1 } }; if s[i] == 96 { i = i + 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } - if c == 39 { i = i + 1; if s[i] == 92 and s[i + 1] != 0 { i = i + 2 } else { if s[i] != 0 and s[i] != 10 { i = i + 1 } }; if s[i] == 39 { i = i + 1 }; push(D.tk_kind, LT_CHAR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + if c == '#' { while s[i] != 0 and s[i] != '\n' { i += 1 }; push(D.tk_kind, LT_COMMENT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + if c == '"' { i += 1; while s[i] != 0 and s[i] != '"' and s[i] != '\n' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }; if s[i] == '"' { i += 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + if c == '`' { i += 1; while s[i] != 0 and s[i] != '`' { if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { i += 1 } }; if s[i] == '`' { i += 1 }; push(D.tk_kind, LT_STR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + if c == '\'' { i += 1; if s[i] == '\\' and s[i + 1] != 0 { i += 2 } else { if s[i] != 0 and s[i] != '\n' { i += 1 } }; if s[i] == '\'' { i += 1 }; push(D.tk_kind, LT_CHAR); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if char_is_digit(c) { - if c == 48 and (s[i + 1] == 120 or s[i + 1] == 88) { i = i + 2; while char_is_hex(s[i]) { i = i + 1 }; push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } - while char_is_digit(s[i]) { i = i + 1 } - if s[i] == 46 and char_is_digit(s[i + 1]) { i = i + 1; while char_is_digit(s[i]) { i = i + 1 }; push(D.tk_kind, LT_FLOAT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + if c == '0' and (s[i + 1] == 'x' or s[i + 1] == 'X') { i += 2; while char_is_hex(s[i]) { i += 1 }; push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + while char_is_digit(s[i]) { i += 1 } + if s[i] == '.' and char_is_digit(s[i + 1]) { i += 1; while char_is_digit(s[i]) { i += 1 }; push(D.tk_kind, LT_FLOAT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } push(D.tk_kind, LT_INT); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } - if c == 64 and (char_is_alpha(s[i + 1]) or s[i + 1] == 95) { i = i + 1; while char_is_alnum(s[i]) { i = i + 1 }; push(D.tk_kind, LT_ANNO); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } + if c == '@' and (char_is_alpha(s[i + 1]) or s[i + 1] == '_') { i += 1; while char_is_alnum(s[i]) { i += 1 }; push(D.tk_kind, LT_ANNO); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } if char_is_alpha(c) { - while char_is_alnum(s[i]) { i = i + 1 } + while char_is_alnum(s[i]) { i += 1 } let w = s[st..i] var k = LT_ID if (w == "true") or (w == "false") or (w == "null") { k = LT_BOOL } @@ -254,13 +262,13 @@ program LudicLsp { else { if is_keyword_word(w) { k = LT_KW } } } } push(D.tk_kind, k); push(D.tk_start, st); push(D.tk_end, i); push(D.tk_line, line); continue } - if is_op2(c, s[i + 1]) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 2); push(D.tk_line, line); i = i + 2; continue } - if is_op1(c) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i = i + 1; continue } + if is_op2(c, s[i + 1]) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 2); push(D.tk_line, line); i += 2; continue } + if is_op1(c) { push(D.tk_kind, LT_OP); push(D.tk_start, i); push(D.tk_end, i + 1); push(D.tk_line, line); i += 1; continue } var ln = 1 if c >= 240 { ln = 4 } else { if c >= 224 { ln = 3 } else { if c >= 128 { ln = 2 } } } var kk = 1 - while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk = kk + 1 } - push(D.tk_kind, LT_ERR); push(D.tk_start, i); push(D.tk_end, i + ln); push(D.tk_line, line); i = i + ln + while kk < ln { if s[i + kk] == 0 or (s[i + kk] & 192) != 128 { ln = kk }; kk += 1 } + push(D.tk_kind, LT_ERR); push(D.tk_start, i); push(D.tk_end, i + ln); push(D.tk_line, line); i += ln } push(D.tk_kind, LT_EOF); push(D.tk_start, i); push(D.tk_end, i); push(D.tk_line, line) } @@ -269,18 +277,18 @@ program LudicLsp { function tlen(D: Doc, i: int) -> int { return D.tk_end[i] - D.tk_start[i] } function ttext(D: Doc, i: int) -> pointer { return D.src[D.tk_start[i]..D.tk_end[i]] } function tis(D: Doc, i: int, s: pointer) -> bool { if i < 0 or i >= ntok(D) { return false }; return (ttext(D, i) == s) } - function next_sig(D: Doc, i: int) -> int { var j = i + 1; while j < ntok(D) { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j + 1 }; return 0 - 1 } - function prev_sig(D: Doc, i: int) -> int { var j = i - 1; while j >= 0 { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j = j - 1 }; return 0 - 1 } + function next_sig(D: Doc, i: int) -> int { var j = i + 1; while j < ntok(D) { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j += 1 }; return -1 } + function prev_sig(D: Doc, i: int) -> int { var j = i - 1; while j >= 0 { let k = D.tk_kind[j]; if k != LT_NL and k != LT_COMMENT { return j }; j -= 1 }; return -1 } # ---- positions (UTF-16 for LSP) ---- function utf16_len(s: pointer, a: int, nbytes: int) -> int { var u = 0; var i = 0 while i < nbytes { let c = s[a + i] - if c < 128 { i = i + 1; u = u + 1 } - else { if c < 224 { i = i + 2; u = u + 1 } - else { if c < 240 { i = i + 3; u = u + 1 } - else { i = i + 4; u = u + 2 } } } + if c < 128 { i += 1; u += 1 } + else { if c < 224 { i += 2; u += 1 } + else { if c < 240 { i += 3; u += 1 } + else { i += 4; u += 2 } } } } return u } @@ -299,15 +307,15 @@ program LudicLsp { var u = 0 while off < endo and u < ch { let c = D.text[off] - if c < 128 { off = off + 1; u = u + 1 } - else { if c < 224 { off = off + 2; u = u + 1 } - else { if c < 240 { off = off + 3; u = u + 1 } - else { off = off + 4; u = u + 2 } } } + if c < 128 { off += 1; u += 1 } + else { if c < 224 { off += 2; u += 1 } + else { if c < 240 { off += 3; u += 1 } + else { off += 4; u += 2 } } } } return off } function tok_at(D: Doc, off: int) -> int { - var touching = 0 - 1 + var touching = -1 var i = 0 let N = ntok(D) while i < N { @@ -317,7 +325,7 @@ program LudicLsp { if off == D.tk_end[i] and touching < 0 { touching = i } if D.tk_start[i] > off { return touching } } - i = i + 1 + i += 1 } return touching } @@ -327,28 +335,28 @@ program LudicLsp { let N = ntok(D) D.tmatch = new []int var i = 0 - while i < N { push(D.tmatch, 0 - 1); i = i + 1 } + while i < N { push(D.tmatch, -1); i += 1 } let stack = words(1200) var top = 0 i = 0 while i < N { if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 { let c = D.src[D.tk_start[i]] - if c == 123 or c == 40 or c == 91 { if top < 1200 { stack[top] = i; top = top + 1 } } - else { if c == 125 or c == 41 or c == 93 { if top > 0 { top = top - 1; let o = stack[top]; D.tmatch[o] = i; D.tmatch[i] = o } } } + if c == '{' or c == '(' or c == '[' { if top < 1200 { stack[top] = i; top += 1 } } + else { if c == '}' or c == ')' or c == ']' { if top > 0 { top -= 1; let o = stack[top]; D.tmatch[o] = i; D.tmatch[i] = o } } } } - i = i + 1 + i += 1 } } # ---- doc comment: the comment block directly above a declaration ---- function collect_doc(D: Doc, decl_tok: int) -> pointer { var line = D.tk_line[decl_tok] - var first = 0 - 1 + var first = -1 var i = decl_tok - 1 while i >= 0 { let k = D.tk_kind[i] - if k == LT_NL { i = i - 1; continue } + if k == LT_NL { i -= 1; continue } if k != LT_COMMENT { break } if D.tk_line[i] >= line { break } let p = prev_sig(D, i) @@ -356,7 +364,7 @@ program LudicLsp { if D.tk_line[i] < line - 1 and first >= 0 { break } if first >= 0 and D.tk_line[first] != D.tk_line[i] + 1 { break } first = i; line = D.tk_line[i] - i = i - 1 + i -= 1 } if first < 0 { return "" } let out = buf_new() @@ -364,11 +372,11 @@ program LudicLsp { while i < decl_tok { if D.tk_kind[i] == LT_COMMENT { var s = D.tk_start[i] + 1 - while s < D.tk_end[i] and (D.text[s] == 32 or D.text[s] == 9 or D.text[s] == 35 or D.text[s] == 42) { s = s + 1 } + while s < D.tk_end[i] and (D.text[s] == ' ' or D.text[s] == '\t' or D.text[s] == '#' or D.text[s] == '*') { s += 1 } buf_addrange(out, D.text, s, D.tk_end[i]) - buf_putc(out, 10) + buf_putc(out, '\n') } - i = i + 1 + i += 1 } return buf_str(out) } @@ -381,13 +389,13 @@ program LudicLsp { function pk() -> int { if pi < ntok(pD) { return pD.tk_kind[pi] }; return LT_EOF } function pis(s: pointer) -> bool { return tis(pD, pi, s) } - function pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi = pi + 1 } } - function padv() -> void { if pi < ntok(pD) { pi = pi + 1 }; pskipnl() } + function pskipnl() -> void { while pi < ntok(pD) and (pk() == LT_NL or pk() == LT_COMMENT) { pi += 1 } } + function padv() -> void { if pi < ntok(pD) { pi += 1 }; pskipnl() } function pword() -> pointer { if pi < ntok(pD) { return ttext(pD, pi) }; return "" } function pname() -> int { let k = pk() if k == LT_ID or k == LT_TYPE or k == LT_PHASE or k == LT_KW or k == LT_BOOL { let t = pi; padv(); return t } - return 0 - 1 + return -1 } function sym_add(kind: int, nametok: int, parent: int) -> int { let s = new Sym @@ -398,14 +406,14 @@ program LudicLsp { push(pD.syms, s) return len(pD.syms) - 1 } - function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return 0 - 1 } + function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 } function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() } # `name: Type` pairs inside ( ) — record params, build the signature into sig function lp_params(owner: int, kind: int, sig: Buf) -> void { if not pis("(") { return } let close = cur_match() - buf_putc(sig, 40) # '(' + buf_putc(sig, '(') # '(' padv() var first = true while pi < ntok(pD) and (close < 0 or pi < close) { @@ -424,7 +432,7 @@ program LudicLsp { } if close >= 0 { pi = close + 1 } pskipnl() - buf_putc(sig, 41) # ')' + buf_putc(sig, ')') # ')' } # (a, b) variable list -> token indices into out; returns count @@ -438,7 +446,7 @@ program LudicLsp { if pis(",") { padv(); continue } let nt = pname() if nt < 0 { padv(); continue } - push(out, nt); n = n + 1 + push(out, nt); n += 1 } if close >= 0 { pi = close + 1 } pskipnl() @@ -463,7 +471,7 @@ program LudicLsp { pD.syms[sidx].bstart = scope_start; pD.syms[sidx].bend = scope_end pD.syms[sidx].detail = concat3(pD.syms[sidx].name, ": ", concat3(pD.syms[sidx].ty, " (query binding)", "")) } - bind = bind + 1 + bind += 1 } if close >= 0 { pi = close + 1 } pskipnl() @@ -592,7 +600,7 @@ program LudicLsp { pD.syms[sidx].doc = collect_doc(pD, decl_tok) while pk() == LT_ANNO { padv() } let det = buf_new() - buf_puts(det, w); buf_putc(det, 32); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {") + buf_puts(det, w); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {") if not pis("{") { pD.syms[sidx].detail = buf_str(det); return } let close = cur_match() pD.syms[sidx].bstart = pD.tk_start[pi] @@ -611,11 +619,11 @@ program LudicLsp { pD.syms[f].ty = fty pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty)) if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } } - if not first { buf_putc(det, 44) } - buf_putc(det, 32); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty) + if not first { buf_putc(det, ',') } + buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty) } else { - if not first { buf_putc(det, 44) } - buf_putc(det, 32); buf_puts(det, ttext(pD, ft)) + if not first { buf_putc(det, ',') } + buf_putc(det, ' '); buf_puts(det, ttext(pD, ft)) } first = false } @@ -633,7 +641,7 @@ program LudicLsp { pD.syms[sidx].doc = collect_doc(pD, decl_tok) if pis(":") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt) } } pD.syms[sidx].detail = concat3(w, " ", concat3(pD.syms[sidx].name, ": ", pD.syms[sidx].ty)) - while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { pi = pi + 1 } } + while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { pi += 1 } } pskipnl() return } @@ -646,7 +654,7 @@ program LudicLsp { lp_params(sidx, LS_PARAM, sig) if pis("->") { padv(); let tt = pname(); if tt >= 0 { pD.syms[sidx].ty = ttext(pD, tt); buf_puts(sig, " -> "); buf_puts(sig, pD.syms[sidx].ty) } } pD.syms[sidx].detail = buf_str(sig) - while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { pi = pi + 1 } + while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF { pi += 1 } pskipnl() return } @@ -665,7 +673,7 @@ program LudicLsp { pD.syms[sidx].detail = buf_str(sig) while pis("requires") or pis("ensures") or pis("invariant") or pis("effects") { padv() - while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF and not pis("{") { if pis("(") or pis("[") { skip_group() } else { pi = pi + 1 } } + while pi < ntok(pD) and pk() != LT_NL and pk() != LT_EOF and not pis("{") { if pis("(") or pis("[") { skip_group() } else { pi += 1 } } pskipnl() } if pis("{") { @@ -673,7 +681,7 @@ program LudicLsp { pD.syms[sidx].bstart = pD.tk_start[pi] if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) } var q = 0 - while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q = q + 1 } + while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 } lp_block(sidx, pD.syms[sidx].bend) } return @@ -706,8 +714,8 @@ program LudicLsp { if have_query { let save = pi var scan = pi - while scan < ntok(pD) and not tis(pD, scan, "{") and pD.tk_kind[scan] != LT_EOF { scan = scan + 1 } - var cl = 0 - 1 + while scan < ntok(pD) and not tis(pD, scan, "{") and pD.tk_kind[scan] != LT_EOF { scan += 1 } + var cl = -1 if scan < ntok(pD) { cl = pD.tmatch[scan] } var bs = 0 if scan < ntok(pD) { bs = pD.tk_start[scan] } @@ -767,7 +775,7 @@ program LudicLsp { if pis("on") { padv(); let x = pname(); lp_block(sidx, pD.syms[sidx].bend) } else { if pis("layer") { padv(); let lt = pname() - var lsidx = 0 - 1 + var lsidx = -1 if lt >= 0 { lsidx = sym_add(LS_LAYER, lt, sidx); pD.syms[lsidx].detail = concat3("layer ", pD.syms[lsidx].name, concat3(" of scene ", pD.syms[sidx].name, "")) } if pis("{") { let lc = cur_match() @@ -801,14 +809,14 @@ program LudicLsp { D.is_unit = 0; D.is_module = 0; D.unit = "" pD = D; pi = 0 pskipnl() - while pis("import") { lp_decl(0 - 1); pskipnl() } + while pis("import") { lp_decl(-1); pskipnl() } if pis("program") { D.is_module = 1; D.is_unit = 1 padv() let nt = pname() - var up = 0 - 1 + var up = -1 if nt >= 0 { - up = sym_add(LS_UNIT, nt, 0 - 1) + up = sym_add(LS_UNIT, nt, -1) D.unit = pD.syms[up].name pD.syms[up].detail = concat3("program ", D.unit, "") } @@ -825,7 +833,7 @@ program LudicLsp { return } while pi < ntok(pD) and pk() != LT_EOF { - let before = pi; lp_decl(0 - 1) + let before = pi; lp_decl(-1) if pi == before { padv() } } } @@ -843,10 +851,10 @@ program LudicLsp { # ---- paths & URIs ---- function hexval(c: int) -> int { - if c >= 48 and c <= 57 { return c - 48 } + if c >= '0' and c <= '9' { return c - 48 } let l = c | 32 if l >= 97 and l <= 102 { return l - 97 + 10 } - return 0 - 1 + return -1 } function uri_to_path(uri: pointer) -> pointer { var s = uri @@ -855,15 +863,15 @@ program LudicLsp { let out = bytes(n + 1) var j = 0; var i = 0 while i < n { - if s[i] == 37 and hexval(s[i + 1]) >= 0 and hexval(s[i + 2]) >= 0 { out[j] = hexval(s[i + 1]) * 16 + hexval(s[i + 2]); j = j + 1; i = i + 3 } - else { out[j] = s[i]; j = j + 1; i = i + 1 } + if s[i] == '%' and hexval(s[i + 1]) >= 0 and hexval(s[i + 2]) >= 0 { out[j] = hexval(s[i + 1]) * 16 + hexval(s[i + 2]); j += 1; i += 3 } + else { out[j] = s[i]; j += 1; i += 1 } } out[j] = 0 return out } function uri_safe(c: int) -> bool { if char_is_alnum(c) { return true } - return c == 45 or c == 46 or c == 126 or c == 47 # - . ~ / + return c == '-' or c == '.' or c == '~' or c == '/' # - . ~ / } function hexch(n: int) -> int { if n < 10 { return 48 + n }; return 55 + n } function path_to_uri(path: pointer) -> pointer { @@ -873,100 +881,100 @@ program LudicLsp { while path[i] != 0 { let c = path[i] if uri_safe(c) { buf_putc(out, c) } - else { buf_putc(out, 37); buf_putc(out, hexch((c & 255) / 16)); buf_putc(out, hexch((c & 255) % 16)) } - i = i + 1 + else { buf_putc(out, '%'); buf_putc(out, hexch((c & 255) / 16)); buf_putc(out, hexch((c & 255) % 16)) } + i += 1 } return buf_str(out) } function dirname(p: pointer) -> pointer { - var last = 0 - 1 + var last = -1 var i = 0 - while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 } + while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 } if last < 0 { return "." } if last == 0 { return "/" } return p[0..last] } function normpath(p: pointer) -> pointer { let n = cstr_len(p) - let abs = (n > 0 and p[0] == 47) + let abs = (n > 0 and p[0] == '/') let list = new []pointer var cnt = 0 var i = 0 while i < n { - while i < n and p[i] == 47 { i = i + 1 } + while i < n and p[i] == '/' { i += 1 } let st = i - while i < n and p[i] != 47 { i = i + 1 } + while i < n and p[i] != '/' { i += 1 } if i > st { let c = p[st..i] if (c == ".") { } else { if (c == "..") { - if cnt > 0 and not (list[cnt - 1] == "..") { cnt = cnt - 1 } - else { if not abs { if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt = cnt + 1 } } + if cnt > 0 and not (list[cnt - 1] == "..") { cnt -= 1 } + else { if not abs { if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt += 1 } } } else { - if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt = cnt + 1 + if cnt < len(list) { list[cnt] = c } else { push(list, c) }; cnt += 1 } } } } if cnt == 0 { if abs { return "/" }; return "." } let out = buf_new() - if abs { buf_putc(out, 47) } + if abs { buf_putc(out, '/') } var k = 0 - while k < cnt { if k > 0 { buf_putc(out, 47) }; buf_puts(out, list[k]); k = k + 1 } + while k < cnt { if k > 0 { buf_putc(out, '/') }; buf_puts(out, list[k]); k += 1 } return buf_str(out) } function join_path(dir: pointer, rel: pointer) -> pointer { - if rel[0] == 47 { return normpath(rel) } + if rel[0] == '/' { return normpath(rel) } return normpath(concat3(dir, "/", rel)) } # ---- markdown scrub: blank everything outside a ```ludic fence ---- function word_ludic_at(s: pointer, at: int) -> bool { - return ((s[at] == 108 or s[at] == 76)) and ((s[at + 1] == 117 or s[at + 1] == 85)) and ((s[at + 2] == 100 or s[at + 2] == 68)) and ((s[at + 3] == 105 or s[at + 3] == 73)) and ((s[at + 4] == 99 or s[at + 4] == 67)) + return ((s[at] == 'l' or s[at] == 'L')) and ((s[at + 1] == 'u' or s[at + 1] == 'U')) and ((s[at + 2] == 'd' or s[at + 2] == 'D')) and ((s[at + 3] == 'i' or s[at + 3] == 'I')) and ((s[at + 4] == 'c' or s[at + 4] == 'C')) } function scrub_markdown(src: pointer) -> pointer { let n = cstr_len(src) let out = bytes(n + 1) var c0 = 0 - while c0 < n { out[c0] = src[c0]; c0 = c0 + 1 } + while c0 < n { out[c0] = src[c0]; c0 += 1 } out[n] = 0 var i = 0 while i < n { let ls = i var le = i - while le < n and src[le] != 10 { le = le + 1 } + while le < n and src[le] != '\n' { le += 1 } var ind = 0 - while ls + ind < le and src[ls + ind] == 32 { ind = ind + 1 } + while ls + ind < le and src[ls + ind] == ' ' { ind += 1 } let j = ls + ind var m = 0 if j < le { m = src[j] } var run = 0 - while j + run < le and src[j + run] == m { run = run + 1 } - let fenced = (m == 96 or m == 126) and run >= 3 + while j + run < le and src[j + run] == m { run += 1 } + let fenced = (m == '`' or m == '~') and run >= 3 let info = j + run var is_ludic = false if fenced { var kk = info - while kk < le and (src[kk] == 32 or src[kk] == 9) { kk = kk + 1 } - if le - kk >= 5 { if word_ludic_at(src, kk) and (le - kk == 5 or src[kk + 5] == 32 or src[kk + 5] == 9 or src[kk + 5] == 13) { is_ludic = true } } + while kk < le and (src[kk] == ' ' or src[kk] == '\t') { kk += 1 } + if le - kk >= 5 { if word_ludic_at(src, kk) and (le - kk == 5 or src[kk + 5] == ' ' or src[kk + 5] == '\t' or src[kk + 5] == '\r') { is_ludic = true } } } var b = ls - while b < le { out[b] = 32; b = b + 1 } + while b < le { out[b] = ' '; b += 1 } i = le + 1; if le >= n { i = n } if not fenced { continue } while i < n { let bs = i var be = i - while be < n and src[be] != 10 { be = be + 1 } + while be < n and src[be] != '\n' { be += 1 } var bi = 0 - while bs + bi < be and src[bs + bi] == 32 { bi = bi + 1 } + while bs + bi < be and src[bs + bi] == ' ' { bi += 1 } let cj = bs + bi var crun = 0 - while cj + crun < be and src[cj + crun] == m { crun = crun + 1 } + while cj + crun < be and src[cj + crun] == m { crun += 1 } var only = true var k2 = cj + crun - while k2 < be { if src[k2] != 32 and src[k2] != 13 { only = false; break }; k2 = k2 + 1 } - if crun >= run and only { var bb = bs; while bb < be { out[bb] = 32; bb = bb + 1 }; i = be + 1; if be >= n { i = n }; break } - if not is_ludic { var bb = bs; while bb < be { out[bb] = 32; bb = bb + 1 } } + while k2 < be { if src[k2] != ' ' and src[k2] != '\r' { only = false; break }; k2 += 1 } + if crun >= run and only { var bb = bs; while bb < be { out[bb] = ' '; bb += 1 }; i = be + 1; if be >= n { i = n }; break } + if not is_ludic { var bb = bs; while bb < be { out[bb] = ' '; bb += 1 } } i = be + 1; if be >= n { i = n } } } @@ -986,7 +994,7 @@ program LudicLsp { D.cls = new []int var i = 0 let N = ntok(D) - while i < N { push(D.cls, SC_NONE); i = i + 1 } + while i < N { push(D.cls, SC_NONE); i += 1 } } function new_doc(path: pointer, text: pointer) -> Doc { let D = new Doc @@ -1002,7 +1010,7 @@ program LudicLsp { function by_path(path: pointer) -> Doc { let p = normpath(path) var i = 0 - while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i = i + 1 } + while i < len(g_docs) { if (g_docs[i].path == p) { return g_docs[i] }; i += 1 } return null } function by_uri(uri: pointer) -> Doc { @@ -1010,7 +1018,7 @@ program LudicLsp { let D = by_path(p) if (D != null) { return D } var i = 0 - while i < len(g_docs) { if (g_docs[i].uri == uri) { return g_docs[i] }; i = i + 1 } + while i < len(g_docs) { if (g_docs[i].uri == uri) { return g_docs[i] }; i += 1 } return null } function ensure_doc(path: pointer) -> Doc { @@ -1025,14 +1033,14 @@ program LudicLsp { function scan_root(root: pointer) -> void { if (root == null) { return } - let listfile = "/tmp/ludic_lsp_scan.txt" + let listfile = `{Os.temp_dir()}/ludic_lsp_scan_{Os.pid()}.txt` # per process: editors run several servers run(concat3(concat3("find '", root, "' -name '*.ludic' -not -path '*/build/*' -not -path '*/node_modules/*' -not -path '*/.*' > "), listfile, " 2>/dev/null")) let list = read_file(listfile) if (list == null) { return } var i = 0; var start = 0 while list[i] != 0 { - if list[i] == 10 { if i > start { let d = ensure_doc(list[start..i]) }; start = i + 1 } - i = i + 1 + if list[i] == '\n' { if i > start { let d = ensure_doc(list[start..i]) }; start = i + 1 } + i += 1 } if i > start { let d = ensure_doc(list[start..i]) } } @@ -1049,10 +1057,10 @@ program LudicLsp { if (I != null) { var seen = false var k = 0 - while k < len(out) { if (out[k].path == I.path) { seen = true }; k = k + 1 } + while k < len(out) { if (out[k].path == I.path) { seen = true }; k += 1 } if not seen { push(out, I); imports_of(I, out) } } - i = i + 1 + i += 1 } } function related(D: Doc) -> []Doc { @@ -1068,23 +1076,23 @@ program LudicLsp { imports_of(R, reach) var hits = false var k = 0 - while k < len(reach) { if (reach[k].path == D.path) { hits = true }; k = k + 1 } + while k < len(reach) { if (reach[k].path == D.path) { hits = true }; k += 1 } if hits { var seen = false k = 0 - while k < len(out) { if (out[k].path == R.path) { seen = true }; k = k + 1 } + while k < len(out) { if (out[k].path == R.path) { seen = true }; k += 1 } if not seen { push(out, R) } k = 0 while k < len(reach) { var s2 = false var q = 0 - while q < len(out) { if (out[q].path == reach[k].path) { s2 = true }; q = q + 1 } + while q < len(out) { if (out[q].path == reach[k].path) { s2 = true }; q += 1 } if not s2 { push(out, reach[k]) } - k = k + 1 + k += 1 } } } - i = i + 1 + i += 1 } return out } @@ -1093,18 +1101,18 @@ program LudicLsp { if D.is_unit == 1 { return D } let rel = related(D) var i = 0 - while i < len(rel) { if rel[i].is_unit == 1 and rel[i].is_md == 0 { return rel[i] }; i = i + 1 } + while i < len(rel) { if rel[i].is_unit == 1 and rel[i].is_md == 0 { return rel[i] }; i += 1 } return null } function find_local(D: Doc, name: pointer, off: int) -> int { - var best = 0 - 1 + var best = -1 var i = 0 while i < len(D.syms) { let s = D.syms[i] if is_local_kind(s.kind) and (s.name == name) { if off >= s.bstart and off <= s.bend { if best < 0 or s.bstart >= D.syms[best].bstart { best = i } } } - i = i + 1 + i += 1 } return best } @@ -1116,42 +1124,42 @@ program LudicLsp { var k = 0 while k < len(rel[i].syms) { if is_toplevel_kind(rel[i].syms[k].kind) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k } - k = k + 1 + k += 1 } - i = i + 1 + i += 1 } i = 0 while i < len(g_docs) { var k = 0 while k < len(g_docs[i].syms) { if is_toplevel_kind(g_docs[i].syms[k].kind) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k } - k = k + 1 + k += 1 } - i = i + 1 + i += 1 } - return 0 - 1 + return -1 } # component sym index (in g_owner) for a dotted receiver, or -1 function receiver_component(D: Doc, dot_tok: int) -> int { let recv = prev_sig(D, dot_tok) - if recv < 0 or D.tk_kind[recv] != LT_ID { return 0 - 1 } + if recv < 0 or D.tk_kind[recv] != LT_ID { return -1 } let name = ttext(D, recv) var ty = null let l = find_local(D, name, D.tk_start[recv]) if l >= 0 { ty = D.syms[l].ty } else { let t = find_top(D, name); if t >= 0 { ty = g_owner.syms[t].ty } } - if (ty == null) { return 0 - 1 } - if (ty == "") { return 0 - 1 } + if (ty == null) { return -1 } + if (ty == "") { return -1 } return find_top(D, ty) } # resolve the token: sets g_owner/g_symidx; returns the token index (or -1) function resolve_tok(D: Doc, tok: int) -> int { - g_owner = null; g_symidx = 0 - 1 - if tok < 0 { return 0 - 1 } - if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return 0 - 1 } + g_owner = null; g_symidx = -1 + if tok < 0 { return -1 } + if D.tk_kind[tok] != LT_ID and D.tk_kind[tok] != LT_STR { return -1 } let name = ttext(D, tok) var i = 0 - while i < len(D.syms) { if D.syms[i].tok == tok { g_owner = D; g_symidx = i; return tok }; i = i + 1 } + while i < len(D.syms) { if D.syms[i].tok == tok { g_owner = D; g_symidx = i; return tok }; i += 1 } let prev = prev_sig(D, tok) if prev >= 0 and tis(D, prev, ".") { let comp = receiver_component(D, prev) @@ -1160,17 +1168,17 @@ program LudicLsp { var k = 0 while k < len(co.syms) { if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp and (co.syms[k].name == name) { g_owner = co; g_symidx = k; return tok } - k = k + 1 + k += 1 } } - g_owner = null; g_symidx = 0 - 1 + g_owner = null; g_symidx = -1 return tok } let l = find_local(D, name, D.tk_start[tok]) if l >= 0 { g_owner = D; g_symidx = l; return tok } let t = find_top(D, name) if t >= 0 { g_symidx = t; return tok } - g_owner = null; g_symidx = 0 - 1 + g_owner = null; g_symidx = -1 return tok } function resolve_at(D: Doc, off: int) -> int { return resolve_tok(D, tok_at(D, off)) } @@ -1178,7 +1186,7 @@ program LudicLsp { function first_on_line(D: Doc, tok: int) -> bool { let p = prev_sig(D, tok); return p < 0 or D.tk_line[p] != D.tk_line[tok] } function in_ui_body(D: Doc, off: int) -> bool { var i = 0 - while i < len(D.syms) { if D.syms[i].kind == LS_UI and off > D.syms[i].bstart and off < D.syms[i].bend { return true }; i = i + 1 } + while i < len(D.syms) { if D.syms[i].kind == LS_UI and off > D.syms[i].bstart and off < D.syms[i].bend { return true }; i += 1 } return false } @@ -1186,8 +1194,8 @@ program LudicLsp { var g_builtins: []pointer var g_bsigs: []pointer function badd(name: pointer, sig: pointer) -> void { push(g_builtins, name); push(g_bsigs, sig) } - function is_builtin_name(name: pointer) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i = i + 1 }; return false } - function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i = i + 1 }; return null } + function is_builtin_name(name: pointer) -> bool { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return true }; i += 1 }; return false } + function builtin_sig(name: pointer) -> pointer { var i = 0; while i < len(g_builtins) { if (g_builtins[i] == name) { return g_bsigs[i] }; i += 1 }; return null } # The namespaced standard-library surface: `Namespace.method`, mirroring the # compiler's emit_ns_call (selfhost/backend/emit_call.ludic). Screen/Random/Input/Map @@ -1576,7 +1584,7 @@ program LudicLsp { } function classify(D: Doc) -> void { let N = ntok(D) - if len(D.cls) < N { D.cls = new []int; var z = 0; while z < N { push(D.cls, SC_NONE); z = z + 1 } } + if len(D.cls) < N { D.cls = new []int; var z = 0; while z < N { push(D.cls, SC_NONE); z += 1 } } var i = 0 while i < N { let k = D.tk_kind[i] @@ -1590,32 +1598,32 @@ program LudicLsp { else { if k == LT_ANNO { c = SC_ANNOTATION } else { if k == LT_OP { c = SC_OPERATOR } else { if k == LT_ID { c = SC_UNKNOWN } } } } } } } } } - if k != LT_ID { D.cls[i] = c; i = i + 1; continue } + if k != LT_ID { D.cls[i] = c; i += 1; continue } let name = ttext(D, i) - var decl = 0 - 1 + var decl = -1 var s = 0 - while s < len(D.syms) { if D.syms[s].tok == i { decl = s; break }; s = s + 1 } - if decl >= 0 { D.cls[i] = class_of_symkind(D.syms[decl].kind); i = i + 1; continue } + while s < len(D.syms) { if D.syms[s].tok == i { decl = s; break }; s += 1 } + if decl >= 0 { D.cls[i] = class_of_symkind(D.syms[decl].kind); i += 1; continue } let prev = prev_sig(D, i) - if prev >= 0 and tis(D, prev, ".") { D.cls[i] = SC_FIELD; i = i + 1; continue } + if prev >= 0 and tis(D, prev, ".") { D.cls[i] = SC_FIELD; i += 1; continue } let nxt = next_sig(D, i) var followed_eq = false if nxt >= 0 and tis(D, nxt, "=") { followed_eq = true } if in_ui_body(D, D.tk_start[i]) { - if followed_eq { D.cls[i] = SC_PROP; i = i + 1; continue } + if followed_eq { D.cls[i] = SC_PROP; i += 1; continue } if first_on_line(D, i) or (prev >= 0 and (tis(D, prev, "{") or tis(D, prev, "}"))) { if is_widget_word(name) { D.cls[i] = SC_WIDGET } else { D.cls[i] = SC_UI } - i = i + 1; continue + i += 1; continue } } let l = find_local(D, name, D.tk_start[i]) - if l >= 0 { if D.syms[l].kind == LS_PARAM { D.cls[i] = SC_PARAM } else { D.cls[i] = SC_VARIABLE }; i = i + 1; continue } + if l >= 0 { if D.syms[l].kind == LS_PARAM { D.cls[i] = SC_PARAM } else { D.cls[i] = SC_VARIABLE }; i += 1; continue } let top = find_top(D, name) - if top >= 0 { D.cls[i] = class_of_symkind(g_owner.syms[top].kind); i = i + 1; continue } - if is_builtin_name(name) { D.cls[i] = SC_BUILTIN; i = i + 1; continue } - if followed_eq { D.cls[i] = SC_FIELD; i = i + 1; continue } + if top >= 0 { D.cls[i] = class_of_symkind(g_owner.syms[top].kind); i += 1; continue } + if is_builtin_name(name) { D.cls[i] = SC_BUILTIN; i += 1; continue } + if followed_eq { D.cls[i] = SC_FIELD; i += 1; continue } D.cls[i] = SC_UNKNOWN - i = i + 1 + i += 1 } } @@ -1646,7 +1654,7 @@ program LudicLsp { if cls == SC_COMMENT { return 12 } if cls == SC_OPERATOR { return 13 } if cls == SC_ANNOTATION { return 14 } - return 0 - 1 + return -1 } # ============================================================================ @@ -1663,10 +1671,10 @@ program LudicLsp { var jsrc: pointer = null var jp: int = 0 - function jskip() -> void { while jsrc[jp] == 32 or jsrc[jp] == 9 or jsrc[jp] == 10 or jsrc[jp] == 13 { jp = jp + 1 } } + function jskip() -> void { while jsrc[jp] == ' ' or jsrc[jp] == '\t' or jsrc[jp] == '\n' or jsrc[jp] == '\r' { jp += 1 } } function jhex4(s: pointer, at: int) -> int { var v = 0; var i = 0 - while i < 4 { let h = hexval(s[at + i]); if h < 0 { return v }; v = v * 16 + h; i = i + 1 } + while i < 4 { let h = hexval(s[at + i]); if h < 0 { return v }; v = v * 16 + h; i += 1 } return v } function utf8_emit(o: Buf, cp: int) -> void { @@ -1676,85 +1684,85 @@ program LudicLsp { else { buf_putc(o, 240 | (cp >> 18)); buf_putc(o, 128 | ((cp >> 12) & 63)); buf_putc(o, 128 | ((cp >> 6) & 63)); buf_putc(o, 128 | (cp & 63)) } } } } function jparse_string() -> pointer { - jp = jp + 1 # skip opening quote + jp += 1 # skip opening quote let o = buf_new() - while jsrc[jp] != 0 and jsrc[jp] != 34 { - if jsrc[jp] == 92 { - jp = jp + 1 - let c = jsrc[jp]; jp = jp + 1 - if c == 110 { buf_putc(o, 10) } - else { if c == 116 { buf_putc(o, 9) } - else { if c == 114 { buf_putc(o, 13) } - else { if c == 98 { buf_putc(o, 8) } - else { if c == 102 { buf_putc(o, 12) } - else { if c == 117 { - var cp = jhex4(jsrc, jp); jp = jp + 4 - if cp >= 55296 and cp < 56320 and jsrc[jp] == 92 and jsrc[jp + 1] == 117 { - let lo = jhex4(jsrc, jp + 2); jp = jp + 6 + while jsrc[jp] != 0 and jsrc[jp] != '"' { + if jsrc[jp] == '\\' { + jp += 1 + let c = jsrc[jp]; jp += 1 + if c == 'n' { buf_putc(o, '\n') } + else { if c == 't' { buf_putc(o, '\t') } + else { if c == 'r' { buf_putc(o, '\r') } + else { if c == 'b' { buf_putc(o, 8) } + else { if c == 'f' { buf_putc(o, 12) } + else { if c == 'u' { + var cp = jhex4(jsrc, jp); jp += 4 + if cp >= 55296 and cp < 56320 and jsrc[jp] == '\\' and jsrc[jp + 1] == 'u' { + let lo = jhex4(jsrc, jp + 2); jp += 6 cp = 65536 + ((cp - 55296) << 10) + (lo - 56320) } utf8_emit(o, cp) } else { buf_putc(o, c) } } } } } } - } else { buf_putc(o, jsrc[jp]); jp = jp + 1 } + } else { buf_putc(o, jsrc[jp]); jp += 1 } } - if jsrc[jp] == 34 { jp = jp + 1 } + if jsrc[jp] == '"' { jp += 1 } return buf_str(o) } function jparse() -> JVal { jskip() let c = jsrc[jp] - if c == 123 { # '{' - jp = jp + 1 + if c == '{' { # '{' + jp += 1 let v = jval(JOBJ) while true { jskip() - if jsrc[jp] == 125 { jp = jp + 1; break } + if jsrc[jp] == '}' { jp += 1; break } var key = "" - if jsrc[jp] == 34 { key = jparse_string() } - jskip(); if jsrc[jp] == 58 { jp = jp + 1 } + if jsrc[jp] == '"' { key = jparse_string() } + jskip(); if jsrc[jp] == ':' { jp += 1 } let kid = jparse() push(v.keys, key); push(v.kids, kid) jskip() - if jsrc[jp] == 44 { jp = jp + 1 } - else { if jsrc[jp] == 125 { jp = jp + 1; break } else { if jsrc[jp] == 0 { break } } } + if jsrc[jp] == ',' { jp += 1 } + else { if jsrc[jp] == '}' { jp += 1; break } else { if jsrc[jp] == 0 { break } } } } return v } - if c == 91 { # '[' - jp = jp + 1 + if c == '[' { # '[' + jp += 1 let v = jval(JARR) while true { jskip() - if jsrc[jp] == 93 { jp = jp + 1; break } + if jsrc[jp] == ']' { jp += 1; break } let kid = jparse() push(v.keys, ""); push(v.kids, kid) jskip() - if jsrc[jp] == 44 { jp = jp + 1 } - else { if jsrc[jp] == 93 { jp = jp + 1; break } else { if jsrc[jp] == 0 { break } } } + if jsrc[jp] == ',' { jp += 1 } + else { if jsrc[jp] == ']' { jp += 1; break } else { if jsrc[jp] == 0 { break } } } } return v } - if c == 34 { let v = jval(JSTR); v.s = jparse_string(); return v } - if jsrc[jp] == 116 and jsrc[jp + 1] == 114 { jp = jp + 4; let v = jval(JBOOL); v.b = 1; return v } # true - if jsrc[jp] == 102 and jsrc[jp + 1] == 97 { jp = jp + 5; let v = jval(JBOOL); v.b = 0; return v } # false - if jsrc[jp] == 110 and jsrc[jp + 1] == 117 { jp = jp + 4; return jval(JNULL) } # null + if c == '"' { let v = jval(JSTR); v.s = jparse_string(); return v } + if jsrc[jp] == 't' and jsrc[jp + 1] == 'r' { jp += 4; let v = jval(JBOOL); v.b = 1; return v } # true + if jsrc[jp] == 'f' and jsrc[jp + 1] == 'a' { jp += 5; let v = jval(JBOOL); v.b = 0; return v } # false + if jsrc[jp] == 'n' and jsrc[jp + 1] == 'u' { jp += 4; return jval(JNULL) } # null # number var neg = false - if jsrc[jp] == 45 { neg = true; jp = jp + 1 } + if jsrc[jp] == '-' { neg = true; jp += 1 } if char_is_digit(jsrc[jp]) { var n = 0 - while char_is_digit(jsrc[jp]) { n = n * 10 + (jsrc[jp] - 48); jp = jp + 1 } - if jsrc[jp] == 46 { jp = jp + 1; while char_is_digit(jsrc[jp]) { jp = jp + 1 } } # skip fraction - let v = jval(JNUM); if neg { v.num = 0 - n } else { v.num = n }; return v + while char_is_digit(jsrc[jp]) { n = n * 10 + (jsrc[jp] - 48); jp += 1 } + if jsrc[jp] == '.' { jp += 1; while char_is_digit(jsrc[jp]) { jp += 1 } } # skip fraction + let v = jval(JNUM); if neg { v.num = -n } else { v.num = n }; return v } - jp = jp + 1 + jp += 1 return jval(JNULL) } function jget(v: JVal, key: pointer) -> JVal { if (v == null) { return null } if v.t != JOBJ { return null } var i = 0 - while i < len(v.keys) { if (v.keys[i] == key) { return v.kids[i] }; i = i + 1 } + while i < len(v.keys) { if (v.keys[i] == key) { return v.kids[i] }; i += 1 } return null } function jat(v: JVal, i: int) -> JVal { if (v == null) { return null }; if i < 0 or i >= len(v.kids) { return null }; return v.kids[i] } @@ -1766,13 +1774,13 @@ program LudicLsp { let n = cstr_len(path) var start = 0; var i = 0 while i <= n { - if i == n or path[i] == 46 { + if i == n or path[i] == '.' { let key = path[start..i] cur = jget(cur, key) if (cur == null) { return null } start = i + 1 } - i = i + 1 + i += 1 } return cur } @@ -1785,8 +1793,8 @@ program LudicLsp { if f_name_like(F.tk_kind[t]) { return true } if F.tk_kind[t] != LT_OP { return false } let n = tlen(F, t); let c0 = F.src[F.tk_start[t]] - if n == 1 and (c0 == 41 or c0 == 93 or c0 == 46) { return true } - if n == 2 and c0 == 46 and F.src[F.tk_start[t] + 1] == 46 { return true } + if n == 1 and (c0 == ')' or c0 == ']' or c0 == '.') { return true } + if n == 2 and c0 == '.' and F.src[F.tk_start[t] + 1] == 46 { return true } return false } function f_is_unary(F: Doc, i: int) -> bool { @@ -1797,7 +1805,7 @@ program LudicLsp { if p < 0 { return true } let pk2 = F.tk_kind[p] if pk2 == LT_ID or pk2 == LT_INT or pk2 == LT_FLOAT or pk2 == LT_STR or pk2 == LT_CHAR or pk2 == LT_BOOL or pk2 == LT_TYPE or pk2 == LT_PHASE { return false } - if pk2 == LT_OP { let c = F.src[F.tk_start[p]]; return not (tlen(F, p) == 1 and (c == 41 or c == 93 or c == 125)) } + if pk2 == LT_OP { let c = F.src[F.tk_start[p]]; return not (tlen(F, p) == 1 and (c == ')' or c == ']' or c == '}')) } return true } function f_space_before(F: Doc, prev: int, cur: int) -> int { @@ -1807,15 +1815,15 @@ program LudicLsp { let p0 = F.src[F.tk_start[prev]]; let c0 = F.src[F.tk_start[cur]] let p1 = (plen == 1); let c1 = (clen == 1) if pk2 == LT_ERR or ck == LT_ERR { return F.tk_start[cur] - F.tk_end[prev] } - if c1 and (c0 == 41 or c0 == 93 or c0 == 44 or c0 == 58 or c0 == 59) { return 0 } - if c1 and c0 == 46 and ck == LT_OP and f_dot_tight(F, prev) { return 0 } - if p1 and p0 == 46 and pk2 == LT_OP and f_dot_tight(F, cur) { return 0 } - if p1 and (p0 == 40 or p0 == 91) and pk2 == LT_OP { return 0 } + if c1 and (c0 == ')' or c0 == ']' or c0 == ',' or c0 == ':' or c0 == ';') { return 0 } + if c1 and c0 == '.' and ck == LT_OP and f_dot_tight(F, prev) { return 0 } + if p1 and p0 == '.' and pk2 == LT_OP and f_dot_tight(F, cur) { return 0 } + if p1 and (p0 == '(' or p0 == '[') and pk2 == LT_OP { return 0 } if pk2 == LT_OP and f_is_unary(F, prev) { return 0 } - if pk2 == LT_ANNO and c1 and c0 == 40 { return 0 } - if c1 and c0 == 40 and ck == LT_OP { + if pk2 == LT_ANNO and c1 and c0 == '(' { return 0 } + if c1 and c0 == '(' and ck == LT_OP { if pk2 == LT_ID or pk2 == LT_TYPE or pk2 == LT_PHASE { return 0 } - if pk2 == LT_OP { if p1 and (p0 == 41 or p0 == 93) { return 1 }; return 0 } + if pk2 == LT_OP { if p1 and (p0 == ')' or p0 == ']') { return 1 }; return 0 } return 1 } return 1 @@ -1833,22 +1841,22 @@ program LudicLsp { let o = buf_new() let stack = words(600) let hang = words(600) - var sp = 0; var cur = 0; var open = 0; var brack = 0; var ui_depth = 0 - 1 + var sp = 0; var cur = 0; var open = 0; var brack = 0; var ui_depth = -1 var pending_blank = 0; var wrote_any = 0; var prev_line_had_comment = 0 let N = ntok(F) var i = 0 while i < N and F.tk_kind[i] != LT_EOF { let a = i - while i < N and F.tk_kind[i] != LT_NL and F.tk_kind[i] != LT_EOF { i = i + 1 } + while i < N and F.tk_kind[i] != LT_NL and F.tk_kind[i] != LT_EOF { i += 1 } let b = i - if i < N and F.tk_kind[i] == LT_NL { i = i + 1 } + if i < N and F.tk_kind[i] == LT_NL { i += 1 } if a == b { if wrote_any == 1 { pending_blank = 1 }; continue } var line_level = cur var tsp = sp; var t = a - while t < b and F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 125 { if tsp > 0 { tsp = tsp - 1; line_level = stack[tsp] }; t = t + 1 } + while t < b and F.tk_kind[t] == LT_OP and tlen(F, t) == 1 and F.src[F.tk_start[t]] == 125 { if tsp > 0 { tsp -= 1; line_level = stack[tsp] }; t += 1 } var orig_ind = 0 var kk = F.linestart[F.tk_line[a]] - while kk < F.tk_start[a] { if F.src[kk] == 9 { orig_ind = orig_ind + 4 } else { orig_ind = orig_ind + 1 }; kk = kk + 1 } + while kk < F.tk_start[a] { if F.src[kk] == '\t' { orig_ind += 4 } else { orig_ind += 1 }; kk += 1 } var comment_run = 0 if F.tk_kind[a] == LT_COMMENT and b == a + 1 and prev_line_had_comment == 1 { comment_run = 1 } var ind = 0 @@ -1857,18 +1865,18 @@ program LudicLsp { if open > 0 or (sp > 0 and hangprev == 1) { var k2 = F.linestart[F.tk_line[a]] ind = 0 - while k2 < F.tk_start[a] { if F.src[k2] == 9 { ind = ind + 4 } else { ind = ind + 1 }; k2 = k2 + 1 } + while k2 < F.tk_start[a] { if F.src[k2] == '\t' { ind += 4 } else { ind += 1 }; k2 += 1 } } else { var extra = 0 if f_is_clause(ttext(F, a)) and F.tk_kind[a] == LT_KW { extra = iw } ind = line_level * iw + extra if comment_run == 1 and orig_ind > ind { ind = orig_ind } } - if pending_blank == 1 and wrote_any == 1 { buf_putc(o, 10) } + if pending_blank == 1 and wrote_any == 1 { buf_putc(o, '\n') } pending_blank = 0 var wi = 0 - while wi < ind { buf_putc(o, 32); wi = wi + 1 } - var prev = 0 - 1 + while wi < ind { buf_putc(o, ' '); wi += 1 } + var prev = -1 var line_brack = brack var line_ui_open = 0 if ui_depth >= 0 and sp > ui_depth { line_ui_open = 1 } @@ -1895,17 +1903,17 @@ program LudicLsp { if gap >= 2 and want >= 1 { if gap > 60 { gap = 60 }; want = gap } if want < 0 { want = 0 } var w2 = 0 - while w2 < want { buf_putc(o, 32); w2 = w2 + 1 } + while w2 < want { buf_putc(o, ' '); w2 += 1 } buf_addrange(o, F.src, F.tk_start[t], F.tk_end[t]) prev = t if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 { let c = F.src[F.tk_start[t]] - if c == 91 { line_brack = line_brack + 1 } - else { if c == 93 { line_brack = line_brack - 1; if line_brack < 0 { line_brack = 0 } } } + if c == '[' { line_brack += 1 } + else { if c == ']' { line_brack -= 1; if line_brack < 0 { line_brack = 0 } } } } - t = t + 1 + t += 1 } - buf_putc(o, 10) + buf_putc(o, '\n') wrote_any = 1 prev_line_had_comment = 0 if prev >= 0 and F.tk_kind[prev] == LT_COMMENT { prev_line_had_comment = 1 } @@ -1915,18 +1923,18 @@ program LudicLsp { while t < b { if F.tk_kind[t] == LT_OP and tlen(F, t) == 1 { let c = F.src[F.tk_start[t]] - if c == 123 { - if sp < 512 { let nxt = next_sig(F, t); stack[sp] = line_level; var h = 0; if nxt >= 0 and nxt < b { h = 1 }; hang[sp] = h; sp = sp + 1 } + if c == '{' { + if sp < 512 { let nxt = next_sig(F, t); stack[sp] = line_level; var h = 0; if nxt >= 0 and nxt < b { h = 1 }; hang[sp] = h; sp += 1 } level = line_level + 1 } - else { if c == 125 { if sp > 0 { sp = sp - 1; level = stack[sp] } } - else { if c == 40 or c == 91 { open = open + 1; if c == 91 { brack = brack + 1 } } - else { if c == 41 or c == 93 { open = open - 1; if open < 0 { open = 0 }; if c == 93 { brack = brack - 1; if brack < 0 { brack = 0 } } } } } } + else { if c == '}' { if sp > 0 { sp -= 1; level = stack[sp] } } + else { if c == '(' or c == '[' { open += 1; if c == '[' { brack += 1 } } + else { if c == ')' or c == ']' { open -= 1; if open < 0 { open = 0 }; if c == ']' { brack -= 1; if brack < 0 { brack = 0 } } } } } } } - t = t + 1 + t += 1 } cur = level - if ui_depth >= 0 and sp <= ui_depth { ui_depth = 0 - 1 } + if ui_depth >= 0 and sp <= ui_depth { ui_depth = -1 } } return buf_str(o) } @@ -1935,25 +1943,25 @@ program LudicLsp { var mf_info: int = 0 var mf_marker: int = 0 function mf_fence_at(s: pointer, i: int) -> bool { - var j = i; while s[j] == 32 { j = j + 1 } + var j = i; while s[j] == ' ' { j += 1 } let m = s[j] - if m != 96 and m != 126 { return false } - var n = 0; while s[j] == m { j = j + 1; n = n + 1 } + if m != '`' and m != '~' { return false } + var n = 0; while s[j] == m { j += 1; n += 1 } if n < 3 { return false } mf_flen = n; mf_info = j; mf_marker = m return true } - function f_line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != 10 { e = e + 1 }; return e } + function f_line_end(s: pointer, i: int) -> int { var e = i; while s[e] != 0 and s[e] != '\n' { e += 1 }; return e } function format_md(s: pointer, iw: int) -> pointer { let o = buf_new() var i = 0 while s[i] != 0 { let ls = i let le = f_line_end(s, ls) - var indent = 0; while s[ls + indent] == 32 { indent = indent + 1 } + var indent = 0; while s[ls + indent] == ' ' { indent += 1 } var is_ludic_fence = false if mf_fence_at(s, ls) { - var kk = mf_info; while s[kk] == 32 or s[kk] == 9 { kk = kk + 1 } + var kk = mf_info; while s[kk] == ' ' or s[kk] == '\t' { kk += 1 } if word_ludic_at(s, kk) { is_ludic_fence = true } } if is_ludic_fence { @@ -1969,7 +1977,7 @@ program LudicLsp { if mf_fence_at(s, ps) { if mf_marker == openm and mf_flen >= openlen { var only = 1; var k = mf_info - while k < pe { if s[k] != 32 and s[k] != 13 { only = 0; break }; k = k + 1 } + while k < pe { if s[k] != ' ' and s[k] != '\r' { only = 0; break }; k += 1 } if only == 1 { be = ps; break } } } @@ -1980,19 +1988,19 @@ program LudicLsp { while p < be { let q = f_line_end(s, p) var skip = 0 - while skip < indent and (p + skip) < q and s[p + skip] == 32 { skip = skip + 1 } + while skip < indent and (p + skip) < q and s[p + skip] == ' ' { skip += 1 } buf_addrange(body, s, p + skip, q) - buf_putc(body, 10) + buf_putc(body, '\n') if s[q] != 0 { p = q + 1 } else { p = q } } let F = make_text_doc(buf_str(body)) let ftext = format_doc(F, iw) var fp = 0 while ftext[fp] != 0 { - var fq = fp; while ftext[fq] != 0 and ftext[fq] != 10 { fq = fq + 1 } - if fq > fp { var wi = 0; while wi < indent { buf_putc(o, 32); wi = wi + 1 } } + var fq = fp; while ftext[fq] != 0 and ftext[fq] != '\n' { fq += 1 } + if fq > fp { var wi = 0; while wi < indent { buf_putc(o, ' '); wi += 1 } } buf_addrange(o, ftext, fp, fq) - buf_putc(o, 10) + buf_putc(o, '\n') if ftext[fq] != 0 { fp = fq + 1 } else { fp = fq } } i = be @@ -2004,7 +2012,7 @@ program LudicLsp { } return buf_str(o) } - function skip_ws2(s: pointer, at: int) -> int { var a = at; while s[a] == 32 or s[a] == 9 { a = a + 1 }; return a } + function skip_ws2(s: pointer, at: int) -> int { var a = at; while s[a] == ' ' or s[a] == '\t' { a += 1 }; return a } # ============================================================================ # framing + output @@ -2012,26 +2020,26 @@ program LudicLsp { extern function fflush(f: pointer) -> int = "fflush" function jstr_out(o: Buf, z: pointer) -> void { - buf_putc(o, 34) - if (z == null) { buf_putc(o, 34); return } + buf_putc(o, '"') + if (z == null) { buf_putc(o, '"'); return } var i = 0 while z[i] != 0 { let c = z[i] - if c == 34 { buf_putc(o, 92); buf_putc(o, 34) } - else { if c == 92 { buf_putc(o, 92); buf_putc(o, 92) } - else { if c == 10 { buf_putc(o, 92); buf_putc(o, 110) } - else { if c == 13 { buf_putc(o, 92); buf_putc(o, 114) } - else { if c == 9 { buf_putc(o, 92); buf_putc(o, 116) } - else { if c >= 0 and c < 32 { buf_putc(o, 92); buf_putc(o, 117); buf_putc(o, 48); buf_putc(o, 48); buf_putc(o, hexch(c / 16)); buf_putc(o, hexch(c % 16)) } + if c == '"' { buf_putc(o, '\\'); buf_putc(o, '"') } + else { if c == '\\' { buf_putc(o, '\\'); buf_putc(o, '\\') } + else { if c == '\n' { buf_putc(o, '\\'); buf_putc(o, 'n') } + else { if c == '\r' { buf_putc(o, '\\'); buf_putc(o, 'r') } + else { if c == '\t' { buf_putc(o, '\\'); buf_putc(o, 't') } + else { if c >= 0 and c < ' ' { buf_putc(o, '\\'); buf_putc(o, 'u'); buf_putc(o, '0'); buf_putc(o, '0'); buf_putc(o, hexch(c / 16)); buf_putc(o, hexch(c % 16)) } else { buf_putc(o, c) } } } } } } - i = i + 1 + i += 1 } - buf_putc(o, 34) + buf_putc(o, '"') } function send_raw(body: pointer) -> void { let hdr = buf_new() - buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, 13); buf_putc(hdr, 10); buf_putc(hdr, 13); buf_putc(hdr, 10) + buf_puts(hdr, "Content-Length: "); buf_puti(hdr, cstr_len(body)); buf_putc(hdr, '\r'); buf_putc(hdr, '\n'); buf_putc(hdr, '\r'); buf_putc(hdr, '\n') let hs = buf_str(hdr) let out = file_stdout() file_write(out, hs, cstr_len(hs)) @@ -2045,47 +2053,47 @@ program LudicLsp { else { if idv.t == JSTR { jstr_out(o, idv.s) } else { buf_puti(o, idv.num) } } buf_puts(o, ",\"result\":") if (result == null) or (result[0] == 0) { buf_puts(o, "null") } else { buf_puts(o, result) } - buf_putc(o, 125) + buf_putc(o, '}') send_raw(buf_str(o)) } function send_notify(method: pointer, params: pointer) -> void { let o = buf_new() - buf_puts(o, "{\"jsonrpc\":\"2.0\",\"method\":"); jstr_out(o, method); buf_puts(o, ",\"params\":"); buf_puts(o, params); buf_putc(o, 125) + buf_puts(o, "{\"jsonrpc\":\"2.0\",\"method\":"); jstr_out(o, method); buf_puts(o, ",\"params\":"); buf_puts(o, params); buf_putc(o, '}') send_raw(buf_str(o)) } - function lower(c: int) -> int { if c >= 65 and c <= 90 { return c + 32 }; return c } + function lower(c: int) -> int { if c >= 'A' and c <= 'Z' { return c + 32 }; return c } function starts_with_ci(s: pointer, pre: pointer) -> bool { var i = 0 - while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i = i + 1 } + while pre[i] != 0 { if s[i] == 0 or lower(s[i]) != lower(pre[i]) { return false }; i += 1 } return true } function read_message() -> pointer { - var length = 0 - 1 + var length = -1 let line = buf_new() while true { line.len = 0 var c = read_char() if c < 0 { return null } - while c >= 0 and c != 10 { buf_putc(line, c); c = read_char() } + while c >= 0 and c != '\n' { buf_putc(line, c); c = read_char() } let s = buf_str(line) var L = cstr_len(s) - if L > 0 and s[L - 1] == 13 { s[L - 1] = 0; L = L - 1 } + if L > 0 and s[L - 1] == '\r' { s[L - 1] = 0; L -= 1 } if L == 0 { break } if starts_with_ci(s, "content-length:") { length = atoi(s[15..cstr_len(s)]) } } if length <= 0 { return null } let buf = bytes(length + 1) var got = 0 - while got < length { let c = read_char(); if c < 0 { break }; buf[got] = c; got = got + 1 } + while got < length { let c = read_char(); if c < 0 { break }; buf[got] = c; got += 1 } buf[got] = 0 return buf } # ---- position output ---- - function put_pos(o: Buf, D: Doc, off: int) -> void { buf_puts(o, "{\"line\":"); buf_puti(o, line_of(D, off)); buf_puts(o, ",\"character\":"); buf_puti(o, col_of(D, off)); buf_putc(o, 125) } - function put_range(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"start\":"); put_pos(o, D, a); buf_puts(o, ",\"end\":"); put_pos(o, D, b); buf_putc(o, 125) } - function put_location(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"range\":"); put_range(o, D, a, b); buf_putc(o, 125) } + function put_pos(o: Buf, D: Doc, off: int) -> void { buf_puts(o, "{\"line\":"); buf_puti(o, line_of(D, off)); buf_puts(o, ",\"character\":"); buf_puti(o, col_of(D, off)); buf_putc(o, '}') } + function put_range(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"start\":"); put_pos(o, D, a); buf_puts(o, ",\"end\":"); put_pos(o, D, b); buf_putc(o, '}') } + function put_location(o: Buf, D: Doc, a: int, b: int) -> void { buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"range\":"); put_range(o, D, a, b); buf_putc(o, '}') } function doc_of(msg: JVal) -> Doc { let uri = jstr(jpath(msg, "params.textDocument.uri"), null) @@ -2099,7 +2107,7 @@ program LudicLsp { let ch = jint(jpath(msg, concat3(base, ".character", "")), 0) return offset_of(D, line, ch) } - function basename(p: pointer) -> pointer { var last = 0 - 1; var i = 0; while p[i] != 0 { if p[i] == 47 { last = i }; i = i + 1 }; return p[last + 1..i] } + function basename(p: pointer) -> pointer { var last = -1; var i = 0; while p[i] != 0 { if p[i] == '/' { last = i }; i += 1 }; return p[last + 1..i] } # ---- LSP symbol kinds & completion kinds ---- function lsp_symbol_kind(k: int) -> int { @@ -2138,45 +2146,105 @@ program LudicLsp { let stack = words(1200); var top = 0 var i = 0; let N = ntok(D) while i < N { - if D.tk_kind[i] != LT_OP or tlen(D, i) != 1 { i = i + 1; continue } + if D.tk_kind[i] != LT_OP or tlen(D, i) != 1 { i += 1; continue } let c = D.src[D.tk_start[i]] - if c == 123 or c == 40 or c == 91 { if top < 1200 { stack[top] = i; top = top + 1 } } - else { if c == 125 or c == 41 or c == 93 { + if c == '{' or c == '(' or c == '[' { if top < 1200 { stack[top] = i; top += 1 } } + else { if c == '}' or c == ')' or c == ']' { if top == 0 { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, concat3("unmatched '", D.src[D.tk_start[i]..D.tk_end[i]], "'")) } - else { top = top - 1; let oi = stack[top]; let oc = D.src[D.tk_start[oi]] - var wantc = 93 - if oc == 123 { wantc = 125 } else { if oc == 40 { wantc = 41 } } + else { top -= 1; let oi = stack[top]; let oc = D.src[D.tk_start[oi]] + var wantc = ']' + if oc == '{' { wantc = '}' } else if oc == '(' { wantc = ')' } if wantc != c { push(ds_s, D.tk_start[i]); push(ds_e, D.tk_end[i]); push(ds_m, "mismatched bracket") } } } } - i = i + 1 + i += 1 } var k = 0 - while k < top { let oi = stack[k]; push(ds_s, D.tk_start[oi]); push(ds_e, D.tk_end[oi]); push(ds_m, concat3("unclosed '", D.src[D.tk_start[oi]..D.tk_end[oi]], "'")); k = k + 1 } + while k < top { let oi = stack[k]; push(ds_s, D.tk_start[oi]); push(ds_e, D.tk_end[oi]); push(ds_m, concat3("unclosed '", D.src[D.tk_start[oi]..D.tk_end[oi]], "'")); k += 1 } var j = 0 while j < len(ds_s) { - if j > 0 { buf_putc(o, 44) } - buf_puts(o, "{\"range\":"); put_range(o, D, ds_s[j], ds_e[j]); buf_puts(o, ",\"severity\":1,\"source\":\"ludic\",\"message\":"); jstr_out(o, ds_m[j]); buf_putc(o, 125) - j = j + 1 + if j > 0 { buf_putc(o, ',') } + buf_puts(o, "{\"range\":"); put_range(o, D, ds_s[j], ds_e[j]); buf_puts(o, ",\"severity\":1,\"source\":\"ludic\",\"message\":"); jstr_out(o, ds_m[j]); buf_putc(o, '}') + j += 1 } } + # the compiler's error, as one whole-line diagnostic from source "ludicc" + function diag_compiler(D: Doc, o: Buf, first: bool) -> void { + if (D.cc_msg == null) or D.cc_line < 1 or D.cc_line > len(D.linestart) { return } + let a = D.linestart[D.cc_line - 1] + var b = cstr_len(D.src) + if D.cc_line < len(D.linestart) { b = D.linestart[D.cc_line] - 1 } + if not first { buf_putc(o, ',') } + buf_puts(o, "{\"range\":"); put_range(o, D, a, b); buf_puts(o, ",\"severity\":1,\"source\":\"ludicc\",\"message\":"); jstr_out(o, D.cc_msg); buf_putc(o, '}') + } function publish_diag(D: Doc) -> void { let o = buf_new() buf_puts(o, "{\"uri\":"); jstr_out(o, D.uri); buf_puts(o, ",\"diagnostics\":[") - if D.is_md == 0 { diag_structural(D, o) } + if D.is_md == 0 { + let before = o.len + diag_structural(D, o) + diag_compiler(D, o, o.len == before) + } buf_puts(o, "]}") send_notify("textDocument/publishDiagnostics", buf_str(o)) } + # ---- compiler diagnostics, on save ---- + # ludicc stops at its first error and reports it as `path:line: error: msg`; + # run it over the saved document's compilation unit, clear the previous + # compiler error from every document of that unit, and pin the new one (if + # any) on the document it names — publishing both so a fixed error clears. + function compiler_diags(D: Doc) -> void { + if (g_compiler == null) or D.is_md == 1 { return } + let root = unit_root(D) + if (root == null) { return } + var i = 0 + while i < len(g_docs) { if (g_docs[i].unit == root.unit) { g_docs[i].cc_msg = null; g_docs[i].cc_line = 0 }; i += 1 } + let errf = `{Os.temp_dir()}/ludic_lsp_cc_{Os.pid()}.txt` + run(`'{g_compiler}' --emit-llvm '{root.path}' -o /dev/null > /dev/null 2> '{errf}'`) + let out = read_file(errf) + run(`rm -f '{errf}'`) + if (out == null) { return } + # the first `path:line: error: msg` line + var a = 0 + while out[a] != 0 { + var b = a + while out[b] != 0 and out[b] != '\n' { b += 1 } + let line = out[a..b] + let e = Text.index_of(line, ": error: ") + if e > 0 { + var c = e - 1 + while c > 0 and line[c - 1] >= '0' and line[c - 1] <= '9' { c -= 1 } + if c > 1 and line[c - 1] == ':' { + let target = by_path(line[0..c - 1]) + if (target != null) { + target.cc_line = Text.to_int(line[c..e]) + target.cc_msg = line[e + 9..cstr_len(line)] + if (target != D) and target.isopen == 1 { publish_diag(target) } + } + } + return + } + a = b + if out[a] == '\n' { a += 1 } + } + } + function on_did_save(msg: JVal) -> void { + let D = doc_of(msg) + if (D == null) { return } + compiler_diags(D) + publish_diag(D) + } + # ---- document symbols ---- function put_doc_symbols(o: Buf, D: Doc, parent: int) -> void { - buf_putc(o, 91) + buf_putc(o, '[') var first = 1 var i = 0 while i < len(D.syms) { let y = D.syms[i] if y.parent == parent and is_outline_kind(y.kind) and (y.name != null) and (cstr_len(y.name) > 0) { - if first == 0 { buf_putc(o, 44) } + if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"name\":"); jstr_out(o, y.name) buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail) @@ -2187,11 +2255,11 @@ program LudicLsp { put_range(o, D, a, b) buf_puts(o, ",\"selectionRange\":"); put_range(o, D, y.start, y.end) buf_puts(o, ",\"children\":"); put_doc_symbols(o, D, i) - buf_putc(o, 125) + buf_putc(o, '}') } - i = i + 1 + i += 1 } - buf_putc(o, 93) + buf_putc(o, ']') } # ---- hover ---- @@ -2265,9 +2333,9 @@ program LudicLsp { resolve_tok(T, k) if (g_owner != null) and (g_owner.path == tdoc.path) and g_symidx == tsym { push(g_occ_doc, T); push(g_occ_tok, k) } } - k = k + 1 + k += 1 } - i = i + 1 + i += 1 } } function on_references(msg: JVal, id: JVal) -> void { @@ -2278,15 +2346,15 @@ program LudicLsp { if tok < 0 or (g_owner == null) or g_symidx < 0 { send_result(id, "[]"); return } let tdoc = g_owner; let tsym = g_symidx find_occurrences(D, tdoc, tsym) - let o = buf_new(); buf_putc(o, 91) + let o = buf_new(); buf_putc(o, '[') var i = 0 while i < len(g_occ_doc) { - if i > 0 { buf_putc(o, 44) } + if i > 0 { buf_putc(o, ',') } let T = g_occ_doc[i]; let k = g_occ_tok[i] put_location(o, T, T.tk_start[k], T.tk_end[k]) - i = i + 1 + i += 1 } - buf_putc(o, 93); send_result(id, buf_str(o)) + buf_putc(o, ']'); send_result(id, buf_str(o)) } function on_rename(msg: JVal, id: JVal) -> void { let D = doc_of(msg) @@ -2304,25 +2372,25 @@ program LudicLsp { while gi < len(g_occ_doc) { var already = false var s = 0 - while s < len(seen) { if (seen[s].path == g_occ_doc[gi].path) { already = true }; s = s + 1 } + while s < len(seen) { if (seen[s].path == g_occ_doc[gi].path) { already = true }; s += 1 } if not already { push(seen, g_occ_doc[gi]) - if len(seen) > 1 { buf_putc(o, 44) } - jstr_out(o, g_occ_doc[gi].uri); buf_putc(o, 58); buf_putc(o, 91) + if len(seen) > 1 { buf_putc(o, ',') } + jstr_out(o, g_occ_doc[gi].uri); buf_putc(o, ':'); buf_putc(o, '[') var first = 1 var k = 0 while k < len(g_occ_doc) { if (g_occ_doc[k].path == g_occ_doc[gi].path) { - if first == 0 { buf_putc(o, 44) } + if first == 0 { buf_putc(o, ',') } first = 0 let T = g_occ_doc[k]; let tk = g_occ_tok[k] - buf_puts(o, "{\"range\":"); put_range(o, T, T.tk_start[tk], T.tk_end[tk]); buf_puts(o, ",\"newText\":"); jstr_out(o, newname); buf_putc(o, 125) + buf_puts(o, "{\"range\":"); put_range(o, T, T.tk_start[tk], T.tk_end[tk]); buf_puts(o, ",\"newText\":"); jstr_out(o, newname); buf_putc(o, '}') } - k = k + 1 + k += 1 } - buf_putc(o, 93) + buf_putc(o, ']') } - gi = gi + 1 + gi += 1 } buf_puts(o, "}}"); send_result(id, buf_str(o)) } @@ -2330,11 +2398,11 @@ program LudicLsp { # ---- completion ---- var g_first: int = 1 function comp_item(o: Buf, label: pointer, kind: int, detail: pointer) -> void { - if g_first == 0 { buf_putc(o, 44) } + if g_first == 0 { buf_putc(o, ',') } g_first = 0 buf_puts(o, "{\"label\":"); jstr_out(o, label); buf_puts(o, ",\"kind\":"); buf_puti(o, kind) if (detail != null) and (detail[0] != 0) { buf_puts(o, ",\"detail\":"); jstr_out(o, detail) } - buf_putc(o, 125) + buf_putc(o, '}') } function comp_scope(o: Buf, D: Doc, off: int, types_only: int) -> void { if types_only == 0 { @@ -2345,7 +2413,7 @@ program LudicLsp { var det = y.detail; if det == "" { det = y.ty } comp_item(o, y.name, 6, det) } - i = i + 1 + i += 1 } } let rel = related(D) @@ -2359,16 +2427,16 @@ program LudicLsp { if types_only == 1 and not (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) { okk = false } if okk { comp_item(o, y.name, comp_kind_of(y.kind), y.detail) } } - k = k + 1 + k += 1 } - r = r + 1 + r += 1 } } function comp_types(o: Buf) -> void { comp_item(o, "int", 14, "built-in type"); comp_item(o, "long", 14, "built-in type"); comp_item(o, "fixed", 14, "built-in type"); comp_item(o, "bool", 14, "built-in type") comp_item(o, "entity", 14, "built-in type"); comp_item(o, "string", 14, "built-in type"); comp_item(o, "pointer", 14, "built-in type"); comp_item(o, "Vector", 14, "built-in type"); comp_item(o, "IVec2", 14, "built-in type"); comp_item(o, "Rect", 14, "built-in type"); comp_item(o, "void", 14, "built-in type") } - function comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i = i + 1 } } + function comp_builtins(o: Buf) -> void { var i = 0; while i < len(g_builtins) { comp_item(o, g_builtins[i], 3, g_bsigs[i]); i += 1 } } function on_completion(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "{\"isIncomplete\":false,\"items\":[]}"); return } @@ -2376,12 +2444,12 @@ program LudicLsp { let o = buf_new() buf_puts(o, "{\"isIncomplete\":false,\"items\":[") g_first = 1 - var tok = 0 - 1 + var tok = -1 var i = ntok(D) - 1 while i >= 0 { let k = D.tk_kind[i] if k != LT_NL and k != LT_EOF and k != LT_COMMENT { if D.tk_end[i] <= off { tok = i; break } } - i = i - 1 + i -= 1 } var anchor = tok if anchor >= 0 and D.tk_kind[anchor] == LT_ID and D.tk_end[anchor] >= off { anchor = prev_sig(D, anchor) } @@ -2391,11 +2459,11 @@ program LudicLsp { while j >= 0 { if D.tk_kind[j] == LT_OP and tlen(D, j) == 1 { let ch = D.src[D.tk_start[j]] - if ch == 93 { depth = depth + 1 } - else { if ch == 91 { if depth == 0 { in_qt = 1; j = 0 - 1 } else { depth = depth - 1 } } - else { if ch == 123 or ch == 125 { j = 0 - 1 } } } + if ch == ']' { depth += 1 } + else { if ch == '[' { if depth == 0 { in_qt = 1; j = -1 } else { depth -= 1 } } + else { if ch == '{' or ch == '}' { j = -1 } } } } - j = j - 1 + j -= 1 } var done = 0 if in_qt == 1 { comp_scope(o, D, off, 1); done = 1 } @@ -2405,7 +2473,7 @@ program LudicLsp { if comp >= 0 and (g_owner != null) { let co = g_owner var k = 0 - while k < len(co.syms) { if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp { comp_item(o, co.syms[k].name, 5, co.syms[k].detail) }; k = k + 1 } + while k < len(co.syms) { if co.syms[k].kind == LS_FIELD and co.syms[k].parent == comp { comp_item(o, co.syms[k].name, 5, co.syms[k].detail) }; k += 1 } } done = 1 } @@ -2432,42 +2500,42 @@ program LudicLsp { function on_inlay_hint(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "[]"); return } - let o = buf_new(); buf_putc(o, 91) + let o = buf_new(); buf_putc(o, '[') var first = 1 var i = 0 while i < len(D.syms) { let y = D.syms[i] if y.kind == LS_QUERYVAR and (y.ty != null) and (y.ty[0] != 0) { - if first == 0 { buf_putc(o, 44) } + if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"position\":"); put_pos(o, D, y.end); buf_puts(o, ",\"label\":"); jstr_out(o, concat3(": ", y.ty, "")); buf_puts(o, ",\"kind\":1,\"paddingLeft\":false}") } - i = i + 1 + i += 1 } - buf_putc(o, 93); send_result(id, buf_str(o)) + buf_putc(o, ']'); send_result(id, buf_str(o)) } function on_signature_help(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "null"); return } let off = off_of(msg, D, "params.position") - var depth = 0; var callee = 0 - 1; var arg = 0 + var depth = 0; var callee = -1; var arg = 0 var i = ntok(D) - 1 while i >= 0 { let t = i - if D.tk_start[t] >= off { i = i - 1; continue } + if D.tk_start[t] >= off { i -= 1; continue } if D.tk_kind[t] == LT_OP and tlen(D, t) == 1 { let c = D.src[D.tk_start[t]] - if c == 41 { depth = depth + 1 } - else { if c == 40 { + if c == ')' { depth += 1 } + else { if c == '(' { if depth == 0 { callee = prev_sig(D, t) var k = t + 1 - while k < ntok(D) and D.tk_start[k] < off { if tis(D, k, ",") { arg = arg + 1 }; k = k + 1 } - i = 0 - 1 - } else { depth = depth - 1 } + while k < ntok(D) and D.tk_start[k] < off { if tis(D, k, ",") { arg += 1 }; k += 1 } + i = -1 + } else { depth -= 1 } } } } - i = i - 1 + i -= 1 } if callee < 0 or D.tk_kind[callee] != LT_ID { send_result(id, "null"); return } let name = ttext(D, callee) @@ -2488,7 +2556,7 @@ program LudicLsp { let o = buf_new() buf_puts(o, "{\"signatures\":[{\"label\":"); jstr_out(o, sig) if (docstr != null) and (docstr[0] != 0) { buf_puts(o, ",\"documentation\":"); jstr_out(o, docstr) } - buf_puts(o, "}],\"activeSignature\":0,\"activeParameter\":"); buf_puti(o, arg); buf_putc(o, 125) + buf_puts(o, "}],\"activeSignature\":0,\"activeParameter\":"); buf_puti(o, arg); buf_putc(o, '}') send_result(id, buf_str(o)) } function on_semantic_tokens(msg: JVal, id: JVal) -> void { @@ -2510,21 +2578,21 @@ program LudicLsp { let dl = line - prev_line var dc = ch if dl == 0 { dc = ch - prev_char } - if first == 0 { buf_putc(o, 44) } + if first == 0 { buf_putc(o, ',') } first = 0 - buf_puti(o, dl); buf_putc(o, 44); buf_puti(o, dc); buf_putc(o, 44); buf_puti(o, ln); buf_putc(o, 44); buf_puti(o, ty); buf_putc(o, 44); buf_puti(o, g_semmods) + buf_puti(o, dl); buf_putc(o, ','); buf_puti(o, dc); buf_putc(o, ','); buf_puti(o, ln); buf_putc(o, ','); buf_puti(o, ty); buf_putc(o, ','); buf_puti(o, g_semmods) prev_line = line; prev_char = ch } } } - i = i + 1 + i += 1 } buf_puts(o, "]}"); send_result(id, buf_str(o)) } function on_folding(msg: JVal, id: JVal) -> void { let D = doc_of(msg) if (D == null) { send_result(id, "[]"); return } - let o = buf_new(); buf_putc(o, 91); var first = 1 + let o = buf_new(); buf_putc(o, '['); var first = 1 var i = 0; let N = ntok(D) while i < N { if D.tk_kind[i] == LT_OP and tlen(D, i) == 1 and D.src[D.tk_start[i]] == 123 { @@ -2532,19 +2600,19 @@ program LudicLsp { if m >= 0 { let a = D.tk_line[i]; let b = D.tk_line[m] if b > a { - if first == 0 { buf_putc(o, 44) } + if first == 0 { buf_putc(o, ',') } first = 0 buf_puts(o, "{\"startLine\":"); buf_puti(o, a); buf_puts(o, ",\"endLine\":"); buf_puti(o, b - 1); buf_puts(o, ",\"kind\":\"region\"}") } } } - i = i + 1 + i += 1 } - buf_putc(o, 93); send_result(id, buf_str(o)) + buf_putc(o, ']'); send_result(id, buf_str(o)) } function on_workspace_symbol(msg: JVal, id: JVal) -> void { let q = jstr(jpath(msg, "params.query"), "") - let o = buf_new(); buf_putc(o, 91); var first = 1 + let o = buf_new(); buf_putc(o, '['); var first = 1 var d = 0 while d < len(g_docs) { let D = g_docs[d] @@ -2555,16 +2623,16 @@ program LudicLsp { var keep = true if q[0] != 0 { keep = contains_ci(y.name, q) } if keep { - if first == 0 { buf_putc(o, 44) } + if first == 0 { buf_putc(o, ',') } first = 0 - buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, 125) + buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}') } } - k = k + 1 + k += 1 } - d = d + 1 + d += 1 } - buf_putc(o, 93); send_result(id, buf_str(o)) + buf_putc(o, ']'); send_result(id, buf_str(o)) } function contains_ci(hay: pointer, needle: pointer) -> bool { let hn = cstr_len(hay); let nn = cstr_len(needle) @@ -2573,9 +2641,9 @@ program LudicLsp { while i + nn <= hn { var m = true var j = 0 - while j < nn { if lower(hay[i + j]) != lower(needle[j]) { m = false; break }; j = j + 1 } + while j < nn { if lower(hay[i + j]) != lower(needle[j]) { m = false; break }; j += 1 } if m { return true } - i = i + 1 + i += 1 } return false } @@ -2651,10 +2719,24 @@ program LudicLsp { buf_puts(o, "},\"serverInfo\":{\"name\":\"ludic-lsp\",\"version\":\"1.0.0\"}}") return buf_str(o) } + # the ludicc that ships beside this server binary, when there is one + function compiler_beside_me() -> pointer { + let me = arg(0) + var i = cstr_len(me) + while i > 0 and me[i - 1] != '/' { i -= 1 } + let cand = me[0..i] + "ludicc" + if run(`test -x '{cand}'`) == 0 { return cand } + return null + } function on_initialize(msg: JVal, id: JVal) -> void { let root = jpath(msg, "params.rootUri") if (root != null) and root.t == JSTR { g_root = uri_to_path(root.s) } if (g_root != null) { scan_root(g_root) } + let want = jpath(msg, "params.initializationOptions.compilerDiagnostics") + if (want != null) and want.t == JBOOL and want.b == 1 { + g_compiler = jstr(jpath(msg, "params.initializationOptions.compilerPath"), null) + if (g_compiler == null) or cstr_len(g_compiler) == 0 { g_compiler = compiler_beside_me() } + } send_result(id, caps()) } @@ -2669,15 +2751,15 @@ program LudicLsp { if (method == "textDocument/didOpen") { on_did_open(msg); return } if (method == "textDocument/didChange") { on_did_change(msg); return } if (method == "textDocument/didClose") { on_did_close(msg); return } - if (method == "textDocument/didSave") { on_did_change(msg); return } - if (method == "textDocument/documentSymbol") { let D = doc_of(msg); if (D == null) { send_result(id, "[]"); return }; let o = buf_new(); put_doc_symbols(o, D, 0 - 1); send_result(id, buf_str(o)); return } + if (method == "textDocument/didSave") { on_did_save(msg); return } + if (method == "textDocument/documentSymbol") { let D = doc_of(msg); if (D == null) { send_result(id, "[]"); return }; let o = buf_new(); put_doc_symbols(o, D, -1); send_result(id, buf_str(o)); return } if (method == "workspace/symbol") { on_workspace_symbol(msg, id); return } if (method == "textDocument/definition") or (method == "textDocument/declaration") or (method == "textDocument/typeDefinition") { on_definition(msg, id); return } if (method == "textDocument/hover") { on_hover(msg, id); return } if (method == "textDocument/references") { on_references(msg, id); return } if (method == "textDocument/documentHighlight") { on_references(msg, id); return } if (method == "textDocument/rename") { on_rename(msg, id); return } - if (method == "textDocument/prepareRename") { let D = doc_of(msg); if (D == null) { send_result(id, "null"); return }; let off = off_of(msg, D, "params.position"); let tok = resolve_at(D, off); if tok < 0 or (g_owner == null) or g_symidx < 0 or D.tk_kind[tok] != LT_ID { send_result(id, "null"); return }; let o = buf_new(); buf_puts(o, "{\"range\":"); put_range(o, D, D.tk_start[tok], D.tk_end[tok]); buf_puts(o, ",\"placeholder\":"); jstr_out(o, ttext(D, tok)); buf_putc(o, 125); send_result(id, buf_str(o)); return } + if (method == "textDocument/prepareRename") { let D = doc_of(msg); if (D == null) { send_result(id, "null"); return }; let off = off_of(msg, D, "params.position"); let tok = resolve_at(D, off); if tok < 0 or (g_owner == null) or g_symidx < 0 or D.tk_kind[tok] != LT_ID { send_result(id, "null"); return }; let o = buf_new(); buf_puts(o, "{\"range\":"); put_range(o, D, D.tk_start[tok], D.tk_end[tok]); buf_puts(o, ",\"placeholder\":"); jstr_out(o, ttext(D, tok)); buf_putc(o, '}'); send_result(id, buf_str(o)); return } if (method == "textDocument/formatting") or (method == "textDocument/rangeFormatting") { on_formatting(msg, id); return } if (method == "textDocument/foldingRange") { on_folding(msg, id); return } if (method == "textDocument/inlayHint") { on_inlay_hint(msg, id); return } diff --git a/tools/ludic-tools/ludic_syntax.h b/tools/ludic-tools/ludic_syntax.h index bff8d9ed..b9547758 100644 --- a/tools/ludic-tools/ludic_syntax.h +++ b/tools/ludic-tools/ludic_syntax.h @@ -6,8 +6,8 @@ * addition to the language shows up in every editor at once instead of drifting * across six hand-maintained copies. * - * Unlike the compiler's lexer (compiler/ludicc.c) this one is written for - * editors: it keeps comments, keeps newlines, records byte spans for every + * Unlike the compiler's lexer (selfhost/frontend/lex.ludic) this one is written + * for editors: it keeps comments, keeps newlines, records byte spans for every * token, and never exits on bad input — a stray character becomes an LT_ERR * token and lexing continues, because a file being typed into is malformed most * of the time. @@ -74,7 +74,7 @@ static const char* LUDIC_TYPES[] = { "int","long","fixed","countdown","bool","entity","string","pointer","byte","words","fixeds","pointers","Vector","IVec2","Rect","void", 0 }; static const char* LUDIC_PHASES[] = { - "Start","Input","FixedUpdate","Update","LateUpdate","Render", 0 + "Start","Input","FixedUpdate","Update","LateUpdate","Render","Overlay", 0 }; static const char* LUDIC_WIDGETS[] = { "panel","col","row","label","button","image","spacer", 0 @@ -208,7 +208,7 @@ static void lline_push(LLex* L, int off){ L->linestart[L->nline++] = off; } -static const char* LUDIC_OPS2[] = { "->","+=","-=","*=","/=","==","!=","<=",">=","&&","||","..","=>", 0 }; +static const char* LUDIC_OPS2[] = { "->","+=","-=","*=","/=","==","!=","<=",">=","<<",">>","..","=>", 0 }; /* Lex the whole buffer. Comments and newlines are kept — the formatter needs * both, and a highlighter needs the comments. */ @@ -279,7 +279,7 @@ static void lud_lex(LLex* L, const char* src){ if (matched) continue; } if (c == ';'){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; } - if (strchr("+-*/%<>=(){}[],:.!@", c)){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; } + if (strchr("+-*/%<>=(){}[],:.!@&|^~", c)){ ltok_push(L, LT_OP, i, i + 1, line, 0); i++; continue; } /* Anything else is an error token — but a whole UTF-8 character's worth * at a time. Splitting a multi-byte character into one token per byte * would let a consumer that re-emits tokens (the formatter) put spaces