diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b78b09..6da1406 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,22 +9,98 @@ permissions: jobs: build-and-test: - name: ${{ matrix.os }} build and test + name: ${{ matrix.target }} build, test, and package runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + target: linux-x64 + cmake_arguments: '' + - os: windows-latest + target: win32-x64 + cmake_arguments: -A x64 + - os: macos-15-intel + target: darwin-x64 + cmake_arguments: '' steps: - name: Check out repository uses: actions/checkout@v7 + - name: Set up Python for process smoke tests + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Set up Node for VS Code client tests + uses: actions/setup-node@v5 + with: + node-version: 22 + - name: Configure - run: cmake -S . -B build -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Release + run: >- + cmake -S . -B build + -DBUILD_TESTING=ON + -DCMAKE_BUILD_TYPE=Release + ${{ matrix.cmake_arguments }} - name: Build run: cmake --build build --config Release --parallel - name: Test - run: ctest --test-dir build --build-config Release --output-on-failure \ No newline at end of file + run: ctest --test-dir build --build-config Release --output-on-failure + + - name: Install VS Code client dependencies + working-directory: editors/vscode + run: npm ci + + - name: Test VS Code language client on Linux + if: runner.os == 'Linux' + working-directory: editors/vscode + run: xvfb-run -a npm test + + - name: Test VS Code language client + if: runner.os != 'Linux' + working-directory: editors/vscode + run: npm test + + - name: Package targeted VSIX + working-directory: editors/vscode + env: + RLS_VSCODE_RELEASE_BUILD_DIRECTORY: build + run: npx --no-install vsce package --target ${{ matrix.target }} --out rando-logic-script-${{ matrix.target }}.vsix + + - name: Validate targeted VSIX + working-directory: editors/vscode + run: npm run validate:vsix -- rando-logic-script-${{ matrix.target }}.vsix ${{ matrix.target }} + + - name: Store validated VSIX + uses: actions/upload-artifact@v7 + with: + name: rando-logic-script-${{ matrix.target }} + path: editors/vscode/rando-logic-script-${{ matrix.target }}.vsix + if-no-files-found: error + retention-days: 7 + + editor-grammar-tests: + name: Editor grammar tests + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Node + uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Test TextMate grammar + working-directory: tooling/textmate + run: npm ci && npm test + + - name: Generate and test Tree-sitter grammar + working-directory: tooling/tree-sitter-rls + run: npm ci && npm run generate && npm test \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..910db39 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,166 @@ +name: Release VS Code extension + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + +jobs: + package: + name: Package ${{ matrix.target }} + runs-on: ${{ matrix.os }} + permissions: + contents: read + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: linux-x64 + cmake_arguments: '' + server: build-vscode-release/lsp/rls_language_server + - os: windows-latest + target: win32-x64 + cmake_arguments: -A x64 + server: build-vscode-release/lsp/Release/rls_language_server.exe + - os: macos-15-intel + target: darwin-x64 + cmake_arguments: '' + server: build-vscode-release/lsp/rls_language_server + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python for process smoke test + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Set up Node for VSIX packaging + uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Verify tag matches extension version + run: node -e "const actual=require('./editors/vscode/package.json').version; const tag=process.env.GITHUB_REF_NAME; if (tag !== 'v' + actual) throw new Error('Tag ' + tag + ' does not match extension v' + actual);" + + - name: Configure release server + run: >- + cmake -S . -B build-vscode-release + -DCMAKE_BUILD_TYPE=Release + -DBUILD_TESTING=OFF + -DRLS_STATIC_MSVC_RUNTIME=ON + ${{ matrix.cmake_arguments }} + + - name: Build release server + run: cmake --build build-vscode-release --config Release --target rls_language_server --parallel + + - name: Smoke test release server + run: python lsp/tests/process_smoke.py --server "${{ matrix.server }}" + + - name: Install extension dependencies + working-directory: editors/vscode + run: npm ci + + - name: Package targeted VSIX + working-directory: editors/vscode + env: + RLS_VSCODE_RELEASE_BUILD_DIRECTORY: build-vscode-release + run: npx --no-install vsce package --target ${{ matrix.target }} --out rando-logic-script-${{ github.ref_name }}-${{ matrix.target }}.vsix + + - name: Validate targeted VSIX + working-directory: editors/vscode + run: npm run validate:vsix -- rando-logic-script-${{ github.ref_name }}-${{ matrix.target }}.vsix ${{ matrix.target }} + + - name: Attest VSIX build provenance + uses: actions/attest@v4 + with: + subject-path: editors/vscode/rando-logic-script-${{ github.ref_name }}-${{ matrix.target }}.vsix + + - name: Store targeted VSIX + uses: actions/upload-artifact@v7 + with: + name: rando-logic-script-${{ matrix.target }} + path: editors/vscode/rando-logic-script-${{ github.ref_name }}-${{ matrix.target }}.vsix + if-no-files-found: error + retention-days: 7 + + release: + name: Create GitHub release + needs: package + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download validated VSIX files + uses: actions/download-artifact@v8 + with: + pattern: rando-logic-script-* + path: artifacts + merge-multiple: true + + - name: Verify release artifact set + shell: bash + run: | + mapfile -t artifacts < <(find artifacts -maxdepth 1 -name '*.vsix' -type f | sort) + test "${#artifacts[@]}" -eq 3 + printf '%s\n' "${artifacts[@]}" + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: gh release create "$GITHUB_REF_NAME" artifacts/*.vsix --verify-tag --generate-notes --title "Rando Logic Script $GITHUB_REF_NAME" + + publish-marketplace: + name: Publish to Visual Studio Marketplace + needs: release + runs-on: ubuntu-latest + environment: marketplace-production + permissions: + contents: read + id-token: write + + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Node + uses: actions/setup-node@v5 + with: + node-version: 22 + + - name: Install extension dependencies + working-directory: editors/vscode + run: npm ci + + - name: Download validated VSIX files + uses: actions/download-artifact@v8 + with: + pattern: rando-logic-script-* + path: artifacts + merge-multiple: true + + - name: Sign in to Azure with GitHub OIDC + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Publish validated VSIX files + working-directory: editors/vscode + shell: bash + run: | + mapfile -t packages < <(find ../../artifacts -maxdepth 1 -name '*.vsix' -type f | sort) + test "${#packages[@]}" -eq 3 + for package in "${packages[@]}"; do + npx --no-install vsce publish --azure-credential --packagePath "$package" + done \ No newline at end of file diff --git a/.gitignore b/.gitignore index b942376..088bbb7 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ # Linker files *.ilk +*.exp # Debugger Files *.pdb @@ -68,3 +69,11 @@ vcpkg_installed/ Testing/ .cache/ /.vs + +# VS Code extension build and test output +editors/vscode/node_modules/ +editors/vscode/out/ +editors/vscode/dist/ +editors/vscode/.vscode-test/ +editors/vscode/server/ +editors/vscode/*.vsix \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f6d7b56 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run RLS Language Extension", + "type": "extensionHost", + "request": "launch", + "preLaunchTask": "Prepare RLS Language Extension", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/editors/vscode" + ], + "outFiles": [ + "${workspaceFolder}/editors/vscode/out/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..9b375f4 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,52 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Bundle RLS Language Server", + "type": "shell", + "command": "npm", + "args": [ + "run", + "bundle:lsp" + ], + "options": { + "cwd": "${workspaceFolder}/editors/vscode" + } + }, + { + "label": "Package RLS VS Code Extension", + "type": "shell", + "command": "vsce", + "args": [ + "package" + ], + "options": { + "cwd": "${workspaceFolder}/editors/vscode" + }, + "problemMatcher": [ + "$msCompile" + ] + }, + { + "label": "Compile RLS VS Code Extension", + "type": "shell", + "command": "npm", + "args": [ + "run", + "compile" + ], + "options": { + "cwd": "${workspaceFolder}/editors/vscode" + }, + "problemMatcher": "$tsc" + }, + { + "label": "Prepare RLS Language Extension", + "dependsOrder": "sequence", + "dependsOn": [ + "Bundle RLS Language Server", + "Compile RLS VS Code Extension" + ] + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 91dca9d..d2c10d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,23 @@ cmake_minimum_required(VERSION 3.14) +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() + project(RandoLogicScript) set(CMAKE_CXX_STANDARD 20) +add_library(rls_build_options INTERFACE) +if(APPLE AND CMAKE_CXX_COMPILER_ID MATCHES "Clang") + target_compile_options(rls_build_options INTERFACE -fexperimental-library) + target_link_options(rls_build_options INTERFACE -fexperimental-library) +endif() + +option(RLS_STATIC_MSVC_RUNTIME "Link the MSVC runtime statically" OFF) +if(MSVC AND RLS_STATIC_MSVC_RUNTIME) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() + include(CTest) include(FetchContent) @@ -29,4 +44,6 @@ add_subdirectory(ast) add_subdirectory(parser) add_subdirectory(sema) add_subdirectory(transpilers) -add_subdirectory(console) \ No newline at end of file +add_subdirectory(project) +add_subdirectory(console) +add_subdirectory(lsp) \ No newline at end of file diff --git a/README.md b/README.md index 99c1a82..11a5481 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,14 @@ The goal for RLS is to be a **declarative, domain-specific language** for defini ## Usage ``` -RandoLogicScript [options] +RandoLogicScript [options] [files/folders...] ``` ### Options | Option | Description | | ------------------------- | ------------------------------------------------------------- | +| `-p, --project ` | Load an `rls.json` manifest or a directory containing one. | | `-t, --transpiler ` | Transpiler to use, must be followed by `-o`. May be repeated. | | `-o, --output ` | Output directory for the preceding transpiler. | | `-h, --help` | Show help message. | @@ -30,6 +31,36 @@ RandoLogicScript -t soh -o out/soh/ -t ap -o out/ap/ src/ extra.rls Input paths can be individual `.rls` files or directories (which are recursively scanned for `.rls` files). +### Project Files + +An `rls.json` file describes a project rooted at the directory containing the manifest: + +```json +{ + "version": 1, + "sources": ["src", "stdlib/host.rls"], + "exclude": ["generated/**"], + "transpilers": { + "soh": { "output": "generated/soh" }, + "ap": { "output": "generated/ap" } + } +} +``` + +The published JSON Schema is [project/rls.schema.json](project/rls.schema.json). It validates the structural version-1 contract and relative paths; installed console builds validate available transpiler implementations separately. + +All manifest paths are relative to the manifest and must stay within the project root. Source directories are scanned recursively in deterministic order. Project scans exclude `build`, VCS directories, caches, configured exclusions, and transpiler output directories. An output directory is included only when it is explicitly named in `sources`. + +Run a project explicitly with: + +``` +RandoLogicScript --project path/to/rls.json +``` + +`--project` also accepts a directory containing `rls.json`. When no input files or folders are given, the CLI searches from the current directory upward for the nearest manifest. Explicit input paths remain supported, but cannot be combined with `--project`. + +Manifest transpiler outputs are used by default. Each command-line `-t -o ` pair replaces the manifest output for the same transpiler name and leaves other manifest transpilers enabled. Transpiler names are validated by the CLI's registered implementations. + ### Available Transpilers | Name | Target | @@ -42,3 +73,5 @@ Input paths can be individual `.rls` files or directories (which are recursively - [Language Overview](docs/RandoLogicScript-Overview.md) - [Language Design Doc](docs/RandoLogicScript-Full.md) - [Building Guide](docs/BUILDING.md) +- [Editor Configuration](docs/EDITOR-CONFIGURATION.md) +- [VS Code Extension Release Guide](docs/RELEASING.md) diff --git a/ast/include/ast.h b/ast/include/ast.h index 8fb618b..4cc9120 100644 --- a/ast/include/ast.h +++ b/ast/include/ast.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -22,6 +23,185 @@ struct Position { uint32_t column = 0; }; +/// A half-open range in a source file. +struct SourceRange { + Position start; + Position end; +}; + +/// A 1-based UTF-16 position for external editor consumers. +struct Utf16Position { + uint32_t line = 0; + uint32_t column = 0; +}; + +/// Immutable, validated UTF-8 source text. +/// +/// Source bytes are preserved exactly, including CRLF. `Position` line and +/// column values are 1-based; columns count UTF-8 bytes. Invalid UTF-8 is +/// rejected by factories and edits. +class SourceText { +public: + SourceText() : lineStarts_{0} {} + + static std::optional FromUtf8(std::string content) { + if (!isValidUtf8(content)) return std::nullopt; + return SourceText(std::move(content)); + } + + const std::string& content() const { return content_; } + const std::vector& lineStarts() const { return lineStarts_; } + + std::optional byteOffsetFromUtf8Position(Position position) const { + if (position.line == 0 || position.column == 0 || position.line > lineStarts_.size()) { + return std::nullopt; + } + + const size_t lineStart = lineStarts_[position.line - 1]; + const size_t offset = lineStart + position.column - 1; + const size_t lineLimit = position.line < lineStarts_.size() + ? lineStarts_[position.line] + : content_.size(); + if (offset > lineLimit || (position.line < lineStarts_.size() && offset == lineLimit)) { + return std::nullopt; + } + return offset; + } + + std::optional utf8PositionAtByteOffset(size_t offset) const { + if (offset > content_.size()) return std::nullopt; + + size_t lineIndex = 0; + while (lineIndex + 1 < lineStarts_.size() && lineStarts_[lineIndex + 1] <= offset) { + ++lineIndex; + } + return Position{ + static_cast(lineIndex + 1), + static_cast(offset - lineStarts_[lineIndex] + 1), + }; + } + + std::optional utf16PositionAtByteOffset(size_t offset) const { + const auto utf8Position = utf8PositionAtByteOffset(offset); + if (!utf8Position) return std::nullopt; + + const size_t lineStart = lineStarts_[utf8Position->line - 1]; + size_t cursor = lineStart; + uint32_t utf16Column = 1; + while (cursor < offset) { + const size_t width = utf8CodePointWidth(static_cast(content_[cursor])); + if (cursor + width > offset) return std::nullopt; + utf16Column += width == 4 ? 2 : 1; + cursor += width; + } + return Utf16Position{utf8Position->line, utf16Column}; + } + + std::optional byteOffsetFromUtf16Position(Utf16Position position) const { + if (position.line == 0 || position.column == 0 || position.line > lineStarts_.size()) { + return std::nullopt; + } + + const size_t lineStart = lineStarts_[position.line - 1]; + const size_t lineLimit = position.line < lineStarts_.size() + ? lineStarts_[position.line] + : content_.size(); + size_t cursor = lineStart; + uint32_t utf16Column = 1; + while (cursor < lineLimit && utf16Column < position.column) { + const size_t width = utf8CodePointWidth(static_cast(content_[cursor])); + const uint32_t units = width == 4 ? 2 : 1; + if (utf16Column + units > position.column || cursor + width > lineLimit) { + return std::nullopt; + } + utf16Column += units; + cursor += width; + } + return utf16Column == position.column ? std::optional(cursor) : std::nullopt; + } + + std::optional replaceAll(std::string replacement) const { + return FromUtf8(std::move(replacement)); + } + + std::optional replace(SourceRange range, std::string replacement) const { + const auto start = byteOffsetFromUtf8Position(range.start); + const auto end = byteOffsetFromUtf8Position(range.end); + if (!start || !end || *start > *end || !isValidUtf8(replacement)) return std::nullopt; + + std::string updated; + updated.reserve(*start + replacement.size() + content_.size() - *end); + updated.append(content_, 0, *start); + updated += replacement; + updated.append(content_, *end, std::string::npos); + return FromUtf8(std::move(updated)); + } + + std::optional incompleteTokenRangeAt(Position position) const { + const auto offset = byteOffsetFromUtf8Position(position); + if (!offset) return std::nullopt; + + size_t start = *offset; + while (start > 0 && isTokenByte(static_cast(content_[start - 1]))) --start; + size_t end = *offset; + while (end < content_.size() && isTokenByte(static_cast(content_[end]))) ++end; + if (start == end) return std::nullopt; + + const auto startPosition = utf8PositionAtByteOffset(start); + const auto endPosition = utf8PositionAtByteOffset(end); + return SourceRange{*startPosition, *endPosition}; + } + +private: + std::string content_; + std::vector lineStarts_; + + explicit SourceText(std::string content) : content_(std::move(content)), lineStarts_{0} { + for (size_t offset = 0; offset < content_.size(); ++offset) { + if (content_[offset] == '\n') lineStarts_.push_back(offset + 1); + } + } + + static bool isTokenByte(unsigned char byte) { + return byte == '_' || (byte >= '0' && byte <= '9') || + (byte >= 'A' && byte <= 'Z') || (byte >= 'a' && byte <= 'z'); + } + + static size_t utf8CodePointWidth(unsigned char leadByte) { + if (leadByte < 0x80) return 1; + if (leadByte < 0xE0) return 2; + if (leadByte < 0xF0) return 3; + return 4; + } + + static bool isValidUtf8(std::string_view text) { + for (size_t offset = 0; offset < text.size();) { + const unsigned char leadByte = static_cast(text[offset]); + if (leadByte < 0x80) { + ++offset; + continue; + } + + const size_t width = utf8CodePointWidth(leadByte); + if ((leadByte < 0xC2) || (leadByte > 0xF4) || offset + width > text.size()) { + return false; + } + for (size_t index = 1; index < width; ++index) { + if ((static_cast(text[offset + index]) & 0xC0) != 0x80) return false; + } + const unsigned char secondByte = static_cast(text[offset + 1]); + if ((leadByte == 0xE0 && secondByte < 0xA0) || + (leadByte == 0xED && secondByte >= 0xA0) || + (leadByte == 0xF0 && secondByte < 0x90) || + (leadByte == 0xF4 && secondByte > 0x8F)) { + return false; + } + offset += width; + } + return true; + } +}; + /// A span of source text: the file it came from plus start/end positions. struct Span { std::string file; @@ -53,6 +233,7 @@ enum class IdentifierKind { Unresolved, Parameter, EnumValue, + DeclaredValue, FunctionRef, }; @@ -145,9 +326,11 @@ struct BinaryExpr { BinaryOp op; ExprPtr left; ExprPtr right; + Span operatorSpan; - BinaryExpr(BinaryOp op, ExprPtr left, ExprPtr right) - : op(op), left(std::move(left)), right(std::move(right)) {} + BinaryExpr(BinaryOp op, ExprPtr left, ExprPtr right, Span operatorSpan = {}) + : op(op), left(std::move(left)), right(std::move(right)), + operatorSpan(std::move(operatorSpan)) {} }; /// Ternary expression: ` ? : `. @@ -277,11 +460,13 @@ struct Param { Name name; std::optional type; ExprPtr defaultValue; // nullptr if no default + Span span; - Param(Name name, std::optional type, ExprPtr defaultValue) + Param(Name name, std::optional type, ExprPtr defaultValue, Span span = {}) : name(std::move(name)), type(std::move(type)), - defaultValue(std::move(defaultValue)) {} + defaultValue(std::move(defaultValue)), + span(std::move(span)) {} }; /// A single entry in a region section: `NAME: condition`. @@ -300,9 +485,10 @@ struct Entry { struct Section { SectionKind kind; std::vector entries; + Span span; - Section(SectionKind kind, std::vector entries) - : kind(kind), entries(std::move(entries)) {} + Section(SectionKind kind, std::vector entries, Span span = {}) + : kind(kind), entries(std::move(entries)), span(std::move(span)) {} }; /// One arbitrary data entry in a region body: `key: value`. @@ -338,7 +524,7 @@ struct RegionBody { // == Top-level declarations =================================================== -/// `region RR_KEY { name: "Display Name" scene: SCENE_ID ... }` +/// `region KEY { }` struct RegionDecl { Name key; RegionBody body; @@ -351,7 +537,7 @@ struct RegionDecl { }; /// `extend region RR_NAME { ... }` -/// Extensions can only add sections, not redefine scene, time_passes, or areas. +/// Extensions add sections and cannot add or replace base-region data. struct ExtendRegionDecl { Name name; std::vector
sections; @@ -469,6 +655,12 @@ using Decl = std::variant< enum class DiagnosticLevel { Error, Warning, Info }; +struct DiagnosticActionData { + uint32_t version = 1; + std::string actionKind; + std::vector arguments; +}; + inline std::string levelToString(rls::ast::DiagnosticLevel level) { switch (level) { case rls::ast::DiagnosticLevel::Error: return "error"; @@ -480,9 +672,21 @@ inline std::string levelToString(rls::ast::DiagnosticLevel level) { /// A diagnostic message produced during parsing or semantic analysis. struct Diagnostic { - DiagnosticLevel level; - std::string message; + std::string code; Span span; // location of the offending construct + DiagnosticLevel level = DiagnosticLevel::Error; + std::string message; + std::optional data; + + Diagnostic() = default; + + Diagnostic(std::string code, Span span, DiagnosticLevel level, std::string message, + std::optional data = std::nullopt) + : code(std::move(code)), + span(std::move(span)), + level(level), + message(std::move(message)), + data(std::move(data)) {} }; // == File ===================================================================== @@ -505,6 +709,9 @@ enum class Type { Callable, // generic callable value Condition, // callable with signature () -> Bool Enum, // user-defined or host-defined enum value, identified by Project metadata + Region, // declared region value + Event, // declared event entry value + Location, // declared location entry value Void, // statements / declarations with no value Error, // poison type — inference failed, suppress cascading errors }; @@ -560,6 +767,8 @@ struct Project { std::map RegionDecls; std::map> ExtendRegionDecls; + std::map> EventDecls; + std::map> LocationDecls; std::map DefineDecls; std::map ExternDefineDecls; std::map EnumInfos; diff --git a/ast/tests/ast_tests.cpp b/ast/tests/ast_tests.cpp index 15682dc..6c528f7 100644 --- a/ast/tests/ast_tests.cpp +++ b/ast/tests/ast_tests.cpp @@ -218,6 +218,57 @@ TEST(ExprTests, DefaultSpanIsZero) { EXPECT_EQ(expr->span.end.column, 0u); } +// == Source text ============================================================= + +TEST(SourceTextTests, PreservesCrlfAndConvertsUtf8Offsets) { + const auto source = SourceText::FromUtf8("one\r\ntwo"); + ASSERT_TRUE(source); + ASSERT_EQ(source->lineStarts().size(), 2u); + EXPECT_EQ(source->lineStarts()[0], 0u); + EXPECT_EQ(source->lineStarts()[1], 5u); + EXPECT_EQ(source->byteOffsetFromUtf8Position({1, 4}), 3u); + EXPECT_EQ(source->byteOffsetFromUtf8Position({2, 1}), 5u); + + const auto position = source->utf8PositionAtByteOffset(7); + ASSERT_TRUE(position); + EXPECT_EQ(position->line, 2u); + EXPECT_EQ(position->column, 3u); +} + +TEST(SourceTextTests, ConvertsUtf16PositionsForMultibyteCharacters) { + const auto source = SourceText::FromUtf8("a\xF0\x9F\x98\x80" "b"); + ASSERT_TRUE(source); + const auto afterEmoji = source->utf16PositionAtByteOffset(5); + ASSERT_TRUE(afterEmoji); + EXPECT_EQ(afterEmoji->line, 1u); + EXPECT_EQ(afterEmoji->column, 4u); + EXPECT_EQ(source->byteOffsetFromUtf16Position({1, 4}), 5u); + EXPECT_FALSE(source->byteOffsetFromUtf16Position({1, 3})); +} + +TEST(SourceTextTests, AppliesImmutableRangedAndFullDocumentEdits) { + const auto source = SourceText::FromUtf8("hello world"); + ASSERT_TRUE(source); + const auto edited = source->replace({{1, 7}, {1, 12}}, "RLS"); + ASSERT_TRUE(edited); + EXPECT_EQ(source->content(), "hello world"); + EXPECT_EQ(edited->content(), "hello RLS"); + + const auto replaced = edited->replaceAll("new document"); + ASSERT_TRUE(replaced); + EXPECT_EQ(replaced->content(), "new document"); +} + +TEST(SourceTextTests, RejectsInvalidUtf8AndFindsLexicalTokenRange) { + EXPECT_FALSE(SourceText::FromUtf8("\xC3\x28")); + const auto source = SourceText::FromUtf8("call unfinished_name"); + ASSERT_TRUE(source); + const auto range = source->incompleteTokenRangeAt({1, 14}); + ASSERT_TRUE(range); + EXPECT_EQ(range->start.column, 6u); + EXPECT_EQ(range->end.column, 21u); +} + // == Nested expressions ======================================================= TEST(ExprTests, NestedBinaryExpressions) { diff --git a/console/CMakeLists.txt b/console/CMakeLists.txt index f844ae7..3480584 100644 --- a/console/CMakeLists.txt +++ b/console/CMakeLists.txt @@ -2,15 +2,23 @@ add_executable(RandoLogicScript main.cpp ) -target_link_libraries(RandoLogicScript PRIVATE ast parser sema soh ap) +target_link_libraries(RandoLogicScript PRIVATE ast sema soh ap project) if(BUILD_TESTING) rls_add_gtest(console_acceptance_tests tests/acceptance_soh_tests.cpp tests/acceptance_ap_tests.cpp ) - target_link_libraries(console_acceptance_tests PRIVATE ast parser sema soh ap) + target_link_libraries(console_acceptance_tests PRIVATE ast sema soh ap) target_compile_definitions(console_acceptance_tests PRIVATE RLS_REPO_ROOT="${CMAKE_SOURCE_DIR}" ) + + rls_add_gtest(console_project_tests + tests/cli_project_tests.cpp + ) + add_dependencies(console_project_tests RandoLogicScript) + target_compile_definitions(console_project_tests PRIVATE + RLS_CONSOLE_PATH="$" + ) endif() diff --git a/console/main.cpp b/console/main.cpp index 392607f..f9a0a68 100644 --- a/console/main.cpp +++ b/console/main.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -8,6 +9,7 @@ #include "output.h" #include "parser.h" +#include "project.h" #include "sema.h" #include "ap.h" #include "soh.h" @@ -22,8 +24,9 @@ static void printUsage(const char* program) { << " [options] \n" << "\n" << "Options:\n" - << " -t, --transpiler -o, --output \n" - << " Transpiler and output directory pair (may be repeated).\n" + << " -p, --project Load an rls.json manifest.\n" + << " -t, --transpiler [-o, --output ]\n" + << " Select a configured manifest transpiler, or override its output.\n" << " Available transpilers: soh, ap\n" << " -h, --help Show this help message.\n"; } @@ -38,16 +41,6 @@ static void printDiagnostic(const rls::ast::Diagnostic& d) { std::cerr << levelToString(d.level) << ": " << d.message << "\n"; } -/// Recursively collect all `.rls` files under `dir`. -static std::vector collectFiles(const fs::path& dir) { - std::vector result; - for (const auto& entry : fs::recursive_directory_iterator(dir)) { - if (entry.is_regular_file() && entry.path().extension() == ".rls") - result.push_back(entry.path()); - } - return result; -} - // == transpiler dispatch ===================================================== /// OutputWriter that creates files in a directory on disk. @@ -104,7 +97,9 @@ static bool runTranspiler(const TranspilerConfig& config, const rls::ast::Projec int main(int argc, char* argv[]) { std::vector transpilers; + std::vector selectedManifestTranspilers; std::vector inputs; + std::optional manifestPath; // == parse arguments ================================================= for (int i = 1; i < argc; ++i) { @@ -114,29 +109,39 @@ int main(int argc, char* argv[]) { printUsage(argv[0]); return 0; } - if (arg == "-t" || arg == "--transpiler") { + if (arg == "-p" || arg == "--project") { if (++i >= argc) { std::cerr << "error: " << arg << " requires a value\n"; return 1; } - std::string name = argv[i]; - - // Expect -o/--output immediately after - if (i + 1 >= argc) { - std::cerr << "error: -t " << name << " must be followed by -o \n"; - return 1; - } - std::string nextArg = argv[++i]; - if (nextArg != "-o" && nextArg != "--output") { - std::cerr << "error: -t " << name << " must be followed by -o \n"; + if (manifestPath) { + std::cerr << "error: " << arg << " may only be specified once\n"; return 1; } + manifestPath.emplace(argv[i]); + continue; + } + if (arg == "-t" || arg == "--transpiler") { if (++i >= argc) { - std::cerr << "error: " << nextArg << " requires a value\n"; + std::cerr << "error: " << arg << " requires a value\n"; return 1; } + std::string name = argv[i]; - transpilers.push_back({std::move(name), argv[i]}); + if (i + 1 < argc) { + const std::string nextArg = argv[i + 1]; + if (nextArg == "-o" || nextArg == "--output") { + i += 2; + if (i >= argc) { + std::cerr << "error: " << nextArg << " requires a value\n"; + return 1; + } + transpilers.push_back({std::move(name), argv[i]}); + continue; + } + } + + selectedManifestTranspilers.push_back(std::move(name)); continue; } if (arg.starts_with("-")) { @@ -149,39 +154,96 @@ int main(int argc, char* argv[]) { } // == validate arguments ============================================== - if (inputs.empty()) { - std::cerr << "error: no input files or folders specified\n"; - printUsage(argv[0]); + if (manifestPath && !inputs.empty()) { + std::cerr << "error: --project cannot be combined with explicit input paths\n"; return 1; } - if (transpilers.empty()) { - std::cerr << "error: at least one -t -o pair must be specified\n"; + if (!selectedManifestTranspilers.empty() && !manifestPath && inputs.empty()) { + manifestPath = rls::project::FindManifest(fs::current_path()); + } + if (!selectedManifestTranspilers.empty() && !manifestPath) { + std::cerr << "error: -t without -o requires an rls.json manifest\n"; return 1; } + if (!manifestPath && inputs.empty()) { + manifestPath = rls::project::FindManifest(fs::current_path()); + if (!manifestPath) { + std::cerr << "error: no input files or folders specified, and no rls.json was found\n"; + printUsage(argv[0]); + return 1; + } + } + // == collect source files ============================================ - std::vector sourceFiles; - for (const auto& input : inputs) { - if (!fs::exists(input)) { - std::cerr << "error: path does not exist: " << input << "\n"; + rls::project::SourceCollection collection; + std::optional manifest; + if (manifestPath) { + const auto path = fs::is_directory(*manifestPath) ? *manifestPath / "rls.json" : *manifestPath; + auto loadResult = rls::project::LoadManifest(path); + if (!loadResult.error.empty()) { + std::cerr << "error: " << loadResult.error << "\n"; return 1; } - if (fs::is_directory(input)) { - auto files = collectFiles(input); - if (files.empty()) - std::cerr << "warning: no .rls files found in " << input << "\n"; - sourceFiles.insert(sourceFiles.end(), files.begin(), files.end()); + manifest = std::move(loadResult.config); + collection = rls::project::CollectManifestSources(*manifest); + + const auto addConfiguredTranspiler = [&](const std::string& name) -> bool { + const auto configured = std::ranges::find_if( + manifest->transpilerOutputs, [&name](const auto& output) { + return output.first == name; + }); + if (configured == manifest->transpilerOutputs.end()) { + std::cerr << "error: manifest does not configure transpiler '" << name << "'\n"; + return false; + } + const bool overridden = std::ranges::any_of( + transpilers, [&name](const TranspilerConfig& config) { + return config.name == name; + }); + if (!overridden) { + transpilers.push_back({configured->first, configured->second}); + } + return true; + }; + + if (selectedManifestTranspilers.empty()) { + for (const auto& [name, outputDir] : manifest->transpilerOutputs) { + if (!addConfiguredTranspiler(name)) { + return 1; + } + } } else { - sourceFiles.push_back(input); + for (const auto& name : selectedManifestTranspilers) { + if (!addConfiguredTranspiler(name)) { + return 1; + } + } } + } else { + collection = rls::project::CollectExplicitSources(inputs); } + if (!collection.error.empty()) { + std::cerr << "error: " << collection.error << "\n"; + return 1; + } + for (const auto& warning : collection.warnings) { + std::cerr << "warning: " << warning << "\n"; + } + + const auto& sourceFiles = collection.sourceFiles; if (sourceFiles.empty()) { std::cerr << "error: no source files to process\n"; return 1; } + if (transpilers.empty()) { + std::cerr << "error: at least one configured or explicit transpiler is required\n"; + return 1; + } + // == parse =========================================================== rls::ast::Project project; bool hasParseErrors = false; diff --git a/console/tests/acceptance_ap_tests.cpp b/console/tests/acceptance_ap_tests.cpp index 1da955b..ae1f82f 100644 --- a/console/tests/acceptance_ap_tests.cpp +++ b/console/tests/acceptance_ap_tests.cpp @@ -4,7 +4,7 @@ using namespace rls::acceptance_tests; TEST(AcceptanceAp, ExamplesRlsMatchesGolden) { std::vector errors; - const auto project = parseAndAnalyzeProject(repoPath("examples/rls"), errors); + const auto project = parseAndAnalyzeProject(repoPath("examples/soh/src"), errors); ASSERT_TRUE(errors.empty()) << joinLines(errors); TempDirectory outputDir("ap"); @@ -15,6 +15,6 @@ TEST(AcceptanceAp, ExamplesRlsMatchesGolden) { expectDirectoryMatchesGolden( outputDir.path(), - repoPath("examples/ap"), - R"(.\build\console\RandoLogicScript.exe -t ap -o .\examples\ap .\examples\rls)"); + repoPath("examples/soh/out_ap"), + R"(.\build\console\RandoLogicScript.exe -p .\examples\soh\rls.json -t ap)"); } diff --git a/console/tests/acceptance_soh_tests.cpp b/console/tests/acceptance_soh_tests.cpp index e261592..7a4b817 100644 --- a/console/tests/acceptance_soh_tests.cpp +++ b/console/tests/acceptance_soh_tests.cpp @@ -4,7 +4,7 @@ using namespace rls::acceptance_tests; TEST(AcceptanceSoh, ExamplesRlsMatchesGolden) { std::vector errors; - const auto project = parseAndAnalyzeProject(repoPath("examples/rls"), errors); + const auto project = parseAndAnalyzeProject(repoPath("examples/soh/src"), errors); ASSERT_TRUE(errors.empty()) << joinLines(errors); TempDirectory outputDir("soh"); @@ -15,6 +15,6 @@ TEST(AcceptanceSoh, ExamplesRlsMatchesGolden) { expectDirectoryMatchesGolden( outputDir.path(), - repoPath("examples/soh"), - R"(.\build\console\RandoLogicScript.exe -t soh -o .\examples\soh .\examples\rls)"); + repoPath("examples/soh/out_soh"), + R"(.\build\console\RandoLogicScript.exe -p .\examples\soh\rls.json -t soh)"); } diff --git a/console/tests/cli_project_tests.cpp b/console/tests/cli_project_tests.cpp new file mode 100644 index 0000000..5fac45c --- /dev/null +++ b/console/tests/cli_project_tests.cpp @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include + +#include + +namespace fs = std::filesystem; + +namespace { + +class TemporaryDirectory { +public: + TemporaryDirectory() : path_(fs::temp_directory_path() / + ("rls-console-tests-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()))) { + fs::create_directories(path_); + } + + ~TemporaryDirectory() { + std::error_code error; + fs::remove_all(path_, error); + } + + const fs::path& path() const { return path_; } + +private: + fs::path path_; +}; + +void writeFile(const fs::path& path, const std::string& content) { + fs::create_directories(path.parent_path()); + std::ofstream(path) << content; +} + +int runConsole(const std::string& arguments, const fs::path& output) { +#ifdef _WIN32 + const auto command = "cmd.exe /D /S /C \"\"" + std::string(RLS_CONSOLE_PATH) + "\" " + + arguments + " > \"" + output.string() + "\" 2>&1\""; +#else + const auto command = "\"" + std::string(RLS_CONSOLE_PATH) + "\" " + arguments + + " > \"" + output.string() + "\" 2>&1"; +#endif + return std::system(command.c_str()); +} + +int runConsoleFrom(const fs::path& directory, const std::string& arguments, const fs::path& output) { + const auto previousDirectory = fs::current_path(); + fs::current_path(directory); + const auto exitCode = runConsole(arguments, output); + fs::current_path(previousDirectory); + return exitCode; +} + +TEST(ConsoleProject, LoadsManifestAndUsesConfiguredOutput) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src"], + "transpilers": { "ap": { "output": "generated/ap" } } + })"); + writeFile(directory.path() / "src" / "logic.rls", "define smoke(): true\n"); + + EXPECT_EQ(runConsole("--project \"" + directory.path().string() + "\"", + directory.path() / "console.log"), 0); + EXPECT_TRUE(fs::exists(directory.path() / "generated" / "ap" / "ap.py")); +} + +TEST(ConsoleProject, SelectsOnlyOneConfiguredManifestTranspiler) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src"], + "transpilers": { + "ap": { "output": "generated/ap" }, + "soh": { "output": "generated/soh" } + } + })"); + writeFile(directory.path() / "src" / "logic.rls", "define smoke(): true\n"); + + EXPECT_EQ(runConsole("--project \"" + directory.path().string() + "\" -t ap", + directory.path() / "selection.log"), 0); + EXPECT_TRUE(fs::exists(directory.path() / "generated" / "ap" / "ap.py")); + EXPECT_FALSE(fs::exists(directory.path() / "generated" / "soh")); +} + +TEST(ConsoleProject, RejectsUnconfiguredManifestTranspilerSelection) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src"], + "transpilers": { "ap": { "output": "generated/ap" } } + })"); + writeFile(directory.path() / "src" / "logic.rls", "define smoke(): true\n"); + + EXPECT_NE(runConsole("--project \"" + directory.path().string() + "\" -t soh", + directory.path() / "selection-error.log"), 0); +} + +TEST(ConsoleProject, DiscoversManifestFromCurrentDirectory) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src"], + "transpilers": { "ap": { "output": "generated/ap" } } + })"); + writeFile(directory.path() / "src" / "logic.rls", "define smoke(): true\n"); + + EXPECT_EQ(runConsoleFrom(directory.path() / "src", "", directory.path() / "discovery.log"), 0); + EXPECT_TRUE(fs::exists(directory.path() / "generated" / "ap" / "ap.py")); +} + +TEST(ConsoleProject, SupportsExplicitInputsAndRejectsUnknownTranspilers) { + TemporaryDirectory directory; + writeFile(directory.path() / "logic.rls", "define smoke(): true\n"); + + EXPECT_EQ(runConsole("-t ap -o \"" + (directory.path() / "ap").string() + "\" \"" + + (directory.path() / "logic.rls").string() + "\"", directory.path() / "explicit.log"), 0); + EXPECT_TRUE(fs::exists(directory.path() / "ap" / "ap.py")); + + EXPECT_NE(runConsole("-t unknown -o \"" + (directory.path() / "unknown").string() + "\" \"" + + (directory.path() / "logic.rls").string() + "\"", directory.path() / "unknown.log"), 0); +} + +} // namespace \ No newline at end of file diff --git a/docs/EDITOR-CONFIGURATION.md b/docs/EDITOR-CONFIGURATION.md new file mode 100644 index 0000000..63a2774 --- /dev/null +++ b/docs/EDITOR-CONFIGURATION.md @@ -0,0 +1,21 @@ +# Editor Configuration + +## Completion Snippet Indentation + +Editors can choose who supplies indentation for multiline section completion snippets through the language-server initialization option: + +```json +{ + "completion": { + "sectionSnippetIndentation": "client" + } +} +``` + +Use `client` when the editor adjusts indentation for multiline completion text. The language server sends relative indentation and sets LSP `insertTextMode` to `adjustIndentation`. + +Use `server` when the editor inserts snippet text as-is. The language server embeds the current line's indentation and sets `insertTextMode` to `asIs`. + +The language server defaults to `server`. The VS Code extension exposes this option as `randoLogicScript.completion.sectionSnippetIndentation` and defaults to `client`. + +Restart the language server after changing this setting. \ No newline at end of file diff --git a/docs/RELEASING.md b/docs/RELEASING.md new file mode 100644 index 0000000..923dab8 --- /dev/null +++ b/docs/RELEASING.md @@ -0,0 +1,119 @@ +# Releasing the VS Code Extension + +The extension is released as separate VSIX files for these targets: + +| VS Code target | Runner | Native server | +| --- | --- | --- | +| `win32-x64` | `windows-latest` | Statically linked Release executable | +| `linux-x64` | `ubuntu-22.04` | Release executable | +| `darwin-x64` | `macos-15-intel` | Release executable targeting macOS 15 or later | + +Native executables are build artifacts. Do not commit them to Git. CI stores validated +VSIX files for seven days, and tagged releases store them permanently as GitHub Release +assets. Each targeted VSIX contains exactly one compatible native server. + +## Release Process + +1. Update the extension version without creating a tag: + + ```sh + cd editors/vscode + npm version --no-git-tag-version + ``` + +2. Update `editors/vscode/CHANGELOG.md`, then commit the version and release notes. +3. Wait for CI to pass. CI compiles and tests the project, packages each targeted VSIX, + validates its manifest and native executable, and stores the validated deliverables. +4. Create and push a matching tag: + + ```sh + git tag v0.1.0 + git push origin v0.1.0 + ``` + +5. The release workflow repeats the clean Release builds, runs the language-server + process smoke test, validates each targeted VSIX, records signed GitHub build + provenance, and creates the GitHub Release. + +The workflow rejects a tag whose version does not exactly match +`editors/vscode/package.json`. + +## Verifying Build Provenance + +Each released VSIX has a GitHub artifact attestation that binds its SHA-256 digest to +this repository, the release workflow, the source commit, and the GitHub Actions build +identity. After downloading a VSIX, verify it online with GitHub CLI: + +```sh +gh attestation verify \ + rando-logic-script-v0.1.0-linux-x64.vsix \ + --repo xxAtrain223/RandoLogicScript +``` + +Use `--format json` to inspect the complete provenance statement: + +```sh +gh attestation verify \ + rando-logic-script-v0.1.0-linux-x64.vsix \ + --repo xxAtrain223/RandoLogicScript \ + --format json +``` + +Verification fails if the VSIX has been modified or if its attestation was not issued +for this repository. Attestation proves origin and integrity; it does not by itself +claim that an independent rebuild will be byte-for-byte identical. + +## Marketplace Publication + +Marketplace publishing uses Microsoft Entra workload identity federation. GitHub +Actions exchanges its short-lived OIDC token for an Azure credential, and `vsce` +uses that credential to publish. No personal access token or client secret is stored. + +### One-Time Configuration + +1. Create a user-assigned managed identity in Azure and grant it the Reader role on + the subscription used by the release workflow. +2. Add a federated identity credential for this repository and the + `marketplace-production` GitHub environment. Its subject is: + + ```text + repo:xxAtrain223/RandoLogicScript:environment:marketplace-production + ``` + + Use `api://AzureADTokenExchange` as the audience. +3. Run `azure/login` with the federated identity, then retrieve its Azure DevOps + profile resource ID with Azure CLI: + + ```sh + az rest \ + --url https://app.vssps.visualstudio.com/_apis/profile/profiles/me \ + --resource 499b84ac-1321-427f-aa17-267ca6975798 \ + --query id \ + --output tsv + ``` + +4. In the Visual Studio Marketplace publisher management page, add that resource ID + to publisher `xxAtrain223` with the Contributor role. +5. Create a protected GitHub environment named `marketplace-production`. Add these + environment secrets: + + - `AZURE_CLIENT_ID`: managed identity client ID + - `AZURE_TENANT_ID`: Microsoft Entra tenant ID + - `AZURE_SUBSCRIPTION_ID`: Azure subscription ID + +The identifiers select the federated identity; none is a password. Restricting them to +the protected environment keeps Marketplace publication separate from pull-request and +ordinary CI jobs. + +### Automated Publication + +After all targeted packages pass validation, the tag workflow creates the GitHub +Release and enters the `marketplace-production` environment. It signs in through +`azure/login`, then publishes every validated platform package with: + +```sh +npx --no-install vsce publish --azure-credential --packagePath +``` + +Publish every target for a version. Configure required reviewers on the GitHub +environment if Marketplace publication should require manual approval. \ No newline at end of file diff --git a/docs/RandoLogicScript-Full.md b/docs/RandoLogicScript-Full.md index 81081bb..0a8c9d8 100644 --- a/docs/RandoLogicScript-Full.md +++ b/docs/RandoLogicScript-Full.md @@ -98,7 +98,10 @@ extend region RR_SPIRIT_TEMPLE_FOYER { | `bool` | `true`, `false`, comparisons, logical expressions | Boolean value. | | `int` | `0`, `3`, arithmetic expressions | Integer value. | | `Condition` | `has(RG_HOOKSHOT)`, a zero-argument callable | Callable condition. | -| Enum name | `Item`, `Setting`, `Region`, `Check`, `Distance`, `Color`, `EnemyDistance` | A first-class enum type declared by `enum` or `extern enum`. | +| `Region` | A key declared by `region`, or `here` | Declared region value. | +| `Event` | A name declared in an `events` section | Declared event value. | +| `Location` | A name declared in a `locations` section | Declared location value. | +| Enum name | `Item`, `Setting`, `Distance`, `Color`, `EnemyDistance` | A first-class enum type declared by `enum` or `extern enum`. | Host enum names use the **same identifiers as their target-language enums**. This keeps generated references straightforward; RLS resolves only its declared enum members and patterns, while the target compiler validates host-specific spelling. @@ -106,22 +109,24 @@ Host enum names use the **same identifiers as their target-language enums**. Thi RLS does not require type annotations in most cases - the transpiler infers types at transpile time from context: -1. **Enum identifiers are resolved from declarations.** Normal and extern enum declarations provide known members and wildcard-pattern matches. Host value categories, including setting keys, regions, and checks, are ordinary named enums; for example `extern enum Setting { RSK_*, RO_* }`, `extern enum Region { RR_* }`, and `extern enum Check { RC_* }`. If a bare identifier matches multiple enums, this is a hard error and requires dotted disambiguation (`EnumName.ValueName`). +1. **Declared domain values resolve before enum patterns.** Region keys and names declared in `events` or `locations` sections have built-in `Region`, `Event`, or `Location` types. Repeating the same event or location name in multiple regions still denotes one symbolic value. A name used across different domain categories is ambiguous. -2. **Host-call signatures are declared with `extern define`.** For example, `extern define has(item: Item) -> bool`, `extern define keys(scene: Scene, n: int) -> int`, and `extern define trick(key: Trick) -> bool`. The transpiler validates arguments against these declared signatures. +2. **Enum identifiers are resolved from declarations.** Normal and extern enum declarations provide known members and wildcard-pattern matches. Host fallback constants can coexist through matching enum names: enum `Region` values are compatible with `Region`, enum `Event` values with `Event`, and enum `Location` values with `Location`. `Logic` and `Check` are not aliases. If a bare identifier matches multiple unrelated enums, this is a hard error and requires dotted disambiguation (`EnumName.ValueName`). -3. **`match` arms provide type context.** `match distance { ED_CLOSE: ... }` tells the transpiler the discriminant is `Distance`. If a call site passes a non-`Distance` value, it's a type error. +3. **Host-call signatures are declared with `extern define`.** For example, `extern define has(item: Item) -> bool`, `extern define keys(scene: Scene, n: int) -> int`, and `extern define trick(key: Trick) -> bool`. The transpiler validates arguments against these declared signatures. -4. **`define` parameters are inferred from usage.** If you write `define foo(d): can_hit_switch(d)` and `can_hit_switch` expects a `Distance` first argument, the transpiler infers `d: Distance`. If a call site passes `foo(RG_HOOKSHOT)`, that's a type error. +4. **`match` arms provide type context.** `match distance { ED_CLOSE: ... }` tells the transpiler the discriminant is `Distance`. If a call site passes a non-`Distance` value, it's a type error. -5. **Literals and booleans.** Number literals are `int`. `true`/`false` (and their aliases `always`/`never`) are `bool`. `and`/`or`/`not` produce `bool`. Condition expressions in `locations`/`exits`/`events` must be `bool`. Integers have an implicit conversion to `bool` - zero is `false`, non-zero is `true` - so functions returning a count can be used directly in conditions. +5. **`define` parameters are inferred from usage.** If you write `define foo(d): can_hit_switch(d)` and `can_hit_switch` expects a `Distance` first argument, the transpiler infers `d: Distance`. If a call site passes `foo(RG_HOOKSHOT)`, that's a type error. -6. **Enum/int implicit conversion is context-aware.** +6. **Literals and booleans.** Number literals are `int`. `true`/`false` (and their aliases `always`/`never`) are `bool`. `and`/`or`/`not` produce `bool`. Condition expressions in `locations`/`exits`/`events` must be `bool`. Integers have an implicit conversion to `bool` - zero is `false`, non-zero is `true` - so functions returning a count can be used directly in conditions. + +7. **Enum/int implicit conversion is context-aware.** - `enum -> int` is allowed in arithmetic, comparison, and call binding where `int` is expected. - `int -> enum` is allowed where an enum is expected (for example, a typed parameter). - If an integer literal could map to multiple enum identities and there is no explicit enum context, this is a hard ambiguity error requiring explicit disambiguation. -7. **Enum identity is enforced for enum-typed parameters.** Two enum-typed values must belong to the same enum identity when binding enum parameters, unless an explicit `int` conversion path is used. +8. **Enum identity is enforced for enum-typed parameters.** Two enum-typed values must belong to the same enum identity when binding enum parameters, unless an explicit `int` conversion path is used. ### 3.4 Enum Declarations And Resolution @@ -140,7 +145,7 @@ extern enum Setting { } extern enum Region { RR_* } -extern enum Check { RC_* } +extern enum Location { RC_* } enum WaterLevel { WL_LOW = 0, diff --git a/editors/vscode/.vscodeignore b/editors/vscode/.vscodeignore new file mode 100644 index 0000000..188b031 --- /dev/null +++ b/editors/vscode/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +src/** +test-fixture/** +out/** +scripts/** +node_modules/** +package-lock.json +tsconfig.json +**/*.map +**/*.ts \ No newline at end of file diff --git a/editors/vscode/CHANGELOG.md b/editors/vscode/CHANGELOG.md new file mode 100644 index 0000000..593e3fe --- /dev/null +++ b/editors/vscode/CHANGELOG.md @@ -0,0 +1,10 @@ +# Change Log + +## 0.1.0 + +- Added Rando Logic Script syntax highlighting and editor configuration. +- Added native language-server diagnostics, completion, signature help, hover, + navigation, symbols, semantic highlighting, and rename support. +- Added project discovery through `rls.json` and support for file and untitled + documents. +- Added targeted Windows x64, Linux x64, and Intel macOS packages. \ No newline at end of file diff --git a/editors/vscode/LICENSE b/editors/vscode/LICENSE new file mode 100644 index 0000000..287597c --- /dev/null +++ b/editors/vscode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 xxAtrain223 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/editors/vscode/README.md b/editors/vscode/README.md new file mode 100644 index 0000000..6c8445b --- /dev/null +++ b/editors/vscode/README.md @@ -0,0 +1,66 @@ +# Rando Logic Script for VS Code + +Language support for Rando Logic Script (`.rls`) files and `rls.json` projects. + +## Features + +- Syntax highlighting, bracket matching, comments, and editor configuration. +- Live parser, semantic, and project-configuration diagnostics. +- Context-aware completion and snippets. +- Signature help and hover information. +- Go to definition, references, document highlights, and symbol search. +- Semantic highlighting and project-wide rename. +- Automatic `rls.json` project discovery and file watching. + +The extension starts a bundled native language server and communicates with it over +stdio. Analysis remains local to the VS Code extension host. + +## Requirements + +- VS Code 1.82 or later. +- A supported x64 extension host: Windows, a glibc-based Linux distribution, or + macOS 15 or later on Intel hardware. + +When using Remote SSH, WSL, or a Dev Container, install the extension in the remote +environment. The native server runs where the VS Code workspace extension host runs. + +## Configuration + +| Setting | Default | Description | +| --- | --- | --- | +| `randoLogicScript.server.path` | Empty | Override the bundled language-server executable. Relative paths resolve from the first workspace folder. | +| `randoLogicScript.server.arguments` | `[]` | Additional arguments passed to the language server. | +| `randoLogicScript.completion.sectionSnippetIndentation` | `client` | Choose whether VS Code or the server supplies multiline snippet indentation. | +| `randoLogicScript.trace.server` | `off` | Log language-client protocol messages for troubleshooting. | + +After changing the server path, arguments, or snippet-indentation mode, run +**Rando Logic Script: Restart Language Server** from the Command Palette. + +## Troubleshooting + +If the bundled server cannot be found or launched, configure +`randoLogicScript.server.path` with an executable built from the +[RandoLogicScript repository](https://github.com/xxAtrain223/RandoLogicScript). +Enable `randoLogicScript.trace.server` when collecting protocol logs for an +[issue report](https://github.com/xxAtrain223/RandoLogicScript/issues). + +## Development + +1. Configure and build the repository with CMake. +2. Run `npm ci` in this directory. +3. Run `npm test` for the extension-host integration test, or use the repository's **Run RLS Language Extension** launch configuration. + +During repository development, the extension discovers common CMake outputs under `build/` and `build-vs/`. Set `randoLogicScript.server.path` to use another executable. The `RLS_LANGUAGE_SERVER_PATH` environment variable is available for automated tests. + +Use **Rando Logic Script: Restart Language Server** after changing the configured executable or arguments. + +### Packaging + +The extension is published as targeted VSIX packages for Windows x64, Linux x64, and +Intel macOS. Each package contains a native Release server under +`server/-/rls_language_server[.exe]`. VS Code selects the +package matching the extension host platform. + +Native servers and VSIX files are generated artifacts and are not committed. See the +[release guide](../../docs/RELEASING.md) for the CI/CD and Marketplace publication +process. \ No newline at end of file diff --git a/editors/vscode/language-configuration.json b/editors/vscode/language-configuration.json new file mode 100644 index 0000000..489e3bb --- /dev/null +++ b/editors/vscode/language-configuration.json @@ -0,0 +1,23 @@ +{ + "comments": { + "lineComment": "#" + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"", "notIn": ["string", "comment"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""] + ], + "wordPattern": "[A-Za-z_][A-Za-z0-9_]*" +} \ No newline at end of file diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json new file mode 100644 index 0000000..962e5cc --- /dev/null +++ b/editors/vscode/package-lock.json @@ -0,0 +1,4916 @@ +{ + "name": "rando-logic-script", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rando-logic-script", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.17.30", + "@types/vscode": "1.82.0", + "@vscode/test-electron": "^3.1.0", + "@vscode/vsce": "^3.6.2", + "esbuild": "^0.28.2", + "jszip": "^3.10.1", + "typescript": "^5.8.2" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/@azu/format-text": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@azu/format-text/-/format-text-1.0.2.tgz", + "integrity": "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@azu/style-format": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azu/style-format/-/style-format-1.0.1.tgz", + "integrity": "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "@azu/format-text": "^1.0.1" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.2.0.tgz", + "integrity": "sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.11.0.tgz", + "integrity": "sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.11.0.tgz", + "integrity": "sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-process": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@azure/core-process/-/core-process-1.0.0.tgz", + "integrity": "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.25.0.tgz", + "integrity": "sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.4.0.tgz", + "integrity": "sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.14.0.tgz", + "integrity": "sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.2.tgz", + "integrity": "sha512-NXL2/pCJctLxgw8bvrwwgge743kEq8LBT+O1pmV0vyUwetzFPH9auP6jhkU/cgZCPPtWoewAe3ncaGCgPo07fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-process": "^1.0.0", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.5", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.4.0.tgz", + "integrity": "sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.19.0.tgz", + "integrity": "sha512-DHe9iRcyByGJuLPkl0K31a1JjOdRY2zX38Q07mQpSbR8zOj1EIgsWfTXhSQVyyijUxlcofVy/br7qWJbrMwVXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.13.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.13.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.13.0.tgz", + "integrity": "sha512-rOAy0KUcyBbdwVJ+f3uPpthXatFLLZN+/KWAsTLzk1aB23Xl9DRmmXYwSvBFOZyXj4jUQQ5FKxxRkhAFW1fOow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.6.0.tgz", + "integrity": "sha512-uFY9NxrWHw8PwZx7gAX6PDn+9vdfS05+levc/kwkx77IkjfaldnQbbcQzzDIZ5Hq5Zdr6/z92oAIoRWKp6MnOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.13.0", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@secretlint/config-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", + "integrity": "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/config-loader": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-10.2.2.tgz", + "integrity": "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "ajv": "^8.17.1", + "debug": "^4.4.1", + "rc-config-loader": "^4.1.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-10.2.2.tgz", + "integrity": "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/profiler": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "structured-source": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-10.2.2.tgz", + "integrity": "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/resolver": "^10.2.2", + "@secretlint/types": "^10.2.2", + "@textlint/linter-formatter": "^15.2.0", + "@textlint/module-interop": "^15.2.0", + "@textlint/types": "^15.2.0", + "chalk": "^5.4.1", + "debug": "^4.4.1", + "pluralize": "^8.0.0", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "terminal-link": "^4.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/formatter/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@secretlint/node": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-10.2.2.tgz", + "integrity": "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-loader": "^10.2.2", + "@secretlint/core": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "@secretlint/source-creator": "^10.2.2", + "@secretlint/types": "^10.2.2", + "debug": "^4.4.1", + "p-map": "^7.0.3" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/profiler": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-10.2.2.tgz", + "integrity": "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/resolver": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-10.2.2.tgz", + "integrity": "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@secretlint/secretlint-formatter-sarif": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-formatter-sarif/-/secretlint-formatter-sarif-10.2.2.tgz", + "integrity": "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-sarif-builder": "^3.2.0" + } + }, + "node_modules/@secretlint/secretlint-rule-no-dotenv": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-no-dotenv/-/secretlint-rule-no-dotenv-10.2.2.tgz", + "integrity": "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/secretlint-rule-preset-recommend": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-10.2.2.tgz", + "integrity": "sha512-K3jPqjva8bQndDKJqctnGfwuAxU2n9XNCPtbXVI5JvC7FnQiNg/yWlQPbMUlBXtBoBGFYp08A94m6fvtc9v+zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/source-creator": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-10.2.2.tgz", + "integrity": "sha512-h6I87xJfwfUTgQ7irWq7UTdq/Bm1RuQ/fYhA3dtTIAop5BwSFmZyrchph4WcoEvbN460BWKmk4RYSvPElIIvxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/types": "^10.2.2", + "istextorbinary": "^9.5.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@secretlint/types": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-10.2.2.tgz", + "integrity": "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@textlint/ast-node-types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.8.0.tgz", + "integrity": "sha512-5CiH9COYmovWmExQgs7763DzX6Gy9zjkjJ7JxCC95wyTcjwQn/8poNF6fv3qzRlmx8CRRde8DHr9FcgAAiPzgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.8.0.tgz", + "integrity": "sha512-+oU3A235NATv6Lzi4xa4kJ65PuNJlIxesaO4AvDhDWA9FWm7y4XKWaoQCW1esgaQQ6dwnUiFKArQ8TcJ86mC4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azu/format-text": "^1.0.2", + "@azu/style-format": "^1.0.1", + "@textlint/module-interop": "15.8.0", + "@textlint/resolver": "15.8.0", + "@textlint/types": "15.8.0", + "debug": "^4.4.3", + "js-yaml": "^4.3.0", + "lodash": "^4.18.1", + "pluralize": "^2.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "table": "^6.9.0", + "text-table": "^0.2.0" + }, + "engines": { + "node": ">=20.18.0" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/linter-formatter/node_modules/pluralize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-2.0.0.tgz", + "integrity": "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/linter-formatter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@textlint/module-interop": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.8.0.tgz", + "integrity": "sha512-rt+OR1WYGoLOY8HkA/aBPrqufF6yUUEsKEAh7XohTsT3lp9IyZFT6zOIbjul9P4FAzsmSPkcrYjVx3Bz/IUfkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/resolver": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.8.0.tgz", + "integrity": "sha512-E88tzfX3K8Jykk+38aJ9cy8RquD8ABVOPTO2rFEESq0wcg8x6/ypdAS8ZgR7OKiGqlRF0hkO/m5PbQwVfKM3VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@textlint/types": { + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.8.0.tgz", + "integrity": "sha512-Anhc6y5736YIsvqae0U6k0YmB2M/QVHkEeOv2aydAn/WIkdI69dCOiDbe3/+RagS3qstFTSFWJzNRA2lUjv19w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@textlint/ast-node-types": "15.8.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/vscode": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.82.0.tgz", + "integrity": "sha512-VSHV+VnpF8DEm8LNrn8OJ8VuUNcBzN3tMvKrNpbhhfuVjFm82+6v44AbDhLvVFgCzn6vs94EJNTp7w8S6+Q1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.8.tgz", + "integrity": "sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@vscode/test-electron": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-3.1.0.tgz", + "integrity": "sha512-CRqv5u+YYoseuNVJ6Tyo4k0sF0mx4qnKMihRB0PjsUF8Dc0WKtCXo6CNL6nWWm5esfFQsQA/pejMj4ZbpJVLTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.6.2.tgz", + "integrity": "sha512-gvBfarWF+Ii20ESqjA3dpnPJpQJ8fFJYtcWtjwbRADommCzGg1emtmb34E+DKKhECYvaVyAl+TF9lWS/3GSPvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@secretlint/node": "^10.1.2", + "@secretlint/secretlint-formatter-sarif": "^10.1.2", + "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", + "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^4.1.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "secretlint": "^10.1.2", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.1.0.tgz", + "integrity": "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/binaryextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-6.11.0.tgz", + "integrity": "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/boundary": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boundary/-/boundary-2.0.0.tgz", + "integrity": "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/editions": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/editions/-/editions-6.22.0.tgz", + "integrity": "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "version-range": "^4.15.0" + }, + "engines": { + "ecmascript": ">= es5", + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/fs-extra": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", + "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istextorbinary": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-9.5.0.tgz", + "integrity": "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "binaryextensions": "^6.11.0", + "editions": "^6.21.0", + "textextensions": "^6.11.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "optional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.6.tgz", + "integrity": "sha512-I4Prw6ivkd6p8PiYR1tXASOAOBzIJwu0TB7fqaX0c/8c3QAehNYmX57EijyGGGBt3c/BIowGwV03RVBtXvHEVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc-config-loader": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.4.tgz", + "integrity": "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "js-yaml": "^4.1.1", + "json5": "^2.2.3", + "require-from-string": "^2.0.2" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/secretlint": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-10.2.2.tgz", + "integrity": "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@secretlint/config-creator": "^10.2.2", + "@secretlint/formatter": "^10.2.2", + "@secretlint/node": "^10.2.2", + "@secretlint/profiler": "^10.2.2", + "debug": "^4.4.1", + "globby": "^14.1.0", + "read-pkg": "^9.0.1" + }, + "bin": { + "secretlint": "bin/secretlint.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-source": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/structured-source/-/structured-source-4.0.0.tgz", + "integrity": "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boundary": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terminal-link": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", + "integrity": "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^3.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/textextensions": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-6.11.0.tgz", + "integrity": "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==", + "dev": true, + "license": "Artistic-2.0", + "dependencies": { + "editions": "^6.21.0" + }, + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/version-range": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", + "integrity": "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==", + "dev": true, + "license": "Artistic-2.0", + "engines": { + "node": ">=4" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageclient/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/editors/vscode/package.json b/editors/vscode/package.json new file mode 100644 index 0000000..b1e807d --- /dev/null +++ b/editors/vscode/package.json @@ -0,0 +1,195 @@ +{ + "name": "rando-logic-script", + "displayName": "Rando Logic Script", + "description": "Language support for Rando Logic Script, including syntax highlighting and live compiler diagnostics.", + "version": "0.1.0", + "publisher": "xxAtrain223", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/xxAtrain223/RandoLogicScript" + }, + "homepage": "https://github.com/xxAtrain223/RandoLogicScript#readme", + "bugs": { + "url": "https://github.com/xxAtrain223/RandoLogicScript/issues" + }, + "pricing": "Free", + "keywords": [ + "rando logic script", + "rls", + "randomizer", + "language server", + "domain specific language" + ], + "engines": { + "vscode": "^1.82.0" + }, + "main": "./dist/extension.js", + "extensionKind": [ + "workspace" + ], + "capabilities": { + "virtualWorkspaces": { + "supported": false, + "description": "RLS projects and the native language server require a local or remote file system." + } + }, + "categories": [ + "Programming Languages" + ], + "galleryBanner": { + "color": "#1f2933", + "theme": "dark" + }, + "contributes": { + "languages": [ + { + "id": "rls", + "aliases": [ + "Rando Logic Script", + "RLS" + ], + "extensions": [ + ".rls" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "rls", + "scopeName": "source.rls", + "path": "./syntaxes/rls.tmLanguage.json" + } + ], + "semanticTokenScopes": [ + { + "language": "rls", + "scopes": { + "function": [ + "entity.name.function.rls" + ], + "function.defaultLibrary": [ + "entity.name.function.rls" + ], + "parameter": [ + "variable.parameter.rls" + ], + "parameter.defaultLibrary": [ + "variable.parameter.rls" + ], + "enum": [ + "entity.name.type.enum.rls" + ], + "enum.defaultLibrary": [ + "entity.name.type.enum.rls" + ], + "enumMember": [ + "variable.other.enummember.rls" + ], + "enumMember.readonly.declaration": [ + "variable.other.enummember.rls" + ], + "enumMember.readonly.declaration.defaultLibrary": [ + "variable.other.enummember.rls" + ], + "enumMember.readonly.defaultLibrary": [ + "variable.other.enummember.rls" + ], + "property": [ + "variable.parameter.rls" + ], + "property.declaration": [ + "variable.parameter.rls" + ], + "operator": [ + "keyword.operator.word.rls" + ], + "variable.readonly": [ + "variable.other.constant.rls" + ], + "variable.readonly.declaration": [ + "variable.other.constant.rls" + ], + "variable.readonly.definition": [ + "variable.other.constant.rls" + ] + } + } + ], + "commands": [ + { + "command": "randoLogicScript.restartServer", + "title": "Restart Language Server", + "category": "Rando Logic Script" + } + ], + "configuration": { + "title": "Rando Logic Script", + "properties": { + "randoLogicScript.server.path": { + "type": "string", + "default": "", + "scope": "machine-overridable", + "description": "Path to the RLS language server executable. Leave empty to use a bundled server or discover a repository build." + }, + "randoLogicScript.server.arguments": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "scope": "machine-overridable", + "description": "Additional arguments passed to the RLS language server." + }, + "randoLogicScript.completion.sectionSnippetIndentation": { + "type": "string", + "enum": [ + "client", + "server" + ], + "enumDescriptions": [ + "Send relative indentation and let the editor adjust multiline completion snippets. Recommended for VS Code.", + "Embed the current line indentation for editors that insert multiline snippet text as-is." + ], + "default": "client", + "scope": "window", + "description": "Controls whether the editor or language server supplies indentation for multiline section completion snippets. Restart the language server after changing this setting." + }, + "randoLogicScript.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "scope": "window", + "description": "Trace communication between VS Code and the RLS language server." + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run bundle:lsp && npm run compile", + "bundle:lsp": "node ./scripts/bundle-language-server.mjs", + "bundle": "esbuild ./src/extension.ts --bundle --minify --outfile=dist/extension.js --external:vscode --format=cjs --platform=node", + "package": "vsce package", + "validate:vsix": "node ./scripts/validate-vsix.mjs", + "compile": "tsc -p ./ && npm run bundle", + "watch": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --format=cjs --platform=node --watch", + "test": "npm run compile && node ./out/test/runTest.js" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.17.30", + "@types/vscode": "1.82.0", + "@vscode/test-electron": "^3.1.0", + "@vscode/vsce": "^3.6.2", + "esbuild": "^0.28.2", + "jszip": "^3.10.1", + "typescript": "^5.8.2" + } +} diff --git a/editors/vscode/scripts/bundle-language-server.mjs b/editors/vscode/scripts/bundle-language-server.mjs new file mode 100644 index 0000000..0fd02fb --- /dev/null +++ b/editors/vscode/scripts/bundle-language-server.mjs @@ -0,0 +1,84 @@ +import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from 'node:path'; +import { execFileSync } from 'node:child_process'; + +const extensionDirectory = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const repositoryDirectory = resolve(extensionDirectory, '..', '..'); +const executableName = process.platform === 'win32' + ? 'rls_language_server.exe' + : 'rls_language_server'; +const expectedArchitectures = { + arm64: { elf: 183, macho: 0x0100000c, pe: 0xaa64 }, + x64: { elf: 62, macho: 0x01000007, pe: 0x8664 }, +}; +const configuredBuildDirectory = process.env.RLS_VSCODE_RELEASE_BUILD_DIRECTORY; +const buildDirectory = configuredBuildDirectory + ? resolve(repositoryDirectory, configuredBuildDirectory) + : join(repositoryDirectory, 'build-vscode-release'); + +execFileSync( + 'cmake', + [ + '-S', repositoryDirectory, + '-B', buildDirectory, + '-DCMAKE_BUILD_TYPE=Release', + '-DBUILD_TESTING=OFF', + '-DRLS_STATIC_MSVC_RUNTIME=ON', + ], + { stdio: 'inherit' }, +); +execFileSync( + 'cmake', + ['--build', buildDirectory, '--config', 'Release', '--target', 'rls_language_server'], + { stdio: 'inherit' }, +); + +const executableCandidates = [ + join(buildDirectory, 'lsp', 'Release', executableName), + join(buildDirectory, 'lsp', executableName), + join(buildDirectory, 'lsp', 'Debug', executableName), +]; +const executable = executableCandidates.find(existsSync); + +if (!executable) { + throw new Error(`The CMake build completed but did not produce ${executableName}.`); +} + +const expectedArchitecture = expectedArchitectures[process.arch]; +if (!expectedArchitecture) { + throw new Error(`Unsupported release architecture ${process.arch}.`); +} + +const image = readFileSync(executable); +if (process.platform === 'win32') { + const peOffset = image.readUInt32LE(0x3c); + const machine = image.readUInt16LE(peOffset + 4); + if (machine !== expectedArchitecture.pe) { + throw new Error(`Refusing to label PE machine 0x${machine.toString(16)} as ${process.arch}.`); + } + const dynamicRuntime = image.toString('latin1').match( + /(?:MSVCP\d+|VCRUNTIME\d+(?:_\d+)?|ucrtbase)d?\.dll/i, + ); + if (dynamicRuntime) { + throw new Error(`Refusing to package a server linked to dynamic runtime ${dynamicRuntime[0]}.`); + } +} else if (process.platform === 'linux') { + const machine = image.readUInt16LE(18); + if (machine !== expectedArchitecture.elf) { + throw new Error(`Refusing to label ELF machine ${machine} as ${process.arch}.`); + } +} else if (process.platform === 'darwin') { + const cpuType = image.readUInt32LE(4); + if (cpuType !== expectedArchitecture.macho) { + throw new Error(`Refusing to label Mach-O CPU 0x${cpuType.toString(16)} as ${process.arch}.`); + } +} + +const destinationDirectory = join( + extensionDirectory, + 'server', + `${process.platform}-${process.arch}`, +); +mkdirSync(destinationDirectory, { recursive: true }); +copyFileSync(executable, join(destinationDirectory, executableName)); \ No newline at end of file diff --git a/editors/vscode/scripts/validate-vsix.mjs b/editors/vscode/scripts/validate-vsix.mjs new file mode 100644 index 0000000..765766a --- /dev/null +++ b/editors/vscode/scripts/validate-vsix.mjs @@ -0,0 +1,78 @@ +import { readFile } from 'node:fs/promises'; +import { basename } from 'node:path'; + +import JSZip from 'jszip'; + +const [, , vsixPath, target] = process.argv; +const targets = { + 'darwin-arm64': { cpu: 0x0100000c, executable: 'rls_language_server', format: 'macho' }, + 'darwin-x64': { cpu: 0x01000007, executable: 'rls_language_server', format: 'macho' }, + 'linux-arm64': { cpu: 183, executable: 'rls_language_server', format: 'elf' }, + 'linux-x64': { cpu: 62, executable: 'rls_language_server', format: 'elf' }, + 'win32-arm64': { cpu: 0xaa64, executable: 'rls_language_server.exe', format: 'pe' }, + 'win32-x64': { cpu: 0x8664, executable: 'rls_language_server.exe', format: 'pe' }, +}; + +if (!vsixPath || !target || !targets[target]) { + throw new Error('Usage: node scripts/validate-vsix.mjs '); +} + +const archive = await JSZip.loadAsync(await readFile(vsixPath), { checkCRC32: true }); +const manifest = await archive.file('extension.vsixmanifest')?.async('string'); +if (!manifest?.includes(`TargetPlatform="${target}"`)) { + throw new Error(`VSIX manifest does not target ${target}.`); +} + +const packageJson = JSON.parse( + await archive.file('extension/package.json')?.async('string') ?? 'null', +); +if (!packageJson || packageJson.engines?.vscode !== '^1.82.0') { + throw new Error('VSIX package manifest has an unexpected VS Code engine requirement.'); +} + +const serverFiles = Object.values(archive.files) + .filter((entry) => !entry.dir && entry.name.startsWith('extension/server/')) + .map((entry) => entry.name); +const expectedServer = `extension/server/${target}/${targets[target].executable}`; +if (serverFiles.length !== 1 || serverFiles[0] !== expectedServer) { + throw new Error(`Expected only ${expectedServer}; found ${serverFiles.join(', ') || 'none'}.`); +} + +const serverEntry = archive.file(expectedServer); +if (!serverEntry) { + throw new Error(`VSIX is missing ${expectedServer}.`); +} +if (!target.startsWith('win32-')) { + const permissions = serverEntry.unixPermissions; + if (typeof permissions !== 'number' || (permissions & 0o111) === 0) { + throw new Error(`${expectedServer} is not marked executable.`); + } +} + +const image = await serverEntry.async('nodebuffer'); +const expected = targets[target]; +if (expected.format === 'pe') { + const peOffset = image.readUInt32LE(0x3c); + const machine = image.readUInt16LE(peOffset + 4); + if (machine !== expected.cpu) { + throw new Error(`PE machine 0x${machine.toString(16)} does not match ${target}.`); + } + const dynamicRuntime = image.toString('latin1').match( + /(?:MSVCP\d+|VCRUNTIME\d+(?:_\d+)?|ucrtbase)d?\.dll/i, + ); + if (dynamicRuntime) { + throw new Error(`Packaged server imports dynamic runtime ${dynamicRuntime[0]}.`); + } +} else if (expected.format === 'elf') { + const machine = image.readUInt16LE(18); + if (machine !== expected.cpu) { + throw new Error(`ELF machine ${machine} does not match ${target}.`); + } +} else { + const cpuType = image.readUInt32LE(4); + if (cpuType !== expected.cpu) { + throw new Error(`Mach-O CPU 0x${cpuType.toString(16)} does not match ${target}.`); + } +} + +console.log(`Validated ${basename(vsixPath)} for ${target}.`); \ No newline at end of file diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts new file mode 100644 index 0000000..cab0177 --- /dev/null +++ b/editors/vscode/src/extension.ts @@ -0,0 +1,149 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import * as vscode from 'vscode'; +import { + Executable, + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, +} from 'vscode-languageclient/node'; + +let client: LanguageClient | undefined; +let watcher: vscode.FileSystemWatcher | undefined; + +type SectionSnippetIndentation = 'client' | 'server'; + +function executableName(): string { + return process.platform === 'win32' ? 'rls_language_server.exe' : 'rls_language_server'; +} + +function existingExecutable(candidates: string[]): string | undefined { + return candidates.find((candidate) => { + try { + return fs.statSync(candidate).isFile(); + } catch { + return false; + } + }); +} + +function configuredServerPath(): string | undefined { + const configured = vscode.workspace + .getConfiguration('randoLogicScript') + .get('server.path', '') + .trim(); + if (!configured) { + return undefined; + } + if (path.isAbsolute(configured)) { + return configured; + } + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + return workspaceRoot ? path.resolve(workspaceRoot, configured) : path.resolve(configured); +} + +function configuredSectionSnippetIndentation(): SectionSnippetIndentation { + return vscode.workspace + .getConfiguration('randoLogicScript') + .get('completion.sectionSnippetIndentation', 'client'); +} + +export function resolveServerExecutable(context: vscode.ExtensionContext): string | undefined { + const executable = executableName(); + const configured = configuredServerPath(); + if (configured) { + return existingExecutable([configured]); + } + + const environmentPath = process.env.RLS_LANGUAGE_SERVER_PATH; + const workspaceRoots = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath) ?? []; + const candidates = [ + environmentPath, + context.asAbsolutePath(path.join('server', `${process.platform}-${process.arch}`, executable)), + ...workspaceRoots.flatMap((root) => [ + path.join(root, 'build', 'lsp', executable), + path.join(root, 'build', 'lsp', 'Debug', executable), + path.join(root, 'build', 'lsp', 'Release', executable), + path.join(root, 'build-vs', 'lsp', 'Debug', executable), + path.join(root, 'build-vs', 'lsp', 'Release', executable), + ]), + ].filter((candidate): candidate is string => Boolean(candidate)); + return existingExecutable(candidates); +} + +async function startClient(context: vscode.ExtensionContext): Promise { + const command = resolveServerExecutable(context); + if (!command) { + const selection = await vscode.window.showErrorMessage( + 'RLS language server executable was not found. Build it or configure randoLogicScript.server.path.', + 'Open Settings', + ); + if (selection === 'Open Settings') { + await vscode.commands.executeCommand( + 'workbench.action.openSettings', + 'randoLogicScript.server.path', + ); + } + return; + } + + const argumentsValue = vscode.workspace + .getConfiguration('randoLogicScript') + .get('server.arguments', []); + const executable: Executable = { + command, + args: argumentsValue, + transport: TransportKind.stdio, + options: { + cwd: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath, + }, + }; + const serverOptions: ServerOptions = executable; + const clientOptions: LanguageClientOptions = { + documentSelector: [ + { scheme: 'file', language: 'rls' }, + { scheme: 'untitled', language: 'rls' }, + ], + initializationOptions: { + completion: { + sectionSnippetIndentation: configuredSectionSnippetIndentation(), + }, + }, + synchronize: { + fileEvents: watcher, + }, + }; + + client = new LanguageClient( + 'randoLogicScript', + 'Rando Logic Script Language Server', + serverOptions, + clientOptions, + ); + await client.start(); +} + +async function stopClient(): Promise { + const activeClient = client; + client = undefined; + await activeClient?.dispose(); +} + +export async function activate(context: vscode.ExtensionContext): Promise { + watcher = vscode.workspace.createFileSystemWatcher('**/{*.rls,rls.json}'); + context.subscriptions.push(watcher); + context.subscriptions.push( + vscode.commands.registerCommand('randoLogicScript.restartServer', async () => { + await stopClient(); + await startClient(context); + }), + ); + await startClient(context); +} + +export async function deactivate(): Promise { + await stopClient(); + watcher = undefined; +} \ No newline at end of file diff --git a/editors/vscode/src/test/runTest.ts b/editors/vscode/src/test/runTest.ts new file mode 100644 index 0000000..7d2e0a1 --- /dev/null +++ b/editors/vscode/src/test/runTest.ts @@ -0,0 +1,58 @@ +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { runTests } from '@vscode/test-electron'; + +function executableName(): string { + return process.platform === 'win32' ? 'rls_language_server.exe' : 'rls_language_server'; +} + +function discoverServer(repositoryRoot: string): string { + const executable = executableName(); + const candidates = [ + process.env.RLS_LANGUAGE_SERVER_PATH, + path.join(repositoryRoot, 'build', 'lsp', executable), + path.join(repositoryRoot, 'build', 'lsp', 'Debug', executable), + path.join(repositoryRoot, 'build', 'lsp', 'Release', executable), + path.join(repositoryRoot, 'build-vs', 'lsp', 'Debug', executable), + path.join(repositoryRoot, 'build-vs', 'lsp', 'Release', executable), + ].filter((candidate): candidate is string => Boolean(candidate)); + const server = candidates.find((candidate) => fs.existsSync(candidate)); + if (!server) { + throw new Error('Build rls_language_server or set RLS_LANGUAGE_SERVER_PATH before testing.'); + } + return server; +} + +async function main(): Promise { + const extensionDevelopmentPath = path.resolve(__dirname, '..', '..'); + const repositoryRoot = path.resolve(extensionDevelopmentPath, '..', '..'); + const extensionTestsPath = path.resolve(__dirname, 'suite', 'index'); + const fixturePath = path.join(extensionDevelopmentPath, 'test-fixture'); + const userDataDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'rls-vscode-')); + process.env.RLS_LANGUAGE_SERVER_PATH = discoverServer(repositoryRoot); + + try { + await runTests({ + version: process.env.RLS_VSCODE_TEST_VERSION ?? '1.82.0', + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [ + fixturePath, + '--disable-extensions', + `--user-data-dir=${userDataDirectory}`, + ], + extensionTestsEnv: { + RLS_LANGUAGE_SERVER_PATH: process.env.RLS_LANGUAGE_SERVER_PATH, + }, + }); + } finally { + fs.rmSync(userDataDirectory, { recursive: true, force: true }); + } +} + +main().catch((error: unknown) => { + console.error(error); + process.exit(1); +}); \ No newline at end of file diff --git a/editors/vscode/src/test/suite/index.ts b/editors/vscode/src/test/suite/index.ts new file mode 100644 index 0000000..f57d9b3 --- /dev/null +++ b/editors/vscode/src/test/suite/index.ts @@ -0,0 +1,5 @@ +import { runLanguageClientTest } from './languageClient.test'; + +export async function run(): Promise { + await runLanguageClientTest(); +} \ No newline at end of file diff --git a/editors/vscode/src/test/suite/languageClient.test.ts b/editors/vscode/src/test/suite/languageClient.test.ts new file mode 100644 index 0000000..7223b19 --- /dev/null +++ b/editors/vscode/src/test/suite/languageClient.test.ts @@ -0,0 +1,70 @@ +import * as assert from 'node:assert/strict'; + +import * as vscode from 'vscode'; + +async function waitForDiagnostic( + uri: vscode.Uri, + code: string, +): Promise { + const deadline = Date.now() + 10_000; + while (Date.now() < deadline) { + const diagnostic = vscode.languages + .getDiagnostics(uri) + .find((candidate) => candidate.code === code); + if (diagnostic) { + return diagnostic; + } + await new Promise((resolve) => setTimeout(resolve, 50)); + } + throw new Error(`Timed out waiting for diagnostic ${code}.`); +} + +export async function runLanguageClientTest(): Promise { + const extension = vscode.extensions.getExtension( + 'xxAtrain223.rando-logic-script', + ); + assert.ok(extension, 'extension is available in the development host'); + const semanticScopeMap = extension.packageJSON.contributes.semanticTokenScopes[0]; + assert.equal(semanticScopeMap.language, 'rls'); + assert.deepEqual(semanticScopeMap.scopes.function, ['entity.name.function.rls']); + assert.deepEqual(semanticScopeMap.scopes['function.defaultLibrary'], [ + 'entity.name.function.rls', + ]); + assert.deepEqual(semanticScopeMap.scopes.enum, ['entity.name.type.enum.rls']); + assert.deepEqual(semanticScopeMap.scopes['enumMember.readonly.declaration'], [ + 'variable.other.enummember.rls', + ]); + assert.deepEqual(semanticScopeMap.scopes.property, ['variable.parameter.rls']); + assert.deepEqual(semanticScopeMap.scopes['property.declaration'], [ + 'variable.parameter.rls', + ]); + assert.deepEqual(semanticScopeMap.scopes.operator, ['keyword.operator.word.rls']); + assert.deepEqual(semanticScopeMap.scopes['variable.readonly.definition'], [ + 'variable.other.constant.rls', + ]); + assert.deepEqual(semanticScopeMap.scopes['variable.readonly.declaration'], [ + 'variable.other.constant.rls', + ]); + + const workspace = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspace, 'test fixture opened as a workspace folder'); + const documentUri = vscode.Uri.joinPath(workspace.uri, 'diagnostic.rls'); + await vscode.workspace.openTextDocument(documentUri); + const diagnostic = await waitForDiagnostic(documentUri, 'RLS-T006'); + assert.equal(extension.isActive, true, 'opening an RLS document activates the extension'); + assert.equal(diagnostic.severity, vscode.DiagnosticSeverity.Error); + assert.match(diagnostic.message, /unknown identifier 'missing'/); + + const untitledDocument = await vscode.workspace.openTextDocument({ + language: 'rls', + content: 'define untitled(): missing\n', + }); + assert.equal(untitledDocument.uri.scheme, 'untitled'); + const untitledDiagnostic = await waitForDiagnostic(untitledDocument.uri, 'RLS-T006'); + assert.equal(untitledDiagnostic.severity, vscode.DiagnosticSeverity.Error); + assert.match(untitledDiagnostic.message, /unknown identifier 'missing'/); + + await vscode.commands.executeCommand('randoLogicScript.restartServer'); + const restartedDiagnostic = await waitForDiagnostic(documentUri, 'RLS-T006'); + assert.equal(restartedDiagnostic.source, 'rls'); +} \ No newline at end of file diff --git a/editors/vscode/syntaxes/rls.tmLanguage.json b/editors/vscode/syntaxes/rls.tmLanguage.json new file mode 100644 index 0000000..f8ffe8c --- /dev/null +++ b/editors/vscode/syntaxes/rls.tmLanguage.json @@ -0,0 +1,226 @@ +{ + "name": "Rando Logic Script", + "scopeName": "source.rls", + "patterns": [ + { "include": "#comments" }, + { "include": "#strings" }, + { "include": "#enum-declarations" }, + { "include": "#declarations" }, + { "include": "#function-calls" }, + { "include": "#sections" }, + { "include": "#parameters" }, + { "include": "#named-arguments" }, + { "include": "#ternary-expression" }, + { "include": "#literals" }, + { "include": "#keywords" }, + { "include": "#operators" }, + { "include": "#qualified-names" }, + { "include": "#punctuation" } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.number-sign.rls", + "match": "#.*$" + } + ] + }, + "strings": { + "patterns": [ + { + "name": "string.quoted.double.rls", + "begin": "\"", + "end": "\"|$", + "patterns": [ + { + "name": "constant.character.escape.rls", + "match": "\\\\[\\\"\\\\]" + } + ] + } + ] + }, + "enum-declarations": { + "patterns": [ + { + "name": "meta.declaration.enum.rls", + "begin": "\\b(?:(extern)\\s+)?(enum)(\\s+)([A-Za-z_][A-Za-z0-9_]*)(\\s*)(\\{)", + "beginCaptures": { + "1": { "name": "storage.modifier.rls" }, + "2": { "name": "storage.type.rls" }, + "4": { "name": "entity.name.type.enum.rls" }, + "6": { "name": "punctuation.section.group.begin.rls" } + }, + "end": "\\}", + "endCaptures": { + "0": { "name": "punctuation.section.group.end.rls" } + }, + "patterns": [ + { + "name": "variable.other.enummember.rls", + "match": "\\b[A-Za-z_][A-Za-z0-9_]*\\b" + }, + { + "name": "constant.numeric.integer.rls", + "match": "-?\\b[0-9]+\\b" + }, + { + "name": "keyword.operator.rls", + "match": "[=*]" + }, + { + "name": "punctuation.separator.rls", + "match": "," + } + ] + } + ] + }, + "declarations": { + "patterns": [ + { + "match": "\\b(region)(\\s+)([A-Za-z_][A-Za-z0-9_]*)", + "captures": { + "1": { "name": "storage.type.rls" }, + "3": { "name": "entity.name.type.rls" } + } + }, + { + "match": "\\b(define)(\\s+)([A-Za-z_][A-Za-z0-9_]*)(?=\\s*\\()", + "captures": { + "1": { "name": "storage.type.function.rls" }, + "3": { "name": "entity.name.function.rls" } + } + } + ] + }, + "sections": { + "patterns": [ + { + "name": "keyword.control.rls", + "match": "\\b(?:extend|extern|events|locations|exits|match)\\b" + } + ] + }, + "function-calls": { + "patterns": [ + { + "name": "entity.name.function.rls", + "match": "\\b[A-Za-z_][A-Za-z0-9_]*(?=\\s*\\()" + } + ] + }, + "parameters": { + "patterns": [ + { + "match": "(?<=\\(|,)\\s*([A-Za-z_][A-Za-z0-9_]*)(?=\\s*(?::|=|,|\\)))", + "captures": { + "1": { "name": "variable.parameter.rls" } + } + }, + { + "match": "(:\\s*)([A-Za-z_][A-Za-z0-9_]*)(?=\\s*(?:=|,|\\)|->))", + "captures": { + "2": { "name": "entity.name.type.rls" } + } + } + ] + }, + "named-arguments": { + "patterns": [ + { + "match": "\\b([A-Za-z_][A-Za-z0-9_]*)(?=\\s*:)", + "captures": { + "1": { "name": "variable.parameter.rls" } + } + } + ] + }, + "ternary-expression": { + "patterns": [ + { + "name": "meta.expression.ternary.rls", + "begin": "\\?", + "beginCaptures": { + "0": { "name": "keyword.operator.ternary.rls" } + }, + "end": ":", + "endCaptures": { + "0": { "name": "keyword.operator.ternary.rls" } + }, + "patterns": [ + { "include": "#comments" }, + { "include": "#strings" }, + { "include": "#ternary-expression" }, + { "include": "#literals" }, + { "include": "#keywords" }, + { "include": "#operators" }, + { "include": "#qualified-names" }, + { "include": "#punctuation" } + ] + } + ] + }, + "literals": { + "patterns": [ + { + "name": "constant.language.boolean.rls", + "match": "\\b(?:true|false|always|never)\\b" + }, + { + "name": "constant.numeric.integer.rls", + "match": "(?=|<=|->|[+*/<>?=-])" + } + ] + }, + "qualified-names": { + "patterns": [ + { + "match": "\\b([A-Za-z_][A-Za-z0-9_]*)(\\.)([A-Za-z_][A-Za-z0-9_]*)\\b", + "captures": { + "1": { "name": "entity.name.type.enum.rls" }, + "2": { "name": "punctuation.accessor.rls" }, + "3": { "name": "variable.other.enummember.rls" } + } + } + ] + }, + "punctuation": { + "patterns": [ + { + "name": "punctuation.separator.rls", + "match": "[,;:]" + }, + { + "name": "punctuation.section.group.begin.rls", + "match": "[({\\[]" + }, + { + "name": "punctuation.section.group.end.rls", + "match": "[)}\\]]" + } + ] + } + } +} \ No newline at end of file diff --git a/editors/vscode/test-fixture/diagnostic.rls b/editors/vscode/test-fixture/diagnostic.rls new file mode 100644 index 0000000..f964cec --- /dev/null +++ b/editors/vscode/test-fixture/diagnostic.rls @@ -0,0 +1 @@ +define broken(): missing \ No newline at end of file diff --git a/editors/vscode/test-fixture/rls.json b/editors/vscode/test-fixture/rls.json new file mode 100644 index 0000000..d238d5b --- /dev/null +++ b/editors/vscode/test-fixture/rls.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "sources": ["diagnostic.rls"] +} \ No newline at end of file diff --git a/editors/vscode/tsconfig.json b/editors/vscode/tsconfig.json new file mode 100644 index 0000000..b0f728f --- /dev/null +++ b/editors/vscode/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "outDir": "out", + "rootDir": "src", + "strict": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} \ No newline at end of file diff --git a/examples/ap/ap.py b/examples/soh/out_ap/ap.py similarity index 100% rename from examples/ap/ap.py rename to examples/soh/out_ap/ap.py diff --git a/examples/soh/functions.gen.cpp b/examples/soh/out_soh/functions.gen.cpp similarity index 100% rename from examples/soh/functions.gen.cpp rename to examples/soh/out_soh/functions.gen.cpp diff --git a/examples/soh/functions.gen.h b/examples/soh/out_soh/functions.gen.h similarity index 100% rename from examples/soh/functions.gen.h rename to examples/soh/out_soh/functions.gen.h diff --git a/examples/soh/regions.gen.cpp b/examples/soh/out_soh/regions.gen.cpp similarity index 100% rename from examples/soh/regions.gen.cpp rename to examples/soh/out_soh/regions.gen.cpp diff --git a/examples/soh/regions.gen.h b/examples/soh/out_soh/regions.gen.h similarity index 100% rename from examples/soh/regions.gen.h rename to examples/soh/out_soh/regions.gen.h diff --git a/examples/soh/rls_match.h b/examples/soh/out_soh/rls_match.h similarity index 100% rename from examples/soh/rls_match.h rename to examples/soh/out_soh/rls_match.h diff --git a/examples/soh/rls.json b/examples/soh/rls.json new file mode 100644 index 0000000..52cade0 --- /dev/null +++ b/examples/soh/rls.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "sources": ["src"], + "transpilers": { + "soh": { + "output": "out_soh/" + }, + "ap": { + "output": "out_ap/" + } + } +} \ No newline at end of file diff --git a/examples/rls/overworld/kokiri_forest.rls b/examples/soh/src/overworld/kokiri_forest.rls similarity index 100% rename from examples/rls/overworld/kokiri_forest.rls rename to examples/soh/src/overworld/kokiri_forest.rls diff --git a/examples/rls/overworld/root.rls b/examples/soh/src/overworld/root.rls similarity index 100% rename from examples/rls/overworld/root.rls rename to examples/soh/src/overworld/root.rls diff --git a/examples/rls/shuffles/beehives.rls b/examples/soh/src/shuffles/beehives.rls similarity index 100% rename from examples/rls/shuffles/beehives.rls rename to examples/soh/src/shuffles/beehives.rls diff --git a/examples/rls/shuffles/cows.rls b/examples/soh/src/shuffles/cows.rls similarity index 100% rename from examples/rls/shuffles/cows.rls rename to examples/soh/src/shuffles/cows.rls diff --git a/examples/rls/shuffles/fairies.rls b/examples/soh/src/shuffles/fairies.rls similarity index 100% rename from examples/rls/shuffles/fairies.rls rename to examples/soh/src/shuffles/fairies.rls diff --git a/examples/rls/shuffles/fish.rls b/examples/soh/src/shuffles/fish.rls similarity index 100% rename from examples/rls/shuffles/fish.rls rename to examples/soh/src/shuffles/fish.rls diff --git a/examples/rls/shuffles/freestanding.rls b/examples/soh/src/shuffles/freestanding.rls similarity index 100% rename from examples/rls/shuffles/freestanding.rls rename to examples/soh/src/shuffles/freestanding.rls diff --git a/examples/rls/shuffles/gold_skulltulas.rls b/examples/soh/src/shuffles/gold_skulltulas.rls similarity index 100% rename from examples/rls/shuffles/gold_skulltulas.rls rename to examples/soh/src/shuffles/gold_skulltulas.rls diff --git a/examples/rls/shuffles/grass.rls b/examples/soh/src/shuffles/grass.rls similarity index 100% rename from examples/rls/shuffles/grass.rls rename to examples/soh/src/shuffles/grass.rls diff --git a/examples/rls/shuffles/hints.rls b/examples/soh/src/shuffles/hints.rls similarity index 100% rename from examples/rls/shuffles/hints.rls rename to examples/soh/src/shuffles/hints.rls diff --git a/examples/rls/shuffles/pots.rls b/examples/soh/src/shuffles/pots.rls similarity index 100% rename from examples/rls/shuffles/pots.rls rename to examples/soh/src/shuffles/pots.rls diff --git a/examples/rls/shuffles/shops.rls b/examples/soh/src/shuffles/shops.rls similarity index 100% rename from examples/rls/shuffles/shops.rls rename to examples/soh/src/shuffles/shops.rls diff --git a/examples/rls/stdlib/enemies.rls b/examples/soh/src/stdlib/enemies.rls similarity index 100% rename from examples/rls/stdlib/enemies.rls rename to examples/soh/src/stdlib/enemies.rls diff --git a/examples/rls/stdlib/functions.rls b/examples/soh/src/stdlib/functions.rls similarity index 100% rename from examples/rls/stdlib/functions.rls rename to examples/soh/src/stdlib/functions.rls diff --git a/examples/rls/stdlib/host.rls b/examples/soh/src/stdlib/host.rls similarity index 85% rename from examples/rls/stdlib/host.rls rename to examples/soh/src/stdlib/host.rls index cfe6221..5bcd3da 100644 --- a/examples/rls/stdlib/host.rls +++ b/examples/soh/src/stdlib/host.rls @@ -1,15 +1,18 @@ +# Builtin type extensions +extern enum Region { RR_* } +extern enum Location { RC_* } +extern enum Event { LOGIC_* } + +# SoH specific enums extern enum Item { RG_* } extern enum Enemy { RE_* } extern enum Distance { ED_* } extern enum Trick { RT_* } -extern enum Logic { LOGIC_* } extern enum Scene { SCENE_* } extern enum Dungeon { DUNGEON_* } extern enum Area { RA_* } extern enum Trial { TK_* } extern enum Setting { RSK_*, RO_* } -extern enum Region { RR_* } -extern enum Check { RC_* } enum TimePasses { Auto, @@ -17,9 +20,17 @@ enum TimePasses { No } +enum WaterLevel { + WL_LOW, + WL_MID, + WL_HIGH, + WL_LOW_OR_MID, + WL_HIGH_OR_MID +} + extern define has(item: Item) -> Bool extern define can_use(item: Item) -> Bool -extern define flag(key: Logic) -> Bool +extern define flag(key: Event) -> Bool extern define setting(key: Setting) -> Int extern define trick(key: Trick) -> Bool extern define can_plant_bean(reg: Region, bean: Item) -> Bool @@ -31,18 +42,10 @@ extern define is_vanilla() -> Bool extern define is_mq() -> Bool extern define required_triforce_pieces() -> Int extern define collected_triforce_pieces() -> Int -extern define check_price(check = RC_UNKNOWN_CHECK) -> Int +extern define check_price(check: Location = RC_UNKNOWN_CHECK) -> Int extern define bottle_count() -> Int extern define any_age(condition: Condition) -> Bool extern define spirit_shared( first_region: Region, first_condition: Condition, any_age: Bool = false, second_region: Region = RR_NONE, second_condition: Condition = false, third_region: Region = RR_NONE, third_condition: Condition = false) -> Bool - -enum WaterLevel { - WL_LOW, - WL_MID, - WL_HIGH, - WL_LOW_OR_MID, - WL_HIGH_OR_MID -} diff --git a/lsp/CMakeLists.txt b/lsp/CMakeLists.txt new file mode 100644 index 0000000..15a3047 --- /dev/null +++ b/lsp/CMakeLists.txt @@ -0,0 +1,44 @@ +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 +) +FetchContent_MakeAvailable(nlohmann_json) +find_package(Threads REQUIRED) + +file(GLOB lsp_sources CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp" +) + +add_library(rls_lsp STATIC ${lsp_sources}) + +target_include_directories(rls_lsp PUBLIC include) +target_link_libraries(rls_lsp PUBLIC + nlohmann_json::nlohmann_json + project + sema + Threads::Threads +) + +add_executable(rls_language_server + main.cpp +) +target_link_libraries(rls_language_server PRIVATE rls_lsp) + +if(BUILD_TESTING) + find_package(Python3 REQUIRED COMPONENTS Interpreter) + + file(GLOB lsp_test_sources CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/tests/*.cpp" + ) + + rls_add_gtest(lsp_tests ${lsp_test_sources}) + target_link_libraries(lsp_tests PRIVATE rls_lsp) + + add_test( + NAME lsp_process_smoke + COMMAND ${Python3_EXECUTABLE} + "${CMAKE_CURRENT_SOURCE_DIR}/tests/process_smoke.py" + --server "$" + ) +endif() \ No newline at end of file diff --git a/lsp/include/rls/lsp/analysis_scheduler.h b/lsp/include/rls/lsp/analysis_scheduler.h new file mode 100644 index 0000000..d518a51 --- /dev/null +++ b/lsp/include/rls/lsp/analysis_scheduler.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "analysis_snapshot.h" + +namespace rls::lsp { + +struct AnalysisSource { + // Stable source identifier passed through parser, sema, and LSP results. + std::string identity; + // Present content bypasses disk I/O; absence delegates to SourceReader. + std::optional content; + // Disk location is deliberately separate from the source identity. + std::optional diskPath; +}; + +struct AnalysisRequest { + std::string projectId; + // Strictly monotonic identity for the complete source-set capture. + uint64_t generation = 0; + std::vector sources; + // Component generations may remain equal while aggregate generation advances. + uint64_t documentGeneration = 0; + uint64_t manifestGeneration = 0; +}; + +class AnalysisScheduler { +public: + using Snapshot = std::shared_ptr; + using Builder = std::function( + std::vector, uint64_t, std::stop_token)>; + using SourceReader = std::function( + const std::filesystem::path&, std::stop_token)>; + using AcceptedHandler = std::function; + + struct Options { + std::chrono::milliseconds debounce{75}; + size_t maximumConcurrency = 2; + }; + + AnalysisScheduler(); + explicit AnalysisScheduler( + Options options, Builder builder = {}, SourceReader sourceReader = {}); + ~AnalysisScheduler(); + + AnalysisScheduler(const AnalysisScheduler&) = delete; + AnalysisScheduler& operator=(const AnalysisScheduler&) = delete; + + bool schedule(AnalysisRequest request); + void removeProject(std::string_view projectId); + void setAcceptedHandler(AcceptedHandler handler); + Snapshot acceptedSnapshot(std::string_view projectId) const; + Snapshot awaitSnapshot( + std::string_view projectId, uint64_t generation, + std::chrono::milliseconds timeout = std::chrono::milliseconds(500)); + void waitForIdle(); + +private: + struct PendingRequest { + AnalysisRequest request; + std::chrono::steady_clock::time_point readyAt; + }; + + struct ProjectState { + uint64_t latestGeneration = 0; + uint64_t latestDocumentGeneration = 0; + uint64_t latestManifestGeneration = 0; + std::optional pending; + std::shared_ptr activeCancellation; + Snapshot accepted; + bool removed = false; + }; + + void worker(std::stop_token shutdown); + bool isIdle() const; + + Options options_; + Builder builder_; + SourceReader sourceReader_; + mutable std::mutex mutex_; + std::condition_variable_any wake_; + std::condition_variable idle_; + std::condition_variable snapshotReady_; + std::unordered_map projects_; + AcceptedHandler acceptedHandler_; + std::vector workers_; + size_t activeBuilds_ = 0; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/client_connection.h b/lsp/include/rls/lsp/client_connection.h new file mode 100644 index 0000000..06bbb52 --- /dev/null +++ b/lsp/include/rls/lsp/client_connection.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace rls::lsp { + +class ServerCompositionRoot; + +class ClientConnection { +public: + ClientConnection(std::istream& input, std::ostream& output, std::ostream& log); + + int run(ServerCompositionRoot& server); + +private: + std::istream& input_; + std::ostream& output_; + std::ostream& log_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/completion_service.h b/lsp/include/rls/lsp/completion_service.h new file mode 100644 index 0000000..5a56def --- /dev/null +++ b/lsp/include/rls/lsp/completion_service.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/presentation.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +enum class CompletionItemKind { + Function, + Enum, + EnumMember, + Type, + Property, + Variable, + Value, + Keyword, +}; + +struct CompletionItem { + std::string label; + CompletionItemKind kind = CompletionItemKind::Value; + std::string detail; + std::string documentation; + std::string insertText; + std::optional snippetText; + std::optional serverIndentedSnippetText; + PresentationRange replacementRange; + std::string sortText; +}; + +class CompletionService { +public: + CompletionService(const ProjectManager& projects, AnalysisScheduler& scheduler); + + std::vector complete( + std::string_view uri, PresentationPosition position) const; + +private: + const ProjectManager& projects_; + AnalysisScheduler& scheduler_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/diagnostic_publisher.h b/lsp/include/rls/lsp/diagnostic_publisher.h new file mode 100644 index 0000000..6bf1f7d --- /dev/null +++ b/lsp/include/rls/lsp/diagnostic_publisher.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "analysis_snapshot.h" +#include "project.h" +#include "rls/lsp/outbound_message_queue.h" + +namespace rls::lsp { + +class DiagnosticPublisher { +public: + explicit DiagnosticPublisher(OutboundMessageQueue& outbound); + + void documentOpened(std::string_view uri); + void documentClosed(std::string_view uri, bool standalone); + void clearProject(std::string_view projectId); + void publishConfigurationDiagnostics( + const std::vector& diagnostics); + void acceptedSnapshot( + std::string projectId, std::shared_ptr snapshot); + +private: + struct PublishedDocument { + std::string uri; + std::string diagnostics; + }; + + using DocumentPayloads = std::unordered_map; + + OutboundMessageQueue& outbound_; + std::mutex mutex_; + std::unordered_map published_; + DocumentPayloads configurationPublished_; + std::unordered_set suppressed_; + std::unordered_map openedUris_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/document_store.h b/lsp/include/rls/lsp/document_store.h new file mode 100644 index 0000000..3480742 --- /dev/null +++ b/lsp/include/rls/lsp/document_store.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace rls::lsp { + +struct TextDocument { + std::string uri; + std::string languageId; + int64_t version = 0; + std::string text; +}; + +enum class DocumentUpdateResult { + Applied, + InvalidUri, + NotOpen, + StaleVersion, +}; + +class DocumentStore { +public: + DocumentUpdateResult open( + std::string uri, std::string languageId, int64_t version, std::string text); + DocumentUpdateResult applyFullChange( + std::string_view uri, int64_t version, std::string text); + bool close(std::string_view uri); + + const TextDocument* find(std::string_view uri) const; + size_t size() const; + +private: + std::unordered_map documents_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/document_synchronization_service.h b/lsp/include/rls/lsp/document_synchronization_service.h new file mode 100644 index 0000000..3668df9 --- /dev/null +++ b/lsp/include/rls/lsp/document_synchronization_service.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/diagnostic_publisher.h" +#include "rls/lsp/document_store.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +enum class DocumentSynchronizationResult { + Applied, + NotReady, + InvalidUri, + NotOpen, + StaleVersion, + ProjectResolutionFailed, +}; + +class DocumentSynchronizationService { +public: + DocumentSynchronizationService( + LifecycleService& lifecycle, DocumentStore& documents, ProjectManager& projects, + AnalysisScheduler& scheduler, DiagnosticPublisher& diagnostics); + + DocumentSynchronizationResult open( + std::string uri, std::string languageId, int64_t version, std::string text); + DocumentSynchronizationResult change( + std::string_view uri, int64_t version, std::string text); + DocumentSynchronizationResult close(std::string_view uri); + +private: + bool schedule(std::string_view uri); + + LifecycleService& lifecycle_; + DocumentStore& documents_; + ProjectManager& projects_; + AnalysisScheduler& scheduler_; + DiagnosticPublisher& diagnostics_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/document_uri.h b/lsp/include/rls/lsp/document_uri.h new file mode 100644 index 0000000..b666e8b --- /dev/null +++ b/lsp/include/rls/lsp/document_uri.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include +#include + +namespace rls::lsp { + +std::optional NormalizeDocumentUri(std::string_view uri); +std::optional DocumentUriKey(std::string_view uri); +std::optional FileUriToPath(std::string_view uri); +std::optional PathToFileUri(const std::filesystem::path& path); + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/hover_service.h b/lsp/include/rls/lsp/hover_service.h new file mode 100644 index 0000000..fbcd4d4 --- /dev/null +++ b/lsp/include/rls/lsp/hover_service.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/presentation.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +struct HoverResult { + std::string markdown; + PresentationRange range; +}; + +class HoverService { +public: + HoverService(const ProjectManager& projects, AnalysisScheduler& scheduler); + + std::optional hover( + std::string_view uri, PresentationPosition position) const; + +private: + const ProjectManager& projects_; + AnalysisScheduler& scheduler_; +}; + +} // namespace rls::lsp diff --git a/lsp/include/rls/lsp/json_rpc_router.h b/lsp/include/rls/lsp/json_rpc_router.h new file mode 100644 index 0000000..647e630 --- /dev/null +++ b/lsp/include/rls/lsp/json_rpc_router.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace rls::lsp { + +class InvalidParams : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class RequestFailed : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +class JsonRpcRouter { +public: + using Json = nlohmann::json; + using RequestHandler = std::function; + using NotificationHandler = std::function; + + void registerRequest(std::string method, RequestHandler handler); + void registerNotification(std::string method, NotificationHandler handler); + + bool contains(std::string_view method) const; + void requireRoutes(std::initializer_list methods) const; + std::vector handlePayload(std::string_view payload) const; + +private: + std::unordered_map requests_; + std::unordered_map notifications_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/lifecycle_service.h b/lsp/include/rls/lsp/lifecycle_service.h new file mode 100644 index 0000000..3dd679c --- /dev/null +++ b/lsp/include/rls/lsp/lifecycle_service.h @@ -0,0 +1,45 @@ +#pragma once + +namespace rls::lsp { + +enum class SectionSnippetIndentation { + Client, + Server, +}; + +class LifecycleService { +public: + void initialize( + bool definitionLinkSupport = false, + bool documentSymbolHierarchySupport = false, + bool completionSnippetSupport = false, + SectionSnippetIndentation sectionSnippetIndentation = + SectionSnippetIndentation::Server, + bool workspaceDocumentChangesSupport = false); + void initialized(); + void shutdown(); + void exit(); + + bool acceptsDocumentUpdates() const; + bool supportsDefinitionLinks() const; + bool supportsDocumentSymbolHierarchy() const; + bool supportsCompletionSnippets() const; + bool supportsWorkspaceDocumentChanges() const; + SectionSnippetIndentation sectionSnippetIndentation() const; + bool shouldExit() const; + int exitCode() const; + +private: + bool initializeRequested_ = false; + bool definitionLinkSupport_ = false; + bool documentSymbolHierarchySupport_ = false; + bool completionSnippetSupport_ = false; + bool workspaceDocumentChangesSupport_ = false; + SectionSnippetIndentation sectionSnippetIndentation_ = + SectionSnippetIndentation::Server; + bool initialized_ = false; + bool shutdownRequested_ = false; + bool exitRequested_ = false; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/message_framer.h b/lsp/include/rls/lsp/message_framer.h new file mode 100644 index 0000000..b0688e6 --- /dev/null +++ b/lsp/include/rls/lsp/message_framer.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +namespace rls::lsp { + +class MessageFramer { +public: + static constexpr size_t DefaultMaximumPayloadSize = 16 * 1024 * 1024; + static constexpr size_t DefaultMaximumHeaderSize = 8 * 1024; + + explicit MessageFramer( + size_t maximumPayloadSize = DefaultMaximumPayloadSize, + size_t maximumHeaderSize = DefaultMaximumHeaderSize); + + void append(std::string_view bytes); + std::optional popMessage(); + + static std::string frame(std::string_view payload); + +private: + size_t maximumPayloadSize_; + size_t maximumHeaderSize_; + std::string buffer_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/navigation_service.h b/lsp/include/rls/lsp/navigation_service.h new file mode 100644 index 0000000..b34abad --- /dev/null +++ b/lsp/include/rls/lsp/navigation_service.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +struct NavigationPosition { + uint32_t line = 0; + uint32_t character = 0; +}; + +struct NavigationRange { + NavigationPosition start; + NavigationPosition end; +}; + +struct DefinitionResult { + NavigationRange originSelectionRange; + std::string targetUri; + NavigationRange targetRange; + NavigationRange targetSelectionRange; +}; + +struct NavigationLocation { + std::string uri; + NavigationRange range; +}; + +enum class NavigationSymbolKind { + Namespace, + Function, + Enum, + EnumMember, + Variable, + Property, + Field, +}; + +struct NavigationDocumentSymbol { + std::string name; + NavigationSymbolKind kind; + NavigationRange range; + NavigationRange selectionRange; + std::vector children; +}; + +struct NavigationWorkspaceSymbol { + std::string name; + NavigationSymbolKind kind; + NavigationLocation location; + std::optional containerName; +}; + +class NavigationService { +public: + NavigationService(const ProjectManager& projects, const AnalysisScheduler& scheduler); + + std::optional definition( + std::string_view uri, NavigationPosition position) const; + std::vector references( + std::string_view uri, NavigationPosition position, bool includeDeclaration) const; + std::vector documentHighlights( + std::string_view uri, NavigationPosition position) const; + std::vector documentSymbols(std::string_view uri) const; + std::vector workspaceSymbols( + std::string_view query, const std::vector& projectIds) const; + +private: + const ProjectManager& projects_; + const AnalysisScheduler& scheduler_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/outbound_message_queue.h b/lsp/include/rls/lsp/outbound_message_queue.h new file mode 100644 index 0000000..e84a82d --- /dev/null +++ b/lsp/include/rls/lsp/outbound_message_queue.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace rls::lsp { + +class OutboundMessageQueue { +public: + bool push(std::string payload); + std::optional tryPop(); + std::optional waitPop(); + void close(); + +private: + std::mutex mutex_; + std::condition_variable ready_; + std::deque messages_; + bool closed_ = false; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/presentation.h b/lsp/include/rls/lsp/presentation.h new file mode 100644 index 0000000..4600bd3 --- /dev/null +++ b/lsp/include/rls/lsp/presentation.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +namespace rls::lsp { + +struct PresentationPosition { + uint32_t line = 0; + uint32_t character = 0; +}; + +struct PresentationRange { + PresentationPosition start; + PresentationPosition end; +}; + +struct PresentationLocation { + std::string uri; + PresentationRange range; +}; + +struct PresentationType { + std::string name; + std::optional enumIdentity; +}; + +struct PresentationParameter { + std::string name; + PresentationType type; + std::optional defaultValue; + bool optional = false; +}; + +struct PresentationCallable { + std::string name; + std::vector parameters; + std::optional returnType; +}; + +enum class PresentationProvenance { + Source, + Extern, + BuiltIn, + Pattern, +}; + +enum class PresentationSymbolKind { + Region, + Function, + Enum, + EnumMember, + Parameter, + Property, + Value, +}; + +struct DocumentationBlock { + std::optional heading; + std::string markdown; +}; + +struct PresentationSymbol { + PresentationSymbolKind kind = PresentationSymbolKind::Value; + std::string name; + PresentationProvenance provenance = PresentationProvenance::Source; + std::optional type; + std::optional callable; + std::vector documentation; + std::optional declaration; +}; + +struct RenderedPresentation { + std::string detail; + std::string documentation; +}; + +class PresentationRenderer { +public: + RenderedPresentation render(const PresentationSymbol& symbol) const; + static std::string renderType(const PresentationType& type); + static std::string renderParameter(const PresentationParameter& parameter); + static std::string renderCallable(const PresentationCallable& callable); +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/project_analysis.h b/lsp/include/rls/lsp/project_analysis.h new file mode 100644 index 0000000..7706ee8 --- /dev/null +++ b/lsp/include/rls/lsp/project_analysis.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace rls::lsp { + +class AnalysisScheduler; +class ProjectManager; + +bool ScheduleProjectAnalysis( + ProjectManager& projects, AnalysisScheduler& scheduler, std::string_view projectId); + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/project_manager.h b/lsp/include/rls/lsp/project_manager.h new file mode 100644 index 0000000..c23d55f --- /dev/null +++ b/lsp/include/rls/lsp/project_manager.h @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "project.h" +#include "rls/lsp/document_store.h" + +namespace rls::lsp { + +struct ProjectSource { + // Canonical LSP URI used by the parser, snapshots, and protocol features. + std::string identity; + // Present for an open editor overlay, including a valid empty overlay. + // Absent when the scheduler must materialize the source from disk. + std::optional content; + // Present only when the scheduler may materialize this source from disk. + std::optional diskPath; +}; + +struct ManagedProject { + std::string id; + std::optional manifestPath; + std::vector sourceFiles; + bool isStandalone = false; + uint64_t generation = 0; + uint64_t documentGeneration = 0; + uint64_t manifestGeneration = 0; +}; + +struct ProjectSourceSet { + std::vector sources; + uint64_t generation = 0; + uint64_t documentGeneration = 0; + uint64_t manifestGeneration = 0; + std::string error; +}; + +struct ProjectRefreshResult { + std::vector changedProjectIds; + std::vector removedProjectIds; + std::vector errors; + std::vector configurationDiagnostics; +}; + +enum class ProjectAssignmentResult { + Assigned, + InvalidUri, + ResolutionFailed, + NotAssigned, +}; + +class ProjectManager { +public: + using Resolver = std::function; + + explicit ProjectManager(DocumentStore& documents, Resolver resolver = project::ResolveFileProject); + + ProjectAssignmentResult documentOpened(std::string_view uri); + ProjectAssignmentResult documentChanged(std::string_view uri); + ProjectAssignmentResult documentClosed(std::string_view uri); + ProjectRefreshResult refreshOpenDocuments( + const std::vector& workspaceRoots = {}, + bool restrictToWorkspaceRoots = false); + + const ManagedProject* projectForDocument(std::string_view uri) const; + const ManagedProject* project(std::string_view projectId) const; + std::optional sourceIdentityForDocument(std::string_view uri) const; + std::vector projectIds() const; + ProjectSourceSet sourceSetForDocument(std::string_view uri) const; + ProjectSourceSet sourceSetForProject(std::string_view projectId) const; + std::vector configurationDiagnostics() const; + +private: + struct Assignment { + std::string uri; + std::filesystem::path path; + std::string pathKey; + std::string sourceIdentity; + std::string projectId; + bool fileBacked = false; + }; + + static std::string projectId(const project::FileProject& project); + void recordConfigurationDiagnostics( + const std::filesystem::path& documentPath, + const std::vector& diagnostics); + void clearConfigurationDiagnostics(const std::filesystem::path& documentPath); + + DocumentStore& documents_; + Resolver resolver_; + std::unordered_map assignments_; + std::unordered_map projects_; + std::unordered_map configurationDiagnostics_; + uint64_t generation_ = 0; + uint64_t documentGeneration_ = 0; + uint64_t manifestGeneration_ = 0; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/rename_service.h b/lsp/include/rls/lsp/rename_service.h new file mode 100644 index 0000000..9f5798a --- /dev/null +++ b/lsp/include/rls/lsp/rename_service.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "rls/lsp/navigation_service.h" + +namespace rls::lsp { + +class AnalysisScheduler; +class DocumentStore; +class ProjectManager; + +struct RenameTextEdit { + NavigationRange range; + std::string newText; +}; + +struct RenameDocumentEdit { + std::string uri; + std::optional version; + std::vector edits; +}; + +struct RenameWorkspaceEdit { + std::vector documents; +}; + +enum class RenameError { + None, + NotRenameable, + InvalidName, + Collision, + StaleSnapshot, + UnsupportedClient, +}; + +template +struct RenameResult { + std::optional value; + RenameError error = RenameError::None; +}; + +class RenameService { +public: + RenameService( + const DocumentStore& documents, const ProjectManager& projects, + const AnalysisScheduler& scheduler); + + RenameResult prepare( + std::string_view uri, NavigationPosition position) const; + RenameResult rename( + std::string_view uri, NavigationPosition position, std::string_view newName, + bool supportsDocumentChanges) const; + +private: + const DocumentStore& documents_; + const ProjectManager& projects_; + const AnalysisScheduler& scheduler_; +}; + +std::string_view RenameErrorMessage(RenameError error); + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/route_modules.h b/lsp/include/rls/lsp/route_modules.h new file mode 100644 index 0000000..2f73194 --- /dev/null +++ b/lsp/include/rls/lsp/route_modules.h @@ -0,0 +1,33 @@ +#pragma once + +namespace rls::lsp { + +class CompletionService; +class DocumentSynchronizationService; +class HoverService; +class JsonRpcRouter; +class LifecycleService; +class NavigationService; +class RenameService; +class SemanticTokensService; +class SignatureHelpService; +class WorkspaceService; + +void RegisterLifecycleRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, WorkspaceService& workspace); +void RegisterDocumentSynchronizationRoutes( + JsonRpcRouter& router, DocumentSynchronizationService& synchronization); +void RegisterAuthoringRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, CompletionService& completion, + SignatureHelpService& signatureHelp, HoverService& hover); +void RegisterNavigationRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, NavigationService& navigation, + WorkspaceService& workspace); +void RegisterRenameRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, RenameService& rename); +void RegisterSemanticTokenRoutes( + JsonRpcRouter& router, SemanticTokensService& semanticTokens); +void RegisterWorkspaceRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, WorkspaceService& workspace); + +} // namespace rls::lsp diff --git a/lsp/include/rls/lsp/semantic_tokens_service.h b/lsp/include/rls/lsp/semantic_tokens_service.h new file mode 100644 index 0000000..731170a --- /dev/null +++ b/lsp/include/rls/lsp/semantic_tokens_service.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +class SemanticTokensService { +public: + SemanticTokensService(const ProjectManager& projects, AnalysisScheduler& scheduler); + + std::vector full(std::string_view uri) const; + + static const std::vector& tokenTypes(); + static const std::vector& tokenModifiers(); + +private: + const ProjectManager& projects_; + AnalysisScheduler& scheduler_; +}; + +} // namespace rls::lsp diff --git a/lsp/include/rls/lsp/server_composition_root.h b/lsp/include/rls/lsp/server_composition_root.h new file mode 100644 index 0000000..5ae2e9a --- /dev/null +++ b/lsp/include/rls/lsp/server_composition_root.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/completion_service.h" +#include "rls/lsp/diagnostic_publisher.h" +#include "rls/lsp/document_synchronization_service.h" +#include "rls/lsp/document_store.h" +#include "rls/lsp/hover_service.h" +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/navigation_service.h" +#include "rls/lsp/outbound_message_queue.h" +#include "rls/lsp/project_manager.h" +#include "rls/lsp/rename_service.h" +#include "rls/lsp/semantic_tokens_service.h" +#include "rls/lsp/signature_help_service.h" +#include "rls/lsp/workspace_service.h" + +namespace rls::lsp { + +class ServerCompositionRoot { +public: + explicit ServerCompositionRoot( + ProjectManager::Resolver resolver = project::ResolveFileProject); + + std::vector handlePayload(std::string_view payload) const; + bool shouldExit() const; + int exitCode() const; + + const DocumentStore& documents() const; + const ProjectManager& projects() const; + AnalysisScheduler& scheduler(); + const WorkspaceService& workspace() const; + OutboundMessageQueue& outbound(); + const JsonRpcRouter& router() const; + +private: + JsonRpcRouter router_; + OutboundMessageQueue outbound_; + DocumentStore documents_; + ProjectManager projects_; + LifecycleService lifecycle_; + DiagnosticPublisher diagnostics_; + AnalysisScheduler scheduler_; + NavigationService navigation_; + RenameService rename_; + CompletionService completion_; + SignatureHelpService signatureHelp_; + HoverService hover_; + SemanticTokensService semanticTokens_; + WorkspaceService workspace_; + DocumentSynchronizationService synchronization_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/signature_help_service.h b/lsp/include/rls/lsp/signature_help_service.h new file mode 100644 index 0000000..f2263d7 --- /dev/null +++ b/lsp/include/rls/lsp/signature_help_service.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/presentation.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +struct SignatureHelpResult { + std::string label; + std::string documentation; + std::vector parameterLabels; + std::optional activeParameter; +}; + +class SignatureHelpService { +public: + SignatureHelpService(const ProjectManager& projects, AnalysisScheduler& scheduler); + + std::optional signatureHelp( + std::string_view uri, PresentationPosition position) const; + +private: + const ProjectManager& projects_; + AnalysisScheduler& scheduler_; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/include/rls/lsp/workspace_service.h b/lsp/include/rls/lsp/workspace_service.h new file mode 100644 index 0000000..d486784 --- /dev/null +++ b/lsp/include/rls/lsp/workspace_service.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/diagnostic_publisher.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +class WorkspaceService { +public: + WorkspaceService( + ProjectManager& projects, AnalysisScheduler& scheduler, + DiagnosticPublisher& diagnostics); + + bool initialize( + std::vector folderUris, bool restrictToWorkspaceFolders = true); + bool changeFolders( + std::vector addedUris, std::vector removedUris); + bool watchedFilesChanged(const std::vector& uris); + + size_t folderCount() const; + std::vector projectIds() const; + +private: + bool refreshProjects(); + + ProjectManager& projects_; + AnalysisScheduler& scheduler_; + DiagnosticPublisher& diagnostics_; + std::unordered_set folders_; + std::vector folderPaths_; + bool restrictToWorkspaceFolders_ = false; +}; + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/main.cpp b/lsp/main.cpp new file mode 100644 index 0000000..1531f28 --- /dev/null +++ b/lsp/main.cpp @@ -0,0 +1,21 @@ +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include "rls/lsp/client_connection.h" +#include "rls/lsp/server_composition_root.h" + +int main() { +#ifdef _WIN32 + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +#endif + + rls::lsp::ServerCompositionRoot server; + rls::lsp::ClientConnection connection(std::cin, std::cout, std::cerr); + return connection.run(server); +} \ No newline at end of file diff --git a/lsp/src/analysis_scheduler.cpp b/lsp/src/analysis_scheduler.cpp new file mode 100644 index 0000000..05876de --- /dev/null +++ b/lsp/src/analysis_scheduler.cpp @@ -0,0 +1,324 @@ +#include "rls/lsp/analysis_scheduler.h" + +#include +#include +#include +#include +#include + +namespace rls::lsp { +namespace { + +std::optional buildSnapshot( + std::vector sources, uint64_t generation, + std::stop_token cancellation) { + return sema::AnalysisSnapshot::Create( + std::move(sources), generation, cancellation); +} + +std::optional readSource( + const std::filesystem::path& path, std::stop_token cancellation) { + if (cancellation.stop_requested()) { + return std::nullopt; + } + std::ifstream input(path, std::ios::binary); + if (!input) { + return std::nullopt; + } + + std::string content; + std::array buffer; + while (input) { + if (cancellation.stop_requested()) { + return std::nullopt; + } + input.read(buffer.data(), static_cast(buffer.size())); + const auto count = input.gcount(); + if (count > 0) { + content.append(buffer.data(), static_cast(count)); + } + } + if (!input.eof() || cancellation.stop_requested()) { + return std::nullopt; + } + return content; +} + +std::string pathString(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string value; + value.reserve(generic.size()); + for (const char8_t byte : generic) { + value.push_back(static_cast(byte)); + } + return value; +} + +std::string sourceIdentity(std::string identity) { + if (identity.starts_with("untitled:")) { + return identity; + } + return pathString(std::filesystem::path(std::move(identity))); +} + +} // namespace + +AnalysisScheduler::AnalysisScheduler() + : AnalysisScheduler(Options{}) {} + +AnalysisScheduler::AnalysisScheduler( + Options options, Builder builder, SourceReader sourceReader) + : options_(options), + builder_(builder ? std::move(builder) : Builder(buildSnapshot)), + sourceReader_(sourceReader ? std::move(sourceReader) : SourceReader(readSource)) { + if (options_.maximumConcurrency == 0) { + throw std::invalid_argument("analysis concurrency must be at least one"); + } + workers_.reserve(options_.maximumConcurrency); + for (size_t index = 0; index < options_.maximumConcurrency; ++index) { + workers_.emplace_back([this](std::stop_token shutdown) { worker(shutdown); }); + } +} + +AnalysisScheduler::~AnalysisScheduler() { + for (auto& workerThread : workers_) { + workerThread.request_stop(); + } + { + std::lock_guard lock(mutex_); + for (auto& [projectId, state] : projects_) { + if (state.activeCancellation) { + state.activeCancellation->request_stop(); + } + } + } + wake_.notify_all(); + workers_.clear(); +} + +bool AnalysisScheduler::schedule(AnalysisRequest request) { + if (request.projectId.empty() || request.sources.empty()) { + return false; + } + + std::lock_guard lock(mutex_); + ProjectState& state = projects_[request.projectId]; + if (request.generation <= state.latestGeneration + || request.documentGeneration < state.latestDocumentGeneration + || request.manifestGeneration < state.latestManifestGeneration) { + return false; + } + + state.latestGeneration = request.generation; + state.latestDocumentGeneration = request.documentGeneration; + state.latestManifestGeneration = request.manifestGeneration; + state.removed = false; + if (state.activeCancellation) { + state.activeCancellation->request_stop(); + } + state.pending = PendingRequest{ + std::move(request), + std::chrono::steady_clock::now() + options_.debounce, + }; + wake_.notify_all(); + snapshotReady_.notify_all(); + return true; +} + +void AnalysisScheduler::removeProject(std::string_view projectId) { + std::lock_guard lock(mutex_); + const auto project = projects_.find(std::string(projectId)); + if (project == projects_.end()) { + return; + } + ProjectState& state = project->second; + state.removed = true; + state.pending.reset(); + state.accepted.reset(); + if (state.activeCancellation) { + state.activeCancellation->request_stop(); + } + if (isIdle()) { + idle_.notify_all(); + } + wake_.notify_all(); + snapshotReady_.notify_all(); +} + +void AnalysisScheduler::setAcceptedHandler(AcceptedHandler handler) { + std::lock_guard lock(mutex_); + acceptedHandler_ = std::move(handler); +} + +AnalysisScheduler::Snapshot AnalysisScheduler::acceptedSnapshot(std::string_view projectId) const { + std::lock_guard lock(mutex_); + const auto project = projects_.find(std::string(projectId)); + return project == projects_.end() ? nullptr : project->second.accepted; +} + +AnalysisScheduler::Snapshot AnalysisScheduler::awaitSnapshot( + std::string_view projectId, uint64_t generation, + std::chrono::milliseconds timeout) { + std::unique_lock lock(mutex_); + const std::string id(projectId); + auto ready = [&]() { + const auto project = projects_.find(id); + if (project == projects_.end() || project->second.removed) return true; + const ProjectState& state = project->second; + if (state.accepted && state.accepted->generation() == generation) return true; + if (state.latestGeneration != generation) return true; + return !state.pending && !state.activeCancellation; + }; + + auto project = projects_.find(id); + if (project == projects_.end() || project->second.removed + || project->second.latestGeneration != generation) { + return nullptr; + } + if (project->second.accepted + && project->second.accepted->generation() == generation) { + return project->second.accepted; + } + if (project->second.pending) { + project->second.pending->readyAt = std::chrono::steady_clock::now(); + wake_.notify_all(); + } + + if (!snapshotReady_.wait_for(lock, timeout, ready)) return nullptr; + project = projects_.find(id); + if (project == projects_.end() || !project->second.accepted + || project->second.accepted->generation() != generation) { + return nullptr; + } + return project->second.accepted; +} + +void AnalysisScheduler::waitForIdle() { + std::unique_lock lock(mutex_); + idle_.wait(lock, [this] { return isIdle(); }); +} + +bool AnalysisScheduler::isIdle() const { + if (activeBuilds_ != 0) { + return false; + } + return std::none_of(projects_.begin(), projects_.end(), [](const auto& entry) { + return entry.second.pending.has_value(); + }); +} + +void AnalysisScheduler::worker(std::stop_token shutdown) { + while (!shutdown.stop_requested()) { + AnalysisRequest request; + std::shared_ptr cancellation; + + { + std::unique_lock lock(mutex_); + while (!shutdown.stop_requested()) { + const auto now = std::chrono::steady_clock::now(); + auto selected = projects_.end(); + auto nextReady = std::chrono::steady_clock::time_point::max(); + + for (auto project = projects_.begin(); project != projects_.end(); ++project) { + ProjectState& state = project->second; + if (state.removed || !state.pending || state.activeCancellation) { + continue; + } + if (state.pending->readyAt <= now) { + selected = project; + break; + } + nextReady = std::min(nextReady, state.pending->readyAt); + } + + if (selected != projects_.end()) { + ProjectState& state = selected->second; + request = std::move(state.pending->request); + state.pending.reset(); + cancellation = std::make_shared(); + state.activeCancellation = cancellation; + ++activeBuilds_; + break; + } + + if (nextReady == std::chrono::steady_clock::time_point::max()) { + wake_.wait(lock); + } else { + wake_.wait_until(lock, nextReady); + } + } + } + + if (shutdown.stop_requested()) { + break; + } + + std::optional snapshot; + try { + std::vector sources; + sources.reserve(request.sources.size()); + for (auto& source : request.sources) { + if (cancellation->stop_requested()) { + sources.clear(); + break; + } + std::optional content = std::move(source.content); + if (!content) { + if (!source.diskPath) { + sources.clear(); + break; + } + content = sourceReader_(*source.diskPath, cancellation->get_token()); + } + if (!content || cancellation->stop_requested()) { + sources.clear(); + break; + } + sources.push_back({sourceIdentity(std::move(source.identity)), std::move(*content)}); + } + if (!sources.empty()) { + snapshot = builder_( + std::move(sources), request.generation, cancellation->get_token()); + } + } catch (...) { + snapshot = std::nullopt; + } + + AcceptedHandler acceptedHandler; + Snapshot acceptedSnapshot; + { + std::lock_guard lock(mutex_); + ProjectState& state = projects_.at(request.projectId); + if (state.activeCancellation == cancellation) { + state.activeCancellation.reset(); + if (snapshot && !cancellation->stop_requested() + && !state.removed && state.latestGeneration == request.generation + && state.latestDocumentGeneration == request.documentGeneration + && state.latestManifestGeneration == request.manifestGeneration) { + state.accepted = std::move(*snapshot); + acceptedSnapshot = state.accepted; + acceptedHandler = acceptedHandler_; + } + } + snapshotReady_.notify_all(); + } + if (acceptedHandler && acceptedSnapshot) { + try { + acceptedHandler(request.projectId, std::move(acceptedSnapshot)); + } catch (...) { + } + } + { + std::lock_guard lock(mutex_); + --activeBuilds_; + if (isIdle()) { + idle_.notify_all(); + } + } + wake_.notify_all(); + } +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/authoring_routes.cpp b/lsp/src/authoring_routes.cpp new file mode 100644 index 0000000..ffceccd --- /dev/null +++ b/lsp/src/authoring_routes.cpp @@ -0,0 +1,178 @@ +#include "rls/lsp/route_modules.h" + +#include +#include + +#include + +#include "rls/lsp/completion_service.h" +#include "rls/lsp/hover_service.h" +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/signature_help_service.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& value) { + if (!value.is_object()) { + throw InvalidParams("expected object parameters"); + } + return value; +} + +uint32_t requirePositionComponent(const Json& value) { + uint64_t component = 0; + if (value.is_number_unsigned()) { + component = value.get(); + } else if (value.is_number_integer()) { + const int64_t signedComponent = value.get(); + if (signedComponent < 0) { + throw InvalidParams("position components must be non-negative"); + } + component = static_cast(signedComponent); + } else { + throw InvalidParams("position components must be integers"); + } + if (component > std::numeric_limits::max()) { + throw InvalidParams("position component is too large"); + } + return static_cast(component); +} + +Json position(const PresentationPosition& value) { + return {{"line", value.line}, {"character", value.character}}; +} + +Json range(const PresentationRange& value) { + return {{"start", position(value.start)}, {"end", position(value.end)}}; +} + +int completionKind(CompletionItemKind kind) { + switch (kind) { + case CompletionItemKind::Function: return 3; + case CompletionItemKind::Type: return 7; + case CompletionItemKind::Property: return 10; + case CompletionItemKind::Variable: return 6; + case CompletionItemKind::Value: return 12; + case CompletionItemKind::Enum: return 13; + case CompletionItemKind::Keyword: return 14; + case CompletionItemKind::EnumMember: return 20; + } + return 1; +} + +} // namespace + +void RegisterAuthoringRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, CompletionService& completion, + SignatureHelpService& signatureHelp, HoverService& hover) { + router.registerRequest("textDocument/completion", [&lifecycle, &completion](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + const auto& requestPosition = requireObject(object.at("position")); + const PresentationPosition cursor{ + requirePositionComponent(requestPosition.at("line")), + requirePositionComponent(requestPosition.at("character")), + }; + + Json result = Json::array(); + for (const auto& item : completion.complete( + document.at("uri").get(), cursor)) { + const bool useSnippet = lifecycle.supportsCompletionSnippets() + && item.snippetText.has_value(); + const bool serverIndented = useSnippet + && lifecycle.sectionSnippetIndentation() + == SectionSnippetIndentation::Server + && item.serverIndentedSnippetText.has_value(); + const std::string& insertion = serverIndented + ? *item.serverIndentedSnippetText + : useSnippet ? *item.snippetText : item.insertText; + Json completionItem = { + {"label", item.label}, + {"kind", completionKind(item.kind)}, + {"sortText", item.sortText}, + {"insertTextFormat", useSnippet ? 2 : 1}, + {"textEdit", { + {"range", range(item.replacementRange)}, + {"newText", insertion}, + }}, + }; + if (useSnippet && item.serverIndentedSnippetText) { + completionItem["insertTextMode"] = serverIndented ? 1 : 2; + } + if (!item.detail.empty()) completionItem["detail"] = item.detail; + if (!item.documentation.empty()) { + completionItem["documentation"] = { + {"kind", "markdown"}, + {"value", item.documentation}, + }; + } + result.push_back(std::move(completionItem)); + } + return result; + }); + + router.registerRequest("textDocument/signatureHelp", [&signatureHelp](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + const auto& requestPosition = requireObject(object.at("position")); + const auto result = signatureHelp.signatureHelp( + document.at("uri").get(), + { + requirePositionComponent(requestPosition.at("line")), + requirePositionComponent(requestPosition.at("character")), + }); + if (!result) return Json(nullptr); + + Json parameters = Json::array(); + for (const auto& label : result->parameterLabels) { + parameters.push_back({{"label", label}}); + } + Json signature = { + {"label", result->label}, + {"parameters", std::move(parameters)}, + }; + if (!result->documentation.empty()) { + signature["documentation"] = { + {"kind", "markdown"}, + {"value", result->documentation}, + }; + } + if (result->activeParameter) { + signature["activeParameter"] = *result->activeParameter; + } + Json response = { + {"signatures", Json::array({std::move(signature)})}, + {"activeSignature", 0}, + }; + if (result->activeParameter) { + response["activeParameter"] = *result->activeParameter; + } + return response; + }); + + router.registerRequest("textDocument/hover", [&hover](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + const auto& requestPosition = requireObject(object.at("position")); + const auto result = hover.hover( + document.at("uri").get(), + { + requirePositionComponent(requestPosition.at("line")), + requirePositionComponent(requestPosition.at("character")), + }); + if (!result) return Json(nullptr); + return Json{ + {"contents", { + {"kind", "markdown"}, + {"value", result->markdown}, + }}, + {"range", range(result->range)}, + }; + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/client_connection.cpp b/lsp/src/client_connection.cpp new file mode 100644 index 0000000..3b491de --- /dev/null +++ b/lsp/src/client_connection.cpp @@ -0,0 +1,52 @@ +#include "rls/lsp/client_connection.h" + +#include +#include +#include +#include + +#include "rls/lsp/message_framer.h" +#include "rls/lsp/server_composition_root.h" + +namespace rls::lsp { + +ClientConnection::ClientConnection( + std::istream& input, std::ostream& output, std::ostream& log) + : input_(input), output_(output), log_(log) {} + +int ClientConnection::run(ServerCompositionRoot& server) { + MessageFramer framer; + char byte = 0; + std::jthread writer([&] { + while (const auto payload = server.outbound().waitPop()) { + const std::string frame = MessageFramer::frame(*payload); + output_.write(frame.data(), static_cast(frame.size())); + output_.flush(); + } + }); + + try { + while (!server.shouldExit() && input_.get(byte)) { + framer.append(std::string_view(&byte, 1)); + while (const auto payload = framer.popMessage()) { + for (const auto& response : server.handlePayload(*payload)) { + server.outbound().push(response); + } + if (server.shouldExit()) { + break; + } + } + } + } catch (const std::exception& error) { + server.outbound().close(); + writer.join(); + log_ << "rls-language-server: " << error.what() << '\n'; + return 1; + } + + server.outbound().close(); + writer.join(); + return server.shouldExit() ? server.exitCode() : 1; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/completion_service.cpp b/lsp/src/completion_service.cpp new file mode 100644 index 0000000..b74e1d3 --- /dev/null +++ b/lsp/src/completion_service.cpp @@ -0,0 +1,738 @@ +#include "rls/lsp/completion_service.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +enum class CompletionContext { + TopLevel, + Type, + RegionBody, + SectionEntry, + MemberAccess, + Expression, + Unsupported, +}; + +struct CurrentDocument { + AnalysisScheduler::Snapshot snapshot; + std::string path; + const ast::SourceText* source = nullptr; + const parser::SourceIndex* sourceIndex = nullptr; +}; + +struct Candidate { + CompletionItem item; + size_t contextRank = 0; + bool prefixMatch = false; +}; + +std::string pathString(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string value; + value.reserve(generic.size()); + for (const char8_t byte : generic) { + value.push_back(static_cast(byte)); + } + return value; +} + +std::optional currentDocument( + const ProjectManager& projects, AnalysisScheduler& scheduler, + std::string_view uri) { + const auto* project = projects.projectForDocument(uri); + const auto identity = projects.sourceIdentityForDocument(uri); + if (!project || !identity) return std::nullopt; + const std::string projectId = project->id; + const uint64_t generation = project->generation; + + auto snapshot = scheduler.acceptedSnapshot(projectId); + if (!snapshot || snapshot->generation() != generation) { + snapshot = scheduler.awaitSnapshot(projectId, generation); + } + if (!snapshot || snapshot->generation() != generation) return std::nullopt; + const std::string& documentPath = *identity; + const auto* source = snapshot->sourceText(documentPath); + const auto* sourceIndex = snapshot->sourceIndex(documentPath); + if (!source || !sourceIndex) return std::nullopt; + return CurrentDocument{snapshot, documentPath, source, sourceIndex}; +} + +std::optional presentationRange( + const ast::SourceText& source, ast::SourceRange range) { + const auto startOffset = source.byteOffsetFromUtf8Position(range.start); + const auto endOffset = source.byteOffsetFromUtf8Position(range.end); + if (!startOffset || !endOffset) return std::nullopt; + const auto start = source.utf16PositionAtByteOffset(*startOffset); + const auto end = source.utf16PositionAtByteOffset(*endOffset); + if (!start || !end) return std::nullopt; + return PresentationRange{ + {start->line - 1, start->column - 1}, + {end->line - 1, end->column - 1}, + }; +} + +std::string asciiLower(std::string_view value) { + std::string result; + result.reserve(value.size()); + for (const char character : value) { + result.push_back(static_cast( + std::tolower(static_cast(character)))); + } + return result; +} + +bool startsWithCaseInsensitive(std::string_view value, std::string_view prefix) { + if (prefix.size() > value.size()) return false; + return asciiLower(value.substr(0, prefix.size())) == asciiLower(prefix); +} + +CompletionContext completionContextAt( + const parser::SourceIndex& index, ast::Position position, + const std::optional& region, + const std::optional& typePosition, + const std::optional& sectionEntry, + const std::optional& memberAccess, + const std::optional& namedArgument, + const std::optional& callArgument) { + if (typePosition) return CompletionContext::Type; + if (sectionEntry) return CompletionContext::SectionEntry; + if (memberAccess) return CompletionContext::MemberAccess; + if (namedArgument || callArgument) return CompletionContext::Expression; + if (const auto name = index.nameAt(position)) { + switch (name->kind) { + case parser::SourceNameKind::Type: + return CompletionContext::Type; + case parser::SourceNameKind::RegionDataKey: + return CompletionContext::RegionBody; + case parser::SourceNameKind::Identifier: + case parser::SourceNameKind::CallCallee: + return CompletionContext::Expression; + case parser::SourceNameKind::MemberObject: + case parser::SourceNameKind::Member: + case parser::SourceNameKind::ArgumentLabel: + return CompletionContext::Unsupported; + default: + break; + } + } + const auto syntax = index.syntaxAt(position); + if (!syntax) { + if (!region) return CompletionContext::TopLevel; + return region->activeSection + ? CompletionContext::Unsupported + : CompletionContext::RegionBody; + } + switch (syntax->kind) { + case parser::SyntaxKind::Expression: + case parser::SyntaxKind::Call: + case parser::SyntaxKind::Argument: + return CompletionContext::Expression; + default: + break; + } + if (region && !region->activeSection) return CompletionContext::RegionBody; + return CompletionContext::Unsupported; +} + +std::string_view sectionName(ast::SectionKind kind) { + switch (kind) { + case ast::SectionKind::Events: return "events"; + case ast::SectionKind::Locations: return "locations"; + case ast::SectionKind::Exits: return "exits"; + } + return {}; +} + +std::string regionKeySnippet(std::string_view key) { + return std::string(key) + ": ${1}"; +} + +std::string sectionSnippet(ast::SectionKind kind) { + return std::string(sectionName(kind)) + " {\n $0\n}"; +} + +std::string sectionSnippet( + ast::SectionKind kind, std::string_view indentation) { + return std::string(sectionName(kind)) + " {\n" + + std::string(indentation) + " $0\n" + + std::string(indentation) + '}'; +} + +std::string lineIndentationAt( + const ast::SourceText& source, ast::Position position) { + const auto offset = source.byteOffsetFromUtf8Position(position); + if (!offset || position.line == 0 || position.line > source.lineStarts().size()) return {}; + const size_t lineStart = source.lineStarts()[position.line - 1]; + const std::string indentation = source.content().substr(lineStart, *offset - lineStart); + const bool onlyWhitespace = std::all_of( + indentation.begin(), indentation.end(), [](char character) { + return character == ' ' || character == '\t'; + }); + return onlyWhitespace ? indentation : std::string{}; +} + +PresentationType presentationType( + ast::Type type, const std::optional& enumName = std::nullopt) { + std::string name; + switch (type) { + case ast::Type::Bool: name = "Bool"; break; + case ast::Type::Int: name = "Int"; break; + case ast::Type::String: name = "String"; break; + case ast::Type::List: name = "List"; break; + case ast::Type::Callable: name = "Callable"; break; + case ast::Type::Condition: name = "Condition"; break; + case ast::Type::Enum: name = "Enum"; break; + case ast::Type::Region: name = "Region"; break; + case ast::Type::Event: name = "Event"; break; + case ast::Type::Location: name = "Location"; break; + case ast::Type::Void: name = "Void"; break; + case ast::Type::Error: name = ""; break; + } + return {std::move(name), enumName}; +} + +PresentationProvenance presentationProvenance(sema::SymbolProvenance provenance) { + switch (provenance) { + case sema::SymbolProvenance::Source: + return PresentationProvenance::Source; + case sema::SymbolProvenance::Extern: + return PresentationProvenance::Extern; + case sema::SymbolProvenance::Pattern: + return PresentationProvenance::Pattern; + } + return PresentationProvenance::Source; +} + +PresentationSymbol presentationSymbol( + const sema::AnalysisSnapshot& snapshot, const sema::SymbolRecord& record) { + PresentationSymbol result{ + .name = record.displayName, + .provenance = presentationProvenance(record.provenance), + }; + if (record.type) result.type = presentationType(*record.type, record.enumName); + + switch (record.category) { + case sema::SymbolCategory::Region: + result.kind = PresentationSymbolKind::Region; + break; + case sema::SymbolCategory::Define: + case sema::SymbolCategory::ExternDefine: { + result.kind = PresentationSymbolKind::Function; + PresentationCallable callable{.name = record.displayName}; + std::vector parameters; + for (const auto& candidate : snapshot.semanticIndex().symbols()) { + if (candidate.category == sema::SymbolCategory::Parameter + && candidate.container == record.id) { + parameters.push_back(&candidate); + } + } + std::sort(parameters.begin(), parameters.end(), [](const auto* left, const auto* right) { + return std::tie(left->selection.start.line, left->selection.start.column) + < std::tie(right->selection.start.line, right->selection.start.column); + }); + for (const auto* parameter : parameters) { + callable.parameters.push_back({ + .name = parameter->displayName, + .type = parameter->type + ? presentationType(*parameter->type, parameter->enumName) + : PresentationType{""}, + .defaultValue = parameter->defaultValue, + .optional = parameter->optional, + }); + } + if (record.type) { + callable.returnType = presentationType(*record.type, record.enumName); + } + result.callable = std::move(callable); + break; + } + case sema::SymbolCategory::Enum: + result.kind = PresentationSymbolKind::Enum; + break; + case sema::SymbolCategory::EnumMember: + result.kind = PresentationSymbolKind::EnumMember; + break; + case sema::SymbolCategory::Parameter: + result.kind = PresentationSymbolKind::Parameter; + break; + default: + result.kind = PresentationSymbolKind::Value; + break; + } + return result; +} + +PresentationSymbol presentationSymbol(const sema::ObservedEnumValue& value) { + return { + .kind = PresentationSymbolKind::EnumMember, + .name = value.displayName, + .provenance = PresentationProvenance::Pattern, + .type = PresentationType{.name = "Enum", .enumIdentity = value.enumName}, + }; +} + +CompletionItemKind completionKind(sema::SymbolCategory category) { + switch (category) { + case sema::SymbolCategory::Define: + case sema::SymbolCategory::ExternDefine: + return CompletionItemKind::Function; + case sema::SymbolCategory::Enum: + return CompletionItemKind::Enum; + case sema::SymbolCategory::EnumMember: + return CompletionItemKind::EnumMember; + case sema::SymbolCategory::Parameter: + return CompletionItemKind::Variable; + default: + return CompletionItemKind::Value; + } +} + +bool matchesExpectedType( + const sema::SymbolRecord& symbol, const std::optional& expected) { + if (!expected) return true; + if (!symbol.type || *symbol.type != expected->type) return false; + return expected->type != ast::Type::Enum || symbol.enumName == expected->enumName; +} + +void addCandidate( + std::vector& candidates, std::set& labels, + CompletionItem item, size_t rank, std::string_view prefix) { + if (!labels.insert(item.label).second) return; + const bool prefixMatch = !prefix.empty() + && startsWithCaseInsensitive(item.label, prefix); + candidates.push_back({std::move(item), rank, prefixMatch}); +} + +} // namespace + +CompletionService::CompletionService( + const ProjectManager& projects, AnalysisScheduler& scheduler) + : projects_(projects), scheduler_(scheduler) {} + +std::vector CompletionService::complete( + std::string_view uri, PresentationPosition position) const { + if (position.line == std::numeric_limits::max() + || position.character == std::numeric_limits::max()) { + return {}; + } + const auto document = currentDocument(projects_, scheduler_, uri); + if (!document) return {}; + + const auto cursorOffset = document->source->byteOffsetFromUtf16Position({ + position.line + 1, position.character + 1}); + if (!cursorOffset) return {}; + const auto cursorPosition = document->source->utf8PositionAtByteOffset(*cursorOffset); + if (!cursorPosition) return {}; + + const auto token = document->source->incompleteTokenRangeAt(*cursorPosition); + ast::SourceRange replacement{*cursorPosition, *cursorPosition}; + ast::Position contextPosition = *cursorPosition; + std::string prefix; + if (token) { + replacement = *token; + const auto tokenStart = document->source->byteOffsetFromUtf8Position(token->start); + if (!tokenStart || *tokenStart > *cursorOffset) return {}; + prefix = document->source->content().substr(*tokenStart, *cursorOffset - *tokenStart); + if (*cursorOffset > *tokenStart) { + const auto previous = document->source->utf8PositionAtByteOffset(*cursorOffset - 1); + if (previous) contextPosition = *previous; + } + } + const auto editRange = presentationRange(*document->source, replacement); + if (!editRange) return {}; + const std::string lineIndentation = lineIndentationAt(*document->source, replacement.start); + + const auto region = document->sourceIndex->regionContextAt(contextPosition); + const auto typePosition = document->sourceIndex->typePositionAt(*cursorPosition); + const auto sectionEntry = document->sourceIndex->sectionEntryAt(*cursorPosition); + const auto memberAccess = document->sourceIndex->memberAccessAt(*cursorPosition); + auto namedArgument = document->sourceIndex->namedArgumentAt(*cursorPosition); + if (namedArgument && document->sourceIndex->syntaxAt(contextPosition) + && !document->sourceIndex->enclosingCall(contextPosition)) { + namedArgument.reset(); + } + auto callArgument = document->sourceIndex->callArgumentAt(*cursorPosition); + if (callArgument && document->sourceIndex->syntaxAt(contextPosition) + && !document->sourceIndex->enclosingCall(contextPosition)) { + callArgument.reset(); + } + const auto context = completionContextAt( + *document->sourceIndex, contextPosition, region, typePosition, + sectionEntry, memberAccess, namedArgument, callArgument); + const auto parametersFor = [&](const sema::SymbolRecord& callable) { + std::vector parameters; + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category == sema::SymbolCategory::Parameter + && symbol.container == callable.id) { + parameters.push_back(&symbol); + } + } + std::sort(parameters.begin(), parameters.end(), [](const auto* left, const auto* right) { + return std::tie(left->selection.start.line, left->selection.start.column) + < std::tie(right->selection.start.line, right->selection.start.column); + }); + return parameters; + }; + + auto expected = document->snapshot->expectedTypeAt(document->path, contextPosition); + std::vector candidates; + std::set labels; + const auto makeItem = [&](std::string label, CompletionItemKind kind, + std::string detail = {}, std::string documentation = {}) { + return CompletionItem{ + .label = label, + .kind = kind, + .detail = std::move(detail), + .documentation = std::move(documentation), + .insertText = std::move(label), + .replacementRange = *editRange, + }; + }; + + if (context == CompletionContext::TopLevel) { + static constexpr std::string_view keywords[] = { + "define", "enum", "extend region", "extern define", "extern enum", "region", + }; + for (const auto keyword : keywords) { + addCandidate(candidates, labels, + makeItem(std::string(keyword), CompletionItemKind::Keyword, "declaration keyword"), + 0, prefix); + } + } else if (context == CompletionContext::Type) { + static constexpr std::string_view builtInTypes[] = { + "Bool", "Callable", "Condition", "Event", "Int", "List", "Location", + "Region", "String", + }; + for (const auto type : builtInTypes) { + addCandidate(candidates, labels, + makeItem(std::string(type), CompletionItemKind::Type, "built-in type"), + 10, prefix); + } + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category != sema::SymbolCategory::Enum) continue; + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, symbol)); + addCandidate(candidates, labels, + makeItem(symbol.displayName, CompletionItemKind::Enum, + rendered.detail, rendered.documentation), + 0, prefix); + } + for (const auto& documentPath : document->snapshot->documentPaths()) { + const auto* sourceIndex = document->snapshot->sourceIndex(documentPath); + if (!sourceIndex) continue; + for (const auto& enumName : sourceIndex->enumNames()) { + PresentationSymbol symbol{ + .kind = PresentationSymbolKind::Enum, + .name = enumName, + .type = PresentationType{.name = "Enum", .enumIdentity = enumName}, + }; + const auto rendered = PresentationRenderer{}.render(symbol); + addCandidate(candidates, labels, + makeItem(enumName, CompletionItemKind::Enum, + rendered.detail, rendered.documentation), + 0, prefix); + } + } + } else if (context == CompletionContext::RegionBody && region) { + if (!region->extension) { + std::set observedKeys; + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category == sema::SymbolCategory::RegionDataEntry) { + observedKeys.insert(symbol.displayName); + } + } + for (const auto& key : observedKeys) { + if (std::find(region->dataKeys.begin(), region->dataKeys.end(), key) + != region->dataKeys.end()) { + continue; + } + addCandidate(candidates, labels, + [&] { + auto item = makeItem(key, CompletionItemKind::Property, + "project region data key"); + item.snippetText = regionKeySnippet(key); + return item; + }(), 0, prefix); + } + } + for (const auto kind : { + ast::SectionKind::Events, + ast::SectionKind::Locations, + ast::SectionKind::Exits, + }) { + if (std::find(region->sectionKinds.begin(), region->sectionKinds.end(), kind) + != region->sectionKinds.end()) { + continue; + } + addCandidate(candidates, labels, + [&] { + auto item = makeItem(std::string(sectionName(kind)), CompletionItemKind::Keyword, + "region section"); + item.snippetText = sectionSnippet(kind); + item.serverIndentedSnippetText = sectionSnippet(kind, lineIndentation); + return item; + }(), 10, prefix); + } + } else if (context == CompletionContext::SectionEntry && sectionEntry && region) { + const auto expectedType = sectionEntry->kind == ast::SectionKind::Events + ? std::optional(ast::Type::Event) + : sectionEntry->kind == ast::SectionKind::Locations + ? std::optional(ast::Type::Location) + : sectionEntry->kind == ast::SectionKind::Exits + ? std::optional(ast::Type::Region) + : std::nullopt; + if (expectedType) { + std::set existingNames( + region->activeSectionEntries.begin(), + region->activeSectionEntries.end()); + std::set recoveredNames; + for (const auto& documentPath : document->snapshot->documentPaths()) { + const auto* sourceIndex = document->snapshot->sourceIndex(documentPath); + if (!sourceIndex) continue; + for (auto& name : sourceIndex->sectionEntryNames(sectionEntry->kind)) { + recoveredNames.insert(std::move(name)); + } + for (auto& name : sourceIndex->sectionEntryNames( + sectionEntry->kind, region->name)) { + existingNames.insert(std::move(name)); + } + if (*expectedType == ast::Type::Region) { + for (auto& name : sourceIndex->regionNames()) { + recoveredNames.insert(std::move(name)); + } + } + } + existingNames.insert(region->name); + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category != sema::SymbolCategory::SectionEntry + || sectionEntry->kind == ast::SectionKind::Exits + || symbol.type != expectedType || !symbol.container) { + continue; + } + const auto container = document->snapshot->declaration(*symbol.container); + if (container && container->displayName == region->name) { + existingNames.insert(symbol.displayName); + } + } + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + const bool matchingDomainEntry = + symbol.category == sema::SymbolCategory::SectionEntry + && sectionEntry->kind != ast::SectionKind::Exits + && symbol.type == expectedType; + const bool matchingRegion = + symbol.category == sema::SymbolCategory::Region + && *expectedType == ast::Type::Region; + if ((!matchingDomainEntry && !matchingRegion) + || existingNames.contains(symbol.displayName)) { + continue; + } + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, symbol)); + auto item = makeItem(symbol.displayName, CompletionItemKind::Value, + rendered.detail, rendered.documentation); + item.insertText += ": "; + item.snippetText = symbol.displayName + ": ${1}"; + addCandidate(candidates, labels, std::move(item), 0, prefix); + } + for (const auto& name : recoveredNames) { + if (existingNames.contains(name)) continue; + PresentationSymbol symbol{ + .name = name, + .type = presentationType(*expectedType), + }; + const auto rendered = PresentationRenderer{}.render(symbol); + auto item = makeItem(name, CompletionItemKind::Value, + rendered.detail, rendered.documentation); + item.insertText += ": "; + item.snippetText = name + ": ${1}"; + addCandidate(candidates, labels, std::move(item), 0, prefix); + } + } + } else if (context == CompletionContext::MemberAccess && memberAccess) { + const sema::SymbolRecord* enumSymbol = nullptr; + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category == sema::SymbolCategory::Enum + && symbol.displayName == memberAccess->object) { + enumSymbol = &symbol; + break; + } + } + if (enumSymbol) { + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category != sema::SymbolCategory::EnumMember + || symbol.container != enumSymbol->id) { + continue; + } + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, symbol)); + addCandidate(candidates, labels, + makeItem(symbol.displayName, CompletionItemKind::EnumMember, + rendered.detail, rendered.documentation), + 0, prefix); + } + for (const auto& value : document->snapshot->semanticIndex().observedEnumValues()) { + if (value.enumName != memberAccess->object) continue; + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(value)); + addCandidate(candidates, labels, + makeItem(value.displayName, CompletionItemKind::EnumMember, + rendered.detail, rendered.documentation), + 0, prefix); + } + } + } else if (context == CompletionContext::Expression) { + if (namedArgument) { + const auto resolvedCall = document->snapshot->callAt( + document->path, *cursorPosition); + const auto callable = resolvedCall && resolvedCall->target + ? document->snapshot->declaration(*resolvedCall->target) + : std::nullopt; + if (callable) { + const auto parameters = parametersFor(*callable); + + std::vector bound(parameters.size(), false); + size_t nextPositional = 0; + for (size_t argumentIndex = 0; + argumentIndex < namedArgument->argumentLabels.size(); ++argumentIndex) { + if (argumentIndex == namedArgument->activeArgument) continue; + const auto& label = namedArgument->argumentLabels[argumentIndex]; + if (label) { + const auto parameter = std::find_if( + parameters.begin(), parameters.end(), [&](const auto* candidate) { + return candidate->displayName == *label; + }); + if (parameter != parameters.end()) { + bound[static_cast(parameter - parameters.begin())] = true; + } + continue; + } + if (argumentIndex > namedArgument->activeArgument) continue; + while (nextPositional < bound.size() && bound[nextPositional]) { + ++nextPositional; + } + if (nextPositional < bound.size()) bound[nextPositional++] = true; + } + + for (size_t parameterIndex = 0; + parameterIndex < parameters.size(); ++parameterIndex) { + if (bound[parameterIndex]) continue; + const auto* parameter = parameters[parameterIndex]; + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, *parameter)); + auto item = makeItem(parameter->displayName, CompletionItemKind::Property, + rendered.detail, rendered.documentation); + item.insertText += ": "; + item.snippetText = parameter->displayName + ": ${1}"; + addCandidate(candidates, labels, std::move(item), 0, prefix); + } + } + } + for (const auto symbolId : document->snapshot->visibleSymbolsAt( + document->path, contextPosition)) { + const auto symbol = document->snapshot->declaration(symbolId); + if (!symbol) continue; + const bool callable = symbol->category == sema::SymbolCategory::Define + || symbol->category == sema::SymbolCategory::ExternDefine; + const bool parameter = symbol->category == sema::SymbolCategory::Parameter; + const bool domainValue = symbol->category == sema::SymbolCategory::Region + || (symbol->category == sema::SymbolCategory::SectionEntry + && (symbol->type == ast::Type::Event + || symbol->type == ast::Type::Location)); + if ((!callable && !parameter && !domainValue) + || ((parameter || domainValue) && !matchesExpectedType(*symbol, expected))) { + continue; + } + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, *symbol)); + const size_t rank = domainValue ? 5 : (parameter ? 10 : 20); + addCandidate(candidates, labels, + makeItem(symbol->displayName, completionKind(symbol->category), + rendered.detail, rendered.documentation), + rank, prefix); + } + + if (expected && expected->type == ast::Type::Enum && expected->enumName) { + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category != sema::SymbolCategory::EnumMember + || symbol.enumName != expected->enumName) { + continue; + } + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, symbol)); + addCandidate(candidates, labels, + makeItem(symbol.displayName, CompletionItemKind::EnumMember, + rendered.detail, rendered.documentation), + 0, prefix); + } + for (const auto& value : document->snapshot->semanticIndex().observedEnumValues()) { + if (value.enumName != expected->enumName) continue; + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(value)); + addCandidate(candidates, labels, + makeItem(value.displayName, CompletionItemKind::EnumMember, + rendered.detail, rendered.documentation), + 0, prefix); + } + } + + if (!expected || expected->type == ast::Type::Bool) { + for (const std::string_view literal : {"always", "false", "never", "true"}) { + PresentationSymbol symbol{ + .name = std::string(literal), + .provenance = PresentationProvenance::BuiltIn, + .type = PresentationType{.name = "Bool"}, + }; + const auto rendered = PresentationRenderer{}.render(symbol); + addCandidate(candidates, labels, + makeItem(std::string(literal), CompletionItemKind::Value, + rendered.detail, rendered.documentation), + 30, prefix); + } + } + for (const std::string_view keyword : {"match", "not"}) { + addCandidate(candidates, labels, + makeItem(std::string(keyword), CompletionItemKind::Keyword, "expression keyword"), + 40, prefix); + } + if (region) { + PresentationSymbol symbol{ + .name = "here", + .provenance = PresentationProvenance::BuiltIn, + .type = PresentationType{.name = "Enum", .enumIdentity = "Region"}, + }; + const auto rendered = PresentationRenderer{}.render(symbol); + addCandidate(candidates, labels, + makeItem("here", CompletionItemKind::Keyword, + rendered.detail, rendered.documentation), + 5, prefix); + } + } + + std::sort(candidates.begin(), candidates.end(), [](const Candidate& left, const Candidate& right) { + return std::tuple(!left.prefixMatch, left.contextRank, asciiLower(left.item.label), left.item.label) + < std::tuple(!right.prefixMatch, right.contextRank, asciiLower(right.item.label), right.item.label); + }); + std::vector result; + result.reserve(candidates.size()); + for (size_t index = 0; index < candidates.size(); ++index) { + const std::string ordinal = std::to_string(index); + const size_t padding = ordinal.size() < 8 ? 8 - ordinal.size() : 0; + candidates[index].item.sortText = std::string(padding, '0') + ordinal; + result.push_back(std::move(candidates[index].item)); + } + return result; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/diagnostic_publisher.cpp b/lsp/src/diagnostic_publisher.cpp new file mode 100644 index 0000000..1152a20 --- /dev/null +++ b/lsp/src/diagnostic_publisher.cpp @@ -0,0 +1,338 @@ +#include "rls/lsp/diagnostic_publisher.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +int severity(ast::DiagnosticLevel level) { + switch (level) { + case ast::DiagnosticLevel::Error: + return 1; + case ast::DiagnosticLevel::Warning: + return 2; + case ast::DiagnosticLevel::Info: + return 3; + } + return 3; +} + +Json zeroRange() { + return { + {"start", {{"line", 0}, {"character", 0}}}, + {"end", {{"line", 0}, {"character", 0}}}, + }; +} + +Json rangeFor(const sema::AnalysisSnapshot& snapshot, const ast::Span& span) { + const ast::SourceText* source = snapshot.sourceText(span.file); + if (!source) { + return zeroRange(); + } + const auto startOffset = source->byteOffsetFromUtf8Position(span.start); + const auto endOffset = source->byteOffsetFromUtf8Position(span.end); + if (!startOffset || !endOffset) { + return zeroRange(); + } + const auto start = source->utf16PositionAtByteOffset(*startOffset); + const auto end = source->utf16PositionAtByteOffset(*endOffset); + if (!start || !end) { + return zeroRange(); + } + return Json{ + {"start", {{"line", start->line - 1}, {"character", start->column - 1}}}, + {"end", {{"line", end->line - 1}, {"character", end->column - 1}}}, + }; +} + +Json rangeFor(const project::ConfigurationDiagnostic& diagnostic) { + std::ifstream input(diagnostic.path, std::ios::binary); + if (!input) { + return zeroRange(); + } + const std::string content{ + std::istreambuf_iterator(input), std::istreambuf_iterator()}; + const auto source = ast::SourceText::FromUtf8(content); + if (!source) { + return zeroRange(); + } + const size_t startOffset = std::min(diagnostic.startByte, content.size()); + const size_t endOffset = std::min( + std::max(diagnostic.endByte, startOffset), content.size()); + const auto start = source->utf16PositionAtByteOffset(startOffset); + const auto end = source->utf16PositionAtByteOffset(endOffset); + if (!start || !end) { + return zeroRange(); + } + return { + {"start", {{"line", start->line - 1}, {"character", start->column - 1}}}, + {"end", {{"line", end->line - 1}, {"character", end->column - 1}}}, + }; +} + +std::optional uriForSource(std::string_view identity) { + const bool windowsDrivePath = identity.size() >= 3 + && std::isalpha(static_cast(identity[0])) + && identity[1] == ':' && identity[2] == '/'; + if (!windowsDrivePath) { + if (const auto normalized = NormalizeDocumentUri(identity)) { + return normalized; + } + } + return PathToFileUri(std::filesystem::path(identity)); +} + +std::optional canonicalUriKey(std::string_view uri) { + const auto path = FileUriToPath(uri); + if (!path) { + return DocumentUriKey(uri); + } + const auto canonicalUri = PathToFileUri(*path); + return canonicalUri ? DocumentUriKey(*canonicalUri) : std::nullopt; +} + +Json actionData(const ast::DiagnosticActionData& data) { + return { + {"version", data.version}, + {"actionKind", data.actionKind}, + {"arguments", data.arguments}, + }; +} + +Json actionData(const project::ConfigurationDiagnosticData& data) { + return { + {"version", data.version}, + {"actionKind", data.actionKind}, + {"arguments", data.arguments}, + }; +} + +Json diagnosticsFor(const sema::AnalysisSnapshot& snapshot, std::string_view path) { + Json diagnostics = Json::array(); + for (const auto& diagnostic : snapshot.diagnosticsFor(path)) { + Json value = { + {"range", rangeFor(snapshot, diagnostic.span)}, + {"severity", severity(diagnostic.level)}, + {"code", diagnostic.code}, + {"source", "rls"}, + {"message", diagnostic.message}, + }; + Json relatedInformation = Json::array(); + for (const auto& related : diagnostic.related) { + const auto relatedUri = uriForSource(related.span.file); + if (!relatedUri) { + continue; + } + relatedInformation.push_back({ + {"location", { + {"uri", *relatedUri}, + {"range", rangeFor(snapshot, related.span)}, + }}, + {"message", related.message}, + }); + } + if (!relatedInformation.empty()) { + value["relatedInformation"] = std::move(relatedInformation); + } + if (diagnostic.data) { + value["data"] = actionData(*diagnostic.data); + } + diagnostics.push_back(std::move(value)); + } + return diagnostics; +} + +std::string notification(std::string_view uri, Json diagnostics) { + return Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/publishDiagnostics"}, + {"params", {{"uri", uri}, {"diagnostics", std::move(diagnostics)}}}, + }.dump(); +} + +} // namespace + +DiagnosticPublisher::DiagnosticPublisher(OutboundMessageQueue& outbound) + : outbound_(outbound) {} + +void DiagnosticPublisher::documentOpened(std::string_view uri) { + const auto key = canonicalUriKey(uri); + const auto normalized = NormalizeDocumentUri(uri); + if (!key || !normalized) { + return; + } + std::lock_guard lock(mutex_); + suppressed_.erase(*key); + openedUris_[*key] = *normalized; +} + +void DiagnosticPublisher::documentClosed(std::string_view uri, bool standalone) { + const auto normalized = NormalizeDocumentUri(uri); + const auto key = canonicalUriKey(uri); + if (!normalized || !key) { + return; + } + + { + std::lock_guard lock(mutex_); + openedUris_.erase(*key); + if (!standalone) { + return; + } + suppressed_.insert(*key); + for (auto& [projectId, documents] : published_) { + documents.erase(*key); + } + } + outbound_.push(notification(*normalized, Json::array())); +} + +void DiagnosticPublisher::clearProject(std::string_view projectId) { + std::vector messages; + { + std::lock_guard lock(mutex_); + const auto project = published_.find(std::string(projectId)); + if (project == published_.end()) { + return; + } + for (const auto& [key, document] : project->second) { + if (!suppressed_.contains(key)) { + messages.push_back(notification(document.uri, Json::array())); + } + } + published_.erase(project); + } + for (auto& message : messages) { + outbound_.push(std::move(message)); + } +} + +void DiagnosticPublisher::publishConfigurationDiagnostics( + const std::vector& diagnostics) { + std::unordered_map grouped; + std::unordered_map uris; + for (const auto& diagnostic : diagnostics) { + const auto uri = PathToFileUri(diagnostic.path); + if (!uri) { + continue; + } + const auto key = DocumentUriKey(*uri); + if (!key) { + continue; + } + if (!grouped.contains(*key)) grouped[*key] = Json::array(); + Json value = { + {"range", rangeFor(diagnostic)}, + {"severity", 1}, + {"code", diagnostic.code}, + {"source", "rls"}, + {"message", diagnostic.message}, + }; + if (diagnostic.data) { + value["data"] = actionData(*diagnostic.data); + } + grouped[*key].push_back(std::move(value)); + uris[*key] = *uri; + } + + DocumentPayloads current; + for (auto& [key, values] : grouped) { + current[key] = PublishedDocument{uris.at(key), values.dump()}; + } + + std::vector messages; + { + std::lock_guard lock(mutex_); + for (const auto& [key, document] : current) { + const auto old = configurationPublished_.find(key); + if (old == configurationPublished_.end() + || old->second.diagnostics != document.diagnostics) { + messages.push_back(notification( + document.uri, Json::parse(document.diagnostics))); + } + } + for (const auto& [key, document] : configurationPublished_) { + if (!current.contains(key)) { + messages.push_back(notification(document.uri, Json::array())); + } + } + configurationPublished_ = std::move(current); + } + for (auto& message : messages) { + outbound_.push(std::move(message)); + } +} + +void DiagnosticPublisher::acceptedSnapshot( + std::string projectId, std::shared_ptr snapshot) { + if (!snapshot) { + return; + } + + DocumentPayloads current; + for (const auto& path : snapshot->documentPaths()) { + const auto uri = uriForSource(path); + if (!uri) { + continue; + } + const auto key = canonicalUriKey(*uri); + if (!key) { + continue; + } + std::string publishedUri = *uri; + { + std::lock_guard lock(mutex_); + const auto opened = openedUris_.find(*key); + if (opened != openedUris_.end()) { + publishedUri = opened->second; + } + } + current[*key] = PublishedDocument{ + std::move(publishedUri), + diagnosticsFor(*snapshot, path).dump(), + }; + } + + std::vector messages; + { + std::lock_guard lock(mutex_); + DocumentPayloads& previous = published_[projectId]; + for (const auto& [key, document] : current) { + if (suppressed_.contains(key)) { + continue; + } + const auto old = previous.find(key); + if (old == previous.end() || old->second.diagnostics != document.diagnostics) { + messages.push_back(notification( + document.uri, Json::parse(document.diagnostics))); + } + } + for (const auto& [key, document] : previous) { + if (!current.contains(key) && !suppressed_.contains(key)) { + messages.push_back(notification(document.uri, Json::array())); + } + } + for (const auto& key : suppressed_) { + current.erase(key); + } + previous = std::move(current); + } + + for (auto& message : messages) { + outbound_.push(std::move(message)); + } +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/document_store.cpp b/lsp/src/document_store.cpp new file mode 100644 index 0000000..876b300 --- /dev/null +++ b/lsp/src/document_store.cpp @@ -0,0 +1,65 @@ +#include "rls/lsp/document_store.h" + +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { + +DocumentUpdateResult DocumentStore::open( + std::string uri, std::string languageId, int64_t version, std::string text) { + auto normalizedUri = NormalizeDocumentUri(uri); + auto key = DocumentUriKey(uri); + if (!normalizedUri || !key) { + return DocumentUpdateResult::InvalidUri; + } + + const auto existing = documents_.find(*key); + if (existing != documents_.end() && version <= existing->second.version) { + return DocumentUpdateResult::StaleVersion; + } + + documents_.insert_or_assign(*key, TextDocument{ + std::move(*normalizedUri), std::move(languageId), version, std::move(text)}); + return DocumentUpdateResult::Applied; +} + +DocumentUpdateResult DocumentStore::applyFullChange( + std::string_view uri, int64_t version, std::string text) { + const auto key = DocumentUriKey(uri); + if (!key) { + return DocumentUpdateResult::InvalidUri; + } + + const auto document = documents_.find(*key); + if (document == documents_.end()) { + return DocumentUpdateResult::NotOpen; + } + if (version <= document->second.version) { + return DocumentUpdateResult::StaleVersion; + } + + document->second.version = version; + document->second.text = std::move(text); + return DocumentUpdateResult::Applied; +} + +bool DocumentStore::close(std::string_view uri) { + const auto key = DocumentUriKey(uri); + return key && documents_.erase(*key) > 0; +} + +const TextDocument* DocumentStore::find(std::string_view uri) const { + const auto key = DocumentUriKey(uri); + if (!key) { + return nullptr; + } + const auto document = documents_.find(*key); + return document == documents_.end() ? nullptr : &document->second; +} + +size_t DocumentStore::size() const { + return documents_.size(); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/document_synchronization_routes.cpp b/lsp/src/document_synchronization_routes.cpp new file mode 100644 index 0000000..da9140d --- /dev/null +++ b/lsp/src/document_synchronization_routes.cpp @@ -0,0 +1,63 @@ +#include "rls/lsp/route_modules.h" + +#include +#include + +#include + +#include "rls/lsp/document_synchronization_service.h" +#include "rls/lsp/json_rpc_router.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& value) { + if (!value.is_object()) { + throw InvalidParams("expected object parameters"); + } + return value; +} + +void requireApplied(DocumentSynchronizationResult result) { + if (result != DocumentSynchronizationResult::Applied) { + throw InvalidParams("document synchronization was rejected"); + } +} + +} // namespace + +void RegisterDocumentSynchronizationRoutes( + JsonRpcRouter& router, DocumentSynchronizationService& synchronization) { + router.registerNotification("textDocument/didOpen", [&synchronization](const Json& params) { + const auto& document = requireObject(requireObject(params).at("textDocument")); + requireApplied(synchronization.open( + document.at("uri").get(), + document.at("languageId").get(), + document.at("version").get(), + document.at("text").get())); + }); + router.registerNotification("textDocument/didChange", [&synchronization](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + const auto& changes = object.at("contentChanges"); + if (!changes.is_array() || changes.size() != 1) { + throw InvalidParams("full synchronization requires one content change"); + } + const auto& change = requireObject(changes.front()); + if (change.contains("range")) { + throw InvalidParams("ranged changes are not supported"); + } + requireApplied(synchronization.change( + document.at("uri").get(), + document.at("version").get(), + change.at("text").get())); + }); + router.registerNotification("textDocument/didClose", [&synchronization](const Json& params) { + const auto& document = requireObject(requireObject(params).at("textDocument")); + requireApplied(synchronization.close(document.at("uri").get())); + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/document_synchronization_service.cpp b/lsp/src/document_synchronization_service.cpp new file mode 100644 index 0000000..034a419 --- /dev/null +++ b/lsp/src/document_synchronization_service.cpp @@ -0,0 +1,104 @@ +#include "rls/lsp/document_synchronization_service.h" + +#include + +#include "rls/lsp/project_analysis.h" + +namespace rls::lsp { +namespace { + +DocumentSynchronizationResult translate(DocumentUpdateResult result) { + switch (result) { + case DocumentUpdateResult::Applied: + return DocumentSynchronizationResult::Applied; + case DocumentUpdateResult::InvalidUri: + return DocumentSynchronizationResult::InvalidUri; + case DocumentUpdateResult::NotOpen: + return DocumentSynchronizationResult::NotOpen; + case DocumentUpdateResult::StaleVersion: + return DocumentSynchronizationResult::StaleVersion; + } + return DocumentSynchronizationResult::ProjectResolutionFailed; +} + +} // namespace + +DocumentSynchronizationService::DocumentSynchronizationService( + LifecycleService& lifecycle, DocumentStore& documents, ProjectManager& projects, + AnalysisScheduler& scheduler, DiagnosticPublisher& diagnostics) + : lifecycle_(lifecycle), documents_(documents), projects_(projects), scheduler_(scheduler), + diagnostics_(diagnostics) {} + +DocumentSynchronizationResult DocumentSynchronizationService::open( + std::string uri, std::string languageId, int64_t version, std::string text) { + if (!lifecycle_.acceptsDocumentUpdates()) { + return DocumentSynchronizationResult::NotReady; + } + + const auto update = documents_.open(uri, std::move(languageId), version, std::move(text)); + if (update != DocumentUpdateResult::Applied) { + return translate(update); + } + + const auto assignment = projects_.documentOpened(uri); + if (assignment != ProjectAssignmentResult::Assigned) { + documents_.close(uri); + return assignment == ProjectAssignmentResult::InvalidUri + ? DocumentSynchronizationResult::InvalidUri + : DocumentSynchronizationResult::ProjectResolutionFailed; + } + diagnostics_.publishConfigurationDiagnostics(projects_.configurationDiagnostics()); + diagnostics_.documentOpened(uri); + return schedule(uri) ? DocumentSynchronizationResult::Applied + : DocumentSynchronizationResult::ProjectResolutionFailed; +} + +DocumentSynchronizationResult DocumentSynchronizationService::change( + std::string_view uri, int64_t version, std::string text) { + if (!lifecycle_.acceptsDocumentUpdates()) { + return DocumentSynchronizationResult::NotReady; + } + if (!projects_.projectForDocument(uri)) { + return DocumentSynchronizationResult::NotOpen; + } + + const auto update = documents_.applyFullChange(uri, version, std::move(text)); + if (update != DocumentUpdateResult::Applied) { + return translate(update); + } + if (projects_.documentChanged(uri) != ProjectAssignmentResult::Assigned) { + return DocumentSynchronizationResult::ProjectResolutionFailed; + } + return schedule(uri) ? DocumentSynchronizationResult::Applied + : DocumentSynchronizationResult::ProjectResolutionFailed; +} + +DocumentSynchronizationResult DocumentSynchronizationService::close(std::string_view uri) { + if (!lifecycle_.acceptsDocumentUpdates()) { + return DocumentSynchronizationResult::NotReady; + } + const ManagedProject* project = projects_.projectForDocument(uri); + if (!project || !documents_.close(uri)) { + return DocumentSynchronizationResult::NotOpen; + } + const bool standalone = project->isStandalone; + if (projects_.documentClosed(uri) != ProjectAssignmentResult::Assigned) { + return DocumentSynchronizationResult::ProjectResolutionFailed; + } + diagnostics_.documentClosed(uri, standalone); + if (!projects_.projectForDocument(uri)) { + return DocumentSynchronizationResult::Applied; + } + return schedule(uri) ? DocumentSynchronizationResult::Applied + : DocumentSynchronizationResult::ProjectResolutionFailed; +} + +bool DocumentSynchronizationService::schedule(std::string_view uri) { + const ManagedProject* project = projects_.projectForDocument(uri); + if (!project) { + return false; + } + return ScheduleProjectAnalysis(projects_, scheduler_, project->id); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/document_uri.cpp b/lsp/src/document_uri.cpp new file mode 100644 index 0000000..855267b --- /dev/null +++ b/lsp/src/document_uri.cpp @@ -0,0 +1,279 @@ +#include "rls/lsp/document_uri.h" + +#include +#include + +namespace rls::lsp { +namespace { + +bool isSchemeCharacter(char character, bool first) { + const auto value = static_cast(character); + return std::isalpha(value) || (!first && (std::isdigit(value) + || character == '+' || character == '-' || character == '.')); +} + +int hexValue(char character) { + if (character >= '0' && character <= '9') return character - '0'; + if (character >= 'a' && character <= 'f') return character - 'a' + 10; + if (character >= 'A' && character <= 'F') return character - 'A' + 10; + return -1; +} + +bool isUnreserved(unsigned char character) { + return std::isalnum(character) || character == '-' || character == '.' + || character == '_' || character == '~'; +} + +bool equalsIgnoringAsciiCase(std::string_view left, std::string_view right) { + return left.size() == right.size() + && std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) { + return std::tolower(static_cast(a)) + == std::tolower(static_cast(b)); + }); +} + +char upperHex(int value) { + return static_cast(value < 10 ? '0' + value : 'A' + value - 10); +} + +bool isValidUtf8(std::string_view value) { + for (size_t index = 0; index < value.size();) { + const auto first = static_cast(value[index]); + if (first <= 0x7f) { + ++index; + continue; + } + + size_t continuationCount = 0; + unsigned char secondMinimum = 0x80; + unsigned char secondMaximum = 0xbf; + if (first >= 0xc2 && first <= 0xdf) { + continuationCount = 1; + } else if (first >= 0xe0 && first <= 0xef) { + continuationCount = 2; + if (first == 0xe0) secondMinimum = 0xa0; + if (first == 0xed) secondMaximum = 0x9f; + } else if (first >= 0xf0 && first <= 0xf4) { + continuationCount = 3; + if (first == 0xf0) secondMinimum = 0x90; + if (first == 0xf4) secondMaximum = 0x8f; + } else { + return false; + } + + if (index + continuationCount >= value.size()) { + return false; + } + const auto second = static_cast(value[index + 1]); + if (second < secondMinimum || second > secondMaximum) { + return false; + } + for (size_t offset = 2; offset <= continuationCount; ++offset) { + const auto continuation = static_cast(value[index + offset]); + if (continuation < 0x80 || continuation > 0xbf) { + return false; + } + } + index += continuationCount + 1; + } + return true; +} + +std::optional canonicalPath(const std::filesystem::path& path) { + std::error_code error; + const auto resolved = std::filesystem::weakly_canonical(path, error); + if (!error) { + return resolved; + } + + const auto absolute = std::filesystem::absolute(path, error); + if (error) { + return std::nullopt; + } + return absolute.lexically_normal(); +} + +} // namespace + +std::optional NormalizeDocumentUri(std::string_view uri) { + const size_t schemeEnd = uri.find(':'); + if (schemeEnd == std::string_view::npos || schemeEnd == 0) { + return std::nullopt; + } + for (size_t index = 0; index < schemeEnd; ++index) { + if (!isSchemeCharacter(uri[index], index == 0)) { + return std::nullopt; + } + } + + std::string normalized; + normalized.reserve(uri.size()); + for (size_t index = 0; index < schemeEnd; ++index) { + normalized.push_back(static_cast( + std::tolower(static_cast(uri[index])))); + } + normalized.push_back(':'); + + for (size_t index = schemeEnd + 1; index < uri.size(); ++index) { + const unsigned char character = static_cast(uri[index]); + if (character <= 0x20 || character == 0x7f || character == '\\') { + return std::nullopt; + } + if (character != '%') { + normalized.push_back(static_cast(character)); + continue; + } + if (index + 2 >= uri.size()) { + return std::nullopt; + } + const int high = hexValue(uri[index + 1]); + const int low = hexValue(uri[index + 2]); + if (high < 0 || low < 0) { + return std::nullopt; + } + const auto decoded = static_cast((high << 4) | low); + if (isUnreserved(decoded)) { + normalized.push_back(static_cast(decoded)); + } else { + normalized.push_back('%'); + normalized.push_back(upperHex(high)); + normalized.push_back(upperHex(low)); + } + index += 2; + } + + if (normalized.starts_with("file:")) { + if (!normalized.starts_with("file://")) { + return std::nullopt; + } + constexpr size_t AuthorityStart = 7; + const size_t pathStart = normalized.find('/', AuthorityStart); + if (pathStart == std::string::npos) { + return std::nullopt; + } + + const std::string_view authority( + normalized.data() + AuthorityStart, pathStart - AuthorityStart); + if (equalsIgnoringAsciiCase(authority, "localhost")) { + normalized.erase(AuthorityStart, authority.size()); + } else { + std::transform( + normalized.begin() + static_cast(AuthorityStart), + normalized.begin() + static_cast(pathStart), + normalized.begin() + static_cast(AuthorityStart), + [](char character) { + return static_cast(std::tolower(static_cast(character))); + }); + } + } + return normalized; +} + +std::optional DocumentUriKey(std::string_view uri) { + auto normalized = NormalizeDocumentUri(uri); + if (!normalized) { + return std::nullopt; + } +#ifdef _WIN32 + if (normalized->starts_with("file:")) { + for (size_t index = 0; index < normalized->size(); ++index) { + if ((*normalized)[index] == '%' && index + 2 < normalized->size()) { + index += 2; + continue; + } + (*normalized)[index] = static_cast( + std::tolower(static_cast((*normalized)[index]))); + } + } +#endif + return normalized; +} + +std::optional FileUriToPath(std::string_view uri) { + const auto normalized = NormalizeDocumentUri(uri); + if (!normalized || !normalized->starts_with("file://")) { + return std::nullopt; + } + + constexpr size_t AuthorityStart = 7; + const size_t pathStart = normalized->find('/', AuthorityStart); + if (pathStart == std::string::npos) { + return std::nullopt; + } + + const std::string authority = normalized->substr( + AuthorityStart, pathStart - AuthorityStart); + std::string path; + path.reserve(normalized->size() - pathStart); + for (size_t index = pathStart; index < normalized->size(); ++index) { + if ((*normalized)[index] != '%') { + path.push_back((*normalized)[index]); + continue; + } + + const int high = hexValue((*normalized)[index + 1]); + const int low = hexValue((*normalized)[index + 2]); + const char decoded = static_cast((high << 4) | low); + if (decoded == '\0') { + return std::nullopt; + } + path.push_back(decoded); + index += 2; + } + + if (!authority.empty()) { + path = "//" + authority + path; + } +#ifdef _WIN32 + if (authority.empty() && path.size() >= 3 && path[0] == '/' + && std::isalpha(static_cast(path[1])) && path[2] == ':') { + path.erase(0, 1); + } +#endif + if (!isValidUtf8(path)) { + return std::nullopt; + } + std::u8string utf8Path; + utf8Path.reserve(path.size()); + for (const unsigned char byte : path) { + utf8Path.push_back(static_cast(byte)); + } + const std::filesystem::path filesystemPath(utf8Path); + return authority.empty() + ? canonicalPath(filesystemPath) + : std::optional(filesystemPath); +} + +std::optional PathToFileUri(const std::filesystem::path& path) { + const auto canonical = canonicalPath(path); + if (!canonical) { + return std::nullopt; + } + + const auto generic = canonical->generic_u8string(); + const std::string_view genericBytes( + reinterpret_cast(generic.data()), generic.size()); + if (!isValidUtf8(genericBytes)) { + return std::nullopt; + } + std::string uri = "file://"; +#ifdef _WIN32 + if (generic.size() >= 2 && generic[1] == u8':') { + uri.push_back('/'); + } +#endif + constexpr std::string_view Hex = "0123456789ABCDEF"; + for (const char8_t byteValue : generic) { + const auto byte = static_cast(byteValue); + if (isUnreserved(byte) || byte == '/' || byte == ':') { + uri.push_back(static_cast(byte)); + } else { + uri.push_back('%'); + uri.push_back(Hex[byte >> 4]); + uri.push_back(Hex[byte & 0x0f]); + } + } + return NormalizeDocumentUri(uri); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/hover_service.cpp b/lsp/src/hover_service.cpp new file mode 100644 index 0000000..a7b359a --- /dev/null +++ b/lsp/src/hover_service.cpp @@ -0,0 +1,296 @@ +#include "rls/lsp/hover_service.h" + +#include +#include +#include +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +struct CurrentDocument { + AnalysisScheduler::Snapshot snapshot; + std::string path; + const ast::SourceText* source = nullptr; + const parser::SourceIndex* sourceIndex = nullptr; +}; + +std::string pathString(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string result; + result.reserve(generic.size()); + for (const char8_t byte : generic) result.push_back(static_cast(byte)); + return result; +} + +std::optional currentDocument( + const ProjectManager& projects, AnalysisScheduler& scheduler, + std::string_view uri) { + const auto* project = projects.projectForDocument(uri); + const auto identity = projects.sourceIdentityForDocument(uri); + if (!project || !identity) return std::nullopt; + const std::string projectId = project->id; + const uint64_t generation = project->generation; + auto snapshot = scheduler.acceptedSnapshot(projectId); + if (!snapshot || snapshot->generation() != generation) { + snapshot = scheduler.awaitSnapshot(projectId, generation); + } + if (!snapshot || snapshot->generation() != generation) return std::nullopt; + const std::string& documentPath = *identity; + const auto* source = snapshot->sourceText(documentPath); + const auto* sourceIndex = snapshot->sourceIndex(documentPath); + if (!source || !sourceIndex) return std::nullopt; + return CurrentDocument{snapshot, documentPath, source, sourceIndex}; +} + +PresentationType presentationType( + ast::Type type, const std::optional& enumName) { + std::string name; + switch (type) { + case ast::Type::Bool: name = "Bool"; break; + case ast::Type::Int: name = "Int"; break; + case ast::Type::String: name = "String"; break; + case ast::Type::List: name = "List"; break; + case ast::Type::Callable: name = "Callable"; break; + case ast::Type::Condition: name = "Condition"; break; + case ast::Type::Enum: name = "Enum"; break; + case ast::Type::Region: name = "Region"; break; + case ast::Type::Event: name = "Event"; break; + case ast::Type::Location: name = "Location"; break; + case ast::Type::Void: name = "Void"; break; + case ast::Type::Error: name = ""; break; + } + return {std::move(name), enumName}; +} + +PresentationProvenance presentationProvenance(sema::SymbolProvenance provenance) { + switch (provenance) { + case sema::SymbolProvenance::Source: return PresentationProvenance::Source; + case sema::SymbolProvenance::Extern: return PresentationProvenance::Extern; + case sema::SymbolProvenance::Pattern: return PresentationProvenance::Pattern; + } + return PresentationProvenance::Source; +} + +std::optional presentationRange( + const ast::SourceText& source, const ast::Span& span) { + const auto startOffset = source.byteOffsetFromUtf8Position(span.start); + const auto endOffset = source.byteOffsetFromUtf8Position(span.end); + if (!startOffset || !endOffset) return std::nullopt; + const auto start = source.utf16PositionAtByteOffset(*startOffset); + const auto end = source.utf16PositionAtByteOffset(*endOffset); + if (!start || !end) return std::nullopt; + return PresentationRange{ + {start->line - 1, start->column - 1}, + {end->line - 1, end->column - 1}, + }; +} + +bool containsInclusive(const ast::Span& span, ast::Position position) { + const auto beforeOrEqual = [](ast::Position left, ast::Position right) { + return left.line < right.line + || (left.line == right.line && left.column <= right.column); + }; + return span.start.line != 0 && beforeOrEqual(span.start, position) + && beforeOrEqual(position, span.end); +} + +std::string categoryDescription( + const sema::AnalysisSnapshot& snapshot, const sema::SymbolRecord& record) { + switch (record.category) { + case sema::SymbolCategory::Region: return "Region value."; + case sema::SymbolCategory::RegionExtension: return "Region extension."; + case sema::SymbolCategory::Define: return "Function declaration."; + case sema::SymbolCategory::ExternDefine: return "External function declaration."; + case sema::SymbolCategory::Enum: return "Enumeration type."; + case sema::SymbolCategory::EnumMember: + return record.enumName + ? "Member of enum `" + *record.enumName + "`." + : "Enumeration member."; + case sema::SymbolCategory::ExternEnumPattern: + return record.enumName + ? "Concrete value matched by extern enum pattern `" + + record.displayName + "` in `" + *record.enumName + "`." + : "Concrete value matched by an external enum wildcard pattern."; + case sema::SymbolCategory::Parameter: + if (record.container) { + const auto container = snapshot.declaration(*record.container); + if (container) return "Parameter of `" + container->displayName + "`."; + } + return "Function parameter."; + case sema::SymbolCategory::RegionDataEntry: return "Region data property."; + case sema::SymbolCategory::SectionEntry: + if (record.type == ast::Type::Event) return "Declared event value."; + if (record.type == ast::Type::Location) return "Declared location value."; + return "Region section entry."; + } + return {}; +} + +PresentationSymbol presentationSymbol( + const sema::AnalysisSnapshot& snapshot, const sema::SymbolRecord& record) { + PresentationSymbol result{ + .name = record.displayName, + .provenance = presentationProvenance(record.provenance), + }; + if (record.type) result.type = presentationType(*record.type, record.enumName); + switch (record.category) { + case sema::SymbolCategory::Region: + case sema::SymbolCategory::RegionExtension: + result.kind = PresentationSymbolKind::Region; + break; + case sema::SymbolCategory::Define: + case sema::SymbolCategory::ExternDefine: { + result.kind = PresentationSymbolKind::Function; + PresentationCallable callable{.name = record.displayName}; + std::vector parameters; + for (const auto& candidate : snapshot.semanticIndex().symbols()) { + if (candidate.category == sema::SymbolCategory::Parameter + && candidate.container == record.id) { + parameters.push_back(&candidate); + } + } + std::sort(parameters.begin(), parameters.end(), [](const auto* left, const auto* right) { + return std::tie(left->selection.start.line, left->selection.start.column) + < std::tie(right->selection.start.line, right->selection.start.column); + }); + for (const auto* parameter : parameters) { + callable.parameters.push_back({ + .name = parameter->displayName, + .type = parameter->type + ? presentationType(*parameter->type, parameter->enumName) + : PresentationType{""}, + .defaultValue = parameter->defaultValue, + .optional = parameter->optional, + }); + } + if (record.type) callable.returnType = presentationType(*record.type, record.enumName); + result.callable = std::move(callable); + break; + } + case sema::SymbolCategory::Enum: + result.kind = PresentationSymbolKind::Enum; + break; + case sema::SymbolCategory::EnumMember: + case sema::SymbolCategory::ExternEnumPattern: + result.kind = PresentationSymbolKind::EnumMember; + break; + case sema::SymbolCategory::Parameter: + result.kind = PresentationSymbolKind::Parameter; + result.documentation.push_back({ + .heading = std::nullopt, + .markdown = categoryDescription(snapshot, record), + }); + break; + case sema::SymbolCategory::RegionDataEntry: + result.kind = PresentationSymbolKind::Property; + break; + case sema::SymbolCategory::SectionEntry: + result.kind = PresentationSymbolKind::Value; + break; + } + if (record.category != sema::SymbolCategory::Parameter) { + result.documentation.push_back({ + .heading = std::nullopt, + .markdown = categoryDescription(snapshot, record), + }); + } + + const auto uri = PathToFileUri(record.declaration.file); + const auto* source = snapshot.sourceText(record.declaration.file); + const auto range = source ? presentationRange(*source, record.selection) : std::nullopt; + if (uri && range) { + result.declaration = PresentationLocation{*uri, *range}; + const uint32_t line = range->start.line + 1; + const uint32_t character = range->start.character + 1; + result.documentation.push_back({ + .heading = "Declaration", + .markdown = "[Open declaration](" + *uri + "#L" + + std::to_string(line) + "," + std::to_string(character) + ")", + }); + } + return result; +} + +std::string hoverMarkdown(const RenderedPresentation& rendered) { + std::string result = "```rls\n" + rendered.detail + "\n```"; + if (!rendered.documentation.empty()) result += "\n\n" + rendered.documentation; + return result; +} + +} // namespace + +HoverService::HoverService( + const ProjectManager& projects, AnalysisScheduler& scheduler) + : projects_(projects), scheduler_(scheduler) {} + +std::optional HoverService::hover( + std::string_view uri, PresentationPosition position) const { + if (position.line == std::numeric_limits::max() + || position.character == std::numeric_limits::max()) { + return std::nullopt; + } + const auto document = currentDocument(projects_, scheduler_, uri); + if (!document) return std::nullopt; + const auto cursorOffset = document->source->byteOffsetFromUtf16Position({ + position.line + 1, position.character + 1}); + if (!cursorOffset) return std::nullopt; + const auto cursor = document->source->utf8PositionAtByteOffset(*cursorOffset); + if (!cursor) return std::nullopt; + + if (const auto occurrence = document->snapshot->occurrenceAt(document->path, *cursor)) { + if (!occurrence->symbol) return std::nullopt; + const auto declaration = document->snapshot->declaration(*occurrence->symbol); + const auto range = presentationRange(*document->source, occurrence->span); + if (!declaration || !range) return std::nullopt; + auto presentation = presentationSymbol(*document->snapshot, *declaration); + if (declaration->category == sema::SymbolCategory::ExternEnumPattern) { + const auto sourceName = document->sourceIndex->nameAt(*cursor); + if (sourceName && sourceName->span.file == occurrence->span.file + && sourceName->span.start.line == occurrence->span.start.line + && sourceName->span.start.column == occurrence->span.start.column + && sourceName->span.end.line == occurrence->span.end.line + && sourceName->span.end.column == occurrence->span.end.column) { + presentation.name = sourceName->text; + } + } + const auto rendered = PresentationRenderer{}.render(presentation); + return HoverResult{hoverMarkdown(rendered), *range}; + } + + if (const auto sourceCall = document->sourceIndex->enclosingCall(*cursor); + sourceCall && containsInclusive(sourceCall->callee, *cursor)) { + const auto call = document->snapshot->callAt(document->path, *cursor); + if (call && call->target) { + const auto declaration = document->snapshot->declaration(*call->target); + const auto range = presentationRange(*document->source, sourceCall->callee); + if (declaration && range) { + const auto rendered = PresentationRenderer{}.render( + presentationSymbol(*document->snapshot, *declaration)); + return HoverResult{hoverMarkdown(rendered), *range}; + } + } + } + + const auto type = document->snapshot->typeAt(document->path, *cursor); + if (!type || type->type == ast::Type::Error) return std::nullopt; + const auto range = presentationRange(*document->source, type->span); + if (!range) return std::nullopt; + PresentationSymbol expression{ + .kind = PresentationSymbolKind::Value, + .name = "expression", + .type = presentationType(type->type, type->enumName), + .documentation = {{ + .heading = std::nullopt, + .markdown = "Inferred expression type.", + }}, + }; + return HoverResult{ + hoverMarkdown(PresentationRenderer{}.render(expression)), *range}; +} + +} // namespace rls::lsp diff --git a/lsp/src/json_rpc_router.cpp b/lsp/src/json_rpc_router.cpp new file mode 100644 index 0000000..b3571ac --- /dev/null +++ b/lsp/src/json_rpc_router.cpp @@ -0,0 +1,147 @@ +#include "rls/lsp/json_rpc_router.h" + +#include +#include + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +Json errorResponse(const Json& id, int code, std::string_view message) { + return { + {"jsonrpc", "2.0"}, + {"id", id}, + {"error", {{"code", code}, {"message", message}}}, + }; +} + +bool isValidId(const Json& id) { + return id.is_null() || id.is_string() || id.is_number_integer() + || id.is_number_unsigned(); +} + +Json invalidRequestId(const Json& message) { + if (message.is_object() && message.contains("id") && isValidId(message["id"])) { + return message["id"]; + } + return nullptr; +} + +} // namespace + +void JsonRpcRouter::registerRequest(std::string method, RequestHandler handler) { + if (method.empty() || !handler) { + throw std::invalid_argument("request route requires a method and handler"); + } + if (contains(method)) { + throw std::logic_error("duplicate JSON-RPC route: " + method); + } + requests_.emplace(std::move(method), std::move(handler)); +} + +void JsonRpcRouter::registerNotification(std::string method, NotificationHandler handler) { + if (method.empty() || !handler) { + throw std::invalid_argument("notification route requires a method and handler"); + } + if (contains(method)) { + throw std::logic_error("duplicate JSON-RPC route: " + method); + } + notifications_.emplace(std::move(method), std::move(handler)); +} + +bool JsonRpcRouter::contains(std::string_view method) const { + return requests_.contains(std::string(method)) + || notifications_.contains(std::string(method)); +} + +void JsonRpcRouter::requireRoutes(std::initializer_list methods) const { + for (const std::string_view method : methods) { + if (!contains(method)) { + throw std::logic_error("missing JSON-RPC route: " + std::string(method)); + } + } +} + +std::vector JsonRpcRouter::handlePayload(std::string_view payload) const { + Json parsed; + try { + parsed = Json::parse(payload); + } catch (const Json::parse_error&) { + return {errorResponse(nullptr, -32700, "Parse error").dump()}; + } + + const auto dispatch = [this](const Json& message) -> std::optional { + if (!message.is_object() || message.value("jsonrpc", "") != "2.0" + || !message.contains("method") || !message["method"].is_string()) { + return errorResponse(invalidRequestId(message), -32600, "Invalid Request"); + } + + const bool isRequest = message.contains("id"); + if (isRequest && !isValidId(message["id"])) { + return errorResponse(nullptr, -32600, "Invalid Request"); + } + if (message.contains("params") + && !message["params"].is_object() && !message["params"].is_array()) { + if (isRequest) { + return errorResponse(message["id"], -32602, "Invalid params"); + } + return std::nullopt; + } + + const std::string method = message["method"].get(); + const Json params = message.value("params", Json(nullptr)); + + if (isRequest) { + const auto route = requests_.find(method); + if (route == requests_.end()) { + return errorResponse(message["id"], -32601, "Method not found"); + } + + try { + return Json{ + {"jsonrpc", "2.0"}, + {"id", message["id"]}, + {"result", route->second(params)}, + }; + } catch (const InvalidParams&) { + return errorResponse(message["id"], -32602, "Invalid params"); + } catch (const RequestFailed& error) { + return errorResponse(message["id"], -32803, error.what()); + } catch (const Json::exception&) { + return errorResponse(message["id"], -32602, "Invalid params"); + } catch (const std::exception&) { + return errorResponse(message["id"], -32603, "Internal error"); + } + } + + const auto route = notifications_.find(method); + if (route == notifications_.end()) { + return std::nullopt; + } + try { + route->second(params); + } catch (const std::exception&) { + } + return std::nullopt; + }; + + if (!parsed.is_array()) { + const auto response = dispatch(parsed); + return response ? std::vector{response->dump()} : std::vector{}; + } + if (parsed.empty()) { + return {errorResponse(nullptr, -32600, "Invalid Request").dump()}; + } + + Json responses = Json::array(); + for (const auto& message : parsed) { + if (const auto response = dispatch(message)) { + responses.push_back(*response); + } + } + return responses.empty() ? std::vector{} + : std::vector{responses.dump()}; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/lifecycle_routes.cpp b/lsp/src/lifecycle_routes.cpp new file mode 100644 index 0000000..7e9e814 --- /dev/null +++ b/lsp/src/lifecycle_routes.cpp @@ -0,0 +1,188 @@ +#include "rls/lsp/route_modules.h" + +#include + +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/semantic_tokens_service.h" +#include "rls/lsp/workspace_service.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& params) { + if (!params.is_object()) { + throw InvalidParams("expected object parameters"); + } + return params; +} + +void requireNull(const Json& params) { + if (!params.is_null()) { + throw InvalidParams("method does not accept parameters"); + } +} + +std::vector workspaceFolders(const Json& params) { + std::vector uris; + if (params.contains("workspaceFolders") && params["workspaceFolders"].is_array()) { + for (const auto& folder : params["workspaceFolders"]) { + if (!folder.is_object()) { + throw InvalidParams("workspace folder must be an object"); + } + uris.push_back(folder.at("uri").get()); + } + } else if (params.contains("rootUri") && params["rootUri"].is_string()) { + uris.push_back(params["rootUri"].get()); + } + return uris; +} + +bool definitionLinkSupport(const Json& params) { + if (!params.contains("capabilities")) { + return false; + } + const auto& capabilities = requireObject(params.at("capabilities")); + if (!capabilities.contains("textDocument")) { + return false; + } + const auto& textDocument = requireObject(capabilities.at("textDocument")); + if (!textDocument.contains("definition")) { + return false; + } + const auto& definition = requireObject(textDocument.at("definition")); + return definition.value("linkSupport", false); +} + +bool documentSymbolHierarchySupport(const Json& params) { + if (!params.contains("capabilities")) { + return false; + } + const auto& capabilities = requireObject(params.at("capabilities")); + if (!capabilities.contains("textDocument")) { + return false; + } + const auto& textDocument = requireObject(capabilities.at("textDocument")); + if (!textDocument.contains("documentSymbol")) { + return false; + } + const auto& documentSymbol = requireObject(textDocument.at("documentSymbol")); + return documentSymbol.value("hierarchicalDocumentSymbolSupport", false); +} + +bool completionSnippetSupport(const Json& params) { + if (!params.contains("capabilities")) return false; + const auto& capabilities = requireObject(params.at("capabilities")); + if (!capabilities.contains("textDocument")) return false; + const auto& textDocument = requireObject(capabilities.at("textDocument")); + if (!textDocument.contains("completion")) return false; + const auto& completion = requireObject(textDocument.at("completion")); + if (!completion.contains("completionItem")) return false; + const auto& completionItem = requireObject(completion.at("completionItem")); + return completionItem.value("snippetSupport", false); +} + +bool workspaceDocumentChangesSupport(const Json& params) { + if (!params.contains("capabilities")) return false; + const auto& capabilities = requireObject(params.at("capabilities")); + if (!capabilities.contains("workspace")) return false; + const auto& workspace = requireObject(capabilities.at("workspace")); + if (!workspace.contains("workspaceEdit")) return false; + const auto& workspaceEdit = requireObject(workspace.at("workspaceEdit")); + return workspaceEdit.value("documentChanges", false); +} + +SectionSnippetIndentation sectionSnippetIndentation(const Json& params) { + if (!params.contains("initializationOptions") + || !params.at("initializationOptions").is_object()) { + return SectionSnippetIndentation::Server; + } + const auto& options = params.at("initializationOptions"); + if (!options.contains("completion") || !options.at("completion").is_object()) { + return SectionSnippetIndentation::Server; + } + const auto& completion = options.at("completion"); + if (!completion.contains("sectionSnippetIndentation") + || !completion.at("sectionSnippetIndentation").is_string()) { + return SectionSnippetIndentation::Server; + } + return completion.at("sectionSnippetIndentation").get() == "client" + ? SectionSnippetIndentation::Client + : SectionSnippetIndentation::Server; +} + +} // namespace + +void RegisterLifecycleRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, WorkspaceService& workspace) { + router.registerRequest("initialize", [&lifecycle, &workspace](const Json& params) { + requireObject(params); + const bool hasWorkspaceRoot = + (params.contains("workspaceFolders") && !params["workspaceFolders"].is_null()) + || (params.contains("rootUri") && params["rootUri"].is_string()); + if (!workspace.initialize(workspaceFolders(params), hasWorkspaceRoot)) { + throw InvalidParams("invalid workspace folder URI"); + } + lifecycle.initialize( + definitionLinkSupport(params), documentSymbolHierarchySupport(params), + completionSnippetSupport(params), sectionSnippetIndentation(params), + workspaceDocumentChangesSupport(params)); + return Json{ + {"capabilities", { + {"textDocumentSync", { + {"openClose", true}, + {"change", 1}, + }}, + {"definitionProvider", true}, + {"referencesProvider", true}, + {"renameProvider", {{"prepareProvider", true}}}, + {"documentHighlightProvider", true}, + {"documentSymbolProvider", true}, + {"completionProvider", { + {"resolveProvider", false}, + }}, + {"signatureHelpProvider", { + {"triggerCharacters", {"(", ","}}, + {"retriggerCharacters", {","}}, + }}, + {"hoverProvider", true}, + {"semanticTokensProvider", { + {"legend", { + {"tokenTypes", SemanticTokensService::tokenTypes()}, + {"tokenModifiers", SemanticTokensService::tokenModifiers()}, + }}, + {"range", false}, + {"full", true}, + }}, + {"workspaceSymbolProvider", true}, + {"workspace", { + {"workspaceFolders", { + {"supported", true}, + {"changeNotifications", true}, + }}, + }}, + }}, + {"serverInfo", { + {"name", "RandoLogicScript"}, + {"version", "0.1.0"}, + }}, + }; + }); + router.registerNotification("initialized", [&lifecycle](const Json& params) { + requireObject(params); + lifecycle.initialized(); + }); + router.registerRequest("shutdown", [&lifecycle](const Json& params) { + requireNull(params); + lifecycle.shutdown(); + return Json(nullptr); + }); + router.registerNotification("exit", [&lifecycle](const Json& params) { + requireNull(params); + lifecycle.exit(); + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/lifecycle_service.cpp b/lsp/src/lifecycle_service.cpp new file mode 100644 index 0000000..c0bff79 --- /dev/null +++ b/lsp/src/lifecycle_service.cpp @@ -0,0 +1,72 @@ +#include "rls/lsp/lifecycle_service.h" + +#include + +namespace rls::lsp { + +void LifecycleService::initialize( + bool definitionLinkSupport, bool documentSymbolHierarchySupport, + bool completionSnippetSupport, SectionSnippetIndentation sectionSnippetIndentation, + bool workspaceDocumentChangesSupport) { + if (initializeRequested_) { + throw std::logic_error("initialize was already requested"); + } + initializeRequested_ = true; + definitionLinkSupport_ = definitionLinkSupport; + documentSymbolHierarchySupport_ = documentSymbolHierarchySupport; + completionSnippetSupport_ = completionSnippetSupport; + workspaceDocumentChangesSupport_ = workspaceDocumentChangesSupport; + sectionSnippetIndentation_ = sectionSnippetIndentation; +} + +void LifecycleService::initialized() { + if (!initializeRequested_ || initialized_ || shutdownRequested_) { + throw std::logic_error("initialized is not valid in the current state"); + } + initialized_ = true; +} + +void LifecycleService::shutdown() { + if (!initializeRequested_ || shutdownRequested_) { + throw std::logic_error("shutdown is not valid in the current state"); + } + shutdownRequested_ = true; +} + +void LifecycleService::exit() { + exitRequested_ = true; +} + +bool LifecycleService::acceptsDocumentUpdates() const { + return initialized_ && !shutdownRequested_; +} + +bool LifecycleService::supportsDefinitionLinks() const { + return definitionLinkSupport_; +} + +bool LifecycleService::supportsDocumentSymbolHierarchy() const { + return documentSymbolHierarchySupport_; +} + +bool LifecycleService::supportsCompletionSnippets() const { + return completionSnippetSupport_; +} + +bool LifecycleService::supportsWorkspaceDocumentChanges() const { + return workspaceDocumentChangesSupport_; +} + +SectionSnippetIndentation LifecycleService::sectionSnippetIndentation() const { + return sectionSnippetIndentation_; +} + +bool LifecycleService::shouldExit() const { + return exitRequested_; +} + +int LifecycleService::exitCode() const { + return shutdownRequested_ ? 0 : 1; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/message_framer.cpp b/lsp/src/message_framer.cpp new file mode 100644 index 0000000..8d15533 --- /dev/null +++ b/lsp/src/message_framer.cpp @@ -0,0 +1,124 @@ +#include "rls/lsp/message_framer.h" + +#include +#include +#include +#include +#include + +namespace rls::lsp { +namespace { + +constexpr std::string_view HeaderTerminator = "\r\n\r\n"; + +std::string_view trim(std::string_view value) { + const auto isWhitespace = [](char character) { + return character == ' ' || character == '\t'; + }; + + while (!value.empty() && isWhitespace(value.front())) { + value.remove_prefix(1); + } + while (!value.empty() && isWhitespace(value.back())) { + value.remove_suffix(1); + } + return value; +} + +bool equalsIgnoringAsciiCase(std::string_view left, std::string_view right) { + return left.size() == right.size() + && std::equal(left.begin(), left.end(), right.begin(), [](char a, char b) { + return std::tolower(static_cast(a)) + == std::tolower(static_cast(b)); + }); +} + +size_t parseContentLength(std::string_view value) { + value = trim(value); + if (value.empty()) { + throw std::runtime_error("empty Content-Length header"); + } + + size_t contentLength = 0; + const auto [end, error] = std::from_chars( + value.data(), value.data() + value.size(), contentLength); + if (error != std::errc{} || end != value.data() + value.size()) { + throw std::runtime_error("invalid Content-Length header"); + } + return contentLength; +} + +} // namespace + +MessageFramer::MessageFramer(size_t maximumPayloadSize, size_t maximumHeaderSize) + : maximumPayloadSize_(maximumPayloadSize), maximumHeaderSize_(maximumHeaderSize) { + if (maximumHeaderSize_ < HeaderTerminator.size()) { + throw std::invalid_argument("maximum header size is too small"); + } +} + +void MessageFramer::append(std::string_view bytes) { + buffer_.append(bytes); +} + +std::optional MessageFramer::popMessage() { + const size_t headerEnd = buffer_.find(HeaderTerminator); + if (headerEnd == std::string::npos) { + if (buffer_.size() > maximumHeaderSize_) { + throw std::runtime_error("JSON-RPC header exceeds configured limit"); + } + return std::nullopt; + } + if (headerEnd + HeaderTerminator.size() > maximumHeaderSize_) { + throw std::runtime_error("JSON-RPC header exceeds configured limit"); + } + + std::optional contentLength; + size_t lineStart = 0; + while (lineStart < headerEnd) { + const size_t lineEnd = buffer_.find("\r\n", lineStart); + if (lineEnd == std::string::npos || lineEnd > headerEnd) { + throw std::runtime_error("malformed JSON-RPC header"); + } + const std::string_view line(buffer_.data() + lineStart, lineEnd - lineStart); + const size_t separator = line.find(':'); + if (separator == std::string_view::npos) { + throw std::runtime_error("malformed JSON-RPC header"); + } + + if (equalsIgnoringAsciiCase(trim(line.substr(0, separator)), "Content-Length")) { + if (contentLength.has_value()) { + throw std::runtime_error("duplicate Content-Length header"); + } + contentLength = parseContentLength(line.substr(separator + 1)); + } + lineStart = lineEnd + 2; + } + + if (!contentLength.has_value()) { + throw std::runtime_error("missing Content-Length header"); + } + if (*contentLength > maximumPayloadSize_) { + throw std::runtime_error("JSON-RPC payload exceeds configured limit"); + } + + const size_t payloadStart = headerEnd + HeaderTerminator.size(); + if (*contentLength > std::numeric_limits::max() - payloadStart) { + throw std::runtime_error("JSON-RPC frame size overflow"); + } + const size_t frameSize = payloadStart + *contentLength; + if (buffer_.size() < frameSize) { + return std::nullopt; + } + + std::string payload = buffer_.substr(payloadStart, *contentLength); + buffer_.erase(0, frameSize); + return payload; +} + +std::string MessageFramer::frame(std::string_view payload) { + return "Content-Length: " + std::to_string(payload.size()) + "\r\n\r\n" + + std::string(payload); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/navigation_routes.cpp b/lsp/src/navigation_routes.cpp new file mode 100644 index 0000000..191d587 --- /dev/null +++ b/lsp/src/navigation_routes.cpp @@ -0,0 +1,203 @@ +#include "rls/lsp/route_modules.h" + +#include +#include + +#include + +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/navigation_service.h" +#include "rls/lsp/workspace_service.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& value) { + if (!value.is_object()) { + throw InvalidParams("expected object parameters"); + } + return value; +} + +uint32_t requirePositionComponent(const Json& value) { + uint64_t component = 0; + if (value.is_number_unsigned()) { + component = value.get(); + } else if (value.is_number_integer()) { + const int64_t signedComponent = value.get(); + if (signedComponent < 0) { + throw InvalidParams("position components must be non-negative"); + } + component = static_cast(signedComponent); + } else { + throw InvalidParams("position components must be integers"); + } + if (component > std::numeric_limits::max()) { + throw InvalidParams("position component is too large"); + } + return static_cast(component); +} + +Json position(const NavigationPosition& value) { + return {{"line", value.line}, {"character", value.character}}; +} + +Json range(const NavigationRange& value) { + return {{"start", position(value.start)}, {"end", position(value.end)}}; +} + +int symbolKind(NavigationSymbolKind kind) { + switch (kind) { + case NavigationSymbolKind::Namespace: return 3; + case NavigationSymbolKind::Function: return 12; + case NavigationSymbolKind::Enum: return 10; + case NavigationSymbolKind::EnumMember: return 22; + case NavigationSymbolKind::Variable: return 13; + case NavigationSymbolKind::Property: return 7; + case NavigationSymbolKind::Field: return 8; + } + return 13; +} + +Json documentSymbol(const NavigationDocumentSymbol& symbol) { + Json children = Json::array(); + for (const auto& child : symbol.children) { + children.push_back(documentSymbol(child)); + } + return { + {"name", symbol.name}, + {"kind", symbolKind(symbol.kind)}, + {"range", range(symbol.range)}, + {"selectionRange", range(symbol.selectionRange)}, + {"children", std::move(children)}, + }; +} + +void appendSymbolInformation( + Json& result, const NavigationDocumentSymbol& symbol, + std::string_view uri, std::optional containerName) { + Json information = { + {"name", symbol.name}, + {"kind", symbolKind(symbol.kind)}, + {"location", { + {"uri", uri}, + {"range", range(symbol.selectionRange)}, + }}, + }; + if (containerName) { + information["containerName"] = *containerName; + } + result.push_back(std::move(information)); + for (const auto& child : symbol.children) { + appendSymbolInformation(result, child, uri, symbol.name); + } +} + +NavigationPosition requestPosition(const Json& object) { + const auto& value = requireObject(object.at("position")); + return { + requirePositionComponent(value.at("line")), + requirePositionComponent(value.at("character")), + }; +} + +} // namespace + +void RegisterNavigationRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, NavigationService& navigation, + WorkspaceService& workspace) { + router.registerRequest("textDocument/definition", [&lifecycle, &navigation](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + const auto definition = navigation.definition( + document.at("uri").get(), + requestPosition(object)); + if (!definition) { + return Json(nullptr); + } + if (!lifecycle.supportsDefinitionLinks()) { + return Json::array({{ + {"uri", definition->targetUri}, + {"range", range(definition->targetSelectionRange)}, + }}); + } + return Json::array({{ + {"originSelectionRange", range(definition->originSelectionRange)}, + {"targetUri", definition->targetUri}, + {"targetRange", range(definition->targetRange)}, + {"targetSelectionRange", range(definition->targetSelectionRange)}, + }}); + }); + router.registerRequest("textDocument/references", [&navigation](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + const auto& context = requireObject(object.at("context")); + if (!context.at("includeDeclaration").is_boolean()) { + throw InvalidParams("includeDeclaration must be a boolean"); + } + Json result = Json::array(); + for (const auto& reference : navigation.references( + document.at("uri").get(), requestPosition(object), + context.at("includeDeclaration").get())) { + result.push_back({ + {"uri", reference.uri}, + {"range", range(reference.range)}, + }); + } + return result; + }); + router.registerRequest("textDocument/documentHighlight", [&navigation](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + Json result = Json::array(); + for (const auto& highlight : navigation.documentHighlights( + document.at("uri").get(), requestPosition(object))) { + result.push_back({ + {"range", range(highlight)}, + {"kind", 1}, + }); + } + return result; + }); + router.registerRequest("textDocument/documentSymbol", [&lifecycle, &navigation](const Json& params) { + const auto& document = requireObject(requireObject(params).at("textDocument")); + const std::string uri = document.at("uri").get(); + Json result = Json::array(); + for (const auto& symbol : navigation.documentSymbols(uri)) { + if (lifecycle.supportsDocumentSymbolHierarchy()) { + result.push_back(documentSymbol(symbol)); + } else { + appendSymbolInformation(result, symbol, uri, std::nullopt); + } + } + return result; + }); + router.registerRequest("workspace/symbol", [&navigation, &workspace](const Json& params) { + const auto& object = requireObject(params); + if (!object.at("query").is_string()) { + throw InvalidParams("workspace symbol query must be a string"); + } + Json result = Json::array(); + for (const auto& symbol : navigation.workspaceSymbols( + object.at("query").get(), workspace.projectIds())) { + Json information = { + {"name", symbol.name}, + {"kind", symbolKind(symbol.kind)}, + {"location", { + {"uri", symbol.location.uri}, + {"range", range(symbol.location.range)}, + }}, + }; + if (symbol.containerName) { + information["containerName"] = *symbol.containerName; + } + result.push_back(std::move(information)); + } + return result; + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/navigation_service.cpp b/lsp/src/navigation_service.cpp new file mode 100644 index 0000000..9a7474c --- /dev/null +++ b/lsp/src/navigation_service.cpp @@ -0,0 +1,412 @@ +#include "rls/lsp/navigation_service.h" + +#include +#include +#include +#include +#include +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +struct NavigationQuery { + AnalysisScheduler::Snapshot snapshot; + std::string documentPath; + std::string occurrenceText; + sema::SymbolId symbol; + sema::OccurrenceRecord occurrence; +}; + +struct CurrentDocument { + AnalysisScheduler::Snapshot snapshot; + std::string path; +}; + +std::string pathString(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string value; + value.reserve(generic.size()); + for (const char8_t byte : generic) { + value.push_back(static_cast(byte)); + } + return value; +} + +std::optional rangeFor( + const sema::AnalysisSnapshot& snapshot, const ast::Span& span) { + const ast::SourceText* source = snapshot.sourceText(span.file); + if (!source) { + return std::nullopt; + } + const auto startOffset = source->byteOffsetFromUtf8Position(span.start); + const auto endOffset = source->byteOffsetFromUtf8Position(span.end); + if (!startOffset || !endOffset) { + return std::nullopt; + } + const auto start = source->utf16PositionAtByteOffset(*startOffset); + const auto end = source->utf16PositionAtByteOffset(*endOffset); + if (!start || !end) { + return std::nullopt; + } + return NavigationRange{ + {start->line - 1, start->column - 1}, + {end->line - 1, end->column - 1}, + }; +} + +std::optional currentDocument( + const ProjectManager& projects, const AnalysisScheduler& scheduler, + std::string_view uri) { + const auto* project = projects.projectForDocument(uri); + const auto identity = projects.sourceIdentityForDocument(uri); + if (!project || !identity) { + return std::nullopt; + } + const auto snapshot = scheduler.acceptedSnapshot(project->id); + if (!snapshot || snapshot->generation() != project->generation) { + return std::nullopt; + } + const std::string& documentPath = *identity; + if (!snapshot->sourceText(documentPath) || !snapshot->sourceIndex(documentPath)) { + return std::nullopt; + } + return CurrentDocument{snapshot, documentPath}; +} + +bool sameSpan(const ast::Span& left, const ast::Span& right) { + return left.file == right.file + && left.start.line == right.start.line + && left.start.column == right.start.column + && left.end.line == right.end.line + && left.end.column == right.end.column; +} + +std::optional queryAt( + const ProjectManager& projects, const AnalysisScheduler& scheduler, + std::string_view uri, NavigationPosition position) { + if (position.line == std::numeric_limits::max() + || position.character == std::numeric_limits::max()) { + return std::nullopt; + } + const auto document = currentDocument(projects, scheduler, uri); + if (!document) { + return std::nullopt; + } + const ast::SourceText* source = document->snapshot->sourceText(document->path); + const auto offset = source->byteOffsetFromUtf16Position({ + position.line + 1, position.character + 1}); + if (!offset) { + return std::nullopt; + } + const auto sourcePosition = source->utf8PositionAtByteOffset(*offset); + if (!sourcePosition) { + return std::nullopt; + } + + const auto symbol = document->snapshot->symbolAt(document->path, *sourcePosition); + const auto occurrence = document->snapshot->occurrenceAt(document->path, *sourcePosition); + const auto sourceName = document->snapshot->sourceIndex(document->path) + ->nameAt(*sourcePosition); + if (!symbol || !occurrence || occurrence->symbol != symbol + || !sourceName || !sameSpan(sourceName->span, occurrence->span)) { + return std::nullopt; + } + return NavigationQuery{ + document->snapshot, document->path, sourceName->text, *symbol, *occurrence}; +} + +bool occurrenceHasText( + const sema::AnalysisSnapshot& snapshot, + const sema::OccurrenceRecord& occurrence, std::string_view text) { + const auto* source = snapshot.sourceText(occurrence.span.file); + if (!source) return false; + const auto start = source->byteOffsetFromUtf8Position(occurrence.span.start); + const auto end = source->byteOffsetFromUtf8Position(occurrence.span.end); + return start && end && *start <= *end + && source->content().substr(*start, *end - *start) == text; +} + +bool isTopLevel(sema::SymbolCategory category) { + return category == sema::SymbolCategory::Region + || category == sema::SymbolCategory::RegionExtension + || category == sema::SymbolCategory::Define + || category == sema::SymbolCategory::ExternDefine + || category == sema::SymbolCategory::Enum; +} + +std::optional symbolKind(sema::SymbolCategory category) { + switch (category) { + case sema::SymbolCategory::Region: + case sema::SymbolCategory::RegionExtension: + return NavigationSymbolKind::Namespace; + case sema::SymbolCategory::Define: + case sema::SymbolCategory::ExternDefine: + return NavigationSymbolKind::Function; + case sema::SymbolCategory::Enum: + return NavigationSymbolKind::Enum; + case sema::SymbolCategory::EnumMember: + case sema::SymbolCategory::ExternEnumPattern: + return NavigationSymbolKind::EnumMember; + case sema::SymbolCategory::Parameter: + return NavigationSymbolKind::Variable; + case sema::SymbolCategory::RegionDataEntry: + return NavigationSymbolKind::Property; + case sema::SymbolCategory::SectionEntry: + return NavigationSymbolKind::Field; + } + return std::nullopt; +} + +bool sourceOrder(const sema::SymbolRecord* left, const sema::SymbolRecord* right) { + return std::tie(left->selection.start.line, left->selection.start.column, + left->selection.end.line, left->selection.end.column) + < std::tie(right->selection.start.line, right->selection.start.column, + right->selection.end.line, right->selection.end.column); +} + +std::optional workspaceCategoryOrder(sema::SymbolCategory category) { + switch (category) { + case sema::SymbolCategory::Region: return 0; + case sema::SymbolCategory::RegionExtension: return 1; + case sema::SymbolCategory::Define: return 2; + case sema::SymbolCategory::ExternDefine: return 3; + case sema::SymbolCategory::Enum: return 4; + case sema::SymbolCategory::EnumMember: return 5; + case sema::SymbolCategory::ExternEnumPattern: return 6; + case sema::SymbolCategory::Parameter: + case sema::SymbolCategory::RegionDataEntry: + case sema::SymbolCategory::SectionEntry: + return std::nullopt; + } + return std::nullopt; +} + +std::string asciiLower(std::string_view value) { + std::string result; + result.reserve(value.size()); + for (const char character : value) { + result.push_back(static_cast( + std::tolower(static_cast(character)))); + } + return result; +} + +} // namespace + +NavigationService::NavigationService( + const ProjectManager& projects, const AnalysisScheduler& scheduler) + : projects_(projects), scheduler_(scheduler) {} + +std::optional NavigationService::definition( + std::string_view uri, NavigationPosition position) const { + const auto query = queryAt(projects_, scheduler_, uri, position); + if (!query) { + return std::nullopt; + } + const auto declaration = query->snapshot->declaration(query->symbol); + if (!declaration) { + return std::nullopt; + } + + const auto originRange = rangeFor(*query->snapshot, query->occurrence.span); + const auto targetRange = rangeFor(*query->snapshot, declaration->declaration); + const auto targetSelectionRange = rangeFor(*query->snapshot, declaration->selection); + const auto targetUri = PathToFileUri(declaration->declaration.file); + if (!originRange || !targetRange || !targetSelectionRange || !targetUri) { + return std::nullopt; + } + return DefinitionResult{ + *originRange, + *targetUri, + *targetRange, + *targetSelectionRange, + }; +} + +std::vector NavigationService::references( + std::string_view uri, NavigationPosition position, bool includeDeclaration) const { + const auto query = queryAt(projects_, scheduler_, uri, position); + if (!query) { + return {}; + } + + const auto declaration = query->snapshot->declaration(query->symbol); + const bool concreteWildcardValue = declaration + && declaration->category == sema::SymbolCategory::ExternEnumPattern; + std::vector result; + for (const auto& occurrence : query->snapshot->references(query->symbol)) { + if (!includeDeclaration && occurrence.kind == sema::OccurrenceKind::Declaration) { + continue; + } + if (concreteWildcardValue + && !occurrenceHasText(*query->snapshot, occurrence, query->occurrenceText)) { + continue; + } + const auto occurrenceUri = PathToFileUri(occurrence.span.file); + const auto occurrenceRange = rangeFor(*query->snapshot, occurrence.span); + if (occurrenceUri && occurrenceRange) { + result.push_back({*occurrenceUri, *occurrenceRange}); + } + } + return result; +} + +std::vector NavigationService::documentHighlights( + std::string_view uri, NavigationPosition position) const { + const auto query = queryAt(projects_, scheduler_, uri, position); + if (!query) { + return {}; + } + + const auto declaration = query->snapshot->declaration(query->symbol); + const bool concreteWildcardValue = declaration + && declaration->category == sema::SymbolCategory::ExternEnumPattern; + std::vector result; + for (const auto& occurrence : query->snapshot->references(query->symbol)) { + if (occurrence.span.file != query->documentPath) { + continue; + } + if (concreteWildcardValue + && !occurrenceHasText(*query->snapshot, occurrence, query->occurrenceText)) { + continue; + } + if (const auto occurrenceRange = rangeFor(*query->snapshot, occurrence.span)) { + result.push_back(*occurrenceRange); + } + } + return result; +} + +std::vector NavigationService::documentSymbols( + std::string_view uri) const { + const auto document = currentDocument(projects_, scheduler_, uri); + if (!document) { + return {}; + } + const auto* sourceIndex = document->snapshot->sourceIndex(document->path); + const auto declarations = sourceIndex->declarationsIn(document->path); + const auto& records = document->snapshot->semanticIndex().symbols(); + + std::function(const sema::SymbolRecord&)> build; + build = [&](const sema::SymbolRecord& record) + -> std::optional { + const auto kind = symbolKind(record.category); + const auto symbolRange = rangeFor(*document->snapshot, record.declaration); + const auto selectionRange = rangeFor(*document->snapshot, record.selection); + if (!kind || !symbolRange || !selectionRange) { + return std::nullopt; + } + + std::vector childRecords; + for (const auto& candidate : records) { + if (candidate.declaration.file == document->path + && candidate.container == record.id + && !isTopLevel(candidate.category)) { + childRecords.push_back(&candidate); + } + } + std::sort(childRecords.begin(), childRecords.end(), sourceOrder); + + NavigationDocumentSymbol result{ + record.displayName, *kind, *symbolRange, *selectionRange, {}}; + for (const auto* child : childRecords) { + if (auto symbol = build(*child)) { + result.children.push_back(std::move(*symbol)); + } + } + return result; + }; + + std::vector topLevelRecords; + for (const auto& record : records) { + if (record.declaration.file != document->path || !isTopLevel(record.category)) { + continue; + } + const bool parserDeclaration = std::any_of( + declarations.begin(), declarations.end(), [&](const auto& declaration) { + return sameSpan(declaration.span, record.declaration); + }); + if (parserDeclaration) { + topLevelRecords.push_back(&record); + } + } + std::sort(topLevelRecords.begin(), topLevelRecords.end(), sourceOrder); + + std::vector result; + for (const auto* record : topLevelRecords) { + if (auto symbol = build(*record)) { + result.push_back(std::move(*symbol)); + } + } + return result; +} + +std::vector NavigationService::workspaceSymbols( + std::string_view query, const std::vector& projectIds) const { + struct Candidate { + NavigationWorkspaceSymbol symbol; + size_t categoryOrder; + std::string foldedName; + }; + + const std::string foldedQuery = asciiLower(query); + std::vector candidates; + for (const auto& projectId : projectIds) { + const auto* project = projects_.project(projectId); + const auto snapshot = scheduler_.acceptedSnapshot(projectId); + if (!project || !snapshot || snapshot->generation() != project->generation) { + continue; + } + for (const auto& record : snapshot->semanticIndex().symbols()) { + const auto categoryOrder = workspaceCategoryOrder(record.category); + const auto kind = symbolKind(record.category); + const std::string foldedName = asciiLower(record.displayName); + if (!categoryOrder || !kind + || foldedName.find(foldedQuery) == std::string::npos) { + continue; + } + const auto uri = PathToFileUri(record.selection.file); + const auto symbolRange = rangeFor(*snapshot, record.selection); + if (!uri || !symbolRange) { + continue; + } + std::optional containerName; + if (record.container) { + const auto container = snapshot->declaration(*record.container); + if (container) containerName = container->displayName; + } + candidates.push_back({ + { + record.displayName, + *kind, + {*uri, *symbolRange}, + std::move(containerName), + }, + *categoryOrder, + foldedName, + }); + } + } + + std::sort(candidates.begin(), candidates.end(), [](const auto& left, const auto& right) { + return std::tie(left.categoryOrder, left.foldedName, left.symbol.name, + left.symbol.location.uri, left.symbol.location.range.start.line, + left.symbol.location.range.start.character) + < std::tie(right.categoryOrder, right.foldedName, right.symbol.name, + right.symbol.location.uri, right.symbol.location.range.start.line, + right.symbol.location.range.start.character); + }); + std::vector result; + result.reserve(candidates.size()); + for (auto& candidate : candidates) { + result.push_back(std::move(candidate.symbol)); + } + return result; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/outbound_message_queue.cpp b/lsp/src/outbound_message_queue.cpp new file mode 100644 index 0000000..acfe43e --- /dev/null +++ b/lsp/src/outbound_message_queue.cpp @@ -0,0 +1,48 @@ +#include "rls/lsp/outbound_message_queue.h" + +#include + +namespace rls::lsp { + +bool OutboundMessageQueue::push(std::string payload) { + { + std::lock_guard lock(mutex_); + if (closed_) { + return false; + } + messages_.push_back(std::move(payload)); + } + ready_.notify_one(); + return true; +} + +std::optional OutboundMessageQueue::tryPop() { + std::lock_guard lock(mutex_); + if (messages_.empty()) { + return std::nullopt; + } + std::string payload = std::move(messages_.front()); + messages_.pop_front(); + return payload; +} + +std::optional OutboundMessageQueue::waitPop() { + std::unique_lock lock(mutex_); + ready_.wait(lock, [this] { return closed_ || !messages_.empty(); }); + if (messages_.empty()) { + return std::nullopt; + } + std::string payload = std::move(messages_.front()); + messages_.pop_front(); + return payload; +} + +void OutboundMessageQueue::close() { + { + std::lock_guard lock(mutex_); + closed_ = true; + } + ready_.notify_all(); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/presentation.cpp b/lsp/src/presentation.cpp new file mode 100644 index 0000000..7dd9d9b --- /dev/null +++ b/lsp/src/presentation.cpp @@ -0,0 +1,116 @@ +#include "rls/lsp/presentation.h" + +#include + +namespace rls::lsp { +namespace { + +std::string_view provenancePrefix(PresentationProvenance provenance) { + switch (provenance) { + case PresentationProvenance::Source: + return {}; + case PresentationProvenance::Extern: + return "extern "; + case PresentationProvenance::BuiltIn: + return "built-in "; + case PresentationProvenance::Pattern: + return "extern pattern "; + } + return {}; +} + +std::string_view provenanceNote(const PresentationSymbol& symbol) { + switch (symbol.provenance) { + case PresentationProvenance::Source: + return {}; + case PresentationProvenance::Extern: + return "*External declaration.*"; + case PresentationProvenance::BuiltIn: + return "*Built-in symbol.*"; + case PresentationProvenance::Pattern: + return symbol.declaration + ? "*External wildcard pattern declaration.*" + : "*External pattern; no source declaration.*"; + } + return {}; +} + +std::string_view symbolKeyword(PresentationSymbolKind kind) { + switch (kind) { + case PresentationSymbolKind::Region: + return "region "; + case PresentationSymbolKind::Enum: + return "enum "; + default: + return {}; + } +} + +void appendMarkdownBlock(std::string& output, std::string_view block) { + if (block.empty()) return; + if (!output.empty()) output += "\n\n"; + output += block; +} + +} // namespace + +std::string PresentationRenderer::renderType(const PresentationType& type) { + return type.enumIdentity.value_or(type.name); +} + +std::string PresentationRenderer::renderParameter( + const PresentationParameter& parameter) { + std::string result = parameter.name + ": " + renderType(parameter.type); + if (parameter.defaultValue) { + result += " = "; + result += *parameter.defaultValue; + } else if (parameter.optional) { + result += " (optional)"; + } + return result; +} + +std::string PresentationRenderer::renderCallable(const PresentationCallable& callable) { + std::string result = callable.name + "("; + for (size_t index = 0; index < callable.parameters.size(); ++index) { + if (index != 0) result += ", "; + result += renderParameter(callable.parameters[index]); + } + result += ')'; + if (callable.returnType) { + result += " -> "; + result += renderType(*callable.returnType); + } + return result; +} + +RenderedPresentation PresentationRenderer::render(const PresentationSymbol& symbol) const { + RenderedPresentation result; + result.detail = provenancePrefix(symbol.provenance); + if (symbol.callable) { + result.detail += renderCallable(*symbol.callable); + } else { + result.detail += symbolKeyword(symbol.kind); + result.detail += symbol.name; + if (symbol.type + && symbol.kind != PresentationSymbolKind::Enum + && symbol.kind != PresentationSymbolKind::Region) { + result.detail += ": "; + result.detail += renderType(*symbol.type); + } + } + + for (const auto& block : symbol.documentation) { + std::string renderedBlock; + if (block.heading) { + renderedBlock = "**" + *block.heading + "**"; + if (!block.markdown.empty()) renderedBlock += "\n\n"; + } + renderedBlock += block.markdown; + appendMarkdownBlock(result.documentation, renderedBlock); + } + appendMarkdownBlock(result.documentation, provenanceNote(symbol)); + return result; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/project_analysis.cpp b/lsp/src/project_analysis.cpp new file mode 100644 index 0000000..5c791c7 --- /dev/null +++ b/lsp/src/project_analysis.cpp @@ -0,0 +1,34 @@ +#include "rls/lsp/project_analysis.h" + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { + +bool ScheduleProjectAnalysis( + ProjectManager& projects, AnalysisScheduler& scheduler, std::string_view projectId) { + ProjectSourceSet sourceSet = projects.sourceSetForProject(projectId); + if (!sourceSet.error.empty()) { + return false; + } + + std::vector sources; + sources.reserve(sourceSet.sources.size()); + for (auto& source : sourceSet.sources) { + sources.push_back({ + std::move(source.identity), + std::move(source.content), + std::move(source.diskPath), + }); + } + + return scheduler.schedule({ + std::string(projectId), + sourceSet.generation, + std::move(sources), + sourceSet.documentGeneration, + sourceSet.manifestGeneration, + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/project_manager.cpp b/lsp/src/project_manager.cpp new file mode 100644 index 0000000..3f971bb --- /dev/null +++ b/lsp/src/project_manager.cpp @@ -0,0 +1,352 @@ +#include "rls/lsp/project_manager.h" + +#include +#include +#include +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +std::filesystem::path canonicalPath(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + return error ? path.lexically_normal() : canonical; +} + +std::string pathKey(const std::filesystem::path& path) { + const auto generic = canonicalPath(path).generic_u8string(); + std::string key; + key.reserve(generic.size()); + for (const char8_t byte : generic) { + key.push_back(static_cast(byte)); + } +#ifdef _WIN32 + std::transform(key.begin(), key.end(), key.begin(), [](char character) { + return static_cast(std::tolower(static_cast(character))); + }); +#endif + return key; +} + +std::string pathIdentity(const std::filesystem::path& path) { + const auto generic = canonicalPath(path).generic_u8string(); + std::string identity; + identity.reserve(generic.size()); + for (const char8_t byte : generic) { + identity.push_back(static_cast(byte)); + } + return identity; +} + +bool isWithin(const std::filesystem::path& path, const std::filesystem::path& root) { + const std::string candidate = pathKey(path); + std::string prefix = pathKey(root); + if (!prefix.ends_with('/')) prefix.push_back('/'); + return candidate == pathKey(root) || candidate.starts_with(prefix); +} + +} // namespace + +ProjectManager::ProjectManager(DocumentStore& documents, Resolver resolver) + : documents_(documents), resolver_(std::move(resolver)) {} + +ProjectAssignmentResult ProjectManager::documentOpened(std::string_view uri) { + const auto key = DocumentUriKey(uri); + const auto path = FileUriToPath(uri); + if (!key) { + return ProjectAssignmentResult::InvalidUri; + } + const TextDocument* document = documents_.find(uri); + if (!document) { + return ProjectAssignmentResult::NotAssigned; + } + + const bool fileBacked = path.has_value(); + const auto sourcePath = fileBacked ? canonicalPath(*path) : std::filesystem::path{}; + project::FileProject resolved; + if (fileBacked) { + resolved = resolver_(*path); + if (!resolved.error.empty()) { + recordConfigurationDiagnostics(*path, resolved.diagnostics); + resolved = {}; + resolved.sourceFiles.push_back(sourcePath); + resolved.isStandalone = true; + } else { + clearConfigurationDiagnostics(*path); + } + } else { + resolved.isStandalone = true; + } + if (fileBacked && resolved.sourceFiles.empty()) { + return ProjectAssignmentResult::ResolutionFailed; + } + + const std::string id = fileBacked ? projectId(resolved) : *key; + auto [projectIt, inserted] = projects_.try_emplace(id); + ManagedProject& managed = projectIt->second; + if (inserted) { + managed.id = id; + managed.manifestGeneration = ++manifestGeneration_; + } + managed.manifestPath = resolved.manifest + ? std::optional(resolved.manifest->manifestPath) : std::nullopt; + managed.sourceFiles = std::move(resolved.sourceFiles); + managed.isStandalone = resolved.isStandalone; + managed.documentGeneration = ++documentGeneration_; + managed.generation = ++generation_; + + assignments_.insert_or_assign(*key, Assignment{ + document->uri, + sourcePath, + pathKey(sourcePath), + fileBacked ? pathIdentity(sourcePath) : *key, + id, + fileBacked, + }); + return ProjectAssignmentResult::Assigned; +} + +ProjectAssignmentResult ProjectManager::documentChanged(std::string_view uri) { + const auto key = DocumentUriKey(uri); + if (!key) { + return ProjectAssignmentResult::InvalidUri; + } + const auto assignment = assignments_.find(*key); + if (assignment == assignments_.end()) { + return ProjectAssignmentResult::NotAssigned; + } + ManagedProject& project = projects_.at(assignment->second.projectId); + project.documentGeneration = ++documentGeneration_; + project.generation = ++generation_; + return ProjectAssignmentResult::Assigned; +} + +ProjectAssignmentResult ProjectManager::documentClosed(std::string_view uri) { + const auto key = DocumentUriKey(uri); + if (!key) { + return ProjectAssignmentResult::InvalidUri; + } + const auto assignment = assignments_.find(*key); + if (assignment == assignments_.end()) { + return ProjectAssignmentResult::NotAssigned; + } + if (assignment->second.fileBacked) { + return documentChanged(uri); + } + projects_.erase(assignment->second.projectId); + assignments_.erase(assignment); + return ProjectAssignmentResult::Assigned; +} + +ProjectRefreshResult ProjectManager::refreshOpenDocuments( + const std::vector& workspaceRoots, + bool restrictToWorkspaceRoots) { + ProjectRefreshResult result; + std::unordered_set previousProjectIds; + for (auto assignment = assignments_.begin(); assignment != assignments_.end();) { + if (!documents_.find(assignment->second.uri)) { + assignment = assignments_.erase(assignment); + continue; + } + previousProjectIds.insert(assignment->second.projectId); + ++assignment; + } + + for (auto& [key, assignment] : assignments_) { + const bool inWorkspace = !assignment.fileBacked || std::any_of( + workspaceRoots.begin(), workspaceRoots.end(), [&](const auto& root) { + return isWithin(assignment.path, root); + }); + project::FileProject resolved; + if (!assignment.fileBacked) { + resolved.isStandalone = true; + } else if (restrictToWorkspaceRoots && !inWorkspace) { + resolved.sourceFiles.push_back(canonicalPath(assignment.path)); + resolved.isStandalone = true; + } else { + resolved = resolver_(assignment.path); + } + if (!resolved.error.empty() || (assignment.fileBacked && resolved.sourceFiles.empty())) { + result.errors.push_back(resolved.error.empty() + ? "project resolves to no source files" : std::move(resolved.error)); + recordConfigurationDiagnostics(assignment.path, resolved.diagnostics); + resolved = {}; + if (assignment.fileBacked) { + resolved.sourceFiles.push_back(canonicalPath(assignment.path)); + } + resolved.isStandalone = true; + } else { + clearConfigurationDiagnostics(assignment.path); + } + + const std::string id = assignment.fileBacked ? projectId(resolved) : key; + ManagedProject& managed = projects_[id]; + managed.id = id; + managed.manifestPath = resolved.manifest + ? std::optional(resolved.manifest->manifestPath) : std::nullopt; + managed.sourceFiles = std::move(resolved.sourceFiles); + managed.isStandalone = resolved.isStandalone; + assignment.projectId = id; + } + + std::unordered_set currentProjectIds; + for (const auto& [key, assignment] : assignments_) { + currentProjectIds.insert(assignment.projectId); + } + for (const auto& id : currentProjectIds) { + ManagedProject& project = projects_.at(id); + project.documentGeneration = ++documentGeneration_; + project.manifestGeneration = ++manifestGeneration_; + project.generation = ++generation_; + result.changedProjectIds.push_back(id); + } + for (const auto& id : previousProjectIds) { + if (!currentProjectIds.contains(id)) { + result.removedProjectIds.push_back(id); + } + } + for (auto project = projects_.begin(); project != projects_.end();) { + if (!currentProjectIds.contains(project->first)) { + project = projects_.erase(project); + } else { + ++project; + } + } + + std::sort(result.changedProjectIds.begin(), result.changedProjectIds.end()); + std::sort(result.removedProjectIds.begin(), result.removedProjectIds.end()); + result.configurationDiagnostics = configurationDiagnostics(); + return result; +} + +const ManagedProject* ProjectManager::projectForDocument(std::string_view uri) const { + const auto key = DocumentUriKey(uri); + if (!key) { + return nullptr; + } + const auto assignment = assignments_.find(*key); + if (assignment == assignments_.end()) { + return nullptr; + } + const auto project = projects_.find(assignment->second.projectId); + return project == projects_.end() ? nullptr : &project->second; +} + +const ManagedProject* ProjectManager::project(std::string_view projectId) const { + const auto project = projects_.find(std::string(projectId)); + return project == projects_.end() ? nullptr : &project->second; +} + +std::optional ProjectManager::sourceIdentityForDocument( + std::string_view uri) const { + const auto key = DocumentUriKey(uri); + if (!key) return std::nullopt; + const auto assignment = assignments_.find(*key); + return assignment == assignments_.end() ? std::nullopt + : std::optional(assignment->second.sourceIdentity); +} + +std::vector ProjectManager::projectIds() const { + std::vector result; + result.reserve(projects_.size()); + for (const auto& [id, project] : projects_) { + result.push_back(id); + } + std::sort(result.begin(), result.end()); + return result; +} + +ProjectSourceSet ProjectManager::sourceSetForDocument(std::string_view uri) const { + const auto project = projectForDocument(uri); + if (!project) { + ProjectSourceSet result; + result.error = "document is not assigned to a project"; + return result; + } + return sourceSetForProject(project->id); +} + +ProjectSourceSet ProjectManager::sourceSetForProject(std::string_view projectId) const { + ProjectSourceSet result; + const auto projectIt = projects_.find(std::string(projectId)); + if (projectIt == projects_.end()) { + result.error = "project is not managed"; + return result; + } + const ManagedProject* project = &projectIt->second; + result.generation = project->generation; + result.documentGeneration = project->documentGeneration; + result.manifestGeneration = project->manifestGeneration; + + for (const auto& sourcePath : project->sourceFiles) { + const TextDocument* overlay = nullptr; + const std::string sourcePathKey = pathKey(sourcePath); + for (const auto& [key, assignment] : assignments_) { + if (assignment.projectId == project->id && assignment.pathKey == sourcePathKey) { + overlay = documents_.find(assignment.uri); + break; + } + } + + if (overlay) { + result.sources.push_back({pathIdentity(sourcePath), overlay->text, sourcePath}); + continue; + } + + result.sources.push_back({pathIdentity(sourcePath), std::nullopt, sourcePath}); + } + for (const auto& [key, assignment] : assignments_) { + if (assignment.projectId != project->id || assignment.fileBacked) { + continue; + } + const auto* overlay = documents_.find(assignment.uri); + if (overlay) { + result.sources.push_back({assignment.sourceIdentity, overlay->text, std::nullopt}); + } + } + return result; +} + +std::vector ProjectManager::configurationDiagnostics() const { + std::vector diagnostics; + diagnostics.reserve(configurationDiagnostics_.size()); + for (const auto& [key, diagnostic] : configurationDiagnostics_) { + diagnostics.push_back(diagnostic); + } + std::sort(diagnostics.begin(), diagnostics.end(), [](const auto& left, const auto& right) { + return pathKey(left.path) < pathKey(right.path); + }); + return diagnostics; +} + +void ProjectManager::recordConfigurationDiagnostics( + const std::filesystem::path& documentPath, + const std::vector& diagnostics) { + clearConfigurationDiagnostics(documentPath); + for (const auto& diagnostic : diagnostics) { + configurationDiagnostics_.insert_or_assign(pathKey(diagnostic.path), diagnostic); + } +} + +void ProjectManager::clearConfigurationDiagnostics(const std::filesystem::path& documentPath) { + for (auto diagnostic = configurationDiagnostics_.begin(); + diagnostic != configurationDiagnostics_.end();) { + if (isWithin(documentPath, diagnostic->second.path.parent_path())) { + diagnostic = configurationDiagnostics_.erase(diagnostic); + } else { + ++diagnostic; + } + } +} + +std::string ProjectManager::projectId(const project::FileProject& project) { + if (project.manifest) { + return pathKey(project.manifest->manifestPath); + } + return pathKey(project.sourceFiles.front()); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/rename_routes.cpp b/lsp/src/rename_routes.cpp new file mode 100644 index 0000000..8626083 --- /dev/null +++ b/lsp/src/rename_routes.cpp @@ -0,0 +1,93 @@ +#include "rls/lsp/route_modules.h" + +#include +#include + +#include + +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/rename_service.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& value) { + if (!value.is_object()) throw InvalidParams("expected object parameters"); + return value; +} + +uint32_t positionComponent(const Json& value) { + if (!value.is_number_integer() && !value.is_number_unsigned()) { + throw InvalidParams("position components must be integers"); + } + const auto component = value.get(); + if (component < 0 + || static_cast(component) > std::numeric_limits::max()) { + throw InvalidParams("invalid position component"); + } + return static_cast(component); +} + +NavigationPosition requestPosition(const Json& object) { + const auto& value = requireObject(object.at("position")); + return {positionComponent(value.at("line")), positionComponent(value.at("character"))}; +} + +Json position(const NavigationPosition& value) { + return {{"line", value.line}, {"character", value.character}}; +} + +Json range(const NavigationRange& value) { + return {{"start", position(value.start)}, {"end", position(value.end)}}; +} + +template +const T& requireRename(const RenameResult& result) { + if (!result.value) throw RequestFailed(std::string(RenameErrorMessage(result.error))); + return *result.value; +} + +} // namespace + +void RegisterRenameRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, RenameService& rename) { + router.registerRequest("textDocument/prepareRename", [&rename](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + return range(requireRename(rename.prepare( + document.at("uri").get(), requestPosition(object)))); + }); + router.registerRequest("textDocument/rename", [&lifecycle, &rename](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + if (!object.at("newName").is_string()) throw InvalidParams("newName must be a string"); + const auto result = rename.rename( + document.at("uri").get(), requestPosition(object), + object.at("newName").get(), + lifecycle.supportsWorkspaceDocumentChanges()); + const auto& edit = requireRename(result); + Json documentChanges = Json::array(); + for (const auto& documentEdit : edit.documents) { + Json textDocument = {{"uri", documentEdit.uri}}; + textDocument["version"] = documentEdit.version + ? Json(*documentEdit.version) : Json(nullptr); + Json edits = Json::array(); + for (const auto& textEdit : documentEdit.edits) { + edits.push_back({ + {"range", range(textEdit.range)}, + {"newText", textEdit.newText}, + }); + } + documentChanges.push_back({ + {"textDocument", std::move(textDocument)}, + {"edits", std::move(edits)}, + }); + } + return Json{{"documentChanges", std::move(documentChanges)}}; + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/rename_service.cpp b/lsp/src/rename_service.cpp new file mode 100644 index 0000000..2c725cb --- /dev/null +++ b/lsp/src/rename_service.cpp @@ -0,0 +1,292 @@ +#include "rls/lsp/rename_service.h" + +#include +#include +#include +#include +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/project_manager.h" + +namespace rls::lsp { +namespace { + +struct RenameQuery { + AnalysisScheduler::Snapshot snapshot; + sema::SymbolRecord declaration; + sema::OccurrenceRecord occurrence; + std::string occurrenceText; +}; + +bool sameSpan(const ast::Span& left, const ast::Span& right) { + return left.file == right.file + && left.start.line == right.start.line + && left.start.column == right.start.column + && left.end.line == right.end.line + && left.end.column == right.end.column; +} + +std::optional rangeFor( + const sema::AnalysisSnapshot& snapshot, const ast::Span& span) { + const auto* source = snapshot.sourceText(span.file); + if (!source) return std::nullopt; + const auto startOffset = source->byteOffsetFromUtf8Position(span.start); + const auto endOffset = source->byteOffsetFromUtf8Position(span.end); + if (!startOffset || !endOffset) return std::nullopt; + const auto start = source->utf16PositionAtByteOffset(*startOffset); + const auto end = source->utf16PositionAtByteOffset(*endOffset); + if (!start || !end) return std::nullopt; + return NavigationRange{ + {start->line - 1, start->column - 1}, + {end->line - 1, end->column - 1}, + }; +} + +bool isRenameable( + const sema::SymbolRecord& symbol, const sema::OccurrenceRecord& occurrence) { + if (symbol.category == sema::SymbolCategory::ExternEnumPattern) { + return occurrence.kind == sema::OccurrenceKind::ExitTarget; + } + if (symbol.provenance != sema::SymbolProvenance::Source + && symbol.category != sema::SymbolCategory::ExternDefine + && symbol.category != sema::SymbolCategory::Enum + && symbol.category != sema::SymbolCategory::EnumMember) { + return false; + } + switch (symbol.category) { + case sema::SymbolCategory::Region: + case sema::SymbolCategory::Define: + case sema::SymbolCategory::ExternDefine: + case sema::SymbolCategory::Enum: + case sema::SymbolCategory::EnumMember: + case sema::SymbolCategory::Parameter: + case sema::SymbolCategory::RegionDataEntry: + case sema::SymbolCategory::SectionEntry: + return true; + case sema::SymbolCategory::RegionExtension: + case sema::SymbolCategory::ExternEnumPattern: + return false; + } + return false; +} + +std::optional occurrenceText( + const sema::AnalysisSnapshot& snapshot, const sema::OccurrenceRecord& occurrence) { + const auto* source = snapshot.sourceText(occurrence.span.file); + if (!source) return std::nullopt; + const auto start = source->byteOffsetFromUtf8Position(occurrence.span.start); + const auto end = source->byteOffsetFromUtf8Position(occurrence.span.end); + if (!start || !end || *start > *end) return std::nullopt; + return source->content().substr(*start, *end - *start); +} + +bool validIdentifier(std::string_view value) { + if (value.empty()) return false; + const auto alpha = [](char character) { + return std::isalpha(static_cast(character)) != 0; + }; + const auto alnum = [](char character) { + return std::isalnum(static_cast(character)) != 0; + }; + if (!alpha(value.front()) && value.front() != '_') return false; + if (!std::all_of(value.begin() + 1, value.end(), [&](char character) { + return alnum(character) || character == '_'; + })) return false; + static constexpr std::array reserved{ + "region", "extend", "extern", "define", "enum", "events", "locations", + "exits", "true", "false", "always", "never", "and", "or", "not", + "is", "here", "match", + }; + return std::find(reserved.begin(), reserved.end(), value) == reserved.end(); +} + +bool conflicts( + const sema::SemanticIndex& index, const sema::SymbolRecord& target, + std::string_view newName) { + if (newName == target.displayName) return false; + for (const auto& candidate : index.symbols()) { + if (target.category == sema::SymbolCategory::EnumMember + && candidate.category == sema::SymbolCategory::ExternEnumPattern + && candidate.container == target.container + && index.patternMatches(candidate.id, newName)) { + return true; + } + if (candidate.displayName != newName) continue; + if (target.category == sema::SymbolCategory::Parameter) { + if (candidate.category == target.category && candidate.container == target.container) { + return true; + } + continue; + } + if (target.category == sema::SymbolCategory::EnumMember) { + if (candidate.category == target.category && candidate.container == target.container) { + return true; + } + continue; + } + if (target.category == sema::SymbolCategory::SectionEntry) { + if (candidate.category == target.category && candidate.type == target.type) return true; + continue; + } + if (target.category == sema::SymbolCategory::RegionDataEntry) { + if (candidate.category == target.category) return true; + continue; + } + if (candidate.category == target.category) return true; + if (((target.category == sema::SymbolCategory::Define + || target.category == sema::SymbolCategory::ExternDefine) + && (candidate.category == sema::SymbolCategory::Define + || candidate.category == sema::SymbolCategory::ExternDefine)) + || (target.category == sema::SymbolCategory::Enum + && candidate.category == sema::SymbolCategory::Enum)) { + return true; + } + } + return false; +} + +bool patternConflicts( + const sema::AnalysisSnapshot& snapshot, const sema::SymbolRecord& target, + std::string_view currentName, std::string_view newName) { + if (newName == currentName) return false; + for (const auto& symbol : snapshot.semanticIndex().symbols()) { + if (symbol.displayName != newName) continue; + if (symbol.category == sema::SymbolCategory::Region + || (symbol.category == sema::SymbolCategory::EnumMember + && symbol.container == target.container)) { + return true; + } + } + for (const auto& occurrence : snapshot.references(target.id)) { + const auto text = occurrenceText(snapshot, occurrence); + if (text && *text == newName) return true; + } + return false; +} + +RenameResult queryAt( + const ProjectManager& projects, const AnalysisScheduler& scheduler, + std::string_view uri, NavigationPosition position) { + const auto* project = projects.projectForDocument(uri); + const auto identity = projects.sourceIdentityForDocument(uri); + if (!project || !identity) return {{}, RenameError::NotRenameable}; + const auto snapshot = scheduler.acceptedSnapshot(project->id); + if (!snapshot || snapshot->generation() != project->generation) { + return {{}, RenameError::StaleSnapshot}; + } + const auto* source = snapshot->sourceText(*identity); + const auto* sourceIndex = snapshot->sourceIndex(*identity); + if (!source || !sourceIndex + || position.line == std::numeric_limits::max() + || position.character == std::numeric_limits::max()) { + return {{}, RenameError::NotRenameable}; + } + const auto offset = source->byteOffsetFromUtf16Position({ + position.line + 1, position.character + 1}); + if (!offset) return {{}, RenameError::NotRenameable}; + std::optional occurrence; + std::optional sourceName; + const auto findAt = [&](size_t byteOffset) { + const auto sourcePosition = source->utf8PositionAtByteOffset(byteOffset); + if (!sourcePosition) return false; + occurrence = snapshot->occurrenceAt(*identity, *sourcePosition); + sourceName = sourceIndex->nameAt(*sourcePosition); + return occurrence && occurrence->symbol && sourceName + && sameSpan(sourceName->span, occurrence->span); + }; + if (!findAt(*offset)) { + if (*offset == 0 + || !std::isalnum(static_cast(source->content()[*offset - 1])) + && source->content()[*offset - 1] != '_' + || !findAt(*offset - 1)) { + return {{}, RenameError::NotRenameable}; + } + } + const auto declaration = snapshot->declaration(*occurrence->symbol); + if (!declaration || !isRenameable(*declaration, *occurrence)) { + return {{}, RenameError::NotRenameable}; + } + return {RenameQuery{ + snapshot, *declaration, *occurrence, sourceName->text}, RenameError::None}; +} + +} // namespace + +RenameService::RenameService( + const DocumentStore& documents, const ProjectManager& projects, + const AnalysisScheduler& scheduler) + : documents_(documents), projects_(projects), scheduler_(scheduler) {} + +RenameResult RenameService::prepare( + std::string_view uri, NavigationPosition position) const { + const auto query = queryAt(projects_, scheduler_, uri, position); + if (!query.value) return {{}, query.error}; + const auto result = rangeFor(*query.value->snapshot, query.value->occurrence.span); + return result ? RenameResult{*result, RenameError::None} + : RenameResult{{}, RenameError::NotRenameable}; +} + +RenameResult RenameService::rename( + std::string_view uri, NavigationPosition position, std::string_view newName, + bool supportsDocumentChanges) const { + if (!supportsDocumentChanges) return {{}, RenameError::UnsupportedClient}; + if (!validIdentifier(newName)) return {{}, RenameError::InvalidName}; + const auto query = queryAt(projects_, scheduler_, uri, position); + if (!query.value) return {{}, query.error}; + const bool patternBacked = query.value->declaration.category + == sema::SymbolCategory::ExternEnumPattern; + if (patternBacked && !query.value->snapshot->semanticIndex().patternMatches( + query.value->declaration.id, newName)) { + return {{}, RenameError::InvalidName}; + } + if ((patternBacked && patternConflicts( + *query.value->snapshot, query.value->declaration, + query.value->occurrenceText, newName)) + || (!patternBacked && conflicts( + query.value->snapshot->semanticIndex(), query.value->declaration, newName))) { + return {{}, RenameError::Collision}; + } + + std::map grouped; + for (const auto& occurrence : query.value->snapshot->references(query.value->declaration.id)) { + if (patternBacked) { + const auto text = occurrenceText(*query.value->snapshot, occurrence); + if (!text || *text != query.value->occurrenceText) continue; + } + const auto occurrenceUri = PathToFileUri(occurrence.span.file); + const auto occurrenceRange = rangeFor(*query.value->snapshot, occurrence.span); + if (!occurrenceUri || !occurrenceRange) return {{}, RenameError::StaleSnapshot}; + auto [entry, inserted] = grouped.try_emplace( + *occurrenceUri, RenameDocumentEdit{*occurrenceUri, std::nullopt, {}}); + if (inserted) { + if (const auto* document = documents_.find(*occurrenceUri)) { + entry->second.version = document->version; + } + } + entry->second.edits.push_back({*occurrenceRange, std::string(newName)}); + } + RenameWorkspaceEdit edit; + for (auto& [unused, document] : grouped) { + edit.documents.push_back(std::move(document)); + } + return {std::move(edit), RenameError::None}; +} + +std::string_view RenameErrorMessage(RenameError error) { + switch (error) { + case RenameError::NotRenameable: return "symbol cannot be renamed"; + case RenameError::InvalidName: return "new name is not a valid RLS identifier"; + case RenameError::Collision: return "new name conflicts with an existing declaration"; + case RenameError::StaleSnapshot: return "rename requires a current analysis snapshot"; + case RenameError::UnsupportedClient: + return "client does not support versioned workspace document changes"; + case RenameError::None: return ""; + } + return "rename failed"; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/semantic_tokens_routes.cpp b/lsp/src/semantic_tokens_routes.cpp new file mode 100644 index 0000000..468cfc8 --- /dev/null +++ b/lsp/src/semantic_tokens_routes.cpp @@ -0,0 +1,32 @@ +#include "rls/lsp/route_modules.h" + +#include + +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/semantic_tokens_service.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& value) { + if (!value.is_object()) throw InvalidParams("expected object parameters"); + return value; +} + +} // namespace + +void RegisterSemanticTokenRoutes( + JsonRpcRouter& router, SemanticTokensService& semanticTokens) { + router.registerRequest( + "textDocument/semanticTokens/full", + [&semanticTokens](const Json& params) { + const auto& object = requireObject(params); + const auto& document = requireObject(object.at("textDocument")); + return Json{{"data", semanticTokens.full( + document.at("uri").get())}}; + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/semantic_tokens_service.cpp b/lsp/src/semantic_tokens_service.cpp new file mode 100644 index 0000000..4853519 --- /dev/null +++ b/lsp/src/semantic_tokens_service.cpp @@ -0,0 +1,268 @@ +#include "rls/lsp/semantic_tokens_service.h" + +#include +#include +#include +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +enum class TokenType : uint32_t { + Function, + Parameter, + Enum, + EnumMember, + Property, + Variable, + Operator, +}; + +enum class TokenModifier : uint32_t { + Declaration, + Definition, + Readonly, + DefaultLibrary, + Deprecated, +}; + +struct AbsoluteToken { + uint32_t line = 0; + uint32_t character = 0; + uint32_t length = 0; + TokenType type = TokenType::Variable; + uint32_t modifiers = 0; +}; + +uint32_t modifier(TokenModifier value) { + return uint32_t{1} << static_cast(value); +} + +std::string pathString(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string result; + result.reserve(generic.size()); + for (const char8_t byte : generic) result.push_back(static_cast(byte)); + return result; +} + +std::optional tokenType(sema::SymbolCategory category) { + switch (category) { + case sema::SymbolCategory::Define: + case sema::SymbolCategory::ExternDefine: + return TokenType::Function; + case sema::SymbolCategory::Parameter: + return TokenType::Parameter; + case sema::SymbolCategory::Enum: + return TokenType::Enum; + case sema::SymbolCategory::EnumMember: + return TokenType::EnumMember; + case sema::SymbolCategory::SectionEntry: + return TokenType::Property; + case sema::SymbolCategory::RegionDataEntry: + case sema::SymbolCategory::Region: + case sema::SymbolCategory::RegionExtension: + case sema::SymbolCategory::ExternEnumPattern: + return std::nullopt; + } + return std::nullopt; +} + +bool isReadonly(sema::SymbolCategory category) { + return category == sema::SymbolCategory::EnumMember; +} + +bool isDefinition(sema::SymbolCategory category) { + return category == sema::SymbolCategory::Define + || category == sema::SymbolCategory::Enum; +} + +bool isDefaultLibrary( + const sema::SemanticIndex& index, const sema::SymbolRecord& symbol) { + if (symbol.provenance == sema::SymbolProvenance::Extern) return true; + if (!symbol.container) return false; + const auto container = index.declaration(*symbol.container); + return container && container->provenance == sema::SymbolProvenance::Extern; +} + +std::optional makeToken( + const sema::AnalysisSnapshot& snapshot, const ast::SourceText& source, + const sema::OccurrenceRecord& occurrence, const sema::SymbolRecord& symbol) { + auto type = tokenType(symbol.category); + const bool concretePatternValue = symbol.category == sema::SymbolCategory::ExternEnumPattern + && occurrence.kind != sema::OccurrenceKind::Declaration; + const bool exitTarget = occurrence.kind == sema::OccurrenceKind::ExitTarget; + if (concretePatternValue) type = TokenType::EnumMember; + if (symbol.category == sema::SymbolCategory::SectionEntry + && occurrence.kind != sema::OccurrenceKind::Declaration) { + type = TokenType::EnumMember; + } + const bool concreteRegionValue = symbol.category == sema::SymbolCategory::Region + && occurrence.kind != sema::OccurrenceKind::Declaration; + if (concreteRegionValue) type = TokenType::EnumMember; + if (exitTarget) { + type = TokenType::Property; + } + if (!type || occurrence.span.start.line == 0 + || occurrence.span.start.line != occurrence.span.end.line) { + return std::nullopt; + } + const auto startOffset = source.byteOffsetFromUtf8Position(occurrence.span.start); + const auto endOffset = source.byteOffsetFromUtf8Position(occurrence.span.end); + if (!startOffset || !endOffset || *startOffset >= *endOffset) return std::nullopt; + const auto start = source.utf16PositionAtByteOffset(*startOffset); + const auto end = source.utf16PositionAtByteOffset(*endOffset); + if (!start || !end || start->line != end->line || start->column >= end->column) { + return std::nullopt; + } + + uint32_t modifiers = 0; + if (occurrence.kind == sema::OccurrenceKind::Declaration) { + modifiers |= modifier(isDefinition(symbol.category) + && symbol.provenance == sema::SymbolProvenance::Source + ? TokenModifier::Definition + : TokenModifier::Declaration); + } + if (isReadonly(symbol.category) || (concretePatternValue && !exitTarget) + || (concreteRegionValue && !exitTarget)) { + modifiers |= modifier(TokenModifier::Readonly); + } + if (!exitTarget && isDefaultLibrary(snapshot.semanticIndex(), symbol)) { + modifiers |= modifier(TokenModifier::DefaultLibrary); + } + return AbsoluteToken{ + start->line - 1, + start->column - 1, + end->column - start->column, + *type, + modifiers, + }; +} + +} // namespace + +SemanticTokensService::SemanticTokensService( + const ProjectManager& projects, AnalysisScheduler& scheduler) + : projects_(projects), scheduler_(scheduler) {} + +const std::vector& SemanticTokensService::tokenTypes() { + static const std::vector result = { + "function", "parameter", "enum", "enumMember", "property", "variable", "operator", + }; + return result; +} + +const std::vector& SemanticTokensService::tokenModifiers() { + static const std::vector result = { + "declaration", "definition", "readonly", "defaultLibrary", "deprecated", + }; + return result; +} + +std::vector SemanticTokensService::full(std::string_view uri) const { + const auto* project = projects_.projectForDocument(uri); + const auto identity = projects_.sourceIdentityForDocument(uri); + if (!project || !identity) return {}; + const std::string projectId = project->id; + const uint64_t generation = project->generation; + auto snapshot = scheduler_.acceptedSnapshot(projectId); + if (!snapshot || snapshot->generation() != generation) { + snapshot = scheduler_.awaitSnapshot(projectId, generation); + } + if (!snapshot || snapshot->generation() != generation) return {}; + const std::string& documentPath = *identity; + const auto* source = snapshot->sourceText(documentPath); + if (!source) return {}; + + std::vector tokens; + if (const auto* sourceIndex = snapshot->sourceIndex(documentPath)) { + for (const auto& logicalOperator : sourceIndex->logicalOperators()) { + const auto startOffset = source->byteOffsetFromUtf8Position(logicalOperator.span.start); + const auto endOffset = source->byteOffsetFromUtf8Position(logicalOperator.span.end); + if (!startOffset || !endOffset || *startOffset >= *endOffset) continue; + const auto start = source->utf16PositionAtByteOffset(*startOffset); + const auto end = source->utf16PositionAtByteOffset(*endOffset); + if (!start || !end || start->line != end->line || start->column >= end->column) continue; + tokens.push_back({ + start->line - 1, + start->column - 1, + end->column - start->column, + TokenType::Operator, + 0, + }); + } + } + for (const auto& occurrence : snapshot->semanticIndex().occurrences()) { + if (occurrence.span.file != documentPath) continue; + if (!occurrence.symbol && occurrence.kind == sema::OccurrenceKind::TypeReference) { + const auto startOffset = source->byteOffsetFromUtf8Position(occurrence.span.start); + const auto endOffset = source->byteOffsetFromUtf8Position(occurrence.span.end); + if (!startOffset || !endOffset || *startOffset >= *endOffset) continue; + const auto start = source->utf16PositionAtByteOffset(*startOffset); + const auto end = source->utf16PositionAtByteOffset(*endOffset); + if (!start || !end || start->line != end->line || start->column >= end->column) continue; + tokens.push_back({ + start->line - 1, + start->column - 1, + end->column - start->column, + TokenType::Enum, + modifier(TokenModifier::DefaultLibrary), + }); + continue; + } + if (!occurrence.symbol) continue; + const auto symbol = snapshot->declaration(*occurrence.symbol); + if (!symbol) continue; + if (const auto token = makeToken(*snapshot, *source, occurrence, *symbol)) { + tokens.push_back(*token); + } + } + std::sort(tokens.begin(), tokens.end(), [](const AbsoluteToken& left, const AbsoluteToken& right) { + return std::tie(left.line, left.character, left.length, + left.type, left.modifiers) + < std::tie(right.line, right.character, right.length, + right.type, right.modifiers); + }); + + std::vector validated; + for (const auto& token : tokens) { + if (!validated.empty() && token.line == validated.back().line) { + const uint32_t previousEnd = validated.back().character + validated.back().length; + if (token.character == validated.back().character + && token.length == validated.back().length + && token.type == validated.back().type) { + validated.back().modifiers |= token.modifiers; + continue; + } + if (token.character < previousEnd) continue; + } + validated.push_back(token); + } + + std::vector data; + data.reserve(validated.size() * 5); + uint32_t previousLine = 0; + uint32_t previousCharacter = 0; + for (const auto& token : validated) { + const uint32_t deltaLine = token.line - previousLine; + const uint32_t deltaStart = deltaLine == 0 + ? token.character - previousCharacter + : token.character; + data.insert(data.end(), { + deltaLine, + deltaStart, + token.length, + static_cast(token.type), + token.modifiers, + }); + previousLine = token.line; + previousCharacter = token.character; + } + return data; +} + +} // namespace rls::lsp diff --git a/lsp/src/server_composition_root.cpp b/lsp/src/server_composition_root.cpp new file mode 100644 index 0000000..f56a949 --- /dev/null +++ b/lsp/src/server_composition_root.cpp @@ -0,0 +1,91 @@ +#include "rls/lsp/server_composition_root.h" + +#include + +#include "rls/lsp/route_modules.h" + +namespace rls::lsp { + +ServerCompositionRoot::ServerCompositionRoot(ProjectManager::Resolver resolver) + : projects_(documents_, std::move(resolver)), + diagnostics_(outbound_), + navigation_(projects_, scheduler_), + rename_(documents_, projects_, scheduler_), + completion_(projects_, scheduler_), + signatureHelp_(projects_, scheduler_), + hover_(projects_, scheduler_), + semanticTokens_(projects_, scheduler_), + workspace_(projects_, scheduler_, diagnostics_), + synchronization_(lifecycle_, documents_, projects_, scheduler_, diagnostics_) { + scheduler_.setAcceptedHandler( + [this](std::string projectId, AnalysisScheduler::Snapshot snapshot) { + diagnostics_.acceptedSnapshot(std::move(projectId), std::move(snapshot)); + }); + RegisterLifecycleRoutes(router_, lifecycle_, workspace_); + RegisterDocumentSynchronizationRoutes(router_, synchronization_); + RegisterAuthoringRoutes(router_, lifecycle_, completion_, signatureHelp_, hover_); + RegisterNavigationRoutes(router_, lifecycle_, navigation_, workspace_); + RegisterRenameRoutes(router_, lifecycle_, rename_); + RegisterSemanticTokenRoutes(router_, semanticTokens_); + RegisterWorkspaceRoutes(router_, lifecycle_, workspace_); + router_.requireRoutes({ + "initialize", + "initialized", + "shutdown", + "exit", + "textDocument/didOpen", + "textDocument/didChange", + "textDocument/didClose", + "textDocument/completion", + "textDocument/signatureHelp", + "textDocument/hover", + "textDocument/semanticTokens/full", + "textDocument/definition", + "textDocument/references", + "textDocument/prepareRename", + "textDocument/rename", + "textDocument/documentHighlight", + "textDocument/documentSymbol", + "workspace/symbol", + "workspace/didChangeWorkspaceFolders", + "workspace/didChangeWatchedFiles", + }); +} + +std::vector ServerCompositionRoot::handlePayload(std::string_view payload) const { + return router_.handlePayload(payload); +} + +bool ServerCompositionRoot::shouldExit() const { + return lifecycle_.shouldExit(); +} + +int ServerCompositionRoot::exitCode() const { + return lifecycle_.exitCode(); +} + +const DocumentStore& ServerCompositionRoot::documents() const { + return documents_; +} + +const ProjectManager& ServerCompositionRoot::projects() const { + return projects_; +} + +AnalysisScheduler& ServerCompositionRoot::scheduler() { + return scheduler_; +} + +const WorkspaceService& ServerCompositionRoot::workspace() const { + return workspace_; +} + +OutboundMessageQueue& ServerCompositionRoot::outbound() { + return outbound_; +} + +const JsonRpcRouter& ServerCompositionRoot::router() const { + return router_; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/signature_help_service.cpp b/lsp/src/signature_help_service.cpp new file mode 100644 index 0000000..63695c1 --- /dev/null +++ b/lsp/src/signature_help_service.cpp @@ -0,0 +1,179 @@ +#include "rls/lsp/signature_help_service.h" + +#include +#include +#include +#include + +#include "rls/lsp/document_uri.h" + +namespace rls::lsp { +namespace { + +struct CurrentDocument { + AnalysisScheduler::Snapshot snapshot; + std::string path; + const ast::SourceText* source = nullptr; + const parser::SourceIndex* sourceIndex = nullptr; +}; + +std::string pathString(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string result; + result.reserve(generic.size()); + for (const char8_t byte : generic) result.push_back(static_cast(byte)); + return result; +} + +std::optional currentDocument( + const ProjectManager& projects, AnalysisScheduler& scheduler, + std::string_view uri) { + const auto* project = projects.projectForDocument(uri); + const auto identity = projects.sourceIdentityForDocument(uri); + if (!project || !identity) return std::nullopt; + const std::string projectId = project->id; + const uint64_t generation = project->generation; + auto snapshot = scheduler.acceptedSnapshot(projectId); + if (!snapshot || snapshot->generation() != generation) { + snapshot = scheduler.awaitSnapshot(projectId, generation); + } + if (!snapshot || snapshot->generation() != generation) return std::nullopt; + const std::string& documentPath = *identity; + const auto* source = snapshot->sourceText(documentPath); + const auto* sourceIndex = snapshot->sourceIndex(documentPath); + if (!source || !sourceIndex) return std::nullopt; + return CurrentDocument{snapshot, documentPath, source, sourceIndex}; +} + +PresentationType presentationType( + ast::Type type, const std::optional& enumName) { + std::string name; + switch (type) { + case ast::Type::Bool: name = "Bool"; break; + case ast::Type::Int: name = "Int"; break; + case ast::Type::String: name = "String"; break; + case ast::Type::List: name = "List"; break; + case ast::Type::Callable: name = "Callable"; break; + case ast::Type::Condition: name = "Condition"; break; + case ast::Type::Enum: name = "Enum"; break; + case ast::Type::Region: name = "Region"; break; + case ast::Type::Event: name = "Event"; break; + case ast::Type::Location: name = "Location"; break; + case ast::Type::Void: name = "Void"; break; + case ast::Type::Error: name = ""; break; + } + return {std::move(name), enumName}; +} + +PresentationProvenance presentationProvenance(sema::SymbolProvenance provenance) { + switch (provenance) { + case sema::SymbolProvenance::Source: return PresentationProvenance::Source; + case sema::SymbolProvenance::Extern: return PresentationProvenance::Extern; + case sema::SymbolProvenance::Pattern: return PresentationProvenance::Pattern; + } + return PresentationProvenance::Source; +} + +bool isBeforeOrEqual(ast::Position left, ast::Position right) { + return left.line < right.line + || (left.line == right.line && left.column <= right.column); +} + +std::optional activeArgumentAt( + const parser::CallContext& call, ast::Position cursor) { + if (call.activeArgument) return call.activeArgument; + if (!isBeforeOrEqual(call.callee.end, cursor)) return std::nullopt; + if (call.argumentRanges.empty()) return 0; + for (size_t index = 0; index < call.argumentRanges.size(); ++index) { + if (isBeforeOrEqual(cursor, call.argumentRanges[index].end)) return index; + } + return call.argumentRanges.size() - 1; +} + +} // namespace + +SignatureHelpService::SignatureHelpService( + const ProjectManager& projects, AnalysisScheduler& scheduler) + : projects_(projects), scheduler_(scheduler) {} + +std::optional SignatureHelpService::signatureHelp( + std::string_view uri, PresentationPosition position) const { + if (position.line == std::numeric_limits::max() + || position.character == std::numeric_limits::max()) { + return std::nullopt; + } + const auto document = currentDocument(projects_, scheduler_, uri); + if (!document) return std::nullopt; + const auto cursorOffset = document->source->byteOffsetFromUtf16Position({ + position.line + 1, position.character + 1}); + if (!cursorOffset) return std::nullopt; + const auto cursor = document->source->utf8PositionAtByteOffset(*cursorOffset); + if (!cursor) return std::nullopt; + + const auto sourceCall = document->sourceIndex->enclosingCall(*cursor); + const auto call = document->snapshot->callAt(document->path, *cursor); + if (!sourceCall || !call || !call->target) return std::nullopt; + const auto callable = document->snapshot->declaration(*call->target); + if (!callable || (callable->category != sema::SymbolCategory::Define + && callable->category != sema::SymbolCategory::ExternDefine)) { + return std::nullopt; + } + + std::vector parameters; + for (const auto& symbol : document->snapshot->semanticIndex().symbols()) { + if (symbol.category == sema::SymbolCategory::Parameter + && symbol.container == callable->id) { + parameters.push_back(&symbol); + } + } + std::sort(parameters.begin(), parameters.end(), [](const auto* left, const auto* right) { + return std::tie(left->selection.start.line, left->selection.start.column) + < std::tie(right->selection.start.line, right->selection.start.column); + }); + + PresentationCallable presentation{.name = callable->displayName}; + for (const auto* parameter : parameters) { + presentation.parameters.push_back({ + .name = parameter->displayName, + .type = parameter->type + ? presentationType(*parameter->type, parameter->enumName) + : PresentationType{""}, + .defaultValue = parameter->defaultValue, + .optional = parameter->optional, + }); + } + if (callable->type) { + presentation.returnType = presentationType( + *callable->type, callable->enumName); + } + + PresentationSymbol symbol{ + .kind = PresentationSymbolKind::Function, + .name = callable->displayName, + .provenance = presentationProvenance(callable->provenance), + .callable = presentation, + }; + const auto rendered = PresentationRenderer{}.render(symbol); + SignatureHelpResult result{ + .label = rendered.detail, + .documentation = rendered.documentation, + }; + for (const auto& parameter : presentation.parameters) { + result.parameterLabels.push_back( + PresentationRenderer::renderParameter(parameter)); + } + + const auto activeArgument = activeArgumentAt(*sourceCall, *cursor); + if (activeArgument && *activeArgument < call->normalizedBindings.size()) { + const auto binding = call->normalizedBindings[*activeArgument]; + if (binding && *binding < parameters.size()) result.activeParameter = *binding; + } else if (activeArgument && call->normalizedBindings.empty() + && !parameters.empty()) { + result.activeParameter = 0; + } + return result; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/workspace_routes.cpp b/lsp/src/workspace_routes.cpp new file mode 100644 index 0000000..4a542c5 --- /dev/null +++ b/lsp/src/workspace_routes.cpp @@ -0,0 +1,71 @@ +#include "rls/lsp/route_modules.h" + +#include +#include + +#include + +#include "rls/lsp/json_rpc_router.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/workspace_service.h" + +namespace rls::lsp { +namespace { + +using Json = nlohmann::json; + +const Json& requireObject(const Json& value) { + if (!value.is_object()) { + throw InvalidParams("expected object parameters"); + } + return value; +} + +std::vector folderUris(const Json& folders) { + if (!folders.is_array()) { + throw InvalidParams("workspace folders must be an array"); + } + std::vector uris; + uris.reserve(folders.size()); + for (const auto& folder : folders) { + uris.push_back(requireObject(folder).at("uri").get()); + } + return uris; +} + +} // namespace + +void RegisterWorkspaceRoutes( + JsonRpcRouter& router, LifecycleService& lifecycle, WorkspaceService& workspace) { + router.registerNotification("workspace/didChangeWorkspaceFolders", + [&lifecycle, &workspace](const Json& params) { + if (!lifecycle.acceptsDocumentUpdates()) { + throw InvalidParams("server is not initialized"); + } + const auto& event = requireObject(requireObject(params).at("event")); + if (!workspace.changeFolders( + folderUris(event.at("added")), folderUris(event.at("removed")))) { + throw InvalidParams("workspace reload failed"); + } + }); + router.registerNotification("workspace/didChangeWatchedFiles", + [&lifecycle, &workspace](const Json& params) { + if (!lifecycle.acceptsDocumentUpdates()) { + throw InvalidParams("server is not initialized"); + } + const auto& changes = requireObject(params).at("changes"); + if (!changes.is_array()) { + throw InvalidParams("file changes must be an array"); + } + std::vector uris; + uris.reserve(changes.size()); + for (const auto& change : changes) { + uris.push_back(requireObject(change).at("uri").get()); + } + if (!workspace.watchedFilesChanged(uris)) { + throw InvalidParams("watched-file reload failed"); + } + }); +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/src/workspace_service.cpp b/lsp/src/workspace_service.cpp new file mode 100644 index 0000000..69ce609 --- /dev/null +++ b/lsp/src/workspace_service.cpp @@ -0,0 +1,154 @@ +#include "rls/lsp/workspace_service.h" + +#include +#include +#include + +#include "rls/lsp/document_uri.h" +#include "rls/lsp/project_analysis.h" + +namespace rls::lsp { +namespace { + +std::string pathKey(const std::filesystem::path& path) { + std::error_code error; + const auto canonical = std::filesystem::weakly_canonical(path, error); + const auto generic = (error ? path.lexically_normal() : canonical).generic_u8string(); + std::string key; + key.reserve(generic.size()); + for (const char8_t byte : generic) { + key.push_back(static_cast(byte)); + } +#ifdef _WIN32 + std::transform(key.begin(), key.end(), key.begin(), [](char character) { + return static_cast(std::tolower(static_cast(character))); + }); +#endif + return key; +} + +bool isWithin(const std::filesystem::path& path, const std::filesystem::path& root) { + const std::string candidate = pathKey(path); + std::string prefix = pathKey(root); + if (!prefix.ends_with('/')) prefix.push_back('/'); + return candidate == pathKey(root) || candidate.starts_with(prefix); +} + +} // namespace + +WorkspaceService::WorkspaceService( + ProjectManager& projects, AnalysisScheduler& scheduler, + DiagnosticPublisher& diagnostics) + : projects_(projects), scheduler_(scheduler), diagnostics_(diagnostics) {} + +bool WorkspaceService::initialize( + std::vector folderUris, bool restrictToWorkspaceFolders) { + std::unordered_set folders; + std::vector folderPaths; + for (const auto& uri : folderUris) { + const auto key = DocumentUriKey(uri); + const auto path = FileUriToPath(uri); + if (!key || !path || !std::filesystem::is_directory(*path)) { + return false; + } + folders.insert(*key); + folderPaths.push_back(*path); + } + folders_ = std::move(folders); + folderPaths_ = std::move(folderPaths); + restrictToWorkspaceFolders_ = restrictToWorkspaceFolders; + return true; +} + +bool WorkspaceService::changeFolders( + std::vector addedUris, std::vector removedUris) { + std::vector> added; + std::vector removed; + for (const auto& uri : removedUris) { + const auto key = DocumentUriKey(uri); + if (!key) { + return false; + } + removed.push_back(*key); + } + for (const auto& uri : addedUris) { + const auto key = DocumentUriKey(uri); + const auto path = FileUriToPath(uri); + if (!key || !path || !std::filesystem::is_directory(*path)) { + return false; + } + added.emplace_back(*key, *path); + } + for (const auto& key : removed) { + folders_.erase(key); + } + for (const auto& [key, path] : added) { + folders_.insert(key); + } + folderPaths_.clear(); + for (const auto& key : folders_) { + const auto path = FileUriToPath(key); + if (path) folderPaths_.push_back(*path); + } + restrictToWorkspaceFolders_ = true; + return refreshProjects(); +} + +bool WorkspaceService::watchedFilesChanged(const std::vector& uris) { + for (const auto& uri : uris) { + if (!FileUriToPath(uri)) { + return false; + } + } + return refreshProjects(); +} + +size_t WorkspaceService::folderCount() const { + return folders_.size(); +} + +std::vector WorkspaceService::projectIds() const { + const auto projectIds = projects_.projectIds(); + if (!restrictToWorkspaceFolders_) { + return projectIds; + } + + std::vector result; + for (const auto& projectId : projectIds) { + const auto* project = projects_.project(projectId); + if (!project) { + continue; + } + const bool manifestInWorkspace = project->manifestPath + && std::any_of(folderPaths_.begin(), folderPaths_.end(), [&](const auto& root) { + return isWithin(*project->manifestPath, root); + }); + const bool sourceInWorkspace = std::any_of( + project->sourceFiles.begin(), project->sourceFiles.end(), [&](const auto& source) { + return std::any_of(folderPaths_.begin(), folderPaths_.end(), [&](const auto& root) { + return isWithin(source, root); + }); + }); + if (manifestInWorkspace || sourceInWorkspace) { + result.push_back(projectId); + } + } + return result; +} + +bool WorkspaceService::refreshProjects() { + ProjectRefreshResult refresh = projects_.refreshOpenDocuments( + folderPaths_, restrictToWorkspaceFolders_); + diagnostics_.publishConfigurationDiagnostics(refresh.configurationDiagnostics); + for (const auto& projectId : refresh.removedProjectIds) { + scheduler_.removeProject(projectId); + diagnostics_.clearProject(projectId); + } + bool succeeded = refresh.errors.empty(); + for (const auto& projectId : refresh.changedProjectIds) { + succeeded = ScheduleProjectAnalysis(projects_, scheduler_, projectId) && succeeded; + } + return succeeded; +} + +} // namespace rls::lsp \ No newline at end of file diff --git a/lsp/tests/analysis_scheduler_tests.cpp b/lsp/tests/analysis_scheduler_tests.cpp new file mode 100644 index 0000000..d77e6f9 --- /dev/null +++ b/lsp/tests/analysis_scheduler_tests.cpp @@ -0,0 +1,332 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "analysis_snapshot.h" +#include "rls/lsp/analysis_scheduler.h" + +namespace { + +using rls::lsp::AnalysisRequest; +using rls::lsp::AnalysisScheduler; + +std::optional snapshotFor( + std::vector sources, uint64_t generation) { + return rls::sema::AnalysisSnapshot::Create(std::move(sources), generation); +} + +AnalysisRequest request(std::string projectId, uint64_t generation) { + return { + std::move(projectId), + generation, + {{"main.rls", "define value(): true\n"}}, + }; +} + +TEST(AnalysisSchedulerTests, DebouncesPendingWorkPerProject) { + std::mutex mutex; + std::vector builtGenerations; + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(40), .maximumConcurrency = 1}, + [&](std::vector sources, uint64_t generation, + std::stop_token) { + { + std::lock_guard lock(mutex); + builtGenerations.push_back(generation); + } + return snapshotFor(std::move(sources), generation); + }); + + EXPECT_TRUE(scheduler.schedule(request("project", 1))); + EXPECT_TRUE(scheduler.schedule(request("project", 2))); + EXPECT_FALSE(scheduler.schedule(request("project", 2))); + scheduler.waitForIdle(); + + std::lock_guard lock(mutex); + ASSERT_EQ(builtGenerations.size(), 1); + EXPECT_EQ(builtGenerations.front(), 2); + ASSERT_NE(scheduler.acceptedSnapshot("project"), nullptr); + EXPECT_EQ(scheduler.acceptedSnapshot("project")->generation(), 2); +} + +TEST(AnalysisSchedulerTests, AwaitSnapshotExpeditesPendingGeneration) { + AnalysisScheduler scheduler( + {.debounce = std::chrono::seconds(5), .maximumConcurrency = 1}); + + ASSERT_TRUE(scheduler.schedule(request("project", 1))); + const auto snapshot = scheduler.awaitSnapshot( + "project", 1, std::chrono::seconds(1)); + + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->generation(), 1u); +} + +TEST(AnalysisSchedulerTests, AwaitSnapshotFailsClosedWithoutScheduledGeneration) { + AnalysisScheduler scheduler( + {.debounce = std::chrono::seconds(5), .maximumConcurrency = 1}); + ASSERT_TRUE(scheduler.schedule(request("project", 1))); + + EXPECT_EQ(scheduler.awaitSnapshot( + "project", 2, std::chrono::milliseconds(10)), nullptr); + const auto snapshot = scheduler.awaitSnapshot( + "project", 1, std::chrono::seconds(1)); + ASSERT_NE(snapshot, nullptr); +} + +TEST(AnalysisSchedulerTests, CancelsRunningWorkAndSuppressesItsResult) { + std::mutex mutex; + std::condition_variable started; + std::condition_variable cancelled; + bool firstStarted = false; + bool firstCancelled = false; + + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}, + [&](std::vector sources, uint64_t generation, + std::stop_token cancellation) -> std::optional { + if (generation == 1) { + std::unique_lock lock(mutex); + firstStarted = true; + started.notify_all(); + std::stop_callback wakeOnCancellation(cancellation, [&] { cancelled.notify_all(); }); + cancelled.wait(lock, [&] { return cancellation.stop_requested(); }); + firstCancelled = true; + } + return snapshotFor(std::move(sources), generation); + }); + + ASSERT_TRUE(scheduler.schedule(request("project", 1))); + { + std::unique_lock lock(mutex); + started.wait(lock, [&] { return firstStarted; }); + } + ASSERT_TRUE(scheduler.schedule(request("project", 2))); + scheduler.waitForIdle(); + + EXPECT_TRUE(firstCancelled); + const auto accepted = scheduler.acceptedSnapshot("project"); + ASSERT_NE(accepted, nullptr); + EXPECT_EQ(accepted->generation(), 2); +} + +TEST(AnalysisSchedulerTests, BoundsConcurrentBuildsAcrossProjects) { + std::mutex mutex; + std::condition_variable started; + std::condition_variable release; + size_t active = 0; + size_t maximumActive = 0; + size_t startedCount = 0; + bool mayComplete = false; + + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 2}, + [&](std::vector sources, uint64_t generation, + std::stop_token) { + { + std::unique_lock lock(mutex); + ++active; + ++startedCount; + maximumActive = std::max(maximumActive, active); + started.notify_all(); + release.wait(lock, [&] { return mayComplete; }); + --active; + } + return snapshotFor(std::move(sources), generation); + }); + + ASSERT_TRUE(scheduler.schedule(request("one", 1))); + ASSERT_TRUE(scheduler.schedule(request("two", 1))); + ASSERT_TRUE(scheduler.schedule(request("three", 1))); + { + std::unique_lock lock(mutex); + started.wait(lock, [&] { return startedCount == 2; }); + EXPECT_EQ(maximumActive, 2); + mayComplete = true; + } + release.notify_all(); + scheduler.waitForIdle(); + + EXPECT_EQ(startedCount, 3); + EXPECT_EQ(maximumActive, 2); +} + +TEST(AnalysisSchedulerTests, DefaultBuilderCreatesWholeProjectSnapshot) { + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}); + + ASSERT_TRUE(scheduler.schedule({ + "project", + 7, + { + {"first.rls", "define first(): true\n"}, + {"second.rls", "define second(): first()\n"}, + }, + })); + scheduler.waitForIdle(); + + const auto accepted = scheduler.acceptedSnapshot("project"); + ASSERT_NE(accepted, nullptr); + EXPECT_EQ(accepted->generation(), 7); + EXPECT_EQ(accepted->documentCount(), 2); +} + +TEST(AnalysisSchedulerTests, BuilderFailureDoesNotStrandScheduler) { + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}, + [](std::vector sources, uint64_t generation, + std::stop_token) -> std::optional { + if (generation == 1) { + throw std::runtime_error("build failed"); + } + return snapshotFor(std::move(sources), generation); + }); + + ASSERT_TRUE(scheduler.schedule(request("project", 1))); + scheduler.waitForIdle(); + EXPECT_EQ(scheduler.acceptedSnapshot("project"), nullptr); + + ASSERT_TRUE(scheduler.schedule(request("project", 2))); + scheduler.waitForIdle(); + ASSERT_NE(scheduler.acceptedSnapshot("project"), nullptr); + EXPECT_EQ(scheduler.acceptedSnapshot("project")->generation(), 2); +} + +TEST(AnalysisSchedulerTests, RejectsRegressedDocumentOrManifestGenerations) { + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(40), .maximumConcurrency = 1}); + AnalysisRequest current = request("project", 10); + current.documentGeneration = 4; + current.manifestGeneration = 6; + ASSERT_TRUE(scheduler.schedule(std::move(current))); + + AnalysisRequest staleDocument = request("project", 11); + staleDocument.documentGeneration = 3; + staleDocument.manifestGeneration = 7; + EXPECT_FALSE(scheduler.schedule(std::move(staleDocument))); + + AnalysisRequest staleManifest = request("project", 12); + staleManifest.documentGeneration = 5; + staleManifest.manifestGeneration = 5; + EXPECT_FALSE(scheduler.schedule(std::move(staleManifest))); + scheduler.waitForIdle(); + ASSERT_NE(scheduler.acceptedSnapshot("project"), nullptr); + EXPECT_EQ(scheduler.acceptedSnapshot("project")->generation(), 10); +} + +TEST(AnalysisSchedulerTests, CancelsSupersededDiskReadBeforeSnapshotBuild) { + std::mutex mutex; + std::condition_variable started; + std::condition_variable cancelled; + bool readStarted = false; + bool readCancelled = false; + std::vector builtGenerations; + + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}, + [&](std::vector sources, uint64_t generation, + std::stop_token) { + builtGenerations.push_back(generation); + return snapshotFor(std::move(sources), generation); + }, + [&](const std::filesystem::path&, std::stop_token cancellationToken) + -> std::optional { + std::unique_lock lock(mutex); + readStarted = true; + started.notify_all(); + std::stop_callback wakeOnCancellation( + cancellationToken, [&] { cancelled.notify_all(); }); + cancelled.wait(lock, [&] { return cancellationToken.stop_requested(); }); + readCancelled = true; + return std::nullopt; + }); + + ASSERT_TRUE(scheduler.schedule({ + "project", 1, {{"slow.rls", std::nullopt, "slow.rls"}}, 1, 1, + })); + { + std::unique_lock lock(mutex); + started.wait(lock, [&] { return readStarted; }); + } + ASSERT_TRUE(scheduler.schedule({ + "project", 2, {{"fresh.rls", "define fresh(): true\n"}}, 2, 1, + })); + scheduler.waitForIdle(); + + EXPECT_TRUE(readCancelled); + ASSERT_EQ(builtGenerations.size(), 1); + EXPECT_EQ(builtGenerations.front(), 2); + ASSERT_NE(scheduler.acceptedSnapshot("project"), nullptr); + EXPECT_EQ(scheduler.acceptedSnapshot("project")->generation(), 2); +} + +TEST(AnalysisSchedulerTests, DefaultReaderAnalyzesEmptyDiskFile) { + const auto path = std::filesystem::temp_directory_path() / + ("rls-empty-source-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()) + ".rls"); + std::ofstream(path, std::ios::binary); + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}); + + ASSERT_TRUE(scheduler.schedule({ + "project", 1, {{path.generic_string(), std::nullopt, path}}, 1, 1, + })); + scheduler.waitForIdle(); + + const auto snapshot = scheduler.acceptedSnapshot("project"); + ASSERT_NE(snapshot, nullptr); + ASSERT_EQ(snapshot->documentCount(), 1); + const std::string canonicalPath = std::filesystem::weakly_canonical(path).generic_string(); + ASSERT_NE(snapshot->sourceText(canonicalPath), nullptr); + EXPECT_TRUE(snapshot->sourceText(canonicalPath)->content().empty()); + std::error_code error; + std::filesystem::remove(path, error); +} + +TEST(AnalysisSchedulerTests, CanonicalizesFilesystemOverlayIdentity) { + const auto path = std::filesystem::temp_directory_path() / + "rls-overlay-parent" / ".." / "rls-overlay-source.rls"; + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}); + + ASSERT_TRUE(scheduler.schedule({ + "project", 1, {{path.generic_string(), "define ready(): true\n"}}, 1, 1, + })); + scheduler.waitForIdle(); + + const auto snapshot = scheduler.acceptedSnapshot("project"); + ASSERT_NE(snapshot, nullptr); + const std::string canonicalPath = std::filesystem::weakly_canonical(path).generic_string(); + ASSERT_NE(snapshot->sourceText(canonicalPath), nullptr); +} + +TEST(AnalysisSchedulerTests, DiskReadFailureDoesNotInvokeSnapshotBuilder) { + size_t buildCount = 0; + AnalysisScheduler scheduler( + {.debounce = std::chrono::milliseconds(0), .maximumConcurrency = 1}, + [&](std::vector sources, uint64_t generation, + std::stop_token) { + ++buildCount; + return snapshotFor(std::move(sources), generation); + }, + [](const std::filesystem::path&, std::stop_token) + -> std::optional { return std::nullopt; }); + + ASSERT_TRUE(scheduler.schedule({ + "project", 1, {{"missing.rls", std::nullopt, "missing.rls"}}, 1, 1, + })); + scheduler.waitForIdle(); + + EXPECT_EQ(buildCount, 0); + EXPECT_EQ(scheduler.acceptedSnapshot("project"), nullptr); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/client_connection_tests.cpp b/lsp/tests/client_connection_tests.cpp new file mode 100644 index 0000000..fabfcbd --- /dev/null +++ b/lsp/tests/client_connection_tests.cpp @@ -0,0 +1,231 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "rls/lsp/client_connection.h" +#include "rls/lsp/message_framer.h" +#include "rls/lsp/server_composition_root.h" + +namespace { + +namespace fs = std::filesystem; + +using Json = nlohmann::json; +using rls::lsp::ClientConnection; +using rls::lsp::MessageFramer; +using rls::lsp::ServerCompositionRoot; + +class BlockingInputBuffer : public std::streambuf { +public: + void append(std::string bytes) { + { + std::lock_guard lock(mutex_); + for (char byte : bytes) bytes_.push_back(byte); + } + ready_.notify_all(); + } + + void close() { + { + std::lock_guard lock(mutex_); + closed_ = true; + } + ready_.notify_all(); + } + +protected: + int_type underflow() override { + std::unique_lock lock(mutex_); + ready_.wait(lock, [this] { return closed_ || !bytes_.empty(); }); + if (bytes_.empty()) return traits_type::eof(); + current_ = bytes_.front(); + bytes_.pop_front(); + setg(¤t_, ¤t_, ¤t_ + 1); + return traits_type::to_int_type(current_); + } + +private: + std::mutex mutex_; + std::condition_variable ready_; + std::deque bytes_; + char current_ = 0; + bool closed_ = false; +}; + +class CapturingOutputBuffer : public std::streambuf { +public: + bool waitForOccurrences(std::string_view value, size_t count) { + std::unique_lock lock(mutex_); + return changed_.wait_for(lock, std::chrono::seconds(3), [&] { + size_t occurrences = 0; + size_t position = 0; + while ((position = bytes_.find(value, position)) != std::string::npos) { + ++occurrences; + position += value.size(); + } + return occurrences >= count; + }); + } + + std::string bytes() const { + std::lock_guard lock(mutex_); + return bytes_; + } + +protected: + std::streamsize xsputn(const char* bytes, std::streamsize count) override { + { + std::lock_guard lock(mutex_); + bytes_.append(bytes, static_cast(count)); + } + changed_.notify_all(); + return count; + } + + int_type overflow(int_type value) override { + if (traits_type::eq_int_type(value, traits_type::eof())) return traits_type::not_eof(value); + const char byte = traits_type::to_char_type(value); + return xsputn(&byte, 1) == 1 ? value : traits_type::eof(); + } + +private: + mutable std::mutex mutex_; + std::condition_variable changed_; + std::string bytes_; +}; + +class InputCloseGuard { +public: + explicit InputCloseGuard(BlockingInputBuffer& input) : input_(input) {} + ~InputCloseGuard() { input_.close(); } + +private: + BlockingInputBuffer& input_; +}; + +std::vector decodeFrames(std::string_view bytes) { + MessageFramer framer; + framer.append(bytes); + std::vector messages; + while (const auto payload = framer.popMessage()) messages.push_back(Json::parse(*payload)); + return messages; +} + +TEST(ClientConnectionTests, ExchangesOnlyFramedProtocolMessagesOnOutput) { + const std::string inputBytes = MessageFramer::frame( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})") + + MessageFramer::frame(R"({"jsonrpc":"2.0","id":2,"method":"shutdown"})") + + MessageFramer::frame(R"({"jsonrpc":"2.0","method":"exit"})"); + std::istringstream input(inputBytes); + std::ostringstream output; + std::ostringstream log; + ServerCompositionRoot server; + ClientConnection connection(input, output, log); + + EXPECT_EQ(connection.run(server), 0); + EXPECT_TRUE(log.str().empty()); + + MessageFramer responses; + responses.append(output.str()); + ASSERT_TRUE(responses.popMessage().has_value()); + const auto shutdown = responses.popMessage(); + ASSERT_TRUE(shutdown.has_value()); + EXPECT_EQ(Json::parse(*shutdown)["id"], 2); + EXPECT_FALSE(responses.popMessage().has_value()); +} + +TEST(ClientConnectionTests, ReportsTransportErrorsOnlyToLogStream) { + std::istringstream input("Bad: header\r\n\r\n"); + std::ostringstream output; + std::ostringstream log; + ServerCompositionRoot server; + ClientConnection connection(input, output, log); + + EXPECT_EQ(connection.run(server), 1); + EXPECT_TRUE(output.str().empty()); + EXPECT_FALSE(log.str().empty()); +} + +TEST(ClientConnectionTests, PublishesLiveDiagnosticsWhileWaitingForInput) { + const fs::path directory = fs::temp_directory_path() / + ("rls-lsp-live-diagnostics-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + fs::create_directories(directory); + const fs::path sourcePath = directory / "main.rls"; + std::ofstream(sourcePath) << "define disk(): true\n"; + const std::string uri = static_cast((fs::path(sourcePath)).generic_string()); +#ifdef _WIN32 + const std::string fileUri = "file:///" + uri; +#else + const std::string fileUri = "file://" + uri; +#endif + + BlockingInputBuffer inputBuffer; + std::istream input(&inputBuffer); + CapturingOutputBuffer outputBuffer; + std::ostream output(&outputBuffer); + std::ostringstream log; + ServerCompositionRoot server; + ClientConnection connection(input, output, log); + auto running = std::async(std::launch::async, [&] { return connection.run(server); }); + InputCloseGuard closeInput(inputBuffer); + + inputBuffer.append(MessageFramer::frame( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})")); + inputBuffer.append(MessageFramer::frame( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})")); + inputBuffer.append(MessageFramer::frame( + Json{{"jsonrpc", "2.0"}, {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", fileUri}, {"languageId", "rls"}, {"version", 1}, + {"text", "region RR_TEST { events { EVENT_TEST: \"invalid\" } }\n"}, + }}}}}.dump())); + ASSERT_TRUE(outputBuffer.waitForOccurrences("textDocument/publishDiagnostics", 1)); + + inputBuffer.append(MessageFramer::frame( + Json{{"jsonrpc", "2.0"}, {"method", "textDocument/didChange"}, + {"params", { + {"textDocument", {{"uri", fileUri}, {"version", 2}}}, + {"contentChanges", Json::array({{{"text", + "region RR_TEST { events { EVENT_TEST: true } }\n"}}})}, + }}}.dump())); + ASSERT_TRUE(outputBuffer.waitForOccurrences("textDocument/publishDiagnostics", 2)); + + inputBuffer.append(MessageFramer::frame( + Json{{"jsonrpc", "2.0"}, {"method", "textDocument/didClose"}, + {"params", {{"textDocument", {{"uri", fileUri}}}}}}.dump())); + ASSERT_TRUE(outputBuffer.waitForOccurrences("textDocument/publishDiagnostics", 3)); + inputBuffer.append(MessageFramer::frame( + R"({"jsonrpc":"2.0","id":2,"method":"shutdown"})")); + inputBuffer.append(MessageFramer::frame( + R"({"jsonrpc":"2.0","method":"exit"})")); + inputBuffer.close(); + + EXPECT_EQ(running.get(), 0); + EXPECT_TRUE(log.str().empty()); + const auto messages = decodeFrames(outputBuffer.bytes()); + std::vector diagnostics; + for (const auto& message : messages) { + if (message.value("method", "") == "textDocument/publishDiagnostics") { + diagnostics.push_back(message); + } + } + ASSERT_GE(diagnostics.size(), 3); + EXPECT_FALSE(diagnostics[0]["params"]["diagnostics"].empty()); + EXPECT_TRUE(diagnostics[1]["params"]["diagnostics"].empty()); + EXPECT_TRUE(diagnostics[2]["params"]["diagnostics"].empty()); + + std::error_code error; + fs::remove_all(directory, error); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/completion_service_tests.cpp b/lsp/tests/completion_service_tests.cpp new file mode 100644 index 0000000..b9c79cf --- /dev/null +++ b/lsp/tests/completion_service_tests.cpp @@ -0,0 +1,879 @@ +#include +#include +#include + +#include + +#include "rls/lsp/completion_service.h" +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::CompletionItem; +using rls::lsp::CompletionService; +using rls::lsp::DocumentStore; +using rls::lsp::PresentationPosition; +using rls::lsp::ProjectManager; + +struct CompletionFixture { + fs::path path = fs::temp_directory_path() / "rls-completion-service.rls"; + std::string uri = *rls::lsp::PathToFileUri(path); + std::string content; + DocumentStore documents; + ProjectManager projects; + AnalysisScheduler scheduler; + + explicit CompletionFixture(std::string source) + : content(std::move(source)), + projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {path}; + project.isStandalone = true; + return project; + }), + scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }) { + EXPECT_EQ(documents.open(uri, "rls", 1, content), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(uri); + EXPECT_NE(project, nullptr); + if (!project) return; + EXPECT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{path.generic_string(), content}}, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + } +}; + +const CompletionItem* findItem( + const std::vector& items, std::string_view label) { + const auto found = std::find_if(items.begin(), items.end(), [&](const auto& item) { + return item.label == label; + }); + return found == items.end() ? nullptr : &*found; +} + +struct CrossFileCompletionFixture { + fs::path root = fs::temp_directory_path() / "rls-cross-file-completion"; + fs::path declarationPath = root / "declaration.rls"; + fs::path usagePath = root / "usage.rls"; + std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + DocumentStore documents; + ProjectManager projects; + AnalysisScheduler scheduler; + + CrossFileCompletionFixture(std::string declarations, std::string usage) + : projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }), + scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }) { + EXPECT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + EXPECT_NE(project, nullptr); + if (!project) return; + EXPECT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), std::move(declarations)}, + {usagePath.generic_string(), std::move(usage)}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + } + + std::vector completeAtEnd(std::string_view usage) { + return CompletionService(projects, scheduler).complete( + usageUri, {0, static_cast(usage.size())}); + } + + std::vector complete(PresentationPosition position) { + return CompletionService(projects, scheduler).complete(usageUri, position); + } +}; + +TEST(CompletionServiceTests, OffersOnlyDeclarationKeywordsAtTopLevel) { + CompletionFixture fixture("def\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {0, 3}); + + ASSERT_NE(findItem(items, "define"), nullptr); + ASSERT_NE(findItem(items, "extern define"), nullptr); + EXPECT_EQ(findItem(items, "true"), nullptr); + EXPECT_EQ(items.front().label, "define"); + EXPECT_EQ(items.front().replacementRange.start.character, 0u); + EXPECT_EQ(items.front().replacementRange.end.character, 3u); +} + +TEST(CompletionServiceTests, OffersBuiltInAndDeclaredTypesInTypePosition) { + CompletionFixture fixture( + "enum Color { RED }\n" + "define choose(value: Color): value\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {1, 26}); + + ASSERT_NE(findItem(items, "Color"), nullptr); + ASSERT_NE(findItem(items, "Bool"), nullptr); + EXPECT_EQ(findItem(items, "RED"), nullptr); + EXPECT_EQ(findItem(items, "choose"), nullptr); +} + +TEST(CompletionServiceTests, CompletesRecoveredParameterAndReturnTypes) { + const std::string parameterSource = + "enum Color { RED }\ndefine choose(value: Col"; + CompletionFixture parameterFixture(parameterSource); + const auto parameters = CompletionService( + parameterFixture.projects, parameterFixture.scheduler) + .complete(parameterFixture.uri, {1, 24}); + + EXPECT_NE(findItem(parameters, "Color"), nullptr); + EXPECT_NE(findItem(parameters, "Condition"), nullptr); + EXPECT_NE(findItem(parameters, "Event"), nullptr); + EXPECT_NE(findItem(parameters, "Location"), nullptr); + EXPECT_NE(findItem(parameters, "Region"), nullptr); + EXPECT_EQ(findItem(parameters, "RED"), nullptr); + EXPECT_EQ(parameters.front().label, "Color"); + + const std::string returnSource = + "enum Color { RED }\nextern define choose() -> Col"; + CompletionFixture returnFixture(returnSource); + const auto returns = CompletionService(returnFixture.projects, returnFixture.scheduler) + .complete(returnFixture.uri, {1, 29}); + + EXPECT_NE(findItem(returns, "Color"), nullptr); + EXPECT_NE(findItem(returns, "Bool"), nullptr); + EXPECT_EQ(findItem(returns, "RED"), nullptr); + EXPECT_EQ(returns.front().label, "Color"); +} + +TEST(CompletionServiceTests, UsesScopeAndExpectedEnumForExpressionCandidates) { + CompletionFixture fixture( + "enum Color { RED, BLUE }\n" + "enum Size { SMALL }\n" + "define choose(value: Color): value\n" + "define other(hidden: Bool): hidden\n" + "define use(input: Color): choose(R)\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {4, 34}); + + ASSERT_NE(findItem(items, "RED"), nullptr); + ASSERT_NE(findItem(items, "BLUE"), nullptr); + ASSERT_NE(findItem(items, "input"), nullptr); + ASSERT_NE(findItem(items, "choose"), nullptr); + EXPECT_EQ(findItem(items, "SMALL"), nullptr); + EXPECT_EQ(findItem(items, "hidden"), nullptr); + EXPECT_EQ(findItem(items, "true"), nullptr); + EXPECT_EQ(items.front().label, "RED"); + EXPECT_EQ(items.front().replacementRange.start.character, 33u); + EXPECT_EQ(items.front().replacementRange.end.character, 34u); + EXPECT_EQ(findItem(items, "choose")->detail, + "choose(value: Color) -> Color"); +} + +TEST(CompletionServiceTests, RendersCallableDefaultsAndReturnType) { + const std::string usage = "define use(): tar"; + CompletionFixture fixture( + "extern define target(first: Bool, second: Int = 2) -> Bool\n" + + usage); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {1, static_cast(usage.size())}); + + const auto* target = findItem(items, "target"); + ASSERT_NE(target, nullptr); + EXPECT_EQ(target->detail, + "extern target(first: Bool, second: Int = 2) -> Bool"); + EXPECT_EQ(target->documentation, "*External declaration.*"); +} + +TEST(CompletionServiceTests, RejectsAStaleAcceptedSnapshot) { + CompletionFixture fixture("define check(flag: Bool): flag\n"); + ASSERT_EQ(fixture.projects.documentChanged(fixture.uri), + rls::lsp::ProjectAssignmentResult::Assigned); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {0, 31}); + + EXPECT_TRUE(items.empty()); +} + +TEST(CompletionServiceTests, ExpeditesLatestScheduledDocumentGeneration) { + const fs::path sourcePath = fs::temp_directory_path() / + "rls-immediate-completion.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + DocumentStore documents; + ASSERT_EQ(documents.open(uri, "rls", 1, "def\n"), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {sourcePath}; + project.isStandalone = true; + return project; + }); + ASSERT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + AnalysisScheduler scheduler({ + .debounce = std::chrono::seconds(5), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{sourcePath.generic_string(), "def\n"}}, + project->documentGeneration, + project->manifestGeneration, + })); + ASSERT_NE(scheduler.awaitSnapshot( + project->id, project->generation, std::chrono::seconds(1)), nullptr); + + const std::string changed = "reg\n"; + ASSERT_EQ(documents.applyFullChange(uri, 2, changed), + rls::lsp::DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentChanged(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{sourcePath.generic_string(), changed}}, + project->documentGeneration, + project->manifestGeneration, + })); + + const auto items = CompletionService(projects, scheduler) + .complete(uri, {0, 3}); + + ASSERT_NE(findItem(items, "region"), nullptr); + EXPECT_EQ(items.front().label, "region"); +} + +TEST(CompletionServiceTests, CompletesRecoveredRegionBodyWithoutDuplicates) { + const std::string usage = + "region RR_CURRENT { displayLabel: \"Current\" wo"; + CrossFileCompletionFixture fixture( + "region RR_TEMPLATE { displayLabel: \"Template\" worldNode: true }\n", + usage); + + const auto items = fixture.completeAtEnd(usage); + + ASSERT_NE(findItem(items, "locations"), nullptr); + ASSERT_NE(findItem(items, "worldNode"), nullptr); + EXPECT_EQ(findItem(items, "worldNode")->snippetText, "worldNode: ${1}"); + EXPECT_EQ(findItem(items, "worldNode")->detail, "project region data key"); + EXPECT_EQ(findItem(items, "displayLabel"), nullptr); + EXPECT_EQ(findItem(items, "define"), nullptr); + EXPECT_EQ(items.front().label, "worldNode"); + EXPECT_EQ(items.front().replacementRange.start.character, usage.size() - 2); + EXPECT_EQ(items.front().replacementRange.end.character, usage.size()); +} + +TEST(CompletionServiceTests, FallsBackToSectionsWithoutObservedRegionKeys) { + CompletionFixture fixture( + "region RR_TEST {\n" + " \n" + "}\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {1, 2}); + + ASSERT_EQ(items.size(), 3u); + EXPECT_NE(findItem(items, "events"), nullptr); + EXPECT_NE(findItem(items, "locations"), nullptr); + EXPECT_NE(findItem(items, "exits"), nullptr); + EXPECT_EQ(findItem(items, "events")->snippetText, "events {\n $0\n}"); + EXPECT_EQ(findItem(items, "locations")->snippetText, "locations {\n $0\n}"); + EXPECT_EQ(findItem(items, "exits")->snippetText, "exits {\n $0\n}"); + EXPECT_EQ(findItem(items, "events")->serverIndentedSnippetText, + "events {\n $0\n }"); + EXPECT_EQ(findItem(items, "locations")->serverIndentedSnippetText, + "locations {\n $0\n }"); + EXPECT_EQ(findItem(items, "exits")->serverIndentedSnippetText, + "exits {\n $0\n }"); +} + +TEST(CompletionServiceTests, LimitsExtensionBodiesToMissingSections) { + CompletionFixture fixture( + "extend region RR_TEST {\n" + " events {}\n" + " ex\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {2, 4}); + + ASSERT_NE(findItem(items, "exits"), nullptr); + ASSERT_NE(findItem(items, "locations"), nullptr); + EXPECT_EQ(findItem(items, "events"), nullptr); + EXPECT_EQ(findItem(items, "name"), nullptr); + EXPECT_EQ(items.front().label, "exits"); +} + +TEST(CompletionServiceTests, OffersHereOnlyInRegionExpressions) { + CompletionFixture fixture( + "region RR_TEST { events { EVENT_TEST: tr } }\n" + "define check(): tr\n"); + CompletionService completion(fixture.projects, fixture.scheduler); + + const auto regionItems = completion.complete(fixture.uri, {0, 40}); + const auto defineItems = completion.complete(fixture.uri, {1, 18}); + + ASSERT_NE(findItem(regionItems, "here"), nullptr); + EXPECT_EQ(findItem(regionItems, "here")->detail, "built-in here: Region"); + EXPECT_EQ(findItem(defineItems, "here"), nullptr); +} + +TEST(CompletionServiceTests, CompletesPreviouslyDeclaredSectionEntriesByKind) { + const std::string declarations = + "region RR_TEMPLATE {\n" + " events {\n" + " EVENT_EXISTING: true\n" + " EVENT_OTHER: true\n" + " }\n" + " locations {\n" + " RC_EXISTING: true\n" + " RC_OTHER: true\n" + " }\n" + "}\n"; + const std::string eventUsage = + "region RR_CURRENT {\n" + " events {\n" + " EVENT_EXISTING: true\n" + " EVENT_\n" + " }\n" + "}\n"; + CrossFileCompletionFixture eventFixture(declarations, eventUsage); + + const auto events = eventFixture.complete({3, 10}); + + const auto* event = findItem(events, "EVENT_OTHER"); + ASSERT_NE(event, nullptr); + EXPECT_EQ(event->insertText, "EVENT_OTHER: "); + EXPECT_EQ(event->snippetText, "EVENT_OTHER: ${1}"); + EXPECT_EQ(event->detail, "EVENT_OTHER: Event"); + EXPECT_EQ(findItem(events, "EVENT_EXISTING"), nullptr); + EXPECT_EQ(findItem(events, "RC_OTHER"), nullptr); + + const std::string locationUsage = + "region RR_CURRENT {\n" + " locations {\n" + " RC_EXISTING: true\n" + " \n" + " }\n" + "}\n"; + CrossFileCompletionFixture locationFixture(declarations, locationUsage); + const auto locations = locationFixture.complete({3, 4}); + + const auto* location = findItem(locations, "RC_OTHER"); + ASSERT_NE(location, nullptr); + EXPECT_EQ(location->snippetText, "RC_OTHER: ${1}"); + EXPECT_EQ(location->detail, "RC_OTHER: Location"); + EXPECT_EQ(findItem(locations, "RC_EXISTING"), nullptr); + EXPECT_EQ(findItem(locations, "EVENT_OTHER"), nullptr); +} + +TEST(CompletionServiceTests, CompletesExitLabelsFromDeclaredRegions) { + const std::string declarations = + "region RR_FIRST {}\n" + "region RR_SECOND {}\n" + "region RR_THIRD {}\n"; + const std::string usage = + "region RR_FIRST {\n" + " exits {\n" + " RR_SECOND: true\n" + " RR_\n" + " }\n" + "}\n"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.complete({3, 7}); + + const auto* third = findItem(items, "RR_THIRD"); + ASSERT_NE(third, nullptr); + EXPECT_EQ(third->insertText, "RR_THIRD: "); + EXPECT_EQ(third->snippetText, "RR_THIRD: ${1}"); + EXPECT_EQ(third->detail, "region RR_THIRD"); + EXPECT_EQ(findItem(items, "RR_FIRST"), nullptr); + EXPECT_EQ(findItem(items, "RR_SECOND"), nullptr); +} + +TEST(CompletionServiceTests, SuppressesEntriesFromOtherContributionsToActiveRegion) { + const std::string declarations = + "region RR_CURRENT { events { EVENT_EXISTING: true } }\n" + "region RR_OTHER { events { EVENT_OTHER: true } }\n"; + const std::string usage = + "extend region RR_CURRENT {\n" + " events {\n" + " EVENT_\n" + " }\n" + "}\n"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.complete({2, 10}); + + EXPECT_EQ(findItem(items, "EVENT_EXISTING"), nullptr); + EXPECT_NE(findItem(items, "EVENT_OTHER"), nullptr); +} + +TEST(CompletionServiceTests, RecoversSameFileEventsWhileRecreatingCommentedRegion) { + CompletionFixture fixture( + "region RR_KOKIRI_FOREST {\n" + " events {\n" + " LOGIC_FAIRY_ACCESS: always\n" + " LOGIC_OTHER: true\n" + " }\n" + "}\n" + "# region RR_KF_STORMS_GROTTO {\n" + "# events {\n" + "# LOGIC_FAIRY_ACCESS: true\n" + "# }\n" + "# }\n" + "region RR_KF_STORMS_GROTTO {\n" + " events {\n" + " LO\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {13, 6}); + + ASSERT_NE(findItem(items, "LOGIC_FAIRY_ACCESS"), nullptr); + ASSERT_NE(findItem(items, "LOGIC_OTHER"), nullptr); + EXPECT_EQ(items.front().label, "LOGIC_FAIRY_ACCESS"); +} + +TEST(CompletionServiceTests, RecoversSameFileRegionsForExitCompletion) { + CompletionFixture fixture( + "region RR_FIRST {}\n" + "region RR_SECOND {}\n" + "# region RR_COMMENTED {}\n" + "region RR_CURRENT {\n" + " exits {\n" + " RR_\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {5, 7}); + + EXPECT_NE(findItem(items, "RR_FIRST"), nullptr); + EXPECT_NE(findItem(items, "RR_SECOND"), nullptr); + EXPECT_EQ(findItem(items, "RR_CURRENT"), nullptr); + EXPECT_EQ(findItem(items, "RR_COMMENTED"), nullptr); +} + +TEST(CompletionServiceTests, CompletesOnlyMembersOfQualifiedEnum) { + CompletionFixture fixture( + "enum Alpha { SHARED, ALPHA_ONLY }\n" + "enum Beta { SHARED, BETA_ONLY }\n" + "define check(): Alpha.S\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {2, 23}); + + ASSERT_NE(findItem(items, "SHARED"), nullptr); + ASSERT_NE(findItem(items, "ALPHA_ONLY"), nullptr); + EXPECT_EQ(findItem(items, "BETA_ONLY"), nullptr); + EXPECT_EQ(findItem(items, "Alpha"), nullptr); + EXPECT_EQ(items.front().label, "SHARED"); + EXPECT_EQ(items.front().replacementRange.start.character, 22u); + EXPECT_EQ(items.front().replacementRange.end.character, 23u); +} + +TEST(CompletionServiceTests, ExcludesPatternsAndUnknownEnumFallbacks) { + CompletionFixture fixture( + "extern enum Status { READY, ST_* }\n" + "define known(): Status.R\n" + "define unknown(): Missing.R\n"); + CompletionService completion(fixture.projects, fixture.scheduler); + + const auto known = completion.complete(fixture.uri, {1, 24}); + const auto unknown = completion.complete(fixture.uri, {2, 27}); + + ASSERT_NE(findItem(known, "READY"), nullptr); + EXPECT_EQ(findItem(known, "ST_*"), nullptr); + EXPECT_TRUE(unknown.empty()); +} + +TEST(CompletionServiceTests, CompletesPreviouslyObservedExternPatternValues) { + const std::string declarations = + "extern enum Item { RG_EXPLICIT, RG_* }\n" + "extern enum Status { ST_* }\n" + "extern define has(item: Item) -> Bool\n" + "define seen(): has(RG_HOOKSHOT)\n" + "define seen_qualified(): Item.RG_BOW\n" + "define other(): Status.ST_READY\n"; + const std::string expectedUsage = "define use(): has(RG_"; + CrossFileCompletionFixture expectedFixture(declarations, expectedUsage); + + const auto expectedItems = expectedFixture.completeAtEnd(expectedUsage); + + EXPECT_NE(findItem(expectedItems, "RG_EXPLICIT"), nullptr); + EXPECT_NE(findItem(expectedItems, "RG_HOOKSHOT"), nullptr); + EXPECT_NE(findItem(expectedItems, "RG_BOW"), nullptr); + EXPECT_EQ(findItem(expectedItems, "RG_*"), nullptr); + EXPECT_EQ(findItem(expectedItems, "ST_READY"), nullptr); + + const std::string qualifiedUsage = "define use(): Item.RG_"; + CrossFileCompletionFixture qualifiedFixture(declarations, qualifiedUsage); + const auto qualifiedItems = qualifiedFixture.completeAtEnd(qualifiedUsage); + + EXPECT_NE(findItem(qualifiedItems, "RG_EXPLICIT"), nullptr); + EXPECT_NE(findItem(qualifiedItems, "RG_HOOKSHOT"), nullptr); + EXPECT_NE(findItem(qualifiedItems, "RG_BOW"), nullptr); + EXPECT_EQ(findItem(qualifiedItems, "RG_*"), nullptr); + EXPECT_EQ(findItem(qualifiedItems, "ST_READY"), nullptr); +} + +TEST(CompletionServiceTests, RecoversEmptyMemberAcrossFiles) { + const fs::path root = fs::temp_directory_path() / "rls-member-completion"; + const fs::path declarationPath = root / "declaration.rls"; + const fs::path usagePath = root / "usage.rls"; + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + const std::string usage = "define choose(): Color.\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), "enum Color { RED, BLUE }\n"}, + {usagePath.generic_string(), usage}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + const auto items = CompletionService(projects, scheduler) + .complete(usageUri, {0, 23}); + + ASSERT_NE(findItem(items, "RED"), nullptr); + ASSERT_NE(findItem(items, "BLUE"), nullptr); + EXPECT_EQ(items.front().replacementRange.start.character, 23u); + EXPECT_EQ(items.front().replacementRange.end.character, 23u); +} + +TEST(CompletionServiceTests, CompletesOnlyUnboundNamedArgumentsAcrossFiles) { + const fs::path root = fs::temp_directory_path() / "rls-named-argument-completion"; + const fs::path declarationPath = root / "declaration.rls"; + const fs::path usagePath = root / "usage.rls"; + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + const std::string usage = + "define use(): target(true, third: false, se"; + DocumentStore documents; + ASSERT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), + "extern define target(first: Bool, second: Bool, third: Bool) -> Bool\n"}, + {usagePath.generic_string(), usage}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + const auto items = CompletionService(projects, scheduler) + .complete(usageUri, {0, static_cast(usage.size())}); + + const auto* second = findItem(items, "second"); + ASSERT_NE(second, nullptr); + EXPECT_EQ(second->insertText, "second: "); + EXPECT_EQ(second->snippetText, "second: ${1}"); + EXPECT_EQ(second->detail, "second: Bool"); + EXPECT_EQ(findItem(items, "first"), nullptr); + EXPECT_EQ(findItem(items, "third"), nullptr); + EXPECT_EQ(items.front().label, "second"); + EXPECT_EQ(second->replacementRange.start.character, usage.size() - 2); + EXPECT_EQ(second->replacementRange.end.character, usage.size()); +} + +TEST(CompletionServiceTests, KeepsNestedCallsOutOfOuterArgumentBinding) { + const fs::path root = fs::temp_directory_path() / "rls-nested-label-completion"; + const fs::path declarationPath = root / "declaration.rls"; + const fs::path usagePath = root / "usage.rls"; + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + const std::string usage = "define use(): outer(nested(true), se"; + DocumentStore documents; + ASSERT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), + "extern define nested(value: Bool) -> Bool\n" + "extern define outer(first: Bool, second: Bool) -> Bool\n"}, + {usagePath.generic_string(), usage}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + const auto items = CompletionService(projects, scheduler) + .complete(usageUri, {0, static_cast(usage.size())}); + + ASSERT_NE(findItem(items, "second"), nullptr); + EXPECT_EQ(findItem(items, "first"), nullptr); +} + +TEST(CompletionServiceTests, DoesNotFabricateLabelsForUnknownCallee) { + CompletionFixture fixture("define use(): missing(arg\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {0, 25}); + + EXPECT_EQ(findItem(items, "arg"), nullptr); +} + +TEST(CompletionServiceTests, DoesNotTreatDeclarationParametersAsArguments) { + CompletionFixture fixture( + "extern define target(first: Bool, second: Bool) -> Bool\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {0, 26}); + + EXPECT_TRUE(items.empty()); +} + +TEST(CompletionServiceTests, CompletesLabelsInParsedCalls) { + CompletionFixture fixture( + "extern define target(first: Bool, second: Bool) -> Bool\n" + "define use(): target(first: true, se: false)\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {1, 36}); + + ASSERT_NE(findItem(items, "second"), nullptr); + EXPECT_EQ(findItem(items, "first"), nullptr); + EXPECT_EQ(findItem(items, "second")->insertText, "second: "); +} + +TEST(CompletionServiceTests, KeepsCandidatesBeforeLaterPositionalArguments) { + CompletionFixture fixture( + "extern define target(first: Bool, second: Bool) -> Bool\n" + "define use(): target(fi, true)\n"); + + const auto items = CompletionService(fixture.projects, fixture.scheduler) + .complete(fixture.uri, {1, 23}); + + ASSERT_NE(findItem(items, "first"), nullptr); + ASSERT_NE(findItem(items, "second"), nullptr); + EXPECT_EQ(items.front().label, "first"); +} + +TEST(CompletionServiceTests, FiltersRecoveredPositionalAndNamedEnumValues) { + const std::string declarations = + "enum Color { RED, BLUE }\n" + "enum Size { SMALL }\n" + "extern define paint(color: Color) -> Bool\n"; + const std::string positionalUsage = "define use(): paint(R"; + CrossFileCompletionFixture positional(declarations, positionalUsage); + + const auto positionalItems = positional.completeAtEnd(positionalUsage); + + ASSERT_NE(findItem(positionalItems, "RED"), nullptr); + ASSERT_NE(findItem(positionalItems, "BLUE"), nullptr); + EXPECT_EQ(findItem(positionalItems, "SMALL"), nullptr); + EXPECT_EQ(findItem(positionalItems, "true"), nullptr); + EXPECT_EQ(positionalItems.front().label, "RED"); + EXPECT_EQ(positionalItems.front().replacementRange.start.character, + positionalUsage.size() - 1); + + const std::string emptyUsage = "define use(): paint("; + CrossFileCompletionFixture empty(declarations, emptyUsage); + const auto emptyItems = empty.completeAtEnd(emptyUsage); + + ASSERT_NE(findItem(emptyItems, "RED"), nullptr); + ASSERT_NE(findItem(emptyItems, "BLUE"), nullptr); + EXPECT_EQ(findItem(emptyItems, "SMALL"), nullptr); + EXPECT_EQ(findItem(emptyItems, "RED")->replacementRange.start.character, + emptyUsage.size()); + EXPECT_EQ(findItem(emptyItems, "RED")->replacementRange.end.character, + emptyUsage.size()); + + const std::string namedUsage = "define use(): paint(color: B"; + CrossFileCompletionFixture named(declarations, namedUsage); + const auto namedItems = named.completeAtEnd(namedUsage); + + ASSERT_NE(findItem(namedItems, "BLUE"), nullptr); + ASSERT_NE(findItem(namedItems, "RED"), nullptr); + EXPECT_EQ(findItem(namedItems, "SMALL"), nullptr); + EXPECT_EQ(namedItems.front().label, "BLUE"); +} + +TEST(CompletionServiceTests, ReplaysBindingsForRecoveredBooleanValues) { + const std::string declarations = + "enum Color { RED }\n" + "extern define target(first: Color, second: Bool, third: Bool) -> Bool\n"; + const std::string usage = "define use(): target(RED, third: false, tr"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.completeAtEnd(usage); + + ASSERT_NE(findItem(items, "true"), nullptr); + ASSERT_NE(findItem(items, "false"), nullptr); + EXPECT_EQ(findItem(items, "RED"), nullptr); + EXPECT_EQ(findItem(items, "third"), nullptr); + EXPECT_EQ(items.front().label, "true"); +} + +TEST(CompletionServiceTests, UsesInnermostRecoveredCallExpectedType) { + const std::string declarations = + "enum Color { RED, BLUE }\n" + "extern define nested(value: Color) -> Bool\n" + "extern define outer(flag: Bool, result: Bool) -> Bool\n"; + const std::string usage = "define use(): outer(true, nested(R"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.completeAtEnd(usage); + + ASSERT_NE(findItem(items, "RED"), nullptr); + ASSERT_NE(findItem(items, "BLUE"), nullptr); + EXPECT_EQ(findItem(items, "true"), nullptr); +} + +TEST(CompletionServiceTests, DoesNotInventExpectedTypeForUnknownCall) { + const std::string declarations = "enum Color { RED }\n"; + const std::string usage = "define use(): missing(R"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.completeAtEnd(usage); + + EXPECT_EQ(findItem(items, "RED"), nullptr); +} + +TEST(CompletionServiceTests, DoesNotResolveAmbiguousRecoveredCall) { + const std::string declarations = + "enum Color { RED }\n" + "enum Size { SMALL }\n" + "extern define paint(value: Color) -> Bool\n" + "extern define paint(value: Size) -> Bool\n"; + const std::string usage = "define use(): paint(R"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.completeAtEnd(usage); + + EXPECT_EQ(findItem(items, "RED"), nullptr); + EXPECT_EQ(findItem(items, "SMALL"), nullptr); +} + +TEST(CompletionServiceTests, DoesNotResolveInvalidRecoveredArgumentBinding) { + const std::string declarations = + "enum Color { RED }\n" + "extern define paint(color: Color) -> Bool\n"; + const std::string usage = "define use(): paint(missing: R"; + CrossFileCompletionFixture fixture(declarations, usage); + + const auto items = fixture.completeAtEnd(usage); + + EXPECT_EQ(findItem(items, "RED"), nullptr); + EXPECT_EQ(findItem(items, "color"), nullptr); +} + +TEST(CompletionServiceTests, FiltersCrossFileDeclaredDomainValuesByExpectedType) { + const std::string declarations = + "region RR_FIRST {\n" + " events { EVENT_FIRST: true }\n" + " locations { RC_FIRST: true }\n" + "}\n" + "region RR_SECOND {\n" + " events { EVENT_SECOND: true }\n" + " locations { RC_SECOND: true }\n" + "}\n" + "extern define use_values(reg: Region, evt: Event, loc: Location) -> Bool\n"; + + const std::string regionUsage = "define use(): use_values(RR_"; + CrossFileCompletionFixture regionFixture(declarations, regionUsage); + const auto regions = regionFixture.completeAtEnd(regionUsage); + EXPECT_NE(findItem(regions, "RR_FIRST"), nullptr); + EXPECT_NE(findItem(regions, "RR_SECOND"), nullptr); + EXPECT_EQ(findItem(regions, "EVENT_FIRST"), nullptr); + EXPECT_EQ(findItem(regions, "RC_FIRST"), nullptr); + + const std::string eventUsage = "define use(): use_values(RR_FIRST, EVENT_"; + CrossFileCompletionFixture eventFixture(declarations, eventUsage); + const auto events = eventFixture.completeAtEnd(eventUsage); + EXPECT_NE(findItem(events, "EVENT_FIRST"), nullptr); + EXPECT_NE(findItem(events, "EVENT_SECOND"), nullptr); + EXPECT_EQ(findItem(events, "RR_FIRST"), nullptr); + EXPECT_EQ(findItem(events, "RC_FIRST"), nullptr); + + const std::string locationUsage = + "define use(): use_values(RR_FIRST, EVENT_FIRST, RC_"; + CrossFileCompletionFixture locationFixture(declarations, locationUsage); + const auto locations = locationFixture.completeAtEnd(locationUsage); + EXPECT_NE(findItem(locations, "RC_FIRST"), nullptr); + EXPECT_NE(findItem(locations, "RC_SECOND"), nullptr); + EXPECT_EQ(findItem(locations, "RR_FIRST"), nullptr); + EXPECT_EQ(findItem(locations, "EVENT_FIRST"), nullptr); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/diagnostic_publisher_tests.cpp b/lsp/tests/diagnostic_publisher_tests.cpp new file mode 100644 index 0000000..60d2590 --- /dev/null +++ b/lsp/tests/diagnostic_publisher_tests.cpp @@ -0,0 +1,343 @@ +#include +#include +#include +#include +#include + +#include +#include + +#include "analysis_snapshot.h" +#include "rls/lsp/diagnostic_publisher.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/outbound_message_queue.h" + +namespace fs = std::filesystem; + +namespace { + +using Json = nlohmann::json; +using rls::lsp::DiagnosticPublisher; +using rls::lsp::OutboundMessageQueue; + +class TemporaryDirectory { +public: + TemporaryDirectory() : path_(fs::temp_directory_path() / + ("rls-lsp-diagnostics-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()))) { + fs::create_directories(path_); + } + + ~TemporaryDirectory() { + std::error_code error; + fs::remove_all(path_, error); + } + + const fs::path& path() const { return path_; } + +private: + fs::path path_; +}; + +std::string genericPath(const fs::path& path) { + const auto generic = fs::weakly_canonical(path).generic_u8string(); + std::string value; + value.reserve(generic.size()); + for (const char8_t byte : generic) value.push_back(static_cast(byte)); + return value; +} + +std::shared_ptr snapshot( + const fs::path& path, std::string content, uint64_t generation) { + const auto result = rls::sema::AnalysisSnapshot::Create({ + {genericPath(path), std::move(content)}, + }, generation); + EXPECT_TRUE(result.has_value()); + return result ? *result : nullptr; +} + +const Json* findDiagnostic(const Json& notification, std::string_view code) { + for (const auto& diagnostic : notification["params"]["diagnostics"]) { + if (diagnostic.value("code", "") == code) return &diagnostic; + } + return nullptr; +} + +TEST(DiagnosticPublisherTests, PublishesUtf16RangesCodesSeverityAndRelatedInformation) { + TemporaryDirectory directory; + const fs::path path = directory.path() / "main.rls"; + std::ofstream(path) << "placeholder"; + const std::string content = + "region RR_TEST { name: \"\xF0\x9F\x98\x80\" name: \"Second\" }\n"; + const auto analyzed = snapshot(path, content, 1); + ASSERT_NE(analyzed, nullptr); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.acceptedSnapshot("project", analyzed); + + const auto payload = outbound.tryPop(); + ASSERT_TRUE(payload.has_value()); + const Json notification = Json::parse(*payload); + EXPECT_EQ(notification["method"], "textDocument/publishDiagnostics"); + EXPECT_EQ(notification["params"]["uri"], *rls::lsp::PathToFileUri(path)); + + const Json* diagnostic = findDiagnostic(notification, "RLS-V002"); + ASSERT_NE(diagnostic, nullptr); + EXPECT_EQ((*diagnostic)["severity"], 1); + EXPECT_EQ((*diagnostic)["source"], "rls"); + ASSERT_TRUE(diagnostic->contains("relatedInformation")); + EXPECT_EQ((*diagnostic)["relatedInformation"][0]["message"], "first definition"); + + const auto compilerDiagnostic = analyzed->diagnosticsFor(genericPath(path)); + const auto duplicate = std::find_if( + compilerDiagnostic.begin(), compilerDiagnostic.end(), [](const auto& value) { + return value.code == "RLS-V002"; + }); + ASSERT_NE(duplicate, compilerDiagnostic.end()); + EXPECT_LT((*diagnostic)["range"]["start"]["character"].get(), + duplicate->span.start.column - 1); +} + +TEST(DiagnosticPublisherTests, PreservesOpenedUriForCanonicalSnapshotPath) { + TemporaryDirectory directory; + const fs::path path = directory.path() / "main.rls"; + fs::create_directory(directory.path() / "alias"); + std::ofstream(path) << "placeholder"; + const std::string canonicalUri = *rls::lsp::PathToFileUri(path); + const size_t filename = canonicalUri.rfind("main.rls"); + ASSERT_NE(filename, std::string::npos); + const std::string openedUri = canonicalUri.substr(0, filename) + + "alias/../main.rls"; + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.documentOpened(openedUri); + publisher.acceptedSnapshot( + "project", snapshot(path, "define broken(): missing\n", 1)); + + const auto payload = outbound.tryPop(); + ASSERT_TRUE(payload.has_value()); + const Json notification = Json::parse(*payload); + EXPECT_EQ(notification["params"]["uri"], openedUri); + EXPECT_NE(findDiagnostic(notification, "RLS-T006"), nullptr); +} + +TEST(DiagnosticPublisherTests, PublishesOnlyChangesAndClearsResolvedDiagnostics) { + TemporaryDirectory directory; + const fs::path path = directory.path() / "main.rls"; + std::ofstream(path) << "placeholder"; + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + const auto broken = snapshot(path, + "region RR_TEST { events { EVENT_TEST: \"invalid\" } }\n", 1); + + publisher.acceptedSnapshot("project", broken); + ASSERT_TRUE(outbound.tryPop().has_value()); + publisher.acceptedSnapshot("project", broken); + EXPECT_FALSE(outbound.tryPop().has_value()); + + publisher.acceptedSnapshot("project", snapshot(path, + "region RR_TEST { events { EVENT_TEST: true } }\n", 2)); + const auto clearPayload = outbound.tryPop(); + ASSERT_TRUE(clearPayload.has_value()); + const Json clear = Json::parse(*clearPayload); + EXPECT_TRUE(clear["params"]["diagnostics"].empty()); +} + +TEST(DiagnosticPublisherTests, ClearsAndSuppressesClosedStandaloneDocuments) { + TemporaryDirectory directory; + const fs::path path = directory.path() / "main.rls"; + std::ofstream(path) << "placeholder"; + const std::string uri = *rls::lsp::PathToFileUri(path); + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + const auto broken = snapshot(path, + "region RR_TEST { events { EVENT_TEST: \"invalid\" } }\n", 1); + + publisher.acceptedSnapshot("project", broken); + ASSERT_TRUE(outbound.tryPop().has_value()); + publisher.documentClosed(uri, true); + const auto clearPayload = outbound.tryPop(); + ASSERT_TRUE(clearPayload.has_value()); + EXPECT_TRUE(Json::parse(*clearPayload)["params"]["diagnostics"].empty()); + + publisher.acceptedSnapshot("project", broken); + EXPECT_FALSE(outbound.tryPop().has_value()); + publisher.documentOpened(uri); + publisher.acceptedSnapshot("project", broken); + EXPECT_TRUE(outbound.tryPop().has_value()); +} + +TEST(DiagnosticPublisherTests, PublishesParserDiagnosticsWithFallbackRanges) { + TemporaryDirectory directory; + const fs::path path = directory.path() / "main.rls"; + std::ofstream(path) << "placeholder"; + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + + publisher.acceptedSnapshot("project", snapshot(path, "define broken(\n", 1)); + const auto payload = outbound.tryPop(); + ASSERT_TRUE(payload.has_value()); + const Json diagnostics = Json::parse(*payload)["params"]["diagnostics"]; + ASSERT_FALSE(diagnostics.empty()); + EXPECT_EQ(diagnostics[0]["severity"], 1); + EXPECT_TRUE(diagnostics[0].contains("range")); +} + +TEST(DiagnosticPublisherTests, ClearsDocumentsRemovedFromAcceptedProjectSnapshot) { + TemporaryDirectory directory; + const fs::path firstPath = directory.path() / "first.rls"; + const fs::path secondPath = directory.path() / "second.rls"; + std::ofstream(firstPath) << "placeholder"; + std::ofstream(secondPath) << "placeholder"; + const auto firstSnapshot = rls::sema::AnalysisSnapshot::Create({ + {genericPath(firstPath), "define first(): missing\n"}, + {genericPath(secondPath), "define second(): missing\n"}, + }, 1); + ASSERT_TRUE(firstSnapshot.has_value()); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.acceptedSnapshot("project", *firstSnapshot); + ASSERT_TRUE(outbound.tryPop().has_value()); + ASSERT_TRUE(outbound.tryPop().has_value()); + + publisher.acceptedSnapshot("project", snapshot(firstPath, + "define first(): missing\n", 2)); + const auto clearPayload = outbound.tryPop(); + ASSERT_TRUE(clearPayload.has_value()); + const Json clear = Json::parse(*clearPayload); + EXPECT_EQ(clear["params"]["uri"], *rls::lsp::PathToFileUri(secondPath)); + EXPECT_TRUE(clear["params"]["diagnostics"].empty()); +} + +TEST(DiagnosticPublisherTests, PublishesCrossFileRelatedDeclarationLocations) { + TemporaryDirectory directory; + const fs::path firstPath = directory.path() / "first.rls"; + const fs::path secondPath = directory.path() / "second.rls"; + std::ofstream(firstPath) << "placeholder"; + std::ofstream(secondPath) << "placeholder"; + const auto analyzed = rls::sema::AnalysisSnapshot::Create({ + {genericPath(firstPath), "region RR_DUP { name: \"First\" }\n"}, + {genericPath(secondPath), "region RR_DUP { name: \"Second\" }\n"}, + }, 1); + ASSERT_TRUE(analyzed.has_value()); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.acceptedSnapshot("project", *analyzed); + + const auto firstPayload = outbound.tryPop(); + const auto secondPayload = outbound.tryPop(); + ASSERT_TRUE(firstPayload.has_value()); + ASSERT_TRUE(secondPayload.has_value()); + const Json first = Json::parse(*firstPayload); + const Json second = Json::parse(*secondPayload); + const Json* duplicate = findDiagnostic(first, "RLS-S001"); + if (!duplicate) duplicate = findDiagnostic(second, "RLS-S001"); + ASSERT_NE(duplicate, nullptr); + ASSERT_TRUE(duplicate->contains("relatedInformation")); + EXPECT_EQ((*duplicate)["relatedInformation"][0]["location"]["uri"], + *rls::lsp::PathToFileUri(firstPath)); +} + +TEST(DiagnosticPublisherTests, PublishesAndClearsManifestConfigurationDiagnostics) { + TemporaryDirectory directory; + const fs::path manifestPath = directory.path() / "rls.json"; + const std::string content = "{\"name\":\"\xF0\x9F\x98\x80\", invalid}"; + std::ofstream(manifestPath, std::ios::binary) << content; + const auto loaded = rls::project::LoadManifest(manifestPath); + ASSERT_EQ(loaded.diagnostics.size(), 1); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.publishConfigurationDiagnostics(loaded.diagnostics); + const auto payload = outbound.tryPop(); + ASSERT_TRUE(payload.has_value()); + const Json notification = Json::parse(*payload); + ASSERT_EQ(notification["params"]["diagnostics"].size(), 1); + const Json& diagnostic = notification["params"]["diagnostics"][0]; + EXPECT_EQ(notification["params"]["uri"], *rls::lsp::PathToFileUri(manifestPath)); + EXPECT_EQ(diagnostic["code"], "RLS-C002"); + EXPECT_EQ(diagnostic["severity"], 1); + EXPECT_EQ(diagnostic["source"], "rls"); + EXPECT_LT(diagnostic["range"]["start"]["character"].get(), + loaded.diagnostics[0].startByte); + + publisher.publishConfigurationDiagnostics({}); + const auto clearPayload = outbound.tryPop(); + ASSERT_TRUE(clearPayload.has_value()); + EXPECT_TRUE(Json::parse(*clearPayload)["params"]["diagnostics"].empty()); +} + +TEST(DiagnosticPublisherTests, KeepsMultipleManifestDiagnosticsIsolated) { + TemporaryDirectory first; + TemporaryDirectory second; + const fs::path firstManifest = first.path() / "rls.json"; + const fs::path secondManifest = second.path() / "rls.json"; + std::ofstream(firstManifest) << "{ invalid"; + std::ofstream(secondManifest) << "{ invalid"; + const auto firstLoad = rls::project::LoadManifest(firstManifest); + const auto secondLoad = rls::project::LoadManifest(secondManifest); + std::vector both = firstLoad.diagnostics; + both.insert(both.end(), secondLoad.diagnostics.begin(), secondLoad.diagnostics.end()); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.publishConfigurationDiagnostics(both); + ASSERT_TRUE(outbound.tryPop().has_value()); + ASSERT_TRUE(outbound.tryPop().has_value()); + + publisher.publishConfigurationDiagnostics(secondLoad.diagnostics); + const auto clearPayload = outbound.tryPop(); + ASSERT_TRUE(clearPayload.has_value()); + const Json clear = Json::parse(*clearPayload); + EXPECT_EQ(clear["params"]["uri"], *rls::lsp::PathToFileUri(firstManifest)); + EXPECT_TRUE(clear["params"]["diagnostics"].empty()); + EXPECT_FALSE(outbound.tryPop().has_value()); +} + +TEST(DiagnosticPublisherTests, PreservesStructuredCompilerActionData) { + TemporaryDirectory directory; + const fs::path path = directory.path() / "main.rls"; + std::ofstream(path) << "placeholder"; + const auto analyzed = snapshot(path, "define broken(): missing\n", 1); + ASSERT_NE(analyzed, nullptr); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.acceptedSnapshot("project", analyzed); + const auto payload = outbound.tryPop(); + ASSERT_TRUE(payload.has_value()); + const Json notification = Json::parse(*payload); + const Json* diagnostic = findDiagnostic(notification, "RLS-T006"); + ASSERT_NE(diagnostic, nullptr); + ASSERT_TRUE(diagnostic->contains("data")); + EXPECT_EQ((*diagnostic)["data"]["version"], 1); + EXPECT_EQ((*diagnostic)["data"]["actionKind"], "rls.declareSymbol"); + ASSERT_EQ((*diagnostic)["data"]["arguments"].size(), 1); + EXPECT_EQ((*diagnostic)["data"]["arguments"][0], "missing"); +} + +TEST(DiagnosticPublisherTests, PreservesStructuredConfigurationActionData) { + TemporaryDirectory directory; + const fs::path manifestPath = directory.path() / "rls.json"; + std::ofstream(manifestPath) << "{ invalid"; + const auto loaded = rls::project::LoadManifest(manifestPath); + ASSERT_EQ(loaded.diagnostics.size(), 1); + ASSERT_TRUE(loaded.diagnostics[0].data.has_value()); + + OutboundMessageQueue outbound; + DiagnosticPublisher publisher(outbound); + publisher.publishConfigurationDiagnostics(loaded.diagnostics); + const auto payload = outbound.tryPop(); + ASSERT_TRUE(payload.has_value()); + const Json diagnostic = Json::parse(*payload)["params"]["diagnostics"][0]; + EXPECT_EQ(diagnostic["data"]["version"], 1); + EXPECT_EQ(diagnostic["data"]["actionKind"], "rls.fixManifestJson"); + ASSERT_EQ(diagnostic["data"]["arguments"].size(), 1); + EXPECT_FALSE(diagnostic["data"]["arguments"][0].get().empty()); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/document_store_tests.cpp b/lsp/tests/document_store_tests.cpp new file mode 100644 index 0000000..08d770d --- /dev/null +++ b/lsp/tests/document_store_tests.cpp @@ -0,0 +1,92 @@ +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::DocumentStore; +using rls::lsp::DocumentUpdateResult; +using rls::lsp::FileUriToPath; +using rls::lsp::NormalizeDocumentUri; +using rls::lsp::PathToFileUri; + +TEST(DocumentUriTests, NormalizesSchemeEscapesAndLocalhost) { + EXPECT_EQ(NormalizeDocumentUri("FILE:///Logic%2fMain%2Erls"), + "file:///Logic%2FMain.rls"); + EXPECT_EQ(NormalizeDocumentUri("file://localhost/work/main.rls"), + "file:///work/main.rls"); + EXPECT_EQ(NormalizeDocumentUri("file://SERVER/share/main.rls"), + "file://server/share/main.rls"); +} + +TEST(DocumentUriTests, RejectsMalformedUris) { + EXPECT_FALSE(NormalizeDocumentUri("C:\\logic\\main.rls").has_value()); + EXPECT_FALSE(NormalizeDocumentUri("file:///bad%2").has_value()); + EXPECT_FALSE(NormalizeDocumentUri("file://relative").has_value()); +} + +TEST(DocumentUriTests, ConvertsEscapedFileUrisToPaths) { +#ifdef _WIN32 + EXPECT_EQ(FileUriToPath("file:///C:/logic/My%20File.rls"), + std::filesystem::path("C:/logic/My File.rls")); + EXPECT_EQ(FileUriToPath("file://server/share/main.rls"), + std::filesystem::path("//server/share/main.rls")); +#else + EXPECT_EQ(FileUriToPath("file:///logic/My%20File.rls"), + std::filesystem::path("/logic/My File.rls")); +#endif + EXPECT_FALSE(FileUriToPath("https://example.com/main.rls").has_value()); + EXPECT_FALSE(FileUriToPath("file:///logic/bad%C3%28.rls").has_value()); +} + +TEST(DocumentUriTests, RoundTripsFilesystemPathsThroughFileUris) { + const fs::path path = fs::temp_directory_path() / "RLS URI" / "main.rls"; + const auto uri = PathToFileUri(path); + ASSERT_TRUE(uri.has_value()); + const auto roundTrip = FileUriToPath(*uri); + ASSERT_TRUE(roundTrip.has_value()); + EXPECT_EQ(*roundTrip, fs::weakly_canonical(path)); +} + +TEST(DocumentStoreTests, StoresDocumentsUnderNormalizedUris) { + DocumentStore store; + EXPECT_EQ(store.open("FILE:///work/My%2Erls", "rls", 1, "old"), + DocumentUpdateResult::Applied); + + const auto* document = store.find("file:///work/My.rls"); + ASSERT_NE(document, nullptr); + EXPECT_EQ(document->uri, "file:///work/My.rls"); + EXPECT_EQ(document->text, "old"); +} + +TEST(DocumentStoreTests, RequiresStrictlyIncreasingVersions) { + DocumentStore store; + ASSERT_EQ(store.open("file:///work/main.rls", "rls", 3, "current"), + DocumentUpdateResult::Applied); + + EXPECT_EQ(store.applyFullChange("file:///work/main.rls", 3, "same"), + DocumentUpdateResult::StaleVersion); + EXPECT_EQ(store.applyFullChange("file:///work/main.rls", 2, "older"), + DocumentUpdateResult::StaleVersion); + EXPECT_EQ(store.applyFullChange("file:///work/main.rls", 4, "newer"), + DocumentUpdateResult::Applied); + EXPECT_EQ(store.find("file:///work/main.rls")->text, "newer"); +} + +TEST(DocumentStoreTests, ReportsInvalidUnknownAndClosedDocuments) { + DocumentStore store; + EXPECT_EQ(store.open("not a uri", "rls", 1, "text"), + DocumentUpdateResult::InvalidUri); + EXPECT_EQ(store.applyFullChange("file:///missing.rls", 2, "text"), + DocumentUpdateResult::NotOpen); + + ASSERT_EQ(store.open("file:///open.rls", "rls", 1, "text"), + DocumentUpdateResult::Applied); + EXPECT_TRUE(store.close("file:///open.rls")); + EXPECT_EQ(store.size(), 0); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/document_synchronization_service_tests.cpp b/lsp/tests/document_synchronization_service_tests.cpp new file mode 100644 index 0000000..18528d8 --- /dev/null +++ b/lsp/tests/document_synchronization_service_tests.cpp @@ -0,0 +1,174 @@ +#include +#include +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_synchronization_service.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/project_manager.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::DiagnosticPublisher; +using rls::lsp::DocumentStore; +using rls::lsp::DocumentSynchronizationResult; +using rls::lsp::DocumentSynchronizationService; +using rls::lsp::LifecycleService; +using rls::lsp::OutboundMessageQueue; +using rls::lsp::ProjectManager; + +class TemporaryDirectory { +public: + TemporaryDirectory() : path_(fs::temp_directory_path() / + ("rls-lsp-document-sync-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()))) { + fs::create_directories(path_); + } + + ~TemporaryDirectory() { + std::error_code error; + fs::remove_all(path_, error); + } + + const fs::path& path() const { return path_; } + +private: + fs::path path_; +}; + +void writeFile(const fs::path& path, const std::string& content) { + std::ofstream(path, std::ios::binary) << content; +} + +std::string fileUri(const fs::path& path) { + const std::string generic = fs::weakly_canonical(path).generic_string(); +#ifdef _WIN32 + return "file:///" + generic; +#else + return "file://" + generic; +#endif +} + +struct Services { + OutboundMessageQueue outbound; + DocumentStore documents; + ProjectManager projects{documents}; + LifecycleService lifecycle; + DiagnosticPublisher diagnostics{outbound}; + AnalysisScheduler scheduler{{ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }}; + DocumentSynchronizationService synchronization{ + lifecycle, documents, projects, scheduler, diagnostics}; + + void start() { + lifecycle.initialize(); + lifecycle.initialized(); + } +}; + +TEST(DocumentSynchronizationServiceTests, RejectsUpdatesUntilInitialized) { + Services services; + + EXPECT_EQ(services.synchronization.open( + "file:///early.rls", "rls", 1, "early"), + DocumentSynchronizationResult::NotReady); + EXPECT_EQ(services.documents.size(), 0); +} + +TEST(DocumentSynchronizationServiceTests, RejectsStaleChangesWithoutAdvancingProject) { + TemporaryDirectory directory; + const fs::path sourcePath = directory.path() / "main.rls"; + writeFile(sourcePath, "disk\n"); + const std::string uri = fileUri(sourcePath); + Services services; + services.start(); + ASSERT_EQ(services.synchronization.open(uri, "rls", 3, "current\n"), + DocumentSynchronizationResult::Applied); + const uint64_t generation = services.projects.projectForDocument(uri)->generation; + + EXPECT_EQ(services.synchronization.change(uri, 3, "stale\n"), + DocumentSynchronizationResult::StaleVersion); + EXPECT_EQ(services.documents.find(uri)->text, "current\n"); + EXPECT_EQ(services.projects.projectForDocument(uri)->generation, generation); +} + +TEST(DocumentSynchronizationServiceTests, FailedProjectResolutionKeepsOverlayStandalone) { + TemporaryDirectory directory; + const fs::path missingPath = directory.path() / "missing.rls"; + const std::string uri = fileUri(missingPath); + Services services; + services.start(); + + EXPECT_EQ(services.synchronization.open(uri, "rls", 1, "overlay\n"), + DocumentSynchronizationResult::Applied); + ASSERT_NE(services.documents.find(uri), nullptr); + ASSERT_NE(services.projects.projectForDocument(uri), nullptr); + EXPECT_TRUE(services.projects.projectForDocument(uri)->isStandalone); + services.scheduler.waitForIdle(); + const auto snapshot = services.scheduler.acceptedSnapshot( + services.projects.projectForDocument(uri)->id); + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->sourceText(fs::weakly_canonical(missingPath).generic_string())->content(), + "overlay\n"); +} + +TEST(DocumentSynchronizationServiceTests, ClosingOverlayRestoresDiskSource) { + TemporaryDirectory directory; + const fs::path sourcePath = directory.path() / "main.rls"; + writeFile(sourcePath, "disk\n"); + const std::string uri = fileUri(sourcePath); + Services services; + services.start(); + ASSERT_EQ(services.synchronization.open(uri, "rls", 1, "overlay\n"), + DocumentSynchronizationResult::Applied); + + ASSERT_EQ(services.synchronization.close(uri), DocumentSynchronizationResult::Applied); + const auto sourceSet = services.projects.sourceSetForDocument(uri); + ASSERT_TRUE(sourceSet.error.empty()) << sourceSet.error; + ASSERT_EQ(sourceSet.sources.size(), 1); + EXPECT_FALSE(sourceSet.sources.front().content.has_value()); + services.scheduler.waitForIdle(); + const auto snapshot = services.scheduler.acceptedSnapshot( + services.projects.projectForDocument(uri)->id); + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->sourceText(fs::weakly_canonical(sourcePath).generic_string())->content(), + "disk\n"); +} + +TEST(DocumentSynchronizationServiceTests, AcceptedChangesScheduleLatestGeneration) { + TemporaryDirectory directory; + const fs::path sourcePath = directory.path() / "main.rls"; + writeFile(sourcePath, "define disk(): true\n"); + const std::string uri = fileUri(sourcePath); + Services services; + services.start(); + ASSERT_EQ(services.synchronization.open( + uri, "rls", 1, "define open(): true\n"), + DocumentSynchronizationResult::Applied); + ASSERT_EQ(services.synchronization.change( + uri, 2, "define changed(): true\n"), + DocumentSynchronizationResult::Applied); + + const auto* project = services.projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + const uint64_t expectedGeneration = project->generation; + const std::string projectId = project->id; + services.scheduler.waitForIdle(); + + const auto snapshot = services.scheduler.acceptedSnapshot(projectId); + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->generation(), expectedGeneration); + ASSERT_EQ(snapshot->documentCount(), 1); + EXPECT_EQ(snapshot->sourceText(fs::weakly_canonical(sourcePath).generic_string())->content(), + "define changed(): true\n"); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/hover_service_tests.cpp b/lsp/tests/hover_service_tests.cpp new file mode 100644 index 0000000..71c5eb3 --- /dev/null +++ b/lsp/tests/hover_service_tests.cpp @@ -0,0 +1,183 @@ +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/hover_service.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::DocumentStore; +using rls::lsp::HoverService; +using rls::lsp::PresentationPosition; +using rls::lsp::ProjectManager; + +struct HoverFixture { + fs::path root = fs::temp_directory_path() / "rls-hover-service"; + fs::path declarationPath = root / "declarations.rls"; + fs::path usagePath = root / "usage.rls"; + std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + DocumentStore documents; + ProjectManager projects; + AnalysisScheduler scheduler; + + HoverFixture(std::string declarations, std::string usage) + : projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }), + scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }) { + EXPECT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + EXPECT_NE(project, nullptr); + if (!project) return; + EXPECT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), std::move(declarations)}, + {usagePath.generic_string(), std::move(usage)}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + } + + std::optional at(PresentationPosition position) { + return HoverService(projects, scheduler).hover(usageUri, position); + } +}; + +TEST(HoverServiceTests, RendersCallSignatureDefaultsProvenanceAndLocation) { + const std::string declarations = + "enum Color { RED }\n" + "extern define paint(color: Color = RED) -> Bool\n"; + const std::string usage = "define use(input: Color): paint(input)\n"; + HoverFixture fixture(declarations, usage); + + const auto result = fixture.at({0, static_cast(usage.find("paint") + 2)}); + + ASSERT_TRUE(result); + EXPECT_NE(result->markdown.find( + "```rls\nextern paint(color: Color = RED) -> Bool\n```"), + std::string::npos); + EXPECT_NE(result->markdown.find("External function declaration."), std::string::npos); + EXPECT_NE(result->markdown.find("*External declaration.*"), std::string::npos); + EXPECT_NE(result->markdown.find("[Open declaration](file:"), std::string::npos); + EXPECT_EQ(result->range.start.line, 0u); + EXPECT_EQ(result->range.start.character, usage.find("paint")); + EXPECT_EQ(result->range.end.character, usage.find("paint") + 5); +} + +TEST(HoverServiceTests, SupportsParameterUsesAndEnumMemberExpressions) { + const std::string declarations = "enum Color { RED }\n"; + const std::string usage = + "define use(input: Color): input == Color.RED\n"; + HoverFixture fixture(declarations, usage); + + const auto parameter = fixture.at({ + 0, static_cast(usage.find("input ==") + 2)}); + ASSERT_TRUE(parameter); + EXPECT_NE(parameter->markdown.find("input: Color"), std::string::npos); + EXPECT_NE(parameter->markdown.find("Parameter of `use`."), std::string::npos); + + const auto enumType = fixture.at({ + 0, static_cast(usage.find("Color.RED") + 2)}); + ASSERT_TRUE(enumType); + EXPECT_NE(enumType->markdown.find("enum Color"), std::string::npos); + + const auto member = fixture.at({ + 0, static_cast(usage.find("Color.RED") + 7)}); + ASSERT_TRUE(member); + EXPECT_NE(member->markdown.find("RED: Color"), std::string::npos); + EXPECT_NE(member->markdown.find("Member of enum `Color`."), std::string::npos); +} + +TEST(HoverServiceTests, DescribesWildcardMatchedEnumValuesAndPatternDeclaration) { + const std::string declarations = "extern enum Item { RG_* }\n"; + const std::string usage = + "define bare(): RG_HOOKSHOT\n" + "define qualified(): Item.RG_BOW\n"; + HoverFixture fixture(declarations, usage); + + const auto bare = fixture.at({0, 17}); + ASSERT_TRUE(bare); + EXPECT_NE(bare->markdown.find("extern pattern RG_HOOKSHOT: Item"), + std::string::npos); + EXPECT_NE(bare->markdown.find( + "Concrete value matched by extern enum pattern `RG_*` in `Item`."), + std::string::npos); + EXPECT_NE(bare->markdown.find("External wildcard pattern declaration."), + std::string::npos); + EXPECT_NE(bare->markdown.find("[Open declaration](file:"), std::string::npos); + + const auto qualified = fixture.at({1, 27}); + ASSERT_TRUE(qualified); + EXPECT_NE(qualified->markdown.find("extern pattern RG_BOW: Item"), + std::string::npos); + EXPECT_NE(qualified->markdown.find("`RG_*`"), std::string::npos); +} + +TEST(HoverServiceTests, SupportsRegionsSectionEntriesAndTypedExpressions) { + const std::string usage = + "region RR_TEST {\n" + " name: \"Test\"\n" + " events { EVENT_READY: true }\n" + " locations { RC_CHEST: true }\n" + "}\n" + "define arithmetic(): 1 + 2\n"; + HoverFixture fixture({}, usage); + + const auto region = fixture.at({0, 9}); + ASSERT_TRUE(region); + EXPECT_NE(region->markdown.find("region RR_TEST"), std::string::npos); + EXPECT_NE(region->markdown.find("Region value."), std::string::npos); + + const auto event = fixture.at({2, 13}); + ASSERT_TRUE(event); + EXPECT_NE(event->markdown.find("EVENT_READY: Event"), std::string::npos); + EXPECT_NE(event->markdown.find("Declared event value."), std::string::npos); + + const auto location = fixture.at({3, 16}); + ASSERT_TRUE(location); + EXPECT_NE(location->markdown.find("RC_CHEST: Location"), std::string::npos); + + const auto expression = fixture.at({5, 23}); + ASSERT_TRUE(expression); + EXPECT_NE(expression->markdown.find("expression: Int"), std::string::npos); + EXPECT_NE(expression->markdown.find("Inferred expression type."), std::string::npos); +} + +TEST(HoverServiceTests, SupportsKnownRecoveredCallAndRejectsUnknownOrStaleData) { + const std::string declarations = + "extern define target(value: Bool) -> Bool\n"; + const std::string usage = "define use(): target("; + HoverFixture recovered(declarations, usage); + const auto known = recovered.at({0, 16}); + ASSERT_TRUE(known); + EXPECT_NE(known->markdown.find("extern target(value: Bool) -> Bool"), + std::string::npos); + + HoverFixture unresolved({}, "define use(): missing\n"); + EXPECT_FALSE(unresolved.at({0, 16})); + + HoverFixture stale(declarations, "define use(): target(true)\n"); + ASSERT_EQ(stale.projects.documentChanged(stale.usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + EXPECT_FALSE(stale.at({0, 16})); +} + +} // namespace diff --git a/lsp/tests/json_rpc_router_tests.cpp b/lsp/tests/json_rpc_router_tests.cpp new file mode 100644 index 0000000..07f8ef6 --- /dev/null +++ b/lsp/tests/json_rpc_router_tests.cpp @@ -0,0 +1,86 @@ +#include + +#include +#include + +#include "rls/lsp/json_rpc_router.h" + +namespace { + +using Json = nlohmann::json; +using rls::lsp::JsonRpcRouter; + +TEST(JsonRpcRouterTests, DispatchesExplicitRequestAndPreservesId) { + JsonRpcRouter router; + router.registerRequest("test/echo", [](const Json& params) { return params.at("value"); }); + + const auto responses = router.handlePayload( + R"({"jsonrpc":"2.0","id":"request-1","method":"test/echo","params":{"value":42}})"); + + ASSERT_EQ(responses.size(), 1); + const auto response = Json::parse(responses.front()); + EXPECT_EQ(response["id"], "request-1"); + EXPECT_EQ(response["result"], 42); +} + +TEST(JsonRpcRouterTests, NotificationsDoNotProduceResponses) { + JsonRpcRouter router; + bool called = false; + router.registerNotification("test/notify", [&called](const Json&) { called = true; }); + + EXPECT_TRUE(router.handlePayload( + R"({"jsonrpc":"2.0","method":"test/notify","params":{}})").empty()); + EXPECT_TRUE(called); +} + +TEST(JsonRpcRouterTests, RejectsDuplicateRoutesAcrossKinds) { + JsonRpcRouter router; + router.registerRequest("test/duplicate", [](const Json&) { return Json(nullptr); }); + + EXPECT_THROW(router.registerNotification("test/duplicate", [](const Json&) {}), std::logic_error); + EXPECT_NO_THROW(router.requireRoutes({"test/duplicate"})); + EXPECT_THROW(router.requireRoutes({"test/missing"}), std::logic_error); +} + +TEST(JsonRpcRouterTests, ReturnsStandardProtocolErrors) { + JsonRpcRouter router; + + auto responses = router.handlePayload("{"); + ASSERT_EQ(responses.size(), 1); + EXPECT_EQ(Json::parse(responses.front())["error"]["code"], -32700); + + responses = router.handlePayload(R"({"jsonrpc":"2.0","id":1,"method":"missing"})"); + ASSERT_EQ(responses.size(), 1); + EXPECT_EQ(Json::parse(responses.front())["error"]["code"], -32601); +} + +TEST(JsonRpcRouterTests, MapsBindingFailuresToInvalidParams) { + JsonRpcRouter router; + router.registerRequest("test/required", [](const Json& params) { + return params.at("required"); + }); + + const auto responses = router.handlePayload( + R"({"jsonrpc":"2.0","id":2,"method":"test/required","params":{}})"); + + ASSERT_EQ(responses.size(), 1); + EXPECT_EQ(Json::parse(responses.front())["error"]["code"], -32602); +} + +TEST(JsonRpcRouterTests, BatchesResponsesAndSuppressesNotifications) { + JsonRpcRouter router; + router.registerRequest("test/request", [](const Json&) { return 7; }); + router.registerNotification("test/notification", [](const Json&) {}); + + const auto responses = router.handlePayload(R"([ + {"jsonrpc":"2.0","id":1,"method":"test/request"}, + {"jsonrpc":"2.0","method":"test/notification"} + ])"); + + ASSERT_EQ(responses.size(), 1); + const auto batch = Json::parse(responses.front()); + ASSERT_EQ(batch.size(), 1); + EXPECT_EQ(batch[0]["result"], 7); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/lifecycle_service_tests.cpp b/lsp/tests/lifecycle_service_tests.cpp new file mode 100644 index 0000000..38641e4 --- /dev/null +++ b/lsp/tests/lifecycle_service_tests.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include "rls/lsp/lifecycle_service.h" + +namespace { + +using rls::lsp::LifecycleService; + +TEST(LifecycleServiceTests, AcceptsDocumentsOnlyAfterInitialized) { + LifecycleService lifecycle; + EXPECT_FALSE(lifecycle.acceptsDocumentUpdates()); + + lifecycle.initialize(); + EXPECT_FALSE(lifecycle.acceptsDocumentUpdates()); + + lifecycle.initialized(); + EXPECT_TRUE(lifecycle.acceptsDocumentUpdates()); + + lifecycle.shutdown(); + EXPECT_FALSE(lifecycle.acceptsDocumentUpdates()); +} + +TEST(LifecycleServiceTests, RejectsDuplicateLifecycleTransitions) { + LifecycleService lifecycle; + lifecycle.initialize(); + EXPECT_THROW(lifecycle.initialize(), std::logic_error); + + lifecycle.initialized(); + EXPECT_THROW(lifecycle.initialized(), std::logic_error); + + lifecycle.shutdown(); + EXPECT_THROW(lifecycle.shutdown(), std::logic_error); +} + +TEST(LifecycleServiceTests, ExitCodeReflectsCleanShutdown) { + LifecycleService earlyExit; + earlyExit.exit(); + EXPECT_TRUE(earlyExit.shouldExit()); + EXPECT_EQ(earlyExit.exitCode(), 1); + + LifecycleService cleanExit; + cleanExit.initialize(); + cleanExit.shutdown(); + cleanExit.exit(); + EXPECT_EQ(cleanExit.exitCode(), 0); +} + +TEST(LifecycleServiceTests, StoresNegotiatedCompletionSnippetSupport) { + LifecycleService unsupported; + unsupported.initialize(); + EXPECT_FALSE(unsupported.supportsCompletionSnippets()); + + LifecycleService supported; + supported.initialize(false, false, true); + EXPECT_TRUE(supported.supportsCompletionSnippets()); +} + +TEST(LifecycleServiceTests, DefaultsSectionSnippetIndentationToServer) { + LifecycleService lifecycle; + lifecycle.initialize(); + EXPECT_EQ(lifecycle.sectionSnippetIndentation(), + rls::lsp::SectionSnippetIndentation::Server); + + LifecycleService clientIndented; + clientIndented.initialize(false, false, true, + rls::lsp::SectionSnippetIndentation::Client); + EXPECT_EQ(clientIndented.sectionSnippetIndentation(), + rls::lsp::SectionSnippetIndentation::Client); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/message_framer_tests.cpp b/lsp/tests/message_framer_tests.cpp new file mode 100644 index 0000000..647e539 --- /dev/null +++ b/lsp/tests/message_framer_tests.cpp @@ -0,0 +1,60 @@ +#include + +#include + +#include "rls/lsp/message_framer.h" + +namespace { + +using rls::lsp::MessageFramer; + +TEST(MessageFramerTests, FramesPayloadUsingByteLength) { + EXPECT_EQ(MessageFramer::frame("{}"), "Content-Length: 2\r\n\r\n{}"); +} + +TEST(MessageFramerTests, WaitsForACompleteSplitFrame) { + MessageFramer framer; + framer.append("Content-Length: 2\r\n"); + EXPECT_FALSE(framer.popMessage().has_value()); + + framer.append("\r\n{}"); + ASSERT_TRUE(framer.popMessage().has_value()); +} + +TEST(MessageFramerTests, PreservesFollowingFrames) { + MessageFramer framer; + framer.append("Content-Length: 2\r\n\r\n{}Content-Length: 2\r\n\r\n[]"); + + EXPECT_EQ(framer.popMessage(), "{}"); + EXPECT_EQ(framer.popMessage(), "[]"); + EXPECT_FALSE(framer.popMessage().has_value()); +} + +TEST(MessageFramerTests, AcceptsCaseInsensitiveHeaderName) { + MessageFramer framer; + framer.append("content-length:\t2\r\n\r\n{}"); + + EXPECT_EQ(framer.popMessage(), "{}"); +} + +TEST(MessageFramerTests, RejectsInvalidOrDuplicateLengths) { + MessageFramer invalid; + invalid.append("Content-Length: 2x\r\n\r\n{}"); + EXPECT_THROW((void)invalid.popMessage(), std::runtime_error); + + MessageFramer duplicate; + duplicate.append("Content-Length: 2\r\nContent-Length: 2\r\n\r\n{}"); + EXPECT_THROW((void)duplicate.popMessage(), std::runtime_error); +} + +TEST(MessageFramerTests, EnforcesConfiguredLimits) { + MessageFramer payloadLimited(1); + payloadLimited.append("Content-Length: 2\r\n\r\n{}"); + EXPECT_THROW((void)payloadLimited.popMessage(), std::runtime_error); + + MessageFramer headerLimited(16, 24); + headerLimited.append("X-Long: 12345678901234567890"); + EXPECT_THROW((void)headerLimited.popMessage(), std::runtime_error); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/navigation_service_tests.cpp b/lsp/tests/navigation_service_tests.cpp new file mode 100644 index 0000000..d2af2ce --- /dev/null +++ b/lsp/tests/navigation_service_tests.cpp @@ -0,0 +1,538 @@ +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/navigation_service.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::DocumentStore; +using rls::lsp::NavigationService; +using rls::lsp::ProjectManager; + +TEST(NavigationServiceTests, FindsCrossFileDefinitionInCurrentSnapshot) { + const fs::path root = fs::temp_directory_path() / "rls-navigation-service"; + const fs::path declarationPath = root / "declaration.rls"; + const fs::path usagePath = root / "usage.rls"; + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + DocumentStore documents; + ASSERT_EQ(documents.open( + usageUri, "rls", 1, "define caller(): target()\n"), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), "extern define target() -> Bool\n"}, + {usagePath.generic_string(), "define caller(): target()\n"}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + NavigationService navigation(projects, scheduler); + const auto definition = navigation.definition(usageUri, {0, 18}); + ASSERT_TRUE(definition); + EXPECT_EQ(definition->targetUri, *rls::lsp::PathToFileUri(declarationPath)); + EXPECT_EQ(definition->originSelectionRange.start.line, 0u); + EXPECT_EQ(definition->originSelectionRange.start.character, 17u); + EXPECT_EQ(definition->originSelectionRange.end.character, 23u); + EXPECT_EQ(definition->targetSelectionRange.start.character, 14u); + EXPECT_EQ(definition->targetSelectionRange.end.character, 20u); + + const auto references = navigation.references(usageUri, {0, 18}, true); + ASSERT_EQ(references.size(), 2u); + EXPECT_EQ(references[0].uri, *rls::lsp::PathToFileUri(declarationPath)); + EXPECT_EQ(references[1].uri, usageUri); + const auto referencesWithoutDeclaration = navigation.references( + usageUri, {0, 18}, false); + ASSERT_EQ(referencesWithoutDeclaration.size(), 1u); + EXPECT_EQ(referencesWithoutDeclaration[0].uri, usageUri); + const auto highlights = navigation.documentHighlights(usageUri, {0, 18}); + ASSERT_EQ(highlights.size(), 1u); + EXPECT_EQ(highlights[0].start.character, 17u); + + ASSERT_EQ(projects.documentChanged(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + EXPECT_FALSE(navigation.definition(usageUri, {0, 18})); + EXPECT_TRUE(navigation.references(usageUri, {0, 18}, true).empty()); + EXPECT_TRUE(navigation.documentHighlights(usageUri, {0, 18}).empty()); +} + +TEST(NavigationServiceTests, NavigatesWildcardEnumValuesToPatternDeclaration) { + const fs::path root = fs::temp_directory_path() / "rls-navigation-pattern"; + const fs::path declarationPath = root / "declaration.rls"; + const fs::path usagePath = root / "usage.rls"; + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + const std::string usage = + "define bare(): RG_HOOKSHOT\n" + "define repeated(): RG_HOOKSHOT\n" + "define qualified(): Item.RG_BOW\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), "extern enum Item { RG_* }\n"}, + {usagePath.generic_string(), usage}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + NavigationService navigation(projects, scheduler); + const auto bare = navigation.definition(usageUri, {0, 17}); + ASSERT_TRUE(bare); + EXPECT_EQ(bare->targetUri, *rls::lsp::PathToFileUri(declarationPath)); + EXPECT_EQ(bare->targetSelectionRange.start.character, 19u); + EXPECT_EQ(bare->targetSelectionRange.end.character, 23u); + + const auto qualified = navigation.definition(usageUri, {2, 27}); + ASSERT_TRUE(qualified); + EXPECT_EQ(qualified->targetUri, bare->targetUri); + EXPECT_EQ(qualified->targetSelectionRange.start.character, 19u); + + const auto references = navigation.references(usageUri, {0, 17}, true); + ASSERT_EQ(references.size(), 2u); + EXPECT_EQ(references[0].uri, usageUri); + EXPECT_EQ(references[0].range.start.line, 0u); + EXPECT_EQ(references[1].uri, usageUri); + EXPECT_EQ(references[1].range.start.line, 1u); + + const auto highlights = navigation.documentHighlights(usageUri, {0, 17}); + ASSERT_EQ(highlights.size(), 2u); + EXPECT_EQ(highlights[0].start.line, 0u); + EXPECT_EQ(highlights[1].start.line, 1u); +} + +TEST(NavigationServiceTests, KeepsSameNameParametersInSeparateScopes) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-navigation-parameters.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string content = + "define first(value: Bool): value\n" + "define second(value: Bool): value\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(uri, "rls", 1, content), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {sourcePath}; + project.isStandalone = true; + return project; + }); + ASSERT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{sourcePath.generic_string(), content}}, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + NavigationService navigation(projects, scheduler); + const auto firstReferences = navigation.references(uri, {0, 28}, true); + ASSERT_EQ(firstReferences.size(), 2u); + EXPECT_EQ(firstReferences[0].range.start.line, 0u); + EXPECT_EQ(firstReferences[0].range.start.character, 13u); + EXPECT_EQ(firstReferences[1].range.start.line, 0u); + EXPECT_EQ(firstReferences[1].range.start.character, 27u); + const auto firstHighlights = navigation.documentHighlights(uri, {0, 28}); + ASSERT_EQ(firstHighlights.size(), 2u); + EXPECT_EQ(firstHighlights[0].start.line, 0u); + EXPECT_EQ(firstHighlights[1].start.line, 0u); +} + +TEST(NavigationServiceTests, BuildsStableSourceOrderedDocumentSymbolHierarchy) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-document-symbols.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string content = + "region RR_BASE { name: \"Base\" events { EVENT_BASE: true } }\n" + "extend region RR_BASE { events { EVENT_EXT: true } }\n" + "define check(value: Bool): value\n" + "extern define host(item: Item) -> Bool\n" + "enum Color { RED, BLUE }\n" + "extern enum Item { RG_HOOKSHOT, RG_* }\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(uri, "rls", 1, content), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {sourcePath}; + project.isStandalone = true; + return project; + }); + ASSERT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{sourcePath.generic_string(), content}}, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + NavigationService navigation(projects, scheduler); + const auto symbols = navigation.documentSymbols(uri); + ASSERT_EQ(symbols.size(), 6u); + EXPECT_EQ(symbols[0].name, "RR_BASE"); + EXPECT_EQ(symbols[1].name, "RR_BASE"); + EXPECT_EQ(symbols[2].name, "check"); + EXPECT_EQ(symbols[3].name, "host"); + EXPECT_EQ(symbols[4].name, "Color"); + EXPECT_EQ(symbols[5].name, "Item"); + + ASSERT_EQ(symbols[0].children.size(), 2u); + EXPECT_EQ(symbols[0].children[0].name, "name"); + EXPECT_EQ(symbols[0].children[1].name, "EVENT_BASE"); + ASSERT_EQ(symbols[1].children.size(), 1u); + EXPECT_EQ(symbols[1].children[0].name, "EVENT_EXT"); + ASSERT_EQ(symbols[2].children.size(), 1u); + EXPECT_EQ(symbols[2].children[0].name, "value"); + EXPECT_EQ(symbols[2].children[0].range.start.character, 13u); + EXPECT_EQ(symbols[2].children[0].range.end.character, 24u); + EXPECT_EQ(symbols[2].children[0].selectionRange.start.character, 13u); + EXPECT_EQ(symbols[2].children[0].selectionRange.end.character, 18u); + ASSERT_EQ(symbols[4].children.size(), 2u); + EXPECT_EQ(symbols[4].children[0].name, "RED"); + EXPECT_EQ(symbols[4].children[1].name, "BLUE"); + ASSERT_EQ(symbols[5].children.size(), 2u); + EXPECT_EQ(symbols[5].children[1].name, "RG_*"); + + ASSERT_EQ(projects.documentChanged(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + EXPECT_TRUE(navigation.documentSymbols(uri).empty()); +} + +TEST(NavigationServiceTests, FiltersAndOrdersWorkspaceProjectDeclarations) { + const fs::path firstPath = fs::temp_directory_path() / "rls-workspace-symbol-first.rls"; + const fs::path secondPath = fs::temp_directory_path() / "rls-workspace-symbol-second.rls"; + const std::string firstUri = *rls::lsp::PathToFileUri(firstPath); + const std::string secondUri = *rls::lsp::PathToFileUri(secondPath); + DocumentStore documents; + ASSERT_EQ(documents.open(firstUri, "rls", 1, + "enum Holder { ALPHA_MEMBER }\n" + "extern define alpha_host() -> Bool\n" + "define alpha_define(): true\n" + "region ALPHA_REGION { name: \"Alpha\" }\n"), + rls::lsp::DocumentUpdateResult::Applied); + ASSERT_EQ(documents.open(secondUri, "rls", 1, "define alpha_other(): true\n"), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [](const fs::path& path) { + rls::project::FileProject project; + project.sourceFiles = {path}; + project.isStandalone = true; + return project; + }); + ASSERT_EQ(projects.documentOpened(firstUri), + rls::lsp::ProjectAssignmentResult::Assigned); + ASSERT_EQ(projects.documentOpened(secondUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* firstProject = projects.projectForDocument(firstUri); + const auto* secondProject = projects.projectForDocument(secondUri); + ASSERT_NE(firstProject, nullptr); + ASSERT_NE(secondProject, nullptr); + const std::string firstProjectId = firstProject->id; + const std::string secondProjectId = secondProject->id; + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + firstProjectId, + firstProject->generation, + {{firstPath.generic_string(), + "enum Holder { ALPHA_MEMBER }\n" + "extern define alpha_host() -> Bool\n" + "define alpha_define(): true\n" + "region ALPHA_REGION { name: \"Alpha\" }\n"}}, + firstProject->documentGeneration, + firstProject->manifestGeneration, + })); + ASSERT_TRUE(scheduler.schedule({ + secondProjectId, + secondProject->generation, + {{secondPath.generic_string(), "define alpha_other(): true\n"}}, + secondProject->documentGeneration, + secondProject->manifestGeneration, + })); + scheduler.waitForIdle(); + + NavigationService navigation(projects, scheduler); + const auto symbols = navigation.workspaceSymbols("AlPhA", {firstProjectId}); + ASSERT_EQ(symbols.size(), 4u); + EXPECT_EQ(symbols[0].name, "ALPHA_REGION"); + EXPECT_EQ(symbols[1].name, "alpha_define"); + EXPECT_EQ(symbols[2].name, "alpha_host"); + EXPECT_EQ(symbols[3].name, "ALPHA_MEMBER"); + EXPECT_EQ(symbols[3].containerName, "Holder"); + EXPECT_EQ(symbols[0].location.uri, firstUri); + + const auto bothProjects = navigation.workspaceSymbols( + "alpha", {firstProjectId, secondProjectId}); + ASSERT_EQ(bothProjects.size(), 5u); + EXPECT_EQ(bothProjects[2].name, "alpha_other"); +} + +TEST(NavigationServiceTests, CoversDefinitionAndReferenceCategoriesAcrossProjectFiles) { + const fs::path root = fs::temp_directory_path() / "rls-navigation-category-matrix"; + const fs::path declarationPath = root / "declarations.rls"; + const fs::path usagePath = root / "usages.rls"; + const std::string declarationUri = *rls::lsp::PathToFileUri(declarationPath); + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + const std::string declarations = + "region RR_HOME { name: \"Home\" }\n" + "enum Color { RED }\n" + "define check(flag: Bool): flag\n" + "extern define host() -> Bool\n"; + const std::string usages = + "extend region RR_HOME { events { EVENT_HOME: true } }\n" + "define use_color(value: Color): Color.RED == value\n" + "define caller(): check(true) and host()\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(declarationUri, "rls", 1, declarations), + rls::lsp::DocumentUpdateResult::Applied); + ASSERT_EQ(documents.open(usageUri, "rls", 1, usages), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(declarationUri), + rls::lsp::ProjectAssignmentResult::Assigned); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), declarations}, + {usagePath.generic_string(), usages}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + struct NavigationCase { + std::string uri; + rls::lsp::NavigationPosition position; + size_t referenceCount; + bool hasCrossFileReference; + }; + const std::vector cases = { + {usageUri, {0, 15}, 2, true}, + {usageUri, {1, 25}, 3, true}, + {usageUri, {1, 39}, 2, true}, + {usageUri, {2, 18}, 2, true}, + {usageUri, {2, 34}, 2, true}, + {declarationUri, {2, 27}, 2, false}, + }; + + NavigationService navigation(projects, scheduler); + for (const auto& navigationCase : cases) { + const auto definition = navigation.definition( + navigationCase.uri, navigationCase.position); + ASSERT_TRUE(definition); + EXPECT_EQ(definition->targetUri, declarationUri); + const auto references = navigation.references( + navigationCase.uri, navigationCase.position, true); + ASSERT_EQ(references.size(), navigationCase.referenceCount); + const bool hasUsageReference = std::any_of( + references.begin(), references.end(), [&](const auto& reference) { + return reference.uri == usageUri; + }); + EXPECT_EQ(hasUsageReference, navigationCase.hasCrossFileReference); + } +} + +TEST(NavigationServiceTests, ResolvesCanonicalRegionAndRejectsUnresolvedOrAmbiguousNames) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-navigation-targets.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string content = + "region RR_BASE { name: \"Base\" }\n" + "extend region RR_BASE { events { EVENT_BASE: true } }\n" + "extend region RR_MISSING { events { EVENT_MISSING: true } }\n" + "extern enum Item { RG_* }\n" + "define item(): RG_SWORD\n" + "enum Alpha { SHARED }\n" + "enum Beta { SHARED }\n" + "define ambiguous(): SHARED\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(uri, "rls", 1, content), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {sourcePath}; + project.isStandalone = true; + return project; + }); + ASSERT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{sourcePath.generic_string(), content}}, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + NavigationService navigation(projects, scheduler); + const auto region = navigation.definition(uri, {1, 15}); + ASSERT_TRUE(region); + EXPECT_EQ(region->targetSelectionRange.start.line, 0u); + EXPECT_EQ(region->targetSelectionRange.start.character, 7u); + EXPECT_EQ(region->targetSelectionRange.end.character, 14u); + EXPECT_FALSE(navigation.definition(uri, {2, 15})); + const auto wildcard = navigation.definition(uri, {4, 16}); + ASSERT_TRUE(wildcard); + EXPECT_EQ(wildcard->targetUri, uri); + EXPECT_EQ(wildcard->targetSelectionRange.start.line, 3u); + EXPECT_EQ(wildcard->targetSelectionRange.start.character, 19u); + EXPECT_EQ(wildcard->targetSelectionRange.end.character, 23u); + EXPECT_TRUE(navigation.references(uri, {2, 15}, true).empty()); + EXPECT_TRUE(navigation.documentHighlights(uri, {2, 15}).empty()); + ASSERT_EQ(navigation.references(uri, {4, 16}, true).size(), 1u); + ASSERT_EQ(navigation.documentHighlights(uri, {4, 16}).size(), 1u); + EXPECT_FALSE(navigation.definition(uri, {7, 20})); + EXPECT_TRUE(navigation.references(uri, {7, 20}, true).empty()); + EXPECT_TRUE(navigation.documentHighlights(uri, {7, 20}).empty()); +} + +TEST(NavigationServiceTests, RejectsNavigationFromCurrentMalformedOverlay) { + const fs::path root = fs::temp_directory_path() / "rls-navigation-malformed"; + const fs::path declarationPath = root / "declaration.rls"; + const fs::path usagePath = root / "usage.rls"; + const std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + DocumentStore documents; + ASSERT_EQ(documents.open( + usageUri, "rls", 1, "define caller(): target()\n"), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }); + ASSERT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + + AnalysisScheduler scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), "define target(): true\n"}, + {usagePath.generic_string(), "define caller(): target()\n"}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + NavigationService navigation(projects, scheduler); + ASSERT_TRUE(navigation.definition(usageUri, {0, 18})); + + ASSERT_EQ(documents.applyFullChange( + usageUri, 2, "define caller(): target(\n"), + rls::lsp::DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentChanged(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + project = projects.projectForDocument(usageUri); + ASSERT_NE(project, nullptr); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), "define target(): true\n"}, + {usagePath.generic_string(), "define caller(): target(\n"}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + + const auto snapshot = scheduler.acceptedSnapshot(project->id); + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->generation(), project->generation); + EXPECT_FALSE(snapshot->diagnosticsFor( + fs::weakly_canonical(usagePath).generic_string()).empty()); + EXPECT_FALSE(navigation.definition(usageUri, {0, 18})); + EXPECT_TRUE(navigation.references(usageUri, {0, 18}, true).empty()); + EXPECT_TRUE(navigation.documentHighlights(usageUri, {0, 18}).empty()); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/presentation_tests.cpp b/lsp/tests/presentation_tests.cpp new file mode 100644 index 0000000..efa2400 --- /dev/null +++ b/lsp/tests/presentation_tests.cpp @@ -0,0 +1,93 @@ +#include + +#include "rls/lsp/presentation.h" + +namespace { + +using rls::lsp::DocumentationBlock; +using rls::lsp::PresentationCallable; +using rls::lsp::PresentationLocation; +using rls::lsp::PresentationParameter; +using rls::lsp::PresentationProvenance; +using rls::lsp::PresentationRenderer; +using rls::lsp::PresentationSymbol; +using rls::lsp::PresentationSymbolKind; +using rls::lsp::PresentationType; + +TEST(PresentationRendererTests, RendersCallableTypesDefaultsAndDocumentation) { + PresentationSymbol symbol{ + .kind = PresentationSymbolKind::Function, + .name = "can_use", + .provenance = PresentationProvenance::Source, + .callable = PresentationCallable{ + .name = "can_use", + .parameters = { + PresentationParameter{ + .name = "item", + .type = PresentationType{.name = "Enum", .enumIdentity = "Item"}, + }, + PresentationParameter{ + .name = "distance", + .type = PresentationType{.name = "Int"}, + .defaultValue = "0", + .optional = true, + }, + }, + .returnType = PresentationType{.name = "Bool"}, + }, + .documentation = { + DocumentationBlock{.heading = "Usage", .markdown = "Checks whether an item is usable."}, + }, + }; + + const auto rendered = PresentationRenderer{}.render(symbol); + + EXPECT_EQ(rendered.detail, "can_use(item: Item, distance: Int = 0) -> Bool"); + EXPECT_EQ(rendered.documentation, + "**Usage**\n\nChecks whether an item is usable."); +} + +TEST(PresentationRendererTests, RendersProvenanceWithoutEmbeddingSourceLocation) { + PresentationSymbol symbol{ + .kind = PresentationSymbolKind::EnumMember, + .name = "RG_HOOKSHOT", + .provenance = PresentationProvenance::Extern, + .type = PresentationType{.name = "Enum", .enumIdentity = "Item"}, + .declaration = PresentationLocation{ + .uri = "file:///project/extern.rls", + .range = {{4, 2}, {4, 13}}, + }, + }; + + const auto rendered = PresentationRenderer{}.render(symbol); + + EXPECT_EQ(rendered.detail, "extern RG_HOOKSHOT: Item"); + EXPECT_EQ(rendered.documentation, "*External declaration.*"); + EXPECT_EQ(rendered.detail.find("extern.rls"), std::string::npos); + ASSERT_TRUE(symbol.declaration); + EXPECT_EQ(symbol.declaration->range.start.line, 4u); +} + +TEST(PresentationRendererTests, DistinguishesBuiltInAndPatternProvenance) { + PresentationSymbol builtIn{ + .name = "true", + .provenance = PresentationProvenance::BuiltIn, + .type = PresentationType{.name = "Bool"}, + }; + PresentationSymbol pattern{ + .name = "RG_*", + .provenance = PresentationProvenance::Pattern, + .type = PresentationType{.name = "Enum", .enumIdentity = "Item"}, + }; + + const auto renderedBuiltIn = PresentationRenderer{}.render(builtIn); + const auto renderedPattern = PresentationRenderer{}.render(pattern); + + EXPECT_EQ(renderedBuiltIn.detail, "built-in true: Bool"); + EXPECT_EQ(renderedBuiltIn.documentation, "*Built-in symbol.*"); + EXPECT_EQ(renderedPattern.detail, "extern pattern RG_*: Item"); + EXPECT_EQ(renderedPattern.documentation, + "*External pattern; no source declaration.*"); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/process_smoke.py b/lsp/tests/process_smoke.py new file mode 100644 index 0000000..26a7ffd --- /dev/null +++ b/lsp/tests/process_smoke.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 + +import argparse +import json +import queue +import subprocess +import tempfile +import threading +from pathlib import Path + + +class ProtocolError(RuntimeError): + pass + + +def encode_message(message: dict) -> bytes: + payload = json.dumps(message, separators=(",", ":"), ensure_ascii=False).encode("utf-8") + return f"Content-Length: {len(payload)}\r\n\r\n".encode("ascii") + payload + + +def read_message(stream) -> dict | None: + headers: dict[str, str] = {} + while True: + line = stream.readline() + if line == b"": + return None + if not line.endswith(b"\r\n"): + raise ProtocolError("stdout contained a malformed protocol header") + if line == b"\r\n": + break + name, separator, value = line.decode("ascii").partition(":") + if not separator: + raise ProtocolError("stdout contained a malformed protocol header") + headers[name.lower()] = value.strip() + + try: + length = int(headers["content-length"]) + except (KeyError, ValueError) as error: + raise ProtocolError("stdout frame omitted a valid Content-Length") from error + + payload = stream.read(length) + if len(payload) != length: + raise ProtocolError("stdout frame ended before its payload") + return json.loads(payload.decode("utf-8")) + + +def output_reader(stream, messages: queue.Queue) -> None: + try: + while True: + message = read_message(stream) + if message is None: + messages.put(None) + return + messages.put(message) + except BaseException as error: + messages.put(error) + + +def send(process: subprocess.Popen, message: dict) -> None: + assert process.stdin is not None + process.stdin.write(encode_message(message)) + process.stdin.flush() + + +def receive_matching(messages: queue.Queue, predicate, description: str) -> dict: + deferred: list[dict] = [] + try: + while True: + try: + message = messages.get(timeout=10) + except queue.Empty as error: + raise ProtocolError(f"timed out waiting for {description}") from error + if isinstance(message, BaseException): + raise message + if message is None: + raise ProtocolError(f"server exited before {description}") + if predicate(message): + return message + deferred.append(message) + finally: + for message in deferred: + messages.put(message) + + +def run_smoke(server: Path) -> None: + with tempfile.TemporaryDirectory(prefix="rls lsp smoke ") as temporary: + root = Path(temporary) + source = root / "logic file.rls" + manifest = root / "rls.json" + source.write_text("define disk(): true\n", encoding="utf-8") + manifest.write_text( + json.dumps({"version": 1, "sources": [source.name]}), encoding="utf-8" + ) + + root_uri = root.as_uri() + source_uri = source.as_uri() + if "%20" not in root_uri or "%20" not in source_uri: + raise ProtocolError("URI fixture did not exercise escaped paths") + + process = subprocess.Popen( + [str(server)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + assert process.stdout is not None + assert process.stderr is not None + messages: queue.Queue = queue.Queue() + reader = threading.Thread( + target=output_reader, args=(process.stdout, messages), daemon=True + ) + reader.start() + + try: + send( + process, + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "workspaceFolders": [{"uri": root_uri, "name": "smoke"}] + }, + }, + ) + initialize = receive_matching( + messages, lambda message: message.get("id") == 1, "initialize response" + ) + capabilities = initialize["result"]["capabilities"] + if capabilities["textDocumentSync"]["change"] != 1: + raise ProtocolError("server did not advertise full document synchronization") + if not capabilities["workspace"]["workspaceFolders"]["supported"]: + raise ProtocolError("server did not advertise workspace folder support") + if capabilities.get("signatureHelpProvider", {}).get( + "triggerCharacters" + ) != ["(", ","]: + raise ProtocolError("server did not advertise signature help triggers") + if capabilities.get("hoverProvider") is not True: + raise ProtocolError("server did not advertise hover support") + semantic_tokens = capabilities.get("semanticTokensProvider", {}) + if semantic_tokens.get("legend", {}).get("tokenTypes") != [ + "function", + "parameter", + "enum", + "enumMember", + "property", + "variable", + "operator", + ] or semantic_tokens.get("full") is not True: + raise ProtocolError("server did not advertise semantic token support") + + send(process, {"jsonrpc": "2.0", "method": "initialized", "params": {}}) + send( + process, + { + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": source_uri, + "languageId": "rls", + "version": 1, + "text": "define broken(): missing\n", + } + }, + }, + ) + diagnostic_message = receive_matching( + messages, + lambda message: message.get("method") + == "textDocument/publishDiagnostics" + and message.get("params", {}).get("uri") == source_uri + and message.get("params", {}).get("diagnostics"), + "live diagnostic notification", + ) + diagnostic = next( + item + for item in diagnostic_message["params"]["diagnostics"] + if item.get("code") == "RLS-T006" + ) + if diagnostic["data"] != { + "version": 1, + "actionKind": "rls.declareSymbol", + "arguments": ["missing"], + }: + raise ProtocolError("structured diagnostic data was not preserved") + + send( + process, + { + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": {"uri": source_uri, "version": 2}, + "contentChanges": [{"text": ""}], + }, + }, + ) + receive_matching( + messages, + lambda message: message.get("method") + == "textDocument/publishDiagnostics" + and message.get("params", {}).get("uri") == source_uri + and not message.get("params", {}).get("diagnostics"), + "diagnostic clear notification", + ) + + signature_source = ( + "extern define target(value: Bool = true) -> Bool\n" + "define use(): target(f" + ) + send( + process, + { + "jsonrpc": "2.0", + "method": "textDocument/didChange", + "params": { + "textDocument": {"uri": source_uri, "version": 3}, + "contentChanges": [{"text": signature_source}], + }, + }, + ) + send( + process, + { + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/signatureHelp", + "params": { + "textDocument": {"uri": source_uri}, + "position": {"line": 1, "character": len("define use(): target(f")}, + }, + }, + ) + signature = receive_matching( + messages, lambda message: message.get("id") == 2, + "signature help response", + )["result"] + if signature["activeParameter"] != 0 or signature["signatures"][0][ + "label" + ] != "extern target(value: Bool = true) -> Bool": + raise ProtocolError("signature help response was incomplete") + + send( + process, + { + "jsonrpc": "2.0", + "id": 3, + "method": "textDocument/hover", + "params": { + "textDocument": {"uri": source_uri}, + "position": {"line": 1, "character": len("define use(): tar")}, + }, + }, + ) + hover = receive_matching( + messages, lambda message: message.get("id") == 3, + "hover response", + )["result"] + if "extern target(value: Bool = true) -> Bool" not in hover[ + "contents" + ]["value"]: + raise ProtocolError("hover response omitted callable presentation") + + send( + process, + { + "jsonrpc": "2.0", + "id": 4, + "method": "textDocument/semanticTokens/full", + "params": {"textDocument": {"uri": source_uri}}, + }, + ) + token_data = receive_matching( + messages, lambda message: message.get("id") == 4, + "semantic token response", + )["result"]["data"] + if not token_data or len(token_data) % 5 != 0: + raise ProtocolError("semantic token response was not delta encoded") + + send(process, {"jsonrpc": "2.0", "id": 5, "method": "shutdown"}) + receive_matching( + messages, lambda message: message.get("id") == 5, "shutdown response" + ) + send(process, {"jsonrpc": "2.0", "method": "exit"}) + assert process.stdin is not None + process.stdin.close() + exit_code = process.wait(timeout=10) + reader.join(timeout=10) + stderr = process.stderr.read().decode("utf-8", errors="replace") + if exit_code != 0: + raise ProtocolError(f"server exited with {exit_code}: {stderr}") + if stderr: + raise ProtocolError(f"server wrote to stderr during clean smoke test: {stderr}") + finally: + if process.poll() is None: + process.kill() + process.wait(timeout=10) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--server", type=Path, required=True) + arguments = parser.parse_args() + run_smoke(arguments.server.resolve()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/lsp/tests/project_manager_tests.cpp b/lsp/tests/project_manager_tests.cpp new file mode 100644 index 0000000..5f01ba8 --- /dev/null +++ b/lsp/tests/project_manager_tests.cpp @@ -0,0 +1,218 @@ +#include +#include +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/project_manager.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::DocumentStore; +using rls::lsp::DocumentUpdateResult; +using rls::lsp::ProjectAssignmentResult; +using rls::lsp::ProjectManager; + +class TemporaryDirectory { +public: + TemporaryDirectory() : path_(fs::temp_directory_path() / + ("rls-lsp-project-manager-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()))) { + fs::create_directories(path_); + } + + ~TemporaryDirectory() { + std::error_code error; + fs::remove_all(path_, error); + } + + const fs::path& path() const { return path_; } + +private: + fs::path path_; +}; + +void writeFile(const fs::path& path, const std::string& content) { + fs::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary) << content; +} + +std::string fileUri(const fs::path& path) { + const std::string generic = fs::weakly_canonical(path).generic_string(); +#ifdef _WIN32 + return "file:///" + generic; +#else + return "file://" + generic; +#endif +} + +TEST(ProjectManagerTests, AssignsDocumentsToTheirNearestManifest) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({"version":1,"sources":["src"]})"); + writeFile(directory.path() / "src" / "first.rls", "define first(): true\n"); + writeFile(directory.path() / "src" / "second.rls", "define second(): true\n"); + + DocumentStore documents; + ProjectManager projects(documents); + const std::string firstUri = fileUri(directory.path() / "src" / "first.rls"); + const std::string secondUri = fileUri(directory.path() / "src" / "second.rls"); + ASSERT_EQ(documents.open(firstUri, "rls", 1, "first overlay"), + DocumentUpdateResult::Applied); + ASSERT_EQ(documents.open(secondUri, "rls", 1, "second overlay"), + DocumentUpdateResult::Applied); + + EXPECT_EQ(projects.documentOpened(firstUri), ProjectAssignmentResult::Assigned); + EXPECT_EQ(projects.documentOpened(secondUri), ProjectAssignmentResult::Assigned); + + const auto* firstProject = projects.projectForDocument(firstUri); + const auto* secondProject = projects.projectForDocument(secondUri); + ASSERT_NE(firstProject, nullptr); + ASSERT_NE(secondProject, nullptr); + EXPECT_EQ(firstProject->id, secondProject->id); + EXPECT_FALSE(firstProject->isStandalone); + EXPECT_EQ(firstProject->sourceFiles.size(), 2); +} + +TEST(ProjectManagerTests, OpenOverlayWinsThenCloseRestoresDiskContent) { + TemporaryDirectory directory; + const fs::path sourcePath = directory.path() / "standalone.rls"; + writeFile(sourcePath, "disk content\n"); + const std::string uri = fileUri(sourcePath); + + DocumentStore documents; + ProjectManager projects(documents); + ASSERT_EQ(documents.open(uri, "rls", 1, "overlay content\n"), + DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentOpened(uri), ProjectAssignmentResult::Assigned); + + auto sourceSet = projects.sourceSetForDocument(uri); + ASSERT_TRUE(sourceSet.error.empty()) << sourceSet.error; + ASSERT_EQ(sourceSet.sources.size(), 1); + ASSERT_TRUE(sourceSet.sources.front().content.has_value()); + EXPECT_EQ(*sourceSet.sources.front().content, "overlay content\n"); + const uint64_t openGeneration = sourceSet.generation; + + ASSERT_TRUE(documents.close(uri)); + ASSERT_EQ(projects.documentClosed(uri), ProjectAssignmentResult::Assigned); + sourceSet = projects.sourceSetForDocument(uri); + ASSERT_TRUE(sourceSet.error.empty()) << sourceSet.error; + ASSERT_EQ(sourceSet.sources.size(), 1); + EXPECT_FALSE(sourceSet.sources.front().content.has_value()); + EXPECT_GT(sourceSet.generation, openGeneration); +} + +TEST(ProjectManagerTests, ChangesAdvanceGenerationAndPreserveOverlay) { + TemporaryDirectory directory; + const fs::path sourcePath = directory.path() / "standalone.rls"; + writeFile(sourcePath, "disk\n"); + const std::string uri = fileUri(sourcePath); + + DocumentStore documents; + ProjectManager projects(documents); + ASSERT_EQ(documents.open(uri, "rls", 1, "one\n"), DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentOpened(uri), ProjectAssignmentResult::Assigned); + const uint64_t before = projects.projectForDocument(uri)->generation; + const uint64_t beforeDocumentGeneration = + projects.projectForDocument(uri)->documentGeneration; + const uint64_t beforeManifestGeneration = + projects.projectForDocument(uri)->manifestGeneration; + + ASSERT_EQ(documents.applyFullChange(uri, 2, "two\n"), DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentChanged(uri), ProjectAssignmentResult::Assigned); + + const auto sourceSet = projects.sourceSetForDocument(uri); + EXPECT_GT(sourceSet.generation, before); + EXPECT_GT(sourceSet.documentGeneration, beforeDocumentGeneration); + EXPECT_EQ(sourceSet.manifestGeneration, beforeManifestGeneration); + ASSERT_EQ(sourceSet.sources.size(), 1); + ASSERT_TRUE(sourceSet.sources.front().content.has_value()); + EXPECT_EQ(*sourceSet.sources.front().content, "two\n"); +} + +TEST(ProjectManagerTests, RequiresAnOpenDocumentBeforeAssignment) { + TemporaryDirectory directory; + const fs::path sourcePath = directory.path() / "standalone.rls"; + writeFile(sourcePath, "disk\n"); + const std::string uri = fileUri(sourcePath); + + DocumentStore documents; + ProjectManager projects(documents); + + EXPECT_EQ(projects.documentOpened(uri), ProjectAssignmentResult::NotAssigned); + EXPECT_EQ(projects.projectForDocument(uri), nullptr); +} + +TEST(ProjectManagerTests, AssignsUntitledDocumentsAsOverlayOnlyStandaloneProjects) { + DocumentStore documents; + ProjectManager projects(documents); + const std::string uri = "untitled:Untitled-1"; + + ASSERT_EQ(documents.open(uri, "rls", 1, "define untitled(): missing\n"), + DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentOpened(uri), ProjectAssignmentResult::Assigned); + + const auto* project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + EXPECT_TRUE(project->isStandalone); + EXPECT_TRUE(project->sourceFiles.empty()); + + const auto sourceSet = projects.sourceSetForDocument(uri); + ASSERT_TRUE(sourceSet.error.empty()) << sourceSet.error; + ASSERT_EQ(sourceSet.sources.size(), 1); + ASSERT_TRUE(sourceSet.sources.front().content.has_value()); + EXPECT_EQ(*sourceSet.sources.front().content, "define untitled(): missing\n"); + EXPECT_EQ(sourceSet.sources.front().identity, uri); + EXPECT_FALSE(sourceSet.sources.front().diskPath.has_value()); + EXPECT_EQ(projects.sourceIdentityForDocument(uri), uri); + + ASSERT_TRUE(documents.close(uri)); + EXPECT_EQ(projects.documentClosed(uri), ProjectAssignmentResult::Assigned); + EXPECT_EQ(projects.projectForDocument(uri), nullptr); +} + +TEST(ProjectManagerTests, RefreshReassignsOpenDocumentAcrossNestedManifestChanges) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({"version":1,"sources":["nested"]})"); + const fs::path sourcePath = directory.path() / "nested" / "logic.rls"; + writeFile(sourcePath, "define disk(): true\n"); + const std::string uri = fileUri(sourcePath); + + DocumentStore documents; + ProjectManager projects(documents); + ASSERT_EQ(documents.open(uri, "rls", 1, "define overlay(): true\n"), + DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentOpened(uri), ProjectAssignmentResult::Assigned); + const std::string outerProjectId = projects.projectForDocument(uri)->id; + const uint64_t outerGeneration = projects.projectForDocument(uri)->generation; + + writeFile(directory.path() / "nested" / "rls.json", + R"({"version":1,"sources":["logic.rls"]})"); + auto refresh = projects.refreshOpenDocuments(); + ASSERT_TRUE(refresh.errors.empty()); + ASSERT_EQ(refresh.changedProjectIds.size(), 1); + ASSERT_EQ(refresh.removedProjectIds.size(), 1); + EXPECT_EQ(refresh.removedProjectIds.front(), outerProjectId); + const auto* nestedProject = projects.projectForDocument(uri); + ASSERT_NE(nestedProject, nullptr); + EXPECT_NE(nestedProject->id, outerProjectId); + EXPECT_GT(nestedProject->generation, outerGeneration); + auto sourceSet = projects.sourceSetForProject(nestedProject->id); + ASSERT_EQ(sourceSet.sources.size(), 1); + ASSERT_TRUE(sourceSet.sources.front().content.has_value()); + EXPECT_EQ(*sourceSet.sources.front().content, "define overlay(): true\n"); + const std::string nestedProjectId = nestedProject->id; + + fs::remove(directory.path() / "nested" / "rls.json"); + refresh = projects.refreshOpenDocuments(); + ASSERT_TRUE(refresh.errors.empty()); + ASSERT_EQ(refresh.removedProjectIds.size(), 1); + EXPECT_EQ(refresh.removedProjectIds.front(), nestedProjectId); + ASSERT_NE(projects.projectForDocument(uri), nullptr); + EXPECT_EQ(projects.projectForDocument(uri)->id, outerProjectId); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/rename_service_tests.cpp b/lsp/tests/rename_service_tests.cpp new file mode 100644 index 0000000..d9457c0 --- /dev/null +++ b/lsp/tests/rename_service_tests.cpp @@ -0,0 +1,290 @@ +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/project_manager.h" +#include "rls/lsp/rename_service.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::DocumentStore; +using rls::lsp::ProjectManager; +using rls::lsp::RenameError; +using rls::lsp::RenameService; + +struct RenameFixture { + fs::path root = fs::temp_directory_path() / "rls-rename-service"; + fs::path declarationPath = root / "declaration.rls"; + fs::path usagePath = root / "usage.rls"; + std::string declarationUri = *rls::lsp::PathToFileUri(declarationPath); + std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + DocumentStore documents; + ProjectManager projects; + AnalysisScheduler scheduler; + + RenameFixture(std::string declaration, std::string usage) + : projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }), + scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }) { + EXPECT_EQ(documents.open(declarationUri, "rls", 7, declaration), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(documents.open(usageUri, "rls", 11, usage), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(projects.documentOpened(declarationUri), + rls::lsp::ProjectAssignmentResult::Assigned); + EXPECT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + EXPECT_NE(project, nullptr); + if (!project) return; + EXPECT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), std::move(declaration)}, + {usagePath.generic_string(), std::move(usage)}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + } +}; + +TEST(RenameServiceTests, ProducesVersionedCrossFileEdits) { + RenameFixture fixture( + "define target(): true\n", + "define caller(): target() and target()\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + const auto prepared = rename.prepare(fixture.usageUri, {0, 18}); + ASSERT_TRUE(prepared.value); + EXPECT_EQ(prepared.value->start.character, 17u); + EXPECT_EQ(prepared.value->end.character, 23u); + EXPECT_TRUE(rename.prepare(fixture.usageUri, {0, 23}).value); + + const auto result = rename.rename(fixture.usageUri, {0, 23}, "replacement", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + EXPECT_EQ(result.value->documents[0].uri, fixture.declarationUri); + EXPECT_EQ(result.value->documents[0].version, 7); + ASSERT_EQ(result.value->documents[0].edits.size(), 1u); + EXPECT_EQ(result.value->documents[1].uri, fixture.usageUri); + EXPECT_EQ(result.value->documents[1].version, 11); + ASSERT_EQ(result.value->documents[1].edits.size(), 2u); + EXPECT_EQ(result.value->documents[1].edits[0].newText, "replacement"); +} + +TEST(RenameServiceTests, RenamesExternDefineAcrossCallsAndFunctionReferences) { + RenameFixture fixture( + "extern define host() -> Bool\n", + "define caller(): host()\n" + "define reference(): host\n" + "define occupied(): true\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {0, 18}).value); + ASSERT_TRUE(rename.prepare(fixture.usageUri, {0, 21}).value); + const auto result = rename.rename( + fixture.usageUri, {0, 21}, "platform_host", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + ASSERT_EQ(result.value->documents[0].edits.size(), 1u); + EXPECT_EQ(result.value->documents[0].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[0].edits[0].range.start.character, 14u); + ASSERT_EQ(result.value->documents[1].edits.size(), 2u); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[1].edits[1].range.start.line, 1u); + EXPECT_EQ(rename.rename( + fixture.usageUri, {0, 21}, "occupied", true).error, + RenameError::Collision); +} + +TEST(RenameServiceTests, RenamesExternEnumTypeAndExplicitMembers) { + RenameFixture fixture( + "extern enum Color { RED, BLUE, COLOR_* }\n" + "extern define choose(color: Color) -> Bool\n" + "enum Other { VALUE }\n", + "define use(): choose(Color.RED) and choose(RED)\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {0, 14}).value); + ASSERT_TRUE(rename.prepare(fixture.usageUri, {0, 23}).value); + const auto typeResult = rename.rename( + fixture.usageUri, {0, 23}, "Palette", true); + ASSERT_TRUE(typeResult.value); + ASSERT_EQ(typeResult.value->documents.size(), 2u); + ASSERT_EQ(typeResult.value->documents[0].edits.size(), 2u); + ASSERT_EQ(typeResult.value->documents[1].edits.size(), 1u); + + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {0, 21}).value); + ASSERT_TRUE(rename.prepare(fixture.usageUri, {0, 28}).value); + const auto memberResult = rename.rename( + fixture.usageUri, {0, 28}, "CRIMSON", true); + ASSERT_TRUE(memberResult.value); + ASSERT_EQ(memberResult.value->documents.size(), 2u); + ASSERT_EQ(memberResult.value->documents[0].edits.size(), 1u); + ASSERT_EQ(memberResult.value->documents[1].edits.size(), 2u); + + EXPECT_EQ(rename.rename( + fixture.usageUri, {0, 28}, "BLUE", true).error, + RenameError::Collision); + EXPECT_EQ(rename.rename( + fixture.usageUri, {0, 28}, "COLOR_NEW", true).error, + RenameError::Collision); + EXPECT_EQ(rename.rename( + fixture.usageUri, {0, 23}, "Other", true).error, + RenameError::Collision); + EXPECT_EQ(rename.prepare(fixture.declarationUri, {0, 34}).error, + RenameError::NotRenameable); +} + +TEST(RenameServiceTests, RenamesRegionExitKeysAcrossBaseAndExtension) { + RenameFixture fixture( + "region RR_TARGET { name: \"Target\" }\n" + "region RR_SOURCE { name: \"Source\" exits { RR_TARGET: true } }\n", + "extend region RR_SOURCE { exits { RR_TARGET: true } }\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + const auto result = rename.rename( + fixture.declarationUri, {0, 8}, "RR_RENAMED", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + ASSERT_EQ(result.value->documents[0].edits.size(), 2u); + EXPECT_EQ(result.value->documents[0].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[0].edits[1].range.start.line, 1u); + ASSERT_EQ(result.value->documents[1].edits.size(), 1u); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.line, 0u); +} + +TEST(RenameServiceTests, RenamesOnlyExactPatternBackedExitValue) { + RenameFixture fixture( + "extern enum Region { RR_* }\n" + "region RR_LOCAL { exits { RR_EXTERNAL: true RR_OTHER: true } }\n", + "region RR_SECOND { exits { RR_EXTERNAL: true } }\n" + "define region_value(): RR_EXTERNAL\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {1, 28}).value); + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {1, 37}).value); + const auto result = rename.rename( + fixture.declarationUri, {1, 37}, "RR_RENAMED", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + ASSERT_EQ(result.value->documents[0].edits.size(), 1u); + EXPECT_EQ(result.value->documents[0].edits[0].range.start.line, 1u); + ASSERT_EQ(result.value->documents[1].edits.size(), 2u); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[1].edits[1].range.start.line, 1u); + + EXPECT_EQ(rename.rename( + fixture.declarationUri, {1, 28}, "EXTERNAL", true).error, + RenameError::InvalidName); + EXPECT_EQ(rename.rename( + fixture.declarationUri, {1, 28}, "RR_OTHER", true).error, + RenameError::Collision); +} + +TEST(RenameServiceTests, RenamesEventDeclarationsAcrossRegionsAndExpressions) { + RenameFixture fixture( + "region RR_FIRST { events { EVENT_SHARED: true } }\n" + "region RR_SECOND { events { EVENT_SHARED: true } }\n", + "region RR_THIRD { events { EVENT_SHARED: true } }\n" + "define event_value(): EVENT_SHARED\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + const auto result = rename.rename( + fixture.declarationUri, {0, 29}, "EVENT_RENAMED", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + ASSERT_EQ(result.value->documents[0].edits.size(), 2u); + EXPECT_EQ(result.value->documents[0].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[0].edits[1].range.start.line, 1u); + ASSERT_EQ(result.value->documents[1].edits.size(), 2u); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[1].edits[1].range.start.line, 1u); +} + +TEST(RenameServiceTests, RenamesRegionDataKeysAcrossAllRegions) { + RenameFixture fixture( + "region RR_FIRST { worldNode: \"First\" scene: SCENE_FIRST }\n" + "region RR_SECOND { worldNode: \"Second\" scene: SCENE_SECOND }\n", + "region RR_THIRD { worldNode: \"Third\" scene: SCENE_THIRD }\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {0, 20}).value); + ASSERT_TRUE(rename.prepare(fixture.declarationUri, {0, 27}).value); + const auto result = rename.rename( + fixture.declarationUri, {0, 27}, "graphNode", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + ASSERT_EQ(result.value->documents[0].edits.size(), 2u); + EXPECT_EQ(result.value->documents[0].edits[0].range.start.line, 0u); + EXPECT_EQ(result.value->documents[0].edits[1].range.start.line, 1u); + ASSERT_EQ(result.value->documents[1].edits.size(), 1u); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.line, 0u); + + EXPECT_EQ(rename.rename( + fixture.declarationUri, {0, 20}, "scene", true).error, + RenameError::Collision); +} + +TEST(RenameServiceTests, KeepsSameNamedParametersInTheirDefineScope) { + RenameFixture fixture( + "define first(value: Bool): value\n", + "define second(value: Bool): value\n" + "define caller(): first(value: true)\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + const auto result = rename.rename(fixture.declarationUri, {0, 14}, "item", true); + ASSERT_TRUE(result.value); + ASSERT_EQ(result.value->documents.size(), 2u); + ASSERT_EQ(result.value->documents[0].edits.size(), 2u); + EXPECT_EQ(result.value->documents[0].uri, fixture.declarationUri); + ASSERT_EQ(result.value->documents[1].edits.size(), 1u); + EXPECT_EQ(result.value->documents[1].uri, fixture.usageUri); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.line, 1u); + EXPECT_EQ(result.value->documents[1].edits[0].range.start.character, 23u); +} + +TEST(RenameServiceTests, RejectsReservedCollisionAndUnsupportedClient) { + RenameFixture fixture( + "extern define host() -> Bool\n", + "define existing(): true\ndefine caller(): host()\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + + EXPECT_TRUE(rename.prepare(fixture.usageUri, {1, 18}).value); + EXPECT_EQ(rename.rename(fixture.usageUri, {0, 8}, "region", true).error, + RenameError::InvalidName); + EXPECT_EQ(rename.rename(fixture.usageUri, {0, 8}, "caller", true).error, + RenameError::Collision); + EXPECT_EQ(rename.rename(fixture.usageUri, {0, 8}, "renamed", false).error, + RenameError::UnsupportedClient); +} + +TEST(RenameServiceTests, RejectsStaleSnapshot) { + RenameFixture fixture( + "define target(): true\n", + "define caller(): target()\n"); + RenameService rename(fixture.documents, fixture.projects, fixture.scheduler); + ASSERT_EQ(fixture.projects.documentChanged(fixture.usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + + EXPECT_EQ(rename.prepare(fixture.usageUri, {0, 18}).error, RenameError::StaleSnapshot); + EXPECT_EQ(rename.rename(fixture.usageUri, {0, 18}, "other", true).error, + RenameError::StaleSnapshot); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/semantic_tokens_service_tests.cpp b/lsp/tests/semantic_tokens_service_tests.cpp new file mode 100644 index 0000000..a4856fb --- /dev/null +++ b/lsp/tests/semantic_tokens_service_tests.cpp @@ -0,0 +1,323 @@ +#include +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/semantic_tokens_service.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::DocumentStore; +using rls::lsp::ProjectManager; +using rls::lsp::SemanticTokensService; + +struct DecodedToken { + uint32_t line; + uint32_t character; + uint32_t length; + uint32_t type; + uint32_t modifiers; +}; + +std::vector decode(const std::vector& data) { + EXPECT_EQ(data.size() % 5, 0u); + std::vector result; + uint32_t line = 0; + uint32_t character = 0; + for (size_t index = 0; index + 4 < data.size(); index += 5) { + line += data[index]; + character = data[index] == 0 ? character + data[index + 1] : data[index + 1]; + result.push_back({line, character, data[index + 2], data[index + 3], data[index + 4]}); + } + return result; +} + +const DecodedToken* tokenAt( + const std::vector& tokens, uint32_t line, uint32_t character) { + const auto found = std::find_if(tokens.begin(), tokens.end(), [&](const auto& token) { + return token.line == line && token.character == character; + }); + return found == tokens.end() ? nullptr : &*found; +} + +struct SemanticTokensFixture { + fs::path path = fs::temp_directory_path() / "rls-semantic-tokens.rls"; + std::string uri = *rls::lsp::PathToFileUri(path); + DocumentStore documents; + ProjectManager projects; + AnalysisScheduler scheduler; + + explicit SemanticTokensFixture(std::string source) + : projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {path}; + project.isStandalone = true; + return project; + }), + scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }) { + EXPECT_EQ(documents.open(uri, "rls", 1, source), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(uri); + EXPECT_NE(project, nullptr); + if (!project) return; + EXPECT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{path.generic_string(), std::move(source)}}, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + } + + std::vector tokens() { + return decode(SemanticTokensService(projects, scheduler).full(uri)); + } +}; + +TEST(SemanticTokensServiceTests, EncodesResolvedCategoriesAndModifiers) { + SemanticTokensFixture fixture( + "extern enum Color { RED }\n" + "extern define paint(color: Color) -> Bool\n" + "define use(input: Color): paint(input == RED)\n" + "region RR_TEST { name: \"Test\" events { EVENT_TEST: true } exits { RR_TEST: true RR_UNKNOWN: true } }\n" + "extend region RR_TEST {}\n" + "define event_value(): EVENT_TEST\n" + "define region_value(): RR_TEST\n"); + + const auto tokens = fixture.tokens(); + const auto* externEnum = tokenAt(tokens, 0, 12); + const auto* enumMember = tokenAt(tokens, 0, 20); + const auto* externFunction = tokenAt(tokens, 1, 14); + const auto* define = tokenAt(tokens, 2, 7); + const auto* parameter = tokenAt(tokens, 2, 11); + const auto* call = tokenAt(tokens, 2, 26); + const auto* parameterUse = tokenAt(tokens, 2, 32); + const auto* memberUse = tokenAt(tokens, 2, 41); + const auto* region = tokenAt(tokens, 3, 7); + const auto* property = tokenAt(tokens, 3, 17); + const auto* entry = tokenAt(tokens, 3, 39); + const auto* exit = tokenAt(tokens, 3, 66); + const auto* unresolvedExit = tokenAt(tokens, 3, 80); + const auto* extensionTarget = tokenAt(tokens, 4, 14); + const auto* entryUse = tokenAt(tokens, 5, 22); + const auto* regionUse = tokenAt(tokens, 6, 23); + + ASSERT_NE(externEnum, nullptr); + EXPECT_EQ(externEnum->type, 2u); + EXPECT_EQ(externEnum->modifiers, 9u); + ASSERT_NE(enumMember, nullptr); + EXPECT_EQ(enumMember->type, 3u); + EXPECT_EQ(enumMember->modifiers, 13u); + ASSERT_NE(externFunction, nullptr); + EXPECT_EQ(externFunction->type, 0u); + EXPECT_EQ(externFunction->modifiers, 9u); + ASSERT_NE(define, nullptr); + EXPECT_EQ(define->type, 0u); + EXPECT_EQ(define->modifiers, 2u); + ASSERT_NE(parameter, nullptr); + EXPECT_EQ(parameter->type, 1u); + EXPECT_EQ(parameter->modifiers, 1u); + ASSERT_NE(call, nullptr); + EXPECT_EQ(call->type, 0u); + EXPECT_EQ(call->modifiers, 8u); + ASSERT_NE(parameterUse, nullptr); + EXPECT_EQ(parameterUse->type, 1u); + EXPECT_EQ(parameterUse->modifiers, 0u); + ASSERT_NE(memberUse, nullptr); + EXPECT_EQ(memberUse->type, 3u); + EXPECT_EQ(memberUse->modifiers, 12u); + EXPECT_EQ(region, nullptr); + EXPECT_EQ(property, nullptr); + ASSERT_NE(entry, nullptr); + EXPECT_EQ(entry->type, 4u); + EXPECT_EQ(entry->modifiers, 1u); + ASSERT_NE(exit, nullptr); + EXPECT_EQ(exit->type, 4u); + EXPECT_EQ(exit->modifiers, 0u); + EXPECT_EQ(unresolvedExit, nullptr); + ASSERT_NE(entryUse, nullptr); + EXPECT_EQ(entryUse->type, 3u); + EXPECT_EQ(entryUse->modifiers, 0u); + ASSERT_NE(regionUse, nullptr); + EXPECT_EQ(regionUse->type, 3u); + EXPECT_EQ(regionUse->modifiers, 4u); + ASSERT_NE(extensionTarget, nullptr); + EXPECT_EQ(extensionTarget->type, 3u); + EXPECT_EQ(extensionTarget->modifiers, 4u); +} + +TEST(SemanticTokensServiceTests, UsesUtf16ColumnsAndOmitsUnresolvedNames) { + SemanticTokensFixture utf16( + "define check(value: Bool): \"😀\" == value\n"); + const auto utf16Tokens = utf16.tokens(); + const auto* parameterUse = tokenAt(utf16Tokens, 0, 35); + ASSERT_NE(parameterUse, nullptr); + EXPECT_EQ(parameterUse->type, 1u); + EXPECT_EQ(parameterUse->length, 5u); + + SemanticTokensFixture ambiguous( + "enum Alpha { SHARED }\n" + "enum Beta { SHARED }\n" + "define use(): SHARED\n"); + const auto ambiguousTokens = ambiguous.tokens(); + EXPECT_EQ(tokenAt(ambiguousTokens, 2, 14), nullptr); +} + +TEST(SemanticTokensServiceTests, HighlightsPatternBackedExitAsPlainProperty) { + SemanticTokensFixture fixture( + "extern enum Region { RR_* }\n" + "region RR_LOCAL { exits { RR_EXTERNAL: true } }\n"); + + const auto tokens = fixture.tokens(); + const auto* exit = tokenAt(tokens, 1, 26); + ASSERT_NE(exit, nullptr); + EXPECT_EQ(exit->type, 4u); + EXPECT_EQ(exit->modifiers, 0u); +} + +TEST(SemanticTokensServiceTests, EmitsLogicalOperatorsWithTheSameType) { + const std::string source = "define check(): true or (true and true)\n"; + SemanticTokensFixture fixture(source); + + const auto tokens = fixture.tokens(); + const auto orPosition = static_cast(source.find("or")); + const auto andPosition = static_cast(source.find("and")); + + const auto* orToken = tokenAt(tokens, 0, orPosition); + const auto* andToken = tokenAt(tokens, 0, andPosition); + + ASSERT_NE(orToken, nullptr); + ASSERT_NE(andToken, nullptr); + EXPECT_EQ(orToken->type, andToken->type); + EXPECT_EQ(orToken->type, 6u); + EXPECT_EQ(orToken->length, 2u); + EXPECT_EQ(andToken->length, 3u); +} + +TEST(SemanticTokensServiceTests, HighlightsConcreteValuesResolvedThroughUniquePatterns) { + SemanticTokensFixture fixture( + "extern enum Item { RG_* }\n" + "define use(): RG_HOOKSHOT == Item.RG_BOW\n"); + + const auto tokens = fixture.tokens(); + const auto* pattern = tokenAt(tokens, 0, 19); + const auto* bare = tokenAt(tokens, 1, 14); + const auto* qualified = tokenAt(tokens, 1, 34); + + EXPECT_EQ(pattern, nullptr); + ASSERT_NE(bare, nullptr); + EXPECT_EQ(bare->type, 3u); + EXPECT_EQ(bare->modifiers, 12u); + ASSERT_NE(qualified, nullptr); + EXPECT_EQ(qualified->type, 3u); + EXPECT_EQ(qualified->modifiers, 12u); +} + +TEST(SemanticTokensServiceTests, HighlightsWildcardValuesInExternParameterDefaults) { + SemanticTokensFixture fixture( + "extern enum Region { RR_* }\n" + "extern define spirit_shared(value: Region = RR_NONE) -> Bool\n"); + + const auto tokens = fixture.tokens(); + const auto* regionDefault = tokenAt(tokens, 1, 44); + + ASSERT_NE(regionDefault, nullptr); + EXPECT_EQ(regionDefault->type, 3u); + EXPECT_EQ(regionDefault->modifiers, 12u); +} + +TEST(SemanticTokensServiceTests, HighlightsBuiltinExternReturnTypes) { + SemanticTokensFixture fixture( + "extern define test1(item: Item) -> Item\n" + "extern define test2(bool: Bool) -> Bool\n"); + + const auto tokens = fixture.tokens(); + const auto* item = tokenAt(tokens, 0, 35); + const auto* boolean = tokenAt(tokens, 1, 35); + + ASSERT_NE(item, nullptr); + EXPECT_EQ(item->type, 2u); + EXPECT_EQ(item->modifiers, 8u); + ASSERT_NE(boolean, nullptr); + EXPECT_EQ(boolean->type, 2u); + EXPECT_EQ(boolean->modifiers, 8u); +} + +TEST(SemanticTokensServiceTests, ReturnsEmptyForMalformedOrStaleDocument) { + SemanticTokensFixture malformed("define broken("); + EXPECT_TRUE(malformed.tokens().empty()); + + SemanticTokensFixture stale("define check(): true\n"); + ASSERT_EQ(stale.projects.documentChanged(stale.uri), + rls::lsp::ProjectAssignmentResult::Assigned); + EXPECT_TRUE(stale.tokens().empty()); +} + +TEST(SemanticTokensServiceTests, ExpeditesLatestScheduledGeneration) { + const fs::path path = fs::temp_directory_path() / + "rls-immediate-semantic-tokens.rls"; + const std::string uri = *rls::lsp::PathToFileUri(path); + const std::string initial = "define check(flag: Bool): flag\n"; + DocumentStore documents; + ASSERT_EQ(documents.open(uri, "rls", 1, initial), + rls::lsp::DocumentUpdateResult::Applied); + ProjectManager projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {path}; + project.isStandalone = true; + return project; + }); + ASSERT_EQ(projects.documentOpened(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + auto* project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + AnalysisScheduler scheduler({ + .debounce = std::chrono::seconds(5), + .maximumConcurrency = 1, + }); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{path.generic_string(), initial}}, + project->documentGeneration, + project->manifestGeneration, + })); + ASSERT_NE(scheduler.awaitSnapshot( + project->id, project->generation, std::chrono::seconds(1)), nullptr); + + const std::string changed = "define renamed(flag: Bool): flag\n"; + ASSERT_EQ(documents.applyFullChange(uri, 2, changed), + rls::lsp::DocumentUpdateResult::Applied); + ASSERT_EQ(projects.documentChanged(uri), + rls::lsp::ProjectAssignmentResult::Assigned); + project = projects.projectForDocument(uri); + ASSERT_NE(project, nullptr); + ASSERT_TRUE(scheduler.schedule({ + project->id, + project->generation, + {{path.generic_string(), changed}}, + project->documentGeneration, + project->manifestGeneration, + })); + + const auto tokens = decode(SemanticTokensService(projects, scheduler).full(uri)); + + const auto* renamed = tokenAt(tokens, 0, 7); + ASSERT_NE(renamed, nullptr); + EXPECT_EQ(renamed->length, 7u); + EXPECT_EQ(renamed->type, 0u); +} + +} // namespace diff --git a/lsp/tests/server_composition_root_tests.cpp b/lsp/tests/server_composition_root_tests.cpp new file mode 100644 index 0000000..5bfc033 --- /dev/null +++ b/lsp/tests/server_composition_root_tests.cpp @@ -0,0 +1,944 @@ +#include +#include +#include + +#include +#include + +#include "rls/lsp/server_composition_root.h" +#include "rls/lsp/document_uri.h" + +namespace fs = std::filesystem; + +namespace { + +using Json = nlohmann::json; +using rls::lsp::ServerCompositionRoot; + +rls::project::FileProject standaloneProject(const std::filesystem::path& path) { + rls::project::FileProject project; + project.sourceFiles.push_back(path.lexically_normal()); + project.isStandalone = true; + return project; +} + +TEST(ServerCompositionRootTests, RegistersOnlyImplementedRoutes) { + ServerCompositionRoot server; + + EXPECT_TRUE(server.router().contains("initialize")); + EXPECT_TRUE(server.router().contains("textDocument/didOpen")); + EXPECT_TRUE(server.router().contains("workspace/didChangeWorkspaceFolders")); + EXPECT_TRUE(server.router().contains("workspace/didChangeWatchedFiles")); + EXPECT_TRUE(server.router().contains("textDocument/definition")); + EXPECT_TRUE(server.router().contains("textDocument/references")); + EXPECT_TRUE(server.router().contains("textDocument/prepareRename")); + EXPECT_TRUE(server.router().contains("textDocument/rename")); + EXPECT_TRUE(server.router().contains("textDocument/documentHighlight")); + EXPECT_TRUE(server.router().contains("textDocument/documentSymbol")); + EXPECT_TRUE(server.router().contains("textDocument/completion")); + EXPECT_TRUE(server.router().contains("textDocument/signatureHelp")); + EXPECT_TRUE(server.router().contains("textDocument/hover")); + EXPECT_TRUE(server.router().contains("textDocument/semanticTokens/full")); + EXPECT_TRUE(server.router().contains("workspace/symbol")); + EXPECT_FALSE(server.router().contains("textDocument/publishDiagnostics")); +} + +TEST(ServerCompositionRootTests, AdvertisesImplementedTextDocumentFeatures) { + ServerCompositionRoot server; + const auto responses = server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + + ASSERT_EQ(responses.size(), 1); + const auto result = Json::parse(responses.front())["result"]; + EXPECT_EQ(result["capabilities"]["textDocumentSync"]["change"], 1); + EXPECT_TRUE(result["capabilities"]["workspace"]["workspaceFolders"]["supported"]); + EXPECT_EQ(result["capabilities"]["definitionProvider"], true); + EXPECT_EQ(result["capabilities"]["referencesProvider"], true); + EXPECT_EQ(result["capabilities"]["renameProvider"]["prepareProvider"], true); + EXPECT_EQ(result["capabilities"]["documentHighlightProvider"], true); + EXPECT_EQ(result["capabilities"]["documentSymbolProvider"], true); + EXPECT_EQ(result["capabilities"]["completionProvider"]["resolveProvider"], false); + EXPECT_EQ(result["capabilities"]["signatureHelpProvider"]["triggerCharacters"], + Json::array({"(", ","})); + EXPECT_EQ(result["capabilities"]["hoverProvider"], true); + EXPECT_EQ(result["capabilities"]["semanticTokensProvider"]["legend"]["tokenTypes"], + Json::array({"function", "parameter", "enum", "enumMember", "property", "variable", "operator"})); + EXPECT_EQ(result["capabilities"]["semanticTokensProvider"]["legend"]["tokenModifiers"], + Json::array({"declaration", "definition", "readonly", "defaultLibrary", "deprecated"})); + EXPECT_EQ(result["capabilities"]["semanticTokensProvider"]["range"], false); + EXPECT_EQ(result["capabilities"]["semanticTokensProvider"]["full"], true); + EXPECT_EQ(result["capabilities"]["workspaceSymbolProvider"], true); +} + +TEST(ServerCompositionRootTests, RoutesCompletionWithActiveTokenTextEdit) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-completion-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "def\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/completion"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 0}, {"character", 3}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_FALSE(result.empty()); + EXPECT_EQ(result[0]["label"], "define"); + EXPECT_EQ(result[0]["kind"], 14); + EXPECT_EQ(result[0]["textEdit"]["newText"], "define"); + EXPECT_EQ(result[0]["insertTextFormat"], 1); + EXPECT_EQ(result[0]["textEdit"]["range"]["start"]["character"], 0); + EXPECT_EQ(result[0]["textEdit"]["range"]["end"]["character"], 3); +} + +TEST(ServerCompositionRootTests, RoutesCompletionImmediatelyAfterDocumentChange) { + const fs::path sourcePath = fs::temp_directory_path() / + "rls-immediate-completion-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "def\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didChange"}, + {"params", { + {"textDocument", {{"uri", uri}, {"version", 2}}}, + {"contentChanges", Json::array({{{"text", "reg\n"}}})}, + }}, + }.dump()); + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/completion"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 0}, {"character", 3}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_FALSE(result.empty()); + EXPECT_EQ(result[0]["label"], "region"); +} + +TEST(ServerCompositionRootTests, RoutesSignatureHelpWithActiveNamedParameter) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-signature-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string usage = "define use(): paint(enabled: false, true)\n"; + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", + "extern define paint(color: Bool, enabled: Bool = true) -> Bool\n" + + usage}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/signatureHelp"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", { + {"line", 1}, + {"character", usage.find("false") + 2}, + }}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_EQ(result["signatures"].size(), 1u); + EXPECT_EQ(result["activeSignature"], 0); + EXPECT_EQ(result["activeParameter"], 1); + EXPECT_EQ(result["signatures"][0]["activeParameter"], 1); + EXPECT_EQ(result["signatures"][0]["label"], + "extern paint(color: Bool, enabled: Bool = true) -> Bool"); + EXPECT_EQ(result["signatures"][0]["parameters"][1]["label"], + "enabled: Bool = true"); + EXPECT_EQ(result["signatures"][0]["documentation"]["kind"], "markdown"); +} + +TEST(ServerCompositionRootTests, AdvancesSignatureImmediatelyAfterCommaEdit) { + const fs::path sourcePath = fs::temp_directory_path() / + "rls-signature-comma-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string before = + "extern define target(first: Bool, second: Bool) -> Bool\n" + "define use(): target(true)\n"; + const std::string after = + "extern define target(first: Bool, second: Bool) -> Bool\n" + "define use(): target(true,)\n"; + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", before}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didChange"}, + {"params", { + {"textDocument", {{"uri", uri}, {"version", 2}}}, + {"contentChanges", Json::array({{{"text", after}}})}, + }}, + }.dump()); + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/signatureHelp"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 1}, {"character", 26}}}, + {"context", { + {"triggerKind", 2}, + {"triggerCharacter", ","}, + {"isRetrigger", true}, + }}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_FALSE(result.is_null()); + EXPECT_EQ(result["activeParameter"], 1); + EXPECT_EQ(result["signatures"][0]["activeParameter"], 1); +} + +TEST(ServerCompositionRootTests, ReturnsNullSignatureHelpForUnresolvedCall) { + const fs::path sourcePath = fs::temp_directory_path() / + "rls-unresolved-signature-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string source = "define use(): missing(R"; + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", source}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/signatureHelp"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 0}, {"character", source.size()}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + EXPECT_TRUE(Json::parse(responses.front())["result"].is_null()); +} + +TEST(ServerCompositionRootTests, RoutesHoverWithMarkdownAndRange) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-hover-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const std::string source = + "extern define target(value: Bool = true) -> Bool\n" + "define use(): target(false)\n"; + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", source}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/hover"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 1}, {"character", 16}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + EXPECT_EQ(result["contents"]["kind"], "markdown"); + EXPECT_NE(result["contents"]["value"].get().find( + "extern target(value: Bool = true) -> Bool"), std::string::npos); + EXPECT_EQ(result["range"]["start"]["line"], 1); + EXPECT_EQ(result["range"]["start"]["character"], 14); + EXPECT_EQ(result["range"]["end"]["character"], 20); +} + +TEST(ServerCompositionRootTests, ReturnsNullHoverForUnresolvedName) { + const fs::path sourcePath = fs::temp_directory_path() / + "rls-unresolved-hover-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "define use(): missing\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/hover"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 0}, {"character", 16}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + EXPECT_TRUE(Json::parse(responses.front())["result"].is_null()); +} + +TEST(ServerCompositionRootTests, RoutesFullSemanticTokensAsDeltaEncodedData) { + const fs::path sourcePath = fs::temp_directory_path() / + "rls-semantic-token-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "define check(flag: Bool): flag\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/semanticTokens/full"}, + {"params", {{"textDocument", {{"uri", uri}}}}}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + EXPECT_EQ(Json::parse(responses.front())["result"]["data"], Json::array({ + 0, 7, 5, 0, 2, + 0, 6, 4, 1, 1, + 0, 6, 4, 2, 8, + 0, 7, 4, 1, 0, + })); +} + +TEST(ServerCompositionRootTests, NegotiatesCompletionSnippetsWithPlainFallback) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-snippet-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + const auto complete = [&](bool snippetSupport, + std::optional indentationMode, + std::string text, + uint32_t line, uint32_t character) { + ServerCompositionRoot server(standaloneProject); + Json initializeParams = Json::object(); + if (snippetSupport) { + initializeParams = { + {"capabilities", {{"textDocument", {{"completion", { + {"completionItem", {{"snippetSupport", true}}}, + }}}}}}, + }; + } + if (indentationMode) { + initializeParams["initializationOptions"] = { + {"completion", { + {"sectionSnippetIndentation", *indentationMode}, + }}, + }; + } + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 1}, + {"method", "initialize"}, + {"params", std::move(initializeParams)}, + }.dump()); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", std::move(text)}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/completion"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", line}, {"character", character}}}, + }}, + }.dump()); + EXPECT_EQ(responses.size(), 1u); + return Json::parse(responses.front())["result"]; + }; + const auto find = [](const Json& items, std::string_view label) -> Json { + for (const auto& item : items) { + if (item.at("label").get() == label) return item; + } + return Json(nullptr); + }; + + const std::string regionText = + "region RR_TEMPLATE { customField: true }\n" + "region RR_TEST {\n" + " \n" + "}\n"; + const auto plainField = find( + complete(false, std::nullopt, regionText, 2, 2), "customField"); + ASSERT_FALSE(plainField.is_null()); + EXPECT_EQ(plainField["insertTextFormat"], 1); + EXPECT_EQ(plainField["textEdit"]["newText"], "customField"); + + const auto serverItems = complete(true, std::nullopt, regionText, 2, 2); + const auto serverEvents = find(serverItems, "events"); + ASSERT_FALSE(serverEvents.is_null()); + EXPECT_EQ(serverEvents["insertTextMode"], 1); + EXPECT_EQ(serverEvents["textEdit"]["newText"], "events {\n $0\n }"); + + const auto snippetItems = complete(true, "client", regionText, 2, 2); + const auto snippetField = find(snippetItems, "customField"); + ASSERT_FALSE(snippetField.is_null()); + EXPECT_EQ(snippetField["insertTextFormat"], 2); + EXPECT_EQ(snippetField["textEdit"]["newText"], "customField: ${1}"); + const auto snippetEvents = find(snippetItems, "events"); + ASSERT_FALSE(snippetEvents.is_null()); + EXPECT_EQ(snippetEvents["insertTextFormat"], 2); + EXPECT_EQ(snippetEvents["insertTextMode"], 2); + EXPECT_EQ(snippetEvents["textEdit"]["newText"], "events {\n $0\n}"); + + const auto plainKeyword = find( + complete(true, "client", "def\n", 0, 3), "define"); + ASSERT_FALSE(plainKeyword.is_null()); + EXPECT_EQ(plainKeyword["insertTextFormat"], 1); + EXPECT_EQ(plainKeyword["textEdit"]["newText"], "define"); +} + +TEST(ServerCompositionRootTests, RoutesDefinitionFromAcceptedSnapshot) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-definition-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload(R"({ + "jsonrpc":"2.0","id":1,"method":"initialize","params":{ + "capabilities":{"textDocument":{"definition":{"linkSupport":true}}} + } + })"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "define target(): true\ndefine caller(): target()\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/definition"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 1}, {"character", 18}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_EQ(result.size(), 1); + EXPECT_EQ(result[0]["targetUri"], uri); + EXPECT_EQ(result[0]["originSelectionRange"]["start"]["character"], 17); + EXPECT_EQ(result[0]["targetSelectionRange"]["start"]["character"], 7); +} + +TEST(ServerCompositionRootTests, FallsBackToLocationWithoutDefinitionLinkSupport) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-definition-location.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "define target(): true\ndefine caller(): target()\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/definition"}, + {"params", { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 1}, {"character", 18}}}, + }}, + }.dump()); + + ASSERT_EQ(responses.size(), 1); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_EQ(result.size(), 1); + EXPECT_EQ(result[0]["uri"], uri); + EXPECT_EQ(result[0]["range"]["start"]["character"], 7); + EXPECT_FALSE(result[0].contains("targetUri")); +} + +TEST(ServerCompositionRootTests, RoutesReferencesAndDocumentHighlights) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-references-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "define target(): true\ndefine caller(): target() and target()\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto request = [&](std::string method, Json extra) { + Json params = { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 1}, {"character", 18}}}, + }; + if (!extra.is_null()) { + params.update(std::move(extra)); + } + return server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", std::move(method)}, + {"params", std::move(params)}, + }.dump()); + }; + + const auto referencesResponse = request( + "textDocument/references", {{"context", {{"includeDeclaration", false}}}}); + ASSERT_EQ(referencesResponse.size(), 1u); + const auto references = Json::parse(referencesResponse.front())["result"]; + ASSERT_EQ(references.size(), 2u); + EXPECT_EQ(references[0]["range"]["start"]["character"], 17); + EXPECT_EQ(references[1]["range"]["start"]["character"], 30); + + const auto highlightsResponse = request("textDocument/documentHighlight", {}); + ASSERT_EQ(highlightsResponse.size(), 1u); + const auto highlights = Json::parse(highlightsResponse.front())["result"]; + ASSERT_EQ(highlights.size(), 3u); + EXPECT_EQ(highlights[0]["kind"], 1); + EXPECT_EQ(highlights[0]["range"]["start"]["line"], 0); +} + +TEST(ServerCompositionRootTests, RoutesPrepareRenameAndVersionedRename) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-rename-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload(R"({ + "jsonrpc":"2.0","id":1,"method":"initialize","params":{ + "capabilities":{"workspace":{"workspaceEdit":{"documentChanges":true}}} + } + })"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 4}, + {"text", "define target(): true\ndefine caller(): target()\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto request = [&](std::string method, Json extra) { + Json params = { + {"textDocument", {{"uri", uri}}}, + {"position", {{"line", 1}, {"character", 18}}}, + }; + if (!extra.is_null()) { + params.update(std::move(extra)); + } + return Json::parse(server.handlePayload(Json{ + {"jsonrpc", "2.0"}, {"id", 2}, {"method", std::move(method)}, + {"params", std::move(params)}, + }.dump()).front()); + }; + + const auto prepared = request("textDocument/prepareRename", {}); + EXPECT_EQ(prepared["result"]["start"]["character"], 17); + EXPECT_EQ(prepared["result"]["end"]["character"], 23); + + const auto renamed = request("textDocument/rename", {{"newName", "replacement"}}); + const auto& changes = renamed["result"]["documentChanges"]; + ASSERT_EQ(changes.size(), 1u); + EXPECT_EQ(changes[0]["textDocument"]["uri"], uri); + EXPECT_EQ(changes[0]["textDocument"]["version"], 4); + ASSERT_EQ(changes[0]["edits"].size(), 2u); + EXPECT_EQ(changes[0]["edits"][0]["newText"], "replacement"); +} + +TEST(ServerCompositionRootTests, RoutesHierarchicalDocumentSymbols) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-document-symbol-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload(R"({ + "jsonrpc":"2.0","id":1,"method":"initialize","params":{ + "capabilities":{"textDocument":{"documentSymbol":{ + "hierarchicalDocumentSymbolSupport":true + }}} + } + })"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "define check(value: Bool): value\nenum Color { RED }\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/documentSymbol"}, + {"params", {{"textDocument", {{"uri", uri}}}}}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_EQ(result.size(), 2u); + EXPECT_EQ(result[0]["name"], "check"); + EXPECT_EQ(result[0]["kind"], 12); + ASSERT_EQ(result[0]["children"].size(), 1u); + EXPECT_EQ(result[0]["children"][0]["name"], "value"); + EXPECT_EQ(result[0]["children"][0]["kind"], 13); + EXPECT_EQ(result[0]["children"][0]["range"]["end"]["character"], 24); + EXPECT_EQ(result[0]["children"][0]["selectionRange"]["end"]["character"], 18); + EXPECT_EQ(result[1]["name"], "Color"); + EXPECT_EQ(result[1]["kind"], 10); + EXPECT_EQ(result[1]["children"][0]["kind"], 22); +} + +TEST(ServerCompositionRootTests, FallsBackToFlatDocumentSymbols) { + const fs::path sourcePath = fs::temp_directory_path() / "rls-flat-symbol-route.rls"; + const std::string uri = *rls::lsp::PathToFileUri(sourcePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", "enum Color { RED }\n"}, + }}}}, + }.dump()); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/documentSymbol"}, + {"params", {{"textDocument", {{"uri", uri}}}}}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_EQ(result.size(), 2u); + EXPECT_EQ(result[0]["name"], "Color"); + EXPECT_EQ(result[0]["location"]["uri"], uri); + EXPECT_FALSE(result[0].contains("children")); + EXPECT_EQ(result[1]["name"], "RED"); + EXPECT_EQ(result[1]["containerName"], "Color"); +} + +TEST(ServerCompositionRootTests, RoutesWorkspaceSymbolsWithoutLeakingExternalProject) { + const auto suffix = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()); + const fs::path workspaceRoot = fs::temp_directory_path() / + ("rls-workspace-symbol-root-" + suffix); + const fs::path externalRoot = fs::temp_directory_path() / + ("rls-workspace-symbol-external-" + suffix); + fs::create_directories(workspaceRoot); + fs::create_directories(externalRoot); + const fs::path insidePath = workspaceRoot / "inside.rls"; + const fs::path outsidePath = externalRoot / "outside.rls"; + const std::string insideUri = *rls::lsp::PathToFileUri(insidePath); + const std::string outsideUri = *rls::lsp::PathToFileUri(outsidePath); + ServerCompositionRoot server(standaloneProject); + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 1}, + {"method", "initialize"}, + {"params", {{"workspaceFolders", Json::array({{ + {"uri", *rls::lsp::PathToFileUri(workspaceRoot)}, + {"name", "workspace"}, + }})}}}, + }.dump()); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + const auto open = [&](const std::string& uri, std::string text) { + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"method", "textDocument/didOpen"}, + {"params", {{"textDocument", { + {"uri", uri}, + {"languageId", "rls"}, + {"version", 1}, + {"text", std::move(text)}, + }}}}, + }.dump()); + }; + open(insideUri, "define InsideTarget(): true\n"); + open(outsideUri, "define OutsideTarget(): true\n"); + server.scheduler().waitForIdle(); + + const auto responses = server.handlePayload(Json{ + {"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "workspace/symbol"}, + {"params", {{"query", "target"}}}, + }.dump()); + + ASSERT_EQ(responses.size(), 1u); + const auto result = Json::parse(responses.front())["result"]; + ASSERT_EQ(result.size(), 1u); + EXPECT_EQ(result[0]["name"], "InsideTarget"); + EXPECT_EQ(result[0]["kind"], 12); + EXPECT_EQ(result[0]["location"]["uri"], insideUri); + + std::error_code error; + fs::remove_all(workspaceRoot, error); + fs::remove_all(externalRoot, error); +} + +TEST(ServerCompositionRootTests, SynchronizesOpenChangeAndClose) { + ServerCompositionRoot server(standaloneProject); + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + server.handlePayload( + R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + server.handlePayload(R"({ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{"textDocument":{ + "uri":"file:///main.rls","languageId":"rls","version":1,"text":"old" + }} + })"); + ASSERT_NE(server.documents().find("file:///main.rls"), nullptr); + + server.handlePayload(R"({ + "jsonrpc":"2.0", + "method":"textDocument/didChange", + "params":{ + "textDocument":{"uri":"file:///main.rls","version":2}, + "contentChanges":[{"text":"new"}] + } + })"); + EXPECT_EQ(server.documents().find("file:///main.rls")->text, "new"); + + server.handlePayload(R"({ + "jsonrpc":"2.0", + "method":"textDocument/didChange", + "params":{ + "textDocument":{"uri":"file:///main.rls","version":2}, + "contentChanges":[{"text":"stale"}] + } + })"); + EXPECT_EQ(server.documents().find("file:///main.rls")->text, "new"); + + server.handlePayload(R"({ + "jsonrpc":"2.0", + "method":"textDocument/didClose", + "params":{"textDocument":{"uri":"file:///main.rls"}} + })"); + EXPECT_EQ(server.documents().find("file:///main.rls"), nullptr); +} + +TEST(ServerCompositionRootTests, TracksCleanShutdownAndExit) { + ServerCompositionRoot server; + server.handlePayload( + R"({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}})"); + auto responses = server.handlePayload( + R"({"jsonrpc":"2.0","id":2,"method":"shutdown"})"); + ASSERT_EQ(responses.size(), 1); + EXPECT_TRUE(Json::parse(responses.front())["result"].is_null()); + + EXPECT_FALSE(server.shouldExit()); + server.handlePayload(R"({"jsonrpc":"2.0","method":"exit"})"); + EXPECT_TRUE(server.shouldExit()); + EXPECT_EQ(server.exitCode(), 0); +} + +TEST(ServerCompositionRootTests, IgnoresDocumentNotificationsBeforeInitialize) { + ServerCompositionRoot server; + server.handlePayload(R"({ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{"textDocument":{ + "uri":"file:///early.rls","languageId":"rls","version":1,"text":"early" + }} + })"); + + EXPECT_EQ(server.documents().find("file:///early.rls"), nullptr); +} + +TEST(ServerCompositionRootTests, RoutesWorkspaceFolderChanges) { + const fs::path first = fs::temp_directory_path() / + ("rls-lsp-workspace-route-first-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + const fs::path second = fs::temp_directory_path() / + ("rls-lsp-workspace-route-second-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count())); + fs::create_directories(first); + fs::create_directories(second); + const std::string firstUri = *rls::lsp::PathToFileUri(first); + const std::string secondUri = *rls::lsp::PathToFileUri(second); + ServerCompositionRoot server; + + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, + {"params", {{"workspaceFolders", Json::array({ + {{"uri", firstUri}, {"name", "first"}}, + })}}}, + }.dump()); + server.handlePayload(R"({"jsonrpc":"2.0","method":"initialized","params":{}})"); + ASSERT_EQ(server.workspace().folderCount(), 1); + + server.handlePayload(Json{ + {"jsonrpc", "2.0"}, {"method", "workspace/didChangeWorkspaceFolders"}, + {"params", {{"event", { + {"added", Json::array({{{"uri", secondUri}, {"name", "second"}}})}, + {"removed", Json::array({{{"uri", firstUri}, {"name", "first"}}})}, + }}}}, + }.dump()); + EXPECT_EQ(server.workspace().folderCount(), 1); + + std::error_code error; + fs::remove_all(first, error); + fs::remove_all(second, error); +} + +} // namespace \ No newline at end of file diff --git a/lsp/tests/signature_help_service_tests.cpp b/lsp/tests/signature_help_service_tests.cpp new file mode 100644 index 0000000..510d79c --- /dev/null +++ b/lsp/tests/signature_help_service_tests.cpp @@ -0,0 +1,150 @@ +#include +#include + +#include + +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/signature_help_service.h" + +namespace fs = std::filesystem; + +namespace { + +using rls::lsp::AnalysisScheduler; +using rls::lsp::DocumentStore; +using rls::lsp::PresentationPosition; +using rls::lsp::ProjectManager; +using rls::lsp::SignatureHelpService; + +struct SignatureFixture { + fs::path root = fs::temp_directory_path() / "rls-signature-help"; + fs::path declarationPath = root / "declarations.rls"; + fs::path usagePath = root / "usage.rls"; + std::string usageUri = *rls::lsp::PathToFileUri(usagePath); + DocumentStore documents; + ProjectManager projects; + AnalysisScheduler scheduler; + + SignatureFixture(std::string declarations, std::string usage) + : projects(documents, [&](const fs::path&) { + rls::project::FileProject project; + project.sourceFiles = {declarationPath, usagePath}; + return project; + }), + scheduler({ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }) { + EXPECT_EQ(documents.open(usageUri, "rls", 1, usage), + rls::lsp::DocumentUpdateResult::Applied); + EXPECT_EQ(projects.documentOpened(usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + const auto* project = projects.projectForDocument(usageUri); + EXPECT_NE(project, nullptr); + if (!project) return; + EXPECT_TRUE(scheduler.schedule({ + project->id, + project->generation, + { + {declarationPath.generic_string(), std::move(declarations)}, + {usagePath.generic_string(), std::move(usage)}, + }, + project->documentGeneration, + project->manifestGeneration, + })); + scheduler.waitForIdle(); + } + + std::optional at(PresentationPosition position) { + return SignatureHelpService(projects, scheduler).signatureHelp(usageUri, position); + } +}; + +TEST(SignatureHelpServiceTests, RendersExternSignatureAndNamedActiveParameter) { + const std::string declarations = + "enum Color { RED }\n" + "extern define paint(color: Color, enabled: Bool = true) -> Bool\n"; + const std::string usage = "define use(): paint(enabled: false, RED)\n"; + SignatureFixture fixture(declarations, usage); + + const auto enabled = fixture.at({0, static_cast(usage.find("false") + 2)}); + ASSERT_TRUE(enabled); + EXPECT_EQ(enabled->label, + "extern paint(color: Color, enabled: Bool = true) -> Bool"); + EXPECT_EQ(enabled->documentation, "*External declaration.*"); + EXPECT_EQ(enabled->parameterLabels, + (std::vector{"color: Color", "enabled: Bool = true"})); + EXPECT_EQ(enabled->activeParameter, 1u); + + const auto color = fixture.at({0, static_cast(usage.find("RED") + 1)}); + ASSERT_TRUE(color); + EXPECT_EQ(color->activeParameter, 0u); +} + +TEST(SignatureHelpServiceTests, SupportsKnownRecoveredIncompleteCall) { + const std::string declarations = + "enum Color { RED }\n" + "extern define paint(color: Color) -> Bool\n"; + const std::string usage = "define use(): paint(R"; + SignatureFixture fixture(declarations, usage); + + const auto result = fixture.at({0, static_cast(usage.size())}); + + ASSERT_TRUE(result); + EXPECT_EQ(result->label, "extern paint(color: Color) -> Bool"); + EXPECT_EQ(result->activeParameter, 0u); +} + +TEST(SignatureHelpServiceTests, AdvancesAfterTrailingComma) { + const std::string declarations = + "extern define target(first: Bool, second: Bool) -> Bool\n"; + const std::string usage = "define use(): target(true,)"; + SignatureFixture fixture(declarations, usage); + + const auto result = fixture.at({ + 0, static_cast(usage.find(',') + 1)}); + + ASSERT_TRUE(result); + EXPECT_EQ(result->activeParameter, 1u); +} + +TEST(SignatureHelpServiceTests, RendersUserDefineInferredReturnType) { + const std::string declarations = + "enum Color { RED }\n" + "define choose(color: Color = RED): color\n"; + const std::string usage = "define use(): choose(R"; + SignatureFixture fixture(declarations, usage); + + const auto result = fixture.at({0, static_cast(usage.size())}); + + ASSERT_TRUE(result); + EXPECT_EQ(result->label, "choose(color: Color = RED) -> Color"); + EXPECT_TRUE(result->documentation.empty()); + EXPECT_EQ(result->activeParameter, 0u); +} + +TEST(SignatureHelpServiceTests, SuppressesUnresolvedAndAmbiguousCalls) { + const std::string unresolvedUsage = "define use(): missing(R"; + SignatureFixture unresolved("enum Color { RED }\n", unresolvedUsage); + EXPECT_FALSE(unresolved.at({0, static_cast(unresolvedUsage.size())})); + + const std::string declarations = + "extern define paint(value: Bool) -> Bool\n" + "extern define paint(value: Int) -> Bool\n"; + const std::string ambiguousUsage = "define use(): paint(R"; + SignatureFixture ambiguous(declarations, ambiguousUsage); + EXPECT_FALSE(ambiguous.at({0, static_cast(ambiguousUsage.size())})); +} + +TEST(SignatureHelpServiceTests, RejectsStaleAcceptedSnapshot) { + const std::string usage = "define use(): target(t"; + SignatureFixture fixture( + "extern define target(value: Bool) -> Bool\n", usage); + ASSERT_EQ(fixture.projects.documentChanged(fixture.usageUri), + rls::lsp::ProjectAssignmentResult::Assigned); + + EXPECT_FALSE(fixture.at({0, static_cast(usage.size())})); +} + +} // namespace diff --git a/lsp/tests/workspace_service_tests.cpp b/lsp/tests/workspace_service_tests.cpp new file mode 100644 index 0000000..66cc9ed --- /dev/null +++ b/lsp/tests/workspace_service_tests.cpp @@ -0,0 +1,345 @@ +#include +#include +#include +#include + +#include +#include + +#include "rls/lsp/analysis_scheduler.h" +#include "rls/lsp/diagnostic_publisher.h" +#include "rls/lsp/document_store.h" +#include "rls/lsp/document_synchronization_service.h" +#include "rls/lsp/document_uri.h" +#include "rls/lsp/lifecycle_service.h" +#include "rls/lsp/outbound_message_queue.h" +#include "rls/lsp/project_manager.h" +#include "rls/lsp/workspace_service.h" + +namespace fs = std::filesystem; + +namespace { + +using Json = nlohmann::json; +using namespace rls::lsp; + +class TemporaryDirectory { +public: + TemporaryDirectory() : path_(fs::temp_directory_path() / + ("rls-lsp-workspace-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()))) { + fs::create_directories(path_); + } + + ~TemporaryDirectory() { + std::error_code error; + fs::remove_all(path_, error); + } + + const fs::path& path() const { return path_; } + +private: + fs::path path_; +}; + +void writeFile(const fs::path& path, const std::string& content) { + fs::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary) << content; +} + +struct Services { + OutboundMessageQueue outbound; + DocumentStore documents; + ProjectManager projects{documents}; + LifecycleService lifecycle; + DiagnosticPublisher diagnostics{outbound}; + AnalysisScheduler scheduler{{ + .debounce = std::chrono::milliseconds(0), + .maximumConcurrency = 1, + }}; + WorkspaceService workspace{projects, scheduler, diagnostics}; + DocumentSynchronizationService synchronization{ + lifecycle, documents, projects, scheduler, diagnostics}; + + Services() { + scheduler.setAcceptedHandler( + [this](std::string projectId, AnalysisScheduler::Snapshot snapshot) { + diagnostics.acceptedSnapshot(std::move(projectId), std::move(snapshot)); + }); + lifecycle.initialize(); + lifecycle.initialized(); + } +}; + +void drain(OutboundMessageQueue& outbound) { + while (outbound.tryPop()) { + } +} + +TEST(WorkspaceServiceTests, TracksInitialAndChangedWorkspaceFolders) { + TemporaryDirectory first; + TemporaryDirectory second; + Services services; + ASSERT_TRUE(services.workspace.initialize({*PathToFileUri(first.path())})); + EXPECT_EQ(services.workspace.folderCount(), 1); + + EXPECT_TRUE(services.workspace.changeFolders( + {*PathToFileUri(second.path())}, {*PathToFileUri(first.path())})); + EXPECT_EQ(services.workspace.folderCount(), 1); +} + +TEST(WorkspaceServiceTests, WatchedManifestReassignsOpenDocumentToNestedProject) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({"version":1,"sources":["nested"]})"); + const fs::path sourcePath = directory.path() / "nested" / "logic.rls"; + writeFile(sourcePath, "define disk(): true\n"); + const std::string sourceUri = *PathToFileUri(sourcePath); + Services services; + ASSERT_TRUE(services.workspace.initialize({*PathToFileUri(directory.path())})); + ASSERT_EQ(services.synchronization.open( + sourceUri, "rls", 1, "define overlay(): true\n"), + DocumentSynchronizationResult::Applied); + services.scheduler.waitForIdle(); + const std::string outerProjectId = services.projects.projectForDocument(sourceUri)->id; + ASSERT_NE(services.scheduler.acceptedSnapshot(outerProjectId), nullptr); + drain(services.outbound); + + const fs::path nestedManifest = directory.path() / "nested" / "rls.json"; + writeFile(nestedManifest, R"({"version":1,"sources":["logic.rls"]})"); + ASSERT_TRUE(services.workspace.watchedFilesChanged({*PathToFileUri(nestedManifest)})); + services.scheduler.waitForIdle(); + + const auto* nestedProject = services.projects.projectForDocument(sourceUri); + ASSERT_NE(nestedProject, nullptr); + EXPECT_NE(nestedProject->id, outerProjectId); + EXPECT_EQ(services.scheduler.acceptedSnapshot(outerProjectId), nullptr); + const auto nestedSnapshot = services.scheduler.acceptedSnapshot(nestedProject->id); + ASSERT_NE(nestedSnapshot, nullptr); + EXPECT_EQ(nestedSnapshot->sourceText( + fs::weakly_canonical(sourcePath).generic_string())->content(), + "define overlay(): true\n"); +} + +TEST(WorkspaceServiceTests, WatchedDiskEditReanalyzesAndClearsDiagnostics) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({"version":1,"sources":["src"]})"); + const fs::path openPath = directory.path() / "src" / "open.rls"; + const fs::path diskPath = directory.path() / "src" / "disk.rls"; + writeFile(openPath, "define open(): true\n"); + writeFile(diskPath, "region RR_TEST { events { EVENT_TEST: \"invalid\" } }\n"); + const std::string openUri = *PathToFileUri(openPath); + const std::string diskUri = *PathToFileUri(diskPath); + Services services; + ASSERT_TRUE(services.workspace.initialize({*PathToFileUri(directory.path())})); + ASSERT_EQ(services.synchronization.open( + openUri, "rls", 1, "define overlay(): true\n"), + DocumentSynchronizationResult::Applied); + services.scheduler.waitForIdle(); + const uint64_t documentGeneration = + services.projects.projectForDocument(openUri)->documentGeneration; + const uint64_t manifestGeneration = + services.projects.projectForDocument(openUri)->manifestGeneration; + drain(services.outbound); + + writeFile(diskPath, "region RR_TEST { events { EVENT_TEST: true } }\n"); + ASSERT_TRUE(services.workspace.watchedFilesChanged({diskUri})); + services.scheduler.waitForIdle(); + EXPECT_GT(services.projects.projectForDocument(openUri)->documentGeneration, + documentGeneration); + EXPECT_GT(services.projects.projectForDocument(openUri)->manifestGeneration, + manifestGeneration); + + bool cleared = false; + while (const auto payload = services.outbound.tryPop()) { + const Json message = Json::parse(*payload); + if (message["params"]["uri"] == diskUri + && message["params"]["diagnostics"].empty()) { + cleared = true; + } + } + EXPECT_TRUE(cleared); +} + +TEST(WorkspaceServiceTests, InitiallyInvalidManifestPublishesErrorAndKeepsOverlayStandalone) { + TemporaryDirectory directory; + const fs::path manifestPath = directory.path() / "rls.json"; + writeFile(manifestPath, "{ invalid"); + const fs::path sourcePath = directory.path() / "logic.rls"; + writeFile(sourcePath, "define disk(): true\n"); + const std::string sourceUri = *PathToFileUri(sourcePath); + Services services; + ASSERT_TRUE(services.workspace.initialize({*PathToFileUri(directory.path())})); + + EXPECT_EQ(services.synchronization.open( + sourceUri, "rls", 1, "define overlay(): true\n"), + DocumentSynchronizationResult::Applied); + ASSERT_NE(services.documents.find(sourceUri), nullptr); + ASSERT_NE(services.projects.projectForDocument(sourceUri), nullptr); + EXPECT_TRUE(services.projects.projectForDocument(sourceUri)->isStandalone); + services.scheduler.waitForIdle(); + const auto snapshot = services.scheduler.acceptedSnapshot( + services.projects.projectForDocument(sourceUri)->id); + ASSERT_NE(snapshot, nullptr); + EXPECT_EQ(snapshot->sourceText(fs::weakly_canonical(sourcePath).generic_string())->content(), + "define overlay(): true\n"); + + bool published = false; + while (const auto payload = services.outbound.tryPop()) { + const Json message = Json::parse(*payload); + if (message["params"]["uri"] == *PathToFileUri(manifestPath) + && !message["params"]["diagnostics"].empty() + && message["params"]["diagnostics"][0]["code"] == "RLS-C002") { + published = true; + } + } + EXPECT_TRUE(published); +} + +TEST(WorkspaceServiceTests, FolderRemovalMakesOpenDocumentStandaloneUntilReadded) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({"version":1,"sources":["src"]})"); + const fs::path sourcePath = directory.path() / "src" / "logic.rls"; + writeFile(sourcePath, "define disk(): true\n"); + const std::string rootUri = *PathToFileUri(directory.path()); + const std::string sourceUri = *PathToFileUri(sourcePath); + Services services; + ASSERT_TRUE(services.workspace.initialize({rootUri})); + ASSERT_EQ(services.synchronization.open( + sourceUri, "rls", 1, "define overlay(): true\n"), + DocumentSynchronizationResult::Applied); + services.scheduler.waitForIdle(); + ASSERT_FALSE(services.projects.projectForDocument(sourceUri)->isStandalone); + const std::string manifestProjectId = services.projects.projectForDocument(sourceUri)->id; + + ASSERT_TRUE(services.workspace.changeFolders({}, {rootUri})); + services.scheduler.waitForIdle(); + const auto* standalone = services.projects.projectForDocument(sourceUri); + ASSERT_NE(standalone, nullptr); + EXPECT_TRUE(standalone->isStandalone); + EXPECT_NE(standalone->id, manifestProjectId); + EXPECT_EQ(services.scheduler.acceptedSnapshot(manifestProjectId), nullptr); + + ASSERT_TRUE(services.workspace.changeFolders({rootUri}, {})); + services.scheduler.waitForIdle(); + ASSERT_NE(services.projects.projectForDocument(sourceUri), nullptr); + EXPECT_FALSE(services.projects.projectForDocument(sourceUri)->isStandalone); + EXPECT_EQ(services.projects.projectForDocument(sourceUri)->id, manifestProjectId); +} + +TEST(WorkspaceServiceTests, InvalidManifestKeepsLastGoodProjectAndRecoversWhenFixed) { + TemporaryDirectory directory; + const fs::path manifestPath = directory.path() / "rls.json"; + writeFile(manifestPath, R"({"version":1,"sources":["src"]})"); + const fs::path sourcePath = directory.path() / "src" / "logic.rls"; + writeFile(sourcePath, "define disk(): true\n"); + const std::string sourceUri = *PathToFileUri(sourcePath); + Services services; + ASSERT_TRUE(services.workspace.initialize({*PathToFileUri(directory.path())})); + ASSERT_EQ(services.synchronization.open( + sourceUri, "rls", 1, "define overlay(): true\n"), + DocumentSynchronizationResult::Applied); + services.scheduler.waitForIdle(); + const std::string projectId = services.projects.projectForDocument(sourceUri)->id; + const uint64_t generation = services.projects.projectForDocument(sourceUri)->generation; + + writeFile(manifestPath, "{ invalid"); + EXPECT_FALSE(services.workspace.watchedFilesChanged({*PathToFileUri(manifestPath)})); + ASSERT_NE(services.projects.projectForDocument(sourceUri), nullptr); + EXPECT_TRUE(services.projects.projectForDocument(sourceUri)->isStandalone); + services.scheduler.waitForIdle(); + bool publishedConfigurationError = false; + while (const auto payload = services.outbound.tryPop()) { + const Json message = Json::parse(*payload); + if (message["params"]["uri"] == *PathToFileUri(manifestPath) + && !message["params"]["diagnostics"].empty() + && message["params"]["diagnostics"][0]["code"] == "RLS-C002") { + publishedConfigurationError = true; + } + } + EXPECT_TRUE(publishedConfigurationError); + + writeFile(manifestPath, R"({"version":1,"sources":["src"]})"); + EXPECT_TRUE(services.workspace.watchedFilesChanged({*PathToFileUri(manifestPath)})); + services.scheduler.waitForIdle(); + ASSERT_NE(services.projects.projectForDocument(sourceUri), nullptr); + EXPECT_EQ(services.projects.projectForDocument(sourceUri)->id, projectId); + EXPECT_GT(services.projects.projectForDocument(sourceUri)->generation, generation); + bool clearedConfigurationError = false; + while (const auto payload = services.outbound.tryPop()) { + const Json message = Json::parse(*payload); + if (message["params"]["uri"] == *PathToFileUri(manifestPath) + && message["params"]["diagnostics"].empty()) { + clearedConfigurationError = true; + } + } + EXPECT_TRUE(clearedConfigurationError); +} + +TEST(WorkspaceServiceTests, MultipleManifestProjectsKeepSourcesAndSnapshotsIsolated) { + TemporaryDirectory first; + TemporaryDirectory second; + writeFile(first.path() / "rls.json", R"({"version":1,"sources":["src"]})"); + writeFile(second.path() / "rls.json", R"({"version":1,"sources":["src"]})"); + const fs::path firstPath = first.path() / "src" / "logic.rls"; + const fs::path secondPath = second.path() / "src" / "logic.rls"; + writeFile(firstPath, "define first_disk(): true\n"); + writeFile(secondPath, "define second_disk(): true\n"); + const std::string firstUri = *PathToFileUri(firstPath); + const std::string secondUri = *PathToFileUri(secondPath); + Services services; + ASSERT_TRUE(services.workspace.initialize({ + *PathToFileUri(first.path()), *PathToFileUri(second.path()), + })); + ASSERT_EQ(services.synchronization.open( + firstUri, "rls", 1, "define first_overlay(): true\n"), + DocumentSynchronizationResult::Applied); + ASSERT_EQ(services.synchronization.open( + secondUri, "rls", 1, "define second_overlay(): true\n"), + DocumentSynchronizationResult::Applied); + services.scheduler.waitForIdle(); + + const std::string firstProjectId = services.projects.projectForDocument(firstUri)->id; + const std::string secondProjectId = services.projects.projectForDocument(secondUri)->id; + ASSERT_NE(firstProjectId, secondProjectId); + writeFile(firstPath, "define first_disk_changed(): true\n"); + ASSERT_TRUE(services.workspace.watchedFilesChanged({firstUri})); + services.scheduler.waitForIdle(); + + const auto firstSnapshot = services.scheduler.acceptedSnapshot(firstProjectId); + const auto secondSnapshot = services.scheduler.acceptedSnapshot(secondProjectId); + ASSERT_NE(firstSnapshot, nullptr); + ASSERT_NE(secondSnapshot, nullptr); + EXPECT_EQ(firstSnapshot->documentCount(), 1); + EXPECT_EQ(secondSnapshot->documentCount(), 1); + EXPECT_NE(firstSnapshot->sourceText(fs::weakly_canonical(firstPath).generic_string()), nullptr); + EXPECT_EQ(firstSnapshot->sourceText(fs::weakly_canonical(secondPath).generic_string()), nullptr); + EXPECT_NE(secondSnapshot->sourceText(fs::weakly_canonical(secondPath).generic_string()), nullptr); + EXPECT_EQ(secondSnapshot->sourceText(fs::weakly_canonical(firstPath).generic_string()), nullptr); +} + +TEST(WorkspaceServiceTests, ProjectIdsExcludeManagedDocumentsOutsideWorkspace) { + TemporaryDirectory workspaceDirectory; + TemporaryDirectory externalDirectory; + const fs::path workspacePath = workspaceDirectory.path() / "inside.rls"; + const fs::path externalPath = externalDirectory.path() / "outside.rls"; + writeFile(workspacePath, "define inside(): true\n"); + writeFile(externalPath, "define outside(): true\n"); + const std::string workspaceUri = *PathToFileUri(workspacePath); + const std::string externalUri = *PathToFileUri(externalPath); + Services services; + ASSERT_TRUE(services.workspace.initialize({ + *PathToFileUri(workspaceDirectory.path()), + })); + ASSERT_EQ(services.synchronization.open( + workspaceUri, "rls", 1, "define inside(): true\n"), + DocumentSynchronizationResult::Applied); + ASSERT_EQ(services.synchronization.open( + externalUri, "rls", 1, "define outside(): true\n"), + DocumentSynchronizationResult::Applied); + + const auto projectIds = services.workspace.projectIds(); + ASSERT_EQ(projectIds.size(), 1u); + EXPECT_EQ(projectIds[0], services.projects.projectForDocument(workspaceUri)->id); +} + +} // namespace \ No newline at end of file diff --git a/parser/CMakeLists.txt b/parser/CMakeLists.txt index 4a219b8..3bc58e7 100644 --- a/parser/CMakeLists.txt +++ b/parser/CMakeLists.txt @@ -26,4 +26,5 @@ if(BUILD_TESTING) rls_add_gtest(parser_tests ${parser_test_sources}) target_link_libraries(parser_tests PRIVATE parser) target_include_directories(parser_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") + target_compile_definitions(parser_tests PRIVATE RLS_REPO_ROOT="${CMAKE_SOURCE_DIR}") endif() diff --git a/parser/include/parser.h b/parser/include/parser.h index 7c3a746..29abaa7 100644 --- a/parser/include/parser.h +++ b/parser/include/parser.h @@ -4,13 +4,35 @@ #include #include "ast.h" +#include "source_index.h" namespace rls::parser { -rls::ast::File ParseString(const std::string& source, const std::string& filename = "in_memory"); +enum class ParseMode { + Strict, + Editor, +}; -rls::ast::File ParseFile(const std::filesystem::path& filepath); +rls::ast::File ParseString( + const std::string& source, const std::string& filename = "in_memory", + ParseMode mode = ParseMode::Strict); -rls::ast::Project ParseProject(const std::filesystem::path& directory); +rls::ast::File ParseFile( + const std::filesystem::path& filepath, ParseMode mode = ParseMode::Strict); + +rls::ast::Project ParseProject( + const std::filesystem::path& directory, ParseMode mode = ParseMode::Strict); + +struct IndexedFile { + rls::ast::File file; + SourceIndex sourceIndex; +}; + +IndexedFile ParseStringWithIndex( + const std::string& source, const std::string& filename = "in_memory", + ParseMode mode = ParseMode::Strict); + +IndexedFile ParseFileWithIndex( + const std::filesystem::path& filepath, ParseMode mode = ParseMode::Strict); } // namespace rls::parser diff --git a/parser/include/source_index.h b/parser/include/source_index.h new file mode 100644 index 0000000..d44dccf --- /dev/null +++ b/parser/include/source_index.h @@ -0,0 +1,166 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "ast.h" + +namespace rls::parser { + +enum class SyntaxKind { + Declaration, + Name, + Expression, + Call, + Argument, + RegionData, + Section, + Entry, +}; + +enum class SourceNameKind { + Declaration, + Parameter, + Type, + Identifier, + MemberObject, + Member, + CallCallee, + ArgumentLabel, + RegionDataKey, + Entry, + EnumMember, +}; + +struct SyntaxContext { + SyntaxKind kind; + ast::Span span; +}; + +struct SourceNameContext { + SourceNameKind kind; + std::string text; + ast::Span span; +}; + +struct LogicalOperatorContext { + ast::Span span; +}; + +struct CallContext { + std::string calleeName; + ast::Span span; + ast::Span callee; + std::vector argumentRanges; + std::vector> argumentLabels; + std::vector> argumentLabelNames; + std::optional activeArgument; +}; + +struct RegionSectionContext { + ast::SectionKind kind; + ast::Span span; + std::vector entryNames; +}; + +struct RegionContext { + ast::Span span; + std::string name; + bool extension = false; + std::vector dataKeys; + std::vector sectionKinds; + std::optional activeSection; + std::vector activeSectionEntries; +}; + +struct SectionEntryContext { + ast::SectionKind kind; + ast::Span labelSpan; +}; + +struct TypePositionContext { + ast::Span typeSpan; +}; + +struct MemberAccessContext { + std::string object; + ast::Span memberSpan; +}; + +struct NamedArgumentContext { + std::string callee; + std::vector> argumentLabels; + size_t activeArgument = 0; + ast::Span labelSpan; +}; + +struct CallArgumentContext { + std::string callee; + std::vector> argumentLabels; + size_t activeArgument = 0; + ast::Span valueSpan; +}; + +/// A value-only cursor index built from trustworthy parser spans. +class SourceIndex { +public: + std::optional syntaxAt(ast::Position position) const; + std::optional nameAt(ast::Position position) const; + std::optional enclosingExpression(ast::Position position) const; + std::optional enclosingCall(ast::Position position) const; + std::optional regionContextAt(ast::Position position) const; + std::optional memberAccessAt(ast::Position position) const; + std::optional namedArgumentAt(ast::Position position) const; + std::optional callArgumentAt(ast::Position position) const; + std::optional sectionEntryAt(ast::Position position) const; + std::optional typePositionAt(ast::Position position) const; + std::vector sectionEntryNames( + ast::SectionKind kind, + std::optional regionName = std::nullopt) const; + std::vector regionNames() const; + const std::vector& enumNames() const { return enumNames_; } + const std::vector& logicalOperators() const { return logicalOperators_; } + const std::vector& declarations() const { return declarations_; } + std::vector declarationsIn(std::string_view file) const; + + // Internal parser construction operations. + void addSyntax(SyntaxKind kind, const ast::Span& span); + void addName(SourceNameKind kind, const ast::Name& name); + void addExpression(const ast::Span& span); + void addLogicalOperator(LogicalOperatorContext context); + void addCall(CallContext call); + void addDeclaration(const ast::Span& span); + void addRegionContext(RegionContext context, std::vector sections); + void addMemberAccess(MemberAccessContext context); + void addNamedArgument(NamedArgumentContext context); + void addCallArgument(CallArgumentContext context); + void addSectionEntry(SectionEntryContext context); + void addTypePosition(TypePositionContext context); + void addEnumName(std::string name); + +private: + std::vector syntax_; + std::vector names_; + std::vector expressions_; + std::vector logicalOperators_; + std::vector calls_; + std::vector declarations_; + struct IndexedRegionContext { + RegionContext context; + std::vector sections; + }; + std::vector regionContexts_; + std::vector memberAccesses_; + std::vector namedArguments_; + std::vector callArguments_; + std::vector sectionEntries_; + std::vector typePositions_; + std::vector enumNames_; +}; + +SourceIndex BuildSourceIndex(const ast::File& file, const ast::SourceText* source = nullptr); + +} // namespace rls::parser \ No newline at end of file diff --git a/parser/src/builder.cpp b/parser/src/builder.cpp index f2a506d..09c42a7 100644 --- a/parser/src/builder.cpp +++ b/parser/src/builder.cpp @@ -24,12 +24,10 @@ ast::Span makeSpan(const Node& n) { static_cast(n.m_begin.line), static_cast(n.m_begin.column) }; - if (n.has_content()) { - span.end = { - static_cast(n.m_end.line), - static_cast(n.m_end.column) - }; - } + span.end = { + static_cast(n.m_end.line), + static_cast(n.m_end.column) + }; return span; } @@ -37,6 +35,10 @@ ast::Name makeName(const Node& n) { return ast::Name(std::string(n.string_view()), makeSpan(n)); } +ast::Span spanFrom(const ast::Span& start, const ast::Span& end) { + return {start.file, start.start, end.end}; +} + std::string unescapeStringLiteral(std::string_view raw) { if (raw.size() >= 2) { raw.remove_prefix(1); @@ -62,11 +64,7 @@ std::string unescapeStringLiteral(std::string_view raw) { using Diags = std::vector; void emitError(Diags& diags, const std::string& msg, const Node& n) { - diags.push_back({ - ast::DiagnosticLevel::Error, - msg, - makeSpan(n) - }); + diags.push_back(ast::Diagnostic{"", makeSpan(n), ast::DiagnosticLevel::Error, msg}); } // ============================================================================= @@ -114,20 +112,21 @@ ast::ExprPtr buildBinaryChain(const Node& n, OpMapper mapOp, Diags& diags) { for (size_t i = 1; i + 1 < n.children.size(); i += 2) { auto op = mapOp(n.children[i]->string_view()); auto right = buildExpr(*n.children[i + 1], diags); + const auto span = spanFrom(result->span, right->span); result = ast::makeExpr(ast::BinaryExpr( - op, std::move(result), std::move(right))); + op, std::move(result), std::move(right), makeSpan(*n.children[i])), span); } return result; } -/// Left-fold for and/or chains whose children are just operands -/// (no explicit operator token nodes). +/// Left-fold for and/or chains whose children alternate operands and operators. ast::ExprPtr buildLogicalChain(const Node& n, ast::BinaryOp op, Diags& diags) { auto result = buildExpr(*n.children[0], diags); - for (size_t i = 1; i < n.children.size(); ++i) { - auto right = buildExpr(*n.children[i], diags); + for (size_t i = 1; i + 1 < n.children.size(); i += 2) { + auto right = buildExpr(*n.children[i + 1], diags); + const auto span = spanFrom(result->span, right->span); result = ast::makeExpr(ast::BinaryExpr( - op, std::move(result), std::move(right))); + op, std::move(result), std::move(right), makeSpan(*n.children[i])), span); } return result; } @@ -155,7 +154,7 @@ ast::ExprPtr buildExpr(const Node& n, Diags& diags) { } if (n.is_type()) { - // children: [ident(enumName), ident(memberName)] + // children: [member_object(enumName), member_name(memberName)] return ast::makeExpr( ast::MemberExpr(makeName(*n.children[0]), makeName(*n.children[1])), makeSpan(n)); @@ -190,9 +189,11 @@ ast::ExprPtr buildExpr(const Node& n, Diags& diags) { if (n.is_type()) { // children: [kw_not, operand] + auto operand = buildExpr(*n.children[1], diags); + const auto span = spanFrom(makeSpan(*n.children[0]), operand->span); return ast::makeExpr( - ast::UnaryExpr(ast::UnaryOp::Not, buildExpr(*n.children[1], diags)), - makeSpan(n)); + ast::UnaryExpr(ast::UnaryOp::Not, std::move(operand)), + span); } // -- Binary chains with explicit operator tokens -------------------------- @@ -210,11 +211,13 @@ ast::ExprPtr buildExpr(const Node& n, Diags& diags) { if (n.is_type()) { // children: [left, comp_op, right] auto op = mapCompOp(n.children[1]->string_view()); + auto left = buildExpr(*n.children[0], diags); + auto right = buildExpr(*n.children[2], diags); + const auto span = spanFrom(left->span, right->span); return ast::makeExpr( ast::BinaryExpr(op, - buildExpr(*n.children[0], diags), - buildExpr(*n.children[2], diags)), - makeSpan(n)); + std::move(left), std::move(right)), + span); } // -- Logical chains (no explicit operator nodes) -------------------------- @@ -234,12 +237,14 @@ ast::ExprPtr buildExpr(const Node& n, Diags& diags) { n.is_type() || n.is_type()) { // children: [condition, thenBranch, elseBranch] + auto condition = buildExpr(*n.children[0], diags); + auto thenBranch = buildExpr(*n.children[1], diags); + auto elseBranch = buildExpr(*n.children[2], diags); + const auto span = spanFrom(condition->span, elseBranch->span); return ast::makeExpr( ast::TernaryExpr( - buildExpr(*n.children[0], diags), - buildExpr(*n.children[1], diags), - buildExpr(*n.children[2], diags)), - makeSpan(n)); + std::move(condition), std::move(thenBranch), std::move(elseBranch)), + span); } // -- Call ----------------------------------------------------------------- @@ -248,7 +253,8 @@ ast::ExprPtr buildExpr(const Node& n, Diags& diags) { // children: [call, invoke_suffix, invoke_suffix, ...] auto result = buildExpr(*n.children[0], diags); for (size_t i = 1; i < n.children.size(); ++i) { - result = ast::makeExpr(ast::InvokeExpr(std::move(result)), makeSpan(*n.children[i])); + const auto span = spanFrom(result->span, makeSpan(*n.children[i])); + result = ast::makeExpr(ast::InvokeExpr(std::move(result)), span); } return result; } @@ -291,6 +297,8 @@ ast::ExprPtr buildExpr(const Node& n, Diags& diags) { for (const auto& p : patNode.children) { if (p->is_type()) { isDefault = true; + } else if (p->is_type()) { + continue; } else { patterns.emplace_back(buildExpr(*p, diags)); } @@ -327,14 +335,17 @@ ast::Param buildParam(const Node& n, Diags& diags) { ast::ExprPtr defaultValue; for (size_t i = 1; i < n.children.size(); ++i) { - if (n.children[i]->is_type()) { + if (n.children[i]->is_type()) { type = ast::TypeRef(makeName(*n.children[i])); } else { defaultValue = buildExpr(*n.children[i], diags); } } - return ast::Param(std::move(name), std::move(type), std::move(defaultValue)); + ast::Span span{name.span.file, name.span.start, name.span.end}; + if (type) span.end = type->name.span.end; + if (defaultValue) span.end = defaultValue->span.end; + return ast::Param(std::move(name), std::move(type), std::move(defaultValue), std::move(span)); } // ============================================================================= @@ -369,7 +380,7 @@ ast::Section buildSection(const Node& n, Diags& diags) { entries.push_back(buildEntry(*n.children[i], diags)); } - return ast::Section(kind, std::move(entries)); + return ast::Section(kind, std::move(entries), makeSpan(n)); } // ============================================================================= @@ -436,7 +447,7 @@ ast::ExternDefineDecl buildExternDefineDecl(const Node& n, Diags& diags) { for (size_t i = 1; i < n.children.size(); ++i) { if (n.children[i]->is_type()) { params.push_back(buildParam(*n.children[i], diags)); - } else if (n.children[i]->is_type()) { + } else if (n.children[i]->is_type()) { returnType = ast::TypeRef(makeName(*n.children[i])); } } diff --git a/parser/src/builder.h b/parser/src/builder.h index 0dd056b..1d2145d 100644 --- a/parser/src/builder.h +++ b/parser/src/builder.h @@ -40,21 +40,23 @@ using selector = tao::pegtl::parse_tree::selector< grammar::string_literal, grammar::atom_keyword, grammar::invoke_suffix, - grammar::type, + grammar::kw_and, + grammar::kw_or, + grammar::parameter_type_name, + grammar::return_type_name, + grammar::enum_name, + grammar::member_object, + grammar::member_name, + grammar::call_callee, + grammar::named_argument_label, + grammar::entry_label, + grammar::region_data_key, + grammar::region_name, grammar::comp_op, grammar::mul_div_op, grammar::add_sub_op, grammar::section_kind, - grammar::kw_not, // marker: unary "not" - grammar::kw_here, // `here` keyword atom (resolves to current region) - grammar::trailing_or // marker: fallthrough in match arms - >, - - // -- Structural nodes (children matter, text doesn't) --------------------- - tao::pegtl::parse_tree::remove_content::on< - // File root - grammar::rls_file, - // Top-level declarations + grammar::section, grammar::region_decl, grammar::extend_decl, grammar::define_decl, @@ -63,14 +65,8 @@ using selector = tao::pegtl::parse_tree::selector< grammar::extern_enum_decl, grammar::enum_member, grammar::extern_enum_entry, - // Region data grammar::region_data_entry, - // Sections & entries - grammar::section, grammar::entry, - // Parameters - grammar::param, - // Expressions grammar::invoke_call, grammar::call, grammar::member_access, @@ -78,7 +74,18 @@ using selector = tao::pegtl::parse_tree::selector< grammar::match_expr, grammar::match_arm, grammar::match_pattern, - grammar::list_expr + grammar::list_expr, + grammar::kw_not, // marker: unary "not" + grammar::kw_here, // `here` keyword atom (resolves to current region) + grammar::trailing_or // marker: fallthrough in match arms + >, + + // -- Structural nodes (children matter, text doesn't) --------------------- + tao::pegtl::parse_tree::remove_content::on< + // File root + grammar::rls_file, + // Parameters + grammar::param >, // -- Transparent wrappers (fold when single child) ------------------------ diff --git a/parser/src/editor_syntax.cpp b/parser/src/editor_syntax.cpp new file mode 100644 index 0000000..19feee0 --- /dev/null +++ b/parser/src/editor_syntax.cpp @@ -0,0 +1,800 @@ +#include "editor_syntax.h" + +#include "grammar.h" + +#include + +#include +#include +#include +#include + +namespace rls::parser { + +namespace { + +struct EditorSyntaxBuilder { + struct CallFrame { + ast::Name callee; + size_t argumentStart = 0; + std::optional pendingLabel; + std::optional label; + std::optional labelDelimiterEnd; + std::vector arguments; + bool expectsArgument = false; + bool recovered = false; + bool closed = false; + }; + struct SectionFrame { + ast::SectionKind kind; + size_t bodyStart = 0; + std::vector entries; + std::optional pendingEntry; + bool entryHasDelimiter = false; + std::optional closeStart; + }; + struct RegionFrame { + ast::Name name; + bool extension = false; + size_t bodyStart = 0; + std::vector dataKeys; + std::vector sections; + std::optional pendingDataKey; + std::optional closeEnd; + }; + + const ast::SourceText& source; + std::string_view filename; + EditorSyntax result; + std::optional memberObject; + std::optional memberAccessIndex; + std::optional typePositionIndex; + std::optional callCallee; + std::vector callFrames; + bool nextRegionExtension = false; + std::optional regionName; + std::vector regionFrames; + std::optional sectionKind; + std::vector sectionFrames; + + EditorSyntaxBuilder( + const ast::SourceText& source, std::string_view filename) + : source(source), filename(filename) {} + + template + std::optional spanFor(const Input& input) const { + const auto* sourceBegin = source.content().data(); + const auto* inputBegin = input.begin(); + if (inputBegin < sourceBegin || inputBegin > sourceBegin + source.content().size()) { + return std::nullopt; + } + const size_t startOffset = static_cast(inputBegin - sourceBegin); + const auto start = source.utf8PositionAtByteOffset(startOffset); + const auto end = source.utf8PositionAtByteOffset(startOffset + input.size()); + if (!start || !end) return std::nullopt; + return ast::Span{std::string(filename), *start, *end}; + } + + void beginTypePosition( + const ast::Span& delimiter, EditorTypePositionKind kind) { + size_t endOffset = source.content().size(); + if (const auto offset = source.byteOffsetFromUtf8Position(delimiter.end)) { + endOffset = *offset; + while (endOffset < source.content().size() + && (source.content()[endOffset] == ' ' + || source.content()[endOffset] == '\t')) { + ++endOffset; + } + } + const auto end = source.utf8PositionAtByteOffset(endOffset); + if (!end) return; + result.typePositions.push_back({ + kind, + ast::Span{std::string(filename), delimiter.end, *end}, + SyntaxRecoveryStatus::Recovered}); + typePositionIndex = result.typePositions.size() - 1; + } + + void completeTypePosition(const ast::Span& name) { + if (!typePositionIndex) return; + auto& position = result.typePositions[*typePositionIndex]; + position.span.end = name.end; + position.status = SyntaxRecoveryStatus::Complete; + typePositionIndex.reset(); + } + + std::optional offsetFor(ast::Position position) const { + return source.byteOffsetFromUtf8Position(position); + } + + std::optional spanFromOffsets(size_t start, size_t end) const { + const auto startPosition = source.utf8PositionAtByteOffset(start); + const auto endPosition = source.utf8PositionAtByteOffset(end); + if (!startPosition || !endPosition) return std::nullopt; + return ast::Span{std::string(filename), *startPosition, *endPosition}; + } + + void finishArgument(CallFrame& frame, size_t end, bool allowEmpty) { + if (end < frame.argumentStart) return; + size_t contentStart = frame.argumentStart; + while (contentStart < end + && std::isspace(static_cast( + source.content()[contentStart]))) { + ++contentStart; + } + size_t contentEnd = end; + while (contentEnd > contentStart + && std::isspace(static_cast( + source.content()[contentEnd - 1]))) { + --contentEnd; + } + if (!allowEmpty && contentStart == contentEnd && !frame.label) return; + + size_t valueStart = frame.labelDelimiterEnd.value_or(contentStart); + while (valueStart < contentEnd + && std::isspace(static_cast( + source.content()[valueStart]))) { + ++valueStart; + } + const auto valueSpan = spanFromOffsets(valueStart, contentEnd); + if (!valueSpan) return; + if (frame.labelDelimiterEnd && valueStart == contentEnd) { + frame.recovered = true; + } + + ast::Span labelSpan; + bool labelCandidate = false; + if (frame.label) { + labelSpan = frame.label->span; + } else { + size_t candidateEnd = contentStart; + if (candidateEnd < contentEnd + && (std::isalpha(static_cast( + source.content()[candidateEnd])) + || source.content()[candidateEnd] == '_')) { + ++candidateEnd; + while (candidateEnd < contentEnd + && (std::isalnum(static_cast( + source.content()[candidateEnd])) + || source.content()[candidateEnd] == '_')) { + ++candidateEnd; + } + } + labelCandidate = candidateEnd == contentEnd; + if (labelCandidate) { + if (const auto candidate = spanFromOffsets(contentStart, candidateEnd)) { + labelSpan = *candidate; + } + } + } + + frame.arguments.push_back({ + frame.label, labelSpan, *valueSpan, labelCandidate}); + frame.pendingLabel.reset(); + frame.label.reset(); + frame.labelDelimiterEnd.reset(); + } + + void finishCall(const ast::Span& span) { + if (callFrames.empty()) return; + auto frame = std::move(callFrames.back()); + callFrames.pop_back(); + if (!frame.closed) { + if (const auto end = offsetFor(span.end)) { + finishArgument(frame, *end, true); + } + } + result.calls.push_back({ + std::move(frame.callee), span, std::move(frame.arguments), + frame.closed && !frame.recovered + ? SyntaxRecoveryStatus::Complete + : SyntaxRecoveryStatus::Recovered}); + } + + void addBlankSectionEntries(SectionFrame& frame, size_t bodyEnd) { + size_t lineStart = frame.bodyStart; + while (lineStart <= bodyEnd) { + size_t lineEnd = source.content().find('\n', lineStart); + if (lineEnd == std::string::npos || lineEnd > bodyEnd) lineEnd = bodyEnd; + if (lineEnd > lineStart && source.content()[lineEnd - 1] == '\r') { + --lineEnd; + } + size_t contentStart = lineStart; + while (contentStart < lineEnd + && (source.content()[contentStart] == ' ' + || source.content()[contentStart] == '\t')) { + ++contentStart; + } + if (contentStart == lineEnd) { + if (const auto span = spanFromOffsets(contentStart, contentStart)) { + const bool duplicate = std::any_of( + frame.entries.begin(), frame.entries.end(), + [&](const EditorSectionEntry& entry) { + return entry.labelSpan.start.line == span->start.line + && entry.labelSpan.start.column == span->start.column; + }); + if (!duplicate) frame.entries.push_back({std::nullopt, *span}); + } + } + if (lineEnd >= bodyEnd) break; + lineStart = lineEnd + 1; + } + } + + void finishSection(const ast::Span& span) { + if (sectionFrames.empty() || regionFrames.empty()) return; + auto frame = std::move(sectionFrames.back()); + sectionFrames.pop_back(); + const size_t bodyEnd = frame.closeStart.value_or( + offsetFor(span.end).value_or(source.content().size())); + addBlankSectionEntries(frame, bodyEnd); + const auto bodySpan = spanFromOffsets(frame.bodyStart, bodyEnd); + if (!bodySpan) return; + regionFrames.back().sections.push_back({ + frame.kind, *bodySpan, std::move(frame.entries)}); + } + + void finishRegion(const ast::Span& span) { + if (regionFrames.empty()) return; + auto frame = std::move(regionFrames.back()); + regionFrames.pop_back(); + const size_t bodyEnd = frame.closeEnd.value_or( + offsetFor(span.end).value_or(source.content().size())); + const auto bodySpan = spanFromOffsets(frame.bodyStart, bodyEnd); + if (!bodySpan) return; + result.regions.push_back({ + std::move(frame.name), *bodySpan, frame.extension, + std::move(frame.dataKeys), std::move(frame.sections), + SyntaxRecoveryStatus::Recovered}); + nextRegionExtension = false; + regionName.reset(); + } +}; + +template +struct editor_action : tao::pegtl::nothing {}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.result.enumDeclarations.push_back({ + ast::Name(input.string(), *span), *span, + SyntaxRecoveryStatus::Recovered}); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (!builder.result.enumDeclarations.empty()) { + if (const auto span = builder.spanFor(input)) { + builder.result.enumDeclarations.back().span = *span; + } + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + builder.memberAccessIndex.reset(); + if (const auto span = builder.spanFor(input)) { + builder.memberObject = ast::Name(input.string(), *span); + } else { + builder.memberObject.reset(); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + const auto delimiter = builder.spanFor(input); + if (!delimiter || !builder.memberObject) return; + builder.result.memberAccesses.push_back({ + *builder.memberObject, + ast::Span{std::string(builder.filename), + builder.memberObject->span.start, delimiter->end}, + ast::Span{std::string(builder.filename), delimiter->end, delimiter->end}, + SyntaxRecoveryStatus::Recovered}); + builder.memberAccessIndex = builder.result.memberAccesses.size() - 1; + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + const auto span = builder.spanFor(input); + if (!span || !builder.memberAccessIndex) return; + auto& access = builder.result.memberAccesses[*builder.memberAccessIndex]; + access.span.end = span->end; + access.memberSpan = *span; + access.status = SyntaxRecoveryStatus::Complete; + builder.memberObject.reset(); + builder.memberAccessIndex.reset(); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.beginTypePosition( + *span, EditorTypePositionKind::Parameter); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.completeTypePosition(*span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.beginTypePosition( + *span, EditorTypePositionKind::Return); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.completeTypePosition(*span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.callCallee = ast::Name(input.string(), *span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + const auto span = builder.spanFor(input); + if (!span || !builder.callCallee) return; + const auto start = builder.offsetFor(span->end); + if (!start) return; + builder.callFrames.push_back({ + std::move(*builder.callCallee), *start}); + builder.callCallee.reset(); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.callFrames.empty()) return; + if (const auto span = builder.spanFor(input)) { + builder.callFrames.back().pendingLabel = ast::Name(input.string(), *span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.callFrames.empty()) return; + if (const auto span = builder.spanFor(input)) { + auto& frame = builder.callFrames.back(); + frame.label = std::move(frame.pendingLabel); + frame.pendingLabel.reset(); + frame.labelDelimiterEnd = builder.offsetFor(span->end); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.callFrames.empty()) return; + const auto span = builder.spanFor(input); + if (!span) return; + const auto end = builder.offsetFor(span->start); + const auto next = builder.offsetFor(span->end); + if (!end || !next) return; + auto& frame = builder.callFrames.back(); + builder.finishArgument(frame, *end, true); + frame.argumentStart = *next; + frame.expectsArgument = true; + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.callFrames.empty()) return; + const auto span = builder.spanFor(input); + if (!span) return; + const auto end = builder.offsetFor(span->start); + if (!end) return; + auto& frame = builder.callFrames.back(); + size_t contentStart = frame.argumentStart; + while (contentStart < *end + && std::isspace(static_cast( + builder.source.content()[contentStart]))) { + ++contentStart; + } + if (contentStart < *end || frame.label) { + builder.finishArgument(frame, *end, false); + } else if (frame.expectsArgument) { + builder.finishArgument(frame, *end, true); + frame.recovered = true; + } + frame.closed = true; + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.finishCall(*span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input&, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + builder.nextRegionExtension = true; + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.regionName = ast::Name(input.string(), *span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + const auto span = builder.spanFor(input); + if (!span || !builder.regionName) return; + const auto start = builder.offsetFor(span->end); + if (!start) return; + builder.regionFrames.push_back({ + std::move(*builder.regionName), builder.nextRegionExtension, *start}); + builder.regionName.reset(); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.regionFrames.empty()) return; + if (const auto span = builder.spanFor(input)) { + builder.regionFrames.back().pendingDataKey = + ast::Name(input.string(), *span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input&, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.regionFrames.empty()) return; + auto& frame = builder.regionFrames.back(); + if (frame.pendingDataKey && !frame.extension) { + frame.dataKeys.push_back(frame.pendingDataKey->text); + } + frame.pendingDataKey.reset(); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + const auto text = input.string_view(); + if (text == "events") builder.sectionKind = ast::SectionKind::Events; + else if (text == "locations") builder.sectionKind = ast::SectionKind::Locations; + else if (text == "exits") builder.sectionKind = ast::SectionKind::Exits; + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + const auto span = builder.spanFor(input); + if (!span || !builder.sectionKind) return; + const auto start = builder.offsetFor(span->end); + if (!start) return; + builder.sectionFrames.push_back({*builder.sectionKind, *start}); + builder.sectionKind.reset(); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.sectionFrames.empty()) return; + if (const auto span = builder.spanFor(input)) { + auto& frame = builder.sectionFrames.back(); + frame.pendingEntry = ast::Name(input.string(), *span); + frame.entryHasDelimiter = false; + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input&, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (!builder.sectionFrames.empty()) { + builder.sectionFrames.back().entryHasDelimiter = true; + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input&, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.sectionFrames.empty()) return; + auto& frame = builder.sectionFrames.back(); + if (!frame.pendingEntry) return; + frame.entries.push_back({ + frame.entryHasDelimiter + ? frame.pendingEntry + : std::nullopt, + frame.pendingEntry->span}); + frame.pendingEntry.reset(); + frame.entryHasDelimiter = false; + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.sectionFrames.empty()) return; + if (const auto span = builder.spanFor(input)) { + builder.sectionFrames.back().closeStart = + builder.offsetFor(span->start); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.finishSection(*span); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (builder.regionFrames.empty()) return; + if (const auto span = builder.spanFor(input)) { + builder.regionFrames.back().closeEnd = + builder.offsetFor(span->end); + } + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) builder.finishRegion(*span); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) builder.finishRegion(*span); + } +}; + +template<> +struct editor_action { + template + static void apply( + const Input& input, EditorSyntaxBuilder& builder, + grammar::ParseState&) { + if (const auto span = builder.spanFor(input)) { + builder.result.declarations.push_back({*span}); + } + } +}; + +bool sameSpan(const ast::Span& left, const ast::Span& right) { + return left.file == right.file + && left.start.line == right.start.line + && left.start.column == right.start.column + && left.end.line == right.end.line + && left.end.column == right.end.column; +} + +void classifyCompleteDeclarationsImpl(EditorSyntax& syntax, const ast::File& file) { + for (auto& candidate : syntax.enumDeclarations) { + for (const auto& declaration : file.declarations) { + const bool complete = std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v + || std::is_same_v) { + return candidate.name.text == node.name.text + && sameSpan(candidate.name.span, node.name.span); + } + return false; + }, declaration); + if (complete) { + candidate.status = SyntaxRecoveryStatus::Complete; + break; + } + } + } + for (auto& candidate : syntax.regions) { + for (const auto& declaration : file.declarations) { + const bool complete = std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return !candidate.extension + && candidate.name.text == node.key.text + && sameSpan(candidate.name.span, node.key.span); + } else if constexpr (std::is_same_v) { + return candidate.extension + && candidate.name.text == node.name.text + && sameSpan(candidate.name.span, node.name.span); + } + return false; + }, declaration); + if (complete) { + candidate.status = SyntaxRecoveryStatus::Complete; + break; + } + } + } +} + +} // namespace + +EditorSyntax ParseEditorSyntax( + const ast::SourceText& source, std::string_view filename, + const ast::File& parsedFile) { + EditorSyntaxBuilder builder(source, filename); + tao::pegtl::memory_input input(source.content(), filename); + grammar::ParseState state{true}; + tao::pegtl::parse( + input, builder, state); + ClassifyEditorSyntax(builder.result, parsedFile); + std::sort(builder.result.calls.begin(), builder.result.calls.end(), + [](const EditorCall& left, const EditorCall& right) { + if (left.span.start.line != right.span.start.line) { + return left.span.start.line < right.span.start.line; + } + if (left.span.start.column != right.span.start.column) { + return left.span.start.column < right.span.start.column; + } + if (left.span.end.line != right.span.end.line) { + return left.span.end.line < right.span.end.line; + } + return left.span.end.column < right.span.end.column; + }); + builder.result.calls.erase(std::unique( + builder.result.calls.begin(), builder.result.calls.end(), + [](const EditorCall& left, const EditorCall& right) { + return sameSpan(left.span, right.span) + && sameSpan(left.callee.span, right.callee.span); + }), builder.result.calls.end()); + return std::move(builder.result); +} + +void ClassifyEditorSyntax(EditorSyntax& syntax, const ast::File& parsedFile) { + classifyCompleteDeclarationsImpl(syntax, parsedFile); +} + +} // namespace rls::parser \ No newline at end of file diff --git a/parser/src/editor_syntax.h b/parser/src/editor_syntax.h new file mode 100644 index 0000000..d74ee6f --- /dev/null +++ b/parser/src/editor_syntax.h @@ -0,0 +1,93 @@ +#pragma once + +#include "ast.h" + +#include +#include +#include + +namespace rls::parser { + +enum class SyntaxRecoveryStatus { + Complete, + Recovered, +}; + +struct EditorEnumDeclaration { + ast::Name name; + ast::Span span; + SyntaxRecoveryStatus status = SyntaxRecoveryStatus::Recovered; +}; + +struct EditorMemberAccess { + ast::Name object; + ast::Span span; + ast::Span memberSpan; + SyntaxRecoveryStatus status = SyntaxRecoveryStatus::Recovered; +}; + +enum class EditorTypePositionKind { + Parameter, + Return, +}; + +struct EditorTypePosition { + EditorTypePositionKind kind; + ast::Span span; + SyntaxRecoveryStatus status = SyntaxRecoveryStatus::Recovered; +}; + +struct EditorCallArgument { + std::optional label; + ast::Span labelSpan; + ast::Span valueSpan; + bool labelCandidate = false; +}; + +struct EditorCall { + ast::Name callee; + ast::Span span; + std::vector arguments; + SyntaxRecoveryStatus status = SyntaxRecoveryStatus::Recovered; +}; + +struct EditorSectionEntry { + std::optional name; + ast::Span labelSpan; +}; + +struct EditorRegionSection { + ast::SectionKind kind; + ast::Span span; + std::vector entries; +}; + +struct EditorRegion { + ast::Name name; + ast::Span span; + bool extension = false; + std::vector dataKeys; + std::vector sections; + SyntaxRecoveryStatus status = SyntaxRecoveryStatus::Recovered; +}; + +struct EditorDeclaration { + ast::Span span; +}; + +struct EditorSyntax { + std::vector declarations; + std::vector enumDeclarations; + std::vector memberAccesses; + std::vector typePositions; + std::vector calls; + std::vector regions; +}; + +EditorSyntax ParseEditorSyntax( + const ast::SourceText& source, std::string_view filename, + const ast::File& parsedFile); + +void ClassifyEditorSyntax(EditorSyntax& syntax, const ast::File& parsedFile); + +} // namespace rls::parser \ No newline at end of file diff --git a/parser/src/grammar.h b/parser/src/grammar.h index 3b0fb40..23af6b1 100644 --- a/parser/src/grammar.h +++ b/parser/src/grammar.h @@ -50,8 +50,10 @@ // // ── Error handling ─────────────────────────────────────────────────────────── // -// must Like seq, but if any rule fails AFTER the first one, -// it throws a `parse_error` instead of backtracking. +// required Like must<> in strict mode. In editor mode, missing +// rules succeed without consuming input so the same +// grammar can retain surrounding syntax. +// must Used by grammar unit tests to require a full match. // We use `must` in tests to assert a rule // matches the ENTIRE input string. // @@ -81,11 +83,87 @@ // ============================================================================= #include +#include + +#include namespace rls::parser::grammar { using namespace tao::pegtl; +struct ParseState { + bool tolerant = false; +}; + +template +bool isTolerant(States&&... states) { + bool result = false; + ([&] { + if constexpr (std::is_same_v< + std::remove_cvref_t, ParseState>) { + result = result || states.tolerant; + } + }(), ...); + return result; +} + +template +struct required_rule { + using rule_t = required_rule; + using subs_t = type_list; + + template class Action, + template class Control, + typename ParseInput, typename... States> + static bool match(ParseInput& input, States&&... states) { + if (Control::template match< + ActionMode, rewind_mode::dontcare, Action, Control>( + input, states...)) { + return true; + } + if (isTolerant(states...)) return true; + Control::raise( + static_cast(input), states...); + return true; + } +}; + +template +struct required : seq...> {}; + +template +struct recoverable { + using rule_t = recoverable; + using subs_t = type_list; + + template class Action, + template class Control, + typename ParseInput, typename... States> + static bool match(ParseInput& input, States&&... states) { + if (Control::template match< + ActionMode, rewind_mode::dontcare, Action, Control>( + input, states...)) { + return true; + } + return isTolerant(states...); + } +}; + +struct tolerant_mode { + using rule_t = tolerant_mode; + using subs_t = type_list<>; + + template class Action, + template class Control, + typename ParseInput, typename... States> + static bool match(ParseInput&, States&&... states) { + return isTolerant(states...); + } +}; + // == Keywords ================================================================= // Top-level declarations @@ -191,7 +269,12 @@ struct integer : seq>, plus> {}; /// String literal: `"..."` with `\"` and `\\` escape support. struct escaped_char : seq, one<'"', '\\'>> {}; struct string_char : sor> {}; -struct string_literal : seq, star, must>> {}; +struct string_literal : seq, star, required>> {}; + +struct recover_source : seq< + tolerant_mode, + sor +> {}; // == Punctuation ============================================================== @@ -253,7 +336,11 @@ struct atom_keyword : sor< struct atom : sor {}; /// Named argument: IDENT ":" expr -struct named_arg : seq {}; +struct named_argument_label : ident {}; +struct named_argument_delimiter : colon {}; +struct named_arg : seq< + named_argument_label, _, named_argument_delimiter, _, recoverable +> {}; /// A single call argument — named (IDENT ":" expr) or positional (expr). /// We try `named_arg` first because it starts with `ident` which would also @@ -261,14 +348,29 @@ struct named_arg : seq {}; struct arg : sor {}; /// Argument list (possibly empty) between parentheses. -struct arg_list : opt>> {}; +struct call_argument_separator : comma {}; +struct arg_list : opt>> +>> {}; /// Function call: IDENT "(" arg_list ")" -struct call : seq> {}; +struct call_callee : ident {}; +struct call_open_paren : open_paren {}; +struct call_close_paren : close_paren {}; +struct call : seq< + call_callee, _, call_open_paren, + required<_, arg_list, _, call_close_paren> +> {}; /// Member access: IDENT "." IDENT (enum type disambiguation) /// Example: Item.RG_HOOKSHOT -struct member_access : seq {}; +struct member_object : ident {}; +struct member_delimiter : dot {}; +struct member_name : ident {}; +struct member_access : seq< + member_object, member_delimiter, recoverable +> {}; /// Invoke-call suffix for callable evaluation: "()". struct invoke_suffix : seq {}; @@ -293,7 +395,7 @@ struct invoke_call : seq>> {}; struct match_expr; /// Parenthesised expression: "(" expr ")" -struct paren_expr : seq> {}; +struct paren_expr : seq> {}; /// primary = invoke_call | call | member_access | match_expr | list | atom | "(" expr ")" /// @@ -336,7 +438,7 @@ struct and_expr : seq, _, comparison>>> {}; struct or_expr : seq, _, and_expr>>> {}; /// ternary = or_expr ("?" ternary ":" ternary)? -struct ternary : seq>>> {}; +struct ternary : seq>>> {}; /// expr = ternary struct expr : ternary {}; @@ -371,17 +473,17 @@ struct match_or_expr : seq>> {}; /// The ternary ? : branches use the REGULAR ternary rule because the ? : /// delimiters scope the expression, and ternary-with-trailing-or in match /// arms is an extremely unlikely edge case. -struct match_ternary : seq>>> {}; +struct match_ternary : seq>>> {}; /// match_arm = match_pattern ":" match_ternary trailing_or? -struct match_arm : seq, _, opt> {}; +struct match_arm : seq, _, opt> {}; /// Detects a dangling 'or' after the last match arm (gives a clear error). struct no_trailing_or : not_at> {}; /// match_expr = "match" IDENT "{" match_arm+ "}" struct match_expr : seq< - kw, must<_, ident, _, + kw, required<_, ident, _, open_brace, _, plus>, no_trailing_or, @@ -399,7 +501,16 @@ struct type : ident {}; struct ident_list : list> {}; /// param = IDENT (":" type)? ("=" expr)? -struct param : seq>, opt, _, expr>> +> {}; /// params = param ("," param)* struct params : list> {}; @@ -407,49 +518,82 @@ struct params : list> {}; // -- Sections (events / locations / exits) ------------------------------------ /// entry = IDENT ":" expr -struct entry : seq> {}; +struct entry_label : ident {}; +struct entry_delimiter : colon {}; +struct entry : seq< + entry_label, required<_, entry_delimiter, _, expr> +> {}; /// section_kind = "events" | "locations" | "exits" struct section_kind : sor, kw, kw> {}; /// section = section_kind "{" entry* "}" -struct section : seq>, close_brace>> {}; +struct section_open_brace : open_brace {}; +struct section_close_brace : close_brace {}; +struct section : seq< + section_kind, + required<_, section_open_brace, _, star>, section_close_brace> +> {}; // -- Region ------------------------------------------------------------------- /// region_data_entry = IDENT ":" expr -struct region_data_entry : seq> {}; +struct region_data_key : ident {}; +struct region_data_delimiter : colon {}; +struct region_data_entry : seq< + region_data_key, required<_, region_data_delimiter, _, expr> +> {}; /// region_body = region_data_entry* section* struct region_body : seq>, star>> {}; +struct declaration_keyword : sor< + kw, kw, kw, kw, kw +> {}; +struct region_recovery_character : seq< + tolerant_mode, + not_at>, + any +> {}; +struct region_recovery : star {}; + /// region = "region" IDENT "{" region_body "}" -struct region_decl : seq, must<_, ident, _, open_brace, _, region_body, _, close_brace>> {}; +struct region_name : ident {}; +struct region_open_brace : open_brace {}; +struct region_close_brace : close_brace {}; +struct region_decl : seq< + kw, required<_, region_name, _, region_open_brace, _, + region_body, region_recovery, _, region_close_brace> +> {}; // -- Extend region ------------------------------------------------------------ /// extend = "extend" "region" IDENT "{" section* "}" struct extend_decl : seq< - kw, must<_, kw, _, ident, _, - open_brace, _, + kw, required<_, kw, _, region_name, _, + region_open_brace, _, star>, - close_brace> + region_recovery, + region_close_brace> > {}; // -- Define ------------------------------------------------------------------- /// define = "define" IDENT "(" params? ")" ":" expr struct define_decl : seq< - kw, must<_, ident, _, + kw, required<_, ident, _, open_paren, _, opt, _, close_paren, _, colon, _, expr> > {}; +struct return_type_delimiter : arrow {}; +struct return_type_name : type {}; + /// extern define = "extern" "define" IDENT "(" params? ")" "->" type struct extern_define_decl : seq< - kw, _, kw, must<_, ident, _, + kw, _, kw, required<_, ident, _, open_paren, _, opt, _, close_paren, _, - arrow, _, type> + return_type_delimiter, _, return_type_name> > {}; /// enum_member = IDENT ("=" INTEGER)? @@ -464,8 +608,10 @@ struct glob_pattern : seq, one<'*'>, star {}; /// enum = "enum" IDENT "{" enum_member ("," enum_member)* "}" +struct enum_name : ident {}; +struct enum_head : seq, required<_, enum_name>> {}; struct enum_decl : seq< - kw, must<_, ident, _, + enum_head, required<_, open_brace, _, opt>>, _, close_brace> @@ -473,7 +619,7 @@ struct enum_decl : seq< /// extern enum = "extern" "enum" IDENT "{" extern_enum_entry ("," extern_enum_entry)* "}" struct extern_enum_decl : seq< - kw, _, kw, must<_, ident, _, + kw, _, enum_head, required<_, open_brace, _, opt>>, _, close_brace> @@ -493,6 +639,27 @@ struct declaration : sor< /// file = _ (declaration _)* eof /// Named `rls_file` to avoid clashing with any PEGTL or std types. -struct rls_file : seq<_, star>, must> {}; +struct rls_file : seq< + _, + star, _>>, + required +> {}; } // namespace rls::parser::grammar + +namespace tao::pegtl { + +template +struct analyze_traits< + Name, rls::parser::grammar::required_rule> + : analyze_opt_traits {}; + +template +struct analyze_traits> + : analyze_opt_traits {}; + +template +struct analyze_traits + : analyze_opt_traits<> {}; + +} // namespace tao::pegtl diff --git a/parser/src/parser.cpp b/parser/src/parser.cpp index 5e0bdb1..74f768b 100644 --- a/parser/src/parser.cpp +++ b/parser/src/parser.cpp @@ -1,12 +1,15 @@ #include "parser.h" #include "builder.h" +#include "editor_syntax.h" #include "grammar.h" #include #include #include +#include +#include #include namespace rls::parser { @@ -25,12 +28,21 @@ struct parse_errors { // -- Punctuation -------------------------------------------------------------- template<> constexpr const char* parse_errors::message = "expected '('"; template<> constexpr const char* parse_errors::message = "expected ')'"; +template<> constexpr const char* parse_errors::message = "expected ')'"; template<> constexpr const char* parse_errors::message = "expected '{'"; template<> constexpr const char* parse_errors::message = "expected '}'"; +template<> constexpr const char* parse_errors::message = "expected '{'"; +template<> constexpr const char* parse_errors::message = "expected '}'"; +template<> constexpr const char* parse_errors::message = "expected '{'"; +template<> constexpr const char* parse_errors::message = "expected '}'"; template<> constexpr const char* parse_errors::message = "expected ':'"; +template<> constexpr const char* parse_errors::message = "expected ':'"; +template<> constexpr const char* parse_errors::message = "expected ':'"; // -- Tokens ------------------------------------------------------------------- template<> constexpr const char* parse_errors::message = "expected identifier"; +template<> constexpr const char* parse_errors::message = "expected identifier"; +template<> constexpr const char* parse_errors::message = "expected identifier"; template<> constexpr const char* parse_errors::message = "expected expression"; template<> constexpr const char* parse_errors::message = "expected expression"; template<> constexpr const char* parse_errors::message = "expected expression"; @@ -53,7 +65,7 @@ using rls_control = tao::pegtl::must_if // ============================================================================= template -rls::ast::File Parse(T&& in) { +rls::ast::File Parse(T&& in, [[maybe_unused]] ParseMode mode) { rls::ast::File file; file.path = std::string(in.source()); @@ -63,11 +75,8 @@ rls::ast::File Parse(T&& in) { >(in); if (!root) { - file.diagnostics.push_back({ - ast::DiagnosticLevel::Error, - "parse failed", - ast::Span{file.path, {}, {}} - }); + file.diagnostics.push_back(ast::Diagnostic{ + "", ast::Span{file.path, {}, {}}, ast::DiagnosticLevel::Error, "parse failed"}); return file; } @@ -87,29 +96,28 @@ rls::ast::File Parse(T&& in) { }; span.end = span.start; } - file.diagnostics.push_back({ - ast::DiagnosticLevel::Error, - std::string(e.message()), - span - }); + file.diagnostics.push_back(ast::Diagnostic{ + "", span, ast::DiagnosticLevel::Error, std::string(e.message())}); } return file; } -rls::ast::File ParseString(const std::string& source, const std::string& filename) { - return Parse(tao::pegtl::memory_input(source, filename)); +rls::ast::File ParseString( + const std::string& source, const std::string& filename, ParseMode mode) { + return Parse(tao::pegtl::memory_input(source, filename), mode); } -rls::ast::File ParseFile(const std::filesystem::path& filepath) { +rls::ast::File ParseFile(const std::filesystem::path& filepath, ParseMode mode) { if (!std::filesystem::is_regular_file(filepath)) { throw std::runtime_error("Not a regular file: " + filepath.string()); } - return Parse(tao::pegtl::file_input(filepath)); + return Parse(tao::pegtl::file_input(filepath), mode); } -rls::ast::Project ParseProject(const std::filesystem::path& directory) { +rls::ast::Project ParseProject( + const std::filesystem::path& directory, ParseMode mode) { if (!std::filesystem::is_directory(directory)) { throw std::runtime_error("Not a directory: " + directory.string()); } @@ -118,11 +126,146 @@ rls::ast::Project ParseProject(const std::filesystem::path& directory) { for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) { if (entry.is_regular_file() && entry.path().extension() == ".rls") { - project.files.emplace_back(ParseFile(entry.path())); + project.files.emplace_back(ParseFile(entry.path(), mode)); } } return project; } +void RecoverCompleteDeclarations( + ast::File& file, const std::string& source, + const ast::SourceText& sourceText, const EditorSyntax& syntax) { + if (file.diagnostics.empty()) return; + + for (const auto& candidate : syntax.declarations) { + const auto start = sourceText.byteOffsetFromUtf8Position(candidate.span.start); + const auto end = sourceText.byteOffsetFromUtf8Position(candidate.span.end); + if (!start || !end || *start >= *end) continue; + + try { + tao::pegtl::memory_input input( + source.data() + *start, source.data() + *end, file.path, + *start, candidate.span.start.line, candidate.span.start.column); + auto root = tao::pegtl::parse_tree::parse< + grammar::rls_file, selector, tao::pegtl::nothing, rls_control + >(input); + if (!root) continue; + + std::vector diagnostics; + auto candidateFile = buildFile(*root, diagnostics); + if (!diagnostics.empty() || candidateFile.declarations.size() != 1) continue; + file.declarations.push_back(std::move(candidateFile.declarations.front())); + } catch (const tao::pegtl::parse_error&) { + // Recovered syntax is not semantic syntax unless strict parsing succeeds. + } + } + + std::sort(file.declarations.begin(), file.declarations.end(), + [](const ast::Decl& left, const ast::Decl& right) { + const auto leftSpan = std::visit( + [](const auto& node) { return node.span; }, left); + const auto rightSpan = std::visit( + [](const auto& node) { return node.span; }, right); + if (leftSpan.start.line != rightSpan.start.line) { + return leftSpan.start.line < rightSpan.start.line; + } + return leftSpan.start.column < rightSpan.start.column; + }); +} + +IndexedFile ParseStringWithIndex( + const std::string& source, const std::string& filename, ParseMode mode) { + auto file = ParseString(source, filename, mode); + const auto sourceText = ast::SourceText::FromUtf8(source); + std::optional editorSyntax; + if (mode == ParseMode::Editor && sourceText) { + editorSyntax = ParseEditorSyntax(*sourceText, filename, file); + RecoverCompleteDeclarations(file, source, *sourceText, *editorSyntax); + ClassifyEditorSyntax(*editorSyntax, file); + } + auto sourceIndex = BuildSourceIndex(file, sourceText ? &*sourceText : nullptr); + if (editorSyntax) { + for (const auto& declaration : editorSyntax->enumDeclarations) { + sourceIndex.addEnumName(declaration.name.text); + } + for (const auto& memberAccess : editorSyntax->memberAccesses) { + sourceIndex.addMemberAccess({ + memberAccess.object.text, memberAccess.memberSpan}); + } + for (const auto& typePosition : editorSyntax->typePositions) { + sourceIndex.addTypePosition({typePosition.span}); + } + for (const auto& call : editorSyntax->calls) { + std::vector> labels; + labels.reserve(call.arguments.size()); + for (const auto& argument : call.arguments) { + labels.push_back(argument.label + ? std::optional(argument.label->text) + : std::nullopt); + } + if (call.status == SyntaxRecoveryStatus::Recovered) { + CallContext context{ + call.callee.text, call.span, call.callee.span, + {}, {}, {}, std::nullopt}; + for (const auto& argument : call.arguments) { + context.argumentRanges.push_back(argument.valueSpan); + context.argumentLabels.push_back(argument.label + ? std::optional(argument.label->span) + : std::nullopt); + context.argumentLabelNames.push_back(argument.label + ? std::optional(argument.label->text) + : std::nullopt); + } + sourceIndex.addCall(std::move(context)); + } + for (size_t index = 0; index < call.arguments.size(); ++index) { + const auto& argument = call.arguments[index]; + sourceIndex.addCallArgument({ + call.callee.text, labels, index, argument.valueSpan}); + if (argument.label || argument.labelCandidate) { + sourceIndex.addNamedArgument({ + call.callee.text, labels, index, argument.labelSpan}); + } + } + } + for (const auto& region : editorSyntax->regions) { + for (const auto& section : region.sections) { + for (const auto& entry : section.entries) { + sourceIndex.addSectionEntry({section.kind, entry.labelSpan}); + } + } + if (region.status == SyntaxRecoveryStatus::Complete) continue; + RegionContext context{ + .span = region.span, + .name = region.name.text, + .extension = region.extension, + .dataKeys = region.dataKeys, + }; + std::vector sections; + for (const auto& section : region.sections) { + context.sectionKinds.push_back(section.kind); + RegionSectionContext sectionContext{ + section.kind, section.span, {}}; + for (const auto& entry : section.entries) { + if (entry.name) { + sectionContext.entryNames.push_back(entry.name->text); + } + } + sections.push_back(std::move(sectionContext)); + } + sourceIndex.addRegionContext( + std::move(context), std::move(sections)); + } + } + return {std::move(file), std::move(sourceIndex)}; +} + +IndexedFile ParseFileWithIndex( + const std::filesystem::path& filepath, ParseMode mode) { + auto file = ParseFile(filepath, mode); + auto sourceIndex = BuildSourceIndex(file); + return {std::move(file), std::move(sourceIndex)}; +} + } // namespace rls::parser diff --git a/parser/src/source_index.cpp b/parser/src/source_index.cpp new file mode 100644 index 0000000..b68016d --- /dev/null +++ b/parser/src/source_index.cpp @@ -0,0 +1,423 @@ +#include "source_index.h" + +#include +#include + +namespace rls::parser { + +namespace { + +bool isBeforeOrEqual(ast::Position left, ast::Position right) { + return left.line < right.line || (left.line == right.line && left.column <= right.column); +} + +bool contains(const ast::Span& span, ast::Position position) { + return span.start.line != 0 && isBeforeOrEqual(span.start, position) && + isBeforeOrEqual(position, span.end) && !(position.line == span.end.line && position.column == span.end.column); +} + +bool containsInclusive(const ast::Span& span, ast::Position position) { + return span.start.line != 0 && isBeforeOrEqual(span.start, position) + && isBeforeOrEqual(position, span.end); +} + +size_t spanSize(const ast::Span& span) { + return (static_cast(span.end.line - span.start.line) << 32) + + span.end.column - span.start.column; +} + +template +std::optional narrowestAt(const std::vector& contexts, ast::Position position) { + const Context* result = nullptr; + for (const auto& context : contexts) { + if (contains(context.span, position) && (!result || spanSize(context.span) < spanSize(result->span))) { + result = &context; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +void indexExpr(SourceIndex& index, const ast::Expr& expr); + +void indexParam(SourceIndex& index, const ast::Param& param) { + index.addName(SourceNameKind::Parameter, param.name); + if (param.type) { + index.addName(SourceNameKind::Type, param.type->name); + index.addTypePosition({param.type->name.span}); + } + if (param.defaultValue) indexExpr(index, *param.defaultValue); +} + +void indexExpr(SourceIndex& index, const ast::Expr& expr) { + index.addExpression(expr.span); + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + index.addName(SourceNameKind::Identifier, node.name); + } else if constexpr (std::is_same_v) { + index.addName(SourceNameKind::MemberObject, node.object); + index.addName(SourceNameKind::Member, node.member); + index.addMemberAccess({node.object.text, node.member.span}); + } else if constexpr (std::is_same_v) { + indexExpr(index, *node.operand); + } else if constexpr (std::is_same_v) { + if ((node.op == ast::BinaryOp::And || node.op == ast::BinaryOp::Or) + && node.operatorSpan.start.line != 0) { + index.addLogicalOperator({node.operatorSpan}); + } + indexExpr(index, *node.left); + indexExpr(index, *node.right); + } else if constexpr (std::is_same_v) { + indexExpr(index, *node.condition); + indexExpr(index, *node.thenBranch); + indexExpr(index, *node.elseBranch); + } else if constexpr (std::is_same_v) { + index.addName(SourceNameKind::CallCallee, node.callee); + CallContext call{ + node.callee.text, expr.span, node.callee.span, + {}, {}, {}, std::nullopt}; + for (const auto& argument : node.args) { + call.argumentRanges.push_back(argument.value->span); + call.argumentLabels.push_back(argument.name ? std::optional(argument.name->span) : std::nullopt); + call.argumentLabelNames.push_back(argument.name + ? std::optional(argument.name->text) + : std::nullopt); + if (argument.name) index.addName(SourceNameKind::ArgumentLabel, *argument.name); + index.addSyntax(SyntaxKind::Argument, argument.value->span); + indexExpr(index, *argument.value); + } + index.addCall(std::move(call)); + index.addSyntax(SyntaxKind::Call, expr.span); + } else if constexpr (std::is_same_v) { + indexExpr(index, *node.callee); + } else if constexpr (std::is_same_v) { + indexExpr(index, *node.discriminant); + for (const auto& arm : node.arms) { + for (const auto& pattern : arm.patterns) indexExpr(index, *pattern); + indexExpr(index, *arm.body); + } + } else if constexpr (std::is_same_v) { + for (const auto& element : node.elements) indexExpr(index, *element); + } + }, expr.node); +} + +void indexEntry(SourceIndex& index, const ast::Entry& entry) { + index.addSyntax(SyntaxKind::Entry, entry.span); + index.addName(SourceNameKind::Entry, entry.name); + indexExpr(index, *entry.condition); +} + +void indexSections(SourceIndex& index, const std::vector& sections) { + for (const auto& section : sections) { + index.addSyntax(SyntaxKind::Section, section.span); + for (const auto& entry : section.entries) indexEntry(index, entry); + } +} + +} // namespace + +void SourceIndex::addSyntax(SyntaxKind kind, const ast::Span& span) { + if (span.start.line != 0) syntax_.push_back({kind, span}); +} + +void SourceIndex::addName(SourceNameKind kind, const ast::Name& name) { + if (name.span.start.line != 0) names_.push_back({kind, name.text, name.span}); +} + +void SourceIndex::addExpression(const ast::Span& span) { + if (span.start.line != 0) expressions_.push_back({SyntaxKind::Expression, span}); + addSyntax(SyntaxKind::Expression, span); +} + +void SourceIndex::addLogicalOperator(LogicalOperatorContext context) { + if (context.span.start.line != 0) logicalOperators_.push_back(std::move(context)); +} + +void SourceIndex::addCall(CallContext call) { + calls_.push_back(std::move(call)); +} + +void SourceIndex::addDeclaration(const ast::Span& span) { + if (span.start.line == 0) return; + declarations_.push_back({SyntaxKind::Declaration, span}); + addSyntax(SyntaxKind::Declaration, span); +} + +void SourceIndex::addRegionContext( + RegionContext context, std::vector sections) { + if (context.span.start.line == 0) return; + regionContexts_.push_back({std::move(context), std::move(sections)}); +} + +void SourceIndex::addMemberAccess(MemberAccessContext context) { + if (context.memberSpan.start.line == 0) return; + memberAccesses_.push_back(std::move(context)); +} + +void SourceIndex::addNamedArgument(NamedArgumentContext context) { + if (context.labelSpan.start.line == 0) return; + namedArguments_.push_back(std::move(context)); +} + +void SourceIndex::addCallArgument(CallArgumentContext context) { + if (context.valueSpan.start.line == 0) return; + callArguments_.push_back(std::move(context)); +} + +void SourceIndex::addSectionEntry(SectionEntryContext context) { + if (context.labelSpan.start.line == 0) return; + sectionEntries_.push_back(std::move(context)); +} + +void SourceIndex::addTypePosition(TypePositionContext context) { + if (context.typeSpan.start.line == 0) return; + typePositions_.push_back(std::move(context)); +} + +void SourceIndex::addEnumName(std::string name) { + if (std::find(enumNames_.begin(), enumNames_.end(), name) == enumNames_.end()) { + enumNames_.push_back(std::move(name)); + std::sort(enumNames_.begin(), enumNames_.end()); + } +} + +std::optional SourceIndex::syntaxAt(ast::Position position) const { + if (const auto name = narrowestAt(names_, position)) return SyntaxContext{SyntaxKind::Name, name->span}; + return narrowestAt(syntax_, position); +} + +std::optional SourceIndex::nameAt(ast::Position position) const { + return narrowestAt(names_, position); +} + +std::optional SourceIndex::enclosingExpression(ast::Position position) const { + return narrowestAt(expressions_, position); +} + +std::optional SourceIndex::enclosingCall(ast::Position position) const { + auto result = narrowestAt(calls_, position); + if (!result) { + for (const auto& call : calls_) { + if (containsInclusive(call.span, position) + || containsInclusive(call.callee, position)) { + result = call; + break; + } + for (size_t index = 0; !result && index < call.argumentRanges.size(); ++index) { + if (containsInclusive(call.argumentRanges[index], position) || + (call.argumentLabels[index] + && containsInclusive(*call.argumentLabels[index], position))) { + result = call; + break; + } + } + } + } + if (!result) return std::nullopt; + for (size_t index = 0; index < result->argumentRanges.size(); ++index) { + if (containsInclusive(result->argumentRanges[index], position) || + (result->argumentLabels[index] + && containsInclusive(*result->argumentLabels[index], position))) { + result->activeArgument = index; + break; + } + } + return result; +} + +std::optional SourceIndex::regionContextAt(ast::Position position) const { + const IndexedRegionContext* result = nullptr; + for (const auto& indexed : regionContexts_) { + if (contains(indexed.context.span, position) + && (!result || spanSize(indexed.context.span) < spanSize(result->context.span))) { + result = &indexed; + } + } + if (!result) return std::nullopt; + RegionContext context = result->context; + for (const auto& section : result->sections) { + if (contains(section.span, position)) { + context.activeSection = section.kind; + context.activeSectionEntries = section.entryNames; + break; + } + } + return context; +} + +std::optional SourceIndex::memberAccessAt(ast::Position position) const { + const MemberAccessContext* result = nullptr; + for (const auto& context : memberAccesses_) { + const bool atMember = isBeforeOrEqual(context.memberSpan.start, position) + && isBeforeOrEqual(position, context.memberSpan.end); + if (atMember && (!result + || spanSize(context.memberSpan) < spanSize(result->memberSpan))) { + result = &context; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +std::optional SourceIndex::namedArgumentAt(ast::Position position) const { + const NamedArgumentContext* result = nullptr; + for (const auto& context : namedArguments_) { + const bool atLabel = isBeforeOrEqual(context.labelSpan.start, position) + && isBeforeOrEqual(position, context.labelSpan.end); + if (atLabel && (!result + || spanSize(context.labelSpan) < spanSize(result->labelSpan))) { + result = &context; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +std::optional SourceIndex::callArgumentAt(ast::Position position) const { + const CallArgumentContext* result = nullptr; + for (const auto& context : callArguments_) { + const bool atValue = isBeforeOrEqual(context.valueSpan.start, position) + && isBeforeOrEqual(position, context.valueSpan.end); + if (atValue && (!result + || spanSize(context.valueSpan) < spanSize(result->valueSpan))) { + result = &context; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +std::optional SourceIndex::sectionEntryAt(ast::Position position) const { + const SectionEntryContext* result = nullptr; + for (const auto& context : sectionEntries_) { + const bool atLabel = isBeforeOrEqual(context.labelSpan.start, position) + && isBeforeOrEqual(position, context.labelSpan.end); + if (atLabel && (!result + || spanSize(context.labelSpan) < spanSize(result->labelSpan))) { + result = &context; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +std::optional SourceIndex::typePositionAt(ast::Position position) const { + const TypePositionContext* result = nullptr; + for (const auto& context : typePositions_) { + const bool atType = isBeforeOrEqual(context.typeSpan.start, position) + && isBeforeOrEqual(position, context.typeSpan.end); + if (atType && (!result + || spanSize(context.typeSpan) < spanSize(result->typeSpan))) { + result = &context; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +std::vector SourceIndex::sectionEntryNames( + ast::SectionKind kind, std::optional regionName) const { + std::vector result; + for (const auto& indexed : regionContexts_) { + if (regionName && indexed.context.name != *regionName) continue; + for (const auto& section : indexed.sections) { + if (section.kind != kind) continue; + result.insert(result.end(), section.entryNames.begin(), section.entryNames.end()); + } + } + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +std::vector SourceIndex::regionNames() const { + std::vector result; + for (const auto& indexed : regionContexts_) { + if (!indexed.context.extension) result.push_back(indexed.context.name); + } + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; +} + +std::vector SourceIndex::declarationsIn(std::string_view file) const { + std::vector result; + for (const auto& declaration : declarations_) { + if (declaration.span.file == file) result.push_back(declaration); + } + return result; +} + +SourceIndex BuildSourceIndex(const ast::File& file, const ast::SourceText* source) { + SourceIndex index; + for (const auto& declaration : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + index.addDeclaration(node.span); + if constexpr (std::is_same_v) { + RegionContext context{ + .span = {node.span.file, node.key.span.end, node.span.end}, + .name = node.key.text, + }; + std::vector sections; + for (const auto& data : node.body.data) context.dataKeys.push_back(data.key.text); + for (const auto& section : node.body.sections) { + context.sectionKinds.push_back(section.kind); + RegionSectionContext sectionContext{section.kind, section.span, {}}; + for (const auto& entry : section.entries) { + sectionContext.entryNames.push_back(entry.name.text); + } + sections.push_back(std::move(sectionContext)); + } + index.addRegionContext(std::move(context), std::move(sections)); + index.addName(SourceNameKind::Declaration, node.key); + for (const auto& data : node.body.data) { + index.addSyntax(SyntaxKind::RegionData, data.span); + index.addName(SourceNameKind::RegionDataKey, data.key); + indexExpr(index, *data.value); + } + indexSections(index, node.body.sections); + } else if constexpr (std::is_same_v) { + RegionContext context{ + .span = {node.span.file, node.name.span.end, node.span.end}, + .name = node.name.text, + .extension = true, + }; + std::vector sections; + for (const auto& section : node.sections) { + context.sectionKinds.push_back(section.kind); + RegionSectionContext sectionContext{section.kind, section.span, {}}; + for (const auto& entry : section.entries) { + sectionContext.entryNames.push_back(entry.name.text); + } + sections.push_back(std::move(sectionContext)); + } + index.addRegionContext(std::move(context), std::move(sections)); + index.addName(SourceNameKind::Declaration, node.name); + indexSections(index, node.sections); + } else if constexpr (std::is_same_v) { + index.addName(SourceNameKind::Declaration, node.name); + for (const auto& parameter : node.params) indexParam(index, parameter); + indexExpr(index, *node.body); + } else if constexpr (std::is_same_v) { + index.addName(SourceNameKind::Declaration, node.name); + for (const auto& parameter : node.params) indexParam(index, parameter); + if (node.returnType) { + index.addName(SourceNameKind::Type, node.returnType->name); + index.addTypePosition({node.returnType->name.span}); + } + } else if constexpr (std::is_same_v) { + index.addEnumName(node.name.text); + index.addName(SourceNameKind::Declaration, node.name); + for (const auto& member : node.members) index.addName(SourceNameKind::EnumMember, member.name); + } else if constexpr (std::is_same_v) { + index.addEnumName(node.name.text); + index.addName(SourceNameKind::Declaration, node.name); + for (const auto& entry : node.entries) { + if (const auto* member = std::get_if(&entry)) { + index.addName(SourceNameKind::EnumMember, member->name); + } + } + } + }, declaration); + } + return index; +} + +} // namespace rls::parser \ No newline at end of file diff --git a/parser/tests/parser_tests.cpp b/parser/tests/parser_tests.cpp index 5ad7fa7..823a533 100644 --- a/parser/tests/parser_tests.cpp +++ b/parser/tests/parser_tests.cpp @@ -1,7 +1,9 @@ #include +#include #include #include "ast.h" +#include "editor_syntax.h" #include "parser.h" using namespace rls::ast; @@ -30,6 +32,14 @@ static const Expr& parseExpr(const std::string& exprSrc) { return *def.body; } +static void expectSameSpan(const Span& strict, const Span& editor) { + EXPECT_EQ(strict.file, editor.file); + EXPECT_EQ(strict.start.line, editor.start.line); + EXPECT_EQ(strict.start.column, editor.start.column); + EXPECT_EQ(strict.end.line, editor.end.line); + EXPECT_EQ(strict.end.column, editor.end.column); +} + // == Basic parsing ============================================================ TEST(ParserTests, ReturnsEmptyFileForEmptySource) { @@ -50,6 +60,14 @@ TEST(ParserTests, InvalidSourceReportsDiagnostic) { EXPECT_EQ(file.diagnostics[0].span.start.column, 1u); } +TEST(ParserTests, StrictModeDoesNotSkipTopLevelStrings) { + const auto file = rls::parser::ParseString("\"enum Fake { VALUE }\""); + + EXPECT_TRUE(file.declarations.empty()); + ASSERT_FALSE(file.diagnostics.empty()); + EXPECT_EQ(file.diagnostics[0].message, "expected declaration or end of file"); +} + TEST(ParserTests, MissingIdentifierAfterDefine) { const auto file = rls::parser::ParseString("define 123"); @@ -147,6 +165,249 @@ TEST(ParserTests, ValidSourceReturnsFile) { EXPECT_EQ(std::get(scene->value->node).name, "SCENE_TEST"); } +TEST(ParserTests, EditorModeMatchesStrictModeForValidSource) { + const std::string source = + "define check(target: Item): can_kill(quantity: target, 2)\n" + "region RR_TEST { events { EVENT_TEST: true } }\n" + "enum Color { RED, BLUE }"; + const auto strict = rls::parser::ParseStringWithIndex( + source, "parity.rls", rls::parser::ParseMode::Strict); + const auto editor = rls::parser::ParseStringWithIndex( + source, "parity.rls", rls::parser::ParseMode::Editor); + + EXPECT_TRUE(strict.file.diagnostics.empty()); + EXPECT_TRUE(editor.file.diagnostics.empty()); + ASSERT_EQ(strict.file.declarations.size(), editor.file.declarations.size()); + ASSERT_EQ(strict.file.declarations.size(), 3u); + + const auto& strictDefine = std::get(strict.file.declarations[0]); + const auto& editorDefine = std::get(editor.file.declarations[0]); + EXPECT_EQ(strictDefine.name.text, editorDefine.name.text); + expectSameSpan(strictDefine.name.span, editorDefine.name.span); + ASSERT_EQ(strictDefine.params.size(), editorDefine.params.size()); + EXPECT_EQ(strictDefine.params[0].name.text, editorDefine.params[0].name.text); + expectSameSpan(strictDefine.params[0].name.span, editorDefine.params[0].name.span); + + const auto& strictRegion = std::get(strict.file.declarations[1]); + const auto& editorRegion = std::get(editor.file.declarations[1]); + EXPECT_EQ(strictRegion.key.text, editorRegion.key.text); + expectSameSpan(strictRegion.key.span, editorRegion.key.span); + + ASSERT_EQ(strict.sourceIndex.declarations().size(), editor.sourceIndex.declarations().size()); + for (size_t index = 0; index < strict.sourceIndex.declarations().size(); ++index) { + EXPECT_EQ(strict.sourceIndex.declarations()[index].kind, + editor.sourceIndex.declarations()[index].kind); + expectSameSpan(strict.sourceIndex.declarations()[index].span, + editor.sourceIndex.declarations()[index].span); + } + + const auto strictName = strict.sourceIndex.nameAt({1, 30}); + const auto editorName = editor.sourceIndex.nameAt({1, 30}); + ASSERT_TRUE(strictName); + ASSERT_TRUE(editorName); + EXPECT_EQ(strictName->kind, editorName->kind); + EXPECT_EQ(strictName->text, editorName->text); + expectSameSpan(strictName->span, editorName->span); + + const auto strictCall = strict.sourceIndex.enclosingCall({1, 49}); + const auto editorCall = editor.sourceIndex.enclosingCall({1, 49}); + ASSERT_TRUE(strictCall); + ASSERT_TRUE(editorCall); + EXPECT_EQ(strictCall->activeArgument, editorCall->activeArgument); + ASSERT_EQ(strictCall->argumentRanges.size(), editorCall->argumentRanges.size()); + for (size_t index = 0; index < strictCall->argumentRanges.size(); ++index) { + expectSameSpan(strictCall->argumentRanges[index], editorCall->argumentRanges[index]); + } + + const auto strictRegionContext = strict.sourceIndex.regionContextAt({2, 32}); + const auto editorRegionContext = editor.sourceIndex.regionContextAt({2, 32}); + ASSERT_TRUE(strictRegionContext); + ASSERT_TRUE(editorRegionContext); + EXPECT_EQ(strictRegionContext->name, editorRegionContext->name); + EXPECT_EQ(strictRegionContext->activeSection, editorRegionContext->activeSection); + EXPECT_EQ(strictRegionContext->activeSectionEntries, + editorRegionContext->activeSectionEntries); + EXPECT_EQ(strict.sourceIndex.enumNames(), editor.sourceIndex.enumNames()); + EXPECT_EQ(strict.sourceIndex.enumNames(), std::vector{"Color"}); +} + +TEST(ParserTests, EditorModeMatchesStrictModeAcrossExamples) { + const auto examples = std::filesystem::path(RLS_REPO_ROOT) / "examples"; + ASSERT_TRUE(std::filesystem::is_directory(examples)); + size_t fileCount = 0; + for (const auto& entry : std::filesystem::recursive_directory_iterator(examples)) { + if (!entry.is_regular_file() || entry.path().extension() != ".rls") continue; + ++fileCount; + std::ifstream stream(entry.path(), std::ios::binary); + ASSERT_TRUE(stream) << entry.path(); + const std::string source{ + std::istreambuf_iterator(stream), + std::istreambuf_iterator()}; + const auto filename = entry.path().generic_string(); + const auto strict = rls::parser::ParseStringWithIndex( + source, filename, rls::parser::ParseMode::Strict); + const auto editor = rls::parser::ParseStringWithIndex( + source, filename, rls::parser::ParseMode::Editor); + + EXPECT_TRUE(strict.file.diagnostics.empty()) << entry.path(); + EXPECT_TRUE(editor.file.diagnostics.empty()) << entry.path(); + EXPECT_EQ(strict.file.declarations.size(), editor.file.declarations.size()) + << entry.path(); + ASSERT_EQ(strict.sourceIndex.declarations().size(), + editor.sourceIndex.declarations().size()) << entry.path(); + for (size_t index = 0; index < strict.sourceIndex.declarations().size(); ++index) { + expectSameSpan(strict.sourceIndex.declarations()[index].span, + editor.sourceIndex.declarations()[index].span); + } + EXPECT_EQ(strict.sourceIndex.regionNames(), editor.sourceIndex.regionNames()) + << entry.path(); + EXPECT_EQ(strict.sourceIndex.enumNames(), editor.sourceIndex.enumNames()) + << entry.path(); + + const auto sourceText = SourceText::FromUtf8(source); + ASSERT_TRUE(sourceText) << entry.path(); + for (size_t offset = 0; offset <= source.size(); ++offset) { + const auto position = sourceText->utf8PositionAtByteOffset(offset); + ASSERT_TRUE(position) << entry.path() << " at byte " << offset; + const auto strictName = strict.sourceIndex.nameAt(*position); + const auto editorName = editor.sourceIndex.nameAt(*position); + ASSERT_EQ(strictName.has_value(), editorName.has_value()) + << entry.path() << " at byte " << offset; + if (strictName && editorName) { + EXPECT_EQ(strictName->kind, editorName->kind); + EXPECT_EQ(strictName->text, editorName->text); + expectSameSpan(strictName->span, editorName->span); + } + const auto strictSyntax = strict.sourceIndex.syntaxAt(*position); + const auto editorSyntax = editor.sourceIndex.syntaxAt(*position); + ASSERT_EQ(strictSyntax.has_value(), editorSyntax.has_value()) + << entry.path() << " at byte " << offset; + if (strictSyntax && editorSyntax) { + EXPECT_EQ(strictSyntax->kind, editorSyntax->kind); + expectSameSpan(strictSyntax->span, editorSyntax->span); + } + } + } + EXPECT_GT(fileCount, 0u); +} + +TEST(ParserTests, EditorModeKeepsCompleteDeclarationsAroundMalformedSyntax) { + const std::string source = + "define before(): true\n" + "define broken(\n" + "define after(): before()\n"; + const auto editor = rls::parser::ParseStringWithIndex( + source, "partial.rls", rls::parser::ParseMode::Editor); + const auto strict = rls::parser::ParseStringWithIndex( + source, "partial.rls", rls::parser::ParseMode::Strict); + + ASSERT_FALSE(editor.file.diagnostics.empty()); + ASSERT_EQ(editor.file.declarations.size(), 2u); + EXPECT_EQ(std::get(editor.file.declarations[0]).name, "before"); + EXPECT_EQ(std::get(editor.file.declarations[1]).name, "after"); + EXPECT_EQ(std::get(editor.file.declarations[1]).name.span.start.line, 3u); + EXPECT_TRUE(editor.sourceIndex.nameAt({1, 8})); + EXPECT_TRUE(editor.sourceIndex.nameAt({3, 8})); + EXPECT_FALSE(editor.sourceIndex.nameAt({2, 8})); + + EXPECT_FALSE(strict.file.diagnostics.empty()); + EXPECT_TRUE(strict.file.declarations.empty()); +} + +TEST(ParserTests, EditorModeSynchronizesAfterMalformedConstructs) { + const std::vector sources = { + "define broken(\ndefine after(): true\n", + "enum Broken {\ndefine after(): true\n", + "region RR_BROKEN { events { EVENT_PARTIAL\ndefine after(): true\n", + "define broken(): target(\ndefine after(): true\n", + "define broken(): true ?\ndefine after(): true\n", + }; + + for (const auto& source : sources) { + SCOPED_TRACE(source); + const auto parsed = rls::parser::ParseStringWithIndex( + source, "synchronization.rls", rls::parser::ParseMode::Editor); + ASSERT_FALSE(parsed.file.diagnostics.empty()); + ASSERT_EQ(parsed.file.declarations.size(), 1u); + const auto* define = std::get_if(&parsed.file.declarations[0]); + ASSERT_NE(define, nullptr); + EXPECT_EQ(define->name, "after"); + EXPECT_EQ(define->name.span.start.line, 2u); + } +} + +TEST(ParserTests, EditorSyntaxClassifiesCompleteAndRecoveredEnums) { + const auto completeSource = SourceText::FromUtf8("enum Color { RED }"); + ASSERT_TRUE(completeSource); + const auto completeFile = rls::parser::ParseString( + completeSource->content(), "complete.rls"); + const auto complete = rls::parser::ParseEditorSyntax( + *completeSource, "complete.rls", completeFile); + ASSERT_EQ(complete.enumDeclarations.size(), 1u); + EXPECT_EQ(complete.enumDeclarations[0].name.text, "Color"); + EXPECT_EQ(complete.enumDeclarations[0].status, + rls::parser::SyntaxRecoveryStatus::Complete); + + const auto recoveredSource = SourceText::FromUtf8("enum Color {"); + ASSERT_TRUE(recoveredSource); + const auto recoveredFile = rls::parser::ParseString( + recoveredSource->content(), "recovered.rls"); + const auto recovered = rls::parser::ParseEditorSyntax( + *recoveredSource, "recovered.rls", recoveredFile); + ASSERT_EQ(recovered.enumDeclarations.size(), 1u); + EXPECT_EQ(recovered.enumDeclarations[0].name.text, "Color"); + EXPECT_EQ(recovered.enumDeclarations[0].status, + rls::parser::SyntaxRecoveryStatus::Recovered); + EXPECT_EQ(recovered.enumDeclarations[0].span.start.column, 1u); + EXPECT_EQ(recovered.enumDeclarations[0].span.end.column, 11u); +} + +TEST(ParserTests, EditorSyntaxRecoversMemberAccesses) { + const auto source = SourceText::FromUtf8( + "define first(): Color.RED\n" + "define second(): Color.\n" + "define ignored(): \"Quoted.FAKE\" # Commented.FAKE\n"); + ASSERT_TRUE(source); + const auto syntax = rls::parser::ParseEditorSyntax( + *source, "members.rls", File{}); + ASSERT_EQ(syntax.memberAccesses.size(), 2u); + EXPECT_EQ(syntax.memberAccesses[0].object.text, "Color"); + EXPECT_EQ(syntax.memberAccesses[0].memberSpan.start.column, 23u); + EXPECT_EQ(syntax.memberAccesses[0].memberSpan.end.column, 26u); + EXPECT_EQ(syntax.memberAccesses[0].status, + rls::parser::SyntaxRecoveryStatus::Complete); + EXPECT_EQ(syntax.memberAccesses[1].object.text, "Color"); + EXPECT_EQ(syntax.memberAccesses[1].memberSpan.start.column, 24u); + EXPECT_EQ(syntax.memberAccesses[1].memberSpan.end.column, 24u); + EXPECT_EQ(syntax.memberAccesses[1].status, + rls::parser::SyntaxRecoveryStatus::Recovered); +} + +TEST(ParserTests, EditorSyntaxRecoversFunctionTypePositions) { + const auto source = SourceText::FromUtf8( + "# define hidden(value: Fake)\n" + "\"extern define hidden() -> Fake\"\n" + "define choose(first = nested(a, b), second: Col\n" + "extern define convert(value: Bool) -> \n" + "define ignored(value = true ? false : true\n"); + ASSERT_TRUE(source); + const auto syntax = rls::parser::ParseEditorSyntax( + *source, "types.rls", File{}); + ASSERT_EQ(syntax.typePositions.size(), 3u); + EXPECT_EQ(syntax.typePositions[0].kind, + rls::parser::EditorTypePositionKind::Parameter); + EXPECT_EQ(syntax.typePositions[0].status, + rls::parser::SyntaxRecoveryStatus::Complete); + EXPECT_EQ(syntax.typePositions[1].kind, + rls::parser::EditorTypePositionKind::Parameter); + EXPECT_EQ(syntax.typePositions[1].status, + rls::parser::SyntaxRecoveryStatus::Complete); + EXPECT_EQ(syntax.typePositions[2].kind, + rls::parser::EditorTypePositionKind::Return); + EXPECT_EQ(syntax.typePositions[2].status, + rls::parser::SyntaxRecoveryStatus::Recovered); +} + TEST(ParserTests, WhitespaceOnlyReturnsEmpty) { const auto file = parse(" \n\n "); EXPECT_TRUE(file.declarations.empty()); @@ -435,6 +696,401 @@ TEST(ParseExpr, CallMixedArgs) { EXPECT_EQ(*call.args[1].name, "distance"); } +// == Source index ============================================================ + +TEST(SourceIndexTests, IndexesDeclarationsNamesExpressionsAndCalls) { + const auto parsed = rls::parser::ParseStringWithIndex( + "define check(target: Item): can_kill(quantity: target, 2)\n" + "region RR_TEST { events { EVENT_TEST: true } }"); + const auto& index = parsed.sourceIndex; + + ASSERT_EQ(index.declarations().size(), 2u); + EXPECT_EQ(index.declarationsIn("in_memory").size(), 2u); + EXPECT_TRUE(index.declarationsIn("other.rls").empty()); + const auto declaration = index.nameAt({1, 9}); + ASSERT_TRUE(declaration); + EXPECT_EQ(declaration->kind, rls::parser::SourceNameKind::Declaration); + EXPECT_EQ(declaration->text, "check"); + + const auto parameter = index.nameAt({1, 15}); + ASSERT_TRUE(parameter); + EXPECT_EQ(parameter->kind, rls::parser::SourceNameKind::Parameter); + EXPECT_EQ(parameter->text, "target"); + const auto& define = std::get(parsed.file.declarations[0]); + ASSERT_EQ(define.params.size(), 1u); + EXPECT_EQ(define.params[0].span.start.column, 14u); + EXPECT_EQ(define.params[0].span.end.column, 26u); + + const auto type = index.nameAt({1, 23}); + ASSERT_TRUE(type); + EXPECT_EQ(type->kind, rls::parser::SourceNameKind::Type); + EXPECT_EQ(type->text, "Item"); + + const auto callee = index.nameAt({1, 30}); + ASSERT_TRUE(callee); + EXPECT_EQ(callee->kind, rls::parser::SourceNameKind::CallCallee); + EXPECT_EQ(callee->text, "can_kill"); + + const auto syntax = index.syntaxAt({1, 30}); + ASSERT_TRUE(syntax); + EXPECT_EQ(syntax->kind, rls::parser::SyntaxKind::Name); + + const auto label = index.nameAt({1, 39}); + ASSERT_TRUE(label); + EXPECT_EQ(label->kind, rls::parser::SourceNameKind::ArgumentLabel); + EXPECT_EQ(label->text, "quantity"); + + const auto expression = index.enclosingExpression({1, 49}); + ASSERT_TRUE(expression); + EXPECT_EQ(expression->kind, rls::parser::SyntaxKind::Expression); + + const auto call = index.enclosingCall({1, 49}); + ASSERT_TRUE(call); + ASSERT_TRUE(call->activeArgument); + EXPECT_EQ(*call->activeArgument, 0u); + EXPECT_EQ(call->argumentRanges.size(), 2u); + + const auto secondArgument = index.enclosingCall({1, 56}); + ASSERT_TRUE(secondArgument); + ASSERT_TRUE(secondArgument->activeArgument); + EXPECT_EQ(*secondArgument->activeArgument, 1u); + + const auto& region = std::get(parsed.file.declarations[1]); + ASSERT_EQ(region.body.sections.size(), 1u); + EXPECT_EQ(region.body.sections[0].span.start.line, 2u); + EXPECT_EQ(region.body.sections[0].span.start.column, 18u); + EXPECT_EQ(region.body.sections[0].span.end.line, 2u); + EXPECT_GT(region.body.sections[0].span.end.column, 18u); + + const auto section = index.syntaxAt({2, 18}); + ASSERT_TRUE(section); + EXPECT_EQ(section->kind, rls::parser::SyntaxKind::Section); +} + +TEST(SourceIndexTests, IgnoresCommentsAndWhitespaceButIndexesStringsAndRecoverySafely) { + const auto parsed = rls::parser::ParseStringWithIndex( + "# a source comment\n" + "define label(): \"value\"\n" + "\n"); + const auto& index = parsed.sourceIndex; + EXPECT_FALSE(index.syntaxAt({1, 4})); + EXPECT_FALSE(index.nameAt({1, 4})); + EXPECT_FALSE(index.syntaxAt({3, 1})); + const auto stringExpression = index.enclosingExpression({2, 18}); + ASSERT_TRUE(stringExpression); + EXPECT_EQ(stringExpression->kind, rls::parser::SyntaxKind::Expression); + + const auto malformed = rls::parser::ParseStringWithIndex("define broken("); + EXPECT_FALSE(malformed.file.diagnostics.empty()); + EXPECT_TRUE(malformed.sourceIndex.declarations().empty()); + EXPECT_FALSE(malformed.sourceIndex.syntaxAt({1, 8})); + EXPECT_FALSE(malformed.sourceIndex.nameAt({1, 8})); +} + +TEST(SourceIndexTests, ReportsCompleteAndRecoveredRegionContexts) { + const auto complete = rls::parser::ParseStringWithIndex( + "region RR_TEST {\n" + " name: \"} region RR_FAKE {\"\n" + " # locations { FAKE: true }\n" + " events { EVENT_TEST: here == here }\n" + "}\n" + "extend region RR_TEST { locations { } }\n", + "regions.rls"); + const auto region = complete.sourceIndex.regionContextAt({2, 3}); + ASSERT_TRUE(region); + EXPECT_FALSE(region->extension); + EXPECT_EQ(region->dataKeys, std::vector{"name"}); + EXPECT_EQ(region->sectionKinds, + std::vector{SectionKind::Events}); + EXPECT_FALSE(region->activeSection); + const auto event = complete.sourceIndex.regionContextAt({4, 24}); + ASSERT_TRUE(event); + EXPECT_EQ(event->activeSection, SectionKind::Events); + const auto extension = complete.sourceIndex.regionContextAt({6, 36}); + ASSERT_TRUE(extension); + EXPECT_TRUE(extension->extension); + EXPECT_EQ(extension->activeSection, SectionKind::Locations); + + const auto recovered = rls::parser::ParseStringWithIndex( + "region RR_BROKEN {\n" + " name: \"Broken\"\n" + " loc\n", + "broken-region.rls", rls::parser::ParseMode::Editor); + ASSERT_FALSE(recovered.file.diagnostics.empty()); + const auto recoveredRegion = recovered.sourceIndex.regionContextAt({3, 5}); + ASSERT_TRUE(recoveredRegion); + EXPECT_FALSE(recoveredRegion->extension); + EXPECT_EQ(recoveredRegion->dataKeys, std::vector{"name"}); + + const auto recoveredExtension = rls::parser::ParseStringWithIndex( + "extend region RR_BROKEN { events { EVENT_PARTIAL\n" + "region RR_NEXT {\n", + "broken-extension.rls", rls::parser::ParseMode::Editor); + const auto extensionContext = + recoveredExtension.sourceIndex.regionContextAt({1, 49}); + ASSERT_TRUE(extensionContext); + EXPECT_TRUE(extensionContext->extension); + EXPECT_EQ(extensionContext->activeSection, SectionKind::Events); + EXPECT_EQ(recoveredExtension.sourceIndex.regionNames(), + std::vector{"RR_NEXT"}); + + const auto strict = rls::parser::ParseStringWithIndex( + "region RR_BROKEN {", "strict-region.rls", + rls::parser::ParseMode::Strict); + EXPECT_FALSE(strict.sourceIndex.regionContextAt({1, 19})); +} + +TEST(SourceIndexTests, ReportsRecoveredSectionEntryLabelContexts) { + const auto parsed = rls::parser::ParseStringWithIndex( + "region RR_TEST {\n" + " events {\n" + " EVENT_EXISTING: true\n" + " EVENT_PAR\n" + " }\n" + " locations {\n" + " \n" + " }\n" + "}\n", + "section-entries.rls", rls::parser::ParseMode::Editor); + ASSERT_FALSE(parsed.file.diagnostics.empty()); + + const auto event = parsed.sourceIndex.sectionEntryAt({4, 14}); + ASSERT_TRUE(event); + EXPECT_EQ(event->kind, SectionKind::Events); + EXPECT_EQ(event->labelSpan.start.column, 5u); + EXPECT_EQ(event->labelSpan.end.column, 14u); + const auto eventRegion = parsed.sourceIndex.regionContextAt({4, 14}); + ASSERT_TRUE(eventRegion); + EXPECT_EQ(eventRegion->name, "RR_TEST"); + EXPECT_EQ(eventRegion->activeSection, SectionKind::Events); + EXPECT_EQ(eventRegion->activeSectionEntries, + std::vector{"EVENT_EXISTING"}); + EXPECT_EQ(parsed.sourceIndex.sectionEntryNames(SectionKind::Events), + std::vector{"EVENT_EXISTING"}); + EXPECT_EQ(parsed.sourceIndex.sectionEntryNames( + SectionKind::Events, "RR_TEST"), + std::vector{"EVENT_EXISTING"}); + EXPECT_EQ(parsed.sourceIndex.regionNames(), + std::vector{"RR_TEST"}); + + const auto location = parsed.sourceIndex.sectionEntryAt({7, 5}); + ASSERT_TRUE(location); + EXPECT_EQ(location->kind, SectionKind::Locations); + EXPECT_EQ(location->labelSpan.start.column, 5u); + EXPECT_EQ(location->labelSpan.end.column, 5u); + EXPECT_FALSE(parsed.sourceIndex.sectionEntryAt({3, 21})); + + const auto strict = rls::parser::ParseStringWithIndex( + "region RR_TEST { events { EVENT_PARTIAL", + "strict-section.rls", rls::parser::ParseMode::Strict); + EXPECT_FALSE(strict.sourceIndex.sectionEntryAt({1, 46})); +} + +TEST(SourceIndexTests, ReportsCompleteAndRecoveredMemberAccessContexts) { + const auto complete = rls::parser::ParseStringWithIndex( + "define check(): Color.RED\n", "member.rls"); + const auto completeMember = complete.sourceIndex.memberAccessAt({1, 24}); + ASSERT_TRUE(completeMember); + EXPECT_EQ(completeMember->object, "Color"); + EXPECT_EQ(completeMember->memberSpan.start.column, 23u); + EXPECT_EQ(completeMember->memberSpan.end.column, 26u); + EXPECT_FALSE(complete.sourceIndex.memberAccessAt({1, 20})); + + const auto recovered = rls::parser::ParseStringWithIndex( + "define first(): Color.\n" + "define second(): Color.R\n" + "define ignored(): \"Color.FAKE\" # Color.COMMENT\n", + "recovered-member.rls", rls::parser::ParseMode::Editor); + ASSERT_FALSE(recovered.file.diagnostics.empty()); + const auto emptyMember = recovered.sourceIndex.memberAccessAt({1, 23}); + ASSERT_TRUE(emptyMember); + EXPECT_EQ(emptyMember->object, "Color"); + EXPECT_EQ(emptyMember->memberSpan.start.column, 23u); + EXPECT_EQ(emptyMember->memberSpan.end.column, 23u); + const auto partialMember = recovered.sourceIndex.memberAccessAt({2, 25}); + ASSERT_TRUE(partialMember); + EXPECT_EQ(partialMember->object, "Color"); + EXPECT_FALSE(recovered.sourceIndex.memberAccessAt({3, 31})); + + const auto strict = rls::parser::ParseStringWithIndex( + "define first(): Color.", + "strict-member.rls", rls::parser::ParseMode::Strict); + EXPECT_FALSE(strict.sourceIndex.memberAccessAt({1, 23})); +} + +TEST(SourceIndexTests, ReportsRecoveredNamedArgumentContexts) { + const auto emptySource = rls::parser::ParseStringWithIndex( + "define first(): target(", "empty-argument.rls", + rls::parser::ParseMode::Editor); + ASSERT_FALSE(emptySource.file.diagnostics.empty()); + const auto empty = emptySource.sourceIndex.namedArgumentAt({1, 24}); + ASSERT_TRUE(empty); + EXPECT_EQ(empty->callee, "target"); + EXPECT_EQ(empty->activeArgument, 0u); + ASSERT_EQ(empty->argumentLabels.size(), 1u); + EXPECT_FALSE(empty->argumentLabels[0]); + EXPECT_EQ(empty->labelSpan.start.column, 24u); + EXPECT_EQ(empty->labelSpan.end.column, 24u); + const auto emptyValue = emptySource.sourceIndex.callArgumentAt({1, 24}); + ASSERT_TRUE(emptyValue); + EXPECT_EQ(emptyValue->callee, "target"); + EXPECT_EQ(emptyValue->activeArgument, 0u); + const auto emptyCall = emptySource.sourceIndex.enclosingCall({1, 24}); + ASSERT_TRUE(emptyCall); + EXPECT_EQ(emptyCall->activeArgument, 0u); + + const auto partialSource = rls::parser::ParseStringWithIndex( + "define second(): target(first: true, se", "partial-argument.rls", + rls::parser::ParseMode::Editor); + ASSERT_FALSE(partialSource.file.diagnostics.empty()); + const auto partial = partialSource.sourceIndex.namedArgumentAt({1, 40}); + ASSERT_TRUE(partial); + EXPECT_EQ(partial->callee, "target"); + EXPECT_EQ(partial->activeArgument, 1u); + ASSERT_EQ(partial->argumentLabels.size(), 2u); + EXPECT_EQ(partial->argumentLabels[0], "first"); + EXPECT_FALSE(partial->argumentLabels[1]); + const auto namedValue = partialSource.sourceIndex.callArgumentAt({1, 32}); + ASSERT_TRUE(namedValue); + EXPECT_EQ(namedValue->activeArgument, 0u); + EXPECT_EQ(namedValue->valueSpan.start.column, 32u); + const auto partialValue = partialSource.sourceIndex.callArgumentAt({1, 40}); + ASSERT_TRUE(partialValue); + EXPECT_EQ(partialValue->activeArgument, 1u); + + const auto nestedSource = rls::parser::ParseStringWithIndex( + "define third(): target(true, nested(value), th", "nested-argument.rls", + rls::parser::ParseMode::Editor); + ASSERT_FALSE(nestedSource.file.diagnostics.empty()); + const auto nested = nestedSource.sourceIndex.namedArgumentAt({1, 47}); + ASSERT_TRUE(nested); + EXPECT_EQ(nested->callee, "target"); + EXPECT_EQ(nested->activeArgument, 2u); + ASSERT_EQ(nested->argumentLabels.size(), 3u); + EXPECT_FALSE(nested->argumentLabels[0]); + EXPECT_FALSE(nested->argumentLabels[1]); + EXPECT_FALSE(nested->argumentLabels[2]); + const auto nestedValue = nestedSource.sourceIndex.callArgumentAt({1, 38}); + ASSERT_TRUE(nestedValue); + EXPECT_EQ(nestedValue->callee, "nested"); + EXPECT_EQ(nestedValue->activeArgument, 0u); + + const std::string blankNamedSource = + "define fourth(): target(first:"; + const auto blankNamed = rls::parser::ParseStringWithIndex( + blankNamedSource, "blank-named-argument.rls", + rls::parser::ParseMode::Editor); + const auto blankNamedValue = blankNamed.sourceIndex.callArgumentAt({1, 31}); + ASSERT_TRUE(blankNamedValue); + EXPECT_EQ(blankNamedValue->argumentLabels[0], "first"); + EXPECT_EQ(blankNamedValue->valueSpan.start.line, + blankNamedValue->valueSpan.end.line); + EXPECT_EQ(blankNamedValue->valueSpan.start.column, + blankNamedValue->valueSpan.end.column); + + const std::string trailingSlotSource = + "define fifth(): target(first: true, "; + const auto trailingSlot = rls::parser::ParseStringWithIndex( + trailingSlotSource, "trailing-call-slot.rls", + rls::parser::ParseMode::Editor); + const auto trailingValue = trailingSlot.sourceIndex.callArgumentAt({1, 37}); + ASSERT_TRUE(trailingValue); + EXPECT_EQ(trailingValue->activeArgument, 1u); + ASSERT_EQ(trailingValue->argumentLabels.size(), 2u); + EXPECT_EQ(trailingValue->argumentLabels[0], "first"); + EXPECT_FALSE(trailingValue->argumentLabels[1]); + + const auto ignored = rls::parser::ParseStringWithIndex( + "define text(): \"target(fake:)\" # target(comment:)\n" + "define broken(", + "ignored-calls.rls", rls::parser::ParseMode::Editor); + EXPECT_FALSE(ignored.sourceIndex.callArgumentAt({1, 28})); + EXPECT_FALSE(ignored.sourceIndex.namedArgumentAt({1, 46})); + + const auto strict = rls::parser::ParseStringWithIndex( + "define strict(): target(", "strict-argument.rls", + rls::parser::ParseMode::Strict); + EXPECT_FALSE(strict.sourceIndex.namedArgumentAt({1, 25})); + EXPECT_FALSE(strict.sourceIndex.callArgumentAt({1, 25})); + + const auto closedTrailingSlot = rls::parser::ParseStringWithIndex( + "define sixth(): target(true,)", "closed-trailing-slot.rls", + rls::parser::ParseMode::Editor); + const auto closedCall = closedTrailingSlot.sourceIndex.enclosingCall({1, 29}); + ASSERT_TRUE(closedCall); + EXPECT_EQ(closedCall->activeArgument, 1u); + ASSERT_EQ(closedCall->argumentRanges.size(), 2u); + EXPECT_EQ(closedCall->argumentRanges[1].start.column, 29u); + EXPECT_EQ(closedCall->argumentRanges[1].end.column, 29u); +} + +TEST(SourceIndexTests, ReportsRecoveredFunctionTypePositions) { + const auto positionAtEnd = [](const std::string& source) { + const auto text = SourceText::FromUtf8(source); + EXPECT_TRUE(text); + return *text->utf8PositionAtByteOffset(source.size()); + }; + + const std::string parameterSource = + "enum Color { RED }\ndefine choose(value: Col"; + const auto parameter = rls::parser::ParseStringWithIndex( + parameterSource, "parameter-type.rls", rls::parser::ParseMode::Editor); + ASSERT_FALSE(parameter.file.diagnostics.empty()); + const auto parameterType = parameter.sourceIndex.typePositionAt( + positionAtEnd(parameterSource)); + ASSERT_TRUE(parameterType); + EXPECT_EQ(parameter.sourceIndex.enumNames(), std::vector{"Color"}); + const auto strictParameter = rls::parser::ParseStringWithIndex( + parameterSource, "parameter-type.rls", rls::parser::ParseMode::Strict); + EXPECT_TRUE(strictParameter.sourceIndex.enumNames().empty()); + + const auto filteredEnums = rls::parser::ParseStringWithIndex( + "# enum Commented { VALUE }\n" + "define text(): \"enum Quoted { VALUE }\"\n" + "enum Real { VALUE }\n" + "define broken(", + "filtered-enums.rls", rls::parser::ParseMode::Editor); + EXPECT_EQ(filteredEnums.sourceIndex.enumNames(), + std::vector{"Real"}); + + const std::string blankParameterSource = "define choose(value: "; + const auto blankParameter = rls::parser::ParseStringWithIndex( + blankParameterSource, "blank-parameter-type.rls", + rls::parser::ParseMode::Editor); + const auto blankParameterType = blankParameter.sourceIndex.typePositionAt( + positionAtEnd(blankParameterSource)); + ASSERT_TRUE(blankParameterType); + EXPECT_EQ(blankParameterType->typeSpan.start.column, 21u); + EXPECT_EQ(blankParameterType->typeSpan.end.column, 22u); + + const std::string returnSource = + "extern define choose(value: Bool) -> Col"; + const auto returnType = rls::parser::ParseStringWithIndex( + returnSource, "return-type.rls"); + ASSERT_TRUE(returnType.file.diagnostics.empty()); + EXPECT_TRUE(returnType.sourceIndex.typePositionAt(positionAtEnd(returnSource))); + + const std::string blankReturnSource = "extern define choose() -> "; + const auto blankReturn = rls::parser::ParseStringWithIndex( + blankReturnSource, "blank-return-type.rls", + rls::parser::ParseMode::Editor); + EXPECT_TRUE(blankReturn.sourceIndex.typePositionAt( + positionAtEnd(blankReturnSource))); + + const std::string defaultSource = + "define choose(value = true ? false : tru"; + const auto defaultExpression = rls::parser::ParseStringWithIndex( + defaultSource, "default-expression.rls", rls::parser::ParseMode::Editor); + EXPECT_FALSE(defaultExpression.sourceIndex.typePositionAt( + positionAtEnd(defaultSource))); + + const auto strictBlank = rls::parser::ParseStringWithIndex( + blankParameterSource, "strict-blank-type.rls", + rls::parser::ParseMode::Strict); + EXPECT_FALSE(strictBlank.sourceIndex.typePositionAt( + positionAtEnd(blankParameterSource))); +} + TEST(ParseExpr, NestedCalls) { const auto& e = parseExpr("can_use(setting(RSK_FOO))"); ASSERT_TRUE(std::holds_alternative(e.node)); @@ -554,6 +1210,89 @@ TEST(ParseExpr, SpanIsNonZero) { EXPECT_GT(def.span.start.column, 0u); } +TEST(ParseSpans, PreservesCompleteRangesForAstNodes) { + const auto file = parse( + "region RR_TEST {\n" + " name: \"Test\"\n" + " events {\n" + " EVENT_TEST: has(ITEM) and true\n" + " }\n" + "}\n" + "define check(value: Item): not Item.VALUE and make_cond([1, 2])() ? true : false\n" + "enum Color { RED, GREEN = 2 }\n" + "extern enum External { VALUE, EXT_* }\n"); + + auto expectCompleteSpan = [](const Span& span) { + EXPECT_EQ(span.file, "in_memory"); + EXPECT_GT(span.start.line, 0u); + EXPECT_GT(span.start.column, 0u); + EXPECT_TRUE(span.end.line > span.start.line || + (span.end.line == span.start.line && span.end.column > span.start.column)); + }; + auto expectPosition = [](Position actual, uint32_t line, uint32_t column) { + EXPECT_EQ(actual.line, line); + EXPECT_EQ(actual.column, column); + }; + + ASSERT_EQ(file.declarations.size(), 4u); + const auto& region = std::get(file.declarations[0]); + expectCompleteSpan(region.span); + expectPosition(region.span.start, 1, 1); + expectPosition(region.span.end, 6, 2); + ASSERT_EQ(region.body.data.size(), 1u); + expectCompleteSpan(region.body.data[0].span); + expectPosition(region.body.data[0].span.start, 2, 3); + expectPosition(region.body.data[0].span.end, 2, 15); + expectCompleteSpan(region.body.data[0].key.span); + expectCompleteSpan(region.body.data[0].value->span); + ASSERT_EQ(region.body.sections.size(), 1u); + const auto& section = region.body.sections[0]; + expectCompleteSpan(section.span); + expectPosition(section.span.start, 3, 3); + expectPosition(section.span.end, 5, 4); + ASSERT_EQ(section.entries.size(), 1u); + expectCompleteSpan(section.entries[0].span); + expectPosition(section.entries[0].span.start, 4, 5); + expectPosition(section.entries[0].span.end, 4, 35); + expectCompleteSpan(section.entries[0].name.span); + expectCompleteSpan(section.entries[0].condition->span); + + const auto& define = std::get(file.declarations[1]); + expectCompleteSpan(define.span); + expectCompleteSpan(define.name.span); + expectCompleteSpan(define.params[0].name.span); + expectCompleteSpan(define.params[0].type->name.span); + expectCompleteSpan(define.body->span); + const auto& ternary = std::get(define.body->node); + expectCompleteSpan(ternary.condition->span); + const auto& logical = std::get(ternary.condition->node); + expectCompleteSpan(logical.left->span); + const auto& member = std::get(std::get(logical.left->node).operand->node); + expectCompleteSpan(member.object.span); + expectCompleteSpan(member.member.span); + const auto& invoke = std::get(logical.right->node); + expectCompleteSpan(invoke.callee->span); + const auto& call = std::get(invoke.callee->node); + expectCompleteSpan(call.callee.span); + expectCompleteSpan(call.args[0].value->span); + + const auto& enumDecl = std::get(file.declarations[2]); + expectCompleteSpan(enumDecl.span); + expectCompleteSpan(enumDecl.name.span); + for (const auto& member : enumDecl.members) { + expectCompleteSpan(member.span); + expectCompleteSpan(member.name.span); + } + + const auto& externEnum = std::get(file.declarations[3]); + expectCompleteSpan(externEnum.span); + expectCompleteSpan(externEnum.name.span); + const auto& externalMember = std::get(externEnum.entries[0]); + expectCompleteSpan(externalMember.span); + expectCompleteSpan(externalMember.name.span); + expectCompleteSpan(std::get(externEnum.entries[1]).span); +} + // == Define declaration ======================================================= TEST(ParseDefine, NoParams) { @@ -741,8 +1480,10 @@ TEST(ParseMemberAccess, BasicDottedAccess) { TEST(ParseMemberAccess, SpanIsNonZero) { const auto& expr = parseExpr("Item.RG_HOOKSHOT"); - // Structural (remove_content) nodes have a valid start position but no end. - EXPECT_GT(expr.span.start.column, 0u); + EXPECT_EQ(expr.span.start.line, 1u); + EXPECT_EQ(expr.span.start.column, 13u); + EXPECT_EQ(expr.span.end.line, 1u); + EXPECT_EQ(expr.span.end.column, 29u); } TEST(ParseMemberAccess, UsedAsCallArg) { diff --git a/project/CMakeLists.txt b/project/CMakeLists.txt new file mode 100644 index 0000000..954b346 --- /dev/null +++ b/project/CMakeLists.txt @@ -0,0 +1,20 @@ +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 +) +FetchContent_MakeAvailable(nlohmann_json) + +add_library(project STATIC + src/project.cpp +) + +target_include_directories(project PUBLIC include) +target_link_libraries(project PUBLIC nlohmann_json::nlohmann_json) + +if(BUILD_TESTING) + rls_add_gtest(project_tests + tests/project_tests.cpp + ) + target_link_libraries(project_tests PRIVATE project) +endif() \ No newline at end of file diff --git a/project/include/project.h b/project/include/project.h new file mode 100644 index 0000000..197b775 --- /dev/null +++ b/project/include/project.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace rls::project { + +struct ConfigurationDiagnosticData { + uint32_t version = 1; + std::string actionKind; + std::vector arguments; +}; + +struct ConfigurationDiagnostic { + std::filesystem::path path; + std::string code; + std::string message; + size_t startByte = 0; + size_t endByte = 0; + std::optional data; +}; + +struct SourceCollection { + std::vector sourceFiles; + std::vector warnings; + std::string error; +}; + +struct ManifestConfig { + std::filesystem::path manifestPath; + std::filesystem::path root; + std::vector sources; + std::vector excludes; + std::vector> transpilerOutputs; +}; + +struct ManifestLoadResult { + std::optional config; + std::string error; + std::vector diagnostics; +}; + +struct FileProject { + std::optional manifest; + std::vector sourceFiles; + bool isStandalone = false; + std::string error; + std::vector diagnostics; +}; + +/// Collect explicit file and directory inputs using canonical, stable paths. +SourceCollection CollectExplicitSources(const std::vector& inputs); + +/// Find the nearest rls.json at or above a file or directory. +std::optional FindManifest(const std::filesystem::path& start); + +/// Read and validate a version-1 rls.json without loading source contents. +ManifestLoadResult LoadManifest(const std::filesystem::path& manifestPath); + +/// Expand manifest source entries into canonical RLS source paths without parsing contents. +SourceCollection CollectManifestSources(const ManifestConfig& config); + +/// Resolve a file to nearest-manifest membership or a standalone one-file configuration. +FileProject ResolveFileProject(const std::filesystem::path& file); + +} // namespace rls::project \ No newline at end of file diff --git a/project/include/project_diagnostics.h b/project/include/project_diagnostics.h new file mode 100644 index 0000000..6b7d376 --- /dev/null +++ b/project/include/project_diagnostics.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include + +#include "project.h" + +namespace rls::project::diagnostics { + +inline ConfigurationDiagnostic ManifestUnavailable( + std::filesystem::path path, std::string_view requestedPath) { + return {std::move(path), "RLS-C001", + "could not open manifest: " + std::string(requestedPath), 0, 0, + ConfigurationDiagnosticData{1, "rls.openManifest", {std::string(requestedPath)}}}; +} + +inline ConfigurationDiagnostic InvalidJson( + std::filesystem::path path, std::string_view detail, + size_t startByte, size_t endByte) { + return {std::move(path), "RLS-C002", + "invalid JSON: " + std::string(detail), startByte, endByte, + ConfigurationDiagnosticData{1, "rls.fixManifestJson", {std::string(detail)}}}; +} + +inline ConfigurationDiagnostic ManifestMustBeObject(std::filesystem::path path) { + return {std::move(path), "RLS-C003", "manifest must be a JSON object", 0, 0}; +} + +inline ConfigurationDiagnostic UnknownManifestField( + std::filesystem::path path, std::string_view field) { + return {std::move(path), "RLS-C003", + "unknown manifest field: " + std::string(field), 0, 0, + ConfigurationDiagnosticData{1, "rls.removeManifestField", {std::string(field)}}}; +} + +inline ConfigurationDiagnostic UnsupportedManifestVersion(std::filesystem::path path) { + return {std::move(path), "RLS-C003", "unsupported manifest version", 0, 0}; +} + +inline ConfigurationDiagnostic SourcesRequired(std::filesystem::path path) { + return {std::move(path), "RLS-C003", + "manifest requires a non-empty sources array", 0, 0}; +} + +inline ConfigurationDiagnostic SourceEntryMustBeString(std::filesystem::path path) { + return {std::move(path), "RLS-C003", "sources entries must be strings", 0, 0}; +} + +inline ConfigurationDiagnostic ManifestPathMustBeRelative( + std::filesystem::path path, std::string_view value) { + return {std::move(path), "RLS-C003", + "manifest paths must be relative: " + std::string(value), 0, 0}; +} + +inline ConfigurationDiagnostic ManifestPathEscapesRoot( + std::filesystem::path path, std::string_view value) { + return {std::move(path), "RLS-C003", + "manifest path escapes the project root: " + std::string(value), 0, 0}; +} + +inline ConfigurationDiagnostic ExcludeMustBeArray(std::filesystem::path path) { + return {std::move(path), "RLS-C003", "exclude must be an array", 0, 0}; +} + +inline ConfigurationDiagnostic ExcludeEntryMustBeString(std::filesystem::path path) { + return {std::move(path), "RLS-C003", "exclude entries must be strings", 0, 0}; +} + +inline ConfigurationDiagnostic TranspilersMustBeObject(std::filesystem::path path) { + return {std::move(path), "RLS-C003", "transpilers must be an object", 0, 0}; +} + +inline ConfigurationDiagnostic InvalidTranspilerConfiguration( + std::filesystem::path path, std::string_view name) { + return {std::move(path), "RLS-C003", + "invalid transpiler configuration: " + std::string(name), 0, 0}; +} + +inline ConfigurationDiagnostic SourceCollectionFailed( + std::filesystem::path path, std::string message) { + ConfigurationDiagnosticData data{ + 1, "rls.configureManifestSources", {message}}; + return {std::move(path), "RLS-C004", std::move(message), 0, 0, + std::move(data)}; +} + +} // namespace rls::project::diagnostics \ No newline at end of file diff --git a/project/rls.schema.json b/project/rls.schema.json new file mode 100644 index 0000000..9b52137 --- /dev/null +++ b/project/rls.schema.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://randologicscript.dev/schema/rls.schema.json", + "title": "Rando Logic Script project", + "type": "object", + "additionalProperties": false, + "required": ["version", "sources"], + "properties": { + "version": { + "const": 1 + }, + "sources": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/relativePath" } + }, + "exclude": { + "type": "array", + "items": { "$ref": "#/$defs/relativePath" } + }, + "transpilers": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "required": ["output"], + "properties": { + "output": { "$ref": "#/$defs/relativePath" } + } + } + } + }, + "$defs": { + "relativePath": { + "type": "string", + "minLength": 1, + "not": { + "anyOf": [ + { "pattern": "^/" }, + { "pattern": "^[A-Za-z]:[/\\\\]" } + ] + } + } + } +} \ No newline at end of file diff --git a/project/src/project.cpp b/project/src/project.cpp new file mode 100644 index 0000000..8394345 --- /dev/null +++ b/project/src/project.cpp @@ -0,0 +1,341 @@ +#include "project.h" +#include "project_diagnostics.h" + +#include +#include +#include +#include + +#include + +namespace fs = std::filesystem; + +namespace rls::project { +namespace { + +fs::path canonicalPath(const fs::path& path) { + std::error_code error; + const auto resolved = fs::weakly_canonical(path, error); + return error ? fs::absolute(path).lexically_normal() : resolved; +} + +bool resolvesWithinRoot(const fs::path& root, const fs::path& path) { + const auto relative = path.lexically_relative(root); + if (relative.empty()) + return false; + return std::ranges::none_of(relative, [](const fs::path& component) { + return component == ".."; + }); +} + +std::optional resolveManifestPath( + const fs::path& root, + const fs::path& manifestPath, + const std::string& value, + std::optional& diagnostic) +{ + const fs::path path(value); + if (path.is_absolute()) { + diagnostic = diagnostics::ManifestPathMustBeRelative(manifestPath, value); + return std::nullopt; + } + + const auto resolved = canonicalPath(root / path); + if (!resolvesWithinRoot(root, resolved)) { + diagnostic = diagnostics::ManifestPathEscapesRoot(manifestPath, value); + return std::nullopt; + } + return resolved; +} + +bool isWithin(const fs::path& parent, const fs::path& path) { + const auto relative = path.lexically_relative(parent); + return !relative.empty() && std::ranges::none_of(relative, [](const fs::path& component) { + return component == ".."; + }); +} + +bool isDefaultExcluded(const fs::path& relativePath) { + static const std::set excludedNames = { + ".git", ".hg", ".svn", ".cache", "build", "node_modules", + }; + return std::ranges::any_of(relativePath, [](const fs::path& component) { + return excludedNames.contains(component); + }); +} + +bool isManifestExcluded(const ManifestConfig& config, const fs::path& path) { + return std::ranges::any_of(config.excludes, [&path](const fs::path& exclude) { + const auto pattern = exclude.generic_string(); + const auto suffix = std::string("/**"); + const auto prefix = pattern.ends_with(suffix) + ? fs::path(pattern.substr(0, pattern.size() - suffix.size())) + : exclude; + return isWithin(prefix, path) || prefix == path; + }); +} + +bool isOutputExcluded(const ManifestConfig& config, const fs::path& path) { + return std::ranges::any_of(config.transpilerOutputs, [&path](const auto& output) { + return isWithin(output.second, path) || output.second == path; + }); +} + +bool isExcluded( + const ManifestConfig& config, + const fs::path& path, + bool overridesDefaultExclusions, + bool includesOutput) +{ + if (isManifestExcluded(config, path)) + return true; + if (!includesOutput && isOutputExcluded(config, path)) + return true; + return !overridesDefaultExclusions && isDefaultExcluded(path.lexically_relative(config.root)); +} + +void setManifestError( + ManifestLoadResult& result, ConfigurationDiagnostic diagnostic) { + result.error = diagnostic.message; + result.diagnostics.push_back(std::move(diagnostic)); +} + +} // namespace + +SourceCollection CollectExplicitSources(const std::vector& inputs) { + SourceCollection result; + std::set paths; + + for (const auto& input : inputs) { + if (!fs::exists(input)) { + result.error = "path does not exist: " + input.string(); + return result; + } + + if (fs::is_directory(input)) { + const auto before = paths.size(); + for (const auto& entry : fs::recursive_directory_iterator(input)) { + if (entry.is_regular_file() && entry.path().extension() == ".rls") + paths.insert(canonicalPath(entry.path())); + } + if (paths.size() == before) + result.warnings.push_back("no .rls files found in " + input.string()); + } else { + paths.insert(canonicalPath(input)); + } + } + + result.sourceFiles.assign(paths.begin(), paths.end()); + return result; +} + +std::optional FindManifest(const fs::path& start) { + fs::path directory = canonicalPath(start); + if (!fs::is_directory(directory)) + directory = directory.parent_path(); + + while (!directory.empty()) { + const auto manifest = directory / "rls.json"; + if (fs::is_regular_file(manifest)) + return manifest; + + const auto parent = directory.parent_path(); + if (parent == directory) + break; + directory = parent; + } + return std::nullopt; +} + +ManifestLoadResult LoadManifest(const fs::path& manifestPath) { + ManifestLoadResult result; + const auto canonicalManifest = canonicalPath(manifestPath); + std::ifstream input(canonicalManifest); + if (!input) { + setManifestError(result, diagnostics::ManifestUnavailable( + canonicalManifest, manifestPath.string())); + return result; + } + + const std::string manifestContent{ + std::istreambuf_iterator(input), std::istreambuf_iterator()}; + + nlohmann::json json; + try { + json = nlohmann::json::parse(manifestContent); + } catch (const nlohmann::json::parse_error& exception) { + const size_t offset = exception.byte == 0 + ? 0 : std::min(exception.byte - 1, manifestContent.size()); + setManifestError(result, diagnostics::InvalidJson( + canonicalManifest, exception.what(), offset, offset)); + return result; + } + + if (!json.is_object()) { + setManifestError(result, diagnostics::ManifestMustBeObject(canonicalManifest)); + return result; + } + for (const auto& [key, value] : json.items()) { + if (key != "version" && key != "sources" && key != "exclude" && key != "transpilers") { + setManifestError(result, diagnostics::UnknownManifestField( + canonicalManifest, key)); + return result; + } + } + if (json.value("version", 0) != 1) { + setManifestError(result, diagnostics::UnsupportedManifestVersion(canonicalManifest)); + return result; + } + if (!json.contains("sources") || !json["sources"].is_array() || json["sources"].empty()) { + setManifestError(result, diagnostics::SourcesRequired(canonicalManifest)); + return result; + } + + ManifestConfig config; + config.manifestPath = canonicalManifest; + config.root = canonicalManifest.parent_path(); + for (const auto& source : json["sources"]) { + if (!source.is_string()) { + setManifestError(result, diagnostics::SourceEntryMustBeString(canonicalManifest)); + return result; + } + std::optional diagnostic; + auto resolved = resolveManifestPath( + config.root, canonicalManifest, source.get(), diagnostic); + if (!resolved) { + setManifestError(result, std::move(*diagnostic)); + return result; + } + config.sources.push_back(std::move(*resolved)); + } + if (json.contains("exclude")) { + if (!json["exclude"].is_array()) { + setManifestError(result, diagnostics::ExcludeMustBeArray(canonicalManifest)); + return result; + } + for (const auto& exclude : json["exclude"]) { + if (!exclude.is_string()) { + setManifestError(result, diagnostics::ExcludeEntryMustBeString(canonicalManifest)); + return result; + } + std::optional diagnostic; + auto resolved = resolveManifestPath( + config.root, canonicalManifest, exclude.get(), diagnostic); + if (!resolved) { + setManifestError(result, std::move(*diagnostic)); + return result; + } + config.excludes.push_back(std::move(*resolved)); + } + } + if (json.contains("transpilers")) { + if (!json["transpilers"].is_object()) { + setManifestError(result, diagnostics::TranspilersMustBeObject(canonicalManifest)); + return result; + } + for (const auto& [name, settings] : json["transpilers"].items()) { + if (!settings.is_object() || + !settings.contains("output") || !settings["output"].is_string()) { + setManifestError(result, diagnostics::InvalidTranspilerConfiguration( + canonicalManifest, name)); + return result; + } + std::optional diagnostic; + auto output = resolveManifestPath( + config.root, canonicalManifest, + settings["output"].get(), diagnostic); + if (!output) { + setManifestError(result, std::move(*diagnostic)); + return result; + } + config.transpilerOutputs.emplace_back(name, std::move(*output)); + } + } + + result.config = std::move(config); + return result; +} + +SourceCollection CollectManifestSources(const ManifestConfig& config) { + SourceCollection result; + std::set paths; + + for (const auto& source : config.sources) { + if (!fs::exists(source)) { + result.error = "manifest source does not exist: " + source.string(); + return result; + } + + if (fs::is_regular_file(source)) { + if (source.extension() == ".rls" && !isExcluded(config, source, true, true)) + paths.insert(source); + continue; + } + + const auto before = paths.size(); + const bool overridesDefaultExclusions = source != config.root && + isDefaultExcluded(source.lexically_relative(config.root)); + const bool includesOutput = std::ranges::any_of( + config.transpilerOutputs, + [&source](const auto& output) { + return source == output.second || isWithin(output.second, source); + }); + for (auto entry = fs::recursive_directory_iterator(source); + entry != fs::recursive_directory_iterator(); ++entry) { + const auto path = canonicalPath(entry->path()); + if (entry->is_directory() && isExcluded( + config, path, overridesDefaultExclusions, includesOutput)) { + entry.disable_recursion_pending(); + continue; + } + if (entry->is_regular_file() && path.extension() == ".rls" && + !isExcluded(config, path, overridesDefaultExclusions, includesOutput)) { + paths.insert(path); + } + } + if (paths.size() == before) + result.warnings.push_back("no .rls files found in manifest source: " + source.string()); + } + + result.sourceFiles.assign(paths.begin(), paths.end()); + if (result.sourceFiles.empty()) + result.error = "manifest does not resolve to any .rls source files"; + return result; +} + +FileProject ResolveFileProject(const fs::path& file) { + FileProject result; + if (!fs::exists(file)) { + result.error = "path does not exist: " + file.string(); + return result; + } + + const auto canonicalFile = canonicalPath(file); + const auto manifestPath = FindManifest(canonicalFile); + if (!manifestPath) { + result.sourceFiles.push_back(canonicalFile); + result.isStandalone = true; + return result; + } + + auto manifest = LoadManifest(*manifestPath); + if (!manifest.config) { + result.error = std::move(manifest.error); + result.diagnostics = std::move(manifest.diagnostics); + return result; + } + + auto sources = CollectManifestSources(*manifest.config); + if (!sources.error.empty()) { + result.error = std::move(sources.error); + result.diagnostics.push_back(diagnostics::SourceCollectionFailed( + *manifestPath, result.error)); + return result; + } + + result.manifest = std::move(manifest.config); + result.sourceFiles = std::move(sources.sourceFiles); + return result; +} + +} // namespace rls::project \ No newline at end of file diff --git a/project/tests/project_tests.cpp b/project/tests/project_tests.cpp new file mode 100644 index 0000000..e6c9918 --- /dev/null +++ b/project/tests/project_tests.cpp @@ -0,0 +1,239 @@ +#include +#include +#include +#include + +#include + +#include "project.h" + +namespace fs = std::filesystem; + +namespace { + +class TemporaryDirectory { +public: + TemporaryDirectory() : path_(fs::temp_directory_path() / + ("rls-project-tests-" + std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()))) { + fs::create_directories(path_); + } + + ~TemporaryDirectory() { + std::error_code error; + fs::remove_all(path_, error); + } + + const fs::path& path() const { return path_; } + +private: + fs::path path_; +}; + +void writeFile(const fs::path& path, const std::string& content = "define test(): true\n") { + fs::create_directories(path.parent_path()); + std::ofstream(path) << content; +} + +TEST(ProjectSources, CanonicalizesDeduplicatesAndSortsFiles) { + TemporaryDirectory directory; + writeFile(directory.path() / "z.rls"); + writeFile(directory.path() / "nested" / "a.rls"); + + const auto result = rls::project::CollectExplicitSources({ + directory.path(), + directory.path() / "nested" / ".." / "z.rls", + }); + + ASSERT_TRUE(result.error.empty()); + ASSERT_EQ(result.sourceFiles.size(), 2); + EXPECT_LT(result.sourceFiles[0], result.sourceFiles[1]); + EXPECT_EQ(result.sourceFiles[1], fs::weakly_canonical(directory.path() / "z.rls")); +} + +TEST(ProjectManifest, FindsNearestManifest) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json"); + writeFile(directory.path() / "nested" / "rls.json"); + writeFile(directory.path() / "nested" / "src" / "logic.rls"); + + const auto manifest = rls::project::FindManifest(directory.path() / "nested" / "src" / "logic.rls"); + + ASSERT_TRUE(manifest.has_value()); + EXPECT_EQ(*manifest, fs::weakly_canonical(directory.path() / "nested" / "rls.json")); +} + +TEST(ProjectManifest, LoadsAndResolvesPathsFromManifestDirectory) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src", "stdlib/host.rls"], + "exclude": ["generated"], + "transpilers": { "soh": { "output": "generated/soh" } } + })"); + + const auto result = rls::project::LoadManifest(directory.path() / "rls.json"); + + ASSERT_TRUE(result.error.empty()); + ASSERT_TRUE(result.config.has_value()); + EXPECT_EQ(result.config->root, fs::weakly_canonical(directory.path())); + EXPECT_EQ(result.config->sources[0], fs::weakly_canonical(directory.path() / "src")); + EXPECT_EQ(result.config->transpilerOutputs[0].second, + fs::weakly_canonical(directory.path() / "generated" / "soh")); +} + +TEST(ProjectManifest, ResolvesRelativeManifestPathsIndependentlyOfCurrentDirectory) { + TemporaryDirectory directory; + writeFile(directory.path() / "nested" / "rls.json", R"({ "version": 1, "sources": ["src"] })"); + writeFile(directory.path() / "nested" / "src" / "logic.rls"); + + const auto result = rls::project::LoadManifest(directory.path() / "nested" / "." / "rls.json"); + + ASSERT_TRUE(result.config.has_value()) << result.error; + const auto sources = rls::project::CollectManifestSources(*result.config); + ASSERT_TRUE(sources.error.empty()); + ASSERT_EQ(sources.sourceFiles.size(), 1); + EXPECT_EQ(sources.sourceFiles[0], + fs::weakly_canonical(directory.path() / "nested" / "src" / "logic.rls")); +} + +TEST(ProjectManifest, RejectsUnknownFieldsAndEscapingOutputPaths) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ "version": 1, "sources": ["src"], "extra": true })"); + EXPECT_EQ(rls::project::LoadManifest(directory.path() / "rls.json").error, + "unknown manifest field: extra"); + + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src"], + "transpilers": { "ap": { "output": "../outside" } } + })"); + EXPECT_EQ(rls::project::LoadManifest(directory.path() / "rls.json").error, + "manifest path escapes the project root: ../outside"); +} + +TEST(ProjectManifest, ReturnsStructuredConfigurationDiagnostics) { + TemporaryDirectory directory; + const fs::path manifestPath = directory.path() / "rls.json"; + writeFile(manifestPath, "{\"name\":\"\xF0\x9F\x98\x80\", invalid}"); + + const auto invalidJson = rls::project::LoadManifest(manifestPath); + ASSERT_FALSE(invalidJson.config.has_value()); + ASSERT_EQ(invalidJson.diagnostics.size(), 1); + EXPECT_EQ(invalidJson.diagnostics[0].path, fs::weakly_canonical(manifestPath)); + EXPECT_EQ(invalidJson.diagnostics[0].code, "RLS-C002"); + EXPECT_GT(invalidJson.diagnostics[0].startByte, 0); + + writeFile(manifestPath, R"({"version":1,"sources":["missing"]})"); + writeFile(directory.path() / "logic.rls"); + const auto resolved = rls::project::ResolveFileProject(directory.path() / "logic.rls"); + ASSERT_FALSE(resolved.error.empty()); + ASSERT_EQ(resolved.diagnostics.size(), 1); + EXPECT_EQ(resolved.diagnostics[0].code, "RLS-C004"); + EXPECT_EQ(resolved.diagnostics[0].path, fs::weakly_canonical(manifestPath)); +} + +TEST(ProjectManifest, AcceptsTranspilerNamesWithoutKnowingImplementations) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["src"], + "transpilers": { "custom-target": { "output": "generated/custom" } } + })"); + + const auto result = rls::project::LoadManifest(directory.path() / "rls.json"); + + ASSERT_TRUE(result.error.empty()); + ASSERT_TRUE(result.config.has_value()); + ASSERT_EQ(result.config->transpilerOutputs.size(), 1); + EXPECT_EQ(result.config->transpilerOutputs[0].first, "custom-target"); +} + +TEST(ProjectManifest, CollectsDeterministicSourcesWithConfiguredAndDefaultExclusions) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["."], + "exclude": ["ignored/**"], + "transpilers": { "soh": { "output": "generated/soh" } } + })"); + writeFile(directory.path() / "src" / "z.rls"); + writeFile(directory.path() / "src" / "a.rls"); + writeFile(directory.path() / "ignored" / "skip.rls"); + writeFile(directory.path() / "generated" / "soh" / "skip.rls"); + writeFile(directory.path() / "build" / "skip.rls"); + writeFile(directory.path() / ".git" / "skip.rls"); + + const auto manifest = rls::project::LoadManifest(directory.path() / "rls.json"); + ASSERT_TRUE(manifest.config.has_value()) << manifest.error; + const auto result = rls::project::CollectManifestSources(*manifest.config); + + ASSERT_TRUE(result.error.empty()); + ASSERT_EQ(result.sourceFiles.size(), 2); + EXPECT_EQ(result.sourceFiles[0], fs::weakly_canonical(directory.path() / "src" / "a.rls")); + EXPECT_EQ(result.sourceFiles[1], fs::weakly_canonical(directory.path() / "src" / "z.rls")); +} + +TEST(ProjectManifest, ReportsMissingAndEmptySourceSets) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ "version": 1, "sources": ["missing"] })"); + + auto manifest = rls::project::LoadManifest(directory.path() / "rls.json"); + ASSERT_TRUE(manifest.config.has_value()) << manifest.error; + EXPECT_EQ(rls::project::CollectManifestSources(*manifest.config).error, + "manifest source does not exist: " + + fs::weakly_canonical(directory.path() / "missing").string()); + + writeFile(directory.path() / "rls.json", R"({ "version": 1, "sources": ["empty"] })"); + fs::create_directories(directory.path() / "empty"); + manifest = rls::project::LoadManifest(directory.path() / "rls.json"); + ASSERT_TRUE(manifest.config.has_value()) << manifest.error; + EXPECT_EQ(rls::project::CollectManifestSources(*manifest.config).error, + "manifest does not resolve to any .rls source files"); +} + +TEST(ProjectManifest, IncludesOutputOnlyWhenExplicitlyListedAsASource) { + TemporaryDirectory directory; + writeFile(directory.path() / "rls.json", R"({ + "version": 1, + "sources": ["generated/soh"], + "transpilers": { "soh": { "output": "generated/soh" } } + })"); + writeFile(directory.path() / "generated" / "soh" / "included.rls"); + + const auto manifest = rls::project::LoadManifest(directory.path() / "rls.json"); + ASSERT_TRUE(manifest.config.has_value()) << manifest.error; + const auto result = rls::project::CollectManifestSources(*manifest.config); + + ASSERT_TRUE(result.error.empty()); + ASSERT_EQ(result.sourceFiles.size(), 1); + EXPECT_EQ(result.sourceFiles[0], + fs::weakly_canonical(directory.path() / "generated" / "soh" / "included.rls")); +} + +TEST(ProjectResolution, UsesNearestManifestOrStandaloneFile) { + TemporaryDirectory directory; + TemporaryDirectory standaloneDirectory; + writeFile(directory.path() / "rls.json", R"({ "version": 1, "sources": ["src"] })"); + writeFile(directory.path() / "src" / "outer.rls"); + writeFile(directory.path() / "nested" / "rls.json", R"({ "version": 1, "sources": ["logic.rls"] })"); + writeFile(directory.path() / "nested" / "logic.rls"); + writeFile(standaloneDirectory.path() / "standalone.rls"); + + const auto nested = rls::project::ResolveFileProject(directory.path() / "nested" / "logic.rls"); + ASSERT_TRUE(nested.error.empty()); + ASSERT_TRUE(nested.manifest.has_value()); + EXPECT_FALSE(nested.isStandalone); + EXPECT_EQ(nested.manifest->manifestPath, + fs::weakly_canonical(directory.path() / "nested" / "rls.json")); + + const auto standalone = rls::project::ResolveFileProject(standaloneDirectory.path() / "standalone.rls"); + ASSERT_TRUE(standalone.error.empty()); + EXPECT_FALSE(standalone.manifest.has_value()); + ASSERT_TRUE(standalone.isStandalone); + ASSERT_EQ(standalone.sourceFiles.size(), 1); + EXPECT_EQ(standalone.sourceFiles[0], + fs::weakly_canonical(standaloneDirectory.path() / "standalone.rls")); +} + +} // namespace \ No newline at end of file diff --git a/sema/CMakeLists.txt b/sema/CMakeLists.txt index 56036e9..7365400 100644 --- a/sema/CMakeLists.txt +++ b/sema/CMakeLists.txt @@ -5,7 +5,7 @@ file(GLOB sema_sources CONFIGURE_DEPENDS add_library(sema STATIC ${sema_sources}) target_include_directories(sema PUBLIC include) -target_link_libraries(sema PUBLIC ast) +target_link_libraries(sema PUBLIC ast parser rls_build_options) if(BUILD_TESTING) file(GLOB sema_test_sources CONFIGURE_DEPENDS diff --git a/sema/include/analysis_snapshot.h b/sema/include/analysis_snapshot.h new file mode 100644 index 0000000..43c1aba --- /dev/null +++ b/sema/include/analysis_snapshot.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "ast.h" +#include "semantic_index.h" +#include "source_index.h" + +namespace rls::sema { + +struct SourceInput { + std::string path; + std::string content; +}; + +/// Immutable result of analyzing one explicit source set. +class AnalysisSnapshot { +public: + static std::optional> Create( + std::vector sources, uint64_t generation = 0, + std::stop_token cancellation = {}); + + uint64_t generation() const { return generation_; } + size_t documentCount() const { return documents_.size(); } + std::vector documentPaths() const; + const SemanticIndex& semanticIndex() const { return semanticIndex_; } + const ast::SourceText* sourceText(std::string_view path) const; + const rls::parser::SourceIndex* sourceIndex(std::string_view path) const; + std::optional syntaxAt(std::string_view path, ast::Position position) const; + std::optional nameAt(std::string_view path, ast::Position position) const; + std::optional symbolAt(std::string_view path, ast::Position position) const; + std::optional occurrenceAt(std::string_view path, ast::Position position) const; + std::optional typeAt(std::string_view path, ast::Position position) const; + std::optional expectedTypeAt(std::string_view path, ast::Position position) const; + std::optional callAt(std::string_view path, ast::Position position) const; + std::optional declaration(SymbolId symbol) const; + std::vector references(SymbolId symbol) const; + std::vector visibleSymbolsAt(std::string_view path, ast::Position position) const; + std::vector diagnosticsFor(std::string_view path) const; + +private: + struct Document { + std::string path; + ast::SourceText sourceText; + rls::parser::SourceIndex sourceIndex; + }; + + uint64_t generation_ = 0; + std::vector documents_; + ast::Project project_; + std::vector diagnostics_; + SemanticIndex semanticIndex_; + std::vector compilerDiagnostics_; +}; + +} // namespace rls::sema \ No newline at end of file diff --git a/sema/include/diagnostics.h b/sema/include/diagnostics.h new file mode 100644 index 0000000..20a6998 --- /dev/null +++ b/sema/include/diagnostics.h @@ -0,0 +1,209 @@ +#pragma once + +#include +#include +#include +#include + +#include "ast.h" + +namespace rls::sema::diagnostics { + +inline ast::Diagnostic FunctionRequiresZeroArguments(ast::Span span, std::string_view function, size_t count) { + return {"RLS-T001", std::move(span), ast::DiagnosticLevel::Error, std::format("function '{}' requires {} argument(s); only zero-argument functions can be used as callable values", function, count)}; +} +inline ast::Diagnostic FunctionReferenceTypeUnavailable(ast::Span span, std::string_view function) { + return {"RLS-T002", std::move(span), ast::DiagnosticLevel::Error, std::format("function '{}' callable reference type is not available yet", function)}; +} +inline ast::Diagnostic FunctionCannotBeCondition(ast::Span span, std::string_view function, std::string_view type) { + return {"RLS-T003", std::move(span), ast::DiagnosticLevel::Error, std::format("function '{}' cannot be used as a Condition callable because it returns {}", function, type)}; +} +inline ast::Diagnostic FunctionCallableMissingReturnType(ast::Span span, std::string_view function) { + return {"RLS-T004", std::move(span), ast::DiagnosticLevel::Error, std::format("function '{}' cannot be used as callable value without a return type", function)}; +} +inline ast::Diagnostic AmbiguousIdentifier(ast::Span span, std::string_view name, std::string_view enums) { + return {"RLS-T005", std::move(span), ast::DiagnosticLevel::Error, std::format("ambiguous identifier '{}' found in multiple enums ({}); use EnumName.{} to disambiguate", name, enums, name)}; +} +inline ast::Diagnostic UnknownIdentifier(ast::Span span, std::string_view name) { + return {"RLS-T006", std::move(span), ast::DiagnosticLevel::Error, + std::format("unknown identifier '{}'", name), + ast::DiagnosticActionData{1, "rls.declareSymbol", {std::string(name)}}}; +} +inline ast::Diagnostic UnaryRequiresBool(ast::Span span, std::string_view type) { + return {"RLS-T007", std::move(span), ast::DiagnosticLevel::Error, std::format("'not' requires a Bool operand, got {}", type)}; +} +inline ast::Diagnostic LogicalRequiresBool(ast::Span span, std::string_view op, std::string_view side, std::string_view type) { + return {"RLS-T008", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' requires Bool operands, {} is {}", op, side, type)}; +} +inline ast::Diagnostic IncompatibleComparison(ast::Span span, std::string_view left, std::string_view right) { + return {"RLS-T009", std::move(span), ast::DiagnosticLevel::Error, std::format("comparison between incompatible types {} and {}", left, right)}; +} +inline ast::Diagnostic EnumComparisonMismatch(ast::Span span, std::string_view left, std::string_view right) { + return {"RLS-T010", std::move(span), ast::DiagnosticLevel::Error, std::format("comparison between enum '{}' and enum '{}'", left, right)}; +} +inline ast::Diagnostic ComparisonRequiresInt(ast::Span span, std::string_view side, std::string_view type) { + return {"RLS-T011", std::move(span), ast::DiagnosticLevel::Error, std::format("comparison requires Int operands, {} is {}", side, type)}; +} +inline ast::Diagnostic ArithmeticRequiresInt(ast::Span span, std::string_view side, std::string_view type) { + return {"RLS-T012", std::move(span), ast::DiagnosticLevel::Error, std::format("arithmetic requires Int operands, {} is {}", side, type)}; +} +inline ast::Diagnostic TernaryConditionType(ast::Span span, std::string_view type) { + return {"RLS-T013", std::move(span), ast::DiagnosticLevel::Error, std::format("ternary condition must be Bool, got {}", type)}; +} +inline ast::Diagnostic TernaryEnumMismatch(ast::Span span, std::string_view thenEnum, std::string_view elseEnum) { + return {"RLS-T014", std::move(span), ast::DiagnosticLevel::Error, std::format("ternary branches have different enum types: '{}' and '{}'", thenEnum, elseEnum)}; +} +inline ast::Diagnostic TernaryImplicitBool(ast::Span span, std::string_view thenType, std::string_view elseType) { + return {"RLS-T015", std::move(span), ast::DiagnosticLevel::Warning, std::format("ternary branches have types {} and {}, implicitly converted to Bool", thenType, elseType)}; +} +inline ast::Diagnostic TernaryBranchMismatch(ast::Span span, std::string_view thenType, std::string_view elseType) { + return {"RLS-T016", std::move(span), ast::DiagnosticLevel::Error, std::format("ternary branches have different types: {} and {}", thenType, elseType)}; +} +inline ast::Diagnostic UnknownNamedArgument(ast::Span span, std::string_view function, std::string_view name) { + return {"RLS-T017", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' unknown named argument '{}'", function, name)}; +} +inline ast::Diagnostic DuplicateArgument(ast::Span span, std::string_view function, std::string_view name) { + return {"RLS-T018", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' duplicate argument for parameter '{}'", function, name)}; +} +inline ast::Diagnostic ArgumentCountMismatch(ast::Span span, std::string_view function, std::string_view expected, size_t actual) { + return {"RLS-T019", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' expects {} argument(s), got {}", function, expected, actual)}; +} +inline ast::Diagnostic MissingRequiredArguments(ast::Span span, std::string_view function, std::string_view names) { + return {"RLS-T020", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' missing required argument(s): {}", function, names)}; +} +inline ast::Diagnostic AmbiguousEnumInteger(ast::Span span, std::string_view function, size_t argument, int value, std::string_view enums) { + return {"RLS-T021", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' argument {} uses ambiguous integer value {}; matching enums: {}; provide explicit enum context or EnumName.ValueName", function, argument, value, enums)}; +} +inline ast::Diagnostic EnumArgumentMismatch(ast::Span span, std::string_view function, size_t argument, std::string_view expected, std::string_view actual) { + return {"RLS-T022", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' argument {} expected enum '{}', got enum '{}'", function, argument, expected, actual)}; +} +inline ast::Diagnostic ArgumentTypeMismatch(ast::Span span, std::string_view function, size_t argument, std::string_view expected, std::string_view actual) { + return {"RLS-T023", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' argument {} expected {}, got {}", function, argument, expected, actual)}; +} +inline ast::Diagnostic ValueNotCallable(ast::Span span, std::string_view name, std::string_view type) { + return {"RLS-T024", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' is not callable (type {})", name, type)}; +} +inline ast::Diagnostic ZeroArgumentCallMismatch(ast::Span span, std::string_view name, size_t count) { + return {"RLS-T025", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' expects 0 argument(s), got {}", name, count)}; +} +inline ast::Diagnostic UnknownFunction(ast::Span span, std::string_view name) { + return {"RLS-T026", std::move(span), ast::DiagnosticLevel::Error, + std::format("unknown function '{}'", name), + ast::DiagnosticActionData{1, "rls.declareFunction", {std::string(name)}}}; +} +inline ast::Diagnostic ExpressionNotCallable(ast::Span span, std::string_view type) { + return {"RLS-T027", std::move(span), ast::DiagnosticLevel::Error, std::format("expression is not callable (type {})", type)}; +} +inline ast::Diagnostic UnknownEnumMember(ast::Span span, std::string_view member, std::string_view enumName) { + return {"RLS-T028", std::move(span), ast::DiagnosticLevel::Error, std::format("'{}' is not a member of enum '{}'", member, enumName)}; +} +inline ast::Diagnostic UnknownEnum(ast::Span span, std::string_view name) { + return {"RLS-T029", std::move(span), ast::DiagnosticLevel::Error, std::format("unknown enum '{}' in member access", name)}; +} +inline ast::Diagnostic HereOutsideRegion(ast::Span span) { + return {"RLS-T030", std::move(span), ast::DiagnosticLevel::Error, "'here' can only be used inside a region entry condition; it has type Region"}; +} +inline ast::Diagnostic MatchWildcardNotStandalone(ast::Span span) { + return {"RLS-T031", std::move(span), ast::DiagnosticLevel::Error, "match wildcard '_' must be a standalone pattern"}; +} +inline ast::Diagnostic MatchWildcardNotLast(ast::Span span) { + return {"RLS-T032", std::move(span), ast::DiagnosticLevel::Error, "match wildcard '_' arm must be last"}; +} +inline ast::Diagnostic MatchPatternTypeMismatch(ast::Span span, std::string_view pattern, std::string_view actual, std::string_view expected) { + return {"RLS-T033", std::move(span), ast::DiagnosticLevel::Error, std::format("match pattern '{}' is {} but expected {}", pattern, actual, expected)}; +} +inline ast::Diagnostic MatchPatternEnumMismatch(ast::Span span, std::string_view pattern, std::string_view actual, std::string_view expected) { + return {"RLS-T034", std::move(span), ast::DiagnosticLevel::Error, std::format("match pattern '{}' is enum '{}' but expected enum '{}'", pattern, actual, expected)}; +} +inline ast::Diagnostic MatchDiscriminantTypeMismatch(ast::Span span, std::string_view name, std::string_view actual, std::string_view expected) { + return {"RLS-T035", std::move(span), ast::DiagnosticLevel::Error, std::format("match discriminant '{}' is {} but patterns are {}", name, actual, expected)}; +} +inline ast::Diagnostic MatchDiscriminantEnumMismatch(ast::Span span, std::string_view name, std::string_view actual, std::string_view expected) { + return {"RLS-T036", std::move(span), ast::DiagnosticLevel::Error, std::format("match discriminant '{}' is enum '{}' but patterns are enum '{}'", name, actual, expected)}; +} +inline ast::Diagnostic MatchArmImplicitBool(ast::Span span, std::string_view actual, std::string_view previous) { + return {"RLS-T037", std::move(span), ast::DiagnosticLevel::Warning, std::format("match arm type {} implicitly converted to Bool (previous arms are {})", actual, previous)}; +} +inline ast::Diagnostic MatchArmTypeMismatch(ast::Span span, std::string_view actual, std::string_view previous) { + return {"RLS-T038", std::move(span), ast::DiagnosticLevel::Error, std::format("match arm type {} doesn't match previous arms ({})", actual, previous)}; +} +inline ast::Diagnostic DefineCycle(ast::Span span, std::string_view names) { + return {"RLS-T039", std::move(span), ast::DiagnosticLevel::Error, std::format("cycle in define call graph: {}", names)}; +} +inline ast::Diagnostic UnknownParameterTypeAnnotation(ast::Span span, std::string_view type, std::string_view parameter) { + return {"RLS-T040", std::move(span), ast::DiagnosticLevel::Error, std::format("unknown type annotation '{}' for parameter '{}'", type, parameter)}; +} + +inline ast::Diagnostic UnknownExtensionTarget(ast::Span span, std::string_view regionName) { + return {"RLS-V001", std::move(span), ast::DiagnosticLevel::Error, + std::format("extend region targets unknown region '{}'", regionName), + ast::DiagnosticActionData{1, "rls.createRegion", {std::string(regionName)}}}; +} +inline ast::Diagnostic DuplicateRegionData(ast::Span span, std::string_view key, std::string_view regionName) { + return {"RLS-V002", std::move(span), ast::DiagnosticLevel::Error, std::format("duplicate data key '{}' in region '{}'", key, regionName)}; +} +inline ast::Diagnostic DuplicateRegionEntry(ast::Span span, std::string_view kind, std::string_view name, std::string_view regionName) { + return {"RLS-V003", std::move(span), ast::DiagnosticLevel::Error, std::format("duplicate {} '{}' in region '{}'", kind, name, regionName)}; +} +inline ast::Diagnostic EntryConditionType(ast::Span span, std::string_view kind, std::string_view name, std::string_view regionName, std::string_view type) { + return {"RLS-V004", std::move(span), ast::DiagnosticLevel::Error, std::format("{} condition for '{}' in region '{}' must be Bool, got {}", kind, name, regionName, type)}; +} +inline ast::Diagnostic UnreachableRegion(ast::Span span, std::string_view regionName) { + return {"RLS-V005", std::move(span), ast::DiagnosticLevel::Warning, std::format("region '{}' is not reachable from 'RR_ROOT'", regionName)}; +} +inline ast::Diagnostic UnusedDefine(ast::Span span, std::string_view name) { + return {"RLS-V006", std::move(span), ast::DiagnosticLevel::Info, std::format("'{}' is defined but never used", name)}; +} +inline ast::Diagnostic DuplicateParameter(ast::Span span, std::string_view parameter, std::string_view kind, std::string_view name) { + return {"RLS-V007", std::move(span), ast::DiagnosticLevel::Error, std::format("duplicate parameter '{}' in {} '{}'", parameter, kind, name)}; +} +inline ast::Diagnostic RequiredAfterOptionalParameter(ast::Span span, std::string_view parameter, std::string_view kind, std::string_view name) { + return {"RLS-V008", std::move(span), ast::DiagnosticLevel::Error, std::format("required parameter '{}' cannot follow optional parameters in {} '{}'", parameter, kind, name)}; +} +inline ast::Diagnostic ExternParameterMissingType(ast::Span span, std::string_view function, std::string_view parameter) { + return {"RLS-V009", std::move(span), ast::DiagnosticLevel::Error, std::format("extern define '{}' parameter '{}' must have a type annotation or a default value", function, parameter)}; +} +inline ast::Diagnostic ExternParameterCannotInfer(ast::Span span, std::string_view function, std::string_view parameter) { + return {"RLS-V010", std::move(span), ast::DiagnosticLevel::Error, std::format("extern define '{}' parameter '{}' needs an explicit type or an inferrable default", function, parameter)}; +} +inline ast::Diagnostic DefaultValueTypeMismatch(ast::Span span, std::string_view parameter, std::string_view kind, std::string_view name, std::string_view actual, std::string_view expected) { + return {"RLS-V011", std::move(span), ast::DiagnosticLevel::Error, std::format("default value for parameter '{}' in {} '{}' has type {}, expected {}", parameter, kind, name, actual, expected)}; +} +inline ast::Diagnostic ExternMissingReturnType(ast::Span span, std::string_view function) { + return {"RLS-V012", std::move(span), ast::DiagnosticLevel::Error, std::format("extern define '{}' must declare a return type", function)}; +} +inline ast::Diagnostic ExternUnknownReturnType(ast::Span span, std::string_view type, std::string_view function) { + return {"RLS-V013", std::move(span), ast::DiagnosticLevel::Error, std::format("unknown return type annotation '{}' for extern define '{}'", type, function)}; +} +inline ast::Diagnostic EnumWildcardPattern(ast::Span span, std::string_view name, std::string_view pattern) { + return {"RLS-V014", std::move(span), ast::DiagnosticLevel::Error, std::format("enum '{}' cannot contain wildcard pattern '{}'", name, pattern)}; +} +inline ast::Diagnostic EnumDuplicateMember(ast::Span span, std::string_view member, std::string_view name) { + return {"RLS-V015", std::move(span), ast::DiagnosticLevel::Error, std::format("duplicate enum member '{}' in enum '{}'", member, name)}; +} +inline ast::Diagnostic ExternEnumDuplicateMember(ast::Span span, std::string_view member, std::string_view name) { + return {"RLS-V015", std::move(span), ast::DiagnosticLevel::Error, std::format("duplicate enum member '{}' in extern enum '{}'", member, name)}; +} +inline ast::Diagnostic EnumDuplicateValue(ast::Span span, int value, std::string_view name) { + return {"RLS-V016", std::move(span), ast::DiagnosticLevel::Error, std::format("duplicate enum value {} in enum '{}'", value, name)}; +} +inline ast::Diagnostic ExternEnumEmpty(ast::Span span, std::string_view name) { + return {"RLS-V017", std::move(span), ast::DiagnosticLevel::Error, std::format("extern enum '{}' must declare at least one member or wildcard pattern", name)}; +} +inline ast::Diagnostic ExternEnumWildcardOverlap(ast::Span span, std::string_view name, std::string_view pattern, std::string_view member) { + return {"RLS-V018", std::move(span), ast::DiagnosticLevel::Warning, std::format("extern enum '{}' wildcard '{}' overlaps explicit member '{}'", name, pattern, member)}; +} +inline ast::Diagnostic EnumValueNameCollision(ast::Span span, std::string_view value, std::string_view enumNames) { + return {"RLS-V019", std::move(span), ast::DiagnosticLevel::Warning, std::format("enum value '{}' appears in multiple enums ({}) and may require dotted disambiguation", value, enumNames)}; +} +inline ast::Diagnostic ExitTargetMissingRegion(ast::Span span, std::string_view regionName) { + return {"RLS-V020", std::move(span), ast::DiagnosticLevel::Warning, + std::format("exit targets region '{}' without a region declaration", regionName), + ast::DiagnosticActionData{1, "rls.createRegion", {std::string(regionName)}}}; +} + +inline bool IsDuplicateRegionData(const ast::Diagnostic& diagnostic) { + return diagnostic.code == "RLS-V002"; +} + +} // namespace rls::sema::diagnostics diff --git a/sema/include/sema.h b/sema/include/sema.h index 3163219..459923a 100644 --- a/sema/include/sema.h +++ b/sema/include/sema.h @@ -3,6 +3,8 @@ #include #include "ast.h" +#include "analysis_snapshot.h" +#include "semantic_index.h" namespace rls::sema { diff --git a/sema/include/semantic_index.h b/sema/include/semantic_index.h new file mode 100644 index 0000000..030d274 --- /dev/null +++ b/sema/include/semantic_index.h @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include +#include + +#include "ast.h" + +namespace rls::sema { + +class SymbolId { +public: + SymbolId() = default; + bool operator==(const SymbolId&) const = default; + +private: + uint64_t value_ = 0; + + explicit SymbolId(uint64_t value) : value_(value) {} + friend class SemanticIndex; +}; + +enum class SymbolCategory { + Region, + RegionExtension, + Define, + ExternDefine, + Enum, + EnumMember, + ExternEnumPattern, + Parameter, + RegionDataEntry, + SectionEntry, +}; + +enum class SymbolProvenance { + Source, + Extern, + Pattern, +}; + +enum class OccurrenceKind { + Declaration, + Reference, + Call, + TypeReference, + MemberAccess, + ExtensionTarget, + ExitTarget, + Unresolved, +}; + +struct SymbolRecord { + SymbolId id; + SymbolCategory category; + SymbolProvenance provenance; + std::string displayName; + ast::Span declaration; + ast::Span selection; + std::optional signature; + std::optional type; + std::optional enumName; + std::optional container; + std::optional defaultValue; + bool optional = false; +}; + +struct OccurrenceRecord { + std::optional symbol; + ast::Span span; + OccurrenceKind kind; +}; + +struct TypeRecord { + ast::Span span; + ast::Type type; + std::optional enumName; +}; + +struct ExpectedTypeRecord { + ast::Span span; + ast::Type type; + std::optional enumName; +}; + +struct ObservedEnumValue { + std::string displayName; + std::string enumName; +}; + +struct CallRecord { + ast::Span span; + std::optional target; + std::vector argumentRanges; + std::vector> normalizedBindings; +}; + +struct DiagnosticRelatedLocation { + std::string message; + ast::Span span; +}; + +struct CompilerDiagnostic { + std::string code; + ast::DiagnosticLevel level; + std::string message; + ast::Span span; + std::vector related; + std::optional data; +}; + +/// Snapshot-local semantic records that retain no AST pointers. +class SemanticIndex { +public: + const std::vector& symbols() const { return symbols_; } + const std::vector& occurrences() const { return occurrences_; } + const std::vector& types() const { return types_; } + const std::vector& expectedTypes() const { return expectedTypes_; } + const std::vector& observedEnumValues() const { return observedEnumValues_; } + const std::vector& calls() const { return calls_; } + const std::vector& diagnostics() const { return diagnostics_; } + std::optional declaration(SymbolId id) const; + std::vector occurrencesFor(SymbolId id) const; + std::optional occurrenceAt(std::string_view file, ast::Position position) const; + std::optional typeAt(std::string_view file, ast::Position position) const; + std::optional expectedTypeAt(std::string_view file, ast::Position position) const; + std::optional callAt(std::string_view file, ast::Position position) const; + std::vector visibleSymbolsAt(std::string_view file, ast::Position position) const; + bool patternMatches(SymbolId id, std::string_view value) const; + +private: + std::vector symbols_; + std::vector occurrences_; + std::vector types_; + std::vector expectedTypes_; + std::vector observedEnumValues_; + std::vector calls_; + std::vector diagnostics_; + + SymbolId addSymbol(SymbolCategory category, SymbolProvenance provenance, + std::string displayName, ast::Span declaration, ast::Span selection, + std::optional container = std::nullopt, + std::optional signature = std::nullopt, + std::optional type = std::nullopt, + std::optional enumName = std::nullopt, + std::optional defaultValue = std::nullopt, + bool optional = false); + + friend SemanticIndex buildSemanticIndex(const ast::Project& project, + const std::vector& diagnostics); +}; + +SemanticIndex buildSemanticIndex(const ast::Project& project, + const std::vector& diagnostics = {}); + +} // namespace rls::sema \ No newline at end of file diff --git a/sema/src/analysis_snapshot.cpp b/sema/src/analysis_snapshot.cpp new file mode 100644 index 0000000..ab30092 --- /dev/null +++ b/sema/src/analysis_snapshot.cpp @@ -0,0 +1,223 @@ +#include "analysis_snapshot.h" + +#include "parser.h" +#include "sema.h" + +#include +#include +#include + +namespace rls::sema { + +namespace { + +std::vector parametersFor( + const SemanticIndex& index, SymbolId callable) { + std::vector result; + for (const auto& symbol : index.symbols()) { + if (symbol.category == SymbolCategory::Parameter + && symbol.container == callable) { + result.push_back(&symbol); + } + } + std::sort(result.begin(), result.end(), [](const auto* left, const auto* right) { + return std::tie(left->selection.start.line, left->selection.start.column) + < std::tie(right->selection.start.line, right->selection.start.column); + }); + return result; +} + +std::optional uniqueCallable( + const SemanticIndex& index, std::string_view name) { + std::optional result; + for (const auto& symbol : index.symbols()) { + const bool callable = symbol.category == SymbolCategory::Define + || symbol.category == SymbolCategory::ExternDefine; + if (!callable || symbol.displayName != name) continue; + if (result) return std::nullopt; + result = symbol.id; + } + return result; +} + +std::optional resolveRecoveredCall( + const SemanticIndex& semanticIndex, + const parser::CallContext& call) { + if (call.argumentRanges.size() != call.argumentLabels.size() + || call.argumentRanges.size() != call.argumentLabelNames.size()) { + return std::nullopt; + } + const auto target = uniqueCallable(semanticIndex, call.calleeName); + if (!target) return std::nullopt; + const auto parameters = parametersFor(semanticIndex, *target); + std::vector bound(parameters.size(), false); + std::vector> bindings; + bindings.reserve(call.argumentRanges.size()); + size_t nextPositional = 0; + + for (const auto& label : call.argumentLabelNames) { + size_t parameterIndex = parameters.size(); + if (label) { + for (size_t index = 0; index < parameters.size(); ++index) { + if (parameters[index]->displayName == *label) { + parameterIndex = index; + break; + } + } + if (parameterIndex == parameters.size() || bound[parameterIndex]) { + return std::nullopt; + } + } else { + while (nextPositional < bound.size() && bound[nextPositional]) { + ++nextPositional; + } + if (nextPositional == parameters.size()) return std::nullopt; + parameterIndex = nextPositional++; + } + bound[parameterIndex] = true; + bindings.push_back(parameterIndex); + } + + return CallRecord{ + call.span, *target, call.argumentRanges, std::move(bindings)}; +} + +} // namespace + +std::optional> AnalysisSnapshot::Create( + std::vector sources, uint64_t generation, std::stop_token cancellation) { + if (cancellation.stop_requested()) return std::nullopt; + auto snapshot = std::make_shared(); + snapshot->generation_ = generation; + std::map effectiveSources; + for (auto& source : sources) effectiveSources[std::move(source.path)] = std::move(source.content); + + for (auto& [path, content] : effectiveSources) { + if (cancellation.stop_requested()) return std::nullopt; + const auto sourceText = ast::SourceText::FromUtf8(content); + if (!sourceText) return std::nullopt; + auto parsed = rls::parser::ParseStringWithIndex( + content, path, rls::parser::ParseMode::Editor); + if (cancellation.stop_requested()) return std::nullopt; + snapshot->documents_.push_back({path, *sourceText, std::move(parsed.sourceIndex)}); + snapshot->project_.files.push_back(std::move(parsed.file)); + } + + if (cancellation.stop_requested()) return std::nullopt; + snapshot->diagnostics_ = analyze(snapshot->project_); + if (cancellation.stop_requested()) return std::nullopt; + for (const auto& file : snapshot->project_.files) { + for (const auto& diagnostic : file.diagnostics) { + snapshot->diagnostics_.push_back(diagnostic); + } + } + if (cancellation.stop_requested()) return std::nullopt; + snapshot->semanticIndex_ = buildSemanticIndex(snapshot->project_, snapshot->diagnostics_); + if (cancellation.stop_requested()) return std::nullopt; + for (const auto& diagnostic : snapshot->diagnostics_) { + if (diagnostic.code.starts_with("RLS-V")) continue; + snapshot->compilerDiagnostics_.push_back({diagnostic.code, diagnostic.level, + diagnostic.message, diagnostic.span, {}, diagnostic.data}); + } + for (const auto& diagnostic : snapshot->semanticIndex_.diagnostics()) { + snapshot->compilerDiagnostics_.push_back(diagnostic); + } + return std::shared_ptr(std::move(snapshot)); +} + +std::vector AnalysisSnapshot::documentPaths() const { + std::vector paths; + paths.reserve(documents_.size()); + for (const auto& document : documents_) paths.push_back(document.path); + return paths; +} + +const ast::SourceText* AnalysisSnapshot::sourceText(std::string_view path) const { + const auto it = std::find_if(documents_.begin(), documents_.end(), [&](const Document& document) { + return document.path == path; + }); + return it == documents_.end() ? nullptr : &it->sourceText; +} + +const rls::parser::SourceIndex* AnalysisSnapshot::sourceIndex(std::string_view path) const { + const auto it = std::find_if(documents_.begin(), documents_.end(), [&](const Document& document) { + return document.path == path; + }); + return it == documents_.end() ? nullptr : &it->sourceIndex; +} + +std::optional AnalysisSnapshot::syntaxAt(std::string_view path, ast::Position position) const { + const auto* index = sourceIndex(path); + return index ? index->syntaxAt(position) : std::nullopt; +} + +std::optional AnalysisSnapshot::nameAt(std::string_view path, ast::Position position) const { + const auto* index = sourceIndex(path); + return index ? index->nameAt(position) : std::nullopt; +} + +std::optional AnalysisSnapshot::occurrenceAt(std::string_view path, ast::Position position) const { + return semanticIndex_.occurrenceAt(path, position); +} + +std::optional AnalysisSnapshot::symbolAt(std::string_view path, ast::Position position) const { + const auto occurrence = occurrenceAt(path, position); + return occurrence ? occurrence->symbol : std::nullopt; +} + +std::optional AnalysisSnapshot::typeAt(std::string_view path, ast::Position position) const { + return semanticIndex_.typeAt(path, position); +} + +std::optional AnalysisSnapshot::expectedTypeAt(std::string_view path, ast::Position position) const { + if (const auto expected = semanticIndex_.expectedTypeAt(path, position)) { + return expected; + } + const auto* index = sourceIndex(path); + if (!index) return std::nullopt; + const auto argument = index->callArgumentAt(position); + const auto call = callAt(path, position); + if (!argument || !call || !call->target + || argument->activeArgument >= call->normalizedBindings.size()) { + return std::nullopt; + } + const auto binding = call->normalizedBindings[argument->activeArgument]; + if (!binding) return std::nullopt; + const auto parameters = parametersFor(semanticIndex_, *call->target); + if (*binding >= parameters.size() || !parameters[*binding]->type) { + return std::nullopt; + } + return ExpectedTypeRecord{ + argument->valueSpan, *parameters[*binding]->type, + parameters[*binding]->enumName}; +} + +std::optional AnalysisSnapshot::callAt(std::string_view path, ast::Position position) const { + if (const auto call = semanticIndex_.callAt(path, position)) return call; + const auto* index = sourceIndex(path); + if (!index) return std::nullopt; + const auto call = index->enclosingCall(position); + return call ? resolveRecoveredCall(semanticIndex_, *call) : std::nullopt; +} + +std::optional AnalysisSnapshot::declaration(SymbolId symbol) const { + return semanticIndex_.declaration(symbol); +} + +std::vector AnalysisSnapshot::references(SymbolId symbol) const { + return semanticIndex_.occurrencesFor(symbol); +} + +std::vector AnalysisSnapshot::visibleSymbolsAt(std::string_view path, ast::Position position) const { + return semanticIndex_.visibleSymbolsAt(path, position); +} + +std::vector AnalysisSnapshot::diagnosticsFor(std::string_view path) const { + std::vector result; + for (const auto& diagnostic : compilerDiagnostics_) { + if (diagnostic.span.file == path) result.push_back(diagnostic); + } + return result; +} + +} // namespace rls::sema \ No newline at end of file diff --git a/sema/src/collect_declarations.cpp b/sema/src/collect_declarations.cpp index 1fa8b85..e75d508 100644 --- a/sema/src/collect_declarations.cpp +++ b/sema/src/collect_declarations.cpp @@ -10,17 +10,17 @@ std::vector collectDeclarations(ast::Project& project) { auto emitDuplicate = [&](std::string_view kind, std::string_view name, const ast::Span& first, const ast::Span& duplicate) { - diagnostics.push_back({ - ast::DiagnosticLevel::Error, + diagnostics.push_back(ast::Diagnostic{ + "", duplicate, ast::DiagnosticLevel::Error, std::format("duplicate {} '{}' (first declared at {}:{})", - kind, name, first.file, first.start.line), - duplicate - }); + kind, name, first.file, first.start.line)}); }; // Clear any previous state so the function is idempotent. project.RegionDecls.clear(); project.ExtendRegionDecls.clear(); + project.EventDecls.clear(); + project.LocationDecls.clear(); project.DefineDecls.clear(); project.ExternDefineDecls.clear(); project.EnumInfos.clear(); @@ -125,6 +125,32 @@ std::vector collectDeclarations(ast::Project& project) { } } + auto collectEntries = [&](const std::vector& sections) { + for (const auto& section : sections) { + auto* declarations = section.kind == ast::SectionKind::Events + ? &project.EventDecls + : section.kind == ast::SectionKind::Locations + ? &project.LocationDecls + : nullptr; + if (declarations == nullptr) continue; + for (const auto& entry : section.entries) { + (*declarations)[entry.name.text].push_back(&entry); + } + } + }; + for (const auto& file : project.files) { + for (const auto& decl : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + collectEntries(node.body.sections); + } else if constexpr (std::is_same_v) { + collectEntries(node.sections); + } + }, decl); + } + } + for (auto& [regionName, decls] : project.ExtendRegionDecls) { std::sort(decls.begin(), decls.end(), [](const ast::ExtendRegionDecl* a, const ast::ExtendRegionDecl* b) { diff --git a/sema/src/resolve_types.cpp b/sema/src/resolve_types.cpp index 9032f49..d3ffa0a 100644 --- a/sema/src/resolve_types.cpp +++ b/sema/src/resolve_types.cpp @@ -1,4 +1,5 @@ #include "resolve_types.h" +#include "diagnostics.h" #include "type_helpers.h" #include @@ -207,33 +208,19 @@ struct ExprResolver { const auto* def = defIt->second; // TODO: Zero-Argument Constraint — functions with parameters cannot be callable. if (!def->params.empty()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("function '{}' requires {} argument(s); only zero-argument functions can be used as callable values", - node.name.text, def->params.size()), - expr.span - }); + diags.push_back(diagnostics::FunctionRequiresZeroArguments(expr.span, node.name.text, def->params.size())); return ast::Type::Error; } auto bodyType = project.getType(def->body.get()); if (!bodyType.has_value()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("function '{}' callable reference type is not available yet", node.name.text), - expr.span - }); + diags.push_back(diagnostics::FunctionReferenceTypeUnavailable(expr.span, node.name.text)); return ast::Type::Error; } // TODO: Bool-Return-Type Constraint — only () -> Bool functions become Condition. if (*bodyType != ast::Type::Bool) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("function '{}' cannot be used as a Condition callable because it returns {}", - node.name.text, typeName(*bodyType)), - expr.span - }); + diags.push_back(diagnostics::FunctionCannotBeCondition(expr.span, node.name.text, typeName(*bodyType))); return ast::Type::Error; } @@ -245,33 +232,19 @@ struct ExprResolver { extIt != project.ExternDefineDecls.end()) { const auto* ext = extIt->second; if (!ext->params.empty()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("function '{}' requires {} argument(s); only zero-argument functions can be used as callable values", - node.name.text, ext->params.size()), - expr.span - }); + diags.push_back(diagnostics::FunctionRequiresZeroArguments(expr.span, node.name.text, ext->params.size())); return ast::Type::Error; } if (!ext->returnType) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("function '{}' cannot be used as callable value without a return type", node.name.text), - expr.span - }); + diags.push_back(diagnostics::FunctionCallableMissingReturnType(expr.span, node.name.text)); return ast::Type::Error; } auto returnType = resolveTypeAnnotation(project, ext->returnType->name.text); if (!returnType.has_value() || returnType->type != ast::Type::Bool) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("function '{}' cannot be used as a Condition callable because it returns {}", - node.name.text, - returnType.has_value() ? typeName(returnType->type) : std::string_view{""}), - expr.span - }); + diags.push_back(diagnostics::FunctionCannotBeCondition(expr.span, node.name.text, + returnType.has_value() ? typeName(returnType->type) : std::string_view{""})); return ast::Type::Error; } @@ -279,6 +252,31 @@ struct ExprResolver { return ast::Type::Condition; } + std::vector> declaredTypes; + if (project.RegionDecls.contains(node.name.text)) { + declaredTypes.emplace_back("Region", T::Region); + } + if (project.EventDecls.contains(node.name.text)) { + declaredTypes.emplace_back("Event", T::Event); + } + if (project.LocationDecls.contains(node.name.text)) { + declaredTypes.emplace_back("Location", T::Location); + } + if (declaredTypes.size() > 1) { + std::string categories(declaredTypes.front().first); + for (size_t index = 1; index < declaredTypes.size(); ++index) { + categories += ", "; + categories += declaredTypes[index].first; + } + diags.push_back(diagnostics::AmbiguousIdentifier( + expr.span, node.name.text, categories)); + return T::Error; + } + if (declaredTypes.size() == 1) { + node.kind = ast::IdentifierKind::DeclaredValue; + return declaredTypes.front().second; + } + // Resolve identifiers exclusively through declared enum metadata. auto lookup = lookupIdentifierInEnums(node.name.text, project); @@ -289,12 +287,7 @@ struct ExprResolver { if (i > 0) enumList += ", "; enumList += lookup.ambiguousEnums[i]; } - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("ambiguous identifier '{}' found in multiple enums ({}); use EnumName.{} to disambiguate", - node.name.text, enumList, node.name.text), - expr.span - }); + diags.push_back(diagnostics::AmbiguousIdentifier(expr.span, node.name.text, enumList)); return ast::Type::Error; } @@ -305,11 +298,7 @@ struct ExprResolver { return *lookup.type; } - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("unknown identifier '{}'", node.name.text), - expr.span - }); + diags.push_back(diagnostics::UnknownIdentifier(expr.span, node.name.text)); return ast::Type::Error; } @@ -317,12 +306,7 @@ struct ExprResolver { inferUntypedParamIdentifier(*node.operand, ast::Type::Bool); auto opType = resolveExpr(*node.operand); if (opType != ast::Type::Error && !isBoolCompatible(opType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'not' requires a Bool operand, got {}", - typeName(opType)), - expr.span - }); + diags.push_back(diagnostics::UnaryRequiresBool(expr.span, typeName(opType))); } return ast::Type::Bool; } @@ -374,20 +358,10 @@ struct ExprResolver { case ast::BinaryOp::Or: { auto opName = node.op == ast::BinaryOp::And ? "and" : "or"; if (leftType != T::Error && !isBoolCompatible(leftType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' requires Bool operands, left is {}", - opName, typeName(leftType)), - node.left->span - }); + diags.push_back(diagnostics::LogicalRequiresBool(node.left->span, opName, "left", typeName(leftType))); } if (rightType != T::Error && !isBoolCompatible(rightType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' requires Bool operands, right is {}", - opName, typeName(rightType)), - node.right->span - }); + diags.push_back(diagnostics::LogicalRequiresBool(node.right->span, opName, "right", typeName(rightType))); } return T::Bool; } @@ -395,30 +369,25 @@ struct ExprResolver { // Equality: both sides must be the same type. case ast::BinaryOp::Eq: case ast::BinaryOp::NotEq: + { + const auto leftEnum = leftType == T::Enum + ? project.getEnumType(node.left.get()) : std::optional{}; + const auto rightEnum = rightType == T::Enum + ? project.getEnumType(node.right.get()) : std::optional{}; if (leftType != T::Error && rightType != T::Error && leftType != rightType && !(leftType == T::Int && isEnumLikeType(rightType)) - && !(rightType == T::Int && isEnumLikeType(leftType))) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("comparison between incompatible types {} and {}", - typeName(leftType), typeName(rightType)), - expr.span - }); + && !(rightType == T::Int && isEnumLikeType(leftType)) + && !areDomainAndEnumCompatible(leftType, leftEnum, rightType, rightEnum)) { + diags.push_back(diagnostics::IncompatibleComparison(expr.span, typeName(leftType), typeName(rightType))); } if (leftType == T::Enum && rightType == T::Enum) { - auto leftEnum = project.getEnumType(node.left.get()); - auto rightEnum = project.getEnumType(node.right.get()); if (leftEnum.has_value() && rightEnum.has_value() && *leftEnum != *rightEnum) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("comparison between enum '{}' and enum '{}'", - *leftEnum, *rightEnum), - expr.span - }); + diags.push_back(diagnostics::EnumComparisonMismatch(expr.span, *leftEnum, *rightEnum)); } } return T::Bool; + } // Ordering: both sides must be Int. case ast::BinaryOp::Lt: @@ -426,20 +395,10 @@ struct ExprResolver { case ast::BinaryOp::Gt: case ast::BinaryOp::GtEq: if (leftType != T::Error && !isIntCompatibleType(leftType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("comparison requires Int operands, left is {}", - typeName(leftType)), - node.left->span - }); + diags.push_back(diagnostics::ComparisonRequiresInt(node.left->span, "left", typeName(leftType))); } if (rightType != T::Error && !isIntCompatibleType(rightType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("comparison requires Int operands, right is {}", - typeName(rightType)), - node.right->span - }); + diags.push_back(diagnostics::ComparisonRequiresInt(node.right->span, "right", typeName(rightType))); } return T::Bool; @@ -449,20 +408,10 @@ struct ExprResolver { case ast::BinaryOp::Mul: case ast::BinaryOp::Div: if (leftType != T::Error && !isIntCompatibleType(leftType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("arithmetic requires Int operands, left is {}", - typeName(leftType)), - node.left->span - }); + diags.push_back(diagnostics::ArithmeticRequiresInt(node.left->span, "left", typeName(leftType))); } if (rightType != T::Error && !isIntCompatibleType(rightType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("arithmetic requires Int operands, right is {}", - typeName(rightType)), - node.right->span - }); + diags.push_back(diagnostics::ArithmeticRequiresInt(node.right->span, "right", typeName(rightType))); } return T::Int; } @@ -478,12 +427,7 @@ struct ExprResolver { auto elseType = resolveExpr(*node.elseBranch); if (condType != T::Error && !isBoolCompatible(condType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("ternary condition must be Bool, got {}", - typeName(condType)), - node.condition->span - }); + diags.push_back(diagnostics::TernaryConditionType(node.condition->span, typeName(condType))); } // Determine result type from branches. @@ -495,12 +439,7 @@ struct ExprResolver { auto thenEnum = project.getEnumType(node.thenBranch.get()); auto elseEnum = project.getEnumType(node.elseBranch.get()); if (thenEnum.has_value() && elseEnum.has_value() && *thenEnum != *elseEnum) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("ternary branches have different enum types: '{}' and '{}'", - *thenEnum, *elseEnum), - expr.span - }); + diags.push_back(diagnostics::TernaryEnumMismatch(expr.span, *thenEnum, *elseEnum)); return T::Error; } if (thenEnum.has_value()) { @@ -512,21 +451,11 @@ struct ExprResolver { // Both bool-compatible but different (e.g. Int + Bool) → unify to Bool. if (isBoolCompatible(thenType) && isBoolCompatible(elseType)) { - diags.push_back({ - ast::DiagnosticLevel::Warning, - std::format("ternary branches have types {} and {}, implicitly converted to Bool", - typeName(thenType), typeName(elseType)), - expr.span - }); + diags.push_back(diagnostics::TernaryImplicitBool(expr.span, typeName(thenType), typeName(elseType))); return T::Bool; } - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("ternary branches have different types: {} and {}", - typeName(thenType), typeName(elseType)), - expr.span - }); + diags.push_back(diagnostics::TernaryBranchMismatch(expr.span, typeName(thenType), typeName(elseType))); return T::Error; } @@ -572,24 +501,14 @@ struct ExprResolver { auto it = paramIndexByName.find(arg.name->text); if (it == paramIndexByName.end()) { result.hasError = true; - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' unknown named argument '{}'", - function, arg.name->text), - arg.value->span - }); + diags.push_back(diagnostics::UnknownNamedArgument(arg.value->span, function, arg.name->text)); continue; } size_t paramIndex = it->second; if (result.paramBound[paramIndex]) { result.hasError = true; - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' duplicate argument for parameter '{}'", - function, arg.name->text), - arg.value->span - }); + diags.push_back(diagnostics::DuplicateArgument(arg.value->span, function, arg.name->text)); continue; } @@ -625,12 +544,7 @@ struct ExprResolver { auto count = required == nParams ? std::format("{}", required) : std::format("{}-{}", required, nParams); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' expects {} argument(s), got {}", - function, count, nArgs), - expr.span - }); + diags.push_back(diagnostics::ArgumentCountMismatch(expr.span, function, count, nArgs)); result.hasError = true; } @@ -638,12 +552,7 @@ struct ExprResolver { auto count = required == nParams ? std::format("{}", required) : std::format("{}-{}", required, nParams); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' expects {} argument(s), got {}", - function, count, nArgs), - expr.span - }); + diags.push_back(diagnostics::ArgumentCountMismatch(expr.span, function, count, nArgs)); result.hasError = true; } @@ -661,12 +570,7 @@ struct ExprResolver { missing += ", "; missing += missingRequired[i]; } - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' missing required argument(s): {}", - function, missing), - expr.span - }); + diags.push_back(diagnostics::MissingRequiredArguments(expr.span, function, missing)); result.hasError = true; } } @@ -718,6 +622,13 @@ struct ExprResolver { } if (argTypes[argIndex] == T::Error) continue; + auto actualEnum = argTypes[argIndex] == T::Enum + ? project.getEnumType(node.args[argIndex].value.get()) + : std::optional{}; + if (areDomainAndEnumCompatible( + *paramType, expectedEnum, argTypes[argIndex], actualEnum)) { + continue; + } // For Enum-typed parameters with known identity, require the same enum. if (*paramType == T::Enum) { @@ -736,12 +647,8 @@ struct ExprResolver { enumList += ", "; enumList += candidateEnums[i]; } - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' argument {} uses ambiguous integer value {}; matching enums: {}; provide explicit enum context or EnumName.ValueName", - function, argIndex + 1, intLiteral->value, enumList), - node.args[argIndex].value->span - }); + diags.push_back(diagnostics::AmbiguousEnumInteger( + node.args[argIndex].value->span, function, argIndex + 1, intLiteral->value, enumList)); continue; } } @@ -751,17 +658,10 @@ struct ExprResolver { } if (expectedEnum.has_value() && argTypes[argIndex] == T::Enum) { - auto actualEnum = project.getEnumType(node.args[argIndex].value.get()); if (!actualEnum.has_value() || *actualEnum != *expectedEnum) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' argument {} expected enum '{}', got enum '{}'", - function, - argIndex + 1, - *expectedEnum, - actualEnum.has_value() ? *actualEnum : std::string_view{""}), - node.args[argIndex].value->span - }); + diags.push_back(diagnostics::EnumArgumentMismatch( + node.args[argIndex].value->span, function, argIndex + 1, *expectedEnum, + actualEnum.has_value() ? *actualEnum : std::string_view{""})); continue; } } @@ -772,12 +672,8 @@ struct ExprResolver { ? std::format("enum '{}'", *expectedEnum) : std::string(typeName(*paramType)); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' argument {} expected {}, got {}", - function, argIndex + 1, expectedName, typeName(argTypes[argIndex])), - node.args[argIndex].value->span - }); + diags.push_back(diagnostics::ArgumentTypeMismatch( + node.args[argIndex].value->span, function, argIndex + 1, expectedName, typeName(argTypes[argIndex]))); } } @@ -841,22 +737,12 @@ struct ExprResolver { auto calleeType = *scopeIt->second; if (!isCallableType(calleeType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' is not callable (type {})", - node.callee.text, typeName(calleeType)), - expr.span - }); + diags.push_back(diagnostics::ValueNotCallable(expr.span, node.callee.text, typeName(calleeType))); return T::Error; } if (!node.args.empty()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' expects 0 argument(s), got {}", - node.callee.text, node.args.size()), - expr.span - }); + diags.push_back(diagnostics::ZeroArgumentCallMismatch(expr.span, node.callee.text, node.args.size())); return T::Error; } @@ -952,11 +838,7 @@ struct ExprResolver { } // Unknown function. - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("unknown function '{}'", node.callee.text), - expr.span - }); + diags.push_back(diagnostics::UnknownFunction(expr.span, node.callee.text)); return T::Error; } @@ -967,11 +849,7 @@ struct ExprResolver { } if (calleeType != T::Callable && calleeType != T::Condition) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("expression is not callable (type {})", typeName(calleeType)), - expr.span - }); + diags.push_back(diagnostics::ExpressionNotCallable(expr.span, typeName(calleeType))); return T::Error; } @@ -986,11 +864,7 @@ struct ExprResolver { ast::Type resolve(const ast::MemberExpr& node, const ast::Expr& expr) { if (const auto* enumInfo = project.getEnumInfo(node.object.text); enumInfo != nullptr) { if (!enumContainsValueName(*enumInfo, node.member.text)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("'{}' is not a member of enum '{}'", node.member.text, node.object.text), - expr.span - }); + diags.push_back(diagnostics::UnknownEnumMember(expr.span, node.member.text, node.object.text)); return T::Error; } @@ -998,26 +872,17 @@ struct ExprResolver { return T::Enum; } - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("unknown enum '{}' in member access", node.object.text), - expr.span - }); + diags.push_back(diagnostics::UnknownEnum(expr.span, node.object.text)); return T::Error; } ast::Type resolve(ast::HereRef& node, ast::Expr& expr) { if (!currentRegion.has_value()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - "'here' can only be used inside a region entry condition; it resolves to enum 'Region'", - expr.span - }); + diags.push_back(diagnostics::HereOutsideRegion(expr.span)); return T::Error; } node.resolvedRegion = *currentRegion; - project.setEnumType(&expr, "Region"); - return T::Enum; + return T::Region; } ast::Type resolve(const ast::MatchExpr& node, const ast::Expr& expr) { @@ -1067,19 +932,11 @@ struct ExprResolver { if (arm.isDefault) { if (!arm.patterns.empty()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - "match wildcard '_' must be a standalone pattern", - expr.span - }); + diags.push_back(diagnostics::MatchWildcardNotStandalone(expr.span)); } if (armIndex + 1 != node.arms.size()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - "match wildcard '_' arm must be last", - expr.span - }); + diags.push_back(diagnostics::MatchWildcardNotLast(expr.span)); } continue; @@ -1100,28 +957,15 @@ struct ExprResolver { } } else if (currentPatternType != *patternType) { auto patternName = patternDisplayName(*pattern); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "match pattern '{}' is {} but expected {}", - patternName, typeName(currentPatternType), - typeName(*patternType)), - expr.span - }); + diags.push_back(diagnostics::MatchPatternTypeMismatch( + expr.span, patternName, typeName(currentPatternType), typeName(*patternType))); } else if (currentPatternType == T::Enum && patternEnumIdentity.has_value() && currentPatternEnumIdentity.has_value() && *currentPatternEnumIdentity != *patternEnumIdentity) { auto patternName = patternDisplayName(*pattern); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "match pattern '{}' is enum '{}' but expected enum '{}'", - patternName, - *currentPatternEnumIdentity, - *patternEnumIdentity), - expr.span - }); + diags.push_back(diagnostics::MatchPatternEnumMismatch( + expr.span, patternName, *currentPatternEnumIdentity, *patternEnumIdentity)); } } } @@ -1146,28 +990,14 @@ struct ExprResolver { } } else if (discrimType != *patternType) { auto name = discrimName.value_or(""); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "match discriminant '{}' is {} but patterns are {}", - name, - typeName(discrimType), - typeName(*patternType)), - expr.span - }); + diags.push_back(diagnostics::MatchDiscriminantTypeMismatch( + expr.span, name, typeName(discrimType), typeName(*patternType))); } else if (discrimType == T::Enum && patternEnumIdentity.has_value()) { auto discrimEnumIdentity = enumIdentityOfExpr(*node.discriminant); if (discrimEnumIdentity.has_value() && *discrimEnumIdentity != *patternEnumIdentity) { auto name = discrimName.value_or(""); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "match discriminant '{}' is enum '{}' but patterns are enum '{}'", - name, - *discrimEnumIdentity, - *patternEnumIdentity), - expr.span - }); + diags.push_back(diagnostics::MatchDiscriminantEnumMismatch( + expr.span, name, *discrimEnumIdentity, *patternEnumIdentity)); } } } @@ -1182,24 +1012,12 @@ struct ExprResolver { bodyType = armType; } else if (armType != bodyType) { if (isBoolCompatible(armType) && isBoolCompatible(bodyType)) { - diags.push_back({ - ast::DiagnosticLevel::Warning, - std::format( - "match arm type {} implicitly " - "converted to Bool (previous arms are {})", - typeName(armType), typeName(bodyType)), - arm.body->span - }); + diags.push_back(diagnostics::MatchArmImplicitBool( + arm.body->span, typeName(armType), typeName(bodyType))); bodyType = T::Bool; } else { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "match arm type {} doesn't match " - "previous arms ({})", - typeName(armType), typeName(bodyType)), - arm.body->span - }); + diags.push_back(diagnostics::MatchArmTypeMismatch( + arm.body->span, typeName(armType), typeName(bodyType))); } } } @@ -1334,11 +1152,7 @@ static std::vector topoSortDefines( } names += " -> "; names += cycle.front(); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("cycle in define call graph: {}", names), - {} - }); + diags.push_back(diagnostics::DefineCycle({}, names)); } return order; @@ -1366,14 +1180,8 @@ std::vector resolveTypes(ast::Project& project) { enumScope[param.name.text] = std::string(*annotation->enumName); } } else { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "unknown type annotation '{}' " - "for parameter '{}'", - param.type->name.text, param.name.text), - decl->span - }); + diags.push_back(diagnostics::UnknownParameterTypeAnnotation( + decl->span, param.type->name.text, param.name.text)); } } @@ -1458,14 +1266,8 @@ std::vector resolveTypes(ast::Project& project) { project.setEnumType(¶m, std::string(*annotation->enumName)); } } else { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "unknown type annotation '{}' " - "for parameter '{}'", - param.type->name.text, param.name.text), - decl->span - }); + diags.push_back(diagnostics::UnknownParameterTypeAnnotation( + decl->span, param.type->name.text, param.name.text)); } } diff --git a/sema/src/semantic_index.cpp b/sema/src/semantic_index.cpp new file mode 100644 index 0000000..9d4f99b --- /dev/null +++ b/sema/src/semantic_index.cpp @@ -0,0 +1,723 @@ +#include "semantic_index.h" + +#include "type_helpers.h" +#include "validate_declarations.h" + +#include +#include +#include +#include +#include + +namespace rls::sema { + +SymbolId SemanticIndex::addSymbol(SymbolCategory category, SymbolProvenance provenance, + std::string displayName, ast::Span declaration, ast::Span selection, + std::optional container, std::optional signature, + std::optional type, std::optional enumName, + std::optional defaultValue, bool optional) { + const SymbolId id{symbols_.size() + 1}; + symbols_.push_back({id, category, provenance, std::move(displayName), + std::move(declaration), std::move(selection), std::move(signature), type, + std::move(enumName), container, std::move(defaultValue), optional}); + occurrences_.push_back({id, symbols_.back().selection, OccurrenceKind::Declaration}); + return id; +} + +std::optional SemanticIndex::declaration(SymbolId id) const { + for (const auto& symbol : symbols_) { + if (symbol.id == id) return symbol; + } + return std::nullopt; +} + +std::vector SemanticIndex::occurrencesFor(SymbolId id) const { + std::vector result; + for (const auto& occurrence : occurrences_) { + if (occurrence.symbol == id) result.push_back(occurrence); + } + std::sort(result.begin(), result.end(), [](const OccurrenceRecord& left, const OccurrenceRecord& right) { + return std::tie(left.span.file, left.span.start.line, left.span.start.column, + left.span.end.line, left.span.end.column) < + std::tie(right.span.file, right.span.start.line, right.span.start.column, + right.span.end.line, right.span.end.column); + }); + return result; +} + +namespace { + +std::string renderDefaultExpression(const ast::Expr& expression); + +std::string escapeString(std::string_view value) { + std::string result = "\""; + for (const char character : value) { + if (character == '\\' || character == '"') result += '\\'; + result += character; + } + result += '"'; + return result; +} + +std::string_view binaryOperator(ast::BinaryOp op) { + switch (op) { + case ast::BinaryOp::And: return "and"; + case ast::BinaryOp::Or: return "or"; + case ast::BinaryOp::Eq: return "=="; + case ast::BinaryOp::NotEq: return "!="; + case ast::BinaryOp::Lt: return "<"; + case ast::BinaryOp::LtEq: return "<="; + case ast::BinaryOp::Gt: return ">"; + case ast::BinaryOp::GtEq: return ">="; + case ast::BinaryOp::Add: return "+"; + case ast::BinaryOp::Sub: return "-"; + case ast::BinaryOp::Mul: return "*"; + case ast::BinaryOp::Div: return "/"; + } + return "?"; +} + +std::string renderDefaultExpression(const ast::Expr& expression) { + return std::visit([&](const auto& node) -> std::string { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return node.value ? "true" : "false"; + } else if constexpr (std::is_same_v) { + return std::to_string(node.value); + } else if constexpr (std::is_same_v) { + return escapeString(node.value); + } else if constexpr (std::is_same_v) { + return node.name.text; + } else if constexpr (std::is_same_v) { + return node.object.text + "." + node.member.text; + } else if constexpr (std::is_same_v) { + return "not " + renderDefaultExpression(*node.operand); + } else if constexpr (std::is_same_v) { + return "(" + renderDefaultExpression(*node.left) + " " + + std::string(binaryOperator(node.op)) + " " + + renderDefaultExpression(*node.right) + ")"; + } else if constexpr (std::is_same_v) { + return "(" + renderDefaultExpression(*node.condition) + " ? " + + renderDefaultExpression(*node.thenBranch) + " : " + + renderDefaultExpression(*node.elseBranch) + ")"; + } else if constexpr (std::is_same_v) { + std::string result = node.callee.text + "("; + for (size_t index = 0; index < node.args.size(); ++index) { + if (index != 0) result += ", "; + if (node.args[index].name) { + result += node.args[index].name->text + ": "; + } + result += renderDefaultExpression(*node.args[index].value); + } + return result + ")"; + } else if constexpr (std::is_same_v) { + return renderDefaultExpression(*node.callee) + "()"; + } else if constexpr (std::is_same_v) { + return "here"; + } else if constexpr (std::is_same_v) { + std::string result = "["; + for (size_t index = 0; index < node.elements.size(); ++index) { + if (index != 0) result += ", "; + result += renderDefaultExpression(*node.elements[index]); + } + return result + "]"; + } else if constexpr (std::is_same_v) { + return "match " + renderDefaultExpression(*node.discriminant) + " { ... }"; + } + return ""; + }, expression.node); +} + +bool isBeforeOrEqual(ast::Position left, ast::Position right) { + return left.line < right.line || (left.line == right.line && left.column <= right.column); +} + +bool contains(const ast::Span& span, std::string_view file, ast::Position position) { + return span.file == file && span.start.line != 0 && isBeforeOrEqual(span.start, position) && + isBeforeOrEqual(position, span.end) && + !(position.line == span.end.line && position.column == span.end.column); +} + +size_t spanSize(const ast::Span& span) { + return (static_cast(span.end.line - span.start.line) << 32) + + span.end.column - span.start.column; +} + +template +std::optional narrowestAt(const std::vector& records, std::string_view file, + ast::Position position) { + const Record* result = nullptr; + for (const auto& record : records) { + if (contains(record.span, file, position) && (!result || spanSize(record.span) < spanSize(result->span))) { + result = &record; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +} // namespace + +std::optional SemanticIndex::occurrenceAt(std::string_view file, + ast::Position position) const { + const OccurrenceRecord* result = nullptr; + for (const auto& occurrence : occurrences_) { + if (!contains(occurrence.span, file, position)) continue; + if (!result || spanSize(occurrence.span) < spanSize(result->span) || + (spanSize(occurrence.span) == spanSize(result->span) && + result->kind == OccurrenceKind::Declaration && occurrence.kind != OccurrenceKind::Declaration)) { + result = &occurrence; + } + } + return result ? std::optional(*result) : std::nullopt; +} + +std::optional SemanticIndex::typeAt(std::string_view file, ast::Position position) const { + return narrowestAt(types_, file, position); +} + +std::optional SemanticIndex::expectedTypeAt(std::string_view file, + ast::Position position) const { + return narrowestAt(expectedTypes_, file, position); +} + +std::optional SemanticIndex::callAt(std::string_view file, ast::Position position) const { + return narrowestAt(calls_, file, position); +} + +std::vector SemanticIndex::visibleSymbolsAt(std::string_view file, + ast::Position position) const { + std::vector result; + for (const auto& symbol : symbols_) { + if (!symbol.container) { + result.push_back(symbol.id); + continue; + } + if (symbol.category == SymbolCategory::SectionEntry + && (symbol.type == ast::Type::Event || symbol.type == ast::Type::Location)) { + result.push_back(symbol.id); + continue; + } + const auto container = declaration(*symbol.container); + if (symbol.category == SymbolCategory::Parameter && container && + container->category == SymbolCategory::Define && contains(container->declaration, file, position)) { + result.push_back(symbol.id); + } + } + return result; +} + +bool SemanticIndex::patternMatches(SymbolId id, std::string_view value) const { + const auto symbol = declaration(id); + return symbol && symbol->category == SymbolCategory::ExternEnumPattern + && globMatches(symbol->displayName, value); +} + +SemanticIndex buildSemanticIndex(const ast::Project& project, + const std::vector& diagnostics) { + SemanticIndex index; + auto addParameters = [&](const std::vector& parameters, SymbolId container) { + for (const auto& parameter : parameters) { + const auto type = project.getType(¶meter); + const auto enumName = project.getEnumType(¶meter); + const auto declaration = parameter.span.start.line == 0 + ? parameter.name.span : parameter.span; + index.addSymbol(SymbolCategory::Parameter, SymbolProvenance::Source, + parameter.name.text, declaration, parameter.name.span, + container, std::nullopt, type, + enumName ? std::optional(*enumName) : + (parameter.type ? std::optional(parameter.type->name.text) : std::nullopt), + parameter.defaultValue + ? std::optional(renderDefaultExpression(*parameter.defaultValue)) + : std::nullopt, + parameter.defaultValue != nullptr); + } + }; + std::unordered_map regionDataSymbols; + std::unordered_map eventSymbols; + std::unordered_map locationSymbols; + auto addSections = [&](const std::vector& sections, SymbolId container) { + for (const auto& section : sections) { + if (section.kind == ast::SectionKind::Exits) continue; + const auto type = section.kind == ast::SectionKind::Events + ? std::optional(ast::Type::Event) + : std::optional(ast::Type::Location); + for (const auto& entry : section.entries) { + const auto id = index.addSymbol( + SymbolCategory::SectionEntry, SymbolProvenance::Source, + entry.name.text, entry.span, entry.name.span, container, + std::nullopt, type); + auto& canonicalSymbols = *type == ast::Type::Event + ? eventSymbols : locationSymbols; + const auto [canonical, inserted] = canonicalSymbols.try_emplace( + entry.name.text, id); + if (!inserted) index.occurrences_.back().symbol = canonical->second; + } + } + }; + + for (const auto& file : project.files) { + for (const auto& declaration : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + const auto id = index.addSymbol(SymbolCategory::Region, SymbolProvenance::Source, + node.key.text, node.span, node.key.span, std::nullopt, + std::nullopt, ast::Type::Region); + for (const auto& data : node.body.data) { + const auto dataId = index.addSymbol( + SymbolCategory::RegionDataEntry, SymbolProvenance::Source, + data.key.text, data.span, data.key.span, id); + const auto [canonical, inserted] = regionDataSymbols.try_emplace( + data.key.text, dataId); + if (!inserted) index.occurrences_.back().symbol = canonical->second; + } + addSections(node.body.sections, id); + } else if constexpr (std::is_same_v) { + const auto id = index.addSymbol(SymbolCategory::RegionExtension, SymbolProvenance::Source, + node.name.text, node.span, node.name.span); + addSections(node.sections, id); + } else if constexpr (std::is_same_v) { + const auto returnType = project.getType(node.body.get()); + const auto returnEnum = project.getEnumType(node.body.get()); + const auto id = index.addSymbol(SymbolCategory::Define, SymbolProvenance::Source, + node.name.text, node.span, node.name.span, std::nullopt, + "define " + node.name.text, returnType, + returnEnum ? std::optional(*returnEnum) : std::nullopt); + addParameters(node.params, id); + } else if constexpr (std::is_same_v) { + const auto returnType = node.returnType + ? resolveTypeAnnotation(project, node.returnType->name.text) + : std::nullopt; + const auto id = index.addSymbol(SymbolCategory::ExternDefine, SymbolProvenance::Extern, + node.name.text, node.span, node.name.span, std::nullopt, + "extern define " + node.name.text, + returnType ? std::optional(returnType->type) : std::nullopt, + returnType && returnType->enumName + ? std::optional(*returnType->enumName) + : std::nullopt); + addParameters(node.params, id); + } else if constexpr (std::is_same_v) { + const auto id = index.addSymbol(SymbolCategory::Enum, SymbolProvenance::Source, + node.name.text, node.span, node.name.span, std::nullopt, std::nullopt, + ast::Type::Enum, node.name.text); + for (const auto& member : node.members) { + index.addSymbol(SymbolCategory::EnumMember, SymbolProvenance::Source, + member.name.text, member.span, member.name.span, id, std::nullopt, + ast::Type::Enum, node.name.text); + } + } else if constexpr (std::is_same_v) { + const auto id = index.addSymbol(SymbolCategory::Enum, SymbolProvenance::Extern, + node.name.text, node.span, node.name.span, std::nullopt, std::nullopt, + ast::Type::Enum, node.name.text); + for (const auto& entry : node.entries) { + if (const auto* member = std::get_if(&entry)) { + index.addSymbol(SymbolCategory::EnumMember, SymbolProvenance::Extern, + member->name.text, member->span, member->name.span, id, std::nullopt, + ast::Type::Enum, node.name.text); + } else { + const auto& pattern = std::get(entry); + index.addSymbol(SymbolCategory::ExternEnumPattern, SymbolProvenance::Pattern, + pattern.pattern, pattern.span, pattern.span, id, std::nullopt, + ast::Type::Enum, node.name.text); + } + } + } + }, declaration); + } + } + + auto findSymbol = [&](SymbolCategory category, std::string_view name) -> std::optional { + for (const auto& symbol : index.symbols_) { + if (symbol.category == category && symbol.displayName == name) return symbol.id; + } + return std::nullopt; + }; + auto findUniquePattern = [&](SymbolId enumId, std::string_view valueName) { + std::optional result; + for (const auto& symbol : index.symbols_) { + if (symbol.category != SymbolCategory::ExternEnumPattern + || symbol.container != enumId + || !globMatches(symbol.displayName, valueName)) { + continue; + } + if (result) return std::optional{}; + result = symbol.id; + } + return result; + }; + auto addExitReferences = [&](const std::vector& sections) { + for (const auto& section : sections) { + if (section.kind != ast::SectionKind::Exits) continue; + for (const auto& entry : section.entries) { + auto target = findSymbol(SymbolCategory::Region, entry.name.text); + if (!target) { + const auto regionEnum = findSymbol(SymbolCategory::Enum, "Region"); + if (regionEnum) target = findUniquePattern(*regionEnum, entry.name.text); + } + index.occurrences_.push_back({target, entry.name.span, + target ? OccurrenceKind::ExitTarget : OccurrenceKind::Unresolved}); + } + } + }; + for (const auto& file : project.files) { + for (const auto& declaration : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + addExitReferences(node.body.sections); + } else if constexpr (std::is_same_v) { + addExitReferences(node.sections); + } + }, declaration); + } + } + auto addTypeReference = [&](const ast::TypeRef& typeReference) { + if (typeFromAnnotation(typeReference.name.text)) { + index.occurrences_.push_back({std::nullopt, typeReference.name.span, + OccurrenceKind::TypeReference}); + return; + } + const auto target = findSymbol(SymbolCategory::Enum, typeReference.name.text); + index.occurrences_.push_back({target, typeReference.name.span, OccurrenceKind::TypeReference}); + }; + for (const auto& file : project.files) { + for (const auto& declaration : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v || std::is_same_v) { + for (const auto& parameter : node.params) { + if (parameter.type) addTypeReference(*parameter.type); + } + if constexpr (std::is_same_v) { + if (node.returnType) addTypeReference(*node.returnType); + } + } + }, declaration); + } + } + auto addDuplicateDiagnostic = [&](std::string_view code, std::string_view kind, + std::string_view name, const ast::Span& first, const ast::Span& duplicate) { + index.diagnostics_.push_back({ + std::string(code), + ast::DiagnosticLevel::Error, + std::format("duplicate {} '{}'", kind, name), + duplicate, + {{"first declaration", first}}, + }); + }; + std::unordered_map regions; + std::unordered_map functions; + std::unordered_map enums; + for (const auto& file : project.files) { + for (const auto& declaration : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + if (const auto [it, inserted] = regions.try_emplace(node.key.text, node.span); !inserted) { + addDuplicateDiagnostic("RLS-S001", "region", node.key.text, it->second, node.span); + } + } else if constexpr (std::is_same_v) { + if (const auto [it, inserted] = functions.try_emplace(node.name.text, node.span); !inserted) { + addDuplicateDiagnostic("RLS-S002", "function", node.name.text, it->second, node.span); + } + } else if constexpr (std::is_same_v) { + if (const auto [it, inserted] = functions.try_emplace(node.name.text, node.span); !inserted) { + addDuplicateDiagnostic("RLS-S002", "function", node.name.text, it->second, node.span); + } + } else if constexpr (std::is_same_v || std::is_same_v) { + if (const auto [it, inserted] = enums.try_emplace(node.name.text, node.span); !inserted) { + addDuplicateDiagnostic("RLS-S003", "enum", node.name.text, it->second, node.span); + } + } + }, declaration); + } + } + for (const auto& file : project.files) { + for (const auto& declaration : file.declarations) { + if (const auto* extension = std::get_if(&declaration)) { + const auto extensionId = findSymbol(SymbolCategory::RegionExtension, extension->name.text); + const auto targetId = findSymbol(SymbolCategory::Region, extension->name.text); + if (extensionId) { + for (auto& symbol : index.symbols_) { + if (symbol.id == *extensionId) { + symbol.container = targetId; + break; + } + } + } + index.occurrences_.push_back({targetId, extension->name.span, + targetId ? OccurrenceKind::ExtensionTarget : OccurrenceKind::Unresolved}); + } + } + } + auto addType = [&](const ast::Expr& expression) { + if (const auto type = project.getType(&expression)) { + const auto enumName = project.getEnumType(&expression); + index.types_.push_back({expression.span, *type, + enumName ? std::optional(*enumName) : std::nullopt}); + } + }; + auto addExpectedType = [&](const ast::Expr& expression, ast::Type type, + std::optional enumName = std::nullopt) { + index.expectedTypes_.push_back({expression.span, type, std::move(enumName)}); + }; + auto addObservedEnumValue = [&](std::string_view displayName, std::string_view enumName) { + const auto existing = std::find_if( + index.observedEnumValues_.begin(), index.observedEnumValues_.end(), + [&](const ObservedEnumValue& value) { + return value.displayName == displayName && value.enumName == enumName; + }); + if (existing == index.observedEnumValues_.end()) { + index.observedEnumValues_.push_back({ + std::string(displayName), std::string(enumName)}); + } + }; + auto isPatternSymbol = [&](std::optional symbolId) { + if (!symbolId) return false; + return std::any_of(index.symbols_.begin(), index.symbols_.end(), + [&](const SymbolRecord& symbol) { + return symbol.id == *symbolId + && symbol.category == SymbolCategory::ExternEnumPattern; + }); + }; + std::function)> indexExpression; + indexExpression = [&](const ast::Expr& expression, std::optional defineScope) { + addType(expression); + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + std::optional target; + OccurrenceKind kind = OccurrenceKind::Unresolved; + if (node.kind == ast::IdentifierKind::Parameter && defineScope) { + for (const auto& symbol : index.symbols_) { + if (symbol.category == SymbolCategory::Parameter && + symbol.container == defineScope && symbol.displayName == node.name.text) { + target = symbol.id; + break; + } + } + kind = target ? OccurrenceKind::Reference : OccurrenceKind::Unresolved; + } else if (node.kind == ast::IdentifierKind::FunctionRef) { + target = findSymbol(SymbolCategory::Define, node.name.text); + if (!target) target = findSymbol(SymbolCategory::ExternDefine, node.name.text); + kind = target ? OccurrenceKind::Reference : OccurrenceKind::Unresolved; + } else if (node.kind == ast::IdentifierKind::DeclaredValue) { + const auto type = project.getType(&expression); + if (type == ast::Type::Region) { + target = findSymbol(SymbolCategory::Region, node.name.text); + } else if (type == ast::Type::Event || type == ast::Type::Location) { + for (const auto& symbol : index.symbols_) { + if (symbol.category == SymbolCategory::SectionEntry + && symbol.type == type + && symbol.displayName == node.name.text) { + target = symbol.id; + break; + } + } + } + kind = target ? OccurrenceKind::Reference : OccurrenceKind::Unresolved; + } else if (node.kind == ast::IdentifierKind::EnumValue) { + const auto enumName = project.getEnumType(&expression); + if (enumName) { + const auto enumId = findSymbol(SymbolCategory::Enum, *enumName); + if (enumId) { + for (const auto& symbol : index.symbols_) { + if (symbol.category == SymbolCategory::EnumMember + && symbol.container == enumId + && symbol.displayName == node.name.text) { + target = symbol.id; + break; + } + } + if (!target) target = findUniquePattern(*enumId, node.name.text); + } + if (!target || isPatternSymbol(target)) { + addObservedEnumValue(node.name.text, *enumName); + } + } + kind = target ? OccurrenceKind::Reference : OccurrenceKind::Unresolved; + } + index.occurrences_.push_back({target, node.name.span, kind}); + } else if constexpr (std::is_same_v) { + const auto enumId = findSymbol(SymbolCategory::Enum, node.object.text); + index.occurrences_.push_back({enumId, node.object.span, + enumId ? OccurrenceKind::Reference : OccurrenceKind::Unresolved}); + std::optional memberId; + if (enumId) { + for (const auto& symbol : index.symbols_) { + if (symbol.category == SymbolCategory::EnumMember + && symbol.container == enumId + && symbol.displayName == node.member.text) { + memberId = symbol.id; + break; + } + } + if (!memberId) memberId = findUniquePattern(*enumId, node.member.text); + } + index.occurrences_.push_back({memberId, node.member.span, + memberId ? OccurrenceKind::MemberAccess : OccurrenceKind::Unresolved}); + const auto expressionType = project.getType(&expression); + const auto expressionEnum = project.getEnumType(&expression); + if (enumId && (!memberId || isPatternSymbol(memberId)) + && expressionType == ast::Type::Enum + && expressionEnum && *expressionEnum == node.object.text) { + addObservedEnumValue(node.member.text, *expressionEnum); + } + } else if constexpr (std::is_same_v) { + addExpectedType(*node.operand, ast::Type::Bool); + indexExpression(*node.operand, defineScope); + } else if constexpr (std::is_same_v) { + switch (node.op) { + case ast::BinaryOp::And: + case ast::BinaryOp::Or: + addExpectedType(*node.left, ast::Type::Bool); + addExpectedType(*node.right, ast::Type::Bool); + break; + case ast::BinaryOp::Lt: + case ast::BinaryOp::LtEq: + case ast::BinaryOp::Gt: + case ast::BinaryOp::GtEq: + case ast::BinaryOp::Add: + case ast::BinaryOp::Sub: + case ast::BinaryOp::Mul: + case ast::BinaryOp::Div: + addExpectedType(*node.left, ast::Type::Int); + addExpectedType(*node.right, ast::Type::Int); + break; + case ast::BinaryOp::Eq: + case ast::BinaryOp::NotEq: { + const auto leftType = project.getType(node.left.get()); + const auto rightType = project.getType(node.right.get()); + if (leftType && *leftType != ast::Type::Error) { + const auto enumName = *leftType == ast::Type::Enum + ? project.getEnumType(node.left.get()) : std::optional{}; + addExpectedType(*node.right, *leftType, + enumName ? std::optional(*enumName) : std::nullopt); + } + if (rightType && *rightType != ast::Type::Error) { + const auto enumName = *rightType == ast::Type::Enum + ? project.getEnumType(node.right.get()) : std::optional{}; + addExpectedType(*node.left, *rightType, + enumName ? std::optional(*enumName) : std::nullopt); + } + break; + } + } + indexExpression(*node.left, defineScope); + indexExpression(*node.right, defineScope); + } else if constexpr (std::is_same_v) { + addExpectedType(*node.condition, ast::Type::Bool); + indexExpression(*node.condition, defineScope); + indexExpression(*node.thenBranch, defineScope); + indexExpression(*node.elseBranch, defineScope); + } else if constexpr (std::is_same_v) { + auto target = findSymbol(SymbolCategory::Define, node.callee.text); + if (!target) target = findSymbol(SymbolCategory::ExternDefine, node.callee.text); + index.occurrences_.push_back({target, node.callee.span, + target ? OccurrenceKind::Call : OccurrenceKind::Unresolved}); + CallRecord call{expression.span, target, {}, {}}; + const auto* normalized = project.getResolvedCallArgs(&node); + for (const auto& argument : node.args) { + call.argumentRanges.push_back(argument.value->span); + std::optional binding; + if (normalized) { + for (size_t indexValue = 0; indexValue < normalized->size(); ++indexValue) { + if ((*normalized)[indexValue] == argument.value.get()) binding = indexValue; + } + } + call.normalizedBindings.push_back(binding); + if (binding && target) { + size_t parameterIndex = 0; + for (const auto& symbol : index.symbols_) { + if (symbol.category != SymbolCategory::Parameter || symbol.container != target) continue; + if (parameterIndex++ != *binding) continue; + if (argument.name) { + index.occurrences_.push_back({ + symbol.id, argument.name->span, OccurrenceKind::Reference}); + } + if (symbol.type) { + index.expectedTypes_.push_back({ + argument.value->span, *symbol.type, symbol.enumName}); + } + break; + } + } + indexExpression(*argument.value, defineScope); + } + index.calls_.push_back(std::move(call)); + } else if constexpr (std::is_same_v) { + indexExpression(*node.callee, defineScope); + } else if constexpr (std::is_same_v) { + const auto discriminatorType = project.getType(node.discriminant.get()); + const auto discriminatorEnum = discriminatorType && *discriminatorType == ast::Type::Enum + ? project.getEnumType(node.discriminant.get()) : std::optional{}; + indexExpression(*node.discriminant, defineScope); + for (const auto& arm : node.arms) { + for (const auto& pattern : arm.patterns) { + if (discriminatorType && *discriminatorType != ast::Type::Error) { + addExpectedType(*pattern, *discriminatorType, + discriminatorEnum ? std::optional(*discriminatorEnum) : std::nullopt); + } + indexExpression(*pattern, defineScope); + } + indexExpression(*arm.body, defineScope); + } + } else if constexpr (std::is_same_v) { + for (const auto& element : node.elements) indexExpression(*element, defineScope); + } + }, expression.node); + }; + + for (const auto& file : project.files) { + for (const auto& declaration : file.declarations) { + std::visit([&](const auto& node) { + using T = std::decay_t; + if constexpr (std::is_same_v || std::is_same_v) { + const auto defineId = findSymbol( + std::is_same_v + ? SymbolCategory::Define + : SymbolCategory::ExternDefine, + node.name.text); + if constexpr (std::is_same_v) { + if (node.body) indexExpression(*node.body, defineId); + } + for (const auto& parameter : node.params) { + if (parameter.defaultValue) { + if (const auto type = project.getType(¶meter)) { + const auto enumName = *type == ast::Type::Enum ? project.getEnumType(¶meter) : std::optional{}; + addExpectedType(*parameter.defaultValue, *type, + enumName ? std::optional(*enumName) : std::nullopt); + } + indexExpression(*parameter.defaultValue, defineId); + } + } + } else if constexpr (std::is_same_v) { + for (const auto& data : node.body.data) indexExpression(*data.value, std::nullopt); + for (const auto& section : node.body.sections) { + for (const auto& entry : section.entries) { + addExpectedType(*entry.condition, ast::Type::Bool); + indexExpression(*entry.condition, std::nullopt); + } + } + } else if constexpr (std::is_same_v) { + for (const auto& section : node.sections) { + for (const auto& entry : section.entries) { + addExpectedType(*entry.condition, ast::Type::Bool); + indexExpression(*entry.condition, std::nullopt); + } + } + } + }, declaration); + } + } + const auto validationDiagnostics = structureValidationDiagnostics(project, diagnostics); + std::sort(index.observedEnumValues_.begin(), index.observedEnumValues_.end(), + [](const ObservedEnumValue& left, const ObservedEnumValue& right) { + return std::tie(left.enumName, left.displayName) + < std::tie(right.enumName, right.displayName); + }); + index.diagnostics_.insert(index.diagnostics_.end(), validationDiagnostics.begin(), validationDiagnostics.end()); + return index; +} + +} // namespace rls::sema \ No newline at end of file diff --git a/sema/src/type_helpers.h b/sema/src/type_helpers.h index bd85153..62d406e 100644 --- a/sema/src/type_helpers.h +++ b/sema/src/type_helpers.h @@ -22,6 +22,9 @@ inline std::string_view typeName(ast::Type t) { case ast::Type::Callable: return "Callable"; case ast::Type::Condition: return "Condition"; case ast::Type::Enum: return "Enum"; + case ast::Type::Region: return "Region"; + case ast::Type::Event: return "Event"; + case ast::Type::Location: return "Location"; case ast::Type::Void: return "Void"; case ast::Type::Error: return ""; } @@ -35,6 +38,28 @@ inline bool isBoolCompatible(ast::Type t) { || t == ast::Type::Int; } +inline bool isDomainEnumCompatible( + ast::Type domainType, std::optional enumName) { + if (!enumName) return false; + switch (domainType) { + case ast::Type::Region: + return *enumName == "Region"; + case ast::Type::Event: + return *enumName == "Event"; + case ast::Type::Location: + return *enumName == "Location"; + default: + return false; + } +} + +inline bool areDomainAndEnumCompatible( + ast::Type expected, std::optional expectedEnum, + ast::Type actual, std::optional actualEnum) { + return (actual == ast::Type::Enum && isDomainEnumCompatible(expected, actualEnum)) + || (expected == ast::Type::Enum && isDomainEnumCompatible(actual, expectedEnum)); +} + /// Parse a built-in type annotation string (e.g. "Bool") to a Type enum value. /// Named enum annotations are resolved from the Project's enum registry. /// Returns nullopt if the annotation is not a recognized type name. @@ -52,6 +77,9 @@ inline std::optional typeFromAnnotation(std::string_view annotation) {"Callable", ast::Type::Callable}, {"Condition", ast::Type::Condition}, {"Enum", ast::Type::Enum}, + {"Region", ast::Type::Region}, + {"Event", ast::Type::Event}, + {"Location", ast::Type::Location}, }; for (const auto& [name, type] : table) { diff --git a/sema/src/validate_declarations.cpp b/sema/src/validate_declarations.cpp index f70bcb8..f74cc4e 100644 --- a/sema/src/validate_declarations.cpp +++ b/sema/src/validate_declarations.cpp @@ -1,8 +1,8 @@ #include "validate_declarations.h" +#include "diagnostics.h" #include "type_helpers.h" #include -#include #include #include #include @@ -35,21 +35,13 @@ static void checkEnumDeclarations( for (const auto& entry : info.entries) { if (std::holds_alternative(entry)) { const auto& pattern = std::get(entry); - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("enum '{}' cannot contain wildcard pattern '{}'", enumName, pattern.pattern), - pattern.span - }); + diags.push_back(diagnostics::EnumWildcardPattern(pattern.span, enumName, pattern.pattern)); continue; } auto member = std::get(entry); if (!seenNames.insert(member.name.text).second) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("duplicate enum member '{}' in enum '{}'", member.name.text, enumName), - member.span - }); + diags.push_back(diagnostics::EnumDuplicateMember(member.span, member.name.text, enumName)); continue; } @@ -60,11 +52,7 @@ static void checkEnumDeclarations( } if (!seenValues.insert(*member.value).second) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("duplicate enum value {} in enum '{}'", *member.value, enumName), - member.span - }); + diags.push_back(diagnostics::EnumDuplicateValue(member.span, *member.value, enumName)); } nextValue = *member.value + 1; @@ -84,11 +72,7 @@ static void checkEnumDeclarations( // Extern enums: explicit members and wildcard patterns are allowed. if (info.entries.empty()) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("extern enum '{}' must declare at least one member or wildcard pattern", enumName), - info.span - }); + diags.push_back(diagnostics::ExternEnumEmpty(info.span, enumName)); continue; } @@ -98,11 +82,7 @@ static void checkEnumDeclarations( if (std::holds_alternative(entry)) { const auto& member = std::get(entry); if (!explicitNames.insert(member.name.text).second) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("duplicate enum member '{}' in extern enum '{}'", member.name.text, enumName), - member.span - }); + diags.push_back(diagnostics::ExternEnumDuplicateMember(member.span, member.name.text, enumName)); } valueNameToEnums[member.name.text].insert(enumName); } else { @@ -118,15 +98,7 @@ static void checkEnumDeclarations( for (const auto& pattern : patterns) { for (const auto& explicitName : explicitNames) { if (globMatches(pattern, explicitName)) { - diags.push_back({ - ast::DiagnosticLevel::Warning, - std::format( - "extern enum '{}' wildcard '{}' overlaps explicit member '{}'", - enumName, - pattern, - explicitName), - info.span - }); + diags.push_back(diagnostics::ExternEnumWildcardOverlap(info.span, enumName, pattern, explicitName)); } } } @@ -146,14 +118,7 @@ static void checkEnumDeclarations( first = false; } - diags.push_back({ - ast::DiagnosticLevel::Warning, - std::format( - "enum value '{}' appears in multiple enums ({}) and may require dotted disambiguation", - valueName, - enumList), - {} - }); + diags.push_back(diagnostics::EnumValueNameCollision({}, valueName, enumList)); } } @@ -164,11 +129,7 @@ static void checkExtendRegionTargets( for (auto& [name, decls] : project.ExtendRegionDecls) { if (!project.RegionDecls.contains(name)) { for (const auto* decl : decls) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("extend region targets unknown region '{}'", name), - decl->span - }); + diags.push_back(diagnostics::UnknownExtensionTarget(decl->span, name)); } } } @@ -182,11 +143,7 @@ static void checkDuplicateRegionData( std::unordered_set seen; for (const auto& entry : regionDecl->body.data) { if (!seen.insert(entry.key.text).second) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("duplicate data key '{}' in region '{}'", entry.key.text, regionName), - entry.key.span - }); + diags.push_back(diagnostics::DuplicateRegionData(entry.key.span, entry.key.text, regionName)); } } } @@ -205,12 +162,8 @@ static void checkDuplicateEntries( auto& set = seen[section.kind]; for (const auto& entry : section.entries) { if (!set.insert(entry.name.text).second) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("duplicate {} '{}' in region '{}'", - sectionKindName(section.kind), entry.name.text, regionName), - entry.span - }); + diags.push_back(diagnostics::DuplicateRegionEntry( + entry.span, sectionKindName(section.kind), entry.name.text, regionName)); } } } @@ -238,13 +191,8 @@ static void checkEntryConditionTypes( auto condType = project.getType(entry.condition.get()); if (!condType || *condType == ast::Type::Error) continue; if (!isBoolCompatible(*condType)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("{} condition for '{}' in region '{}' must be Bool, got {}", - sectionKindName(section.kind), entry.name.text, regionName, - typeName(*condType)), - entry.span - }); + diags.push_back(diagnostics::EntryConditionType( + entry.span, sectionKindName(section.kind), entry.name.text, regionName, typeName(*condType))); } } } @@ -260,7 +208,33 @@ static void checkEntryConditionTypes( } } -/// Check 4: Every region must be reachable from RR_ROOT via exits. +/// Check 4: Every exit must target a declared region. +static void checkExitTargets( + ast::Project& project, std::vector& diags) +{ + auto check = [&](const std::vector& sections) { + for (const auto& section : sections) { + if (section.kind != ast::SectionKind::Exits) continue; + for (const auto& entry : section.entries) { + if (!project.RegionDecls.contains(entry.name.text)) { + diags.push_back(diagnostics::ExitTargetMissingRegion( + entry.name.span, entry.name.text)); + } + } + } + }; + + for (const auto& [_, decl] : project.RegionDecls) { + check(decl->body.sections); + } + for (const auto& [_, decls] : project.ExtendRegionDecls) { + for (const auto* decl : decls) { + check(decl->sections); + } + } +} + +/// Check 5: Every region must be reachable from RR_ROOT via exits. static void checkRegionReachability( ast::Project& project, std::vector& diags) { @@ -313,11 +287,7 @@ static void checkRegionReachability( for (auto& [regionName, decl] : project.RegionDecls) { if (!visited.contains(regionName)) { - diags.push_back({ - ast::DiagnosticLevel::Warning, - std::format("region '{}' is not reachable from 'RR_ROOT'", regionName), - decl->span - }); + diags.push_back(diagnostics::UnreachableRegion(decl->span, regionName)); } } } @@ -357,11 +327,7 @@ static void checkUnusedDefines( } for (auto& [name, decl] : project.DefineDecls) { if (!usedFunctions.contains(name)) { - diags.push_back({ - ast::DiagnosticLevel::Info, - std::format("'{}' is defined but never used", name), - decl->span - }); + diags.push_back(diagnostics::UnusedDefine(decl->span, name)); } } } @@ -381,50 +347,24 @@ static void checkFunctionSignatures( for (const auto& param : params) { if (!seenNames.insert(param.name.text).second) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "duplicate parameter '{}' in {} '{}'", - param.name.text, kind, name), - declSpan - }); + diags.push_back(diagnostics::DuplicateParameter(declSpan, param.name.text, kind, name)); } if (param.defaultValue) { seenDefault = true; } else if (seenDefault) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "required parameter '{}' cannot follow optional parameters in {} '{}'", - param.name.text, kind, name), - declSpan - }); + diags.push_back(diagnostics::RequiredAfterOptionalParameter(declSpan, param.name.text, kind, name)); } if (isExtern && !param.type && !param.defaultValue) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "extern define '{}' parameter '{}' must have a type annotation or a default value", - name, - param.name.text), - declSpan - }); + diags.push_back(diagnostics::ExternParameterMissingType(declSpan, name, param.name.text)); continue; } if (isExtern && !param.type && param.defaultValue) { auto defaultType = project.getType(param.defaultValue.get()); if (!defaultType || *defaultType == ast::Type::Error) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "extern define '{}' parameter '{}' needs an explicit type or an inferrable default", - name, - param.name.text), - param.defaultValue->span - }); + diags.push_back(diagnostics::ExternParameterCannotInfer(param.defaultValue->span, name, param.name.text)); } continue; } @@ -448,10 +388,14 @@ static void checkFunctionSignatures( }; bool defaultCompatible = isDefaultCompatible(*paramType, *defaultType); + const auto parameterEnum = *paramType == ast::Type::Enum + ? project.getEnumType(¶m) : std::optional{}; + const auto defaultEnum = *defaultType == ast::Type::Enum + ? project.getEnumType(param.defaultValue.get()) : std::optional{}; + defaultCompatible = defaultCompatible || areDomainAndEnumCompatible( + *paramType, parameterEnum, *defaultType, defaultEnum); if (defaultCompatible && *paramType == ast::Type::Enum && *defaultType == ast::Type::Enum) { - auto parameterEnum = project.getEnumType(¶m); - auto defaultEnum = project.getEnumType(param.defaultValue.get()); defaultCompatible = !parameterEnum.has_value() || !defaultEnum.has_value() || *parameterEnum == *defaultEnum; } @@ -465,17 +409,10 @@ static void checkFunctionSignatures( } return std::string(typeName(type)); }; - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "default value for parameter '{}' in {} '{}' has type {}, expected {}", - param.name.text, - kind, - name, - typeDisplayName(*defaultType, param.defaultValue.get()), - typeDisplayName(*paramType, ¶m)), - param.defaultValue->span - }); + diags.push_back(diagnostics::DefaultValueTypeMismatch( + param.defaultValue->span, param.name.text, kind, name, + typeDisplayName(*defaultType, param.defaultValue.get()), + typeDisplayName(*paramType, ¶m))); } } }; @@ -487,23 +424,13 @@ static void checkFunctionSignatures( validateParams("extern define", true, name, decl->params, decl->span); if (!decl->returnType) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format("extern define '{}' must declare a return type", name), - decl->span - }); + diags.push_back(diagnostics::ExternMissingReturnType(decl->span, name)); continue; } if (!resolveTypeAnnotation(project, decl->returnType->name.text)) { - diags.push_back({ - ast::DiagnosticLevel::Error, - std::format( - "unknown return type annotation '{}' for extern define '{}'", - decl->returnType->name.text, - name), - decl->span - }); + diags.push_back(diagnostics::ExternUnknownReturnType( + decl->span, decl->returnType->name.text, name)); } } } @@ -515,6 +442,7 @@ std::vector validateDeclarations(ast::Project& project) { checkDuplicateRegionData(project, diags); checkDuplicateEntries(project, diags); checkEntryConditionTypes(project, diags); + checkExitTargets(project, diags); checkRegionReachability(project, diags); checkUnusedDefines(project, diags); checkFunctionSignatures(project, diags); @@ -523,4 +451,36 @@ std::vector validateDeclarations(ast::Project& project) { return diags; } +std::vector structureValidationDiagnostics( + const ast::Project& project, + const std::vector& diagnostics) { + std::vector result; + for (const auto& diagnostic : diagnostics) { + if (!diagnostic.code.empty()) { + std::vector related; + if (diagnostics::IsDuplicateRegionData(diagnostic)) { + for (const auto& [_, region] : project.RegionDecls) { + for (size_t index = 0; index < region->body.data.size(); ++index) { + const auto& duplicate = region->body.data[index]; + if (duplicate.key.span.file != diagnostic.span.file || + duplicate.key.span.start.line != diagnostic.span.start.line || + duplicate.key.span.start.column != diagnostic.span.start.column) { + continue; + } + for (size_t prior = 0; prior < index; ++prior) { + const auto& first = region->body.data[prior]; + if (first.key.text == duplicate.key.text) { + related.push_back({"first definition", first.key.span}); + break; + } + } + } + } + } + result.push_back({diagnostic.code, diagnostic.level, diagnostic.message, diagnostic.span, std::move(related)}); + } + } + return result; +} + } // namespace rls::sema diff --git a/sema/src/validate_declarations.h b/sema/src/validate_declarations.h index ae2ceeb..37878df 100644 --- a/sema/src/validate_declarations.h +++ b/sema/src/validate_declarations.h @@ -3,6 +3,7 @@ #include #include "ast.h" +#include "semantic_index.h" namespace rls::sema { @@ -23,4 +24,9 @@ namespace rls::sema { /// overlap and cross-enum value-name ambiguity warnings std::vector validateDeclarations(ast::Project& project); +/// Convert declaration-validation diagnostics to stable value diagnostics. +std::vector structureValidationDiagnostics( + const ast::Project& project, + const std::vector& diagnostics); + } // namespace rls::sema diff --git a/sema/tests/resolve_types_tests.cpp b/sema/tests/resolve_types_tests.cpp index 77fdd02..3ac6e5e 100644 --- a/sema/tests/resolve_types_tests.cpp +++ b/sema/tests/resolve_types_tests.cpp @@ -30,14 +30,14 @@ static std::string withHostExterns(const std::string& source) { "extern enum Enemy { RE_* }\n" "extern enum Distance { ED_* }\n" "extern enum Trick { RT_* }\n" - "extern enum Logic { LOGIC_* }\n" + "extern enum Event { LOGIC_* }\n" "extern enum Scene { SCENE_* }\n" "extern enum Dungeon { DUNGEON_* }\n" "extern enum Area { RA_* }\n" "extern enum Trial { TK_* }\n" "extern enum Setting { RSK_*, RO_* }\n" "extern enum Region { RR_* }\n" - "extern enum Check { RC_* }\n" + "extern enum Location { RC_* }\n" "extern define has(item: Item) -> Bool\n" "extern define can_use(item: Item) -> Bool\n" "extern define keys(sc: Scene, amount: Int) -> Bool\n" @@ -46,7 +46,7 @@ static std::string withHostExterns(const std::string& source) { "extern define any_age(condition: Condition) -> Bool\n" "extern define spirit_shared(first_region: Region, first_condition: Condition, any_age: Bool = false, second_region: Region = RR_NONE, second_condition: Condition = false, third_region: Region = RR_NONE, third_condition: Condition = false) -> Bool\n" "extern define hearts() -> Int\n" - "extern define check_price(chk: Check = RC_UNKNOWN_CHECK) -> Int\n" + "extern define check_price(chk: Location = RC_UNKNOWN_CHECK) -> Int\n" + source; } @@ -728,7 +728,7 @@ TEST(ResolveTypes, UntypedForwardingParamInheritsEnumIdentity) { " locations { TEST_LOC: forwards_color(Color.RED) }\n" "}\n"); - EXPECT_TRUE(diags.empty()); + ASSERT_TRUE(diags.empty()) << diags.front().message; const auto* decl = project.DefineDecls.at("forwards_color"); EXPECT_EQ(project.getType(&decl->params[0]), Type::Enum); EXPECT_EQ(project.getEnumType(&decl->params[0]), "Color"); @@ -841,7 +841,7 @@ TEST(ResolveTypes, HostCallTooManyArgs) { } TEST(ResolveTypes, HostCallOptionalArgOmitted) { - // check_price() — 0 args, optional Check param. + // check_price() — 0 args, optional Location param. auto [project, diags] = resolveFromSource( "region RR_TEST {\n" " name: \"Test\"\n" @@ -1443,8 +1443,8 @@ TEST(ResolveTypes, HereResolvesToCurrentRegion) { ASSERT_EQ(resolved->size(), 1u); ASSERT_TRUE(std::holds_alternative((*resolved)[0]->node)); EXPECT_EQ(std::get((*resolved)[0]->node).resolvedRegion, "RR_TEST"); - EXPECT_EQ(project.getType((*resolved)[0]), Type::Enum); - EXPECT_EQ(project.getEnumType((*resolved)[0]), "Region"); + EXPECT_EQ(project.getType((*resolved)[0]), Type::Region); + EXPECT_FALSE(project.getEnumType((*resolved)[0])); } TEST(ResolveTypes, HereInExtendRegionResolvesToTargetName) { @@ -1465,8 +1465,8 @@ TEST(ResolveTypes, HereInExtendRegionResolvesToTargetName) { ASSERT_NE(resolved, nullptr); ASSERT_TRUE(std::holds_alternative((*resolved)[0]->node)); EXPECT_EQ(std::get((*resolved)[0]->node).resolvedRegion, "RR_BASE"); - EXPECT_EQ(project.getType((*resolved)[0]), Type::Enum); - EXPECT_EQ(project.getEnumType((*resolved)[0]), "Region"); + EXPECT_EQ(project.getType((*resolved)[0]), Type::Region); + EXPECT_FALSE(project.getEnumType((*resolved)[0])); } TEST(ResolveTypes, HereOutsideRegionIsError) { @@ -1474,7 +1474,7 @@ TEST(ResolveTypes, HereOutsideRegionIsError) { "extern define uses_region(r: Region) -> Bool\n" "define test(): uses_region(here)\n"); EXPECT_EQ(countErrors(diags), 1u); - EXPECT_NE(diags[0].message.find("resolves to enum 'Region'"), std::string::npos); + EXPECT_NE(diags[0].message.find("has type Region"), std::string::npos); } TEST(ResolveTypes, RegionParameterDiagnosticUsesEnumIdentity) { @@ -1487,7 +1487,7 @@ TEST(ResolveTypes, RegionParameterDiagnosticUsesEnumIdentity) { "}\n"); ASSERT_EQ(countErrors(diags), 1u); - EXPECT_NE(diags[0].message.find("expected enum 'Region', got Bool"), std::string::npos); + EXPECT_NE(diags[0].message.find("expected Region, got Bool"), std::string::npos); } // -- Match expression --------------------------------------------------------- @@ -1815,11 +1815,21 @@ TEST(TypeAnnotation, CoreTypes) { EXPECT_EQ(typeFromAnnotation("Callable"), Type::Callable); EXPECT_EQ(typeFromAnnotation("Condition"), Type::Condition); EXPECT_EQ(typeFromAnnotation("Enum"), Type::Enum); + EXPECT_EQ(typeFromAnnotation("Region"), Type::Region); + EXPECT_EQ(typeFromAnnotation("Event"), Type::Event); + EXPECT_EQ(typeFromAnnotation("Location"), Type::Location); EXPECT_FALSE(typeFromAnnotation("Setting").has_value()); - EXPECT_FALSE(typeFromAnnotation("Region").has_value()); EXPECT_FALSE(typeFromAnnotation("Check").has_value()); } +TEST(TypeAnnotation, DomainEnumCompatibilityUsesExactNames) { + EXPECT_TRUE(isDomainEnumCompatible(Type::Region, "Region")); + EXPECT_TRUE(isDomainEnumCompatible(Type::Event, "Event")); + EXPECT_TRUE(isDomainEnumCompatible(Type::Location, "Location")); + EXPECT_FALSE(isDomainEnumCompatible(Type::Event, "Logic")); + EXPECT_FALSE(isDomainEnumCompatible(Type::Location, "Check")); +} + TEST(TypeAnnotation, Unknown) { EXPECT_FALSE(typeFromAnnotation("Foo").has_value()); EXPECT_FALSE(typeFromAnnotation("").has_value()); @@ -1856,6 +1866,73 @@ TEST(ResolveTypes, DefineParamWithProjectEnumAnnotation) { EXPECT_EQ(project.getEnumType(body), "Color"); } +TEST(ResolveTypes, DeclaredRegionsEventsAndLocationsAreTypedValues) { + auto [project, diags] = resolveFromSource( + "region RR_TARGET {\n" + " events { EVENT_OPEN: true }\n" + " locations { RC_CHEST: true }\n" + "}\n" + "define region_value(): RR_TARGET\n" + "define event_value(): EVENT_OPEN\n" + "define location_value(): RC_CHEST\n" + "extern define take_region(value: Region) -> Bool\n" + "extern define take_event(value: Event) -> Bool\n" + "extern define take_location(value: Location) -> Bool\n" + "extern define flag(value: Event) -> Bool\n" + "define use_values(): take_region(RR_TARGET) and take_region(RR_NONE)\n" + " and take_event(EVENT_OPEN) and flag(EVENT_OPEN)\n" + " and take_location(RC_CHEST) and (check_price(RC_CHEST) >= 0)\n"); + + ASSERT_TRUE(diags.empty()) << diags.front().message; + const auto expectValue = [&](std::string_view defineName, Type type) { + const auto* body = project.DefineDecls.at(std::string(defineName))->body.get(); + EXPECT_EQ(project.getType(body), type); + ASSERT_TRUE(std::holds_alternative(body->node)); + EXPECT_EQ(std::get(body->node).kind, IdentifierKind::DeclaredValue); + }; + expectValue("region_value", Type::Region); + expectValue("event_value", Type::Event); + expectValue("location_value", Type::Location); +} + +TEST(ResolveTypes, DeclaredDomainValuesRejectWrongCategories) { + auto [project, diags] = resolveFromSource( + "region RR_TARGET { events { EVENT_OPEN: true } }\n" + "extern define take_event(value: Event) -> Bool\n" + "define wrong(): take_event(RR_TARGET)\n"); + + ASSERT_EQ(countErrors(diags), 1u); + EXPECT_NE(diags[0].message.find("expected Event, got Region"), std::string::npos); +} + +TEST(ResolveTypes, RepeatedLocationDeclarationsShareLocationType) { + auto [project, diags] = resolveFromSource( + "region RR_FIRST { locations { RC_SHARED: true } }\n" + "region RR_SECOND { locations { RC_SHARED: true } }\n" + "define location_value(): RC_SHARED\n"); + + EXPECT_TRUE(diags.empty()); + EXPECT_EQ(project.LocationDecls.at("RC_SHARED").size(), 2u); + EXPECT_EQ(project.getType(project.DefineDecls.at("location_value")->body.get()), + Type::Location); +} + +TEST(ResolveTypes, DeclaredDomainValuesCompareWithLegacyEnumSentinels) { + auto [project, diags] = resolveFromSource( + "region RR_TARGET {\n" + " events { LOGIC_OPEN: true }\n" + " locations { RC_CHEST: true }\n" + "}\n" + "define compare_region(): RR_TARGET != RR_NONE\n" + "define compare_event(): LOGIC_OPEN != LOGIC_NONE\n" + "define compare_location(): RC_CHEST != RC_UNKNOWN_CHECK\n"); + + EXPECT_TRUE(diags.empty()); + EXPECT_EQ(project.getType(project.DefineDecls.at("compare_region")->body.get()), Type::Bool); + EXPECT_EQ(project.getType(project.DefineDecls.at("compare_event")->body.get()), Type::Bool); + EXPECT_EQ(project.getType(project.DefineDecls.at("compare_location")->body.get()), Type::Bool); +} + TEST(ResolveTypes, DefineParamWithDefault) { // define foo(d = ED_CLOSE): d auto [project, diags] = resolveFromSource( diff --git a/sema/tests/sema_tests.cpp b/sema/tests/sema_tests.cpp index d6aed85..d3e564f 100644 --- a/sema/tests/sema_tests.cpp +++ b/sema/tests/sema_tests.cpp @@ -1,3 +1,6 @@ +#include +#include + #include #include "ast.h" @@ -105,6 +108,636 @@ static size_t countWarnings(const std::vector& diags) { return n; } +// == Semantic index =========================================================== + +TEST(AnalysisSnapshotTests, OwnsExplicitSourcesAndDerivedIndexes) { + const auto snapshot = AnalysisSnapshot::Create({ + {"overlay.rls", "define check(): true\ndefine run(): check()\n"}, + }, 42); + ASSERT_TRUE(snapshot); + EXPECT_EQ((*snapshot)->generation(), 42u); + ASSERT_EQ((*snapshot)->documentCount(), 1u); + const auto* sourceText = (*snapshot)->sourceText("overlay.rls"); + ASSERT_NE(sourceText, nullptr); + EXPECT_EQ(sourceText->content(), "define check(): true\ndefine run(): check()\n"); + const auto* sourceIndex = (*snapshot)->sourceIndex("overlay.rls"); + ASSERT_NE(sourceIndex, nullptr); + EXPECT_TRUE(sourceIndex->nameAt({1, 8})); + EXPECT_TRUE((*snapshot)->syntaxAt("overlay.rls", {1, 8})); + EXPECT_TRUE((*snapshot)->nameAt("overlay.rls", {1, 8})); + const auto symbol = (*snapshot)->symbolAt("overlay.rls", {1, 8}); + ASSERT_TRUE(symbol); + EXPECT_TRUE((*snapshot)->declaration(*symbol)); + EXPECT_FALSE((*snapshot)->references(*symbol).empty()); + EXPECT_FALSE((*snapshot)->visibleSymbolsAt("overlay.rls", {2, 15}).empty()); + const auto type = (*snapshot)->typeAt("overlay.rls", {1, 17}); + ASSERT_TRUE(type); + EXPECT_EQ(type->type, Type::Bool); + EXPECT_FALSE((*snapshot)->expectedTypeAt("overlay.rls", {1, 17})); + const auto call = (*snapshot)->callAt("overlay.rls", {2, 15}); + ASSERT_TRUE(call); + EXPECT_TRUE(call->target); + EXPECT_FALSE((*snapshot)->diagnosticsFor("overlay.rls").empty()); + EXPECT_TRUE((*snapshot)->diagnosticsFor("other.rls").empty()); + + EXPECT_FALSE(AnalysisSnapshot::Create( + std::vector{{"bad.rls", std::string("\xC3\x28", 2)}}, 43)); + + const auto overlay = AnalysisSnapshot::Create({ + {"overlay.rls", "define check(): true\n"}, + {"overlay.rls", "define check(): false\n"}, + }, 44); + ASSERT_TRUE(overlay); + EXPECT_EQ((*overlay)->documentCount(), 1u); + EXPECT_EQ((*overlay)->sourceText("overlay.rls")->content(), "define check(): false\n"); +} + +TEST(AnalysisSnapshotTests, HonorsCancellationBeforeWorkStarts) { + std::stop_source cancellation; + cancellation.request_stop(); + + EXPECT_FALSE(AnalysisSnapshot::Create({ + {"cancelled.rls", "define cancelled(): true\n"}, + }, 45, cancellation.get_token())); +} + +TEST(AnalysisSnapshotTests, IsolatesParseFailuresAcrossExplicitSources) { + const auto first = AnalysisSnapshot::Create({ + {"broken.rls", "define broken(\n"}, + {"valid.rls", "define valid(): true\n"}, + }, 100); + ASSERT_TRUE(first); + ASSERT_EQ((*first)->documentCount(), 2u); + EXPECT_FALSE((*first)->diagnosticsFor("broken.rls").empty()); + const auto* validIndex = (*first)->sourceIndex("valid.rls"); + ASSERT_NE(validIndex, nullptr); + EXPECT_TRUE(validIndex->nameAt({1, 8})); + EXPECT_TRUE(std::any_of((*first)->semanticIndex().symbols().begin(), + (*first)->semanticIndex().symbols().end(), [](const SymbolRecord& symbol) { + return symbol.category == SymbolCategory::Define && symbol.displayName == "valid"; + })); + + const auto second = AnalysisSnapshot::Create({ + {"valid.rls", "define valid(): false\n"}, + }, 101); + ASSERT_TRUE(second); + EXPECT_EQ((*first)->generation(), 100u); + EXPECT_EQ((*second)->generation(), 101u); + EXPECT_EQ((*first)->sourceText("valid.rls")->content(), "define valid(): true\n"); + EXPECT_EQ((*second)->sourceText("valid.rls")->content(), "define valid(): false\n"); +} + +TEST(AnalysisSnapshotTests, AnalyzesCompleteNeighborsInMalformedDocument) { + const auto snapshot = AnalysisSnapshot::Create({{ + "partial.rls", + "define before(): true\n" + "define broken(\n" + "define after(): before()\n", + }}, 102); + ASSERT_TRUE(snapshot); + EXPECT_FALSE((*snapshot)->diagnosticsFor("partial.rls").empty()); + + const auto& symbols = (*snapshot)->semanticIndex().symbols(); + const auto hasDefine = [&](std::string_view name) { + return std::any_of(symbols.begin(), symbols.end(), [&](const SymbolRecord& symbol) { + return symbol.category == SymbolCategory::Define + && symbol.displayName == name; + }); + }; + EXPECT_TRUE(hasDefine("before")); + EXPECT_TRUE(hasDefine("after")); + EXPECT_FALSE(hasDefine("broken")); + + const auto before = (*snapshot)->symbolAt("partial.rls", {1, 8}); + const auto reference = (*snapshot)->symbolAt("partial.rls", {3, 18}); + ASSERT_TRUE(before); + ASSERT_TRUE(reference); + EXPECT_EQ(*reference, *before); + EXPECT_FALSE((*snapshot)->symbolAt("partial.rls", {2, 8})); +} + +TEST(AnalysisSnapshotTests, ResolvesOnlyTrustworthyRecoveredCalls) { + const std::string declarations = + "enum Color { RED, BLUE }\n" + "extern define paint(color: Color, enabled: Bool) -> Bool\n"; + const auto endPosition = [](std::string_view source) { + const auto text = SourceText::FromUtf8(std::string(source)); + EXPECT_TRUE(text); + return *text->utf8PositionAtByteOffset(source.size()); + }; + + const std::string validUsage = "define use(): paint(R"; + const auto valid = AnalysisSnapshot::Create({ + {"declarations.rls", declarations}, + {"valid-usage.rls", validUsage}, + }, 103); + ASSERT_TRUE(valid); + const auto validPosition = endPosition(validUsage); + const auto call = (*valid)->callAt("valid-usage.rls", validPosition); + ASSERT_TRUE(call); + ASSERT_TRUE(call->target); + ASSERT_EQ(call->normalizedBindings.size(), 1u); + EXPECT_EQ(call->normalizedBindings[0], 0u); + const auto expected = (*valid)->expectedTypeAt( + "valid-usage.rls", validPosition); + ASSERT_TRUE(expected); + EXPECT_EQ(expected->type, Type::Enum); + EXPECT_EQ(expected->enumName, "Color"); + + const std::string unknownLabelUsage = + "define use(): paint(missing: R"; + const auto unknownLabel = AnalysisSnapshot::Create({ + {"declarations.rls", declarations}, + {"unknown-label.rls", unknownLabelUsage}, + }, 104); + ASSERT_TRUE(unknownLabel); + const auto unknownPosition = endPosition(unknownLabelUsage); + EXPECT_FALSE((*unknownLabel)->callAt("unknown-label.rls", unknownPosition)); + EXPECT_FALSE((*unknownLabel)->expectedTypeAt( + "unknown-label.rls", unknownPosition)); + + const std::string duplicateLabelUsage = + "define use(): paint(color: RED, color: R"; + const auto duplicateLabel = AnalysisSnapshot::Create({ + {"declarations.rls", declarations}, + {"duplicate-label.rls", duplicateLabelUsage}, + }, 105); + ASSERT_TRUE(duplicateLabel); + const auto duplicatePosition = endPosition(duplicateLabelUsage); + EXPECT_FALSE((*duplicateLabel)->callAt( + "duplicate-label.rls", duplicatePosition)); + EXPECT_FALSE((*duplicateLabel)->expectedTypeAt( + "duplicate-label.rls", duplicatePosition)); + + const std::string ambiguousUsage = "define use(): paint(R"; + const auto ambiguous = AnalysisSnapshot::Create({ + {"first.rls", "extern define paint(color: Color) -> Bool\n"}, + {"second.rls", "extern define paint(color: Color) -> Bool\n"}, + {"ambiguous.rls", ambiguousUsage}, + }, 106); + ASSERT_TRUE(ambiguous); + const auto ambiguousPosition = endPosition(ambiguousUsage); + EXPECT_FALSE((*ambiguous)->callAt("ambiguous.rls", ambiguousPosition)); + EXPECT_FALSE((*ambiguous)->expectedTypeAt( + "ambiguous.rls", ambiguousPosition)); +} + +TEST(AnalysisSnapshotTests, ExposesStructuredValidationDiagnostics) { + const auto snapshot = AnalysisSnapshot::Create({ + {"validation.rls", "region RR_TEST { events { EVENT_TEST: \"invalid\" } }\n"}, + }, 102); + ASSERT_TRUE(snapshot); + const auto diagnostics = (*snapshot)->diagnosticsFor("validation.rls"); + const auto diagnostic = std::find_if(diagnostics.begin(), diagnostics.end(), [](const CompilerDiagnostic& candidate) { + return candidate.code == "RLS-V004"; + }); + ASSERT_NE(diagnostic, diagnostics.end()); + EXPECT_EQ(diagnostic->level, DiagnosticLevel::Error); + EXPECT_EQ(diagnostic->span.file, "validation.rls"); + EXPECT_NE(diagnostic->message.find("must be Bool"), std::string::npos); +} + +TEST(AnalysisSnapshotTests, PreservesStructuredDiagnosticActionData) { + const auto snapshot = AnalysisSnapshot::Create({ + {"actions.rls", "define broken(): missing\n"}, + }, 104); + ASSERT_TRUE(snapshot); + const auto diagnostics = (*snapshot)->diagnosticsFor("actions.rls"); + const auto diagnostic = std::find_if(diagnostics.begin(), diagnostics.end(), + [](const CompilerDiagnostic& candidate) { return candidate.code == "RLS-T006"; }); + ASSERT_NE(diagnostic, diagnostics.end()); + ASSERT_TRUE(diagnostic->data.has_value()); + EXPECT_EQ(diagnostic->data->version, 1u); + EXPECT_EQ(diagnostic->data->actionKind, "rls.declareSymbol"); + ASSERT_EQ(diagnostic->data->arguments.size(), 1u); + EXPECT_EQ(diagnostic->data->arguments[0], "missing"); +} + +TEST(AnalysisSnapshotTests, RelatesDuplicateRegionDataToFirstDefinition) { + const auto snapshot = AnalysisSnapshot::Create({ + {"duplicate-data.rls", "region RR_TEST { name: \"First\" name: \"Second\" }\n"}, + }, 103); + ASSERT_TRUE(snapshot); + const auto diagnostics = (*snapshot)->diagnosticsFor("duplicate-data.rls"); + const auto diagnostic = std::find_if(diagnostics.begin(), diagnostics.end(), + [](const CompilerDiagnostic& candidate) { return candidate.code == "RLS-V002"; }); + ASSERT_NE(diagnostic, diagnostics.end()); + ASSERT_EQ(diagnostic->related.size(), 1u); + EXPECT_EQ(diagnostic->related[0].message, "first definition"); + EXPECT_EQ(diagnostic->related[0].span.file, "duplicate-data.rls"); + EXPECT_LT(diagnostic->related[0].span.start.column, diagnostic->span.start.column); +} + +TEST(SemanticIndexTests, RecordsStableValueOnlyDeclarationIdentity) { + SemanticIndex index; + { + Project project; + project.files.push_back(rls::parser::ParseString( + "region RR_TEST { name: \"Test\" events { EVENT_TEST: true } }\n" + "define check(value: Color): true\n" + "extern define external(value: Color) -> Bool\n" + "enum Color { RED }\n" + "extern enum External { VALUE, EXT_* }\n", + "semantic.rls")); + analyze(project); + index = buildSemanticIndex(project); + } + + auto findSymbol = [&](SymbolCategory category, std::string_view displayName) { + for (const auto& symbol : index.symbols()) { + if (symbol.category == category && symbol.displayName == displayName) { + return std::optional(symbol); + } + } + return std::optional{}; + }; + + ASSERT_EQ(index.symbols().size(), 12u); + const auto region = findSymbol(SymbolCategory::Region, "RR_TEST"); + const auto define = findSymbol(SymbolCategory::Define, "check"); + const auto parameter = findSymbol(SymbolCategory::Parameter, "value"); + const auto enumType = findSymbol(SymbolCategory::Enum, "Color"); + const auto pattern = findSymbol(SymbolCategory::ExternEnumPattern, "EXT_*"); + ASSERT_TRUE(region); + ASSERT_TRUE(define); + ASSERT_TRUE(parameter); + ASSERT_TRUE(enumType); + ASSERT_TRUE(pattern); + EXPECT_NE(region->id, define->id); + EXPECT_EQ(parameter->container, define->id); + EXPECT_EQ(define->signature, "define check"); + EXPECT_EQ(enumType->type, Type::Enum); + EXPECT_EQ(enumType->enumName, "Color"); + EXPECT_EQ(pattern->provenance, SymbolProvenance::Pattern); + EXPECT_EQ(pattern->declaration.file, "semantic.rls"); + + const auto declaration = index.declaration(enumType->id); + ASSERT_TRUE(declaration); + EXPECT_EQ(declaration->displayName, "Color"); + const auto occurrences = index.occurrencesFor(enumType->id); + ASSERT_EQ(occurrences.size(), 3u); + EXPECT_EQ(occurrences[0].kind, OccurrenceKind::TypeReference); + EXPECT_EQ(occurrences[1].kind, OccurrenceKind::TypeReference); + EXPECT_EQ(occurrences[2].kind, OccurrenceKind::Declaration); + EXPECT_EQ(occurrences[2].span.file, declaration->selection.file); + EXPECT_EQ(occurrences[2].span.start.line, declaration->selection.start.line); + EXPECT_EQ(occurrences[2].span.start.column, declaration->selection.start.column); + EXPECT_EQ(occurrences[2].span.end.line, declaration->selection.end.line); + EXPECT_EQ(occurrences[2].span.end.column, declaration->selection.end.column); +} + +TEST(SemanticIndexTests, RecordsCallableSignatureMetadata) { + Project project; + project.files.push_back(rls::parser::ParseString( + "enum Color { RED }\n" + "define choose(color: Color = RED, enabled: Bool = true): color\n" + "extern define external(count: Int = 2) -> Color\n", + "signatures.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + const auto findCallable = [&](std::string_view name) { + return std::find_if(index.symbols().begin(), index.symbols().end(), + [&](const SymbolRecord& symbol) { + return (symbol.category == SymbolCategory::Define + || symbol.category == SymbolCategory::ExternDefine) + && symbol.displayName == name; + }); + }; + const auto choose = findCallable("choose"); + const auto external = findCallable("external"); + ASSERT_NE(choose, index.symbols().end()); + ASSERT_NE(external, index.symbols().end()); + EXPECT_EQ(choose->type, Type::Enum); + EXPECT_EQ(choose->enumName, "Color"); + EXPECT_EQ(external->type, Type::Enum); + EXPECT_EQ(external->enumName, "Color"); + + std::vector chooseParameters; + std::vector externalParameters; + for (const auto& symbol : index.symbols()) { + if (symbol.category != SymbolCategory::Parameter || !symbol.container) continue; + if (symbol.container == choose->id) chooseParameters.push_back(&symbol); + if (symbol.container == external->id) externalParameters.push_back(&symbol); + } + ASSERT_EQ(chooseParameters.size(), 2u); + EXPECT_EQ(chooseParameters[0]->defaultValue, "RED"); + EXPECT_TRUE(chooseParameters[0]->optional); + EXPECT_EQ(chooseParameters[1]->defaultValue, "true"); + EXPECT_TRUE(chooseParameters[1]->optional); + ASSERT_EQ(externalParameters.size(), 1u); + EXPECT_EQ(externalParameters[0]->defaultValue, "2"); + EXPECT_TRUE(externalParameters[0]->optional); +} + +TEST(SemanticIndexTests, RecordsRegionExtensionTargetRelations) { + Project project; + project.files.push_back(rls::parser::ParseString( + "region RR_BASE { name: \"Base\" }\n" + "extend region RR_BASE { events { EVENT_BASE: true } }\n" + "extend region RR_UNKNOWN { events { EVENT_UNKNOWN: true } }\n", + "extensions.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + std::optional base; + std::vector extensions; + for (const auto& symbol : index.symbols()) { + if (symbol.category == SymbolCategory::Region && symbol.displayName == "RR_BASE") base = symbol; + if (symbol.category == SymbolCategory::RegionExtension) extensions.push_back(symbol); + } + ASSERT_TRUE(base); + ASSERT_EQ(extensions.size(), 2u); + EXPECT_EQ(extensions[0].container, base->id); + EXPECT_FALSE(extensions[1].container); + + const auto validTarget = index.occurrenceAt("extensions.rls", {2, 15}); + ASSERT_TRUE(validTarget); + EXPECT_EQ(validTarget->kind, OccurrenceKind::ExtensionTarget); + EXPECT_EQ(validTarget->symbol, base->id); + const auto unknownTarget = index.occurrenceAt("extensions.rls", {3, 15}); + ASSERT_TRUE(unknownTarget); + EXPECT_EQ(unknownTarget->kind, OccurrenceKind::Unresolved); + EXPECT_FALSE(unknownTarget->symbol); +} + +TEST(SemanticIndexTests, RecordsDuplicateDeclarationDiagnostics) { + SemanticIndex index; + { + Project project; + project.files.push_back(rls::parser::ParseString( + "region RR_DUP { name: \"First\" }\n", "first.rls")); + project.files.push_back(rls::parser::ParseString( + "region RR_DUP { name: \"Second\" }\n", "second.rls")); + analyze(project); + index = buildSemanticIndex(project); + } + + ASSERT_EQ(index.diagnostics().size(), 1u); + const auto& diagnostic = index.diagnostics()[0]; + EXPECT_EQ(diagnostic.code, "RLS-S001"); + EXPECT_EQ(diagnostic.level, DiagnosticLevel::Error); + EXPECT_EQ(diagnostic.message, "duplicate region 'RR_DUP'"); + EXPECT_EQ(diagnostic.span.file, "second.rls"); + ASSERT_EQ(diagnostic.related.size(), 1u); + EXPECT_EQ(diagnostic.related[0].message, "first declaration"); + EXPECT_EQ(diagnostic.related[0].span.file, "first.rls"); +} + +TEST(SemanticIndexTests, SeparatesParameterScopesAndKeepsUnknownOccurrences) { + Project project; + project.files.push_back(rls::parser::ParseString( + "define first(value: Bool): value\n" + "define second(value: Bool): value and unknown\n", + "scopes.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + std::optional first; + std::optional second; + std::vector parameters; + for (const auto& symbol : index.symbols()) { + if (symbol.category == SymbolCategory::Define && symbol.displayName == "first") first = symbol.id; + if (symbol.category == SymbolCategory::Define && symbol.displayName == "second") second = symbol.id; + if (symbol.category == SymbolCategory::Parameter && symbol.displayName == "value") parameters.push_back(symbol); + } + ASSERT_TRUE(first); + ASSERT_TRUE(second); + ASSERT_EQ(parameters.size(), 2u); + const auto firstParameter = parameters[0].container == first ? parameters[0] : parameters[1]; + const auto secondParameter = parameters[0].container == second ? parameters[0] : parameters[1]; + EXPECT_EQ(firstParameter.container, first); + EXPECT_EQ(secondParameter.container, second); + + const auto firstOccurrences = index.occurrencesFor(firstParameter.id); + const auto secondOccurrences = index.occurrencesFor(secondParameter.id); + ASSERT_EQ(firstOccurrences.size(), 2u); + ASSERT_EQ(secondOccurrences.size(), 2u); + EXPECT_EQ(firstOccurrences[1].kind, OccurrenceKind::Reference); + EXPECT_EQ(secondOccurrences[1].kind, OccurrenceKind::Reference); + const auto firstUse = index.occurrenceAt("scopes.rls", {1, 28}); + ASSERT_TRUE(firstUse); + EXPECT_EQ(firstUse->symbol, firstParameter.id); + const auto secondUse = index.occurrenceAt("scopes.rls", {2, 29}); + ASSERT_TRUE(secondUse); + EXPECT_EQ(secondUse->symbol, secondParameter.id); + const auto unknown = index.occurrenceAt("scopes.rls", {2, 39}); + ASSERT_TRUE(unknown); + EXPECT_EQ(unknown->kind, OccurrenceKind::Unresolved); + EXPECT_FALSE(unknown->symbol); +} + +TEST(SemanticIndexTests, CapturesCrossFileExternsAndAmbiguousEnumValues) { + Project project; + project.files.push_back(rls::parser::ParseString("extern define host() -> Bool\n", "host.rls")); + project.files.push_back(rls::parser::ParseString("enum Alpha { SHARED }\n", "alpha.rls")); + project.files.push_back(rls::parser::ParseString("enum Beta { SHARED }\n", "beta.rls")); + project.files.push_back(rls::parser::ParseString( + "define call(): host()\n" + "define ambiguous(): SHARED\n", "use.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + std::optional host; + for (const auto& symbol : index.symbols()) { + if (symbol.category == SymbolCategory::ExternDefine && symbol.displayName == "host") host = symbol; + } + ASSERT_TRUE(host); + EXPECT_EQ(host->provenance, SymbolProvenance::Extern); + const auto hostCall = index.occurrenceAt("use.rls", {1, 16}); + ASSERT_TRUE(hostCall); + EXPECT_EQ(hostCall->kind, OccurrenceKind::Call); + EXPECT_EQ(hostCall->symbol, host->id); + const auto ambiguous = index.occurrenceAt("use.rls", {2, 21}); + ASSERT_TRUE(ambiguous); + EXPECT_EQ(ambiguous->kind, OccurrenceKind::Unresolved); + EXPECT_FALSE(ambiguous->symbol); +} + +TEST(SemanticIndexTests, RecordsConcreteValuesObservedThroughExternPatterns) { + Project project; + project.files.push_back(rls::parser::ParseString( + "extern enum Item { RG_EXPLICIT, RG_* }\n" + "define first(): RG_HOOKSHOT\n" + "define repeated(): RG_HOOKSHOT\n" + "define qualified(): Item.RG_BOW\n" + "define explicit(): RG_EXPLICIT\n", + "observed-enum-values.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + ASSERT_EQ(index.observedEnumValues().size(), 2u); + EXPECT_EQ(index.observedEnumValues()[0].enumName, "Item"); + EXPECT_EQ(index.observedEnumValues()[0].displayName, "RG_BOW"); + EXPECT_EQ(index.observedEnumValues()[1].enumName, "Item"); + EXPECT_EQ(index.observedEnumValues()[1].displayName, "RG_HOOKSHOT"); + + const auto bare = index.occurrenceAt("observed-enum-values.rls", {2, 18}); + ASSERT_TRUE(bare); + ASSERT_TRUE(bare->symbol); + const auto barePattern = index.declaration(*bare->symbol); + ASSERT_TRUE(barePattern); + EXPECT_EQ(barePattern->category, SymbolCategory::ExternEnumPattern); + EXPECT_EQ(barePattern->displayName, "RG_*"); + EXPECT_EQ(bare->kind, OccurrenceKind::Reference); + + const auto qualified = index.occurrenceAt( + "observed-enum-values.rls", {4, 27}); + ASSERT_TRUE(qualified); + ASSERT_TRUE(qualified->symbol); + EXPECT_EQ(qualified->symbol, bare->symbol); + EXPECT_EQ(qualified->kind, OccurrenceKind::MemberAccess); +} + +TEST(SemanticIndexTests, LeavesOverlappingExternPatternsUnresolved) { + Project project; + project.files.push_back(rls::parser::ParseString( + "extern enum Item { RG_*, *_HOOKSHOT }\n" + "define use(): RG_HOOKSHOT\n", + "ambiguous-pattern.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + const auto occurrence = index.occurrenceAt("ambiguous-pattern.rls", {2, 16}); + ASSERT_TRUE(occurrence); + EXPECT_EQ(occurrence->kind, OccurrenceKind::Unresolved); + EXPECT_FALSE(occurrence->symbol); +} + +TEST(SemanticIndexTests, RecordsOperatorAndTernaryExpectedTypes) { + Project project; + project.files.push_back(rls::parser::ParseString( + "define check(flag: Bool, count: Int): flag ? count + 1 : count\n", + "expected.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + const auto condition = index.expectedTypeAt("expected.rls", {1, 39}); + ASSERT_TRUE(condition); + EXPECT_EQ(condition->type, Type::Bool); + const auto arithmeticParameter = index.expectedTypeAt("expected.rls", {1, 46}); + ASSERT_TRUE(arithmeticParameter); + EXPECT_EQ(arithmeticParameter->type, Type::Int); + const auto arithmeticLiteral = index.expectedTypeAt("expected.rls", {1, 54}); + ASSERT_TRUE(arithmeticLiteral); + EXPECT_EQ(arithmeticLiteral->type, Type::Int); +} + +TEST(SemanticIndexTests, CopiesResolvedTypesCallsAndMemberOccurrences) { + Project project; + project.files.push_back(rls::parser::ParseString( + "enum Color { RED }\n" + "define identity(value: Color): value\n" + "define check(): identity(RED)\n", + "calls.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + auto findSymbol = [&](SymbolCategory category, std::string_view displayName) { + for (const auto& symbol : index.symbols()) { + if (symbol.category == category && symbol.displayName == displayName) { + return std::optional(symbol); + } + } + return std::optional{}; + }; + + const auto identity = findSymbol(SymbolCategory::Define, "identity"); + const auto value = findSymbol(SymbolCategory::Parameter, "value"); + const auto color = findSymbol(SymbolCategory::Enum, "Color"); + const auto red = findSymbol(SymbolCategory::EnumMember, "RED"); + ASSERT_TRUE(identity); + ASSERT_TRUE(value); + ASSERT_TRUE(color); + ASSERT_TRUE(red); + ASSERT_EQ(index.calls().size(), 1u); + const auto& call = index.calls()[0]; + EXPECT_EQ(call.target, identity->id); + ASSERT_EQ(call.argumentRanges.size(), 1u); + ASSERT_EQ(call.normalizedBindings.size(), 1u); + EXPECT_EQ(call.normalizedBindings[0], 0u); + const auto callAt = index.callAt("calls.rls", {3, 17}); + ASSERT_TRUE(callAt); + EXPECT_EQ(callAt->target, identity->id); + const auto occurrenceAt = index.occurrenceAt("calls.rls", {3, 17}); + ASSERT_TRUE(occurrenceAt); + EXPECT_EQ(occurrenceAt->symbol, identity->id); + EXPECT_EQ(occurrenceAt->kind, OccurrenceKind::Call); + const auto typeAt = index.typeAt("calls.rls", {3, 26}); + ASSERT_TRUE(typeAt); + EXPECT_EQ(typeAt->type, Type::Enum); + EXPECT_EQ(typeAt->enumName, "Color"); + const auto expectedTypeAt = index.expectedTypeAt("calls.rls", {3, 26}); + ASSERT_TRUE(expectedTypeAt); + EXPECT_EQ(expectedTypeAt->type, Type::Enum); + EXPECT_EQ(expectedTypeAt->enumName, "Color"); + const auto typeReference = index.occurrenceAt("calls.rls", {2, 24}); + ASSERT_TRUE(typeReference); + EXPECT_EQ(typeReference->kind, OccurrenceKind::TypeReference); + EXPECT_EQ(typeReference->symbol, color->id); + + ASSERT_FALSE(index.types().empty()); + EXPECT_TRUE(std::any_of(index.types().begin(), index.types().end(), [](const TypeRecord& record) { + return record.type == Type::Enum && record.enumName == "Color"; + })); + const auto memberOccurrences = index.occurrencesFor(red->id); + ASSERT_EQ(memberOccurrences.size(), 2u); + EXPECT_EQ(memberOccurrences[0].kind, OccurrenceKind::Declaration); + EXPECT_EQ(memberOccurrences[1].kind, OccurrenceKind::Reference); + EXPECT_LT(memberOccurrences[0].span.start.line, memberOccurrences[1].span.start.line); + + const auto visibleInIdentity = index.visibleSymbolsAt("calls.rls", {2, 17}); + EXPECT_TRUE(std::find(visibleInIdentity.begin(), visibleInIdentity.end(), identity->id) != visibleInIdentity.end()); + EXPECT_TRUE(std::find(visibleInIdentity.begin(), visibleInIdentity.end(), value->id) != visibleInIdentity.end()); + const auto visibleInCheck = index.visibleSymbolsAt("calls.rls", {3, 17}); + EXPECT_TRUE(std::find(visibleInCheck.begin(), visibleInCheck.end(), identity->id) != visibleInCheck.end()); + EXPECT_TRUE(std::find(visibleInCheck.begin(), visibleInCheck.end(), value->id) == visibleInCheck.end()); +} + +TEST(SemanticIndexTests, TypesAndLinksDeclaredDomainValues) { + Project project; + project.files.push_back(rls::parser::ParseString( + "region RR_TARGET {\n" + " events { EVENT_OPEN: true }\n" + " locations { RC_CHEST: true }\n" + "}\n" + "define region_value(): RR_TARGET\n" + "define event_value(): EVENT_OPEN\n" + "define location_value(): RC_CHEST\n", + "domain-values.rls")); + analyze(project); + const auto index = buildSemanticIndex(project); + + const auto find = [&](SymbolCategory category, std::string_view name) { + return std::find_if(index.symbols().begin(), index.symbols().end(), + [&](const SymbolRecord& symbol) { + return symbol.category == category && symbol.displayName == name; + }); + }; + const auto region = find(SymbolCategory::Region, "RR_TARGET"); + const auto event = find(SymbolCategory::SectionEntry, "EVENT_OPEN"); + const auto location = find(SymbolCategory::SectionEntry, "RC_CHEST"); + ASSERT_NE(region, index.symbols().end()); + ASSERT_NE(event, index.symbols().end()); + ASSERT_NE(location, index.symbols().end()); + EXPECT_EQ(region->type, Type::Region); + EXPECT_EQ(event->type, Type::Event); + EXPECT_EQ(location->type, Type::Location); + + const auto regionUse = index.occurrenceAt("domain-values.rls", {5, 25}); + const auto eventUse = index.occurrenceAt("domain-values.rls", {6, 24}); + const auto locationUse = index.occurrenceAt("domain-values.rls", {7, 27}); + ASSERT_TRUE(regionUse && eventUse && locationUse); + EXPECT_EQ(regionUse->symbol, region->id); + EXPECT_EQ(eventUse->symbol, event->id); + EXPECT_EQ(locationUse->symbol, location->id); + + const auto visible = index.visibleSymbolsAt("domain-values.rls", {5, 25}); + EXPECT_NE(std::find(visible.begin(), visible.end(), region->id), visible.end()); + EXPECT_NE(std::find(visible.begin(), visible.end(), event->id), visible.end()); + EXPECT_NE(std::find(visible.begin(), visible.end(), location->id), visible.end()); +} + // == Empty project ============================================================ TEST(CollectDeclarations, EmptyProject) { diff --git a/sema/tests/validate_declarations_tests.cpp b/sema/tests/validate_declarations_tests.cpp index f606fdd..540fa1d 100644 --- a/sema/tests/validate_declarations_tests.cpp +++ b/sema/tests/validate_declarations_tests.cpp @@ -28,11 +28,11 @@ static std::pair> validateFromSource( const std::string hostRegionEnum = source.find("enum Region") == std::string::npos ? "extern enum Region { RR_* }\n" : ""; - const std::string hostCheckEnum = source.find("enum Check") == std::string::npos - ? "extern enum Check { RC_* }\n" + const std::string hostLocationEnum = source.find("enum Location") == std::string::npos + ? "extern enum Location { RC_* }\n" : ""; project.files.push_back(rls::parser::ParseString( - hostItemEnum + hostSettingEnum + hostRegionEnum + hostCheckEnum + hostItemEnum + hostSettingEnum + hostRegionEnum + hostLocationEnum + "extern enum Distance { ED_* }\n" + "extern define setting(key: Setting) -> Int\n" + source)); @@ -129,6 +129,9 @@ TEST(ValidateDeclarations, ExtendRegionMultipleExtendsOnSameValidRegion) { " name: \"Foyer\"\n" " scene: SCENE_SPIRIT_TEMPLE\n" "}\n" + "region RR_OTHER {\n" + " name: \"Other\"\n" + "}\n" "extend region RR_FOYER {\n" " locations { RC_POT: always }\n" "}\n" @@ -336,6 +339,42 @@ TEST(ValidateDeclarations, EntryConditionBool_Ok) { EXPECT_EQ(countErrors(diags), 0u); } +// == Exit targets have region declarations =================================== + +TEST(ValidateDeclarations, ExitTargetMissingRegionWarnsAtExitKey) { + auto [project, diags] = validateFromSource( + "region RR_ROOT {\n" + " exits {\n" + " RR_MISSING: always\n" + " }\n" + "}\n"); + + ASSERT_EQ(countWarnings(diags), 1u); + const auto& diagnostic = diags[0]; + EXPECT_EQ(diagnostic.code, "RLS-V020"); + EXPECT_EQ(diagnostic.level, DiagnosticLevel::Warning); + EXPECT_EQ(diagnostic.message, + "exit targets region 'RR_MISSING' without a region declaration"); + const auto& exit = project.RegionDecls.at("RR_ROOT")->body.sections[0].entries[0]; + EXPECT_EQ(diagnostic.span.file, exit.name.span.file); + EXPECT_EQ(diagnostic.span.start.line, exit.name.span.start.line); + EXPECT_EQ(diagnostic.span.start.column, exit.name.span.start.column); + EXPECT_EQ(diagnostic.span.end.line, exit.name.span.end.line); + EXPECT_EQ(diagnostic.span.end.column, exit.name.span.end.column); +} + +TEST(ValidateDeclarations, ExitTargetMissingRegionInExtensionWarns) { + auto [project, diags] = validateFromSource( + "region RR_ROOT {}\n" + "extend region RR_ROOT {\n" + " exits { RR_MISSING: always }\n" + "}\n"); + + ASSERT_EQ(countWarnings(diags), 1u); + EXPECT_EQ(diags[0].code, "RLS-V020"); + EXPECT_NE(diags[0].message.find("RR_MISSING"), std::string::npos); +} + TEST(ValidateDeclarations, EntryConditionInt_Ok) { auto [project, diags] = validateFromSource( "region RR_FOYER {\n" @@ -637,6 +676,15 @@ TEST(ValidateDeclarations, ExternDefineTypedDefaults_Ok) { EXPECT_EQ(countErrors(diags), 0u); } +TEST(ValidateDeclarations, DomainTypedLegacyEnumDefaults_Ok) { + auto [project, diags] = validateFromSource( + "extern enum Event { LOGIC_* }\n" + "extern define values(reg: Region = RR_NONE, event: Event = LOGIC_NONE, " + "location: Location = RC_UNKNOWN_CHECK) -> Bool\n"); + + EXPECT_EQ(countErrors(diags), 0u); +} + TEST(ValidateDeclarations, ExternDefineTypedDefaultMismatch) { auto [project, diags] = validateFromSource( "extern define can_hit_switch(distance: Distance = false) -> Bool\n" diff --git a/tooling/syntax-fixtures/representative.lexical.json b/tooling/syntax-fixtures/representative.lexical.json new file mode 100644 index 0000000..8db7642 --- /dev/null +++ b/tooling/syntax-fixtures/representative.lexical.json @@ -0,0 +1,22 @@ +{ + "fixture": "representative.rls", + "categories": { + "comments": ["# A comment with delimiters: { [ (", "# Keep this incomplete source editable while typing."], + "strings": ["\"Sample \\\"Region\\\"\""], + "declarationKeywords": ["enum", "extern", "define", "region", "extend"], + "controlKeywords": ["match", "and", "never", "always"], + "sectionKeywords": ["events", "exits", "locations"], + "booleanLiterals": ["never", "always"], + "numericLiterals": ["3", "2"], + "declarationNames": ["Color", "Item", "has", "can_enter", "RR_SAMPLE", "unfinished"], + "parameterNames": ["item", "distance", "value"], + "typeNames": ["Item", "bool", "int"], + "qualifiedNames": ["Scene.OVERWORLD"], + "operators": ["=", "->", ">=", "?"], + "ternaryOperators": ["?", ":"], + "punctuation": ["{", "}", "(", ")", "[", "]", ":", ","] + }, + "incompleteConstructs": [ + "An unterminated match expression must retain the scopes parsed before the missing closing brace." + ] +} \ No newline at end of file diff --git a/tooling/syntax-fixtures/representative.rls b/tooling/syntax-fixtures/representative.rls new file mode 100644 index 0000000..34e7551 --- /dev/null +++ b/tooling/syntax-fixtures/representative.rls @@ -0,0 +1,35 @@ +# A comment with delimiters: { [ ( +enum Color { RED, GREEN = 3 } +extern enum Item { RG_HOOKSHOT, RG_*, *_KEY } + +extern define has(item: Item) -> bool +define can_enter(item: Item, distance = ED_CLOSE): + match item { + RG_HOOKSHOT: has(item) and distance >= 2 + _: never + } + +region RR_SAMPLE { + name: "Sample \"Region\"" + scene: Scene.OVERWORLD + areas: [RA_SAMPLE, RA_OTHER] + fallback: has(RG_HOOKSHOT) ? always : never + + events { + LOGIC_READY: can_enter(item: RG_HOOKSHOT) + } + + exits { + RR_NEXT: + always + } +} + +extend region RR_SAMPLE { + locations { + RC_SAMPLE: can_enter(RG_HOOKSHOT) + } +} + +# Keep this incomplete source editable while typing. +define unfinished(value: int): match value { \ No newline at end of file diff --git a/tooling/textmate/.gitignore b/tooling/textmate/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/tooling/textmate/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/tooling/textmate/GRAMMAR-CHANGE-CHECKLIST.md b/tooling/textmate/GRAMMAR-CHANGE-CHECKLIST.md new file mode 100644 index 0000000..f0fb086 --- /dev/null +++ b/tooling/textmate/GRAMMAR-CHANGE-CHECKLIST.md @@ -0,0 +1,9 @@ +# Editor Grammar Change Checklist + +When changing the PEGTL grammar in `parser/src/grammar.h`, review the editor artifacts whenever the change affects a keyword, declaration, expression form, comment form, delimiter, literal, or identifier syntax. + +- Add or update an example in `tooling/syntax-fixtures/` and its lexical expectations. +- Update `editors/vscode/syntaxes/rls.tmLanguage.json` and verify the changed source in VS Code's scope inspector. +- Update the Tree-sitter grammar and highlight, fold, and indent queries when that grammar is present. +- Add malformed or incomplete input when the change introduces an open construct. +- Run the grammar-specific tests before merging. \ No newline at end of file diff --git a/tooling/textmate/package-lock.json b/tooling/textmate/package-lock.json new file mode 100644 index 0000000..b4cfd6f --- /dev/null +++ b/tooling/textmate/package-lock.json @@ -0,0 +1,31 @@ +{ + "name": "rls-textmate-tests", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "rls-textmate-tests", + "version": "0.1.0", + "devDependencies": { + "vscode-oniguruma": "1.7.0", + "vscode-textmate": "5.4.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-5.4.0.tgz", + "integrity": "sha512-c0Q4zYZkcLizeYJ3hNyaVUM2AA8KDhNCA3JvXY8CeZSJuBdAy3bAvSbv46RClC4P3dSO9BdwhnKEx2zOo6vP/w==", + "dev": true + } + } +} diff --git a/tooling/textmate/package.json b/tooling/textmate/package.json new file mode 100644 index 0000000..5fe52cd --- /dev/null +++ b/tooling/textmate/package.json @@ -0,0 +1,17 @@ +{ + "name": "rls-textmate-tests", + "private": true, + "version": "0.1.0", + "description": "TextMate scope snapshot tests for Rando Logic Script.", + "engines": { + "node": ">=10" + }, + "scripts": { + "test": "node test-scopes.js", + "update-snapshots": "node test-scopes.js --update" + }, + "devDependencies": { + "vscode-oniguruma": "1.7.0", + "vscode-textmate": "5.4.0" + } +} diff --git a/tooling/textmate/snapshots/representative.scopes.json b/tooling/textmate/snapshots/representative.scopes.json new file mode 100644 index 0000000..f36a47b --- /dev/null +++ b/tooling/textmate/snapshots/representative.scopes.json @@ -0,0 +1,1823 @@ +[ + { + "line": 1, + "text": "# A comment with delimiters: { [ (", + "tokens": [ + { + "start": 0, + "end": 34, + "scopes": [ + "source.rls", + "comment.line.number-sign.rls" + ] + } + ] + }, + { + "line": 2, + "text": "enum Color { RED, GREEN = 3 }", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "storage.type.rls" + ] + }, + { + "start": 4, + "end": 5, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 5, + "end": 10, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "entity.name.type.enum.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 11, + "end": 12, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 12, + "end": 13, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 13, + "end": 16, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "variable.other.enummember.rls" + ] + }, + { + "start": 16, + "end": 17, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 18, + "end": 23, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "variable.other.enummember.rls" + ] + }, + { + "start": 23, + "end": 24, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 24, + "end": 25, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "keyword.operator.rls" + ] + }, + { + "start": 25, + "end": 26, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 26, + "end": 27, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "constant.numeric.integer.rls" + ] + }, + { + "start": 27, + "end": 28, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 28, + "end": 29, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 3, + "text": "extern enum Item { RG_HOOKSHOT, RG_*, *_KEY }", + "tokens": [ + { + "start": 0, + "end": 6, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "storage.modifier.rls" + ] + }, + { + "start": 6, + "end": 7, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 7, + "end": 11, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "storage.type.rls" + ] + }, + { + "start": 11, + "end": 12, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 12, + "end": 16, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "entity.name.type.enum.rls" + ] + }, + { + "start": 16, + "end": 17, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 18, + "end": 19, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 19, + "end": 30, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "variable.other.enummember.rls" + ] + }, + { + "start": 30, + "end": 31, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 31, + "end": 32, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 32, + "end": 35, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "variable.other.enummember.rls" + ] + }, + { + "start": 35, + "end": 36, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "keyword.operator.rls" + ] + }, + { + "start": 36, + "end": 37, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 37, + "end": 38, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 38, + "end": 39, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "keyword.operator.rls" + ] + }, + { + "start": 39, + "end": 43, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "variable.other.enummember.rls" + ] + }, + { + "start": 43, + "end": 44, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls" + ] + }, + { + "start": 44, + "end": 45, + "scopes": [ + "source.rls", + "meta.declaration.enum.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 4, + "text": "", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 5, + "text": "extern define has(item: Item) -> bool", + "tokens": [ + { + "start": 0, + "end": 6, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 6, + "end": 7, + "scopes": [ + "source.rls" + ] + }, + { + "start": 7, + "end": 13, + "scopes": [ + "source.rls", + "storage.type.function.rls" + ] + }, + { + "start": 13, + "end": 14, + "scopes": [ + "source.rls" + ] + }, + { + "start": 14, + "end": 17, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 18, + "end": 22, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 22, + "end": 24, + "scopes": [ + "source.rls" + ] + }, + { + "start": 24, + "end": 28, + "scopes": [ + "source.rls", + "entity.name.type.rls" + ] + }, + { + "start": 28, + "end": 29, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + }, + { + "start": 29, + "end": 30, + "scopes": [ + "source.rls" + ] + }, + { + "start": 30, + "end": 32, + "scopes": [ + "source.rls", + "keyword.operator.rls" + ] + }, + { + "start": 32, + "end": 38, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 6, + "text": "define can_enter(item: Item, distance = ED_CLOSE):", + "tokens": [ + { + "start": 0, + "end": 6, + "scopes": [ + "source.rls", + "storage.type.function.rls" + ] + }, + { + "start": 6, + "end": 7, + "scopes": [ + "source.rls" + ] + }, + { + "start": 7, + "end": 16, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 16, + "end": 17, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 17, + "end": 21, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 21, + "end": 23, + "scopes": [ + "source.rls" + ] + }, + { + "start": 23, + "end": 27, + "scopes": [ + "source.rls", + "entity.name.type.rls" + ] + }, + { + "start": 27, + "end": 28, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 28, + "end": 29, + "scopes": [ + "source.rls" + ] + }, + { + "start": 29, + "end": 37, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 37, + "end": 38, + "scopes": [ + "source.rls" + ] + }, + { + "start": 38, + "end": 39, + "scopes": [ + "source.rls", + "keyword.operator.rls" + ] + }, + { + "start": 39, + "end": 48, + "scopes": [ + "source.rls" + ] + }, + { + "start": 48, + "end": 49, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + }, + { + "start": 49, + "end": 50, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + } + ] + }, + { + "line": 7, + "text": " match item {", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 9, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 9, + "end": 15, + "scopes": [ + "source.rls" + ] + }, + { + "start": 15, + "end": 16, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + }, + { + "line": 8, + "text": " RG_HOOKSHOT: has(item) and distance >= 2", + "tokens": [ + { + "start": 0, + "end": 8, + "scopes": [ + "source.rls" + ] + }, + { + "start": 8, + "end": 19, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 19, + "end": 20, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 20, + "end": 21, + "scopes": [ + "source.rls" + ] + }, + { + "start": 21, + "end": 24, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 24, + "end": 25, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 25, + "end": 29, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 29, + "end": 30, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + }, + { + "start": 30, + "end": 31, + "scopes": [ + "source.rls" + ] + }, + { + "start": 31, + "end": 34, + "scopes": [ + "source.rls", + "keyword.operator.word.rls" + ] + }, + { + "start": 34, + "end": 44, + "scopes": [ + "source.rls" + ] + }, + { + "start": 44, + "end": 46, + "scopes": [ + "source.rls", + "keyword.operator.rls" + ] + }, + { + "start": 46, + "end": 47, + "scopes": [ + "source.rls" + ] + }, + { + "start": 47, + "end": 48, + "scopes": [ + "source.rls", + "constant.numeric.integer.rls" + ] + } + ] + }, + { + "line": 9, + "text": " _: never", + "tokens": [ + { + "start": 0, + "end": 8, + "scopes": [ + "source.rls" + ] + }, + { + "start": 8, + "end": 9, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 9, + "end": 10, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls" + ] + }, + { + "start": 11, + "end": 16, + "scopes": [ + "source.rls", + "constant.language.boolean.rls" + ] + } + ] + }, + { + "line": 10, + "text": " }", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 5, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 11, + "text": "", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 12, + "text": "region RR_SAMPLE {", + "tokens": [ + { + "start": 0, + "end": 6, + "scopes": [ + "source.rls", + "storage.type.rls" + ] + }, + { + "start": 6, + "end": 7, + "scopes": [ + "source.rls" + ] + }, + { + "start": 7, + "end": 16, + "scopes": [ + "source.rls", + "entity.name.type.rls" + ] + }, + { + "start": 16, + "end": 17, + "scopes": [ + "source.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + }, + { + "line": 13, + "text": " name: \"Sample \\\"Region\\\"\"", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 8, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 8, + "end": 9, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 9, + "end": 10, + "scopes": [ + "source.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls", + "string.quoted.double.rls" + ] + }, + { + "start": 11, + "end": 18, + "scopes": [ + "source.rls", + "string.quoted.double.rls" + ] + }, + { + "start": 18, + "end": 20, + "scopes": [ + "source.rls", + "string.quoted.double.rls", + "constant.character.escape.rls" + ] + }, + { + "start": 20, + "end": 26, + "scopes": [ + "source.rls", + "string.quoted.double.rls" + ] + }, + { + "start": 26, + "end": 28, + "scopes": [ + "source.rls", + "string.quoted.double.rls", + "constant.character.escape.rls" + ] + }, + { + "start": 28, + "end": 29, + "scopes": [ + "source.rls", + "string.quoted.double.rls" + ] + } + ] + }, + { + "line": 14, + "text": " scene: Scene.OVERWORLD", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 9, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 9, + "end": 10, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls" + ] + }, + { + "start": 11, + "end": 16, + "scopes": [ + "source.rls", + "entity.name.type.enum.rls" + ] + }, + { + "start": 16, + "end": 17, + "scopes": [ + "source.rls", + "punctuation.accessor.rls" + ] + }, + { + "start": 17, + "end": 26, + "scopes": [ + "source.rls", + "variable.other.enummember.rls" + ] + } + ] + }, + { + "line": 15, + "text": " areas: [RA_SAMPLE, RA_OTHER]", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 9, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 9, + "end": 10, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls" + ] + }, + { + "start": 11, + "end": 12, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 12, + "end": 21, + "scopes": [ + "source.rls" + ] + }, + { + "start": 21, + "end": 22, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 22, + "end": 31, + "scopes": [ + "source.rls" + ] + }, + { + "start": 31, + "end": 32, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 16, + "text": " fallback: has(RG_HOOKSHOT) ? always : never", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 12, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 12, + "end": 13, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 13, + "end": 14, + "scopes": [ + "source.rls" + ] + }, + { + "start": 14, + "end": 17, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 18, + "end": 29, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 29, + "end": 30, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + }, + { + "start": 30, + "end": 31, + "scopes": [ + "source.rls" + ] + }, + { + "start": 31, + "end": 32, + "scopes": [ + "source.rls", + "meta.expression.ternary.rls", + "keyword.operator.ternary.rls" + ] + }, + { + "start": 32, + "end": 33, + "scopes": [ + "source.rls", + "meta.expression.ternary.rls" + ] + }, + { + "start": 33, + "end": 39, + "scopes": [ + "source.rls", + "meta.expression.ternary.rls", + "constant.language.boolean.rls" + ] + }, + { + "start": 39, + "end": 40, + "scopes": [ + "source.rls", + "meta.expression.ternary.rls" + ] + }, + { + "start": 40, + "end": 41, + "scopes": [ + "source.rls", + "meta.expression.ternary.rls", + "keyword.operator.ternary.rls" + ] + }, + { + "start": 41, + "end": 42, + "scopes": [ + "source.rls" + ] + }, + { + "start": 42, + "end": 47, + "scopes": [ + "source.rls", + "constant.language.boolean.rls" + ] + } + ] + }, + { + "line": 17, + "text": "", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 18, + "text": " events {", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 10, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls" + ] + }, + { + "start": 11, + "end": 12, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + }, + { + "line": 19, + "text": " LOGIC_READY: can_enter(item: RG_HOOKSHOT)", + "tokens": [ + { + "start": 0, + "end": 8, + "scopes": [ + "source.rls" + ] + }, + { + "start": 8, + "end": 19, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 19, + "end": 20, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 20, + "end": 21, + "scopes": [ + "source.rls" + ] + }, + { + "start": 21, + "end": 30, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 30, + "end": 31, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 31, + "end": 35, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 35, + "end": 37, + "scopes": [ + "source.rls" + ] + }, + { + "start": 37, + "end": 48, + "scopes": [ + "source.rls", + "entity.name.type.rls" + ] + }, + { + "start": 48, + "end": 49, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 20, + "text": " }", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 5, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 21, + "text": "", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 22, + "text": " exits {", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 9, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 9, + "end": 10, + "scopes": [ + "source.rls" + ] + }, + { + "start": 10, + "end": 11, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + }, + { + "line": 23, + "text": " RR_NEXT:", + "tokens": [ + { + "start": 0, + "end": 8, + "scopes": [ + "source.rls" + ] + }, + { + "start": 8, + "end": 15, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 15, + "end": 16, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + } + ] + }, + { + "line": 24, + "text": " always", + "tokens": [ + { + "start": 0, + "end": 12, + "scopes": [ + "source.rls" + ] + }, + { + "start": 12, + "end": 18, + "scopes": [ + "source.rls", + "constant.language.boolean.rls" + ] + } + ] + }, + { + "line": 25, + "text": " }", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 5, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 26, + "text": "}", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 27, + "text": "", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 28, + "text": "extend region RR_SAMPLE {", + "tokens": [ + { + "start": 0, + "end": 6, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 6, + "end": 7, + "scopes": [ + "source.rls" + ] + }, + { + "start": 7, + "end": 13, + "scopes": [ + "source.rls", + "storage.type.rls" + ] + }, + { + "start": 13, + "end": 14, + "scopes": [ + "source.rls" + ] + }, + { + "start": 14, + "end": 23, + "scopes": [ + "source.rls", + "entity.name.type.rls" + ] + }, + { + "start": 23, + "end": 24, + "scopes": [ + "source.rls" + ] + }, + { + "start": 24, + "end": 25, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + }, + { + "line": 29, + "text": " locations {", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 13, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 13, + "end": 14, + "scopes": [ + "source.rls" + ] + }, + { + "start": 14, + "end": 15, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + }, + { + "line": 30, + "text": " RC_SAMPLE: can_enter(RG_HOOKSHOT)", + "tokens": [ + { + "start": 0, + "end": 8, + "scopes": [ + "source.rls" + ] + }, + { + "start": 8, + "end": 17, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 18, + "end": 19, + "scopes": [ + "source.rls" + ] + }, + { + "start": 19, + "end": 28, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 28, + "end": 29, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 29, + "end": 40, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 40, + "end": 41, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 31, + "text": " }", + "tokens": [ + { + "start": 0, + "end": 4, + "scopes": [ + "source.rls" + ] + }, + { + "start": 4, + "end": 5, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 32, + "text": "}", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + } + ] + }, + { + "line": 33, + "text": "", + "tokens": [ + { + "start": 0, + "end": 1, + "scopes": [ + "source.rls" + ] + } + ] + }, + { + "line": 34, + "text": "# Keep this incomplete source editable while typing.", + "tokens": [ + { + "start": 0, + "end": 52, + "scopes": [ + "source.rls", + "comment.line.number-sign.rls" + ] + } + ] + }, + { + "line": 35, + "text": "define unfinished(value: int): match value {", + "tokens": [ + { + "start": 0, + "end": 6, + "scopes": [ + "source.rls", + "storage.type.function.rls" + ] + }, + { + "start": 6, + "end": 7, + "scopes": [ + "source.rls" + ] + }, + { + "start": 7, + "end": 17, + "scopes": [ + "source.rls", + "entity.name.function.rls" + ] + }, + { + "start": 17, + "end": 18, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + }, + { + "start": 18, + "end": 23, + "scopes": [ + "source.rls", + "variable.parameter.rls" + ] + }, + { + "start": 23, + "end": 25, + "scopes": [ + "source.rls" + ] + }, + { + "start": 25, + "end": 28, + "scopes": [ + "source.rls", + "entity.name.type.rls" + ] + }, + { + "start": 28, + "end": 29, + "scopes": [ + "source.rls", + "punctuation.section.group.end.rls" + ] + }, + { + "start": 29, + "end": 30, + "scopes": [ + "source.rls", + "punctuation.separator.rls" + ] + }, + { + "start": 30, + "end": 31, + "scopes": [ + "source.rls" + ] + }, + { + "start": 31, + "end": 36, + "scopes": [ + "source.rls", + "keyword.control.rls" + ] + }, + { + "start": 36, + "end": 43, + "scopes": [ + "source.rls" + ] + }, + { + "start": 43, + "end": 44, + "scopes": [ + "source.rls", + "punctuation.section.group.begin.rls" + ] + } + ] + } +] diff --git a/tooling/textmate/test-scopes.js b/tooling/textmate/test-scopes.js new file mode 100644 index 0000000..b07a5c0 --- /dev/null +++ b/tooling/textmate/test-scopes.js @@ -0,0 +1,68 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); +const oniguruma = require("vscode-oniguruma"); +const textmate = require("vscode-textmate"); + +const textmateDirectory = __dirname; +const repositoryDirectory = path.resolve(textmateDirectory, "..", ".."); +const grammarPath = path.join(repositoryDirectory, "editors", "vscode", "syntaxes", "rls.tmLanguage.json"); +const fixturePath = path.join(repositoryDirectory, "tooling", "syntax-fixtures", "representative.rls"); +const snapshotPath = path.join(textmateDirectory, "snapshots", "representative.scopes.json"); + +function loadOniguruma() { + const wasmPath = require.resolve("vscode-oniguruma/release/onig.wasm"); + return oniguruma.loadWASM(fs.readFileSync(wasmPath).buffer); +} + +function serializeScopes(grammar, source) { + let ruleStack = textmate.INITIAL; + return source.split(/\r?\n/).map((line, index) => { + const tokenizedLine = grammar.tokenizeLine(line, ruleStack); + ruleStack = tokenizedLine.ruleStack; + return { + line: index + 1, + text: line, + tokens: tokenizedLine.tokens.map((token) => ({ + start: token.startIndex, + end: token.endIndex, + scopes: token.scopes + })) + }; + }); +} + +async function main() { + await loadOniguruma(); + const registry = new textmate.Registry({ + onigLib: Promise.resolve({ + createOnigScanner: (sources) => new oniguruma.OnigScanner(sources), + createOnigString: (value) => new oniguruma.OnigString(value) + }), + loadGrammar: (scopeName) => { + if (scopeName !== "source.rls") { + return null; + } + return JSON.parse(fs.readFileSync(grammarPath, "utf8")); + } + }); + + const grammar = await registry.loadGrammar("source.rls"); + assert(grammar, "Expected the source.rls grammar to load."); + const snapshot = serializeScopes(grammar, fs.readFileSync(fixturePath, "utf8")); + + if (process.argv.includes("--update")) { + fs.mkdirSync(path.dirname(snapshotPath), { recursive: true }); + fs.writeFileSync(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`); + return; + } + + assert(fs.existsSync(snapshotPath), "Scope snapshot is missing. Run npm run update-snapshots."); + const expected = JSON.parse(fs.readFileSync(snapshotPath, "utf8")); + assert.deepStrictEqual(snapshot, expected, "TextMate scopes changed. Review the diff and run npm run update-snapshots if intentional."); +} + +main().catch((error) => { + console.error(error.stack || error.message); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/tooling/tree-sitter-rls/.gitignore b/tooling/tree-sitter-rls/.gitignore new file mode 100644 index 0000000..40b878d --- /dev/null +++ b/tooling/tree-sitter-rls/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/tooling/tree-sitter-rls/Cargo.toml b/tooling/tree-sitter-rls/Cargo.toml new file mode 100644 index 0000000..4c17a9a --- /dev/null +++ b/tooling/tree-sitter-rls/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "tree-sitter-rls" +description = "rls grammar for the tree-sitter parsing library" +version = "0.0.1" +keywords = ["incremental", "parsing", "rls"] +categories = ["parsing", "text-editors"] +repository = "https://github.com/tree-sitter/tree-sitter-rls" +edition = "2018" +license = "MIT" + +build = "bindings/rust/build.rs" +include = [ + "bindings/rust/*", + "grammar.js", + "queries/*", + "src/*", +] + +[lib] +path = "bindings/rust/lib.rs" + +[dependencies] +tree-sitter = "~0.20.10" + +[build-dependencies] +cc = "1.0" diff --git a/tooling/tree-sitter-rls/README.md b/tooling/tree-sitter-rls/README.md new file mode 100644 index 0000000..6003b81 --- /dev/null +++ b/tooling/tree-sitter-rls/README.md @@ -0,0 +1,17 @@ +# Tree-sitter RLS Grammar + +This package is the incremental editor grammar for Rando Logic Script. It provides syntax trees, highlighting, folding, and indentation queries for editor integrations. + +`parser/src/grammar.h` remains compiler-authoritative. Changes to that PEGTL grammar which affect lexical syntax, declarations, expressions, comments, or delimiters must be reflected here and in the TextMate grammar. + +## Commands + +Run these commands from this directory: + +```powershell +npm install +npm run generate +npm test +``` + +`npm test` exercises the valid part of the shared fixture, verifies controlled error recovery for its incomplete tail, and runs the highlight, fold, and indent queries. \ No newline at end of file diff --git a/tooling/tree-sitter-rls/binding.gyp b/tooling/tree-sitter-rls/binding.gyp new file mode 100644 index 0000000..02a7343 --- /dev/null +++ b/tooling/tree-sitter-rls/binding.gyp @@ -0,0 +1,19 @@ +{ + "targets": [ + { + "target_name": "tree_sitter_rls_binding", + "include_dirs": [ + " +#include "nan.h" + +using namespace v8; + +extern "C" TSLanguage * tree_sitter_rls(); + +namespace { + +NAN_METHOD(New) {} + +void Init(Local exports, Local module) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("Language").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Local constructor = Nan::GetFunction(tpl).ToLocalChecked(); + Local instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); + Nan::SetInternalFieldPointer(instance, 0, tree_sitter_rls()); + + Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("rls").ToLocalChecked()); + Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance); +} + +NODE_MODULE(tree_sitter_rls_binding, Init) + +} // namespace diff --git a/tooling/tree-sitter-rls/bindings/node/index.js b/tooling/tree-sitter-rls/bindings/node/index.js new file mode 100644 index 0000000..f6b4199 --- /dev/null +++ b/tooling/tree-sitter-rls/bindings/node/index.js @@ -0,0 +1,19 @@ +try { + module.exports = require("../../build/Release/tree_sitter_rls_binding"); +} catch (error1) { + if (error1.code !== 'MODULE_NOT_FOUND') { + throw error1; + } + try { + module.exports = require("../../build/Debug/tree_sitter_rls_binding"); + } catch (error2) { + if (error2.code !== 'MODULE_NOT_FOUND') { + throw error2; + } + throw error1 + } +} + +try { + module.exports.nodeTypeInfo = require("../../src/node-types.json"); +} catch (_) {} diff --git a/tooling/tree-sitter-rls/bindings/rust/build.rs b/tooling/tree-sitter-rls/bindings/rust/build.rs new file mode 100644 index 0000000..c6061f0 --- /dev/null +++ b/tooling/tree-sitter-rls/bindings/rust/build.rs @@ -0,0 +1,40 @@ +fn main() { + let src_dir = std::path::Path::new("src"); + + let mut c_config = cc::Build::new(); + c_config.include(&src_dir); + c_config + .flag_if_supported("-Wno-unused-parameter") + .flag_if_supported("-Wno-unused-but-set-variable") + .flag_if_supported("-Wno-trigraphs"); + let parser_path = src_dir.join("parser.c"); + c_config.file(&parser_path); + + // If your language uses an external scanner written in C, + // then include this block of code: + + /* + let scanner_path = src_dir.join("scanner.c"); + c_config.file(&scanner_path); + println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); + */ + + c_config.compile("parser"); + println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); + + // If your language uses an external scanner written in C++, + // then include this block of code: + + /* + let mut cpp_config = cc::Build::new(); + cpp_config.cpp(true); + cpp_config.include(&src_dir); + cpp_config + .flag_if_supported("-Wno-unused-parameter") + .flag_if_supported("-Wno-unused-but-set-variable"); + let scanner_path = src_dir.join("scanner.cc"); + cpp_config.file(&scanner_path); + cpp_config.compile("scanner"); + println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); + */ +} diff --git a/tooling/tree-sitter-rls/bindings/rust/lib.rs b/tooling/tree-sitter-rls/bindings/rust/lib.rs new file mode 100644 index 0000000..0c766ac --- /dev/null +++ b/tooling/tree-sitter-rls/bindings/rust/lib.rs @@ -0,0 +1,52 @@ +//! This crate provides rls language support for the [tree-sitter][] parsing library. +//! +//! Typically, you will use the [language][language func] function to add this language to a +//! tree-sitter [Parser][], and then use the parser to parse some code: +//! +//! ``` +//! let code = ""; +//! let mut parser = tree_sitter::Parser::new(); +//! parser.set_language(tree_sitter_rls::language()).expect("Error loading rls grammar"); +//! let tree = parser.parse(code, None).unwrap(); +//! ``` +//! +//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html +//! [language func]: fn.language.html +//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html +//! [tree-sitter]: https://tree-sitter.github.io/ + +use tree_sitter::Language; + +extern "C" { + fn tree_sitter_rls() -> Language; +} + +/// Get the tree-sitter [Language][] for this grammar. +/// +/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html +pub fn language() -> Language { + unsafe { tree_sitter_rls() } +} + +/// The content of the [`node-types.json`][] file for this grammar. +/// +/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types +pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json"); + +// Uncomment these to include any queries that this grammar contains + +// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm"); +// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm"); +// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm"); +// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm"); + +#[cfg(test)] +mod tests { + #[test] + fn test_can_load_grammar() { + let mut parser = tree_sitter::Parser::new(); + parser + .set_language(super::language()) + .expect("Error loading rls language"); + } +} diff --git a/tooling/tree-sitter-rls/grammar.js b/tooling/tree-sitter-rls/grammar.js new file mode 100644 index 0000000..bf5e227 --- /dev/null +++ b/tooling/tree-sitter-rls/grammar.js @@ -0,0 +1,100 @@ +const PREC = { + TERNARY: 1, + OR: 2, + AND: 3, + COMPARISON: 4, + ADD: 5, + MULTIPLY: 6, + UNARY: 7, + CALL: 8, + MEMBER: 9 +}; + +module.exports = grammar({ + name: "rls", + + extras: ($) => [/[\s\r\n]/, $.comment], + + word: ($) => $.identifier, + + rules: { + source_file: ($) => repeat($.declaration), + + comment: () => token(seq("#", /.*/)), + identifier: () => /[A-Za-z_][A-Za-z0-9_]*/, + number: () => /-?\d+/, + string: () => token(seq('"', repeat(choice(/[^"\\\n]/, /\\["\\]/)), '"')), + + declaration: ($) => choice( + $.region_declaration, + $.extend_region_declaration, + $.define_declaration, + $.extern_define_declaration, + $.enum_declaration, + $.extern_enum_declaration + ), + + region_declaration: ($) => seq("region", field("name", $.identifier), "{", repeat(choice($.region_data_entry, $.section)), "}"), + extend_region_declaration: ($) => seq("extend", "region", field("name", $.identifier), "{", repeat($.section), "}"), + region_data_entry: ($) => seq(field("key", $.identifier), ":", field("value", $.expression)), + + section: ($) => seq(field("kind", choice("events", "locations", "exits")), "{", repeat($.entry), "}"), + entry: ($) => seq(field("name", $.identifier), ":", field("value", $.expression)), + + define_declaration: ($) => seq("define", field("name", $.identifier), "(", optional($.parameters), ")", ":", field("body", $.expression)), + extern_define_declaration: ($) => seq("extern", "define", field("name", $.identifier), "(", optional($.parameters), ")", "->", field("return_type", $.identifier)), + parameters: ($) => commaSep1($.parameter), + parameter: ($) => seq( + field("name", $.identifier), + optional(seq(":", field("type", $.identifier))), + optional(seq("=", field("default", $.expression))) + ), + + enum_declaration: ($) => seq("enum", field("name", $.identifier), "{", optional(commaSep1($.enum_member)), "}"), + extern_enum_declaration: ($) => seq("extern", "enum", field("name", $.identifier), "{", optional(commaSep1($.extern_enum_entry)), "}"), + enum_member: ($) => seq(field("name", $.identifier), optional(seq("=", field("value", $.number)))), + extern_enum_entry: ($) => choice($.enum_member, $.glob_pattern), + glob_pattern: () => token(/[A-Za-z0-9_]*\*[A-Za-z0-9_*]*/), + + expression: ($) => choice($.ternary_expression, $.binary_expression, $.unary_expression, $.primary_expression), + primary_expression: ($) => choice( + $.boolean, + "here", + $.string, + $.number, + $.call_expression, + $.member_expression, + $.match_expression, + $.list_expression, + $.identifier, + seq("(", $.expression, ")") + ), + boolean: () => choice("true", "false", "always", "never"), + list_expression: ($) => seq("[", optional(commaSep1($.expression)), "]"), + call_expression: ($) => prec(PREC.CALL, seq(field("function", $.identifier), "(", optional(commaSep1($.argument)), ")")), + argument: ($) => choice($.named_argument, $.expression), + named_argument: ($) => seq(field("name", $.identifier), ":", field("value", $.expression)), + member_expression: ($) => prec(PREC.MEMBER, seq(field("object", $.identifier), ".", field("member", $.identifier))), + unary_expression: ($) => prec(PREC.UNARY, seq("not", field("argument", $.expression))), + binary_expression: ($) => choice( + prec.left(PREC.MULTIPLY, seq($.expression, choice("*", "/"), $.expression)), + prec.left(PREC.ADD, seq($.expression, choice("+", "-"), $.expression)), + prec.left(PREC.COMPARISON, seq($.expression, choice("==", "!=", ">=", "<=", ">", "<", "is", seq("is", "not")), $.expression)), + prec.left(PREC.AND, seq($.expression, "and", $.expression)), + prec.left(PREC.OR, seq($.expression, "or", $.expression)) + ), + ternary_expression: ($) => prec.right(PREC.TERNARY, seq($.expression, "?", $.expression, ":", $.expression)), + + match_expression: ($) => seq("match", field("subject", $.identifier), "{", repeat1($.match_arm), "}"), + match_arm: ($) => seq($.match_pattern, ":", $.expression, optional("or")), + match_pattern: ($) => choice("_", commaOrSep1($.identifier)) + } +}); + +function commaSep1(rule) { + return seq(rule, repeat(seq(",", rule))); +} + +function commaOrSep1(rule) { + return seq(rule, repeat(seq("or", rule))); +} \ No newline at end of file diff --git a/tooling/tree-sitter-rls/package-lock.json b/tooling/tree-sitter-rls/package-lock.json new file mode 100644 index 0000000..051f6c7 --- /dev/null +++ b/tooling/tree-sitter-rls/package-lock.json @@ -0,0 +1,30 @@ +{ + "name": "tree-sitter-rls", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tree-sitter-rls", + "version": "0.1.0", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "tree-sitter-cli": "0.20.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tree-sitter-cli": { + "version": "0.20.8", + "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.20.8.tgz", + "integrity": "sha512-XjTcS3wdTy/2cc/ptMLc/WRyOLECRYcMTrSWyhZnj1oGSOWbHLTklgsgRICU3cPfb0vy+oZCC33M43u6R1HSCA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "tree-sitter": "cli.js" + } + } + } +} diff --git a/tooling/tree-sitter-rls/package.json b/tooling/tree-sitter-rls/package.json new file mode 100644 index 0000000..5de655e --- /dev/null +++ b/tooling/tree-sitter-rls/package.json @@ -0,0 +1,19 @@ +{ + "name": "tree-sitter-rls", + "version": "0.1.0", + "description": "Tree-sitter grammar for Rando Logic Script.", + "license": "MIT", + "private": true, + "engines": { + "node": ">=10" + }, + "scripts": { + "install": "node -e \"console.log('Skipping optional native binding build for grammar tests.')\"", + "generate": "tree-sitter generate", + "test": "node test-shared-corpus.js" + }, + "devDependencies": { + "tree-sitter-cli": "0.20.8" + }, + "main": "bindings/node" +} diff --git a/tooling/tree-sitter-rls/queries/folds.scm b/tooling/tree-sitter-rls/queries/folds.scm new file mode 100644 index 0000000..e30c693 --- /dev/null +++ b/tooling/tree-sitter-rls/queries/folds.scm @@ -0,0 +1,9 @@ +[ + (region_declaration) + (extend_region_declaration) + (section) + (enum_declaration) + (extern_enum_declaration) + (match_expression) + (list_expression) +] @fold \ No newline at end of file diff --git a/tooling/tree-sitter-rls/queries/highlights.scm b/tooling/tree-sitter-rls/queries/highlights.scm new file mode 100644 index 0000000..5f4495d --- /dev/null +++ b/tooling/tree-sitter-rls/queries/highlights.scm @@ -0,0 +1,54 @@ +(comment) @comment +(string) @string +(number) @number +(boolean) @boolean + +[ + "region" + "extend" + "extern" + "define" + "enum" + "events" + "locations" + "exits" + "match" + "here" +] @keyword + +[ + "and" + "or" + "not" + "is" + "==" + "!=" + ">=" + "<=" + ">" + "<" + "+" + "-" + "*" + "/" + "=" + "->" + "?" +] @operator + +(region_declaration name: (identifier) @type) +(extend_region_declaration name: (identifier) @type) +(enum_declaration name: (identifier) @type) +(extern_enum_declaration name: (identifier) @type) +(define_declaration name: (identifier) @function) +(extern_define_declaration name: (identifier) @function) +(parameter name: (identifier) @variable.parameter) +(parameter type: (identifier) @type) +(extern_define_declaration return_type: (identifier) @type) +(enum_member name: (identifier) @constant) +(glob_pattern) @constant +(member_expression object: (identifier) @type) +(member_expression member: (identifier) @property) +(named_argument name: (identifier) @variable.parameter) +(entry name: (identifier) @property) +(region_data_entry key: (identifier) @property) \ No newline at end of file diff --git a/tooling/tree-sitter-rls/queries/indents.scm b/tooling/tree-sitter-rls/queries/indents.scm new file mode 100644 index 0000000..23df662 --- /dev/null +++ b/tooling/tree-sitter-rls/queries/indents.scm @@ -0,0 +1,2 @@ +["{" "[" "("] @indent +["}" "]" ")"] @outdent \ No newline at end of file diff --git a/tooling/tree-sitter-rls/src/grammar.json b/tooling/tree-sitter-rls/src/grammar.json new file mode 100644 index 0000000..88113e6 --- /dev/null +++ b/tooling/tree-sitter-rls/src/grammar.json @@ -0,0 +1,1236 @@ +{ + "name": "rls", + "word": "identifier", + "rules": { + "source_file": { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "declaration" + } + }, + "comment": { + "type": "TOKEN", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "#" + }, + { + "type": "PATTERN", + "value": ".*" + } + ] + } + }, + "identifier": { + "type": "PATTERN", + "value": "[A-Za-z_][A-Za-z0-9_]*" + }, + "number": { + "type": "PATTERN", + "value": "-?\\d+" + }, + "string": { + "type": "TOKEN", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "\"" + }, + { + "type": "REPEAT", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "PATTERN", + "value": "[^\"\\\\\\n]" + }, + { + "type": "PATTERN", + "value": "\\\\[\"\\\\]" + } + ] + } + }, + { + "type": "STRING", + "value": "\"" + } + ] + } + }, + "declaration": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "region_declaration" + }, + { + "type": "SYMBOL", + "name": "extend_region_declaration" + }, + { + "type": "SYMBOL", + "name": "define_declaration" + }, + { + "type": "SYMBOL", + "name": "extern_define_declaration" + }, + { + "type": "SYMBOL", + "name": "enum_declaration" + }, + { + "type": "SYMBOL", + "name": "extern_enum_declaration" + } + ] + }, + "region_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "region" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "REPEAT", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "region_data_entry" + }, + { + "type": "SYMBOL", + "name": "section" + } + ] + } + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "extend_region_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "extend" + }, + { + "type": "STRING", + "value": "region" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "section" + } + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "region_data_entry": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "key", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "value", + "content": { + "type": "SYMBOL", + "name": "expression" + } + } + ] + }, + "section": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "kind", + "content": { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "events" + }, + { + "type": "STRING", + "value": "locations" + }, + { + "type": "STRING", + "value": "exits" + } + ] + } + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "REPEAT", + "content": { + "type": "SYMBOL", + "name": "entry" + } + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "entry": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "value", + "content": { + "type": "SYMBOL", + "name": "expression" + } + } + ] + }, + "define_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "define" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "(" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": ")" + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "body", + "content": { + "type": "SYMBOL", + "name": "expression" + } + } + ] + }, + "extern_define_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "extern" + }, + { + "type": "STRING", + "value": "define" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "(" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "parameters" + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": ")" + }, + { + "type": "STRING", + "value": "->" + }, + { + "type": "FIELD", + "name": "return_type", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + } + ] + }, + "parameters": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "parameter" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "parameter" + } + ] + } + } + ] + }, + "parameter": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "type", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "=" + }, + { + "type": "FIELD", + "name": "default", + "content": { + "type": "SYMBOL", + "name": "expression" + } + } + ] + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "enum_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "enum" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "enum_member" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "enum_member" + } + ] + } + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "extern_enum_declaration": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "extern" + }, + { + "type": "STRING", + "value": "enum" + }, + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "extern_enum_entry" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "extern_enum_entry" + } + ] + } + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "enum_member": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "=" + }, + { + "type": "FIELD", + "name": "value", + "content": { + "type": "SYMBOL", + "name": "number" + } + } + ] + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "extern_enum_entry": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "enum_member" + }, + { + "type": "SYMBOL", + "name": "glob_pattern" + } + ] + }, + "glob_pattern": { + "type": "TOKEN", + "content": { + "type": "PATTERN", + "value": "[A-Za-z0-9_]*\\*[A-Za-z0-9_*]*" + } + }, + "expression": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "ternary_expression" + }, + { + "type": "SYMBOL", + "name": "binary_expression" + }, + { + "type": "SYMBOL", + "name": "unary_expression" + }, + { + "type": "SYMBOL", + "name": "primary_expression" + } + ] + }, + "primary_expression": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "boolean" + }, + { + "type": "STRING", + "value": "here" + }, + { + "type": "SYMBOL", + "name": "string" + }, + { + "type": "SYMBOL", + "name": "number" + }, + { + "type": "SYMBOL", + "name": "call_expression" + }, + { + "type": "SYMBOL", + "name": "member_expression" + }, + { + "type": "SYMBOL", + "name": "match_expression" + }, + { + "type": "SYMBOL", + "name": "list_expression" + }, + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "(" + }, + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "STRING", + "value": ")" + } + ] + } + ] + }, + "boolean": { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "true" + }, + { + "type": "STRING", + "value": "false" + }, + { + "type": "STRING", + "value": "always" + }, + { + "type": "STRING", + "value": "never" + } + ] + }, + "list_expression": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "[" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": "]" + } + ] + }, + "call_expression": { + "type": "PREC", + "value": 8, + "content": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "function", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "(" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "argument" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "," + }, + { + "type": "SYMBOL", + "name": "argument" + } + ] + } + } + ] + }, + { + "type": "BLANK" + } + ] + }, + { + "type": "STRING", + "value": ")" + } + ] + } + }, + "argument": { + "type": "CHOICE", + "members": [ + { + "type": "SYMBOL", + "name": "named_argument" + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + }, + "named_argument": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "name", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "FIELD", + "name": "value", + "content": { + "type": "SYMBOL", + "name": "expression" + } + } + ] + }, + "member_expression": { + "type": "PREC", + "value": 9, + "content": { + "type": "SEQ", + "members": [ + { + "type": "FIELD", + "name": "object", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "." + }, + { + "type": "FIELD", + "name": "member", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + } + ] + } + }, + "unary_expression": { + "type": "PREC", + "value": 7, + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "not" + }, + { + "type": "FIELD", + "name": "argument", + "content": { + "type": "SYMBOL", + "name": "expression" + } + } + ] + } + }, + "binary_expression": { + "type": "CHOICE", + "members": [ + { + "type": "PREC_LEFT", + "value": 6, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "*" + }, + { + "type": "STRING", + "value": "/" + } + ] + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + }, + { + "type": "PREC_LEFT", + "value": 5, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "+" + }, + { + "type": "STRING", + "value": "-" + } + ] + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + }, + { + "type": "PREC_LEFT", + "value": 4, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "==" + }, + { + "type": "STRING", + "value": "!=" + }, + { + "type": "STRING", + "value": ">=" + }, + { + "type": "STRING", + "value": "<=" + }, + { + "type": "STRING", + "value": ">" + }, + { + "type": "STRING", + "value": "<" + }, + { + "type": "STRING", + "value": "is" + }, + { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "is" + }, + { + "type": "STRING", + "value": "not" + } + ] + } + ] + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + }, + { + "type": "PREC_LEFT", + "value": 3, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "STRING", + "value": "and" + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + }, + { + "type": "PREC_LEFT", + "value": 2, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "STRING", + "value": "or" + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + } + ] + }, + "ternary_expression": { + "type": "PREC_RIGHT", + "value": 1, + "content": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "STRING", + "value": "?" + }, + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "SYMBOL", + "name": "expression" + } + ] + } + }, + "match_expression": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "match" + }, + { + "type": "FIELD", + "name": "subject", + "content": { + "type": "SYMBOL", + "name": "identifier" + } + }, + { + "type": "STRING", + "value": "{" + }, + { + "type": "REPEAT1", + "content": { + "type": "SYMBOL", + "name": "match_arm" + } + }, + { + "type": "STRING", + "value": "}" + } + ] + }, + "match_arm": { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "match_pattern" + }, + { + "type": "STRING", + "value": ":" + }, + { + "type": "SYMBOL", + "name": "expression" + }, + { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "or" + }, + { + "type": "BLANK" + } + ] + } + ] + }, + "match_pattern": { + "type": "CHOICE", + "members": [ + { + "type": "STRING", + "value": "_" + }, + { + "type": "SEQ", + "members": [ + { + "type": "SYMBOL", + "name": "identifier" + }, + { + "type": "REPEAT", + "content": { + "type": "SEQ", + "members": [ + { + "type": "STRING", + "value": "or" + }, + { + "type": "SYMBOL", + "name": "identifier" + } + ] + } + } + ] + } + ] + } + }, + "extras": [ + { + "type": "PATTERN", + "value": "[\\s\\r\\n]" + }, + { + "type": "SYMBOL", + "name": "comment" + } + ], + "conflicts": [], + "precedences": [], + "externals": [], + "inline": [], + "supertypes": [] +} + diff --git a/tooling/tree-sitter-rls/src/node-types.json b/tooling/tree-sitter-rls/src/node-types.json new file mode 100644 index 0000000..91041f1 --- /dev/null +++ b/tooling/tree-sitter-rls/src/node-types.json @@ -0,0 +1,895 @@ +[ + { + "type": "argument", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "expression", + "named": true + }, + { + "type": "named_argument", + "named": true + } + ] + } + }, + { + "type": "binary_expression", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + }, + { + "type": "boolean", + "named": true, + "fields": {} + }, + { + "type": "call_expression", + "named": true, + "fields": { + "function": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "argument", + "named": true + } + ] + } + }, + { + "type": "declaration", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "define_declaration", + "named": true + }, + { + "type": "enum_declaration", + "named": true + }, + { + "type": "extend_region_declaration", + "named": true + }, + { + "type": "extern_define_declaration", + "named": true + }, + { + "type": "extern_enum_declaration", + "named": true + }, + { + "type": "region_declaration", + "named": true + } + ] + } + }, + { + "type": "define_declaration", + "named": true, + "fields": { + "body": { + "multiple": false, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "parameters", + "named": true + } + ] + } + }, + { + "type": "entry", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "value": { + "multiple": false, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + } + }, + { + "type": "enum_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "enum_member", + "named": true + } + ] + } + }, + { + "type": "enum_member", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "value": { + "multiple": false, + "required": false, + "types": [ + { + "type": "number", + "named": true + } + ] + } + } + }, + { + "type": "expression", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "binary_expression", + "named": true + }, + { + "type": "primary_expression", + "named": true + }, + { + "type": "ternary_expression", + "named": true + }, + { + "type": "unary_expression", + "named": true + } + ] + } + }, + { + "type": "extend_region_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "section", + "named": true + } + ] + } + }, + { + "type": "extern_define_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "return_type": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "parameters", + "named": true + } + ] + } + }, + { + "type": "extern_enum_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "extern_enum_entry", + "named": true + } + ] + } + }, + { + "type": "extern_enum_entry", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": true, + "types": [ + { + "type": "enum_member", + "named": true + }, + { + "type": "glob_pattern", + "named": true + } + ] + } + }, + { + "type": "list_expression", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + }, + { + "type": "match_arm", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "expression", + "named": true + }, + { + "type": "match_pattern", + "named": true + } + ] + } + }, + { + "type": "match_expression", + "named": true, + "fields": { + "subject": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "match_arm", + "named": true + } + ] + } + }, + { + "type": "match_pattern", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + { + "type": "member_expression", + "named": true, + "fields": { + "member": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "object": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "named_argument", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "value": { + "multiple": false, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + } + }, + { + "type": "parameter", + "named": true, + "fields": { + "default": { + "multiple": false, + "required": false, + "types": [ + { + "type": "expression", + "named": true + } + ] + }, + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "type": { + "multiple": false, + "required": false, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + } + }, + { + "type": "parameters", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "parameter", + "named": true + } + ] + } + }, + { + "type": "primary_expression", + "named": true, + "fields": {}, + "children": { + "multiple": false, + "required": false, + "types": [ + { + "type": "boolean", + "named": true + }, + { + "type": "call_expression", + "named": true + }, + { + "type": "expression", + "named": true + }, + { + "type": "identifier", + "named": true + }, + { + "type": "list_expression", + "named": true + }, + { + "type": "match_expression", + "named": true + }, + { + "type": "member_expression", + "named": true + }, + { + "type": "number", + "named": true + }, + { + "type": "string", + "named": true + } + ] + } + }, + { + "type": "region_data_entry", + "named": true, + "fields": { + "key": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + }, + "value": { + "multiple": false, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + } + }, + { + "type": "region_declaration", + "named": true, + "fields": { + "name": { + "multiple": false, + "required": true, + "types": [ + { + "type": "identifier", + "named": true + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "region_data_entry", + "named": true + }, + { + "type": "section", + "named": true + } + ] + } + }, + { + "type": "section", + "named": true, + "fields": { + "kind": { + "multiple": false, + "required": true, + "types": [ + { + "type": "events", + "named": false + }, + { + "type": "exits", + "named": false + }, + { + "type": "locations", + "named": false + } + ] + } + }, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "entry", + "named": true + } + ] + } + }, + { + "type": "source_file", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "declaration", + "named": true + } + ] + } + }, + { + "type": "ternary_expression", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + }, + { + "type": "unary_expression", + "named": true, + "fields": { + "argument": { + "multiple": false, + "required": true, + "types": [ + { + "type": "expression", + "named": true + } + ] + } + } + }, + { + "type": "!=", + "named": false + }, + { + "type": "(", + "named": false + }, + { + "type": ")", + "named": false + }, + { + "type": "*", + "named": false + }, + { + "type": "+", + "named": false + }, + { + "type": ",", + "named": false + }, + { + "type": "-", + "named": false + }, + { + "type": "->", + "named": false + }, + { + "type": ".", + "named": false + }, + { + "type": "/", + "named": false + }, + { + "type": ":", + "named": false + }, + { + "type": "<", + "named": false + }, + { + "type": "<=", + "named": false + }, + { + "type": "=", + "named": false + }, + { + "type": "==", + "named": false + }, + { + "type": ">", + "named": false + }, + { + "type": ">=", + "named": false + }, + { + "type": "?", + "named": false + }, + { + "type": "[", + "named": false + }, + { + "type": "]", + "named": false + }, + { + "type": "_", + "named": false + }, + { + "type": "always", + "named": false + }, + { + "type": "and", + "named": false + }, + { + "type": "comment", + "named": true + }, + { + "type": "define", + "named": false + }, + { + "type": "enum", + "named": false + }, + { + "type": "events", + "named": false + }, + { + "type": "exits", + "named": false + }, + { + "type": "extend", + "named": false + }, + { + "type": "extern", + "named": false + }, + { + "type": "false", + "named": false + }, + { + "type": "glob_pattern", + "named": true + }, + { + "type": "here", + "named": false + }, + { + "type": "identifier", + "named": true + }, + { + "type": "is", + "named": false + }, + { + "type": "locations", + "named": false + }, + { + "type": "match", + "named": false + }, + { + "type": "never", + "named": false + }, + { + "type": "not", + "named": false + }, + { + "type": "number", + "named": true + }, + { + "type": "or", + "named": false + }, + { + "type": "region", + "named": false + }, + { + "type": "string", + "named": true + }, + { + "type": "true", + "named": false + }, + { + "type": "{", + "named": false + }, + { + "type": "}", + "named": false + } +] \ No newline at end of file diff --git a/tooling/tree-sitter-rls/src/parser.c b/tooling/tree-sitter-rls/src/parser.c new file mode 100644 index 0000000..2528ea8 --- /dev/null +++ b/tooling/tree-sitter-rls/src/parser.c @@ -0,0 +1,4933 @@ +#include + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + +#define LANGUAGE_VERSION 14 +#define STATE_COUNT 159 +#define LARGE_STATE_COUNT 2 +#define SYMBOL_COUNT 87 +#define ALIAS_COUNT 0 +#define TOKEN_COUNT 47 +#define EXTERNAL_TOKEN_COUNT 0 +#define FIELD_COUNT 13 +#define MAX_ALIAS_SEQUENCE_LENGTH 8 +#define PRODUCTION_ID_COUNT 18 + +enum { + sym_identifier = 1, + sym_comment = 2, + sym_number = 3, + sym_string = 4, + anon_sym_region = 5, + anon_sym_LBRACE = 6, + anon_sym_RBRACE = 7, + anon_sym_extend = 8, + anon_sym_COLON = 9, + anon_sym_events = 10, + anon_sym_locations = 11, + anon_sym_exits = 12, + anon_sym_define = 13, + anon_sym_LPAREN = 14, + anon_sym_RPAREN = 15, + anon_sym_extern = 16, + anon_sym_DASH_GT = 17, + anon_sym_COMMA = 18, + anon_sym_EQ = 19, + anon_sym_enum = 20, + sym_glob_pattern = 21, + anon_sym_here = 22, + anon_sym_true = 23, + anon_sym_false = 24, + anon_sym_always = 25, + anon_sym_never = 26, + anon_sym_LBRACK = 27, + anon_sym_RBRACK = 28, + anon_sym_DOT = 29, + anon_sym_not = 30, + anon_sym_STAR = 31, + anon_sym_SLASH = 32, + anon_sym_PLUS = 33, + anon_sym_DASH = 34, + anon_sym_EQ_EQ = 35, + anon_sym_BANG_EQ = 36, + anon_sym_GT_EQ = 37, + anon_sym_LT_EQ = 38, + anon_sym_GT = 39, + anon_sym_LT = 40, + anon_sym_is = 41, + anon_sym_and = 42, + anon_sym_or = 43, + anon_sym_QMARK = 44, + anon_sym_match = 45, + anon_sym__ = 46, + sym_source_file = 47, + sym_declaration = 48, + sym_region_declaration = 49, + sym_extend_region_declaration = 50, + sym_region_data_entry = 51, + sym_section = 52, + sym_entry = 53, + sym_define_declaration = 54, + sym_extern_define_declaration = 55, + sym_parameters = 56, + sym_parameter = 57, + sym_enum_declaration = 58, + sym_extern_enum_declaration = 59, + sym_enum_member = 60, + sym_extern_enum_entry = 61, + sym_expression = 62, + sym_primary_expression = 63, + sym_boolean = 64, + sym_list_expression = 65, + sym_call_expression = 66, + sym_argument = 67, + sym_named_argument = 68, + sym_member_expression = 69, + sym_unary_expression = 70, + sym_binary_expression = 71, + sym_ternary_expression = 72, + sym_match_expression = 73, + sym_match_arm = 74, + sym_match_pattern = 75, + aux_sym_source_file_repeat1 = 76, + aux_sym_region_declaration_repeat1 = 77, + aux_sym_extend_region_declaration_repeat1 = 78, + aux_sym_section_repeat1 = 79, + aux_sym_parameters_repeat1 = 80, + aux_sym_enum_declaration_repeat1 = 81, + aux_sym_extern_enum_declaration_repeat1 = 82, + aux_sym_list_expression_repeat1 = 83, + aux_sym_call_expression_repeat1 = 84, + aux_sym_match_expression_repeat1 = 85, + aux_sym_match_pattern_repeat1 = 86, +}; + +static const char * const ts_symbol_names[] = { + [ts_builtin_sym_end] = "end", + [sym_identifier] = "identifier", + [sym_comment] = "comment", + [sym_number] = "number", + [sym_string] = "string", + [anon_sym_region] = "region", + [anon_sym_LBRACE] = "{", + [anon_sym_RBRACE] = "}", + [anon_sym_extend] = "extend", + [anon_sym_COLON] = ":", + [anon_sym_events] = "events", + [anon_sym_locations] = "locations", + [anon_sym_exits] = "exits", + [anon_sym_define] = "define", + [anon_sym_LPAREN] = "(", + [anon_sym_RPAREN] = ")", + [anon_sym_extern] = "extern", + [anon_sym_DASH_GT] = "->", + [anon_sym_COMMA] = ",", + [anon_sym_EQ] = "=", + [anon_sym_enum] = "enum", + [sym_glob_pattern] = "glob_pattern", + [anon_sym_here] = "here", + [anon_sym_true] = "true", + [anon_sym_false] = "false", + [anon_sym_always] = "always", + [anon_sym_never] = "never", + [anon_sym_LBRACK] = "[", + [anon_sym_RBRACK] = "]", + [anon_sym_DOT] = ".", + [anon_sym_not] = "not", + [anon_sym_STAR] = "*", + [anon_sym_SLASH] = "/", + [anon_sym_PLUS] = "+", + [anon_sym_DASH] = "-", + [anon_sym_EQ_EQ] = "==", + [anon_sym_BANG_EQ] = "!=", + [anon_sym_GT_EQ] = ">=", + [anon_sym_LT_EQ] = "<=", + [anon_sym_GT] = ">", + [anon_sym_LT] = "<", + [anon_sym_is] = "is", + [anon_sym_and] = "and", + [anon_sym_or] = "or", + [anon_sym_QMARK] = "\?", + [anon_sym_match] = "match", + [anon_sym__] = "_", + [sym_source_file] = "source_file", + [sym_declaration] = "declaration", + [sym_region_declaration] = "region_declaration", + [sym_extend_region_declaration] = "extend_region_declaration", + [sym_region_data_entry] = "region_data_entry", + [sym_section] = "section", + [sym_entry] = "entry", + [sym_define_declaration] = "define_declaration", + [sym_extern_define_declaration] = "extern_define_declaration", + [sym_parameters] = "parameters", + [sym_parameter] = "parameter", + [sym_enum_declaration] = "enum_declaration", + [sym_extern_enum_declaration] = "extern_enum_declaration", + [sym_enum_member] = "enum_member", + [sym_extern_enum_entry] = "extern_enum_entry", + [sym_expression] = "expression", + [sym_primary_expression] = "primary_expression", + [sym_boolean] = "boolean", + [sym_list_expression] = "list_expression", + [sym_call_expression] = "call_expression", + [sym_argument] = "argument", + [sym_named_argument] = "named_argument", + [sym_member_expression] = "member_expression", + [sym_unary_expression] = "unary_expression", + [sym_binary_expression] = "binary_expression", + [sym_ternary_expression] = "ternary_expression", + [sym_match_expression] = "match_expression", + [sym_match_arm] = "match_arm", + [sym_match_pattern] = "match_pattern", + [aux_sym_source_file_repeat1] = "source_file_repeat1", + [aux_sym_region_declaration_repeat1] = "region_declaration_repeat1", + [aux_sym_extend_region_declaration_repeat1] = "extend_region_declaration_repeat1", + [aux_sym_section_repeat1] = "section_repeat1", + [aux_sym_parameters_repeat1] = "parameters_repeat1", + [aux_sym_enum_declaration_repeat1] = "enum_declaration_repeat1", + [aux_sym_extern_enum_declaration_repeat1] = "extern_enum_declaration_repeat1", + [aux_sym_list_expression_repeat1] = "list_expression_repeat1", + [aux_sym_call_expression_repeat1] = "call_expression_repeat1", + [aux_sym_match_expression_repeat1] = "match_expression_repeat1", + [aux_sym_match_pattern_repeat1] = "match_pattern_repeat1", +}; + +static const TSSymbol ts_symbol_map[] = { + [ts_builtin_sym_end] = ts_builtin_sym_end, + [sym_identifier] = sym_identifier, + [sym_comment] = sym_comment, + [sym_number] = sym_number, + [sym_string] = sym_string, + [anon_sym_region] = anon_sym_region, + [anon_sym_LBRACE] = anon_sym_LBRACE, + [anon_sym_RBRACE] = anon_sym_RBRACE, + [anon_sym_extend] = anon_sym_extend, + [anon_sym_COLON] = anon_sym_COLON, + [anon_sym_events] = anon_sym_events, + [anon_sym_locations] = anon_sym_locations, + [anon_sym_exits] = anon_sym_exits, + [anon_sym_define] = anon_sym_define, + [anon_sym_LPAREN] = anon_sym_LPAREN, + [anon_sym_RPAREN] = anon_sym_RPAREN, + [anon_sym_extern] = anon_sym_extern, + [anon_sym_DASH_GT] = anon_sym_DASH_GT, + [anon_sym_COMMA] = anon_sym_COMMA, + [anon_sym_EQ] = anon_sym_EQ, + [anon_sym_enum] = anon_sym_enum, + [sym_glob_pattern] = sym_glob_pattern, + [anon_sym_here] = anon_sym_here, + [anon_sym_true] = anon_sym_true, + [anon_sym_false] = anon_sym_false, + [anon_sym_always] = anon_sym_always, + [anon_sym_never] = anon_sym_never, + [anon_sym_LBRACK] = anon_sym_LBRACK, + [anon_sym_RBRACK] = anon_sym_RBRACK, + [anon_sym_DOT] = anon_sym_DOT, + [anon_sym_not] = anon_sym_not, + [anon_sym_STAR] = anon_sym_STAR, + [anon_sym_SLASH] = anon_sym_SLASH, + [anon_sym_PLUS] = anon_sym_PLUS, + [anon_sym_DASH] = anon_sym_DASH, + [anon_sym_EQ_EQ] = anon_sym_EQ_EQ, + [anon_sym_BANG_EQ] = anon_sym_BANG_EQ, + [anon_sym_GT_EQ] = anon_sym_GT_EQ, + [anon_sym_LT_EQ] = anon_sym_LT_EQ, + [anon_sym_GT] = anon_sym_GT, + [anon_sym_LT] = anon_sym_LT, + [anon_sym_is] = anon_sym_is, + [anon_sym_and] = anon_sym_and, + [anon_sym_or] = anon_sym_or, + [anon_sym_QMARK] = anon_sym_QMARK, + [anon_sym_match] = anon_sym_match, + [anon_sym__] = anon_sym__, + [sym_source_file] = sym_source_file, + [sym_declaration] = sym_declaration, + [sym_region_declaration] = sym_region_declaration, + [sym_extend_region_declaration] = sym_extend_region_declaration, + [sym_region_data_entry] = sym_region_data_entry, + [sym_section] = sym_section, + [sym_entry] = sym_entry, + [sym_define_declaration] = sym_define_declaration, + [sym_extern_define_declaration] = sym_extern_define_declaration, + [sym_parameters] = sym_parameters, + [sym_parameter] = sym_parameter, + [sym_enum_declaration] = sym_enum_declaration, + [sym_extern_enum_declaration] = sym_extern_enum_declaration, + [sym_enum_member] = sym_enum_member, + [sym_extern_enum_entry] = sym_extern_enum_entry, + [sym_expression] = sym_expression, + [sym_primary_expression] = sym_primary_expression, + [sym_boolean] = sym_boolean, + [sym_list_expression] = sym_list_expression, + [sym_call_expression] = sym_call_expression, + [sym_argument] = sym_argument, + [sym_named_argument] = sym_named_argument, + [sym_member_expression] = sym_member_expression, + [sym_unary_expression] = sym_unary_expression, + [sym_binary_expression] = sym_binary_expression, + [sym_ternary_expression] = sym_ternary_expression, + [sym_match_expression] = sym_match_expression, + [sym_match_arm] = sym_match_arm, + [sym_match_pattern] = sym_match_pattern, + [aux_sym_source_file_repeat1] = aux_sym_source_file_repeat1, + [aux_sym_region_declaration_repeat1] = aux_sym_region_declaration_repeat1, + [aux_sym_extend_region_declaration_repeat1] = aux_sym_extend_region_declaration_repeat1, + [aux_sym_section_repeat1] = aux_sym_section_repeat1, + [aux_sym_parameters_repeat1] = aux_sym_parameters_repeat1, + [aux_sym_enum_declaration_repeat1] = aux_sym_enum_declaration_repeat1, + [aux_sym_extern_enum_declaration_repeat1] = aux_sym_extern_enum_declaration_repeat1, + [aux_sym_list_expression_repeat1] = aux_sym_list_expression_repeat1, + [aux_sym_call_expression_repeat1] = aux_sym_call_expression_repeat1, + [aux_sym_match_expression_repeat1] = aux_sym_match_expression_repeat1, + [aux_sym_match_pattern_repeat1] = aux_sym_match_pattern_repeat1, +}; + +static const TSSymbolMetadata ts_symbol_metadata[] = { + [ts_builtin_sym_end] = { + .visible = false, + .named = true, + }, + [sym_identifier] = { + .visible = true, + .named = true, + }, + [sym_comment] = { + .visible = true, + .named = true, + }, + [sym_number] = { + .visible = true, + .named = true, + }, + [sym_string] = { + .visible = true, + .named = true, + }, + [anon_sym_region] = { + .visible = true, + .named = false, + }, + [anon_sym_LBRACE] = { + .visible = true, + .named = false, + }, + [anon_sym_RBRACE] = { + .visible = true, + .named = false, + }, + [anon_sym_extend] = { + .visible = true, + .named = false, + }, + [anon_sym_COLON] = { + .visible = true, + .named = false, + }, + [anon_sym_events] = { + .visible = true, + .named = false, + }, + [anon_sym_locations] = { + .visible = true, + .named = false, + }, + [anon_sym_exits] = { + .visible = true, + .named = false, + }, + [anon_sym_define] = { + .visible = true, + .named = false, + }, + [anon_sym_LPAREN] = { + .visible = true, + .named = false, + }, + [anon_sym_RPAREN] = { + .visible = true, + .named = false, + }, + [anon_sym_extern] = { + .visible = true, + .named = false, + }, + [anon_sym_DASH_GT] = { + .visible = true, + .named = false, + }, + [anon_sym_COMMA] = { + .visible = true, + .named = false, + }, + [anon_sym_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_enum] = { + .visible = true, + .named = false, + }, + [sym_glob_pattern] = { + .visible = true, + .named = true, + }, + [anon_sym_here] = { + .visible = true, + .named = false, + }, + [anon_sym_true] = { + .visible = true, + .named = false, + }, + [anon_sym_false] = { + .visible = true, + .named = false, + }, + [anon_sym_always] = { + .visible = true, + .named = false, + }, + [anon_sym_never] = { + .visible = true, + .named = false, + }, + [anon_sym_LBRACK] = { + .visible = true, + .named = false, + }, + [anon_sym_RBRACK] = { + .visible = true, + .named = false, + }, + [anon_sym_DOT] = { + .visible = true, + .named = false, + }, + [anon_sym_not] = { + .visible = true, + .named = false, + }, + [anon_sym_STAR] = { + .visible = true, + .named = false, + }, + [anon_sym_SLASH] = { + .visible = true, + .named = false, + }, + [anon_sym_PLUS] = { + .visible = true, + .named = false, + }, + [anon_sym_DASH] = { + .visible = true, + .named = false, + }, + [anon_sym_EQ_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_BANG_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_GT_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_LT_EQ] = { + .visible = true, + .named = false, + }, + [anon_sym_GT] = { + .visible = true, + .named = false, + }, + [anon_sym_LT] = { + .visible = true, + .named = false, + }, + [anon_sym_is] = { + .visible = true, + .named = false, + }, + [anon_sym_and] = { + .visible = true, + .named = false, + }, + [anon_sym_or] = { + .visible = true, + .named = false, + }, + [anon_sym_QMARK] = { + .visible = true, + .named = false, + }, + [anon_sym_match] = { + .visible = true, + .named = false, + }, + [anon_sym__] = { + .visible = true, + .named = false, + }, + [sym_source_file] = { + .visible = true, + .named = true, + }, + [sym_declaration] = { + .visible = true, + .named = true, + }, + [sym_region_declaration] = { + .visible = true, + .named = true, + }, + [sym_extend_region_declaration] = { + .visible = true, + .named = true, + }, + [sym_region_data_entry] = { + .visible = true, + .named = true, + }, + [sym_section] = { + .visible = true, + .named = true, + }, + [sym_entry] = { + .visible = true, + .named = true, + }, + [sym_define_declaration] = { + .visible = true, + .named = true, + }, + [sym_extern_define_declaration] = { + .visible = true, + .named = true, + }, + [sym_parameters] = { + .visible = true, + .named = true, + }, + [sym_parameter] = { + .visible = true, + .named = true, + }, + [sym_enum_declaration] = { + .visible = true, + .named = true, + }, + [sym_extern_enum_declaration] = { + .visible = true, + .named = true, + }, + [sym_enum_member] = { + .visible = true, + .named = true, + }, + [sym_extern_enum_entry] = { + .visible = true, + .named = true, + }, + [sym_expression] = { + .visible = true, + .named = true, + }, + [sym_primary_expression] = { + .visible = true, + .named = true, + }, + [sym_boolean] = { + .visible = true, + .named = true, + }, + [sym_list_expression] = { + .visible = true, + .named = true, + }, + [sym_call_expression] = { + .visible = true, + .named = true, + }, + [sym_argument] = { + .visible = true, + .named = true, + }, + [sym_named_argument] = { + .visible = true, + .named = true, + }, + [sym_member_expression] = { + .visible = true, + .named = true, + }, + [sym_unary_expression] = { + .visible = true, + .named = true, + }, + [sym_binary_expression] = { + .visible = true, + .named = true, + }, + [sym_ternary_expression] = { + .visible = true, + .named = true, + }, + [sym_match_expression] = { + .visible = true, + .named = true, + }, + [sym_match_arm] = { + .visible = true, + .named = true, + }, + [sym_match_pattern] = { + .visible = true, + .named = true, + }, + [aux_sym_source_file_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_region_declaration_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_extend_region_declaration_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_section_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_parameters_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_enum_declaration_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_extern_enum_declaration_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_list_expression_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_call_expression_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_match_expression_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_match_pattern_repeat1] = { + .visible = false, + .named = false, + }, +}; + +enum { + field_argument = 1, + field_body = 2, + field_default = 3, + field_function = 4, + field_key = 5, + field_kind = 6, + field_member = 7, + field_name = 8, + field_object = 9, + field_return_type = 10, + field_subject = 11, + field_type = 12, + field_value = 13, +}; + +static const char * const ts_field_names[] = { + [0] = NULL, + [field_argument] = "argument", + [field_body] = "body", + [field_default] = "default", + [field_function] = "function", + [field_key] = "key", + [field_kind] = "kind", + [field_member] = "member", + [field_name] = "name", + [field_object] = "object", + [field_return_type] = "return_type", + [field_subject] = "subject", + [field_type] = "type", + [field_value] = "value", +}; + +static const TSFieldMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = { + [1] = {.index = 0, .length = 1}, + [2] = {.index = 1, .length = 1}, + [3] = {.index = 2, .length = 1}, + [4] = {.index = 3, .length = 2}, + [5] = {.index = 5, .length = 1}, + [6] = {.index = 6, .length = 2}, + [7] = {.index = 8, .length = 2}, + [8] = {.index = 10, .length = 2}, + [9] = {.index = 12, .length = 2}, + [10] = {.index = 14, .length = 1}, + [11] = {.index = 15, .length = 2}, + [12] = {.index = 17, .length = 2}, + [13] = {.index = 19, .length = 1}, + [14] = {.index = 20, .length = 2}, + [15] = {.index = 22, .length = 3}, + [16] = {.index = 25, .length = 2}, + [17] = {.index = 27, .length = 1}, +}; + +static const TSFieldMapEntry ts_field_map_entries[] = { + [0] = + {field_name, 1}, + [1] = + {field_name, 0}, + [2] = + {field_name, 2}, + [3] = + {field_key, 0}, + {field_value, 2}, + [5] = + {field_kind, 0}, + [6] = + {field_name, 0}, + {field_type, 2}, + [8] = + {field_default, 2}, + {field_name, 0}, + [10] = + {field_body, 5}, + {field_name, 1}, + [12] = + {field_name, 0}, + {field_value, 2}, + [14] = + {field_argument, 1}, + [15] = + {field_body, 6}, + {field_name, 1}, + [17] = + {field_name, 2}, + {field_return_type, 6}, + [19] = + {field_function, 0}, + [20] = + {field_member, 2}, + {field_object, 0}, + [22] = + {field_default, 4}, + {field_name, 0}, + {field_type, 2}, + [25] = + {field_name, 2}, + {field_return_type, 7}, + [27] = + {field_subject, 1}, +}; + +static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { + [0] = {0}, +}; + +static const uint16_t ts_non_terminal_alias_map[] = { + 0, +}; + +static const TSStateId ts_primary_state_ids[STATE_COUNT] = { + [0] = 0, + [1] = 1, + [2] = 2, + [3] = 3, + [4] = 4, + [5] = 5, + [6] = 6, + [7] = 7, + [8] = 8, + [9] = 9, + [10] = 10, + [11] = 11, + [12] = 12, + [13] = 13, + [14] = 14, + [15] = 15, + [16] = 16, + [17] = 17, + [18] = 18, + [19] = 19, + [20] = 20, + [21] = 21, + [22] = 22, + [23] = 23, + [24] = 24, + [25] = 25, + [26] = 26, + [27] = 27, + [28] = 28, + [29] = 29, + [30] = 30, + [31] = 31, + [32] = 32, + [33] = 33, + [34] = 34, + [35] = 35, + [36] = 36, + [37] = 37, + [38] = 38, + [39] = 39, + [40] = 40, + [41] = 41, + [42] = 42, + [43] = 43, + [44] = 44, + [45] = 45, + [46] = 46, + [47] = 47, + [48] = 48, + [49] = 49, + [50] = 50, + [51] = 51, + [52] = 52, + [53] = 53, + [54] = 54, + [55] = 55, + [56] = 56, + [57] = 57, + [58] = 58, + [59] = 59, + [60] = 60, + [61] = 61, + [62] = 62, + [63] = 63, + [64] = 64, + [65] = 65, + [66] = 66, + [67] = 67, + [68] = 68, + [69] = 69, + [70] = 70, + [71] = 71, + [72] = 72, + [73] = 73, + [74] = 74, + [75] = 75, + [76] = 76, + [77] = 77, + [78] = 78, + [79] = 79, + [80] = 80, + [81] = 81, + [82] = 82, + [83] = 83, + [84] = 84, + [85] = 85, + [86] = 86, + [87] = 87, + [88] = 88, + [89] = 89, + [90] = 90, + [91] = 91, + [92] = 92, + [93] = 93, + [94] = 94, + [95] = 95, + [96] = 96, + [97] = 97, + [98] = 98, + [99] = 99, + [100] = 100, + [101] = 101, + [102] = 102, + [103] = 103, + [104] = 104, + [105] = 105, + [106] = 106, + [107] = 107, + [108] = 108, + [109] = 109, + [110] = 110, + [111] = 111, + [112] = 112, + [113] = 113, + [114] = 114, + [115] = 115, + [116] = 116, + [117] = 117, + [118] = 118, + [119] = 119, + [120] = 120, + [121] = 121, + [122] = 122, + [123] = 123, + [124] = 124, + [125] = 125, + [126] = 126, + [127] = 127, + [128] = 128, + [129] = 129, + [130] = 130, + [131] = 131, + [132] = 132, + [133] = 133, + [134] = 134, + [135] = 135, + [136] = 136, + [137] = 137, + [138] = 138, + [139] = 139, + [140] = 140, + [141] = 141, + [142] = 142, + [143] = 143, + [144] = 144, + [145] = 145, + [146] = 146, + [147] = 147, + [148] = 148, + [149] = 149, + [150] = 150, + [151] = 151, + [152] = 152, + [153] = 153, + [154] = 154, + [155] = 155, + [156] = 156, + [157] = 157, + [158] = 158, +}; + +static bool ts_lex(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + if (eof) ADVANCE(10); + if (lookahead == '!') ADVANCE(5); + if (lookahead == '"') ADVANCE(2); + if (lookahead == '#') ADVANCE(11); + if (lookahead == '(') ADVANCE(19); + if (lookahead == ')') ADVANCE(20); + if (lookahead == '*') ADVANCE(29); + if (lookahead == '+') ADVANCE(31); + if (lookahead == ',') ADVANCE(22); + if (lookahead == '-') ADVANCE(33); + if (lookahead == '.') ADVANCE(28); + if (lookahead == '/') ADVANCE(30); + if (lookahead == ':') ADVANCE(18); + if (lookahead == '<') ADVANCE(39); + if (lookahead == '=') ADVANCE(24); + if (lookahead == '>') ADVANCE(38); + if (lookahead == '?') ADVANCE(40); + if (lookahead == '[') ADVANCE(26); + if (lookahead == ']') ADVANCE(27); + if (lookahead == '{') ADVANCE(16); + if (lookahead == '}') ADVANCE(17); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') SKIP(0) + if (('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(13); + END_STATE(); + case 1: + if (lookahead == '"') ADVANCE(2); + if (lookahead == '#') ADVANCE(11); + if (lookahead == '(') ADVANCE(19); + if (lookahead == ')') ADVANCE(20); + if (lookahead == ',') ADVANCE(22); + if (lookahead == '-') ADVANCE(7); + if (lookahead == ':') ADVANCE(18); + if (lookahead == '=') ADVANCE(23); + if (lookahead == '[') ADVANCE(26); + if (lookahead == ']') ADVANCE(27); + if (lookahead == '}') ADVANCE(17); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') SKIP(1) + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(14); + if (('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(13); + END_STATE(); + case 2: + if (lookahead == '"') ADVANCE(15); + if (lookahead == '\\') ADVANCE(8); + if (lookahead != 0 && + lookahead != '\n') ADVANCE(2); + END_STATE(); + case 3: + if (lookahead == '#') ADVANCE(11); + if (lookahead == '*') ADVANCE(25); + if (lookahead == '}') ADVANCE(17); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') SKIP(3) + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(4); + if (('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(12); + END_STATE(); + case 4: + if (lookahead == '*') ADVANCE(25); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(4); + END_STATE(); + case 5: + if (lookahead == '=') ADVANCE(35); + END_STATE(); + case 6: + if (lookahead == '=') ADVANCE(34); + END_STATE(); + case 7: + if (lookahead == '>') ADVANCE(21); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(14); + END_STATE(); + case 8: + if (lookahead == '"' || + lookahead == '\\') ADVANCE(2); + END_STATE(); + case 9: + if (eof) ADVANCE(10); + if (lookahead == '!') ADVANCE(5); + if (lookahead == '#') ADVANCE(11); + if (lookahead == '(') ADVANCE(19); + if (lookahead == ')') ADVANCE(20); + if (lookahead == '*') ADVANCE(29); + if (lookahead == '+') ADVANCE(31); + if (lookahead == ',') ADVANCE(22); + if (lookahead == '-') ADVANCE(32); + if (lookahead == '.') ADVANCE(28); + if (lookahead == '/') ADVANCE(30); + if (lookahead == ':') ADVANCE(18); + if (lookahead == '<') ADVANCE(39); + if (lookahead == '=') ADVANCE(6); + if (lookahead == '>') ADVANCE(38); + if (lookahead == '?') ADVANCE(40); + if (lookahead == ']') ADVANCE(27); + if (lookahead == '}') ADVANCE(17); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') SKIP(9) + if (('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(13); + END_STATE(); + case 10: + ACCEPT_TOKEN(ts_builtin_sym_end); + END_STATE(); + case 11: + ACCEPT_TOKEN(sym_comment); + if (lookahead != 0 && + lookahead != '\n') ADVANCE(11); + END_STATE(); + case 12: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == '*') ADVANCE(25); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(12); + END_STATE(); + case 13: + ACCEPT_TOKEN(sym_identifier); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(13); + END_STATE(); + case 14: + ACCEPT_TOKEN(sym_number); + if (('0' <= lookahead && lookahead <= '9')) ADVANCE(14); + END_STATE(); + case 15: + ACCEPT_TOKEN(sym_string); + END_STATE(); + case 16: + ACCEPT_TOKEN(anon_sym_LBRACE); + END_STATE(); + case 17: + ACCEPT_TOKEN(anon_sym_RBRACE); + END_STATE(); + case 18: + ACCEPT_TOKEN(anon_sym_COLON); + END_STATE(); + case 19: + ACCEPT_TOKEN(anon_sym_LPAREN); + END_STATE(); + case 20: + ACCEPT_TOKEN(anon_sym_RPAREN); + END_STATE(); + case 21: + ACCEPT_TOKEN(anon_sym_DASH_GT); + END_STATE(); + case 22: + ACCEPT_TOKEN(anon_sym_COMMA); + END_STATE(); + case 23: + ACCEPT_TOKEN(anon_sym_EQ); + END_STATE(); + case 24: + ACCEPT_TOKEN(anon_sym_EQ); + if (lookahead == '=') ADVANCE(34); + END_STATE(); + case 25: + ACCEPT_TOKEN(sym_glob_pattern); + if (lookahead == '*' || + ('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(25); + END_STATE(); + case 26: + ACCEPT_TOKEN(anon_sym_LBRACK); + END_STATE(); + case 27: + ACCEPT_TOKEN(anon_sym_RBRACK); + END_STATE(); + case 28: + ACCEPT_TOKEN(anon_sym_DOT); + END_STATE(); + case 29: + ACCEPT_TOKEN(anon_sym_STAR); + END_STATE(); + case 30: + ACCEPT_TOKEN(anon_sym_SLASH); + END_STATE(); + case 31: + ACCEPT_TOKEN(anon_sym_PLUS); + END_STATE(); + case 32: + ACCEPT_TOKEN(anon_sym_DASH); + END_STATE(); + case 33: + ACCEPT_TOKEN(anon_sym_DASH); + if (lookahead == '>') ADVANCE(21); + END_STATE(); + case 34: + ACCEPT_TOKEN(anon_sym_EQ_EQ); + END_STATE(); + case 35: + ACCEPT_TOKEN(anon_sym_BANG_EQ); + END_STATE(); + case 36: + ACCEPT_TOKEN(anon_sym_GT_EQ); + END_STATE(); + case 37: + ACCEPT_TOKEN(anon_sym_LT_EQ); + END_STATE(); + case 38: + ACCEPT_TOKEN(anon_sym_GT); + if (lookahead == '=') ADVANCE(36); + END_STATE(); + case 39: + ACCEPT_TOKEN(anon_sym_LT); + if (lookahead == '=') ADVANCE(37); + END_STATE(); + case 40: + ACCEPT_TOKEN(anon_sym_QMARK); + END_STATE(); + default: + return false; + } +} + +static bool ts_lex_keywords(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + if (lookahead == '_') ADVANCE(1); + if (lookahead == 'a') ADVANCE(2); + if (lookahead == 'd') ADVANCE(3); + if (lookahead == 'e') ADVANCE(4); + if (lookahead == 'f') ADVANCE(5); + if (lookahead == 'h') ADVANCE(6); + if (lookahead == 'i') ADVANCE(7); + if (lookahead == 'l') ADVANCE(8); + if (lookahead == 'm') ADVANCE(9); + if (lookahead == 'n') ADVANCE(10); + if (lookahead == 'o') ADVANCE(11); + if (lookahead == 'r') ADVANCE(12); + if (lookahead == 't') ADVANCE(13); + if (lookahead == '\t' || + lookahead == '\n' || + lookahead == '\r' || + lookahead == ' ') SKIP(0) + END_STATE(); + case 1: + ACCEPT_TOKEN(anon_sym__); + END_STATE(); + case 2: + if (lookahead == 'l') ADVANCE(14); + if (lookahead == 'n') ADVANCE(15); + END_STATE(); + case 3: + if (lookahead == 'e') ADVANCE(16); + END_STATE(); + case 4: + if (lookahead == 'n') ADVANCE(17); + if (lookahead == 'v') ADVANCE(18); + if (lookahead == 'x') ADVANCE(19); + END_STATE(); + case 5: + if (lookahead == 'a') ADVANCE(20); + END_STATE(); + case 6: + if (lookahead == 'e') ADVANCE(21); + END_STATE(); + case 7: + if (lookahead == 's') ADVANCE(22); + END_STATE(); + case 8: + if (lookahead == 'o') ADVANCE(23); + END_STATE(); + case 9: + if (lookahead == 'a') ADVANCE(24); + END_STATE(); + case 10: + if (lookahead == 'e') ADVANCE(25); + if (lookahead == 'o') ADVANCE(26); + END_STATE(); + case 11: + if (lookahead == 'r') ADVANCE(27); + END_STATE(); + case 12: + if (lookahead == 'e') ADVANCE(28); + END_STATE(); + case 13: + if (lookahead == 'r') ADVANCE(29); + END_STATE(); + case 14: + if (lookahead == 'w') ADVANCE(30); + END_STATE(); + case 15: + if (lookahead == 'd') ADVANCE(31); + END_STATE(); + case 16: + if (lookahead == 'f') ADVANCE(32); + END_STATE(); + case 17: + if (lookahead == 'u') ADVANCE(33); + END_STATE(); + case 18: + if (lookahead == 'e') ADVANCE(34); + END_STATE(); + case 19: + if (lookahead == 'i') ADVANCE(35); + if (lookahead == 't') ADVANCE(36); + END_STATE(); + case 20: + if (lookahead == 'l') ADVANCE(37); + END_STATE(); + case 21: + if (lookahead == 'r') ADVANCE(38); + END_STATE(); + case 22: + ACCEPT_TOKEN(anon_sym_is); + END_STATE(); + case 23: + if (lookahead == 'c') ADVANCE(39); + END_STATE(); + case 24: + if (lookahead == 't') ADVANCE(40); + END_STATE(); + case 25: + if (lookahead == 'v') ADVANCE(41); + END_STATE(); + case 26: + if (lookahead == 't') ADVANCE(42); + END_STATE(); + case 27: + ACCEPT_TOKEN(anon_sym_or); + END_STATE(); + case 28: + if (lookahead == 'g') ADVANCE(43); + END_STATE(); + case 29: + if (lookahead == 'u') ADVANCE(44); + END_STATE(); + case 30: + if (lookahead == 'a') ADVANCE(45); + END_STATE(); + case 31: + ACCEPT_TOKEN(anon_sym_and); + END_STATE(); + case 32: + if (lookahead == 'i') ADVANCE(46); + END_STATE(); + case 33: + if (lookahead == 'm') ADVANCE(47); + END_STATE(); + case 34: + if (lookahead == 'n') ADVANCE(48); + END_STATE(); + case 35: + if (lookahead == 't') ADVANCE(49); + END_STATE(); + case 36: + if (lookahead == 'e') ADVANCE(50); + END_STATE(); + case 37: + if (lookahead == 's') ADVANCE(51); + END_STATE(); + case 38: + if (lookahead == 'e') ADVANCE(52); + END_STATE(); + case 39: + if (lookahead == 'a') ADVANCE(53); + END_STATE(); + case 40: + if (lookahead == 'c') ADVANCE(54); + END_STATE(); + case 41: + if (lookahead == 'e') ADVANCE(55); + END_STATE(); + case 42: + ACCEPT_TOKEN(anon_sym_not); + END_STATE(); + case 43: + if (lookahead == 'i') ADVANCE(56); + END_STATE(); + case 44: + if (lookahead == 'e') ADVANCE(57); + END_STATE(); + case 45: + if (lookahead == 'y') ADVANCE(58); + END_STATE(); + case 46: + if (lookahead == 'n') ADVANCE(59); + END_STATE(); + case 47: + ACCEPT_TOKEN(anon_sym_enum); + END_STATE(); + case 48: + if (lookahead == 't') ADVANCE(60); + END_STATE(); + case 49: + if (lookahead == 's') ADVANCE(61); + END_STATE(); + case 50: + if (lookahead == 'n') ADVANCE(62); + if (lookahead == 'r') ADVANCE(63); + END_STATE(); + case 51: + if (lookahead == 'e') ADVANCE(64); + END_STATE(); + case 52: + ACCEPT_TOKEN(anon_sym_here); + END_STATE(); + case 53: + if (lookahead == 't') ADVANCE(65); + END_STATE(); + case 54: + if (lookahead == 'h') ADVANCE(66); + END_STATE(); + case 55: + if (lookahead == 'r') ADVANCE(67); + END_STATE(); + case 56: + if (lookahead == 'o') ADVANCE(68); + END_STATE(); + case 57: + ACCEPT_TOKEN(anon_sym_true); + END_STATE(); + case 58: + if (lookahead == 's') ADVANCE(69); + END_STATE(); + case 59: + if (lookahead == 'e') ADVANCE(70); + END_STATE(); + case 60: + if (lookahead == 's') ADVANCE(71); + END_STATE(); + case 61: + ACCEPT_TOKEN(anon_sym_exits); + END_STATE(); + case 62: + if (lookahead == 'd') ADVANCE(72); + END_STATE(); + case 63: + if (lookahead == 'n') ADVANCE(73); + END_STATE(); + case 64: + ACCEPT_TOKEN(anon_sym_false); + END_STATE(); + case 65: + if (lookahead == 'i') ADVANCE(74); + END_STATE(); + case 66: + ACCEPT_TOKEN(anon_sym_match); + END_STATE(); + case 67: + ACCEPT_TOKEN(anon_sym_never); + END_STATE(); + case 68: + if (lookahead == 'n') ADVANCE(75); + END_STATE(); + case 69: + ACCEPT_TOKEN(anon_sym_always); + END_STATE(); + case 70: + ACCEPT_TOKEN(anon_sym_define); + END_STATE(); + case 71: + ACCEPT_TOKEN(anon_sym_events); + END_STATE(); + case 72: + ACCEPT_TOKEN(anon_sym_extend); + END_STATE(); + case 73: + ACCEPT_TOKEN(anon_sym_extern); + END_STATE(); + case 74: + if (lookahead == 'o') ADVANCE(76); + END_STATE(); + case 75: + ACCEPT_TOKEN(anon_sym_region); + END_STATE(); + case 76: + if (lookahead == 'n') ADVANCE(77); + END_STATE(); + case 77: + if (lookahead == 's') ADVANCE(78); + END_STATE(); + case 78: + ACCEPT_TOKEN(anon_sym_locations); + END_STATE(); + default: + return false; + } +} + +static const TSLexMode ts_lex_modes[STATE_COUNT] = { + [0] = {.lex_state = 0}, + [1] = {.lex_state = 0}, + [2] = {.lex_state = 9}, + [3] = {.lex_state = 9}, + [4] = {.lex_state = 9}, + [5] = {.lex_state = 9}, + [6] = {.lex_state = 9}, + [7] = {.lex_state = 9}, + [8] = {.lex_state = 9}, + [9] = {.lex_state = 9}, + [10] = {.lex_state = 9}, + [11] = {.lex_state = 9}, + [12] = {.lex_state = 9}, + [13] = {.lex_state = 9}, + [14] = {.lex_state = 9}, + [15] = {.lex_state = 9}, + [16] = {.lex_state = 9}, + [17] = {.lex_state = 9}, + [18] = {.lex_state = 9}, + [19] = {.lex_state = 9}, + [20] = {.lex_state = 9}, + [21] = {.lex_state = 9}, + [22] = {.lex_state = 9}, + [23] = {.lex_state = 1}, + [24] = {.lex_state = 1}, + [25] = {.lex_state = 1}, + [26] = {.lex_state = 1}, + [27] = {.lex_state = 1}, + [28] = {.lex_state = 1}, + [29] = {.lex_state = 1}, + [30] = {.lex_state = 1}, + [31] = {.lex_state = 1}, + [32] = {.lex_state = 1}, + [33] = {.lex_state = 1}, + [34] = {.lex_state = 1}, + [35] = {.lex_state = 1}, + [36] = {.lex_state = 1}, + [37] = {.lex_state = 1}, + [38] = {.lex_state = 1}, + [39] = {.lex_state = 1}, + [40] = {.lex_state = 1}, + [41] = {.lex_state = 1}, + [42] = {.lex_state = 1}, + [43] = {.lex_state = 1}, + [44] = {.lex_state = 1}, + [45] = {.lex_state = 1}, + [46] = {.lex_state = 1}, + [47] = {.lex_state = 9}, + [48] = {.lex_state = 9}, + [49] = {.lex_state = 9}, + [50] = {.lex_state = 9}, + [51] = {.lex_state = 9}, + [52] = {.lex_state = 9}, + [53] = {.lex_state = 9}, + [54] = {.lex_state = 9}, + [55] = {.lex_state = 9}, + [56] = {.lex_state = 9}, + [57] = {.lex_state = 9}, + [58] = {.lex_state = 9}, + [59] = {.lex_state = 9}, + [60] = {.lex_state = 9}, + [61] = {.lex_state = 0}, + [62] = {.lex_state = 0}, + [63] = {.lex_state = 0}, + [64] = {.lex_state = 0}, + [65] = {.lex_state = 0}, + [66] = {.lex_state = 0}, + [67] = {.lex_state = 0}, + [68] = {.lex_state = 0}, + [69] = {.lex_state = 0}, + [70] = {.lex_state = 0}, + [71] = {.lex_state = 0}, + [72] = {.lex_state = 0}, + [73] = {.lex_state = 0}, + [74] = {.lex_state = 0}, + [75] = {.lex_state = 0}, + [76] = {.lex_state = 0}, + [77] = {.lex_state = 0}, + [78] = {.lex_state = 0}, + [79] = {.lex_state = 0}, + [80] = {.lex_state = 0}, + [81] = {.lex_state = 0}, + [82] = {.lex_state = 0}, + [83] = {.lex_state = 0}, + [84] = {.lex_state = 3}, + [85] = {.lex_state = 0}, + [86] = {.lex_state = 0}, + [87] = {.lex_state = 0}, + [88] = {.lex_state = 0}, + [89] = {.lex_state = 0}, + [90] = {.lex_state = 3}, + [91] = {.lex_state = 0}, + [92] = {.lex_state = 1}, + [93] = {.lex_state = 0}, + [94] = {.lex_state = 0}, + [95] = {.lex_state = 1}, + [96] = {.lex_state = 0}, + [97] = {.lex_state = 0}, + [98] = {.lex_state = 0}, + [99] = {.lex_state = 0}, + [100] = {.lex_state = 0}, + [101] = {.lex_state = 0}, + [102] = {.lex_state = 0}, + [103] = {.lex_state = 0}, + [104] = {.lex_state = 0}, + [105] = {.lex_state = 0}, + [106] = {.lex_state = 0}, + [107] = {.lex_state = 1}, + [108] = {.lex_state = 0}, + [109] = {.lex_state = 0}, + [110] = {.lex_state = 0}, + [111] = {.lex_state = 0}, + [112] = {.lex_state = 0}, + [113] = {.lex_state = 0}, + [114] = {.lex_state = 0}, + [115] = {.lex_state = 0}, + [116] = {.lex_state = 0}, + [117] = {.lex_state = 0}, + [118] = {.lex_state = 0}, + [119] = {.lex_state = 0}, + [120] = {.lex_state = 0}, + [121] = {.lex_state = 0}, + [122] = {.lex_state = 0}, + [123] = {.lex_state = 0}, + [124] = {.lex_state = 0}, + [125] = {.lex_state = 0}, + [126] = {.lex_state = 0}, + [127] = {.lex_state = 0}, + [128] = {.lex_state = 0}, + [129] = {.lex_state = 0}, + [130] = {.lex_state = 0}, + [131] = {.lex_state = 0}, + [132] = {.lex_state = 0}, + [133] = {.lex_state = 0}, + [134] = {.lex_state = 0}, + [135] = {.lex_state = 1}, + [136] = {.lex_state = 0}, + [137] = {.lex_state = 0}, + [138] = {.lex_state = 0}, + [139] = {.lex_state = 0}, + [140] = {.lex_state = 0}, + [141] = {.lex_state = 0}, + [142] = {.lex_state = 0}, + [143] = {.lex_state = 0}, + [144] = {.lex_state = 0}, + [145] = {.lex_state = 0}, + [146] = {.lex_state = 0}, + [147] = {.lex_state = 0}, + [148] = {.lex_state = 0}, + [149] = {.lex_state = 0}, + [150] = {.lex_state = 1}, + [151] = {.lex_state = 0}, + [152] = {.lex_state = 0}, + [153] = {.lex_state = 0}, + [154] = {.lex_state = 0}, + [155] = {.lex_state = 0}, + [156] = {.lex_state = 1}, + [157] = {.lex_state = 0}, + [158] = {.lex_state = 0}, +}; + +static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { + [0] = { + [ts_builtin_sym_end] = ACTIONS(1), + [sym_identifier] = ACTIONS(1), + [sym_comment] = ACTIONS(3), + [sym_string] = ACTIONS(1), + [anon_sym_region] = ACTIONS(1), + [anon_sym_LBRACE] = ACTIONS(1), + [anon_sym_RBRACE] = ACTIONS(1), + [anon_sym_extend] = ACTIONS(1), + [anon_sym_COLON] = ACTIONS(1), + [anon_sym_events] = ACTIONS(1), + [anon_sym_locations] = ACTIONS(1), + [anon_sym_exits] = ACTIONS(1), + [anon_sym_define] = ACTIONS(1), + [anon_sym_LPAREN] = ACTIONS(1), + [anon_sym_RPAREN] = ACTIONS(1), + [anon_sym_extern] = ACTIONS(1), + [anon_sym_DASH_GT] = ACTIONS(1), + [anon_sym_COMMA] = ACTIONS(1), + [anon_sym_EQ] = ACTIONS(1), + [anon_sym_enum] = ACTIONS(1), + [anon_sym_here] = ACTIONS(1), + [anon_sym_true] = ACTIONS(1), + [anon_sym_false] = ACTIONS(1), + [anon_sym_always] = ACTIONS(1), + [anon_sym_never] = ACTIONS(1), + [anon_sym_LBRACK] = ACTIONS(1), + [anon_sym_RBRACK] = ACTIONS(1), + [anon_sym_DOT] = ACTIONS(1), + [anon_sym_not] = ACTIONS(1), + [anon_sym_STAR] = ACTIONS(1), + [anon_sym_SLASH] = ACTIONS(1), + [anon_sym_PLUS] = ACTIONS(1), + [anon_sym_DASH] = ACTIONS(1), + [anon_sym_EQ_EQ] = ACTIONS(1), + [anon_sym_BANG_EQ] = ACTIONS(1), + [anon_sym_GT_EQ] = ACTIONS(1), + [anon_sym_LT_EQ] = ACTIONS(1), + [anon_sym_GT] = ACTIONS(1), + [anon_sym_LT] = ACTIONS(1), + [anon_sym_is] = ACTIONS(1), + [anon_sym_and] = ACTIONS(1), + [anon_sym_or] = ACTIONS(1), + [anon_sym_QMARK] = ACTIONS(1), + [anon_sym_match] = ACTIONS(1), + [anon_sym__] = ACTIONS(1), + }, + [1] = { + [sym_source_file] = STATE(151), + [sym_declaration] = STATE(62), + [sym_region_declaration] = STATE(69), + [sym_extend_region_declaration] = STATE(69), + [sym_define_declaration] = STATE(69), + [sym_extern_define_declaration] = STATE(69), + [sym_enum_declaration] = STATE(69), + [sym_extern_enum_declaration] = STATE(69), + [aux_sym_source_file_repeat1] = STATE(62), + [ts_builtin_sym_end] = ACTIONS(5), + [sym_comment] = ACTIONS(3), + [anon_sym_region] = ACTIONS(7), + [anon_sym_extend] = ACTIONS(9), + [anon_sym_define] = ACTIONS(11), + [anon_sym_extern] = ACTIONS(13), + [anon_sym_enum] = ACTIONS(15), + }, +}; + +static const uint16_t ts_small_parse_table[] = { + [0] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(21), 1, + anon_sym_LPAREN, + ACTIONS(23), 1, + anon_sym_DOT, + ACTIONS(17), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(19), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [44] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(25), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(27), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [82] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(29), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(31), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [120] = 11, + ACTIONS(3), 1, + sym_comment, + ACTIONS(45), 1, + anon_sym_is, + ACTIONS(47), 1, + anon_sym_and, + ACTIONS(49), 1, + anon_sym_or, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + ACTIONS(33), 6, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + ACTIONS(35), 10, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym__, + [174] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(53), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(55), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [212] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(57), 13, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(59), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [252] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(61), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(63), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [290] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(17), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(19), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [328] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(65), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(67), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [366] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(69), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(71), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [404] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(73), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(75), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [442] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(77), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(79), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [480] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(57), 11, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(59), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [522] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(81), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(83), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [560] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(69), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(71), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [598] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(85), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(87), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [636] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(89), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(91), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [674] = 8, + ACTIONS(3), 1, + sym_comment, + ACTIONS(45), 1, + anon_sym_is, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + ACTIONS(57), 7, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_QMARK, + ACTIONS(59), 12, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_and, + anon_sym_or, + anon_sym__, + [722] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(57), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(59), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [760] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(93), 15, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_QMARK, + ACTIONS(95), 15, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_GT, + anon_sym_LT, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym__, + [798] = 9, + ACTIONS(3), 1, + sym_comment, + ACTIONS(45), 1, + anon_sym_is, + ACTIONS(47), 1, + anon_sym_and, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + ACTIONS(57), 7, + ts_builtin_sym_end, + anon_sym_RBRACE, + anon_sym_COLON, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_RBRACK, + anon_sym_QMARK, + ACTIONS(59), 11, + sym_identifier, + anon_sym_region, + anon_sym_extend, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + anon_sym_or, + anon_sym__, + [848] = 15, + ACTIONS(3), 1, + sym_comment, + ACTIONS(97), 1, + sym_identifier, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(103), 1, + anon_sym_RPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + STATE(58), 1, + sym_expression, + STATE(114), 1, + sym_argument, + STATE(115), 1, + sym_named_argument, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [905] = 14, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + ACTIONS(117), 1, + anon_sym_RBRACE, + ACTIONS(119), 1, + anon_sym__, + STATE(22), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [959] = 14, + ACTIONS(3), 1, + sym_comment, + ACTIONS(97), 1, + sym_identifier, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + STATE(58), 1, + sym_expression, + STATE(115), 1, + sym_named_argument, + STATE(119), 1, + sym_argument, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1013] = 13, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + ACTIONS(121), 1, + anon_sym_RBRACK, + STATE(51), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1064] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(14), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1112] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(19), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1160] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(47), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1208] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(11), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1256] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(16), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1304] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(55), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1352] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(52), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1400] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(59), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1448] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(5), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1496] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(20), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1544] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(50), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1592] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(54), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1640] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(7), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1688] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(57), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1736] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(53), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1784] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(60), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1832] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(22), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1880] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(56), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1928] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(111), 1, + anon_sym_not, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + STATE(48), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [1976] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(101), 1, + anon_sym_LPAREN, + ACTIONS(105), 1, + anon_sym_here, + ACTIONS(109), 1, + anon_sym_LBRACK, + ACTIONS(113), 1, + anon_sym_match, + ACTIONS(115), 1, + sym_identifier, + ACTIONS(123), 1, + anon_sym_not, + STATE(14), 1, + sym_expression, + ACTIONS(99), 2, + sym_number, + sym_string, + ACTIONS(107), 4, + anon_sym_true, + anon_sym_false, + anon_sym_always, + anon_sym_never, + STATE(17), 4, + sym_primary_expression, + sym_unary_expression, + sym_binary_expression, + sym_ternary_expression, + STATE(9), 5, + sym_boolean, + sym_list_expression, + sym_call_expression, + sym_member_expression, + sym_match_expression, + [2024] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + ACTIONS(125), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2066] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + ACTIONS(133), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2108] = 6, + ACTIONS(3), 1, + sym_comment, + ACTIONS(21), 1, + anon_sym_LPAREN, + ACTIONS(23), 1, + anon_sym_DOT, + ACTIONS(135), 1, + anon_sym_COLON, + ACTIONS(19), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(17), 14, + anon_sym_RPAREN, + anon_sym_COMMA, + anon_sym_STAR, + anon_sym_SLASH, + anon_sym_PLUS, + anon_sym_DASH, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + anon_sym_is, + anon_sym_and, + anon_sym_or, + anon_sym_QMARK, + [2141] = 11, + ACTIONS(3), 1, + sym_comment, + ACTIONS(45), 1, + anon_sym_is, + ACTIONS(47), 1, + anon_sym_and, + ACTIONS(49), 1, + anon_sym_or, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(139), 1, + anon_sym_RBRACE, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + ACTIONS(137), 4, + sym_identifier, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + [2184] = 12, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(141), 1, + anon_sym_COMMA, + ACTIONS(143), 1, + anon_sym_RBRACK, + STATE(105), 1, + aux_sym_list_expression_repeat1, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2227] = 11, + ACTIONS(3), 1, + sym_comment, + ACTIONS(45), 1, + anon_sym_is, + ACTIONS(47), 1, + anon_sym_and, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(147), 1, + anon_sym_RBRACE, + ACTIONS(149), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(145), 2, + sym_identifier, + anon_sym__, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2268] = 11, + ACTIONS(3), 1, + sym_comment, + ACTIONS(45), 1, + anon_sym_is, + ACTIONS(47), 1, + anon_sym_and, + ACTIONS(49), 1, + anon_sym_or, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(151), 1, + sym_identifier, + ACTIONS(153), 1, + anon_sym_RBRACE, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2308] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(155), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2346] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(157), 2, + anon_sym_COMMA, + anon_sym_RBRACK, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2384] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(159), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2422] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(161), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2460] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(163), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2498] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(165), 1, + anon_sym_RPAREN, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2535] = 10, + ACTIONS(3), 1, + sym_comment, + ACTIONS(51), 1, + anon_sym_QMARK, + ACTIONS(127), 1, + anon_sym_is, + ACTIONS(129), 1, + anon_sym_and, + ACTIONS(131), 1, + anon_sym_or, + ACTIONS(167), 1, + anon_sym_COLON, + ACTIONS(37), 2, + anon_sym_STAR, + anon_sym_SLASH, + ACTIONS(39), 2, + anon_sym_PLUS, + anon_sym_DASH, + ACTIONS(43), 2, + anon_sym_GT, + anon_sym_LT, + ACTIONS(41), 4, + anon_sym_EQ_EQ, + anon_sym_BANG_EQ, + anon_sym_GT_EQ, + anon_sym_LT_EQ, + [2572] = 9, + ACTIONS(3), 1, + sym_comment, + ACTIONS(169), 1, + ts_builtin_sym_end, + ACTIONS(171), 1, + anon_sym_region, + ACTIONS(174), 1, + anon_sym_extend, + ACTIONS(177), 1, + anon_sym_define, + ACTIONS(180), 1, + anon_sym_extern, + ACTIONS(183), 1, + anon_sym_enum, + STATE(61), 2, + sym_declaration, + aux_sym_source_file_repeat1, + STATE(69), 6, + sym_region_declaration, + sym_extend_region_declaration, + sym_define_declaration, + sym_extern_define_declaration, + sym_enum_declaration, + sym_extern_enum_declaration, + [2606] = 9, + ACTIONS(3), 1, + sym_comment, + ACTIONS(7), 1, + anon_sym_region, + ACTIONS(9), 1, + anon_sym_extend, + ACTIONS(11), 1, + anon_sym_define, + ACTIONS(13), 1, + anon_sym_extern, + ACTIONS(15), 1, + anon_sym_enum, + ACTIONS(186), 1, + ts_builtin_sym_end, + STATE(61), 2, + sym_declaration, + aux_sym_source_file_repeat1, + STATE(69), 6, + sym_region_declaration, + sym_extend_region_declaration, + sym_define_declaration, + sym_extern_define_declaration, + sym_enum_declaration, + sym_extern_enum_declaration, + [2640] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(188), 1, + sym_identifier, + ACTIONS(191), 1, + anon_sym_RBRACE, + ACTIONS(193), 3, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + STATE(63), 3, + sym_region_data_entry, + sym_section, + aux_sym_region_declaration_repeat1, + [2660] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(196), 1, + sym_identifier, + ACTIONS(198), 1, + anon_sym_RBRACE, + ACTIONS(200), 3, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + STATE(65), 3, + sym_region_data_entry, + sym_section, + aux_sym_region_declaration_repeat1, + [2680] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(196), 1, + sym_identifier, + ACTIONS(202), 1, + anon_sym_RBRACE, + ACTIONS(200), 3, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + STATE(63), 3, + sym_region_data_entry, + sym_section, + aux_sym_region_declaration_repeat1, + [2700] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(204), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2712] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(206), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2724] = 6, + ACTIONS(3), 1, + sym_comment, + ACTIONS(208), 1, + sym_identifier, + ACTIONS(211), 1, + anon_sym_RBRACE, + ACTIONS(213), 1, + anon_sym__, + STATE(141), 1, + sym_match_pattern, + STATE(68), 2, + sym_match_arm, + aux_sym_match_expression_repeat1, + [2744] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(216), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2756] = 6, + ACTIONS(3), 1, + sym_comment, + ACTIONS(218), 1, + sym_identifier, + ACTIONS(220), 1, + anon_sym_RBRACE, + ACTIONS(222), 1, + anon_sym__, + STATE(141), 1, + sym_match_pattern, + STATE(68), 2, + sym_match_arm, + aux_sym_match_expression_repeat1, + [2776] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(224), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2788] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(226), 1, + anon_sym_RBRACE, + STATE(74), 2, + sym_section, + aux_sym_extend_region_declaration_repeat1, + ACTIONS(228), 3, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + [2804] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(230), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2816] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(232), 1, + anon_sym_RBRACE, + STATE(74), 2, + sym_section, + aux_sym_extend_region_declaration_repeat1, + ACTIONS(234), 3, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + [2832] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(237), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2844] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(239), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2856] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(241), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2868] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(243), 1, + anon_sym_RBRACE, + STATE(72), 2, + sym_section, + aux_sym_extend_region_declaration_repeat1, + ACTIONS(228), 3, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + [2884] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(245), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2896] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(247), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2908] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(249), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2920] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(251), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2932] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(253), 6, + ts_builtin_sym_end, + anon_sym_region, + anon_sym_extend, + anon_sym_define, + anon_sym_extern, + anon_sym_enum, + [2944] = 6, + ACTIONS(3), 1, + sym_comment, + ACTIONS(255), 1, + sym_identifier, + ACTIONS(257), 1, + anon_sym_RBRACE, + ACTIONS(259), 1, + sym_glob_pattern, + STATE(99), 1, + sym_extern_enum_entry, + STATE(123), 1, + sym_enum_member, + [2963] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(218), 1, + sym_identifier, + ACTIONS(222), 1, + anon_sym__, + STATE(141), 1, + sym_match_pattern, + STATE(70), 2, + sym_match_arm, + aux_sym_match_expression_repeat1, + [2980] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(263), 1, + anon_sym_RBRACE, + ACTIONS(261), 4, + sym_identifier, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + [2993] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(267), 1, + anon_sym_RBRACE, + ACTIONS(265), 4, + sym_identifier, + anon_sym_events, + anon_sym_locations, + anon_sym_exits, + [3006] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(269), 1, + sym_identifier, + ACTIONS(271), 1, + anon_sym_RPAREN, + STATE(109), 1, + sym_parameter, + STATE(154), 1, + sym_parameters, + [3022] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(273), 1, + sym_identifier, + ACTIONS(276), 1, + anon_sym_RBRACE, + STATE(89), 2, + sym_entry, + aux_sym_section_repeat1, + [3036] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(255), 1, + sym_identifier, + ACTIONS(259), 1, + sym_glob_pattern, + STATE(118), 1, + sym_extern_enum_entry, + STATE(123), 1, + sym_enum_member, + [3052] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(278), 1, + sym_identifier, + ACTIONS(280), 1, + anon_sym_RBRACE, + STATE(94), 2, + sym_entry, + aux_sym_section_repeat1, + [3066] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(282), 1, + anon_sym_COLON, + ACTIONS(286), 1, + anon_sym_EQ, + ACTIONS(284), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + [3080] = 5, + ACTIONS(3), 1, + sym_comment, + ACTIONS(269), 1, + sym_identifier, + ACTIONS(288), 1, + anon_sym_RPAREN, + STATE(109), 1, + sym_parameter, + STATE(139), 1, + sym_parameters, + [3096] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(278), 1, + sym_identifier, + ACTIONS(290), 1, + anon_sym_RBRACE, + STATE(89), 2, + sym_entry, + aux_sym_section_repeat1, + [3110] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(294), 1, + anon_sym_EQ, + ACTIONS(292), 2, + anon_sym_RBRACE, + anon_sym_COMMA, + [3121] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(296), 1, + anon_sym_RBRACE, + ACTIONS(298), 1, + anon_sym_COMMA, + STATE(96), 1, + aux_sym_extern_enum_declaration_repeat1, + [3134] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(301), 1, + anon_sym_COLON, + ACTIONS(303), 1, + anon_sym_or, + STATE(97), 1, + aux_sym_match_pattern_repeat1, + [3147] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(306), 1, + anon_sym_RPAREN, + ACTIONS(308), 1, + anon_sym_COMMA, + STATE(98), 1, + aux_sym_parameters_repeat1, + [3160] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(311), 1, + anon_sym_RBRACE, + ACTIONS(313), 1, + anon_sym_COMMA, + STATE(100), 1, + aux_sym_extern_enum_declaration_repeat1, + [3173] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(313), 1, + anon_sym_COMMA, + ACTIONS(315), 1, + anon_sym_RBRACE, + STATE(96), 1, + aux_sym_extern_enum_declaration_repeat1, + [3186] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(317), 1, + anon_sym_RPAREN, + ACTIONS(319), 1, + anon_sym_COMMA, + STATE(103), 1, + aux_sym_call_expression_repeat1, + [3199] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(321), 1, + anon_sym_COLON, + ACTIONS(323), 1, + anon_sym_or, + STATE(97), 1, + aux_sym_match_pattern_repeat1, + [3212] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(325), 1, + anon_sym_RPAREN, + ACTIONS(327), 1, + anon_sym_COMMA, + STATE(103), 1, + aux_sym_call_expression_repeat1, + [3225] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(330), 1, + anon_sym_RPAREN, + ACTIONS(332), 1, + anon_sym_COMMA, + STATE(98), 1, + aux_sym_parameters_repeat1, + [3238] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(141), 1, + anon_sym_COMMA, + ACTIONS(334), 1, + anon_sym_RBRACK, + STATE(106), 1, + aux_sym_list_expression_repeat1, + [3251] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(157), 1, + anon_sym_RBRACK, + ACTIONS(336), 1, + anon_sym_COMMA, + STATE(106), 1, + aux_sym_list_expression_repeat1, + [3264] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(341), 1, + anon_sym_EQ, + ACTIONS(339), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + [3275] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(323), 1, + anon_sym_or, + ACTIONS(343), 1, + anon_sym_COLON, + STATE(102), 1, + aux_sym_match_pattern_repeat1, + [3288] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(332), 1, + anon_sym_COMMA, + ACTIONS(345), 1, + anon_sym_RPAREN, + STATE(104), 1, + aux_sym_parameters_repeat1, + [3301] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(347), 1, + sym_identifier, + ACTIONS(349), 1, + anon_sym_RBRACE, + STATE(111), 1, + sym_enum_member, + [3314] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(351), 1, + anon_sym_RBRACE, + ACTIONS(353), 1, + anon_sym_COMMA, + STATE(112), 1, + aux_sym_enum_declaration_repeat1, + [3327] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(353), 1, + anon_sym_COMMA, + ACTIONS(355), 1, + anon_sym_RBRACE, + STATE(113), 1, + aux_sym_enum_declaration_repeat1, + [3340] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(357), 1, + anon_sym_RBRACE, + ACTIONS(359), 1, + anon_sym_COMMA, + STATE(113), 1, + aux_sym_enum_declaration_repeat1, + [3353] = 4, + ACTIONS(3), 1, + sym_comment, + ACTIONS(319), 1, + anon_sym_COMMA, + ACTIONS(362), 1, + anon_sym_RPAREN, + STATE(101), 1, + aux_sym_call_expression_repeat1, + [3366] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(163), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + [3374] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(347), 1, + sym_identifier, + STATE(117), 1, + sym_enum_member, + [3384] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(357), 2, + anon_sym_RBRACE, + anon_sym_COMMA, + [3392] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(296), 2, + anon_sym_RBRACE, + anon_sym_COMMA, + [3400] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(325), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + [3408] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(269), 1, + sym_identifier, + STATE(124), 1, + sym_parameter, + [3418] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(364), 2, + anon_sym_RBRACE, + anon_sym_COMMA, + [3426] = 3, + ACTIONS(3), 1, + sym_comment, + ACTIONS(366), 1, + anon_sym_define, + ACTIONS(368), 1, + anon_sym_enum, + [3436] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(370), 2, + anon_sym_RBRACE, + anon_sym_COMMA, + [3444] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(306), 2, + anon_sym_RPAREN, + anon_sym_COMMA, + [3452] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(301), 2, + anon_sym_COLON, + anon_sym_or, + [3460] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(372), 1, + sym_identifier, + [3467] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(374), 1, + anon_sym_LPAREN, + [3474] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(376), 1, + sym_identifier, + [3481] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(378), 1, + anon_sym_LBRACE, + [3488] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(380), 1, + anon_sym_LPAREN, + [3495] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(382), 1, + sym_identifier, + [3502] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(384), 1, + anon_sym_LBRACE, + [3509] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(386), 1, + anon_sym_LBRACE, + [3516] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(388), 1, + anon_sym_LBRACE, + [3523] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(390), 1, + anon_sym_DASH_GT, + [3530] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(392), 1, + sym_identifier, + [3537] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(394), 1, + sym_identifier, + [3544] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(396), 1, + anon_sym_COLON, + [3551] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(398), 1, + anon_sym_RPAREN, + [3558] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(343), 1, + anon_sym_COLON, + [3565] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(400), 1, + anon_sym_COLON, + [3572] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(402), 1, + sym_identifier, + [3579] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(404), 1, + anon_sym_COLON, + [3586] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(406), 1, + sym_identifier, + [3593] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(408), 1, + anon_sym_LBRACE, + [3600] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(410), 1, + sym_identifier, + [3607] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(412), 1, + anon_sym_COLON, + [3614] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(414), 1, + anon_sym_COLON, + [3621] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(416), 1, + sym_identifier, + [3628] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(418), 1, + anon_sym_DASH_GT, + [3635] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(420), 1, + ts_builtin_sym_end, + [3642] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(422), 1, + sym_identifier, + [3649] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(424), 1, + sym_identifier, + [3656] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(426), 1, + anon_sym_RPAREN, + [3663] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(428), 1, + anon_sym_LBRACE, + [3670] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(430), 1, + sym_number, + [3677] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(432), 1, + sym_identifier, + [3684] = 2, + ACTIONS(3), 1, + sym_comment, + ACTIONS(434), 1, + anon_sym_region, +}; + +static const uint32_t ts_small_parse_table_map[] = { + [SMALL_STATE(2)] = 0, + [SMALL_STATE(3)] = 44, + [SMALL_STATE(4)] = 82, + [SMALL_STATE(5)] = 120, + [SMALL_STATE(6)] = 174, + [SMALL_STATE(7)] = 212, + [SMALL_STATE(8)] = 252, + [SMALL_STATE(9)] = 290, + [SMALL_STATE(10)] = 328, + [SMALL_STATE(11)] = 366, + [SMALL_STATE(12)] = 404, + [SMALL_STATE(13)] = 442, + [SMALL_STATE(14)] = 480, + [SMALL_STATE(15)] = 522, + [SMALL_STATE(16)] = 560, + [SMALL_STATE(17)] = 598, + [SMALL_STATE(18)] = 636, + [SMALL_STATE(19)] = 674, + [SMALL_STATE(20)] = 722, + [SMALL_STATE(21)] = 760, + [SMALL_STATE(22)] = 798, + [SMALL_STATE(23)] = 848, + [SMALL_STATE(24)] = 905, + [SMALL_STATE(25)] = 959, + [SMALL_STATE(26)] = 1013, + [SMALL_STATE(27)] = 1064, + [SMALL_STATE(28)] = 1112, + [SMALL_STATE(29)] = 1160, + [SMALL_STATE(30)] = 1208, + [SMALL_STATE(31)] = 1256, + [SMALL_STATE(32)] = 1304, + [SMALL_STATE(33)] = 1352, + [SMALL_STATE(34)] = 1400, + [SMALL_STATE(35)] = 1448, + [SMALL_STATE(36)] = 1496, + [SMALL_STATE(37)] = 1544, + [SMALL_STATE(38)] = 1592, + [SMALL_STATE(39)] = 1640, + [SMALL_STATE(40)] = 1688, + [SMALL_STATE(41)] = 1736, + [SMALL_STATE(42)] = 1784, + [SMALL_STATE(43)] = 1832, + [SMALL_STATE(44)] = 1880, + [SMALL_STATE(45)] = 1928, + [SMALL_STATE(46)] = 1976, + [SMALL_STATE(47)] = 2024, + [SMALL_STATE(48)] = 2066, + [SMALL_STATE(49)] = 2108, + [SMALL_STATE(50)] = 2141, + [SMALL_STATE(51)] = 2184, + [SMALL_STATE(52)] = 2227, + [SMALL_STATE(53)] = 2268, + [SMALL_STATE(54)] = 2308, + [SMALL_STATE(55)] = 2346, + [SMALL_STATE(56)] = 2384, + [SMALL_STATE(57)] = 2422, + [SMALL_STATE(58)] = 2460, + [SMALL_STATE(59)] = 2498, + [SMALL_STATE(60)] = 2535, + [SMALL_STATE(61)] = 2572, + [SMALL_STATE(62)] = 2606, + [SMALL_STATE(63)] = 2640, + [SMALL_STATE(64)] = 2660, + [SMALL_STATE(65)] = 2680, + [SMALL_STATE(66)] = 2700, + [SMALL_STATE(67)] = 2712, + [SMALL_STATE(68)] = 2724, + [SMALL_STATE(69)] = 2744, + [SMALL_STATE(70)] = 2756, + [SMALL_STATE(71)] = 2776, + [SMALL_STATE(72)] = 2788, + [SMALL_STATE(73)] = 2804, + [SMALL_STATE(74)] = 2816, + [SMALL_STATE(75)] = 2832, + [SMALL_STATE(76)] = 2844, + [SMALL_STATE(77)] = 2856, + [SMALL_STATE(78)] = 2868, + [SMALL_STATE(79)] = 2884, + [SMALL_STATE(80)] = 2896, + [SMALL_STATE(81)] = 2908, + [SMALL_STATE(82)] = 2920, + [SMALL_STATE(83)] = 2932, + [SMALL_STATE(84)] = 2944, + [SMALL_STATE(85)] = 2963, + [SMALL_STATE(86)] = 2980, + [SMALL_STATE(87)] = 2993, + [SMALL_STATE(88)] = 3006, + [SMALL_STATE(89)] = 3022, + [SMALL_STATE(90)] = 3036, + [SMALL_STATE(91)] = 3052, + [SMALL_STATE(92)] = 3066, + [SMALL_STATE(93)] = 3080, + [SMALL_STATE(94)] = 3096, + [SMALL_STATE(95)] = 3110, + [SMALL_STATE(96)] = 3121, + [SMALL_STATE(97)] = 3134, + [SMALL_STATE(98)] = 3147, + [SMALL_STATE(99)] = 3160, + [SMALL_STATE(100)] = 3173, + [SMALL_STATE(101)] = 3186, + [SMALL_STATE(102)] = 3199, + [SMALL_STATE(103)] = 3212, + [SMALL_STATE(104)] = 3225, + [SMALL_STATE(105)] = 3238, + [SMALL_STATE(106)] = 3251, + [SMALL_STATE(107)] = 3264, + [SMALL_STATE(108)] = 3275, + [SMALL_STATE(109)] = 3288, + [SMALL_STATE(110)] = 3301, + [SMALL_STATE(111)] = 3314, + [SMALL_STATE(112)] = 3327, + [SMALL_STATE(113)] = 3340, + [SMALL_STATE(114)] = 3353, + [SMALL_STATE(115)] = 3366, + [SMALL_STATE(116)] = 3374, + [SMALL_STATE(117)] = 3384, + [SMALL_STATE(118)] = 3392, + [SMALL_STATE(119)] = 3400, + [SMALL_STATE(120)] = 3408, + [SMALL_STATE(121)] = 3418, + [SMALL_STATE(122)] = 3426, + [SMALL_STATE(123)] = 3436, + [SMALL_STATE(124)] = 3444, + [SMALL_STATE(125)] = 3452, + [SMALL_STATE(126)] = 3460, + [SMALL_STATE(127)] = 3467, + [SMALL_STATE(128)] = 3474, + [SMALL_STATE(129)] = 3481, + [SMALL_STATE(130)] = 3488, + [SMALL_STATE(131)] = 3495, + [SMALL_STATE(132)] = 3502, + [SMALL_STATE(133)] = 3509, + [SMALL_STATE(134)] = 3516, + [SMALL_STATE(135)] = 3523, + [SMALL_STATE(136)] = 3530, + [SMALL_STATE(137)] = 3537, + [SMALL_STATE(138)] = 3544, + [SMALL_STATE(139)] = 3551, + [SMALL_STATE(140)] = 3558, + [SMALL_STATE(141)] = 3565, + [SMALL_STATE(142)] = 3572, + [SMALL_STATE(143)] = 3579, + [SMALL_STATE(144)] = 3586, + [SMALL_STATE(145)] = 3593, + [SMALL_STATE(146)] = 3600, + [SMALL_STATE(147)] = 3607, + [SMALL_STATE(148)] = 3614, + [SMALL_STATE(149)] = 3621, + [SMALL_STATE(150)] = 3628, + [SMALL_STATE(151)] = 3635, + [SMALL_STATE(152)] = 3642, + [SMALL_STATE(153)] = 3649, + [SMALL_STATE(154)] = 3656, + [SMALL_STATE(155)] = 3663, + [SMALL_STATE(156)] = 3670, + [SMALL_STATE(157)] = 3677, + [SMALL_STATE(158)] = 3684, +}; + +static const TSParseActionEntry ts_parse_actions[] = { + [0] = {.entry = {.count = 0, .reusable = false}}, + [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), + [3] = {.entry = {.count = 1, .reusable = true}}, SHIFT_EXTRA(), + [5] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 0), + [7] = {.entry = {.count = 1, .reusable = true}}, SHIFT(131), + [9] = {.entry = {.count = 1, .reusable = true}}, SHIFT(158), + [11] = {.entry = {.count = 1, .reusable = true}}, SHIFT(157), + [13] = {.entry = {.count = 1, .reusable = true}}, SHIFT(122), + [15] = {.entry = {.count = 1, .reusable = true}}, SHIFT(152), + [17] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_primary_expression, 1), + [19] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_primary_expression, 1), + [21] = {.entry = {.count = 1, .reusable = true}}, SHIFT(23), + [23] = {.entry = {.count = 1, .reusable = true}}, SHIFT(128), + [25] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_boolean, 1), + [27] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_boolean, 1), + [29] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_call_expression, 3, .production_id = 13), + [31] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_call_expression, 3, .production_id = 13), + [33] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_ternary_expression, 5), + [35] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_ternary_expression, 5), + [37] = {.entry = {.count = 1, .reusable = true}}, SHIFT(36), + [39] = {.entry = {.count = 1, .reusable = true}}, SHIFT(39), + [41] = {.entry = {.count = 1, .reusable = true}}, SHIFT(27), + [43] = {.entry = {.count = 1, .reusable = false}}, SHIFT(27), + [45] = {.entry = {.count = 1, .reusable = false}}, SHIFT(46), + [47] = {.entry = {.count = 1, .reusable = false}}, SHIFT(28), + [49] = {.entry = {.count = 1, .reusable = false}}, SHIFT(43), + [51] = {.entry = {.count = 1, .reusable = true}}, SHIFT(42), + [53] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_match_expression, 5, .production_id = 17), + [55] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_match_expression, 5, .production_id = 17), + [57] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_binary_expression, 3), + [59] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_binary_expression, 3), + [61] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_call_expression, 5, .production_id = 13), + [63] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_call_expression, 5, .production_id = 13), + [65] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_primary_expression, 3), + [67] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_primary_expression, 3), + [69] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_unary_expression, 2, .production_id = 10), + [71] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_unary_expression, 2, .production_id = 10), + [73] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_list_expression, 4), + [75] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_list_expression, 4), + [77] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_list_expression, 2), + [79] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_list_expression, 2), + [81] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_call_expression, 4, .production_id = 13), + [83] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_call_expression, 4, .production_id = 13), + [85] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_expression, 1), + [87] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_expression, 1), + [89] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_member_expression, 3, .production_id = 14), + [91] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_member_expression, 3, .production_id = 14), + [93] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_list_expression, 3), + [95] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_list_expression, 3), + [97] = {.entry = {.count = 1, .reusable = false}}, SHIFT(49), + [99] = {.entry = {.count = 1, .reusable = true}}, SHIFT(9), + [101] = {.entry = {.count = 1, .reusable = true}}, SHIFT(34), + [103] = {.entry = {.count = 1, .reusable = true}}, SHIFT(4), + [105] = {.entry = {.count = 1, .reusable = false}}, SHIFT(9), + [107] = {.entry = {.count = 1, .reusable = false}}, SHIFT(3), + [109] = {.entry = {.count = 1, .reusable = true}}, SHIFT(26), + [111] = {.entry = {.count = 1, .reusable = false}}, SHIFT(31), + [113] = {.entry = {.count = 1, .reusable = false}}, SHIFT(153), + [115] = {.entry = {.count = 1, .reusable = false}}, SHIFT(2), + [117] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_match_arm, 4), + [119] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_match_arm, 4), + [121] = {.entry = {.count = 1, .reusable = true}}, SHIFT(13), + [123] = {.entry = {.count = 1, .reusable = false}}, SHIFT(30), + [125] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_define_declaration, 7, .production_id = 11), + [127] = {.entry = {.count = 1, .reusable = true}}, SHIFT(46), + [129] = {.entry = {.count = 1, .reusable = true}}, SHIFT(28), + [131] = {.entry = {.count = 1, .reusable = true}}, SHIFT(43), + [133] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_define_declaration, 6, .production_id = 8), + [135] = {.entry = {.count = 1, .reusable = true}}, SHIFT(40), + [137] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_region_data_entry, 3, .production_id = 4), + [139] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_region_data_entry, 3, .production_id = 4), + [141] = {.entry = {.count = 1, .reusable = true}}, SHIFT(32), + [143] = {.entry = {.count = 1, .reusable = true}}, SHIFT(21), + [145] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_match_arm, 3), + [147] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_match_arm, 3), + [149] = {.entry = {.count = 1, .reusable = false}}, SHIFT(24), + [151] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_entry, 3, .production_id = 9), + [153] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_entry, 3, .production_id = 9), + [155] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameter, 5, .production_id = 15), + [157] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_list_expression_repeat1, 2), + [159] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameter, 3, .production_id = 7), + [161] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_named_argument, 3, .production_id = 9), + [163] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_argument, 1), + [165] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), + [167] = {.entry = {.count = 1, .reusable = true}}, SHIFT(35), + [169] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2), + [171] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2), SHIFT_REPEAT(131), + [174] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2), SHIFT_REPEAT(158), + [177] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2), SHIFT_REPEAT(157), + [180] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2), SHIFT_REPEAT(122), + [183] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2), SHIFT_REPEAT(152), + [186] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 1), + [188] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_region_declaration_repeat1, 2), SHIFT_REPEAT(143), + [191] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_region_declaration_repeat1, 2), + [193] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_region_declaration_repeat1, 2), SHIFT_REPEAT(132), + [196] = {.entry = {.count = 1, .reusable = false}}, SHIFT(143), + [198] = {.entry = {.count = 1, .reusable = true}}, SHIFT(77), + [200] = {.entry = {.count = 1, .reusable = false}}, SHIFT(132), + [202] = {.entry = {.count = 1, .reusable = true}}, SHIFT(66), + [204] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_region_declaration, 5, .production_id = 1), + [206] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extend_region_declaration, 5, .production_id = 3), + [208] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_match_expression_repeat1, 2), SHIFT_REPEAT(108), + [211] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_match_expression_repeat1, 2), + [213] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_match_expression_repeat1, 2), SHIFT_REPEAT(140), + [216] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_declaration, 1), + [218] = {.entry = {.count = 1, .reusable = false}}, SHIFT(108), + [220] = {.entry = {.count = 1, .reusable = true}}, SHIFT(6), + [222] = {.entry = {.count = 1, .reusable = false}}, SHIFT(140), + [224] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extern_enum_declaration, 5, .production_id = 3), + [226] = {.entry = {.count = 1, .reusable = true}}, SHIFT(73), + [228] = {.entry = {.count = 1, .reusable = true}}, SHIFT(132), + [230] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extend_region_declaration, 6, .production_id = 3), + [232] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_extend_region_declaration_repeat1, 2), + [234] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_extend_region_declaration_repeat1, 2), SHIFT_REPEAT(132), + [237] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extern_enum_declaration, 7, .production_id = 3), + [239] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extern_define_declaration, 8, .production_id = 16), + [241] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_region_declaration, 4, .production_id = 1), + [243] = {.entry = {.count = 1, .reusable = true}}, SHIFT(67), + [245] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extern_define_declaration, 7, .production_id = 12), + [247] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extern_enum_declaration, 6, .production_id = 3), + [249] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 5, .production_id = 1), + [251] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 4, .production_id = 1), + [253] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_declaration, 6, .production_id = 1), + [255] = {.entry = {.count = 1, .reusable = false}}, SHIFT(95), + [257] = {.entry = {.count = 1, .reusable = true}}, SHIFT(71), + [259] = {.entry = {.count = 1, .reusable = true}}, SHIFT(123), + [261] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_section, 4, .production_id = 5), + [263] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_section, 4, .production_id = 5), + [265] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_section, 3, .production_id = 5), + [267] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_section, 3, .production_id = 5), + [269] = {.entry = {.count = 1, .reusable = true}}, SHIFT(92), + [271] = {.entry = {.count = 1, .reusable = true}}, SHIFT(150), + [273] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_section_repeat1, 2), SHIFT_REPEAT(147), + [276] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_section_repeat1, 2), + [278] = {.entry = {.count = 1, .reusable = true}}, SHIFT(147), + [280] = {.entry = {.count = 1, .reusable = true}}, SHIFT(87), + [282] = {.entry = {.count = 1, .reusable = true}}, SHIFT(146), + [284] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameter, 1, .production_id = 2), + [286] = {.entry = {.count = 1, .reusable = true}}, SHIFT(44), + [288] = {.entry = {.count = 1, .reusable = true}}, SHIFT(138), + [290] = {.entry = {.count = 1, .reusable = true}}, SHIFT(86), + [292] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_member, 1, .production_id = 2), + [294] = {.entry = {.count = 1, .reusable = true}}, SHIFT(156), + [296] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_extern_enum_declaration_repeat1, 2), + [298] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_extern_enum_declaration_repeat1, 2), SHIFT_REPEAT(90), + [301] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_match_pattern_repeat1, 2), + [303] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_match_pattern_repeat1, 2), SHIFT_REPEAT(149), + [306] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_parameters_repeat1, 2), + [308] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_parameters_repeat1, 2), SHIFT_REPEAT(120), + [311] = {.entry = {.count = 1, .reusable = true}}, SHIFT(80), + [313] = {.entry = {.count = 1, .reusable = true}}, SHIFT(90), + [315] = {.entry = {.count = 1, .reusable = true}}, SHIFT(75), + [317] = {.entry = {.count = 1, .reusable = true}}, SHIFT(8), + [319] = {.entry = {.count = 1, .reusable = true}}, SHIFT(25), + [321] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_match_pattern, 2), + [323] = {.entry = {.count = 1, .reusable = true}}, SHIFT(149), + [325] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_call_expression_repeat1, 2), + [327] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_call_expression_repeat1, 2), SHIFT_REPEAT(25), + [330] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameters, 2), + [332] = {.entry = {.count = 1, .reusable = true}}, SHIFT(120), + [334] = {.entry = {.count = 1, .reusable = true}}, SHIFT(12), + [336] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_list_expression_repeat1, 2), SHIFT_REPEAT(32), + [339] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameter, 3, .production_id = 6), + [341] = {.entry = {.count = 1, .reusable = true}}, SHIFT(38), + [343] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_match_pattern, 1), + [345] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_parameters, 1), + [347] = {.entry = {.count = 1, .reusable = true}}, SHIFT(95), + [349] = {.entry = {.count = 1, .reusable = true}}, SHIFT(82), + [351] = {.entry = {.count = 1, .reusable = true}}, SHIFT(81), + [353] = {.entry = {.count = 1, .reusable = true}}, SHIFT(116), + [355] = {.entry = {.count = 1, .reusable = true}}, SHIFT(83), + [357] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_enum_declaration_repeat1, 2), + [359] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_enum_declaration_repeat1, 2), SHIFT_REPEAT(116), + [362] = {.entry = {.count = 1, .reusable = true}}, SHIFT(15), + [364] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_enum_member, 3, .production_id = 9), + [366] = {.entry = {.count = 1, .reusable = true}}, SHIFT(137), + [368] = {.entry = {.count = 1, .reusable = true}}, SHIFT(136), + [370] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_extern_enum_entry, 1), + [372] = {.entry = {.count = 1, .reusable = true}}, SHIFT(79), + [374] = {.entry = {.count = 1, .reusable = true}}, SHIFT(93), + [376] = {.entry = {.count = 1, .reusable = true}}, SHIFT(18), + [378] = {.entry = {.count = 1, .reusable = true}}, SHIFT(84), + [380] = {.entry = {.count = 1, .reusable = true}}, SHIFT(88), + [382] = {.entry = {.count = 1, .reusable = true}}, SHIFT(145), + [384] = {.entry = {.count = 1, .reusable = true}}, SHIFT(91), + [386] = {.entry = {.count = 1, .reusable = true}}, SHIFT(78), + [388] = {.entry = {.count = 1, .reusable = true}}, SHIFT(110), + [390] = {.entry = {.count = 1, .reusable = true}}, SHIFT(142), + [392] = {.entry = {.count = 1, .reusable = true}}, SHIFT(129), + [394] = {.entry = {.count = 1, .reusable = true}}, SHIFT(130), + [396] = {.entry = {.count = 1, .reusable = true}}, SHIFT(45), + [398] = {.entry = {.count = 1, .reusable = true}}, SHIFT(148), + [400] = {.entry = {.count = 1, .reusable = true}}, SHIFT(33), + [402] = {.entry = {.count = 1, .reusable = true}}, SHIFT(76), + [404] = {.entry = {.count = 1, .reusable = true}}, SHIFT(37), + [406] = {.entry = {.count = 1, .reusable = true}}, SHIFT(133), + [408] = {.entry = {.count = 1, .reusable = true}}, SHIFT(64), + [410] = {.entry = {.count = 1, .reusable = true}}, SHIFT(107), + [412] = {.entry = {.count = 1, .reusable = true}}, SHIFT(41), + [414] = {.entry = {.count = 1, .reusable = true}}, SHIFT(29), + [416] = {.entry = {.count = 1, .reusable = true}}, SHIFT(125), + [418] = {.entry = {.count = 1, .reusable = true}}, SHIFT(126), + [420] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), + [422] = {.entry = {.count = 1, .reusable = true}}, SHIFT(134), + [424] = {.entry = {.count = 1, .reusable = true}}, SHIFT(155), + [426] = {.entry = {.count = 1, .reusable = true}}, SHIFT(135), + [428] = {.entry = {.count = 1, .reusable = true}}, SHIFT(85), + [430] = {.entry = {.count = 1, .reusable = true}}, SHIFT(121), + [432] = {.entry = {.count = 1, .reusable = true}}, SHIFT(127), + [434] = {.entry = {.count = 1, .reusable = true}}, SHIFT(144), +}; + +#ifdef __cplusplus +extern "C" { +#endif +#ifdef _WIN32 +#define extern __declspec(dllexport) +#endif + +extern const TSLanguage *tree_sitter_rls(void) { + static const TSLanguage language = { + .version = LANGUAGE_VERSION, + .symbol_count = SYMBOL_COUNT, + .alias_count = ALIAS_COUNT, + .token_count = TOKEN_COUNT, + .external_token_count = EXTERNAL_TOKEN_COUNT, + .state_count = STATE_COUNT, + .large_state_count = LARGE_STATE_COUNT, + .production_id_count = PRODUCTION_ID_COUNT, + .field_count = FIELD_COUNT, + .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, + .parse_table = &ts_parse_table[0][0], + .small_parse_table = ts_small_parse_table, + .small_parse_table_map = ts_small_parse_table_map, + .parse_actions = ts_parse_actions, + .symbol_names = ts_symbol_names, + .field_names = ts_field_names, + .field_map_slices = ts_field_map_slices, + .field_map_entries = ts_field_map_entries, + .symbol_metadata = ts_symbol_metadata, + .public_symbol_map = ts_symbol_map, + .alias_map = ts_non_terminal_alias_map, + .alias_sequences = &ts_alias_sequences[0][0], + .lex_modes = ts_lex_modes, + .lex_fn = ts_lex, + .keyword_lex_fn = ts_lex_keywords, + .keyword_capture_token = sym_identifier, + .primary_state_ids = ts_primary_state_ids, + }; + return &language; +} +#ifdef __cplusplus +} +#endif diff --git a/tooling/tree-sitter-rls/src/tree_sitter/parser.h b/tooling/tree-sitter-rls/src/tree_sitter/parser.h new file mode 100644 index 0000000..2b14ac1 --- /dev/null +++ b/tooling/tree-sitter-rls/src/tree_sitter/parser.h @@ -0,0 +1,224 @@ +#ifndef TREE_SITTER_PARSER_H_ +#define TREE_SITTER_PARSER_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#define ts_builtin_sym_error ((TSSymbol)-1) +#define ts_builtin_sym_end 0 +#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 + +typedef uint16_t TSStateId; + +#ifndef TREE_SITTER_API_H_ +typedef uint16_t TSSymbol; +typedef uint16_t TSFieldId; +typedef struct TSLanguage TSLanguage; +#endif + +typedef struct { + TSFieldId field_id; + uint8_t child_index; + bool inherited; +} TSFieldMapEntry; + +typedef struct { + uint16_t index; + uint16_t length; +} TSFieldMapSlice; + +typedef struct { + bool visible; + bool named; + bool supertype; +} TSSymbolMetadata; + +typedef struct TSLexer TSLexer; + +struct TSLexer { + int32_t lookahead; + TSSymbol result_symbol; + void (*advance)(TSLexer *, bool); + void (*mark_end)(TSLexer *); + uint32_t (*get_column)(TSLexer *); + bool (*is_at_included_range_start)(const TSLexer *); + bool (*eof)(const TSLexer *); +}; + +typedef enum { + TSParseActionTypeShift, + TSParseActionTypeReduce, + TSParseActionTypeAccept, + TSParseActionTypeRecover, +} TSParseActionType; + +typedef union { + struct { + uint8_t type; + TSStateId state; + bool extra; + bool repetition; + } shift; + struct { + uint8_t type; + uint8_t child_count; + TSSymbol symbol; + int16_t dynamic_precedence; + uint16_t production_id; + } reduce; + uint8_t type; +} TSParseAction; + +typedef struct { + uint16_t lex_state; + uint16_t external_lex_state; +} TSLexMode; + +typedef union { + TSParseAction action; + struct { + uint8_t count; + bool reusable; + } entry; +} TSParseActionEntry; + +struct TSLanguage { + uint32_t version; + uint32_t symbol_count; + uint32_t alias_count; + uint32_t token_count; + uint32_t external_token_count; + uint32_t state_count; + uint32_t large_state_count; + uint32_t production_id_count; + uint32_t field_count; + uint16_t max_alias_sequence_length; + const uint16_t *parse_table; + const uint16_t *small_parse_table; + const uint32_t *small_parse_table_map; + const TSParseActionEntry *parse_actions; + const char * const *symbol_names; + const char * const *field_names; + const TSFieldMapSlice *field_map_slices; + const TSFieldMapEntry *field_map_entries; + const TSSymbolMetadata *symbol_metadata; + const TSSymbol *public_symbol_map; + const uint16_t *alias_map; + const TSSymbol *alias_sequences; + const TSLexMode *lex_modes; + bool (*lex_fn)(TSLexer *, TSStateId); + bool (*keyword_lex_fn)(TSLexer *, TSStateId); + TSSymbol keyword_capture_token; + struct { + const bool *states; + const TSSymbol *symbol_map; + void *(*create)(void); + void (*destroy)(void *); + bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); + unsigned (*serialize)(void *, char *); + void (*deserialize)(void *, const char *, unsigned); + } external_scanner; + const TSStateId *primary_state_ids; +}; + +/* + * Lexer Macros + */ + +#define START_LEXER() \ + bool result = false; \ + bool skip = false; \ + bool eof = false; \ + int32_t lookahead; \ + goto start; \ + next_state: \ + lexer->advance(lexer, skip); \ + start: \ + skip = false; \ + lookahead = lexer->lookahead; + +#define ADVANCE(state_value) \ + { \ + state = state_value; \ + goto next_state; \ + } + +#define SKIP(state_value) \ + { \ + skip = true; \ + state = state_value; \ + goto next_state; \ + } + +#define ACCEPT_TOKEN(symbol_value) \ + result = true; \ + lexer->result_symbol = symbol_value; \ + lexer->mark_end(lexer); + +#define END_STATE() return result; + +/* + * Parse Table Macros + */ + +#define SMALL_STATE(id) id - LARGE_STATE_COUNT + +#define STATE(id) id + +#define ACTIONS(id) id + +#define SHIFT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = state_value \ + } \ + }} + +#define SHIFT_REPEAT(state_value) \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .state = state_value, \ + .repetition = true \ + } \ + }} + +#define SHIFT_EXTRA() \ + {{ \ + .shift = { \ + .type = TSParseActionTypeShift, \ + .extra = true \ + } \ + }} + +#define REDUCE(symbol_val, child_count_val, ...) \ + {{ \ + .reduce = { \ + .type = TSParseActionTypeReduce, \ + .symbol = symbol_val, \ + .child_count = child_count_val, \ + __VA_ARGS__ \ + }, \ + }} + +#define RECOVER() \ + {{ \ + .type = TSParseActionTypeRecover \ + }} + +#define ACCEPT_INPUT() \ + {{ \ + .type = TSParseActionTypeAccept \ + }} + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_PARSER_H_ diff --git a/tooling/tree-sitter-rls/test-shared-corpus.js b/tooling/tree-sitter-rls/test-shared-corpus.js new file mode 100644 index 0000000..80c7d66 --- /dev/null +++ b/tooling/tree-sitter-rls/test-shared-corpus.js @@ -0,0 +1,58 @@ +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const { spawnSync } = require("child_process"); + +const grammarDirectory = __dirname; +const fixturePath = path.resolve(grammarDirectory, "..", "syntax-fixtures", "representative.rls"); +const highlightsPath = path.join(grammarDirectory, "queries", "highlights.scm"); +const treeSitter = process.platform === "win32" ? "tree-sitter.cmd" : "tree-sitter"; + +function runTreeSitter(arguments) { + const result = spawnSync(treeSitter, arguments, { + cwd: grammarDirectory, + encoding: "utf8", + shell: process.platform === "win32" + }); + return { + status: result.status, + output: `${result.stdout || ""}${result.stderr || ""}` + }; +} + +const fixture = fs.readFileSync(fixturePath, "utf8"); +const incompleteMarker = "# Keep this incomplete source editable while typing."; +const validFixture = fixture.slice(0, fixture.indexOf(incompleteMarker)); +const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "rls-tree-sitter-")); +const validFixturePath = path.join(temporaryDirectory, "representative-valid.rls"); +fs.writeFileSync(validFixturePath, validFixture); + +try { + const validParse = runTreeSitter(["parse", validFixturePath]); + assert.strictEqual(validParse.status, 0, validParse.output); + assert(!validParse.output.includes("ERROR"), validParse.output); + + const incompleteParse = runTreeSitter(["parse", fixturePath]); + assert.notStrictEqual(incompleteParse.status, 0, "Expected incomplete input to report a parse error."); + assert(incompleteParse.output.includes("(ERROR"), incompleteParse.output); + + const highlights = runTreeSitter(["query", highlightsPath, validFixturePath]); + assert.strictEqual(highlights.status, 0, highlights.output); + ["comment", "string", "number", "boolean", "type", "function", "constant", "variable.parameter", "property", "operator"].forEach((capture) => { + assert(highlights.output.includes(capture), `Expected @${capture} in highlight output.\n${highlights.output}`); + }); + + const folds = runTreeSitter(["query", "queries/folds.scm", validFixturePath]); + assert.strictEqual(folds.status, 0, folds.output); + assert(folds.output.includes("fold"), folds.output); + + const indents = runTreeSitter(["query", "queries/indents.scm", validFixturePath]); + assert.strictEqual(indents.status, 0, indents.output); + ["indent", "outdent"].forEach((capture) => { + assert(indents.output.includes(capture), `Expected @${capture} in indent output.\n${indents.output}`); + }); +} finally { + fs.unlinkSync(validFixturePath); + fs.rmdirSync(temporaryDirectory); +} \ No newline at end of file diff --git a/transpilers/soh/CMakeLists.txt b/transpilers/soh/CMakeLists.txt index 01c0815..790eb5a 100644 --- a/transpilers/soh/CMakeLists.txt +++ b/transpilers/soh/CMakeLists.txt @@ -25,6 +25,6 @@ if(BUILD_TESTING) ) rls_add_gtest(soh_tests ${soh_test_sources}) - target_link_libraries(soh_tests PRIVATE soh parser sema) + target_link_libraries(soh_tests PRIVATE soh sema) target_include_directories(soh_tests PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") endif() diff --git a/transpilers/soh/src/enum_mappings.h b/transpilers/soh/src/enum_mappings.h index 7d157a9..e1137b6 100644 --- a/transpilers/soh/src/enum_mappings.h +++ b/transpilers/soh/src/enum_mappings.h @@ -18,8 +18,8 @@ inline const HostEnumMapping* findHostEnumMapping(std::string_view enumName) { {"Trick", "RandomizerTrick", "RandomizerTrick"}, {"Setting", "RandomizerSettingKey", ""}, {"Region", "RandomizerRegion", "RandomizerRegion"}, - {"Check", "RandomizerCheck", "RandomizerCheck"}, - {"Logic", "LogicVal", "LogicVal"}, + {"Location", "RandomizerCheck", "RandomizerCheck"}, + {"Event", "LogicVal", "LogicVal"}, {"Scene", "SceneID", "SceneID"}, {"Dungeon", "DungeonKey", "DungeonKey"}, {"Area", "RandomizerArea", "RandomizerArea"}, diff --git a/transpilers/soh/src/generate_expression.cpp b/transpilers/soh/src/generate_expression.cpp index 2b9662e..27db75d 100644 --- a/transpilers/soh/src/generate_expression.cpp +++ b/transpilers/soh/src/generate_expression.cpp @@ -59,6 +59,17 @@ std::string SohTranspiler::GenerateExpression(const rls::ast::Identifier& node) return node.name.text; } else if (node.kind == rls::ast::IdentifierKind::Parameter) { return node.name.text; + } else if (node.kind == rls::ast::IdentifierKind::DeclaredValue) { + if (project.RegionDecls.contains(node.name.text)) { + return qualifyEnumValue("Region", node.name.text); + } + if (project.EventDecls.contains(node.name.text)) { + return qualifyEnumValue("Event", node.name.text); + } + if (project.LocationDecls.contains(node.name.text)) { + return qualifyEnumValue("Location", node.name.text); + } + return ""; } else if (node.kind == rls::ast::IdentifierKind::FunctionRef) { return node.name.text; } else { diff --git a/transpilers/soh/src/generate_functions.cpp b/transpilers/soh/src/generate_functions.cpp index 18528d0..72175a8 100644 --- a/transpilers/soh/src/generate_functions.cpp +++ b/transpilers/soh/src/generate_functions.cpp @@ -74,6 +74,9 @@ std::string nodeType(const rls::ast::Project& p, const T* node) { case AT::Callable: return "std::function"; case AT::Condition: return "std::function"; case AT::Enum: return enumNodeType(p, node); + case AT::Region: return "RandomizerRegion"; + case AT::Event: return "LogicVal"; + case AT::Location: return "RandomizerCheck"; default: return "unsupported_type"; } } diff --git a/transpilers/soh/src/generate_regions.cpp b/transpilers/soh/src/generate_regions.cpp index f5e8ee1..31abb19 100644 --- a/transpilers/soh/src/generate_regions.cpp +++ b/transpilers/soh/src/generate_regions.cpp @@ -15,11 +15,7 @@ void addDataError( std::string message, const rls::ast::Span& span) { - diagnostics.push_back({ - rls::ast::DiagnosticLevel::Error, - std::move(message), - span - }); + diagnostics.push_back(ast::Diagnostic{"", span, rls::ast::DiagnosticLevel::Error, std::move(message)}); } bool hasEnumType( diff --git a/transpilers/soh/tests/generate_expression_test.cpp b/transpilers/soh/tests/generate_expression_test.cpp index 52730d6..2e8dd09 100644 --- a/transpilers/soh/tests/generate_expression_test.cpp +++ b/transpilers/soh/tests/generate_expression_test.cpp @@ -464,6 +464,23 @@ TEST(SohExpressions, CallHostFunctions) { "any_age([]{return has(RandomizerGet::RG_HOOKSHOT) || can_use(RandomizerGet::RG_BOOMERANG);})"); } +TEST(SohExpressions, DeclaredDomainValuesUseHostNamespaces) { + EXPECT_EQ(GenerateExpression(sourceToExpression( + "extern define use_event(value: Event) -> Bool\n" + "extern define use_location(value: Location) -> Bool\n" + "region RR_TARGET {\n" + " events { LOGIC_OPEN: true }\n" + " locations { RC_CHEST: true }\n" + "}\n" + "define test(): can_plant_bean(RR_TARGET, RG_KOKIRI_FOREST_BEAN_SOUL)\n" + " and use_event(LOGIC_OPEN)\n" + " and use_location(RC_CHEST)\n", + "test")), + "can_plant_bean(RandomizerRegion::RR_TARGET, RandomizerGet::RG_KOKIRI_FOREST_BEAN_SOUL)" + " && use_event(LogicVal::LOGIC_OPEN)" + " && use_location(RandomizerCheck::RC_CHEST)"); +} + TEST(SohExpressions, CallExternDefineReorderedAndDefaultedArgs) { EXPECT_EQ(GenerateExpression(sourceToExpression( "extern define host_custom(item: Item, distance: Distance = ED_CLOSE, enabled: Bool = false) -> Bool\n" diff --git a/transpilers/soh/tests/generate_functions_tests.cpp b/transpilers/soh/tests/generate_functions_tests.cpp index eed1861..4231fb1 100644 --- a/transpilers/soh/tests/generate_functions_tests.cpp +++ b/transpilers/soh/tests/generate_functions_tests.cpp @@ -259,4 +259,15 @@ TEST(SohSolverTests, GenerateFunctionWithUnidentifiedEnumParamUsesInt) { " return value;\n" "}\n" ); +} + +TEST(SohSolverTests, GenerateFunctionWithDomainValueParams) { + auto project = resolveFromSource( + "define uses_domain(reg: Region, evt: Event, loc: Location): true\n"); + MemoryWriter out; + rls::transpilers::soh::SohTranspiler(project).GenerateFunctionDefinitionsHeader(out); + + EXPECT_NE(out.content("functions.gen.h").find( + "bool uses_domain(const RandomizerRegion reg, const LogicVal evt, " + "const RandomizerCheck loc);"), std::string::npos); } \ No newline at end of file diff --git a/transpilers/soh/tests/helpers.h b/transpilers/soh/tests/helpers.h index fa52326..055506a 100644 --- a/transpilers/soh/tests/helpers.h +++ b/transpilers/soh/tests/helpers.h @@ -49,24 +49,24 @@ inline std::string withHostExterns(const std::string& source) { "extern enum Enemy { RE_* }\n" "extern enum Distance { ED_* }\n" "extern enum Trick { RT_* }\n" - "extern enum Logic { LOGIC_* }\n" + "extern enum Event { LOGIC_* }\n" "extern enum Scene { SCENE_* }\n" "extern enum Dungeon { DUNGEON_* }\n" "extern enum Area { RA_* }\n" "extern enum Trial { TK_* }\n" "extern enum Setting { RSK_*, RO_* }\n" "extern enum Region { RR_* }\n" - "extern enum Check { RC_* }\n" + "extern enum Location { RC_* }\n" "extern define has(item: Item) -> Bool\n" "extern define can_use(item: Item) -> Bool\n" "extern define keys(sc: Scene, amount: Int) -> Bool\n" - "extern define flag(key: Logic) -> Bool\n" + "extern define flag(key: Event) -> Bool\n" "extern define setting(key: Setting) -> Int\n" "extern define trick(key: Trick) -> Bool\n" "extern define hearts() -> Int\n" "extern define effective_health() -> Int\n" "extern define trial_skipped(key: Trial) -> Bool\n" - "extern define check_price(chk: Check = RC_UNKNOWN_CHECK) -> Int\n" + "extern define check_price(chk: Location = RC_UNKNOWN_CHECK) -> Int\n" "extern define can_plant_bean(reg: Region, bean: Item) -> Bool\n" "extern define triforce_pieces() -> Int\n" "extern define big_poes() -> Int\n"