diff --git a/README.md b/README.md index a3b6427a..748f1020 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Prometeu Studio is the desktop development environment for the Prometeu toolchain. -This repository packages the Studio shell together with the embedded services it depends on: project VFS, language tooling, compiler pipeline integration, and asset packing. The current implementation is a Java/JavaFX multi-module Gradle build. +This repository packages the Studio shell together with the embedded services it depends on: compiler pipeline integration, asset packing, runtime/debug orchestration, and project-local Studio state. The current implementation is a Java/JavaFX multi-module Gradle build. ## What Lives Here @@ -10,8 +10,6 @@ The root project is a monorepo with these main areas: - `prometeu-app`: JavaFX application entry point (`p.studio.App`). - `prometeu-studio`: Studio UI, workspaces, project session lifecycle, play/stop flow, and debugger-facing surfaces. -- `prometeu-vfs`: project document access and save/open contracts. -- `prometeu-lsp`: language-service integration used by the editor workflow. - `prometeu-packer`: embedded packer APIs and implementation used by Studio. - `prometeu-compiler`: compiler core, build pipeline, frontend APIs, and the PBS frontend. - `prometeu-infra`: shared infrastructure used across modules. @@ -21,14 +19,13 @@ The root project is a monorepo with these main areas: ## Current Product Shape -The Studio shell currently organizes the product around four workspaces: +The Studio shell currently organizes the product around three workspaces: - Assets -- Code editor - Debug - Shipper -At runtime, the app boots an embedded packer, creates a project document VFS, wires LSP services, and opens the Studio launcher window. Project creation currently scaffolds a Prometeu workspace with `prometeu.json`, `src/`, `assets/`, `build/`, `cartridge/`, `.workspace/`, and local Studio metadata. +At runtime, the app boots an embedded packer, restores project-local Studio state, and opens the Studio launcher window. Project creation currently scaffolds a Prometeu workspace with `prometeu.json`, `src/`, `assets/`, `build/`, `cartridge/`, `.workspace/`, and local Studio metadata. ## Requirements @@ -68,7 +65,7 @@ If you are new to the codebase, this order is usually the fastest path: 1. Start at `prometeu-app` to see application bootstrap. 2. Read `prometeu-studio` for the shell, workspaces, and user-facing flows. -3. Read `prometeu-vfs`, `prometeu-lsp`, and `prometeu-packer` for service boundaries. +3. Read `prometeu-packer` for asset and packaging boundaries. 4. Read `prometeu-compiler` when you need compiler or PBS language behavior. ## Specs And Architecture diff --git a/discussion/.backups/index.ndjson.20260331-004213.bak b/discussion/.backups/index.ndjson.20260331-004213.bak deleted file mode 100644 index bf3215ca..00000000 --- a/discussion/.backups/index.ndjson.20260331-004213.bak +++ /dev/null @@ -1,12 +0,0 @@ -{"type":"meta","next_id":{"DSC":12,"AGD":12,"DEC":9,"PLN":15,"LSN":26,"CLSN":1}} -{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} -{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} -{"type":"discussion","id":"DSC-0004","status":"open","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0006","status":"done","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-30","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0006-pbs-game-facing-asset-refs-and-call-result-discard/LSN-0024-addressable-surface-host-metadata-and-ignored-value-discipline.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} -{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} -{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} -{"type":"discussion","id":"DSC-0009","status":"open","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["studio","debugger","workspace","integration","shell"],"agendas":[{"id":"AGD-0009","file":"AGD-0009-studio-debugger-workspace-integration.md","status":"open","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0010","status":"in_progress","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[{"id":"AGD-0010","file":"AGD-0010-studio-code-editor-workspace-foundations.md","status":"accepted","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[{"id":"DEC-0008","file":"DEC-0008-studio-code-editor-read-only-workspace-foundations.md","status":"accepted","created_at":"2026-03-30","updated_at":"2026-03-30","ref_agenda":"AGD-0010"}],"plans":[{"id":"PLN-0012","file":"PLN-0012-studio-code-editor-spec-propagation.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0008"]},{"id":"PLN-0013","file":"PLN-0013-editor-workspace-layout-and-passive-surfaces.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0008"]},{"id":"PLN-0014","file":"PLN-0014-project-navigator-snapshot-and-read-only-file-opening.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30","ref_decisions":["DEC-0008"]}],"lessons":[]} -{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} diff --git a/discussion/.backups/index.ndjson.20260331-092554.bak b/discussion/.backups/index.ndjson.20260331-092554.bak deleted file mode 100644 index 1945b847..00000000 --- a/discussion/.backups/index.ndjson.20260331-092554.bak +++ /dev/null @@ -1,13 +0,0 @@ -{"type":"meta","next_id":{"DSC":13,"AGD":13,"DEC":10,"PLN":19,"LSN":27,"CLSN":1}} -{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} -{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} -{"type":"discussion","id":"DSC-0004","status":"open","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0006","status":"done","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-30","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0006-pbs-game-facing-asset-refs-and-call-result-discard/LSN-0024-addressable-surface-host-metadata-and-ignored-value-discipline.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} -{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} -{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} -{"type":"discussion","id":"DSC-0009","status":"open","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["studio","debugger","workspace","integration","shell"],"agendas":[{"id":"AGD-0009","file":"AGD-0009-studio-debugger-workspace-integration.md","status":"open","created_at":"2026-03-30","updated_at":"2026-03-30"}],"decisions":[],"plans":[],"lessons":[]} -{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} -{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} -{"type":"discussion","id":"DSC-0012","status":"open","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[{"id":"AGD-0012","file":"AGD-0012-studio-editor-document-vfs-boundary.md","status":"in_progress","created_at":"2026-03-31","updated_at":"2026-03-31"}],"decisions":[{"id":"DEC-0009","file":"DEC-0009-studio-prometeu-vfs-project-document-boundary.md","status":"in_progress","created_at":"2026-03-31","updated_at":"2026-03-31","ref_agenda":"AGD-0012"}],"plans":[{"id":"PLN-0015","file":"PLN-0015-propagate-dec-0009-into-studio-specs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0016","file":"PLN-0016-build-prometeu-vfs-filesystem-backed-core.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0017","file":"PLN-0017-add-studio-project-session-ownership-for-prometeu-vfs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]},{"id":"PLN-0018","file":"PLN-0018-migrate-code-editor-to-prometeu-vfs.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31","ref_decisions":["DEC-0009"]}],"lessons":[]} diff --git a/discussion/.backups/index.ndjson.20260507-142320.bak b/discussion/.backups/index.ndjson.20260507-142320.bak new file mode 100644 index 00000000..f17e173d --- /dev/null +++ b/discussion/.backups/index.ndjson.20260507-142320.bak @@ -0,0 +1,35 @@ +{"type":"meta","next_id":{"DSC":35,"AGD":38,"DEC":35,"PLN":75,"LSN":47,"CLSN":1}} +{"type":"discussion","id":"DSC-0034","status":"in_progress","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-06","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[{"id":"AGD-0037","file":"AGD-0037-frontend-semantic-host-projection-flexibility.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06"}],"decisions":[{"id":"DEC-0034","file":"DEC-0034-frontend-semantic-host-projection-flexibility.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06","ref_agenda":"AGD-0037"}],"plans":[{"id":"PLN-0072","file":"PLN-0072-frontend-semantic-host-projection-contract.md","status":"done","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]},{"id":"PLN-0073","file":"PLN-0073-lsp-semantic-host-projection-transport.md","status":"done","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]},{"id":"PLN-0074","file":"PLN-0074-vscode-semantic-host-projection-adapter.md","status":"done","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0034"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0033","status":"in_progress","ticket":"frontend-visual-theme-spec-and-css-retirement","title":"Frontend visual theme spec and retirement of host-consumed semantic CSS","created_at":"2026-05-06","updated_at":"2026-05-06","tags":["compiler","compiler-general","frontend","presentation","theming","studio","vscode","lsp","pbs"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-frontend-visual-theme-spec-and-css-retirement.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06"}],"decisions":[{"id":"DEC-0033","file":"DEC-0033-frontend-visual-theme-spec-and-css-retirement.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0068","file":"PLN-0068-frontend-visual-contract-and-pbs-migration.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0069","file":"PLN-0069-lsp-visual-theme-transport-and-selection.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0070","file":"PLN-0070-studio-visual-theme-adapter-and-css-retirement.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0071","file":"PLN-0071-vscode-dynamic-theme-translation-adapter.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0032","status":"in_progress","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[{"id":"AGD-0035","file":"AGD-0035-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0032","file":"DEC-0032-studio-new-lsp-api-and-v1-boundary.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0035"}],"plans":[{"id":"PLN-0065","file":"PLN-0065-lsp-boundary-and-module-scaffolding.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0066","file":"PLN-0066-project-scoped-lsp-server-lifecycle-in-studio.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]},{"id":"PLN-0067","file":"PLN-0067-compiler-backed-dumb-lsp-server-baseline.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0032"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]} +{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} +{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} +{"type":"discussion","id":"DSC-0027","status":"abandoned","ticket":"studio-scene-workspace","title":"Scene Workspace for SCENE Authoring","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["studio","workspace","scene","tilemap","asset","runtime-alignment"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted agenda and its downstream work."}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0029","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted decision and stop using it as normative guidance."}],"plans":[{"id":"PLN-0049","file":"PLN-0049-scene-workspace-spec-and-boundary-propagation.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0050","file":"PLN-0050-scene-workspace-shell-and-project-state-foundations.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0051","file":"PLN-0051-scene-artifact-and-assets-handoff-contract.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0052","file":"PLN-0052-scene-workspace-wave-1-tilemap-editor.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."}],"lessons":[]} +{"type":"discussion","id":"DSC-0026","status":"done","ticket":"glyph-bank-naming-alignment-with-runtime","title":"Glyph Bank Naming Alignment with Runtime DEC-0006","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["packer","studio","naming","asset-contract","runtime-alignment","glyph-bank"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0026-glyph-bank-naming-alignment-with-runtime/LSN-0040-glyph-bank-artifact-naming-alignment.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0025","status":"done","ticket":"packer-pipeline-metadata-ownership","title":"Pipeline Metadata Ownership and Runtime Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["packer","metadata","runtime-contract","tooling","studio"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0025-packer-pipeline-metadata-ownership/LSN-0039-runtime-header-boundary-and-tooling-owned-pipeline-metadata.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"},{"id":"LSN-0044","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0044-jenkins-jacoco-alignment.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} +{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} +{"type":"discussion","id":"DSC-0002","status":"done","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-04-23","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0002-palette-management-in-studio/LSN-0042-schema-driven-palette-authoring-and-local-metadata-events.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} +{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} +{"type":"discussion","id":"DSC-0004","status":"abandoned","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-04-24","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"abandoned","created_at":"2026-03-26","updated_at":"2026-04-24","_override_reason":"Explicit user request on 2026-04-24 to abandon AGD-0004 because the agenda is no longer valid."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0006","status":"done","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-30","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0006-pbs-game-facing-asset-refs-and-call-result-discard/LSN-0024-addressable-surface-host-metadata-and-ignored-value-discipline.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} +{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0009","status":"done","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-04-07","tags":["studio","debugger","workspace","integration","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} +{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} +{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} +{"type":"discussion","id":"DSC-0013","status":"done","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-04-02","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} +{"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} +{"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]} +{"type":"discussion","id":"DSC-0016","status":"done","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-06","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"discussion/lessons/DSC-0016-studio-editor-scope-guides-and-brace-anchoring/LSN-0034-active-scope-gutter-and-frontend-owned-structural-anchors.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]} +{"type":"discussion","id":"DSC-0017","status":"done","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-06","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"discussion/lessons/DSC-0017-studio-editor-inline-type-hints-for-let-bindings/LSN-0035-frontend-owned-inline-hints-with-decorative-inline-rendering.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]} +{"type":"discussion","id":"DSC-0018","status":"done","ticket":"studio-project-local-studio-state-under-dot-studio","title":"Persist project-local Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","shell","layout","setup"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"discussion/lessons/DSC-0018-studio-project-local-studio-state-under-dot-studio/LSN-0031-project-local-studio-state-and-lifecycle-safe-layout-restoration.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]} +{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]} +{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]} +{"type":"discussion","id":"DSC-0021","status":"done","ticket":"studio-frontend-editor-write-and-save-wave","title":"Enable Frontend Editing and Save in the Studio Code Editor","created_at":"2026-04-04","updated_at":"2026-04-06","tags":["studio","editor","frontend","write","save","vfs","lsp","pbs","access-policy"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]} +{"type":"discussion","id":"DSC-0022","status":"done","ticket":"studio-play-stop-cartridge-flow","title":"Wire the Studio Play/Stop button to shipper phase 1 cartridge preparation and runtime execution","created_at":"2026-04-06","updated_at":"2026-04-07","tags":["studio","play-stop","shipper","runtime","cartridge","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0043-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} +{"type":"discussion","id":"DSC-0023","status":"done","ticket":"gradle-version-catalog-migration","title":"Move dependency version management to Version Catalog (TOML)","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["gradle","buildSrc","dependency-management","version-catalog"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0023-gradle-version-catalog-migration/LSN-0037-gradle-version-catalog-adoption.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} diff --git a/discussion/.backups/index.ndjson.20260508-091553.bak b/discussion/.backups/index.ndjson.20260508-091553.bak new file mode 100644 index 00000000..b88da099 --- /dev/null +++ b/discussion/.backups/index.ndjson.20260508-091553.bak @@ -0,0 +1,35 @@ +{"type":"meta","next_id":{"DSC":35,"AGD":38,"DEC":35,"PLN":75,"LSN":49,"CLSN":1}} +{"type":"discussion","id":"DSC-0034","status":"done","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-07","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md","status":"done","created_at":"2026-05-07","updated_at":"2026-05-07"}]} +{"type":"discussion","id":"DSC-0033","status":"in_progress","ticket":"frontend-visual-theme-spec-and-css-retirement","title":"Frontend visual theme spec and retirement of host-consumed semantic CSS","created_at":"2026-05-06","updated_at":"2026-05-06","tags":["compiler","compiler-general","frontend","presentation","theming","studio","vscode","lsp","pbs"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-frontend-visual-theme-spec-and-css-retirement.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06"}],"decisions":[{"id":"DEC-0033","file":"DEC-0033-frontend-visual-theme-spec-and-css-retirement.md","status":"accepted","created_at":"2026-05-06","updated_at":"2026-05-06","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0068","file":"PLN-0068-frontend-visual-contract-and-pbs-migration.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0069","file":"PLN-0069-lsp-visual-theme-transport-and-selection.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0070","file":"PLN-0070-studio-visual-theme-adapter-and-css-retirement.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]},{"id":"PLN-0071","file":"PLN-0071-vscode-dynamic-theme-translation-adapter.md","status":"open","created_at":"2026-05-06","updated_at":"2026-05-06","ref_decisions":["DEC-0033"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0032","status":"done","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-07","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0032-studio-new-lsp-api-and-v1-boundary/LSN-0047-project-scoped-lsp-boundary-and-protocol-containment.md","status":"done","created_at":"2026-05-07","updated_at":"2026-05-07"}]} +{"type":"discussion","id":"DSC-0031","status":"in_progress","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-05","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[{"id":"AGD-0034","file":"AGD-0034-vscode-editor-migration-feasibility.md","status":"accepted","created_at":"2026-05-05","updated_at":"2026-05-05"}],"decisions":[{"id":"DEC-0031","file":"DEC-0031-studio-editor-workspace-cleanup.md","status":"in_progress","created_at":"2026-05-05","updated_at":"2026-05-05","ref_agenda":"AGD-0034"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-shell-session-and-state-cleanup-after-editor-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0063","file":"PLN-0063-legacy-lsp-and-vfs-module-removal-with-runtime-preservation.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]},{"id":"PLN-0064","file":"PLN-0064-normative-test-and-lesson-cleanup-for-editor-stack-removal.md","status":"done","created_at":"2026-05-05","updated_at":"2026-05-05","ref_decisions":["DEC-0031"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]} +{"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} +{"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} +{"type":"discussion","id":"DSC-0027","status":"abandoned","ticket":"studio-scene-workspace","title":"Scene Workspace for SCENE Authoring","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["studio","workspace","scene","tilemap","asset","runtime-alignment"],"agendas":[{"id":"AGD-0029","file":"AGD-0029-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted agenda and its downstream work."}],"decisions":[{"id":"DEC-0026","file":"DEC-0026-studio-scene-workspace.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0029","_override_reason":"Explicit user request on 2026-04-15 to abandon the accepted decision and stop using it as normative guidance."}],"plans":[{"id":"PLN-0049","file":"PLN-0049-scene-workspace-spec-and-boundary-propagation.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0050","file":"PLN-0050-scene-workspace-shell-and-project-state-foundations.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0051","file":"PLN-0051-scene-artifact-and-assets-handoff-contract.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."},{"id":"PLN-0052","file":"PLN-0052-scene-workspace-wave-1-tilemap-editor.md","status":"abandoned","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0026"],"_override_reason":"Explicit user request on 2026-04-15 to abandon all plans derived from DEC-0026."}],"lessons":[]} +{"type":"discussion","id":"DSC-0026","status":"done","ticket":"glyph-bank-naming-alignment-with-runtime","title":"Glyph Bank Naming Alignment with Runtime DEC-0006","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["packer","studio","naming","asset-contract","runtime-alignment","glyph-bank"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0026-glyph-bank-naming-alignment-with-runtime/LSN-0040-glyph-bank-artifact-naming-alignment.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0025","status":"done","ticket":"packer-pipeline-metadata-ownership","title":"Pipeline Metadata Ownership and Runtime Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["packer","metadata","runtime-contract","tooling","studio"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0025-packer-pipeline-metadata-ownership/LSN-0039-runtime-header-boundary-and-tooling-owned-pipeline-metadata.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} +{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"},{"id":"LSN-0044","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0044-jenkins-jacoco-alignment.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} +{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} +{"type":"discussion","id":"DSC-0002","status":"done","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-04-23","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0002-palette-management-in-studio/LSN-0042-schema-driven-palette-authoring-and-local-metadata-events.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} +{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]} +{"type":"discussion","id":"DSC-0004","status":"abandoned","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-04-24","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"abandoned","created_at":"2026-03-26","updated_at":"2026-04-24","_override_reason":"Explicit user request on 2026-04-24 to abandon AGD-0004 because the agenda is no longer valid."}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0006","status":"done","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-30","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0006-pbs-game-facing-asset-refs-and-call-result-discard/LSN-0024-addressable-surface-host-metadata-and-ignored-value-discipline.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} +{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]} +{"type":"discussion","id":"DSC-0009","status":"done","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-04-07","tags":["studio","debugger","workspace","integration","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} +{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]} +{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]} +{"type":"discussion","id":"DSC-0013","status":"done","ticket":"studio-editor-write-wave-supported-non-frontend-files","title":"Definir a wave inicial de edicao no Code Editor apenas para arquivos aceitos e nao relacionados ao FE","created_at":"2026-03-31","updated_at":"2026-04-02","tags":["studio","editor","workspace","write","read-only","vfs","frontend-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0028","file":"discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} +{"type":"discussion","id":"DSC-0014","status":"done","ticket":"studio-frontend-owned-semantic-editor-presentation","title":"Definir ownership do schema visual semantico do editor por frontend","created_at":"2026-04-02","updated_at":"2026-04-02","tags":["studio","editor","frontend","presentation","semantic-highlighting","compiler","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0029","file":"discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md","status":"done","created_at":"2026-04-02","updated_at":"2026-04-02"}]} +{"type":"discussion","id":"DSC-0015","status":"done","ticket":"pbs-service-facade-reserved-metadata","title":"SDK Service Bodies Calling Builtin/Intrinsic Proxies as Ordinary PBS Code","created_at":"2026-04-03","updated_at":"2026-04-03","tags":["compiler","pbs","sdk","stdlib","lowering","service","intrinsic","sdk-interface"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"discussion/lessons/DSC-0015-pbs-service-facade-reserved-metadata/LSN-0030-sdk-service-bodies-over-private-reserved-proxies.md","status":"done","created_at":"2026-04-03","updated_at":"2026-04-03"}]} +{"type":"discussion","id":"DSC-0016","status":"done","ticket":"studio-editor-scope-guides-and-brace-anchoring","title":"Scope Guides do Code Editor com ancoragem exata em braces e destaque do escopo ativo","created_at":"2026-04-03","updated_at":"2026-04-06","tags":["studio","editor","scope-guides","braces","semantic-read","frontend-contract"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0034","file":"discussion/lessons/DSC-0016-studio-editor-scope-guides-and-brace-anchoring/LSN-0034-active-scope-gutter-and-frontend-owned-structural-anchors.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]} +{"type":"discussion","id":"DSC-0017","status":"done","ticket":"studio-editor-inline-type-hints-for-let-bindings","title":"Inline Type Hints for Let Bindings in the Studio Editor","created_at":"2026-04-03","updated_at":"2026-04-06","tags":["studio","editor","inline-hints","inlay-hints","lsp","pbs","type-inference"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0035","file":"discussion/lessons/DSC-0017-studio-editor-inline-type-hints-for-let-bindings/LSN-0035-frontend-owned-inline-hints-with-decorative-inline-rendering.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]} +{"type":"discussion","id":"DSC-0018","status":"done","ticket":"studio-project-local-studio-state-under-dot-studio","title":"Persist project-local Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","shell","layout","setup"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"discussion/lessons/DSC-0018-studio-project-local-studio-state-under-dot-studio/LSN-0031-project-local-studio-state-and-lifecycle-safe-layout-restoration.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]} +{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]} +{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]} +{"type":"discussion","id":"DSC-0021","status":"done","ticket":"studio-frontend-editor-write-and-save-wave","title":"Enable Frontend Editing and Save in the Studio Code Editor","created_at":"2026-04-04","updated_at":"2026-04-06","tags":["studio","editor","frontend","write","save","vfs","lsp","pbs","access-policy"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]} +{"type":"discussion","id":"DSC-0022","status":"done","ticket":"studio-play-stop-cartridge-flow","title":"Wire the Studio Play/Stop button to shipper phase 1 cartridge preparation and runtime execution","created_at":"2026-04-06","updated_at":"2026-04-07","tags":["studio","play-stop","shipper","runtime","cartridge","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0043-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} +{"type":"discussion","id":"DSC-0023","status":"done","ticket":"gradle-version-catalog-migration","title":"Move dependency version management to Version Catalog (TOML)","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["gradle","buildSrc","dependency-management","version-catalog"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0023-gradle-version-catalog-migration/LSN-0037-gradle-version-catalog-adoption.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 1c6ed076..f52fcf37 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,10 @@ -{"type":"meta","next_id":{"DSC":31,"AGD":34,"DEC":31,"PLN":62,"LSN":47,"CLSN":1}} +{"type":"meta","next_id":{"DSC":37,"AGD":40,"DEC":36,"PLN":78,"LSN":51,"CLSN":1}} +{"type":"discussion","id":"DSC-0036","status":"open","ticket":"pbs-symbol-documentation-and-hover-markdown","title":"Modelo de documentacao de simbolos em PBS e consumo markdown no hover","created_at":"2026-05-08","updated_at":"2026-05-08","tags":["compiler","compiler-pbs","studio","lsp","vscode","editor","hover","documentation","markdown"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-pbs-symbol-documentation-and-hover-markdown.md","status":"open","created_at":"2026-05-08","updated_at":"2026-05-08"}],"decisions":[],"plans":[],"lessons":[]} +{"type":"discussion","id":"DSC-0035","status":"in_progress","ticket":"pbs-lsp-editor-assistance-wave-1","title":"Wave 1 de assistencia editorial via LSP para PBS no VS Code","created_at":"2026-05-08","updated_at":"2026-05-08","tags":["studio","lsp","vscode","compiler","compiler-pbs","editor","completion","hover","signature-help"],"agendas":[{"id":"AGD-0038","file":"AGD-0038-pbs-lsp-editor-assistance-wave-1.md","status":"accepted","created_at":"2026-05-08","updated_at":"2026-05-08"}],"decisions":[{"id":"DEC-0035","file":"DEC-0035-pbs-lsp-editor-assistance-wave-1.md","status":"accepted","created_at":"2026-05-08","updated_at":"2026-05-08","ref_agenda":"AGD-0038"}],"plans":[{"id":"PLN-0075","file":"PLN-0075-pbs-editorial-resolution-surface-for-symbols-members-and-signatures.md","status":"open","created_at":"2026-05-08","updated_at":"2026-05-08","ref_decisions":["DEC-0035"]},{"id":"PLN-0076","file":"PLN-0076-lsp-completion-hover-and-signature-help-over-editorial-resolution.md","status":"open","created_at":"2026-05-08","updated_at":"2026-05-08","ref_decisions":["DEC-0035"]},{"id":"PLN-0077","file":"PLN-0077-vscode-consumption-and-wave-1-editor-assistance-validation.md","status":"open","created_at":"2026-05-08","updated_at":"2026-05-08","ref_decisions":["DEC-0035"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0034","status":"done","ticket":"frontend-semantic-host-projection-flexibility","title":"Frontend semantic vocabulary flexibility and declarative host projection","created_at":"2026-05-06","updated_at":"2026-05-07","tags":["compiler","compiler-general","frontend","semantics","vscode","host-projection","lsp"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0048","file":"discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md","status":"done","created_at":"2026-05-07","updated_at":"2026-05-07"}]} +{"type":"discussion","id":"DSC-0033","status":"done","ticket":"frontend-visual-theme-spec-and-css-retirement","title":"Frontend visual theme spec and retirement of host-consumed semantic CSS","created_at":"2026-05-06","updated_at":"2026-05-08","tags":["compiler","compiler-general","frontend","presentation","theming","studio","vscode","lsp","pbs"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0050","file":"discussion/lessons/DSC-0033-frontend-visual-theme-spec-and-css-retirement/LSN-0050-frontend-owned-visual-themes-with-structured-contract-and-host-adapters.md","status":"done","created_at":"2026-05-08","updated_at":"2026-05-08"}]} +{"type":"discussion","id":"DSC-0032","status":"done","ticket":"studio-new-lsp-api-and-v1-boundary","title":"Novo boundary entre lsp-api, lsp-v1 e a extensao VS Code","created_at":"2026-05-05","updated_at":"2026-05-07","tags":["studio","lsp","vscode","protocol","api","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0047","file":"discussion/lessons/DSC-0032-studio-new-lsp-api-and-v1-boundary/LSN-0047-project-scoped-lsp-boundary-and-protocol-containment.md","status":"done","created_at":"2026-05-07","updated_at":"2026-05-07"}]} +{"type":"discussion","id":"DSC-0031","status":"done","ticket":"studio-editor-workspace-cleanup","title":"Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados","created_at":"2026-05-05","updated_at":"2026-05-08","tags":["studio","editor","cleanup","vfs","lsp","migration"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0049","file":"discussion/lessons/DSC-0031-studio-editor-workspace-cleanup/LSN-0049-remove-the-embedded-editor-stack-before-rebuilding-code-workflows.md","status":"done","created_at":"2026-05-08","updated_at":"2026-05-08"}]} {"type":"discussion","id":"DSC-0030","status":"done","ticket":"studio-scene-pack-runtime-binary-contract","title":"Studio scene pack contract for runtime SCENE binary payload","created_at":"2026-04-24","updated_at":"2026-05-01","tags":["studio","packer","runtime","scene","asset-pack","binary-format","tiled"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0030-studio-scene-pack-runtime-binary-contract/LSN-0046-canonical-scene-owns-editorial-truth-while-pack-stays-request-driven.md","status":"done","created_at":"2026-05-01","updated_at":"2026-05-01"}]} {"type":"discussion","id":"DSC-0029","status":"done","ticket":"studio-frame-composer-syscall-and-sprite-alignment","title":"Studio Alignment with Runtime FrameComposer Syscalls and Sprite Composition","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["studio","compiler","pbs","stdlib","runtime-alignment","abi","syscall","frame-composer","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0029-studio-frame-composer-syscall-and-sprite-alignment/LSN-0041-composer-must-own-public-sprite-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]} {"type":"discussion","id":"DSC-0028","status":"done","ticket":"studio-tiled-parser-assets-scene-asset-type","title":"Tiled Parser and Scene Asset-Type Ownership in Assets Workspace","created_at":"2026-04-15","updated_at":"2026-04-23","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0028-studio-tiled-parser-assets-scene-asset-type/LSN-0045-asset-owned-scene-bank-and-tiled-xml-workflow.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]} diff --git a/discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md b/discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md index 6fb7d673..59ff60de 100644 --- a/discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md +++ b/discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md @@ -6,6 +6,10 @@ created: 2026-03-31 tags: [studio, editor, workspace, read-only, navigator, tabs, lsp-deferred, ui-foundations] --- +## Historical Status + +This lesson documents the removed embedded Studio code editor. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context Studio needed a real `Code Editor` workspace, but the team explicitly did not want the first wave to collapse UI structure, file management, write behavior, and language semantics into one premature implementation. diff --git a/discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md b/discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md index 12253223..02548f45 100644 --- a/discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md +++ b/discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md @@ -6,6 +6,10 @@ created: 2026-03-31 tags: [studio, editor, vfs, project-session, filesystem, boundary, architecture] --- +## Historical Status + +This lesson documents the removed embedded Studio editor/VFS split. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The first Studio `Code Editor` wave had already established a read-only editorial shell, but the concrete implementation still let the editor package own direct filesystem concerns for: diff --git a/discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md b/discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md index 5d1af977..fd1ac79b 100644 --- a/discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md +++ b/discussion/lessons/DSC-0013-studio-editor-write-wave-supported-non-frontend-files/LSN-0028-controlled-editor-write-wave-and-read-only-frontend-semantic-phase.md @@ -6,6 +6,10 @@ created: 2026-04-02 tags: [studio, editor, vfs, lsp, access-policy, save, read-only, frontend-boundary] --- +## Historical Status + +This lesson documents the removed embedded Studio editor, VFS, and legacy LSP flow. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The first Studio `Code Editor` implementation had already established a session-owned document boundary through `prometeu-vfs`, but it was still missing the next operational split: diff --git a/discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md b/discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md index 08b8eaaa..e696df55 100644 --- a/discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md +++ b/discussion/lessons/DSC-0014-studio-frontend-owned-semantic-editor-presentation/LSN-0029-frontend-owned-semantic-presentation-descriptor-and-host-consumption.md @@ -6,6 +6,10 @@ created: 2026-04-02 tags: [studio, editor, frontend, semantic-highlighting, lsp, compiler, pbs, presentation] --- +## Historical Status + +This lesson documents the removed embedded Studio semantic editor presentation path. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The first semantic highlight wave for frontend documents had already proved that Studio could consume semantic spans through the integrated LSP path. diff --git a/discussion/lessons/DSC-0016-studio-editor-scope-guides-and-brace-anchoring/LSN-0034-active-scope-gutter-and-frontend-owned-structural-anchors.md b/discussion/lessons/DSC-0016-studio-editor-scope-guides-and-brace-anchoring/LSN-0034-active-scope-gutter-and-frontend-owned-structural-anchors.md index 6fef5de8..3dc30133 100644 --- a/discussion/lessons/DSC-0016-studio-editor-scope-guides-and-brace-anchoring/LSN-0034-active-scope-gutter-and-frontend-owned-structural-anchors.md +++ b/discussion/lessons/DSC-0016-studio-editor-scope-guides-and-brace-anchoring/LSN-0034-active-scope-gutter-and-frontend-owned-structural-anchors.md @@ -6,6 +6,10 @@ created: 2026-04-06 tags: [studio, editor, scope-guides, structural-anchors, semantic-read, frontend-contract] --- +## Historical Status + +This lesson documents the removed embedded Studio editor gutter and structural-anchor flow. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The Studio editor already exposed structural guides in the gutter, but the old behavior mixed two different concerns: diff --git a/discussion/lessons/DSC-0017-studio-editor-inline-type-hints-for-let-bindings/LSN-0035-frontend-owned-inline-hints-with-decorative-inline-rendering.md b/discussion/lessons/DSC-0017-studio-editor-inline-type-hints-for-let-bindings/LSN-0035-frontend-owned-inline-hints-with-decorative-inline-rendering.md index 6148aa40..28c9df00 100644 --- a/discussion/lessons/DSC-0017-studio-editor-inline-type-hints-for-let-bindings/LSN-0035-frontend-owned-inline-hints-with-decorative-inline-rendering.md +++ b/discussion/lessons/DSC-0017-studio-editor-inline-type-hints-for-let-bindings/LSN-0035-frontend-owned-inline-hints-with-decorative-inline-rendering.md @@ -6,6 +6,10 @@ created: 2026-04-06 tags: [studio, editor, inline-hints, lsp, compiler, pbs, type-inference, rendering] --- +## Historical Status + +This lesson documents the removed embedded Studio inline-hint path. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The repository needed inline type hints for inferred PBS `let` bindings, but the real architectural issue was broader than one PBS feature. diff --git a/discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md b/discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md index faa8ecc2..0167ed5c 100644 --- a/discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md +++ b/discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md @@ -6,6 +6,10 @@ created: 2026-04-04 tags: [studio, editor, indentation, tabs, setup, dot-studio, project-creation, gitignore] --- +## Historical Status + +This lesson documents indentation policy choices made for the removed embedded Studio editor. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The Studio editor exposed an indentation chip in the status bar, but that chip was driven by file-content heuristics rather than by a stable editor policy. diff --git a/discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md b/discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md index 15045bb1..10ae674c 100644 --- a/discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md +++ b/discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md @@ -6,6 +6,10 @@ created: 2026-04-06 tags: [studio, editor, frontend, write, save, vfs, lsp, access-policy, persistence] --- +## Historical Status + +This lesson documents the removed embedded Studio frontend edit/save wave. Keep it as historical context only; do not treat it as current product guidance after `DEC-0031`. + ## Context The first editor write wave had deliberately kept frontend files hard `read-only` while semantic-read support shipped through `prometeu-lsp`. diff --git a/discussion/lessons/DSC-0031-studio-editor-workspace-cleanup/LSN-0049-remove-the-embedded-editor-stack-before-rebuilding-code-workflows.md b/discussion/lessons/DSC-0031-studio-editor-workspace-cleanup/LSN-0049-remove-the-embedded-editor-stack-before-rebuilding-code-workflows.md new file mode 100644 index 00000000..f43682a2 --- /dev/null +++ b/discussion/lessons/DSC-0031-studio-editor-workspace-cleanup/LSN-0049-remove-the-embedded-editor-stack-before-rebuilding-code-workflows.md @@ -0,0 +1,84 @@ +--- +id: LSN-0049 +discussion: DSC-0031 +title: Remove the embedded editor stack before rebuilding code workflows +status: done +created: 2026-05-08 +updated: 2026-05-08 +tags: [studio, editor, cleanup, lsp, vfs, shell, migration] +--- + +## Original Problem + +Studio still carried an embedded `Code Editor` workspace together with a `prometeu-vfs` boundary and a legacy `prometeu-lsp` path that existed only to support that editor. + +That stack had become architectural drag: + +- the JavaFX editor was no longer a product direction; +- the legacy `VFS` and `LSP` contracts were shaped around that editor model; +- shell state and project session still persisted editor-specific assumptions; +- and keeping those surfaces alive made any future code-backend redesign harder. + +## Consolidated Decision + +`DEC-0031` locked a full cleanup of the embedded editor stack. + +The repository removed: + +- the `Code Editor` workspace as an active Studio surface; +- the legacy editor-owned `VFS` boundary; +- the legacy editor-era `LSP` assumptions; +- shell/session/state dependencies that expected the editor to exist; +- and normative references that treated that stack as current architecture. + +The decision also locked an equally important limit: + +- this cleanup was not allowed to define the future code backend; +- a replacement LSP or automation boundary had to come later, in separate discussion. + +## Final Result + +The Studio shell now opens with surviving workspaces only: `Assets`, `Debug`, and `Shipper`. + +Project-local state no longer persists editor restoration or editor layout fields, while still loading old state safely and falling back to surviving shell behavior. + +The cleanup also succeeded without freezing the repository in a "no LSP ever again" state. +Later work in `DSC-0032` introduced a new project-scoped LSP boundary as separate architecture, which is exactly what `DEC-0031` required: remove the old stack first, then rebuild cleanly under a new decision. + +## Implementation Notes + +The cleanup landed in three coordinated waves: + +1. shell/session/state cleanup removed `WorkspaceId.EDITOR`, editor restoration state, and editor-owned session composition; +2. the old editor-era `VFS` and `LSP` stack stopped being the active Studio foundation; +3. specs, tests, and historical references were revised so the removed editor stack no longer appears as current product guidance. + +This produced an important boundary reset: + +- Studio remains responsible for launcher, assets, shipper, debug, build, and run orchestration; +- code-editing architecture is no longer smuggled in through dead UI or legacy session seams. + +## Example + +Before the cleanup, an old `.studio/state.json` could still restore editor-centric state. + +After the cleanup, the same persisted payload is treated defensively: + +- old editor fields are ignored; +- the selected workspace falls back safely when `EDITOR` is present; +- the project reopens into a surviving workspace instead of reviving a dead surface. + +## Pitfalls and Anti-Patterns + +- Do not preserve dead editor-era boundaries "for compatibility" when the product direction has already moved. +- Do not treat naming continuity as architectural continuity. A later `prometeu-lsp` module can be valid if it comes from a new discussion and a new boundary. +- Do not leave project-local state as an accidental migration trap; persisted legacy fields must degrade safely. + +## References + +- `AGD-0034` Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados +- `DEC-0031` Limpeza completa do Workspace Editor do Studio e remoção dos acoplamentos legados +- `PLN-0062` Shell Session and State Cleanup After Editor Removal +- `PLN-0063` Legacy LSP and VFS Module Removal with Runtime Preservation +- `PLN-0064` Normative Test and Lesson Cleanup for Editor Stack Removal +- `LSN-0047` Project-scoped LSP boundary and protocol containment diff --git a/discussion/lessons/DSC-0032-studio-new-lsp-api-and-v1-boundary/LSN-0047-project-scoped-lsp-boundary-and-protocol-containment.md b/discussion/lessons/DSC-0032-studio-new-lsp-api-and-v1-boundary/LSN-0047-project-scoped-lsp-boundary-and-protocol-containment.md new file mode 100644 index 00000000..a099f39a --- /dev/null +++ b/discussion/lessons/DSC-0032-studio-new-lsp-api-and-v1-boundary/LSN-0047-project-scoped-lsp-boundary-and-protocol-containment.md @@ -0,0 +1,106 @@ +--- +id: LSN-0047 +ticket: studio-new-lsp-api-and-v1-boundary +title: Project-Scoped LSP Boundary and Protocol Containment +created: 2026-05-07 +tags: [studio, lsp, vscode, protocol, api, boundary, compiler] +--- + +## Context + +After the legacy embedded editor stack was removed, Prometeu needed a new LSP baseline without repeating the old mistake of collapsing protocol, session lifecycle, host UI, and semantic ownership into one module. + +The main architectural pressure came from two facts: + +- the VS Code extension already existed as a real LSP client over TCP, +- and the compiler already existed as the canonical owner of semantic behavior. + +That meant the missing piece was not "add editor features first". The missing piece was a strict boundary that let the Studio host, the protocol adapter, and the compiler evolve without contaminating each other. + +## Key Decisions + +### Keep `lsp-api` Minimal and Internal + +**What:** +`lsp-api` became a narrow internal Studio boundary, starting with project-scoped lifecycle operations such as boot and shutdown instead of mirroring the LSP protocol. + +**Why:** +The Studio needed a reusable internal service boundary, not a second copy of the external wire protocol. + +**Trade-offs:** +The API starts intentionally small and may need explicit growth later, but that is safer than locking protocol-shaped DTOs into the internal architecture too early. + +### Contain `LSP4J` and Protocol DTOs Inside `lsp-v1` + +**What:** +`lsp-v1` became the only concrete protocol adapter and the only module allowed to depend on `LSP4J`. + +**Why:** +Protocol libraries are integration details. If they leak outward, the host architecture starts depending on the current transport and implementation framework instead of on stable domain boundaries. + +**Trade-offs:** +This forces explicit mapping layers and a little more ceremony, but it keeps protocol churn from infecting the rest of the codebase. + +### Make the Server Lifecycle Project-Scoped + +**What:** +The LSP server now belongs to project open/close lifecycle instead of global Studio process startup. + +**Why:** +Project scope is the real ownership boundary for source roots, compiler context, and editor-facing behavior. + +**Trade-offs:** +Lifecycle wiring is slightly more involved, but resource ownership becomes predictable and multi-project behavior stays sane. + +### Keep `compiler` as Semantic Owner Even in a Dumb First Wave + +**What:** +The baseline server remained intentionally simple in behavior, but its request handling already routes through compiler-facing bridge seams. + +**Why:** +A "temporary mock" becomes dangerous when it also becomes the architecture. The repository needed a structurally correct baseline before adding semantic depth. + +**Trade-offs:** +Wave 1 delivered less feature richness, but it created a safe foundation for later layering. + +## Patterns and Algorithms + +### Pattern: Internal Boundary, External Adapter + +The stable split is: + +1. `lsp-api` exposes internal lifecycle operations, +2. `lsp-v1` speaks protocol and transport, +3. `compiler` owns semantic and analysis behavior, +4. the VS Code extension remains an ordinary external LSP client. + +### Pattern: Build the Seams Before the Features + +The server can begin "dumb" in capability coverage if: + +- the lifecycle boundary is already correct, +- compiler access already flows through explicit bridge services, +- and protocol containment is already enforced. + +That sequence is safer than shipping richer features on top of a blurred module boundary. + +## Pitfalls + +- Do not let `lsp-api` grow into a shadow copy of the LSP protocol. +- Do not import `LSP4J` outside `lsp-v1`, even for convenience. +- Do not move semantic ownership into the protocol adapter just because the adapter is the caller-facing layer. +- Do not boot a global server for the entire Studio process when the actual ownership boundary is the project session. +- Do not treat a connectivity mock as an acceptable long-term module shape. + +## References + +- `DEC-0032` Boundary normativo entre lsp-api, lsp-v1 e a extensao VS Code +- `PLN-0065` LSP Boundary and Module Scaffolding +- `PLN-0066` Project-Scoped LSP Server Lifecycle in Studio +- `PLN-0067` Compiler-Backed Dumb LSP Server Baseline + +## Takeaways + +- Protocol adapters should stay narrow and concrete; internal Studio boundaries should stay protocol-agnostic. +- Project-scoped lifecycle is the correct owner for the LSP server. +- A dumb first wave is acceptable only when the architecture is already correct. diff --git a/discussion/lessons/DSC-0033-frontend-visual-theme-spec-and-css-retirement/LSN-0050-frontend-owned-visual-themes-with-structured-contract-and-host-adapters.md b/discussion/lessons/DSC-0033-frontend-visual-theme-spec-and-css-retirement/LSN-0050-frontend-owned-visual-themes-with-structured-contract-and-host-adapters.md new file mode 100644 index 00000000..67fd8eca --- /dev/null +++ b/discussion/lessons/DSC-0033-frontend-visual-theme-spec-and-css-retirement/LSN-0050-frontend-owned-visual-themes-with-structured-contract-and-host-adapters.md @@ -0,0 +1,93 @@ +--- +id: LSN-0050 +discussion: DSC-0033 +title: Frontend-owned visual themes with structured contract and host adapters +status: done +created: 2026-05-08 +updated: 2026-05-08 +tags: [compiler, compiler-general, frontend, presentation, theming, lsp, vscode, pbs] +--- + +## Original Problem + +Frontend semantic presentation was frontend-owned in principle, but not in a form that could survive multiple hosts. + +PBS still depended on authored semantic CSS as the practical visual source of truth, which created three problems: + +- CSS was a host-facing artifact rather than a compiler-general contract; +- VS Code could only approximate frontend intent through manual translation; +- every host risked drifting away from the frontend's real visual identity. + +## Consolidated Decision + +`DEC-0033` replaced authored host-consumed semantic CSS with a structured frontend-owned visual contract. + +That contract now owns: + +- the frontend theme list; +- the default active theme; +- token-scoped styles; +- editor-surface palette data; +- and host projections used to translate frontend semantic keys into host-native semantic selectors. + +Hosts are adapters over this data. +They do not own frontend visual truth. + +## Final Result + +The canonical visual source now lives in `FrontendSemanticPresentationSpec`, with concrete PBS theme data and host projections authored in structured Java models instead of CSS resources. + +The new LSP path transports: + +- frontend semantic keys; +- visual themes; +- active theme identity; +- and host projection metadata. + +The VS Code extension consumes that payload and writes semantic token and workbench color customizations dynamically, which means routine frontend color changes no longer require hand-edited extension theme tables. + +The old PBS semantic CSS still exists only as migration residue and derivative compatibility material. +It is no longer the authored canonical contract. + +## Implementation Notes + +The decision was realized across four layers: + +1. compiler/frontend metadata grew a structured presentation contract; +2. PBS migrated its theme definition into that contract; +3. the LSP description path started exporting visual themes and host projections; +4. the VS Code extension became a mechanical translator of the contract at runtime. + +This also clarified the relationship between `DSC-0033` and `DSC-0034`: + +- `DSC-0033` established the structured theme contract and CSS retirement; +- `DSC-0034` established that host projections belong in the frontend contract rather than in the host. + +Together they define the current highlight pipeline. + +## Example + +For PBS today: + +- the frontend publishes semantic keys such as `pbs-keyword` and `pbs-service`; +- the same frontend publishes the canonical theme colors and emphasis flags for those keys; +- the LSP sends the theme plus a `vscode` host projection; +- the VS Code extension applies both the canonical semantic selectors and the projected host selectors. + +This is why a color change in PBS theme data can flow to VS Code without a new curated palette in `package.json`. + +## Pitfalls and Anti-Patterns + +- Do not reintroduce CSS as a silent second source of truth. +- Do not move frontend theme authorship into host adapters just because one host has capability gaps. +- Do not collapse frontend semantic identity into generic host categories during transport; host categories are projections, not the canonical vocabulary. + +## References + +- `AGD-0036` Frontend Visual Theme Spec and Retirement of Host-Consumed Semantic CSS +- `DEC-0033` Frontend-owned visual theme spec as canonical presentation contract +- `PLN-0068` Frontend Visual Contract and PBS Migration +- `PLN-0069` LSP Visual Theme Transport and Theme Selection Propagation +- `PLN-0070` Studio Visual Theme Adapter and CSS Retirement +- `PLN-0071` VS Code Dynamic Theme Translation Adapter +- `LSN-0048` Frontend-owned semantic vocabularies with declarative host projection diff --git a/discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md b/discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md new file mode 100644 index 00000000..ee5fcedd --- /dev/null +++ b/discussion/lessons/DSC-0034-frontend-semantic-host-projection-flexibility/LSN-0048-frontend-owned-semantic-vocabularies-with-declarative-host-projection.md @@ -0,0 +1,90 @@ +--- +id: LSN-0048 +ticket: frontend-semantic-host-projection-flexibility +title: Frontend-Owned Semantic Vocabularies with Declarative Host Projection +created: 2026-05-07 +tags: [compiler, compiler-general, frontend, semantics, vscode, host-projection, lsp] +--- + +## Context + +Prometeu had already established that frontend-owned visual themes should remain canonical. +The remaining semantic risk was subtler: VS Code prefers host-shaped token categories, and that pressure could easily turn the host taxonomy into the real source of semantic truth. + +This discussion closed that gap by making semantic ownership explicit: + +- the frontend keeps the canonical semantic vocabulary, +- host adaptation is additional metadata, +- and the adapter translates mechanically instead of inventing semantic policy. + +## Key Decisions + +### Keep Frontend Semantic Keys Canonical + +**What:** +Each frontend remains free to define and evolve its own semantic keys, and those keys stay canonical across the stack. + +**Why:** +Semantic meaning belongs to the language frontend, not to a single host's preferred category system. + +**Trade-offs:** +Adapters must handle more varied vocabularies, but that is the correct cost of preserving frontend ownership. + +### Add Declarative Host Projection Instead of Global Normalization + +**What:** +The frontend now authors host projection metadata describing how canonical semantic keys should map into VS Code token types, modifiers, and fallbacks. + +**Why:** +Hosts still need a practical adaptation path, but that adaptation should be explicit projection data rather than a hidden normalization rule. + +**Trade-offs:** +The contract grows an extra layer, but it stays declarative and prevents semantic meaning from being reauthored inside the adapter. + +### Keep LSP and the Extension Mechanical + +**What:** +LSP transports canonical keys plus projection metadata, and the VS Code extension consumes both mechanically. + +**Why:** +If either layer starts hand-mapping semantic meaning, the host stack becomes the de facto author of language semantics. + +**Trade-offs:** +The transport and adapter need clearer model shapes, but their responsibilities stay cleaner and more reusable. + +## Patterns and Algorithms + +### Pattern: Canonical Vocabulary Plus Per-Host Projection + +The stable flow is: + +1. frontend defines semantic keys, +2. frontend defines host projections for those keys, +3. LSP transports canonical keys and projection data together, +4. the host adapter applies the projection without reinterpretation. + +### Pattern: Mechanical Translation with Explicit Fallbacks + +Projection metadata should say not only the preferred host token type, but also the fallback when the host cannot represent the frontend key exactly. +That keeps fallback behavior visible and reviewable instead of implicit in adapter code. + +## Pitfalls + +- Do not collapse frontend semantic keys into a repository-wide host taxonomy. +- Do not hide semantic policy inside the VS Code extension just because the platform has its own token vocabulary. +- Do not transport only host-shaped token types and discard canonical frontend identity. +- Do not confuse projection metadata with the canonical semantic contract itself. + +## References + +- `DEC-0034` Frontend semantic vocabularies remain canonical and hosts consume declarative projections +- `PLN-0072` Frontend Semantic Host Projection Contract +- `PLN-0073` LSP Semantic Host Projection Transport +- `PLN-0074` VS Code Semantic Host Projection Adapter +- `DEC-0033` Frontend-owned visual theme spec as canonical presentation contract + +## Takeaways + +- Semantic ownership stays with the frontend even when the host prefers a different taxonomy. +- Host projection is the right adaptation mechanism; global normalization is not. +- LSP and host adapters stay healthier when they translate declaratively instead of authoring semantic meaning. diff --git a/discussion/workflow/agendas/AGD-0038-pbs-lsp-editor-assistance-wave-1.md b/discussion/workflow/agendas/AGD-0038-pbs-lsp-editor-assistance-wave-1.md new file mode 100644 index 00000000..f31beaf2 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0038-pbs-lsp-editor-assistance-wave-1.md @@ -0,0 +1,207 @@ +--- +id: AGD-0038 +ticket: pbs-lsp-editor-assistance-wave-1 +title: Wave 1 de assistencia editorial via LSP para PBS no VS Code +status: accepted +created: 2026-05-08 +resolved: 2026-05-08 +decision: DEC-0035 +tags: [studio, lsp, vscode, compiler, compiler-pbs, editor, completion, hover, signature-help] +--- + +## Pain + +O stack atual de PBS no VS Code ja entrega duas capacidades relevantes: + +- diagnostics basicos; +- semantic highlighting com tema frontend-owned. + +Mas ele ainda nao entrega o minimo de assistencia editorial para um fluxo real de escrita de codigo: + +- nao existe `completion`; +- `hover` ainda eh placeholder; +- nao existe `signature help`. + +Sem isso, o editor continua servindo mais como viewer colorido com erros do que como ambiente minimamente utilizavel para escrever PBS com conforto. + +## Context + +Domain owner: `studio` + +Subdomains touched: + +- `studio/lsp` +- `compiler/pbs` +- `compiler/general` + +Estado atual observado no codigo: + +- o servidor anuncia `hover` e `semanticTokens`, mas nao anuncia `completionProvider`, `signatureHelpProvider` ou `documentFormattingProvider`; +- o bridge interno do LSP so expõe `describeServer`, `analyzeDocument`, `hover`, `semanticTokens` e `onSave`; +- `hover` hoje retorna texto fixo, sem usar simbolo, assinatura, tipo ou docs; +- o pipeline do compiler ja consegue analisar overlay do documento e possui superficies semanticas e de linking relevantes no PBS; +- o highlighting recente ja resolve bastante informacao lexical e parte da superficie stdlib/imports. + +Esta agenda trata apenas da wave 1 de assistencia editorial. +Ela nao tenta fechar todo o problema de IDE features. + +## Open Questions + +- Qual deve ser o escopo minimo de `completion` para chamarmos essa wave de utilizavel? +- [x] Qual deve ser o escopo minimo de `completion` para chamarmos essa wave de utilizavel? + R: keywords, nomes locais em escopo, simbolos importados/publicos, e members apos `.` para superfícies reconhecidas (`service`, `host`, `builtin type`, `struct` e constructors/funcoes relacionados). Sem isso, a feature ainda nao ajuda na descoberta real da API. +- `completion` deve nascer apenas com heuristicas lexicais/sintaticas, ou ja deve usar semantica e linking do compiler onde isso for viavel? +- [x] `completion` deve nascer apenas com heuristicas lexicais/sintaticas, ou ja deve usar semantica e linking do compiler onde isso for viavel? + R: deve nascer compiler-backed sempre que a informacao ja existir de forma confiavel no pipeline semantico/linking. Heuristica local pode existir apenas como complemento estritamente limitado para casos ainda nao resolvidos, nunca como segunda semantica canônica. +- Em `hover`, quais informacoes sao obrigatorias na primeira entrega: + assinatura, kind, modulo de origem, tipo inferido, documentacao? +- [x] Em `hover`, quais informacoes sao obrigatorias na primeira entrega: + assinatura, kind, modulo de origem, tipo inferido, documentacao? + R: obrigatorios: `kind`, assinatura quando aplicavel, modulo/origem quando relevante, e tipo/shape basico quando houver. Documentacao textual eh desejavel para stdlib e builtins, mas nao deve bloquear a wave 1 se a infraestrutura de docstring ainda nao existir. Se a linguagem vier a adotar algo como `Document(...)` com markdown pronto para hover, isso deve nascer em discussao propria; nesta wave atual, basta preparar o local de consumo para documentacao opcional quando ela existir. +- `signature help` deve usar a mesma fonte semantica de `hover/completion`, ou pode nascer numa implementacao mais local para chamadas conhecidas? +- [x] `signature help` deve usar a mesma fonte semantica de `hover/completion`, ou pode nascer numa implementacao mais local para chamadas conhecidas? + R: deve usar a mesma base semantica. O objetivo da wave eh justamente evitar tres caminhos tecnicos diferentes para responder "qual assinatura esta sendo chamada?". +- Como tratar membros de stdlib, aliases importados, services, hosts, builtin types e metodos de struct sem criar uma segunda semantica paralela no LSP? +- [x] Como tratar membros de stdlib, aliases importados, services, hosts, builtin types e metodos de struct sem criar uma segunda semantica paralela no LSP? + R: o LSP deve perguntar ao compiler por uma superficie editorial/resolutiva unica, derivada das mesmas fontes usadas para analise, linking, stdlib e metadata do frontend. O LSP nao deve reconstruir manualmente tabelas paralelas de membros. +- Esta wave deve incluir `go to definition`, ou isso deve ficar explicitamente fora do escopo para nao diluir a entrega? +- [x] Esta wave deve incluir `go to definition`, ou isso deve ficar explicitamente fora do escopo para nao diluir a entrega? + R: deve ficar fora do escopo desta wave. Ele eh um proximo passo natural, mas nao eh requisito para chamar `completion + hover + signature help` de minimamente utilizaveis. +- Formatacao e identacao automatica devem ficar fora desta agenda como non-goal explicito? +- [x] Formatacao e identacao automatica devem ficar fora desta agenda como non-goal explicito? + R: sim. Podem virar discussao/plan posterior. Nesta wave, elas seriam custo adicional sem impacto proporcional comparado a completion/hover/signature help. + +## Options + +### Option A - Completion e hover por heuristica leve, com signature help posterior + +- **Approach:** implementar `completion` usando contexto local, imports, keywords e alguns casos de member access, implementar `hover` com informacao minima, e deixar `signature help` para uma wave seguinte. +- **Pro:** menor custo inicial e menor risco de travar na arquitetura semantica. +- **Con:** pode criar uma primeira entrega inconsistente, especialmente em chamadas de stdlib e tipos importados, e pode forcar refactor cedo demais depois. +- **Maintainability:** media. + +### Option B - Wave 1 coerente com completion, hover e signature help sobre uma mesma base semantica + +- **Approach:** desenhar uma pequena superficie de assistencia editorial no LSP que use uma base comum para resolver simbolos, membros e assinaturas. Entregar juntos: + - completion minimo utilizavel; + - hover real; + - signature help em chamadas. +- **Pro:** evita duplicacao conceitual e produz uma experiencia de editor mais consistente desde a primeira wave. +- **Con:** custo maior de desenho e implementacao agora. +- **Maintainability:** forte. + +### Option C - Adiar assistencia semantica e depender do VS Code + snippets + +- **Approach:** manter diagnostics e colors como estao, adicionar no maximo snippets ou pequenas ajudas na extensao. +- **Pro:** quase nenhum custo de backend. +- **Con:** nao resolve o problema real; o editor continua insuficiente para escrita cotidiana de PBS. +- **Maintainability:** fraca. + +## Tradeoffs + +Option C nao fecha a dor real e deve ser descartada. + +Option A parece atraente para ganhar velocidade, mas carrega um risco ruim: + +- introduzir `completion`, `hover` e depois `signature help` por caminhos tecnicos diferentes; +- duplicar logica entre parser local, heuristicas e linking; +- e transformar a wave 2 num retrabalho de consolidacao. + +Option B custa mais, mas o custo eh justificavel porque estas tres features se apoiam nas mesmas perguntas: + +- o que este simbolo representa? +- quais membros ele expõe? +- qual eh a assinatura desta chamada? +- qual o modulo/origem deste item? + +Se essas perguntas forem respondidas por uma base comum, a wave 1 fica mais coerente e abre caminho natural para futuras features como `definition`, `references` e `inlay hints` via LSP. + +## Recommendation + +Seguir com **Option B**. + +A recomendacao eh abrir uma wave 1 coerente de assistencia editorial com: + +- `completion` minimo utilizavel; +- `hover` funcional; +- `signature help` funcional. + +Escopo minimo recomendado para `completion`: + +- keywords; +- simbolos locais em escopo; +- nomes importados/publicos relevantes; +- members apos `.` para stdlib, builtin types, structs e services/hosts reconhecidos. + +Escopo minimo recomendado para `hover`: + +- kind do simbolo; +- assinatura quando aplicavel; +- modulo/origem quando relevante; +- tipo ou shape basico quando isso existir de forma confiavel. + +Escopo minimo recomendado para `signature help`: + +- chamadas de funcao/metodo/construtor; +- parametro ativo; +- labels consistentes com a mesma representacao de assinatura usada no hover. + +## Discussion + +O ponto principal desta agenda nao eh “adicionar IDE features” genericamente. + +O ponto principal eh definir a primeira wave de superfícies de assistencia que tiram o PBS no VS Code do estado de: + +- diagnostics + coloracao + +para um estado de: + +- escrita assistida minima, com descoberta de API e leitura de assinatura. + +Isso exige disciplina de boundary. + +O VS Code extension nao deve inventar semantica local. +O servidor LSP deve continuar sendo o owner da assistencia editorial. + +Da mesma forma, o LSP nao deve criar uma semantica paralela desconectada do compiler se ja existe informacao aproveitavel em parser, linking, stdlib e superficies semanticas do PBS. + +Tambem vale explicitar o que fica fora desta agenda: + +- formatter completo; +- identacao automatica robusta de documento; +- quick fixes; +- definition/references; +- rename; +- code actions mais amplas. + +Esses itens podem virar agenda posterior. + +Tambem fica registrado um criterio importante de arquitetura: + +- o primeiro passo nao eh encher o protocolo de features; +- o primeiro passo eh criar uma base editorial coerente no lado `compiler/pbs` + `studio/lsp` que consiga responder simbolo, members e assinatura; +- `completion`, `hover` e `signature help` sao projeções diferentes sobre a mesma base. + +E um criterio adicional de preparacao editorial: + +- a wave 1 deve prever um campo/superficie opcional de documentacao no modelo editorial resolvido por simbolo; +- mas a autoria dessa documentacao na linguagem PBS, inclusive eventual anotacao `Document(...)` com markdown, fica fora desta agenda e pode seguir para discussao separada. + +## Resolution + +Resolucao preliminar: + +- a wave 1 deve ser considerada valida quando o VS Code conseguir oferecer descoberta minima de API, leitura de assinatura e ajuda em chamadas sem depender de conhecimento local na extensao; +- `completion`, `hover` e `signature help` devem nascer sobre uma mesma base compiler-backed; +- `go to definition`, formatter e outras features de IDE ficam explicitamente fora do escopo. + +Se esta leitura fizer sentido, o proximo passo eh aceitar a agenda e fechar uma `decision` que: + +1. defina o escopo normativo exato de `completion`, `hover` e `signature help`; +2. fixe o boundary entre `compiler/pbs`, `studio/lsp` e `tools/vscode-extension`; +3. declare explicitamente os non-goals desta wave; +4. determine se a base deve nascer compiler-backed desde o inicio ou com alguma heuristica local estritamente limitada. + +## Next Step + +Se voce concordar com o enquadramento, eu converto esta agenda em `accepted` e escrevo a `decision` normativa da wave 1 de assistencia editorial para PBS no VS Code. diff --git a/discussion/workflow/agendas/AGD-0039-pbs-symbol-documentation-and-hover-markdown.md b/discussion/workflow/agendas/AGD-0039-pbs-symbol-documentation-and-hover-markdown.md new file mode 100644 index 00000000..c3c0504b --- /dev/null +++ b/discussion/workflow/agendas/AGD-0039-pbs-symbol-documentation-and-hover-markdown.md @@ -0,0 +1,137 @@ +--- +id: AGD-0039 +ticket: pbs-symbol-documentation-and-hover-markdown +title: Modelo de documentacao de simbolos em PBS e consumo markdown no hover +status: open +created: 2026-05-08 +tags: [compiler, compiler-pbs, studio, lsp, vscode, editor, hover, documentation, markdown] +--- + +## Pain + +A wave atual de assistencia editorial quer `hover` util, mas ainda nao existe um modelo claro para documentacao authored dentro de PBS. + +Sem esse modelo, o hover pode até mostrar: + +- kind; +- assinatura; +- origem; +- tipo/shape. + +Mas continua sem uma superficie limpa para explicar intencao de uso, contrato semantico, detalhes de parametros ou observacoes de API. + +Hoje a ideia proposta eh algo como: + +- `Document(qualquer markdown aqui dentro...)` + +como anotacao consumida diretamente pelo hover. + +Isso parece simples no uso, mas toca em decisoes de linguagem e contrato editorial que merecem discussao propria. + +## Context + +Domain owner: `compiler/pbs` + +Subdomains touched: + +- `compiler/general` +- `studio/lsp` +- `tools/vscode-extension` + +Esta agenda eh deliberadamente separada de `AGD-0038`. + +Razao: + +- `AGD-0038` trata da wave 1 de `completion`, `hover` e `signature help`; +- esta nova agenda trata da autoria, armazenamento, propagacao e consumo de documentacao de simbolos. + +O objetivo eh evitar que a primeira wave de assistencia editorial fique bloqueada por uma decisao de linguagem ainda nao fechada. + +## Open Questions + +- A autoria da documentacao deve acontecer por anotacao explicita como `Document(...)`, por comentario estruturado, ou por outro mecanismo? +- O payload authored deve aceitar markdown arbitrario como fonte canonica, ou deve haver alguma normalizacao/restricao? +- Quais simbolos podem carregar documentacao na wave inicial: + funcoes, methods, services, hosts, builtin types, structs, enums, fields, constructors? +- A documentacao deve viver no AST/semantics como superficie compiler-backed explicita? +- Como a documentacao authored em PBS se relaciona com documentacao de stdlib gerada ou importada de outras fontes? +- O hover deve consumir markdown pronto sem transformacao, ou deve haver um pipeline intermediario de sanitizacao/normalizacao? +- Como tratar multilinhas, escaping e ergonomia de escrita se a surface for `Document(...)`? +- A agenda deve definir apenas o modelo de autoria, ou tambem o transporte LSP e a composicao final do hover? + +## Options + +### Option A - Anotacao `Document(...)` com markdown como payload canonico + +- **Approach:** introduzir uma anotacao explicita na linguagem PBS cujo conteudo ja eh markdown pronto para o hover. +- **Pro:** autoria direta, intencao clara, pipeline simples de consumo no LSP e no hover. +- **Con:** precisa desenhar sintaxe, escaping, multiline e regras de onde a anotacao eh permitida. +- **Maintainability:** forte se a surface de linguagem ficar limpa. + +### Option B - Comentarios estruturados como fonte de documentacao + +- **Approach:** usar comentarios especiais ou doc-comments, com parser especifico para gerar documentacao de simbolo. +- **Pro:** mais familiar para quem vem de outras linguagens e menos intrusivo no corpo semantico da linguagem. +- **Con:** exige novo contrato de binding entre comentarios e simbolos, e pode aumentar ambiguidade editorial. +- **Maintainability:** media. + +### Option C - Nao definir autoria em PBS agora e suportar apenas docs de stdlib por metadata externa + +- **Approach:** deixar a linguagem sem surface authored por enquanto e alimentar hover apenas com docs externas/geradas. +- **Pro:** menor custo imediato de linguagem. +- **Con:** resolve mal a autoria de codigo de usuario e deixa o modelo incompleto. +- **Maintainability:** fraca. + +## Tradeoffs + +Option C nao parece suficiente como direcao de produto se a intencao eh ter hover realmente util para APIs authored em PBS. + +Option B eh plausivel, mas move o problema para um binding entre comentario e simbolo que tambem precisa ser projetado com cuidado. Isso costuma parecer mais barato do que realmente eh. + +Option A tem um custo claro de sintaxe e modelo, mas pelo menos deixa explicito que documentacao eh metadado semantico e nao apenas texto solto ao redor do codigo. + +Se a equipe realmente quer markdown authored pronto para consumo no hover, Option A parece a discussao mais direta e honesta. + +## Recommendation + +Discutir esta agenda com viés inicial para **Option A**, mas sem fechar ainda a sintaxe final. + +A recomendacao preliminar eh separar o problema em duas perguntas: + +1. qual eh a surface canonical de autoria da documentacao em PBS? +2. como essa documentacao vira um payload editorial resolvido para hover e futuros consumers? + +Mesmo que `Document(...)` seja a direcao vencedora, a decisao deve deixar claro: + +- onde ela pode ser usada; +- qual formato textual aceita; +- como ela eh preservada no compiler; +- como chega ao LSP; +- e como o hover deve compor isso com assinatura e metadados do simbolo. + +## Discussion + +Esta agenda nao deve ser reduzida a "mostrar markdown no hover". + +O problema real eh definir ownership e boundary da documentacao: + +- o author escreve onde? +- o compiler preserva como? +- o LSP transporta em qual shape? +- o hover monta o resultado com qual ordem e responsabilidade? + +Tambem vale observar que a escolha aqui pode influenciar mais do que hover: + +- completions futuras podem querer `detail` e `documentation`; +- signature help pode querer descricoes de parametros; +- docs de stdlib podem querer surface unificada com docs authored. + +Por isso, mesmo sendo uma agenda separada, ela conversa diretamente com a futura qualidade do editor. + +## Resolution + +Ainda em aberto. + +## Next Step + +Se o enquadramento fizer sentido, o proximo passo eh discutir as open questions e fechar uma `decision` propria para o modelo de documentacao de simbolos em PBS. diff --git a/discussion/workflow/decisions/DEC-0035-pbs-lsp-editor-assistance-wave-1.md b/discussion/workflow/decisions/DEC-0035-pbs-lsp-editor-assistance-wave-1.md new file mode 100644 index 00000000..9991f391 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0035-pbs-lsp-editor-assistance-wave-1.md @@ -0,0 +1,192 @@ +--- +id: DEC-0035 +ticket: pbs-lsp-editor-assistance-wave-1 +title: Wave 1 de assistencia editorial via LSP para PBS no VS Code +status: accepted +created: 2026-05-08 +accepted: 2026-05-08 +agenda: AGD-0038 +plans: [PLN-0075, PLN-0076, PLN-0077] +tags: [studio, lsp, vscode, compiler, compiler-pbs, editor, completion, hover, signature-help] +--- + +## Context + +O stack atual de PBS no VS Code ja entrega diagnostics basicos e semantic highlighting frontend-owned, mas ainda nao oferece assistencia editorial minima para escrita cotidiana de codigo. + +Hoje: + +1. nao existe `completion`; +2. `hover` existe apenas como placeholder; +3. nao existe `signature help`; +4. o servidor ainda nao anuncia capabilities correspondentes para essas superficies; +5. o compiler e o frontend PBS ja possuem informacao semantica, de linking, stdlib e metadata suficiente para nao precisarmos inventar uma segunda semantica local na extensao. + +Esta decisao trata apenas da primeira wave de assistencia editorial para PBS no VS Code. +Ela nao cobre formatter, identacao automatica robusta, quick fixes, `go to definition`, `references`, `rename` nem code actions gerais. + +## Decision + +O repositório SHALL entregar uma wave 1 de assistencia editorial via LSP para PBS no VS Code com tres superfícies funcionais: + +1. `completion`; +2. `hover`; +3. `signature help`. + +Esta wave SHALL ser considerada valida quando essas tres superfícies permitirem: + +1. descoberta minima de API; +2. leitura de assinatura em simbolos e chamadas; +3. ajuda de chamada sem dependencia de conhecimento semantico local na extensao VS Code. + +Esta decisao ALSO locks que: + +1. `completion`, `hover` e `signature help` MUST nascer sobre uma mesma base editorial compiler-backed; +2. o LSP MUST ser o owner operacional da assistencia editorial; +3. a extensao VS Code MUST atuar apenas como cliente/adaptador do protocolo, e MUST NOT reconstruir semantica propria para simbolos, members ou assinaturas; +4. heuristicas locais no LSP MAY existir apenas como complemento temporario e estritamente limitado para lacunas nao resolvidas, mas MUST NOT virar uma segunda semantica canônica; +5. `go to definition`, formatter, identacao automatica robusta, quick fixes, `references`, `rename` e code actions amplas MUST ficar fora desta wave. + +## Rationale + +As tres features desta wave respondem essencialmente ao mesmo conjunto de perguntas: + +1. que simbolo eh este? +2. quais members ele expõe? +3. qual assinatura ele tem? +4. de onde ele vem? + +Implementa-las por caminhos tecnicos diferentes introduziria: + +1. duplicacao entre parser local, heuristicas e linking; +2. incoerencia entre o que aparece em `completion`, `hover` e `signature help`; +3. retrabalho na wave seguinte. + +Portanto, a primeira entrega precisa nascer sobre uma base editorial unificada, reaproveitando o que o compiler e o frontend PBS ja sabem sobre: + +1. escopo local; +2. imports e aliases; +3. stdlib; +4. builtin types; +5. services, hosts, structs, enums, constructors, functions e methods. + +## Technical Specification + +### 1. Shared Editorial Resolution Surface + +`compiler/pbs` e `studio/lsp` MUST introduzir uma superficie editorial resolvida comum para uso de `completion`, `hover` e `signature help`. + +Essa superficie MUST ser capaz de responder, de forma compiler-backed: + +1. identidade do simbolo; +2. `kind` do simbolo; +3. modulo/origem quando relevante; +4. assinatura ou shape quando aplicavel; +5. members disponíveis para receivers reconhecidos; +6. documentacao opcional quando existir, sem bloquear esta wave. + +Essa superficie MAY ser exposta por novos modelos internos do compiler, novos adapters no bridge do LSP, ou ambos. +Mas ela MUST permanecer derivada do compiler e MUST NOT ser reautoriada na extensao. + +### 2. Completion Scope + +`completion` MUST ser entregue como superfície minima utilizavel. + +Na wave 1, isso MUST cobrir pelo menos: + +1. keywords da linguagem; +2. nomes locais em escopo; +3. simbolos importados/publicos relevantes; +4. members apos `.` para superfícies reconhecidas, incluindo `service`, `host`, `builtin type`, `struct`, e call surfaces relacionadas. + +`completion` SHOULD expor labels, kinds e detalhes coerentes com a mesma base usada por `hover`. + +`completion` MUST NOT depender de tabela hardcoded na extensao para stdlib ou members de PBS. + +### 3. Hover Scope + +`hover` MUST deixar de ser placeholder e passar a ser simbolo-resolved. + +Na wave 1, o hover MUST incluir quando disponivel: + +1. `kind` do simbolo; +2. assinatura quando aplicavel; +3. modulo/origem quando relevante; +4. tipo ou shape basico quando isso existir de forma confiavel. + +`hover` MAY incluir documentacao textual opcional. +Mas a autoria de documentacao em PBS, incluindo eventual anotacao como `Document(...)`, MUST seguir discussao separada e MUST NOT bloquear esta wave. + +### 4. Signature Help Scope + +`signature help` MUST ser implementado para chamadas de: + +1. funcao; +2. metodo; +3. construtor. + +Na wave 1, ele MUST informar: + +1. a assinatura ativa; +2. o parametro ativo; +3. labels coerentes com a mesma representacao de assinatura usada no hover. + +`signature help` MUST usar a mesma base editorial de resolucao usada por `completion` e `hover`. + +### 5. Boundary Responsibilities + +`compiler/pbs`: + +1. MUST continuar sendo a fonte de verdade de semantica, linking e metadata relevante para simbolos e call surfaces; +2. MUST fornecer a base necessaria para resolucao editorial de simbolos, members e assinaturas. + +`studio/lsp`: + +1. MUST projetar essa base editorial em endpoints/protocol features adequados; +2. MUST anunciar as capabilities LSP necessarias para `completion`, `hover` e `signature help`; +3. MUST manter o transporte fiel ao modelo compiler-backed. + +`tools/vscode-extension`: + +1. MUST consumir o protocolo como cliente; +2. MUST NOT carregar tabelas paralelas de membros, stdlib ou assinaturas como fonte principal; +3. MAY conter apenas a logica estrutural normal de integração exigida pelo VS Code. + +### 6. Documentation Preparation Rule + +Esta wave MUST preparar um campo ou superfície opcional para documentacao editorial resolvida por simbolo. + +Mas esta decisao ALSO locks que: + +1. a surface de autoria dessa documentacao em PBS nao faz parte desta wave; +2. a sintaxe ou contrato de linguagem para documentacao authored deve ser discutido separadamente; +3. a ausencia dessa autoria nao bloqueia `completion`, `hover` nem `signature help` da wave 1. + +### 7. Explicit Non-Goals + +Esta decisao MUST NOT: + +1. incluir formatter completo; +2. incluir identacao automatica robusta de documento; +3. incluir `go to definition`; +4. incluir `references`; +5. incluir `rename`; +6. incluir quick fixes; +7. incluir code actions mais amplas; +8. definir o modelo de autoria de documentacao em PBS. + +## Constraints + +1. A wave 1 MUST priorizar coerencia entre `completion`, `hover` e `signature help` sobre cobertura enciclopedica de todos os casos. +2. A extensao VS Code MUST permanecer thin; a inteligencia editorial MUST ficar no compiler/LSP. +3. A implementacao MUST favorecer evolucao futura para `definition`, `references`, `documentation`, e outras editor features sem exigir reescrita conceitual da base editorial. +4. A implementacao MUST evitar surfaces duplicadas para stdlib, aliases, members e signatures. +5. Qualquer plan derivado desta decisao MUST separar claramente: + - base editorial compiler-backed; + - projeção LSP/protocolo; + - integração VS Code. + +## Revision Log + +- 2026-05-08: Initial draft from AGD-0038. +- 2026-05-08: Accepted and decomposed into PLN-0075, PLN-0076, and PLN-0077. diff --git a/discussion/workflow/plans/PLN-0075-pbs-editorial-resolution-surface-for-symbols-members-and-signatures.md b/discussion/workflow/plans/PLN-0075-pbs-editorial-resolution-surface-for-symbols-members-and-signatures.md new file mode 100644 index 00000000..de592972 --- /dev/null +++ b/discussion/workflow/plans/PLN-0075-pbs-editorial-resolution-surface-for-symbols-members-and-signatures.md @@ -0,0 +1,135 @@ +--- +id: PLN-0075 +ticket: pbs-lsp-editor-assistance-wave-1 +title: PBS Editorial Resolution Surface for Symbols, Members, and Signatures +status: open +created: 2026-05-08 +completed: +tags: [compiler, compiler-pbs, editor, completion, hover, signature-help, semantics] +--- + +## Objective + +Introduce a compiler-backed editorial resolution surface in `compiler/pbs` that can answer symbol identity, kind, origin, signature/shape, callable details, and receiver members for use by LSP `completion`, `hover`, and `signature help`. + +## Background + +`DEC-0035` requires all three editor-assistance features to be built over one shared compiler-backed base. The current repository already has useful pieces: + +- parser and AST for PBS; +- semantic validation and flow analysis; +- stdlib and import visibility rules; +- semantic token classification; +- builder-pipeline overlay analysis. + +What is still missing is an explicit editorial surface designed for interactive symbol resolution instead of only diagnostics or coloring. + +## Scope + +### Included + +- Define internal compiler-side models for editorial symbol resolution. +- Resolve local names, imported/public symbols, and recognized member surfaces. +- Resolve callable signatures for functions, methods, and constructors. +- Provide optional documentation slot(s) in the model without defining authored PBS documentation syntax. +- Add PBS-focused tests for symbol/member/signature resolution. + +### Excluded + +- LSP protocol capability changes. +- VS Code client behavior. +- Authored documentation syntax such as `Document(...)`. +- `go to definition`, `references`, `rename`, formatter, or code actions. + +## Non-Goals + +- Rebuilding the full compiler around IDE concerns. +- Solving every ambiguous or partial parse state in wave 1. +- Designing a generic multi-language IDE framework before PBS proves the model. + +## Execution Steps + +### Step 1 - Define editorial resolution models in compiler-owned surfaces + +**What:** Introduce explicit models for resolved symbols, member candidates, callable signatures, and optional documentation payload. +**How:** Add compiler-owned DTO/model types that can express: + +1. symbol identity and kind; +2. declared/imported origin; +3. callable label/signature shape; +4. receiver member entries; +5. optional documentation text field. + +The model must be expressive enough for `completion`, `hover`, and `signature help` without forcing each consumer to reinterpret raw AST nodes. +**File(s):** `prometeu-compiler/prometeu-compiler-core/src/main/java/**` and/or `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/**` depending on final ownership split. + +### Step 2 - Build PBS symbol resolution for local and imported surfaces + +**What:** Resolve names in editorial contexts. +**How:** Reuse parser, name/linking, stdlib import knowledge, and any existing semantic tables to resolve: + +1. local bindings in scope; +2. declared top-level PBS symbols; +3. imported/public symbols and aliases; +4. stdlib-owned surfaces currently recognized by the frontend. + +The implementation must avoid duplicating semantic truth already present in compiler/linking logic. +**File(s):** `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/**`. + +### Step 3 - Build member and callable resolution + +**What:** Resolve members and callable signatures for recognized receiver and call surfaces. +**How:** Support at minimum: + +1. member lookup after `.` for services, hosts, builtin types, and structs; +2. direct callable lookup for functions and constructors; +3. active-call signature resolution for later `signature help` use. + +This step must produce stable labels/details that can be reused unchanged by LSP features. +**File(s):** `prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/**`. + +### Step 4 - Add editorial conformance tests + +**What:** Lock the compiler-backed editorial contract before LSP integration. +**How:** Add tests for: + +1. local variable and parameter resolution; +2. imported service/host/builtin-type resolution; +3. member lookup after `.` on recognized surfaces; +4. callable signature extraction for function/method/constructor cases; +5. optional documentation field stability when absent. + +**File(s):** PBS/compiler test suites under `prometeu-compiler/**/src/test/java/**`. + +## Test Requirements + +### Unit Tests + +- Model invariants for symbol/member/signature payloads. +- Resolver tests covering locals, imports, aliases, members, and constructors. + +### Integration Tests + +- Compiler-backed overlay/editorial tests using representative PBS snippets and stdlib imports. + +### Manual Verification + +- Inspect resolved payloads in tests or debug output and confirm they are sufficient to drive all three editor features without ad hoc reinterpretation. + +## Acceptance Criteria + +- [ ] Compiler-side editorial models exist for symbols, members, signatures, and optional documentation. +- [ ] PBS can resolve local names, imported/public symbols, and recognized member surfaces. +- [ ] Function, method, and constructor signatures are available through the shared editorial surface. +- [ ] No consumer is required to reconstruct signature/member meaning from raw AST-only data. + +## Dependencies + +- `DEC-0035` accepted and normatively locked. +- Existing PBS semantic and import/linking infrastructure remains the truth source. + +## Risks + +- Overfitting the first resolver to the current stdlib shape may hurt future fronts or future PBS growth. +- Mixing diagnostics-only semantics with editorial semantics carelessly may produce partial or contradictory answers. +- Incomplete member resolution can make `completion` appear flaky even if `hover` works. diff --git a/discussion/workflow/plans/PLN-0076-lsp-completion-hover-and-signature-help-over-editorial-resolution.md b/discussion/workflow/plans/PLN-0076-lsp-completion-hover-and-signature-help-over-editorial-resolution.md new file mode 100644 index 00000000..064f7d7d --- /dev/null +++ b/discussion/workflow/plans/PLN-0076-lsp-completion-hover-and-signature-help-over-editorial-resolution.md @@ -0,0 +1,121 @@ +--- +id: PLN-0076 +ticket: pbs-lsp-editor-assistance-wave-1 +title: LSP Completion, Hover, and Signature Help over Editorial Resolution +status: open +created: 2026-05-08 +completed: +tags: [studio, lsp, protocol, completion, hover, signature-help, editor] +--- + +## Objective + +Expose `completion`, real `hover`, and `signature help` through the project-scoped LSP stack by projecting the compiler-backed editorial resolution surface into explicit protocol capabilities and request handlers. + +## Background + +`DEC-0035` requires `studio/lsp` to be the operational owner of these editor features while remaining faithful to compiler-backed symbol, member, and signature resolution. The current LSP path only handles diagnostics, semantic tokens, and a placeholder hover. + +## Scope + +### Included + +- Extend the internal bridge/API with completion and signature-help operations. +- Replace placeholder hover with symbol-resolved hover. +- Announce LSP capabilities for completion and signature help. +- Add message models/mappers/tests for the new request/response paths. + +### Excluded + +- Creation of the underlying compiler editorial model itself. +- Authored documentation syntax. +- VS Code-specific UI behavior beyond normal LSP consumption. + +## Non-Goals + +- `go to definition`, `references`, `rename`, formatter, code actions, or quick fixes. +- Rich workspace-wide indexing beyond what the shared editorial surface already provides. + +## Execution Steps + +### Step 1 - Extend the LSP bridge contract + +**What:** Add compiler-backed editor-assistance operations to the bridge. +**How:** Evolve `LanguageServiceBridge` and related internal messages to support at minimum: + +1. completion requests; +2. symbol-resolved hover payloads; +3. signature-help requests. + +The bridge contract must align structurally with the shared editorial resolution surface rather than inventing a second feature-specific abstraction. +**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/**`, `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/**`. + +### Step 2 - Implement compiler-backed request handling + +**What:** Wire LSP requests to compiler-backed editorial resolution. +**How:** Update `CompilerLanguageServiceBridge` so it: + +1. resolves completion candidates from local/import/member contexts; +2. resolves hover payloads from symbol identity, origin, and signatures; +3. resolves active call signatures for `signature help`. + +This step must remove the current fixed hover placeholder. +**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/**`. + +### Step 3 - Announce and map protocol capabilities + +**What:** Make the server advertise the new feature set and map payloads into lsp4j types. +**How:** Update protocol mappers and text-document service so the server announces: + +1. `completionProvider`; +2. `signatureHelpProvider`; +3. improved `hoverProvider` backed by real payloads. + +Map completion items, hover markdown, and signature-help structures into concrete LSP responses. +**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/**`, `.../mapping/**`. + +### Step 4 - Add transport and conformance tests + +**What:** Prevent regression to placeholder/editor-local behavior. +**How:** Add tests that verify: + +1. capabilities are announced correctly; +2. completion returns compiler-backed candidates; +3. hover no longer returns the baseline placeholder; +4. signature-help returns deterministic active-signature information. + +**File(s):** `prometeu-lsp/prometeu-lsp-v1/src/test/java/**`. + +## Test Requirements + +### Unit Tests + +- DTO/model invariants for completion, hover, and signature-help payloads. +- Mapper tests for lsp4j conversion. + +### Integration Tests + +- LSP request/response tests using realistic PBS snippets with locals, imports, and member access. + +### Manual Verification + +- Connect the VS Code extension and confirm server capabilities include completion and signature help. +- Confirm hover content changes with the hovered symbol rather than returning static baseline text. + +## Acceptance Criteria + +- [ ] The LSP bridge exposes completion and signature-help operations in addition to diagnostics, hover, and semantic tokens. +- [ ] Hover is symbol-resolved and no longer hardcoded placeholder text. +- [ ] The server advertises completion and signature-help capabilities. +- [ ] Completion, hover, and signature help all derive from the same compiler-backed editorial surface. + +## Dependencies + +- `DEC-0035` accepted and normatively locked. +- `PLN-0075` for the shared compiler-backed editorial resolution surface. + +## Risks + +- Protocol mapping can drift from compiler payload shape if feature-specific shortcuts are added. +- Active parameter/signature calculation may be brittle around incomplete syntax unless request handling is conservative. +- Completion can feel noisy if candidate ranking or filtering is too weak in wave 1. diff --git a/discussion/workflow/plans/PLN-0077-vscode-consumption-and-wave-1-editor-assistance-validation.md b/discussion/workflow/plans/PLN-0077-vscode-consumption-and-wave-1-editor-assistance-validation.md new file mode 100644 index 00000000..5dfd483c --- /dev/null +++ b/discussion/workflow/plans/PLN-0077-vscode-consumption-and-wave-1-editor-assistance-validation.md @@ -0,0 +1,103 @@ +--- +id: PLN-0077 +ticket: pbs-lsp-editor-assistance-wave-1 +title: VS Code Consumption and Wave 1 Editor Assistance Validation +status: open +created: 2026-05-08 +completed: +tags: [vscode, extension, editor, completion, hover, signature-help, validation] +--- + +## Objective + +Keep the VS Code extension thin while validating that the new LSP-delivered `completion`, `hover`, and `signature help` features actually produce a minimally usable PBS editing experience end to end. + +## Background + +`DEC-0035` explicitly requires the extension to remain a client/adaptor rather than a second semantic engine. Because VS Code already knows how to consume LSP completion, hover, and signature-help responses, the extension work should be small and mostly about ensuring transport, configuration, and real validation are correct. + +## Scope + +### Included + +- Confirm the extension does not need or gain local semantic tables for stdlib, members, or signatures. +- Adjust only the minimal extension/bootstrap behavior needed for the new LSP features to flow correctly. +- Add verification coverage and manual validation guidance for the full wave. + +### Excluded + +- Compiler-side editorial resolution logic. +- LSP-side protocol feature implementation. +- Formatter/indentation, code actions, or non-wave-1 IDE features. + +## Non-Goals + +- Turning the extension into a feature-rich semantic client. +- Adding hardcoded completion/signature content for PBS inside TypeScript runtime code. + +## Execution Steps + +### Step 1 - Verify thin-client integration boundaries + +**What:** Confirm the extension remains a pure LSP consumer for the new features. +**How:** Review and adjust the extension only where necessary so completion, hover, and signature help are obtained from the LSP session without client-side semantic fallback tables. +**File(s):** `tools/vscode-extension/src/**`, `tools/vscode-extension/package.json`. + +### Step 2 - Add any minimal structural configuration required by VS Code + +**What:** Keep static contribution surfaces minimal and structural. +**How:** If VS Code requires additional structural contribution metadata for the new features, add only that metadata. Do not add curated PBS semantic or signature data into the extension package. +**File(s):** `tools/vscode-extension/package.json`. + +### Step 3 - Add validation coverage for end-to-end editor assistance + +**What:** Lock the no-local-semantics rule and the end-to-end feature expectations. +**How:** Add tests or deterministic verification steps that ensure: + +1. the extension connects and receives the feature set from the server; +2. no local completion/signature tables were introduced; +3. the user-facing wave can be exercised end to end. + +**File(s):** `tools/vscode-extension/**`, existing validation harnesses if present, and related docs/test notes. + +### Step 4 - Define manual smoke scenarios for wave 1 usability + +**What:** Make the wave verifiable as a product experience rather than only a protocol exercise. +**How:** Document and run manual checks for: + +1. keyword and local-name completion; +2. imported stdlib/member completion after `.`; +3. hover on locals, imported services, builtin types, and methods; +4. signature help during function/method/constructor calls. + +**File(s):** extension verification notes or adjacent implementation docs/tests as appropriate. + +## Test Requirements + +### Unit Tests + +- Any extension-side helpers introduced for structural integration only. + +### Integration Tests + +- End-to-end or near-end-to-end validation that VS Code consumes the server feature set without local semantic duplication. + +### Manual Verification + +- Open representative PBS files in VS Code and verify `completion`, `hover`, and `signature help` behavior matches the expected wave-1 scope. + +## Acceptance Criteria + +- [ ] The extension remains thin and contains no new hardcoded PBS semantic tables for members or signatures. +- [ ] VS Code can consume the new LSP completion, hover, and signature-help responses without extension-side semantic authorship. +- [ ] Wave-1 smoke scenarios are documented and manually verifiable. + +## Dependencies + +- `DEC-0035` accepted and normatively locked. +- `PLN-0076` for the actual LSP feature delivery. + +## Risks + +- It is easy to accidentally patch around server gaps with client-local logic; this plan must resist that. +- Validation can become too protocol-centric and miss real usability failures if manual smoke scenarios are weak. diff --git a/docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md b/docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md index dcfa325c..5de9ba5a 100644 --- a/docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md +++ b/docs/specs/studio/1. Studio Shell and Workspace Layout Specification.md @@ -44,15 +44,10 @@ The left workspace selector remains fixed in the baseline Studio shell. The baseline workspace set includes: -- Code Editor; - Asset Management; - Debugger/Profiler; - Shipper, temporarily represented by the current `BuilderWorkspace`. -Workspace-local behavior for the first `Code Editor` wave is defined in: - -- `5. Code Editor Workspace Specification.md` - `BuilderWorkspace` is transitional and must not be treated as a long-term architectural reference for the final shipping surface. ## Workspace Architecture Model @@ -71,7 +66,7 @@ Baseline workspace architecture rules are: The shell must not encourage a workspace model where every interaction falls back to whole-workspace refresh. Detailed local workspace composition remains workspace-owned. -For example, `Code Editor`-local layout, navigator behavior, tab rules, passive placeholder surfaces, document access-mode behavior, and editor-local save surfaces are not shell-global rules and must be defined by the workspace-local editor specification. +For example, `Assets`-local navigator behavior, detail-pane composition, and mutation flows are not shell-global rules and must be defined by the workspace-local specifications that still exist. Project-session boundaries may still exist above a workspace when the state must remain alive while the project window stays open. diff --git a/docs/specs/studio/5. Code Editor Workspace Specification.md b/docs/specs/studio/5. Code Editor Workspace Specification.md deleted file mode 100644 index b498d34d..00000000 --- a/docs/specs/studio/5. Code Editor Workspace Specification.md +++ /dev/null @@ -1,377 +0,0 @@ -# Code Editor Workspace Specification - -## Status - -Active - -## Applies To - -- `prometeu-studio` -- the Studio `Code Editor` workspace -- the first controlled editor write wave for supported documents including frontend sources - -## Purpose - -Define the normative Studio contract for the first `Code Editor` workspace wave. - -This specification stabilizes: - -- the baseline visual composition of the workspace, -- the `Project Navigator` role and scope, -- the controlled editable file-opening model, -- the responsive tab baseline, -- the editor-owned composition surfaces that host save and semantic-read UX, -- the gutter-based active-structure indicator model for semantic editor scopes, -- the editor-local save surfaces, -- and the workspace boundary with the integrated frontend semantic-read phase. - -## Authority and Precedence - -This specification extends: - -- [`1. Studio Shell and Workspace Layout Specification.md`](1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md) -- [`2. Studio UI Foundations Specification.md`](2.%20Studio%20UI%20Foundations%20Specification.md) -- [`3. Studio Components Module Specification.md`](3.%20Studio%20Components%20Module%20Specification.md) -- [`6. Project Document VFS Specification.md`](6.%20Project%20Document%20VFS%20Specification.md) -- [`7. Integrated LSP Semantic Read Phase Specification.md`](7.%20Integrated%20LSP%20Semantic%20Read%20Phase%20Specification.md) - -If this document conflicts with the global Studio shell specifications, the shell specifications control shell-wide behavior and this document controls workspace-local editor behavior. - -## Normative Inputs - -The `Code Editor` workspace must assume: - -- the Studio shell already mounts `Code Editor` as a baseline workspace, -- the current wave allows editing for the supported document classes classified by `prometeu-vfs` as editable, including frontend-scoped supported documents, -- all project files remain visible in the editor workspace even when only some are frontend-relevant, -- `prometeu.json` plus the selected frontend may identify source roots worth tagging, -- the integrated frontend semantic-read phase may add diagnostics, symbols, outline-facing structure, definition, and highlight for frontend documents, -- the integrated frontend semantic-read phase may later add dedicated structural-anchor metadata for exact scope-indicator anchoring, -- frontend semantic presentation contract data comes from frontend-owned metadata published through `FrontendSpec`, -- and the current wave must not depend on semantic behavior to remain coherent. - -This workspace may consume Studio project metadata and Studio-owned workspace framework primitives, but it must not infer a semantic-provider contract beyond the explicit frontend-owned descriptor surfaced by the integrated semantic-read phase. -This workspace must consume project tree and document data through `prometeu-vfs` rather than through editor-owned direct filesystem readers. -This workspace must consume document access policy and save capability from `prometeu-vfs` rather than rederiving editability locally. - -## Workspace Model - -The `Code Editor` workspace is: - -- project-aware, -- file-oriented, -- controlled-editable in this wave, with supported documents following the canonical access mode provided by `prometeu-vfs`, -- and not a full semantic IDE surface yet. - -The workspace must help the user: - -- see the full project tree, -- identify frontend-relevant source roots visually, -- open supported files into editor tabs, -- save editable supported documents through editor-local commands, -- consume frontend semantic-read UX provided through the integrated LSP phase when that phase is active, -- understand the active file context, -- and understand that broader IDE automation remains outside this wave. - -The workspace must not pretend to offer: - -- merge behavior, -- completion, -- rename, code actions, or formatting, -- or editor-owned semantic inference that bypasses the integrated LSP phase. - -## Baseline Layout - -The baseline workspace layout is: - -- a left editor column, -- a central editor work area, -- a lower helper region, -- and a bottom status bar. - -The left editor column must be a vertical stack containing: - -- a functional `Project Navigator` at the top, -- and a reserved `Outline` region below it. - -The central editor work area must contain: - -- a top command bar containing at least `Save` and `Save All`, -- a tab strip at the top, -- and the editor body below it. - -The lower helper region is present in this wave only as a passive placeholder. - -## Component Ownership Rules - -The `Code Editor` workspace must be implemented as a composition root plus child controls/panels rather than one monolithic render surface. - -Rules: - -- the workspace root should coordinate layout, workspace-level session state, and structural synchronization; -- the workspace root should consume project-session-owned document services such as `prometeu-vfs` rather than owning their lifecycle locally; -- the `Project Navigator`, tab strip, editor body, reserved `Outline`, helper region, and status bar should exist as explicit local surfaces; -- passive placeholder surfaces must remain visually real without implying unsupported functionality; -- and the first wave should preserve a clean path for later extraction of reusable Studio controls when the surfaces become stable enough to justify that move. - -## Project Navigator Rules - -### Primary Navigation Unit - -- The primary navigation unit is the project file or directory. -- The navigator must cover the whole project rather than only frontend-tagged roots. -- Frontend tagging is supplementary context, not an inclusion filter. - -### Structural Snapshot - -- The navigator must consume a structural project-tree entity provided by `prometeu-vfs`. -- That structural tree must not replace the filesystem as the source of truth. -- Content snapshots are separate and are only required for files that are actually opened in the editor through `prometeu-vfs`. -- The navigator may request more targeted tree refresh operations than a full-tree reload when the `prometeu-vfs` contract provides them. - -### Visibility and Ordering - -- All project files and directories must be visible in the navigator. -- Hidden files must be included by default. -- Folders must sort before files. -- Items within those groups must sort alphabetically. - -### Frontend-Aware Tagging - -- `prometeu.json` plus the selected frontend may be used to tag relevant source roots or source directories. -- That tagging must not hide non-source project files. -- That tagging must not imply semantic/editor-language ownership by the navigator itself. -- Rendering those tags remains a Studio UI concern even when the source data comes from `prometeu-vfs`. - -### Refresh Model - -- The navigator must perform one initial refresh when the project opens. -- The navigator must expose a manual refresh action directly on the navigator surface and route that request through `prometeu-vfs`. -- Watcher-driven automatic refresh is explicitly deferred from this wave. - -### Unsupported Files - -- Unsupported or non-text files may still appear in the navigator. -- File support and unsupported-file classification belong to `prometeu-vfs`. -- Attempting to open such a file must show a simple modal stating that the file is not supported in this wave. -- The workspace must not fake a partial preview to imply unsupported file-format coverage. - -## Tabs and File Opening Rules - -- Selecting a supported file that is not already open must open it in a new tab. -- File opening must resolve document content through `prometeu-vfs`. -- The editor must maintain opened-file content in memory for the active Studio session only. -- Frontend-scoped supported documents may coexist in tabs with other editable supported documents. -- The tab strip must be responsive rather than fixed to one hardcoded tab count. -- Overflow tabs must remain accessible through an IntelliJ-style overflow control. -- The active tab must remain visible. -- The tab label must use only the file name with extension. - -The first wave must not define: - -- tab pinning, -- drag-and-drop tab reordering, -- grouped tabs, -- split editors. - -Project-session-owned cross-session tab restoration is accepted for the project-local Studio state wave. - -Rules: - -- persisted editor restoration state must come from the project-session-owned `.studio/` state store rather than from an editor-owned ad hoc file; -- accepted restoration scope in this wave is limited to open tabs and active tab; -- editor layout restoration accepted in this wave may include divider-driven editor sizing, `Project Navigator` expansion state, and dock-panel configuration such as `Outline` and `Helper` open/closed state plus their restored size; -- restoration must degrade safely when a previously open file is missing or unsupported. - -## Access-Mode and Save Model - -The first `Code Editor` write wave is controlled rather than universally editable. - -Rules: - -- the workspace must treat `prometeu-vfs` as the canonical source of document access mode for supported files; -- the workspace must not infer frontend scope from path heuristics, local UI state, or ad hoc extension checks; -- supported frontend-scoped documents may be editable when `prometeu-vfs` classifies them as editable; -- editable scope is limited to the supported textual classes exposed by `prometeu-vfs` for this wave, including supported frontend sources; -- the workspace must expose save behavior only through an editor-local command bar containing at least `Save` and `Save All`; -- the global shell `Save` menu item must not be the save surface for this wave; -- save intent must route through `prometeu-vfs`, which remains the owner of persistence policy; -- the active indentation policy for editable documents MUST come from project-local setup rather than from file-content heuristics; -- the status-bar indentation chip MUST display the active configured policy, not inferred file contents; -- pressing `Tab` in an editable document MUST insert spaces according to the active configured indentation width; -- the active indentation policy MUST remain stable while the user edits document contents; -- opening a file whose existing indentation diverges from the configured policy MUST NOT trigger automatic reformatting; -- and the workspace must not define local merge/conflict behavior against disk changes. - -## Outline Rules - -- The `Outline` region must exist structurally in this wave. -- Before the integrated LSP semantic-read phase is implemented, it may show a discreet placeholder state. -- When the integrated LSP semantic-read phase is active, the `Outline` region may render frontend document-symbol structure and workspace-symbol-backed navigation owned by that phase. -- The workspace must not fake semantic outline structure before real semantic ownership exists. - -## Helper Region Rules - -- The helper region must exist structurally in this wave. -- Before the integrated LSP semantic-read phase is implemented, it may remain passive. -- It exists only to stabilize workspace composition and reserve future space. -- It may later host diagnostics or related semantic-read detail for frontend documents without changing workspace ownership. -- It must not become a substitute for the shell-level `Activity` surface. - -## Integrated Semantic-Read Boundary - -- Frontend documents may be editable when `prometeu-vfs` grants editable access mode. -- Diagnostics, document symbols, workspace symbols, outline-facing structure, definition, and frontend highlight must come through the integrated LSP semantic-read phase rather than from editor-local inference. -- Exact anchor positions for semantic scope indicators must come from frontend-owned structural metadata rather than Studio-local text scanning when that metadata is available. -- Opened frontend documents must be analyzed from the VFS-owned in-memory snapshot exposed through `prometeu-vfs`. -- Unopened frontend documents may be analyzed from filesystem-backed state exposed through the same `prometeu-vfs` boundary. -- Non-frontend highlight may remain Studio-local in this phase. -- Frontend highlight must come from the integrated LSP semantic-read phase and use frontend-owned semantic presentation contract data derived from `FrontendSpec`. -- Frontend semantic presentation resources must remain frontend-owned and must not be replaced by a Studio-owned generic fallback theme. -- When semantic presentation descriptor data or usable frontend resources are absent, the workspace must continue without semantic highlight for that frontend document. -- The workspace must not surface this condition as a product-facing editor error. -- The workspace must not treat semantic-read over editorial snapshots as authorization for build participation or for bypassing `prometeu-vfs` access policy. - -## Inline Hint Rules - -Inline hints must be treated as a host-generic editor capability rather than as a PBS-only feature. - -The Studio `Code Editor` must render inline hints only from frontend-owned semantic payloads transported through the integrated semantic-read phase. - -The editor must not invent: - -- hint existence policy, -- hint text, -- hint categories, -- or host-owned semantic fallback hints. - -If a frontend publishes valid inline hints through the accepted semantic-read contract, the editor must render them. - -If a frontend does not publish inline hints for a construct, the editor must not synthesize one. - -Inline hints are decorative only. - -Therefore the editor must ensure that inline hints: - -- do not modify document text; -- do not become part of persisted file content; -- do not participate as copied document text; -- do not redefine selection ranges; -- and do not redefine caret movement over the text model. - -The completed capability must render inline hints as real inline editor decorations attached to document positions inside the editor flow. - -An approximate visual implementation may exist only as an explicit transitional wave. - -Any transitional approximation: - -- must be documented as transitional; -- must not redefine the final editor contract; -- and must converge to real inline rendering in a later execution step. - -When only some inline hint spans remain valid under degraded semantic analysis, the editor must continue rendering those valid spans rather than disabling all inline hints for the document. - -## Scope Indicator Rules - -The default semantic scope-indicator surface in the Code Editor must remain the gutter. - -The first active-state wave must render at most two simultaneous semantic indicators: - -- `activeContainer` -- `activeScope` - -The editor must not restore the previous full ancestry stack as the default active-state presentation. - -`activeScope` must be selected as the smallest structural range that contains the caret. - -`activeContainer` must be selected as the immediate structural ancestor of `activeScope`. - -If no valid structural ancestor exists, the editor may omit `activeContainer`. - -The editor must not promote a higher semantic owner such as function, type, or module when a nearer structural ancestor exists. - -The concrete visual presentation of `activeContainer` and `activeScope` remains frontend-owned. This specification intentionally does not fix: - -- colors, -- stroke style, -- dashed versus solid treatment, -- opacity, -- thickness, -- or equivalent visual-emphasis details. - -The Studio editor must only preserve the semantic distinction between the two indicator roles. - -Exact guide start and end anchoring must consume the dedicated structural-anchor semantic surface defined by the integrated semantic-read specification. - -The editor must not treat local brace scanning heuristics as the final production contract for exact anchor placement. - -## Status Bar Rules - -The status bar must remain mostly passive in this wave. - -Its left side must show the breadcrumb path of the active file, such as `proj > src > file.pbs`. - -Its right side must show: - -- `L:C`, -- line separator, -- tabs/spaces mode, -- file extension or language, -- and access-mode state. - -For this wave: - -- `L:C` may remain a visual placeholder, -- editable non-frontend documents may show their current file-type state without implying build participation, -- hard `read-only` frontend documents must show an explicit `read-only` state, -- and the status-bar `read-only` surface should be shaped so it can evolve into a per-file toggle later without requiring a model rewrite. - -## Session State Rules - -Visual/editorial state in this wave is session-local only. - -That includes at minimum: - -- open tabs, -- active tab selection, -- tree expansion state, -- and similar visual editor state. - -The following remain outside editor-owned session state: - -- project-session ownership of `prometeu-vfs`, -- structural project-tree data returned by `prometeu-vfs`, -- support classification rules, -- document loading responsibilities, -- save persistence policy, -- and build-facing document ownership. - -Editable documents may have session-local editorial snapshots while the project session remains open. -Those snapshots are editorial only and must not be treated as canonical build input by implication. - -Persisting that state across Studio executions is deferred. - -## Non-Goals - -- merge/conflict resolution -- watcher-driven automatic refresh -- public `prometeu-vfs` event APIs -- helper panel functionality -- editor-owned semantic analysis that bypasses the integrated LSP phase -- frontend editing -- completion, rename, code actions, or formatting -- a normative editor event contract in this wave -- any implication that editor-owned in-memory snapshots participate in the build -- host-owned semantic inline hint policy - -## Exit Criteria - -This specification is complete enough when: - -- the controlled write-wave scope is unambiguous, -- the `Project Navigator` and tab responsibilities are explicit, -- editor-local `Save` and `Save All` are part of the normative workspace contract, -- hard frontend `read-only` behavior is explicit in both warning and status-bar surfaces, -- the workspace boundary with the integrated frontend semantic-read phase is explicit, -- placeholder versus functional regions are clearly separated, -- and excluded frontend editing and completion behavior are clearly outside the current contract. diff --git a/docs/specs/studio/6. Project Document VFS Specification.md b/docs/specs/studio/6. Project Document VFS Specification.md deleted file mode 100644 index 3efd9559..00000000 --- a/docs/specs/studio/6. Project Document VFS Specification.md +++ /dev/null @@ -1,235 +0,0 @@ -# Project Document VFS Specification - -## Status - -Active - -## Applies To - -- `prometeu-vfs` -- `prometeu-studio` -- the Studio project-document boundary for the first controlled Code Editor write wave - -## Purpose - -Define the normative Studio contract for `prometeu-vfs` as the project-document boundary consumed by Studio workspaces. - -This specification stabilizes: - -- `prometeu-vfs` module role, -- project-session ownership, -- the filesystem-backed first-wave contract, -- structural tree and document access responsibilities, -- canonical frontend scope and access policy ownership, -- editorial snapshot and save ownership for editable supported documents including frontend sources, -- the semantic-read consumer boundary used by the integrated LSP phase, -- the RPC-first public API baseline, -- and explicit first-wave exclusions such as public event publication and watchers. - -## Authority and Precedence - -This specification extends: - -- [`1. Studio Shell and Workspace Layout Specification.md`](1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md) -- [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md) - -If this document conflicts with shell-wide Studio rules, the shell specification controls shell-wide behavior and this document controls the project-document boundary contract. - -## Normative Inputs - -`prometeu-vfs` must assume: - -- the current migration wave carries only the Code Editor capabilities that already exist today, -- the initial backend is filesystem-backed, -- the boundary is project-scoped rather than product-global, -- the boundary must outlive workspace focus changes for an opened project, -- Studio UI remains the owner of visual tree state and error presentation, -- and the integrated `prometeu-lsp` phase consumes this boundary as a semantic-read client rather than as a persistence owner. - -`prometeu-vfs` must not infer: - -- watcher-driven refresh in this wave, -- public event API exposure in this wave, -- build participation for editorial snapshots by implication, -- or ownership over non-Studio domains such as packer assets. - -## Module Role - -`prometeu-vfs` is: - -- the Studio project-document boundary, -- the owner of project structural tree data for the covered scope, -- the owner of project document resolution for the covered scope, -- the owner of file support and unsupported-file classification for the covered scope, -- the owner of document access policy for the covered scope, -- the owner of save persistence for editable documents in this wave, -- and the required path through which Studio accesses project-document filesystem state in this wave. - -`prometeu-vfs` is not: - -- a visual tree control, -- a Studio shell replacement, -- a product-wide universal filesystem layer, -- or a public event transport in this wave. - -## Project Session Ownership - -The primary `prometeu-vfs` instance must belong to the Studio project session. - -Rules: - -- a project-scoped `prometeu-vfs` instance must be created when a Studio project session is opened; -- that instance must remain alive while the project session remains open; -- switching workspace focus must not destroy or recreate the project-document state by default; -- the `Code Editor` workspace must consume the session-owned `prometeu-vfs` instance rather than owning the boundary lifecycle itself; -- the project-session scope must cover only the opened project. - -## Filesystem Access Rules - -For the scope covered by this specification, Studio must access the project filesystem through `prometeu-vfs`. - -Rules: - -- the first implementation must be filesystem-backed; -- direct filesystem reads for project tree and document loading must not remain a workspace-owned responsibility in the Code Editor; -- this rule does not grant `prometeu-vfs` ownership over unrelated domains or unrelated filesystem responsibilities elsewhere in the product. - -## Structural Tree Contract - -`prometeu-vfs` must expose a structural representation of the project tree. - -Rules: - -- the structural tree must cover project content only; -- the structural tree must contain structural and documentary data only; -- the structural tree must not contain visual presentation state; -- the structural tree must not contain navigator chrome decisions; -- the structural tree may include structural metadata useful to Studio consumers; -- consumers may request narrower refreshes than a full-tree reload when the request remains within the project boundary. - -The following concerns remain outside `prometeu-vfs`: - -- visual expansion state, -- selection state, -- focus state, -- reveal or scroll behavior, -- icon choice, -- and other view-model concerns of the navigator UI. - -## Document Access Contract - -`prometeu-vfs` must own document resolution for the covered Studio scope. - -Rules: - -- opening a supported file in the Code Editor must go through `prometeu-vfs`; -- the returned document content must describe only project-session document state for the opened project; -- `prometeu-vfs` must classify each supported document as at least editable or hard `read-only`; -- unsupported documents must remain distinguishable from supported `read-only` and supported editable documents; -- `prometeu-vfs` must own editable in-memory editorial snapshots for the current Studio session; -- save operations for editable documents in this wave must persist those editorial snapshots to disk through `prometeu-vfs`; -- Studio consumers must treat `prometeu-vfs` access policy as canonical rather than rederiving editability locally; -- and editorial snapshots must remain outside build-facing document state. - -## Frontend Scope Contract - -Frontend scope must be decided canonically inside `prometeu-vfs`. - -Rules: - -- `FrontendSpec.allowedExtensions` is the source of truth for frontend scope in this wave; -- the VFS document contract must expose a canonical frontend-compatible `typeId` or equivalent scope marker derived from that source of truth; -- consumers must not infer frontend scope from raw dynamic language identifiers, path heuristics, or local UI extension checks; -- frontend-scoped supported documents must be editable in this wave when they are supported by the current frontend; -- the initial editable set is limited to the currently supported textual classes represented as `text`, `json`, `ndjson`, `bash`, and supported frontend source documents resolved from `FrontendSpec.allowedExtensions`; -- and no additional editable class may be inferred by implementation convenience during this wave. - -## Document Access Context - -`prometeu-vfs` must reserve a single access-context entity for document-access-related values. - -Rules: - -- the access-context entity may remain session-local in this wave; -- it must already support lookup and mutation under current demand; -- future persistence may be added later without redesigning the access model; -- and the current reservation must not be treated as authorization for build participation or broader product-wide state ownership. - -## Semantic-Read Consumer Boundary - -`prometeu-vfs` must remain the source-of-truth document substrate consumed by the integrated LSP semantic-read phase. - -Rules: - -- opened frontend documents must be exposed to `prometeu-lsp` from the in-memory editorial snapshot held by `prometeu-vfs`, including dirty snapshots not yet saved to disk; -- unopened frontend documents may be exposed to `prometeu-lsp` from filesystem-backed state through the same boundary; -- `prometeu-lsp` must not bypass `prometeu-vfs` with ad hoc filesystem reads inside Studio UI code; -- `prometeu-lsp` must not become the owner of save, persistence, or access policy; -- semantic analysis over editorial snapshots must remain separate from build-facing document ownership; -- and this boundary must remain suitable for frontend diagnostics, symbols, definition, and highlight without implying frontend edit rights. - -## Support and Plugin Rules - -Support and unsupported-file decisions belong to `prometeu-vfs`. - -Rules: - -- file support classification must be decided inside `prometeu-vfs`; -- plugin or handler resolution for supported file access must be decided inside `prometeu-vfs`; -- Studio UI may render the resulting success or failure state, but it must not own the classification rule itself. - -## Communication Model - -The official first-wave `prometeu-vfs` API is RPC-oriented. - -Rules: - -- commands and queries are the public API baseline for this wave; -- internal runtime events may exist inside `prometeu-vfs`; -- those events must remain internal in this wave; -- internal events must not be promoted to public API by inference during planning or implementation. - -## Refresh and Watchers - -Refresh remains manual in this wave. - -Rules: - -- Studio consumers may request manual refresh through `prometeu-vfs`; -- watcher-driven automatic refresh is deferred; -- file watching must not be introduced implicitly while implementing this specification. - -## Cross-Domain Boundary - -`prometeu-vfs` belongs to the Studio domain. - -Rules: - -- `prometeu-vfs` serves `prometeu-lsp` as a consumer-facing substrate for the semantic-read phase; -- `prometeu-lsp` remains a separate layer and must not be renamed into `prometeu-vfs`; -- `prometeu-vfs` must not absorb packer responsibilities; -- `prometeu-vfs` must not redefine asset ownership; -- extending this boundary outside project-document concerns requires a new decision. - -## Non-Goals - -- public event publication -- watcher-driven refresh -- dirty tracking -- merge or conflict handling -- non-project content snapshots -- a generic product-wide filesystem abstraction -- treating editorial snapshots as canonical build input - -## Exit Criteria - -This specification is complete enough when: - -- `prometeu-vfs` ownership is clearly separated from shell and workspace UI ownership, -- the project-session lifecycle rule is unambiguous, -- the structural tree contract is explicitly non-visual, -- frontend scope and access policy ownership are explicit, -- editable supported-document snapshot and save ownership are explicit, -- the semantic-read consumer boundary with `prometeu-lsp` is explicit, -- the RPC-first public API rule is explicit, -- and deferred public events and watchers are clearly out of scope. diff --git a/docs/specs/studio/7. Integrated LSP Semantic Read Phase Specification.md b/docs/specs/studio/7. Integrated LSP Semantic Read Phase Specification.md deleted file mode 100644 index 61ca1f1b..00000000 --- a/docs/specs/studio/7. Integrated LSP Semantic Read Phase Specification.md +++ /dev/null @@ -1,183 +0,0 @@ -# Integrated LSP Semantic Read Phase Specification - -## Status - -Active - -## Applies To - -- `prometeu-studio` -- `prometeu-vfs` -- `prometeu-lsp` -- the integrated frontend semantic phase in the Studio `Code Editor` workspace - -## Purpose - -Define the normative Studio contract for the integrated frontend semantic-read phase that arrives before any frontend editing release. - -This specification stabilizes: - -- the phase boundary between semantic ownership and frontend editing ownership, -- the ownership relationship between `prometeu-vfs`, `prometeu-lsp`, and the Studio editor, -- the minimum semantic capability set for frontend documents, -- the dedicated semantic surface used for structural anchors and guide-aware editor structure, -- the packaging expectations for the public `prometeu-lsp` API surface, -- and the explicit exclusions that remain outside this phase. - -## Authority and Precedence - -This specification extends: - -- [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md) -- [`6. Project Document VFS Specification.md`](6.%20Project%20Document%20VFS%20Specification.md) - -If this document conflicts with shell-wide Studio rules, shell rules control shell behavior, the Code Editor specification controls workspace-local editor UX, the VFS specification controls document-boundary ownership, and this document controls the integrated semantic-read phase itself. - -## Normative Inputs - -The integrated semantic-read phase must assume: - -- frontend-scoped documents may be editable under the controlled write wave exposed by `prometeu-vfs`, -- editable supported documents remain governed by the controlled write wave, -- `FrontendSpec.allowedExtensions` remains the source of truth for frontend scope, -- `FrontendSpec` is the canonical source of frontend semantic presentation contract data, -- `prometeu-vfs` owns document state, snapshots, persistence, and access policy, -- and semantic-read over editorial snapshots must remain separate from build-facing ownership. - -## Phase Boundary - -This phase is a semantic phase for frontend documents, not an ownership phase for frontend editing. - -Rules: - -- no capability in this phase may override `prometeu-vfs` access policy, save policy, or snapshot ownership; -- completion, rename, code actions, and formatting remain outside this phase. - -## Ownership Rules - -- `prometeu-vfs` owns document state, access policy, editorial snapshots, and persistence. -- `prometeu-lsp` is a semantic consumer above `prometeu-vfs`. -- the Studio `Code Editor` owns UX surfaces that render semantic-read output. -- each frontend owns its semantic vocabulary and semantic presentation resources. -- build-facing document ownership remains outside this phase. - -`prometeu-lsp` must not become the owner of: - -- save, -- persistence, -- document access policy, -- frontend semantic presentation assets, -- or frontend edit-right release. - -## Document Source-of-Truth Rules - -- opened frontend documents must be analyzed from the in-memory snapshot exposed by `prometeu-vfs`; -- unopened frontend documents may be analyzed from filesystem-backed state exposed through the same `prometeu-vfs` boundary; -- Studio UI code must not bypass `prometeu-vfs` with ad hoc file loading for semantic-read behavior; -- semantic-read over editorial snapshots must not be treated as canonical build input by implication. - -## Minimum Capability Set - -The minimum semantic capability set of this phase must include: - -- diagnostics for frontend documents; -- document symbols for frontend documents; -- workspace symbols for frontend documents; -- outline-facing structural symbol data; -- go-to-definition for frontend documents; -- frontend semantic highlight. - -The phase may ship the dedicated structural-anchor surface after the baseline capability set is stable, but Studio and frontend implementations must treat that surface as the canonical source of exact scope-indicator anchoring once it exists. - -The phase may also ship a dedicated inline-hint semantic surface. - -Once shipped, frontend and Studio implementations must treat that surface as the canonical source of decorative inline hint payloads. - -## Highlight Ownership Rules - -- frontend highlight must come from semantic information provided through `prometeu-lsp`; -- frontend highlight must use frontend-owned semantic presentation contract data derived from `FrontendSpec`; -- `prometeu-lsp` must expose a dedicated semantic presentation descriptor derived from the resolved `FrontendSpec`; -- that descriptor must remain simple and include frontend-owned `semanticKeys` plus frontend-owned `resources` in the same message surface; -- `prometeu-lsp` must not collapse frontend semantic keys into host-owned generic categories; -- Studio must project semantic keys mechanically to CSS classes without semantic translation; -- frontend semantic presentation resources must remain under frontend `resources/` and be resolved like ordinary Java resources; -- when semantic presentation descriptor data or usable resources are absent, Studio must continue without semantic highlight; -- this condition must not become a product-facing Studio error and may at most produce normal development logs; -- non-frontend highlight may remain on the existing Studio-local highlighting path in this phase. - -## Inline Hint Ownership Rules - -- frontend inline hints must come from semantic information provided through `prometeu-lsp`; -- frontend hint existence and hint payload semantics remain frontend-owned; -- `prometeu-lsp` must expose a dedicated transport contract for inline hints rather than relying on Studio-local inference; -- `prometeu-lsp` must preserve deterministic anchor information required for host rendering; -- `prometeu-lsp` must not invent host-owned hint policy when a frontend does not publish hints; -- Studio must render transported hints mechanically as host decorations rather than as document text; -- valid inline hint spans must survive partial degradation when the frontend can still produce them; -- invalid or missing hint spans may be omitted locally; -- Studio must not disable all inline hints for a document solely because some hint spans are unavailable. - -## Structural Anchor Semantic Surface - -Exact structural anchoring for editor scope indicators must not be overloaded into `documentSymbols`. - -`documentSymbols` remains the outline and navigation surface. - -Guide-oriented structure must be exposed through a dedicated semantic surface for structural anchors and related guide metadata. - -That dedicated surface must be frontend-owned and language-agnostic. - -The contract must describe generic structural positions rather than PBS-specific brace semantics. - -The surface must be able to represent structures whose boundaries are expressed by: - -- braces; -- keywords; -- indentation-derived blocks; -- or mixed delimiter schemes. - -At minimum, the dedicated structural-anchor surface must be able to communicate: - -- structural ranges that can participate in active-scope selection; -- immediate parentage or equivalent information needed to recover the nearest ancestor relationship; -- exact structural anchor positions used for guide start and end placement; -- enough structural identity to distinguish the active structural range from its immediate container. - -The semantic-read contract must not require Studio to scan source text in order to discover the final structural anchor positions. - -Temporary local heuristics may exist only as development aids. They must not become the canonical production contract. - -The exact serialized schema of this dedicated surface may evolve in implementation plans, but any accepted schema must preserve the invariants above. - -## Packaging Rules - -The public `prometeu-lsp` API should follow flat packaging similar to `prometeu-packer-api`. - -Rules: - -- public contract surfaces should remain shallow and explicit; -- packages such as `dtos`, `messages`, and `events` are the preferred shape for public API grouping where those surfaces exist; -- deep public packaging by internal implementation concern should be avoided; -- runtime or implementation layering may exist internally, but it must not leak into the public contract surface by default. - -## Non-Goals - -- frontend save policy -- completion -- rename -- code actions -- formatting -- build participation inferred from editorial snapshots -- external-editor compatibility requirements -- Studio-owned semantic hint generation - -## Exit Criteria - -This specification is complete enough when: - -- the semantic-read versus frontend-editing phase boundary is explicit, -- `prometeu-vfs` and `prometeu-lsp` ownership remains unambiguous, -- the minimum semantic capability set is explicit, -- frontend and non-frontend highlight ownership is clearly separated, -- and the excluded editing and completion capabilities remain unambiguous. diff --git a/docs/specs/studio/8. Project-Local Studio State Specification.md b/docs/specs/studio/8. Project-Local Studio State Specification.md index 70b9a703..3f50d8ff 100644 --- a/docs/specs/studio/8. Project-Local Studio State Specification.md +++ b/docs/specs/studio/8. Project-Local Studio State Specification.md @@ -22,7 +22,6 @@ This specification implements the accepted decision for project-local Studio sta It extends: - [`1. Studio Shell and Workspace Layout Specification.md`](1.%20Studio%20Shell%20and%20Workspace%20Layout%20Specification.md) -- [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md) ## Core Rules @@ -31,7 +30,7 @@ It extends: 3. The main session-restoration store MUST remain a single file at `.studio/state.json`. 4. The main session-restoration store MUST be versioned. 5. The main session-restoration store MUST load when the project session opens. -6. Restorable shell and editor state accepted by this wave MUST restore when the project shell mounts. +6. Restorable shell state accepted by this wave MUST restore when the project shell mounts. 7. The main session-restoration store MUST save when the project shell or project session closes. 8. Missing, invalid, or schema-incompatible session-restoration state MUST fall back to safe default values and MUST NOT block project open. 9. Project-local setup configuration MUST live in a separate file at `.studio/setup.json`. @@ -44,13 +43,11 @@ The main session-restoration store MUST include: - `shellLayout` - `openShellState` -- `editorRestoration` Wave-1 accepted meaning is: -- `shellLayout`: restorable layout state already accepted by implementation, including divider positions and equivalent layout proportions for accepted workspaces such as `Assets` and `Code Editor`, `Project Navigator` expansion state, and editor dock-panel open/closed plus restored-size configuration; +- `shellLayout`: restorable layout state already accepted by implementation, including divider positions and equivalent layout proportions for accepted workspaces such as `Assets`; - `openShellState`: which workspace or equivalent shell-owned project view was open; -- `editorRestoration`: open editor tabs and active tab. ## Project-Local Setup Configuration @@ -61,8 +58,8 @@ Rules: - project-local setup MUST live in `.studio/setup.json`; - project-local setup MUST be treated as project configuration rather than session state; - project-local setup MAY contain runtime-oriented values such as the Prometeu runtime path; -- project-local setup MUST own the project-wide editor indentation policy; -- the editor indentation policy MUST define at least indentation mode and indentation width; +- project-local setup MAY continue carrying project-wide indentation policy defaults for future code-tooling integrations; +- the indentation policy MUST define at least indentation mode and indentation width; - when setup does not provide an explicit indentation policy, consumers MUST resolve it to `spaces` with width `4`; - project-local setup MAY grow with additional manual or automatic configuration keys over time; - consumers such as `Play` MUST read project-local setup from the dedicated setup file rather than from `.studio/state.json`; diff --git a/docs/specs/studio/README.md b/docs/specs/studio/README.md index 67aa024e..affec2d3 100644 --- a/docs/specs/studio/README.md +++ b/docs/specs/studio/README.md @@ -54,10 +54,7 @@ The current Studio core corpus is: 2. [`2. Studio UI Foundations Specification.md`](2.%20Studio%20UI%20Foundations%20Specification.md) 3. [`3. Studio Components Module Specification.md`](3.%20Studio%20Components%20Module%20Specification.md) 4. [`4. Assets Workspace Specification.md`](4.%20Assets%20Workspace%20Specification.md) -5. [`5. Code Editor Workspace Specification.md`](5.%20Code%20Editor%20Workspace%20Specification.md) -6. [`6. Project Document VFS Specification.md`](6.%20Project%20Document%20VFS%20Specification.md) -7. [`7. Integrated LSP Semantic Read Phase Specification.md`](7.%20Integrated%20LSP%20Semantic%20Read%20Phase%20Specification.md) -8. [`8. Project-Local Studio State Specification.md`](8.%20Project-Local%20Studio%20State%20Specification.md) +5. [`8. Project-Local Studio State Specification.md`](8.%20Project-Local%20Studio%20State%20Specification.md) ## Reading Order @@ -67,19 +64,14 @@ Recommended order: 2. shared UI foundations; 3. components module policy; 4. assets workspace behavior; -5. project document VFS boundary; -6. code editor workspace behavior; -7. integrated LSP semantic-read behavior; -8. project-local Studio state behavior. +5. project-local Studio state behavior. ## Current Wave Notes -The current `Code Editor` wave is not globally read-only anymore. +The embedded Studio code-editor stack was removed by `DEC-0031`. Normative reminders: -- editable scope is limited to supported non-frontend textual documents classified by `prometeu-vfs`; -- frontend-scoped supported documents remain hard `read-only`; -- the editor-local `Save` and `Save All` surfaces belong to the `Code Editor` workspace rather than the shell; -- editorial in-memory snapshots remain outside build-facing state; -- frontend semantic-read capability lands before any frontend editing release and is documented by the integrated LSP phase specification. +- the active Studio workspace corpus is `Assets`, `Debug`, and `Shipper`; +- project-local Studio session state remains normative and is owned by `StudioProjectSession`; +- any future external-editor backend or replacement language-service architecture requires a new accepted decision before new Studio specs are introduced. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 84d8cc26..8324a30f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,6 +6,7 @@ jacoco = "0.8.12" lombok = "1.18.32" junit-jupiter = "5.12.1" slf4j = "2.0.7" +lsp4j = "1.0.0" [libraries] javafx-controls = { group = "org.openjfx", name = "javafx-controls", version.ref = "javafx" } @@ -20,6 +21,7 @@ lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok" junit-jupiter = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit-jupiter" } slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +lsp4j = { group = "org.eclipse.lsp4j", name = "org.eclipse.lsp4j", version.ref = "lsp4j" } [plugins] javafx = { id = "org.openjfx.javafxplugin", version = "0.1.0" } diff --git a/prometeu-app/build.gradle.kts b/prometeu-app/build.gradle.kts index 253ad5e0..92e953ee 100644 --- a/prometeu-app/build.gradle.kts +++ b/prometeu-app/build.gradle.kts @@ -7,8 +7,6 @@ dependencies { implementation(project(":prometeu-infra")) implementation(project(":prometeu-lsp:prometeu-lsp-api")) implementation(project(":prometeu-lsp:prometeu-lsp-v1")) - implementation(project(":prometeu-vfs:prometeu-vfs-api")) - implementation(project(":prometeu-vfs:prometeu-vfs-v1")) implementation(project(":prometeu-packer:prometeu-packer-api")) implementation(project(":prometeu-packer:prometeu-packer-v1")) implementation(project(":prometeu-studio")) @@ -23,4 +21,4 @@ application { javafx { version = libs.versions.javafx.get() modules("javafx.controls", "javafx.fxml") -} \ No newline at end of file +} diff --git a/prometeu-app/src/main/java/p/studio/AppContainer.java b/prometeu-app/src/main/java/p/studio/AppContainer.java index 44e825d3..9b1851c5 100644 --- a/prometeu-app/src/main/java/p/studio/AppContainer.java +++ b/prometeu-app/src/main/java/p/studio/AppContainer.java @@ -2,14 +2,12 @@ package p.studio; import com.fasterxml.jackson.databind.ObjectMapper; import p.packer.Packer; -import p.studio.lsp.LspServiceFactory; -import p.studio.lsp.LspServiceFactoryImpl; +import p.studio.lsp.api.LspServerLifecycle; import p.studio.lsp.events.StudioEventBus; import p.studio.lsp.events.StudioPackerEventAdapter; +import p.studio.lsp.services.LspServerLifecycleImpl; import p.studio.utilities.ThemeService; import p.studio.utilities.i18n.I18nService; -import p.studio.vfs.FilesystemProjectDocumentVfsFactory; -import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -21,22 +19,20 @@ public final class AppContainer implements Container { private final ThemeService themeService; private final StudioEventBus studioEventBus; private final ObjectMapper mapper; - private final LspServiceFactory lspServiceFactory; - private final ProjectDocumentVfsFactory projectDocumentVfsFactory; private final EmbeddedPacker embeddedPacker; private final StudioBackgroundTasks backgroundTasks; + private final LspServerLifecycle lspServerLifecycle; public AppContainer() { this.i18nService = new I18nService(); this.themeService = new ThemeService(); this.studioEventBus = new StudioEventBus(); this.mapper = new ObjectMapper(); - this.lspServiceFactory = new LspServiceFactoryImpl(); - this.projectDocumentVfsFactory = new FilesystemProjectDocumentVfsFactory(); final ExecutorService backgroundExecutor = Executors.newFixedThreadPool(2, new StudioWorkerThreadFactory()); this.backgroundTasks = new StudioBackgroundTasks(backgroundExecutor); final Packer packer = Packer.bootstrap(this.mapper, new StudioPackerEventAdapter(studioEventBus)); this.embeddedPacker = new EmbeddedPacker(packer.workspaceService(), packer::close); + this.lspServerLifecycle = new LspServerLifecycleImpl(); } @Override @@ -59,16 +55,6 @@ public final class AppContainer implements Container { return mapper; } - @Override - public LspServiceFactory getPrometeuLspServiceFactory() { - return lspServiceFactory; - } - - @Override - public ProjectDocumentVfsFactory getProjectDocumentVfsFactory() { - return projectDocumentVfsFactory; - } - @Override public EmbeddedPacker getPacker() { return embeddedPacker; @@ -79,6 +65,11 @@ public final class AppContainer implements Container { return backgroundTasks; } + @Override + public p.studio.lsp.api.LspServerLifecycle getLspServerLifecycle() { + return lspServerLifecycle; + } + @Override public void shutdownContainer() { embeddedPacker.shutdown(); diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java index 27b9781c..ea3135f3 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java @@ -1,8 +1,7 @@ package p.studio.compiler; -import p.studio.compiler.models.FrontendSpec; import p.studio.compiler.models.FrontendSemanticPresentationSpec; -import p.studio.compiler.pbs.PbsSemanticKind; +import p.studio.compiler.models.FrontendSpec; import p.studio.utilities.structures.ReadOnlySet; import java.util.List; @@ -15,7 +14,9 @@ public class PBSDefinitions { .sourceRoots(ReadOnlySet.from("src")) .stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1))) .semanticPresentation(new FrontendSemanticPresentationSpec( - PbsSemanticKind.semanticKeys(), - List.of("/themes/pbs/semantic-highlighting.css"))) + List.of(PBSFrontendThemes.DEFAULT_PBS), + "pbs-default", + List.of(PBSFrontendHostProjections.VSCODE))) + .semanticTokenProvider(new PBSSemanticTokenProvider()) .build(); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendHostProjections.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendHostProjections.java new file mode 100644 index 00000000..068a10ea --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendHostProjections.java @@ -0,0 +1,78 @@ +package p.studio.compiler; + +import p.studio.compiler.models.FrontendHostProjectionEntrySpec; +import p.studio.compiler.models.FrontendHostProjectionSpec; +import p.studio.compiler.pbs.PbsSemanticKind; + +import java.util.Arrays; +import java.util.List; + +import static p.studio.compiler.pbs.PbsSemanticKind.*; + +public final class PBSFrontendHostProjections { + public static final String VSCODE_HOST_ID = "vscode"; + + public static final FrontendHostProjectionSpec VSCODE = new FrontendHostProjectionSpec( + VSCODE_HOST_ID, + Arrays.stream(PbsSemanticKind.values()) + .map(PBSFrontendHostProjections::mapVscodeProjection) + .toList()); + + private PBSFrontendHostProjections() { + } + + private static FrontendHostProjectionEntrySpec mapVscodeProjection(final PbsSemanticKind kind) { + return switch (kind) { + case COMMENT -> projection(COMMENT.semanticKey(), "comment"); + case STRING -> projection(STRING.semanticKey(), "string"); + case NUMBER -> projection(NUMBER.semanticKey(), "number"); + case LITERAL -> projection(LITERAL.semanticKey(), "enumMember", "variable"); + case LIFECYCLE -> projection(LIFECYCLE.semanticKey(), "function", "variable", "async"); + case KEYWORD -> projection(KEYWORD.semanticKey(), "keyword"); + case OPERATOR -> projection(OPERATOR.semanticKey(), "operator", "keyword"); + case PUNCTUATION -> projection(PUNCTUATION.semanticKey(), "operator", "keyword"); + case FUNCTION -> projection(FUNCTION.semanticKey(), "function"); + case METHOD -> projection(METHOD.semanticKey(), "method", "function"); + case CONSTRUCTOR -> projection(CONSTRUCTOR.semanticKey(), "class", "type", "declaration"); + case STRUCT -> projection(STRUCT.semanticKey(), "struct", "type"); + case CONTRACT -> projection(CONTRACT.semanticKey(), "interface", "type"); + case HOST -> projection(HOST.semanticKey(), "class", "type", "defaultLibrary"); + case BUILTIN_TYPE -> projection(BUILTIN_TYPE.semanticKey(), "type", "variable", "defaultLibrary"); + case SERVICE -> projection(SERVICE.semanticKey(), "class", "type", "defaultLibrary"); + case ASSET -> projection(ASSET.semanticKey(), "variable", "string", "readonly"); + case ERROR -> projection(ERROR.semanticKey(), "enum", "type"); + case ENUM -> projection(ENUM.semanticKey(), "enum", "type"); + case CALLBACK -> projection(CALLBACK.semanticKey(), "interface", "type"); + case GLOBAL -> projection(GLOBAL.semanticKey(), "variable", "variable", "readonly"); + case CONST -> projection(CONST.semanticKey(), "variable", "variable", "readonly"); + case IMPLEMENTS -> projection(IMPLEMENTS.semanticKey(), "keyword"); + case IDENTIFIER -> projection(IDENTIFIER.semanticKey(), "variable"); + }; + } + + private static FrontendHostProjectionEntrySpec projection( + final String semanticKey, + final String hostTokenType) { + return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, List.of(), hostTokenType, List.of()); + } + + private static FrontendHostProjectionEntrySpec projection( + final String semanticKey, + final String hostTokenType, + final String fallbackTokenType) { + return new FrontendHostProjectionEntrySpec(semanticKey, hostTokenType, List.of(), fallbackTokenType, List.of()); + } + + private static FrontendHostProjectionEntrySpec projection( + final String semanticKey, + final String hostTokenType, + final String fallbackTokenType, + final String modifier) { + return new FrontendHostProjectionEntrySpec( + semanticKey, + hostTokenType, + List.of(modifier), + fallbackTokenType, + List.of(modifier)); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendThemes.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendThemes.java new file mode 100644 index 00000000..da9bc63c --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSFrontendThemes.java @@ -0,0 +1,46 @@ +package p.studio.compiler; + +import p.studio.compiler.models.FrontendEditorPaletteSpec; +import p.studio.compiler.models.FrontendTokenStyleSpec; +import p.studio.compiler.models.FrontendVisualThemeSpec; + +import java.util.List; + +public class PBSFrontendThemes { + public static final FrontendVisualThemeSpec DEFAULT_PBS = new FrontendVisualThemeSpec( + "pbs-default", + "PBS Default", + new FrontendEditorPaletteSpec( + "#d9e2ec", + "#2f3f5b", + "#23314a", + "#6b7f99", + "#241f38", + "#b392f0", + "#f5efff"), + List.of( + new FrontendTokenStyleSpec("pbs-comment", "#8b949e", true, false, false), + new FrontendTokenStyleSpec("pbs-string", "#d8b86a", false, false, false), + new FrontendTokenStyleSpec("pbs-number", "#ff8cc6", false, false, false), + new FrontendTokenStyleSpec("pbs-literal", "#7fb8ff", false, false, false), + new FrontendTokenStyleSpec("pbs-lifecycle", "#ff7b72", false, true, false), + new FrontendTokenStyleSpec("pbs-keyword", "#7fb8ff", false, false, false), + new FrontendTokenStyleSpec("pbs-operator", "#c9d1d9", false, false, false), + new FrontendTokenStyleSpec("pbs-punctuation", "#8b949e", false, false, false), + new FrontendTokenStyleSpec("pbs-function", "#7ee787", false, true, false), + new FrontendTokenStyleSpec("pbs-method", "#7ee787", false, false, false), + new FrontendTokenStyleSpec("pbs-constructor", "#7ee787", false, true, false), + new FrontendTokenStyleSpec("pbs-struct", "#56d4dd", true, false, false), + new FrontendTokenStyleSpec("pbs-contract", "#79c0ff", false, false, true), + new FrontendTokenStyleSpec("pbs-host", "#c297ff", false, false, false), + new FrontendTokenStyleSpec("pbs-builtin-type", "#56d4dd", false, false, false), + new FrontendTokenStyleSpec("pbs-service", "#ff8e7a", false, true, false), + new FrontendTokenStyleSpec("pbs-asset", "#c297ff", false, false, false), + new FrontendTokenStyleSpec("pbs-error", "#ff7b72", false, false, true), + new FrontendTokenStyleSpec("pbs-enum", "#56d4dd", false, true, false), + new FrontendTokenStyleSpec("pbs-callback", "#ffb3e6", false, false, false), + new FrontendTokenStyleSpec("pbs-global", "#f778ba", true, false, false), + new FrontendTokenStyleSpec("pbs-const", "#ffab70", false, true, false), + new FrontendTokenStyleSpec("pbs-implements", "#a5d6ff", false, false, false), + new FrontendTokenStyleSpec("pbs-identifier", "#d4d4d4", false, false, false))); +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSSemanticTokenProvider.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSSemanticTokenProvider.java new file mode 100644 index 00000000..164841a0 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSSemanticTokenProvider.java @@ -0,0 +1,365 @@ +package p.studio.compiler; + +import p.studio.compiler.models.FrontendSemanticToken; +import p.studio.compiler.models.FrontendSemanticTokenProvider; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.PbsSemanticKind; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.compiler.pbs.parser.PbsBarrelParser; +import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.pbs.stdlib.ResourceStdlibEnvironmentResolver; +import p.studio.compiler.pbs.stdlib.StdlibModuleSource; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public final class PBSSemanticTokenProvider implements FrontendSemanticTokenProvider { + private static final Set BUILTIN_TYPE_NAMES = Set.of("int", "float", "bool", "string", "i32", "i64", "f32", "f64"); + private static final int DEFAULT_STDLIB_VERSION = 1; + private static final ResourceStdlibEnvironmentResolver STDLIB_RESOLVER = new ResourceStdlibEnvironmentResolver(); + private static final Map DECLARATION_KINDS = declarationKinds(); + + @Override + public List tokenize(final String documentText) { + final String effectiveText = documentText == null ? "" : documentText; + final ReadOnlyList tokens = PbsLexer.lex(effectiveText, FileId.none(), DiagnosticSink.empty()); + final Map declaredKindsByName = collectDeclaredKinds(tokens); + final Set assetReferenceTokenIndexes = collectAssetReferenceTokenIndexes(tokens); + final ArrayList semanticTokens = new ArrayList<>(); + + for (int index = 0; index < tokens.size(); index += 1) { + final PbsToken token = tokens.get(index); + final PbsSemanticKind semanticKind = classifyToken(tokens, index, declaredKindsByName, assetReferenceTokenIndexes); + if (semanticKind == null) { + continue; + } + semanticTokens.add(new FrontendSemanticToken(token.start(), token.end(), semanticKind.semanticKey())); + } + + return List.copyOf(semanticTokens); + } + + private PbsSemanticKind classifyToken( + final ReadOnlyList tokens, + final int index, + final Map declaredKindsByName, + final Set assetReferenceTokenIndexes) { + final PbsToken token = tokens.get(index); + final PbsSemanticKind lexicalKind = PbsSemanticKind.forToken(token); + if (lexicalKind != null) { + return lexicalKind; + } + if (token.kind() == PbsTokenKind.IDENTIFIER) { + if (assetReferenceTokenIndexes.contains(index)) { + return PbsSemanticKind.ASSET; + } + if (isLifecycleToken(tokens, index, token)) { + return PbsSemanticKind.LIFECYCLE; + } + return classifyIdentifierToken(tokens, index, token, declaredKindsByName); + } + return null; + } + + private PbsSemanticKind classifyIdentifierToken( + final ReadOnlyList tokens, + final int index, + final PbsToken token, + final Map declaredKindsByName) { + final PbsTokenKind previousKind = previousSignificantKind(tokens, index); + final PbsTokenKind nextKind = nextSignificantKind(tokens, index); + final boolean typeReferenceContext = isTypeReferenceContext(previousKind); + + if (previousKind == PbsTokenKind.STRUCT) { + return PbsSemanticKind.STRUCT; + } + if (previousKind == PbsTokenKind.CONTRACT) { + return PbsSemanticKind.CONTRACT; + } + if (previousKind == PbsTokenKind.HOST) { + return PbsSemanticKind.HOST; + } + if (previousKind == PbsTokenKind.SERVICE) { + return PbsSemanticKind.SERVICE; + } + if (previousKind == PbsTokenKind.ENUM || previousKind == PbsTokenKind.ERROR) { + return PbsSemanticKind.ENUM; + } + if (previousKind == PbsTokenKind.CALLBACK) { + return PbsSemanticKind.CALLBACK; + } + if (previousKind == PbsTokenKind.GLOBAL) { + return PbsSemanticKind.GLOBAL; + } + if (previousKind == PbsTokenKind.CONST) { + return PbsSemanticKind.CONST; + } + if (previousKind == PbsTokenKind.CTOR || previousKind == PbsTokenKind.NEW) { + return PbsSemanticKind.CONSTRUCTOR; + } + if (previousKind == PbsTokenKind.BUILTIN) { + return PbsSemanticKind.BUILTIN_TYPE; + } + final PbsSemanticKind declaredKind = declaredKindsByName.get(token.lexeme()); + if (declaredKind != null) { + if (previousKind == PbsTokenKind.DOT && nextKind == PbsTokenKind.LEFT_PAREN) { + return PbsSemanticKind.METHOD; + } + if (nextKind == PbsTokenKind.DOT + || previousKind == PbsTokenKind.NEW + || typeReferenceContext) { + return declaredKind; + } + } + if (previousKind == PbsTokenKind.DOT && nextKind == PbsTokenKind.LEFT_PAREN) { + return PbsSemanticKind.METHOD; + } + if (nextKind == PbsTokenKind.LEFT_PAREN) { + return PbsSemanticKind.FUNCTION; + } + if (typeReferenceContext && BUILTIN_TYPE_NAMES.contains(token.lexeme())) { + return PbsSemanticKind.BUILTIN_TYPE; + } + return PbsSemanticKind.IDENTIFIER; + } + + private Map collectDeclaredKinds(final ReadOnlyList tokens) { + final Map declaredKindsByName = new HashMap<>(); + for (int index = 0; index < tokens.size() - 1; index += 1) { + final PbsToken token = tokens.get(index); + final PbsToken nextToken = tokens.get(index + 1); + if (nextToken.kind() != PbsTokenKind.IDENTIFIER) { + continue; + } + final PbsSemanticKind declarationKind = declarationKind(token.kind()); + if (declarationKind == null) { + continue; + } + declaredKindsByName.put(nextToken.lexeme(), declarationKind); + } + declaredKindsByName.putAll(collectStdlibImportedKinds(tokens)); + return Map.copyOf(declaredKindsByName); + } + + private Set collectAssetReferenceTokenIndexes(final ReadOnlyList tokens) { + final Set indexes = new HashSet<>(); + for (int index = 0; index < tokens.size(); index += 1) { + final PbsToken token = tokens.get(index); + if (token.kind() != PbsTokenKind.IDENTIFIER || !"assets".equals(token.lexeme())) { + continue; + } + final Integer firstDotIndex = nextSignificantIndex(tokens, index); + if (firstDotIndex == null || tokens.get(firstDotIndex).kind() != PbsTokenKind.DOT) { + continue; + } + + indexes.add(index); + Integer cursor = firstDotIndex; + while (cursor != null && tokens.get(cursor).kind() == PbsTokenKind.DOT) { + final Integer memberIndex = nextSignificantIndex(tokens, cursor); + if (memberIndex == null || tokens.get(memberIndex).kind() != PbsTokenKind.IDENTIFIER) { + break; + } + indexes.add(memberIndex); + cursor = nextSignificantIndex(tokens, memberIndex); + } + } + return Set.copyOf(indexes); + } + + private PbsSemanticKind declarationKind(final PbsTokenKind kind) { + return DECLARATION_KINDS.get(kind); + } + + private Map collectStdlibImportedKinds(final ReadOnlyList tokens) { + final DiagnosticSink diagnostics = DiagnosticSink.empty(); + final PbsAst.File file = PbsParser.parse(tokens, FileId.none(), diagnostics); + final Map importedKindsByLocalName = new HashMap<>(); + for (final PbsAst.ImportDecl importDecl : file.imports()) { + if (importDecl.items().isEmpty()) { + continue; + } + final Map exportedKinds = resolveStdlibExportKinds(importDecl.moduleRef()); + if (exportedKinds.isEmpty()) { + continue; + } + for (final Map.Entry entry : exportedKinds.entrySet()) { + if (entry.getValue() == PbsSemanticKind.BUILTIN_TYPE) { + importedKindsByLocalName.put(entry.getKey(), entry.getValue()); + } + } + for (final PbsAst.ImportItem item : importDecl.items()) { + final PbsSemanticKind semanticKind = exportedKinds.get(item.name()); + if (semanticKind == null) { + continue; + } + importedKindsByLocalName.put(item.alias() == null ? item.name() : item.alias(), semanticKind); + } + } + return importedKindsByLocalName; + } + + private Map resolveStdlibExportKinds(final PbsAst.ModuleRef moduleRef) { + final var stdlibEnvironment = STDLIB_RESOLVER.resolve(DEFAULT_STDLIB_VERSION); + return stdlibEnvironment.resolveModule(moduleRef.project(), moduleRef.pathSegments()) + .map(this::parseStdlibVisibleKinds) + .orElseGet(Map::of); + } + + private Map parseStdlibVisibleKinds(final StdlibModuleSource moduleSource) { + final Map visibleKinds = new HashMap<>(parseBarrelExportKinds(moduleSource.barrelSource())); + for (final StdlibModuleSource.SourceFile sourceFile : moduleSource.sourceFiles()) { + visibleKinds.putAll(parseBuiltinTypeKinds(sourceFile.source())); + } + return visibleKinds; + } + + private Map parseBarrelExportKinds(final String barrelSource) { + final ReadOnlyList barrelTokens = PbsLexer.lex(barrelSource, FileId.none(), DiagnosticSink.empty()); + final PbsAst.BarrelFile barrelFile = PbsBarrelParser.parse(barrelTokens, FileId.none(), DiagnosticSink.empty()); + final Map exportedKinds = new HashMap<>(); + for (final PbsAst.BarrelItem item : barrelFile.items()) { + final PbsSemanticKind semanticKind = semanticKindForBarrelItem(item); + if (semanticKind == null) { + continue; + } + if (item instanceof PbsAst.BarrelStructItem structItem && structItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(structItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelContractItem contractItem && contractItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(contractItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelHostItem hostItem && hostItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(hostItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelErrorItem errorItem && errorItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(errorItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelEnumItem enumItem && enumItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(enumItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelServiceItem serviceItem && serviceItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(serviceItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelGlobalItem globalItem && globalItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(globalItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelConstItem constItem && constItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(constItem.name(), semanticKind); + } else if (item instanceof PbsAst.BarrelCallbackItem callbackItem && callbackItem.visibility() == PbsAst.Visibility.PUB) { + exportedKinds.put(callbackItem.name(), semanticKind); + } + } + return exportedKinds; + } + + private Map parseBuiltinTypeKinds(final String source) { + final ReadOnlyList sourceTokens = PbsLexer.lex(source, FileId.none(), DiagnosticSink.empty()); + final PbsAst.File file = PbsParser.parse( + sourceTokens, + FileId.none(), + DiagnosticSink.empty(), + PbsParser.ParseMode.INTERFACE_MODULE); + final Map builtinKinds = new HashMap<>(); + for (final PbsAst.TopDecl topDecl : file.topDecls()) { + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + builtinKinds.put(builtinTypeDecl.name(), PbsSemanticKind.BUILTIN_TYPE); + } + } + return builtinKinds; + } + + private PbsSemanticKind semanticKindForBarrelItem(final PbsAst.BarrelItem item) { + return switch (item) { + case PbsAst.BarrelStructItem ignored -> PbsSemanticKind.STRUCT; + case PbsAst.BarrelContractItem ignored -> PbsSemanticKind.CONTRACT; + case PbsAst.BarrelHostItem ignored -> PbsSemanticKind.HOST; + case PbsAst.BarrelErrorItem ignored -> PbsSemanticKind.ENUM; + case PbsAst.BarrelEnumItem ignored -> PbsSemanticKind.ENUM; + case PbsAst.BarrelServiceItem ignored -> PbsSemanticKind.SERVICE; + case PbsAst.BarrelGlobalItem ignored -> PbsSemanticKind.GLOBAL; + case PbsAst.BarrelConstItem ignored -> PbsSemanticKind.CONST; + case PbsAst.BarrelCallbackItem ignored -> PbsSemanticKind.CALLBACK; + case PbsAst.BarrelFunctionItem ignored -> null; + }; + } + + private static Map declarationKinds() { + final Map kinds = new EnumMap<>(PbsTokenKind.class); + kinds.put(PbsTokenKind.STRUCT, PbsSemanticKind.STRUCT); + kinds.put(PbsTokenKind.CONTRACT, PbsSemanticKind.CONTRACT); + kinds.put(PbsTokenKind.HOST, PbsSemanticKind.HOST); + kinds.put(PbsTokenKind.SERVICE, PbsSemanticKind.SERVICE); + kinds.put(PbsTokenKind.ENUM, PbsSemanticKind.ENUM); + kinds.put(PbsTokenKind.ERROR, PbsSemanticKind.ENUM); + kinds.put(PbsTokenKind.CALLBACK, PbsSemanticKind.CALLBACK); + kinds.put(PbsTokenKind.GLOBAL, PbsSemanticKind.GLOBAL); + kinds.put(PbsTokenKind.CONST, PbsSemanticKind.CONST); + kinds.put(PbsTokenKind.CTOR, PbsSemanticKind.CONSTRUCTOR); + kinds.put(PbsTokenKind.BUILTIN, PbsSemanticKind.BUILTIN_TYPE); + return Map.copyOf(kinds); + } + + private boolean isTypeReferenceContext(final PbsTokenKind previousKind) { + if (previousKind == null) { + return false; + } + if (previousKind != PbsTokenKind.COLON + && previousKind != PbsTokenKind.ARROW + && previousKind != PbsTokenKind.AS + && previousKind != PbsTokenKind.IMPLEMENTS) { + return false; + } + return true; + } + + private PbsTokenKind previousSignificantKind( + final ReadOnlyList tokens, + final int index) { + for (int cursor = index - 1; cursor >= 0; cursor -= 1) { + final PbsTokenKind kind = tokens.get(cursor).kind(); + if (kind == PbsTokenKind.COMMENT || kind == PbsTokenKind.EOF) { + continue; + } + return kind; + } + return null; + } + + private PbsTokenKind nextSignificantKind( + final ReadOnlyList tokens, + final int index) { + final Integer cursor = nextSignificantIndex(tokens, index); + return cursor == null ? null : tokens.get(cursor).kind(); + } + + private Integer nextSignificantIndex( + final ReadOnlyList tokens, + final int index) { + for (int cursor = index + 1; cursor < tokens.size(); cursor += 1) { + final PbsTokenKind kind = tokens.get(cursor).kind(); + if (kind == PbsTokenKind.COMMENT || kind == PbsTokenKind.EOF) { + continue; + } + return cursor; + } + return null; + } + + private boolean isLifecycleToken( + final ReadOnlyList tokens, + final int index, + final PbsToken token) { + if (!"Init".equals(token.lexeme()) && !"Frame".equals(token.lexeme())) { + return false; + } + if (index == 0 || index + 1 >= tokens.size()) { + return false; + } + return tokens.get(index - 1).kind() == PbsTokenKind.LEFT_BRACKET + && tokens.get(index + 1).kind() == PbsTokenKind.RIGHT_BRACKET; + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java index 0c577e4a..3d72d9a6 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/PbsSemanticKind.java @@ -22,6 +22,7 @@ public enum PbsSemanticKind { HOST("pbs-host"), BUILTIN_TYPE("pbs-builtin-type"), SERVICE("pbs-service"), + ASSET("pbs-asset"), ERROR("pbs-error"), ENUM("pbs-enum"), CALLBACK("pbs-callback"), @@ -51,12 +52,13 @@ public enum PbsSemanticKind { case COMMENT -> COMMENT; case STRING_LITERAL -> STRING; case INT_LITERAL, FLOAT_LITERAL, BOUNDED_LITERAL -> NUMBER; - case TRUE, FALSE, NONE -> LITERAL; + case TRUE, FALSE, NONE, SOME, OK, ERR -> LITERAL; + case VOID, OPTIONAL, RESULT -> BUILTIN_TYPE; case IMPORT, FROM, AS, SERVICE, HOST, FN, APPLY, BIND, NEW, IMPLEMENTS, USING, CTOR, DECLARE, LET, CONST, GLOBAL, STRUCT, CONTRACT, ERROR, ENUM, CALLBACK, BUILTIN, SELF, THIS, PUB, MUT, MOD, TYPE, IF, ELSE, SWITCH, DEFAULT, FOR, UNTIL, STEP, - WHILE, BREAK, CONTINUE, RETURN, VOID, OPTIONAL, RESULT, SOME, OK, ERR, HANDLE, - AND, OR, NOT, SPAWN, YIELD, SLEEP, MATCH -> KEYWORD; + WHILE, BREAK, CONTINUE, RETURN, HANDLE, AND, OR, NOT, SPAWN, YIELD, SLEEP, + MATCH -> KEYWORD; case PLUS, MINUS, STAR, SLASH, PERCENT, BANG, PLUS_EQUAL, MINUS_EQUAL, STAR_EQUAL, SLASH_EQUAL, PERCENT_EQUAL, BANG_EQUAL, EQUAL, EQUAL_EQUAL, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL, AND_AND, OR_OR, ARROW, QUESTION -> OPERATOR; diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialCompletionCandidate.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialCompletionCandidate.java new file mode 100644 index 00000000..4001004b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialCompletionCandidate.java @@ -0,0 +1,30 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.Objects; + +public record PbsEditorialCompletionCandidate( + String label, + PbsEditorialSymbolKind kind, + String detail, + String origin) { + public PbsEditorialCompletionCandidate { + label = requireText(label, "label"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + origin = normalize(origin); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = normalize(value); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java new file mode 100644 index 00000000..43abee2b --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialResolvedSymbol.java @@ -0,0 +1,35 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.List; +import java.util.Objects; + +public record PbsEditorialResolvedSymbol( + String displayName, + PbsEditorialSymbolKind kind, + String detail, + String origin, + List signatures, + String documentation) { + public PbsEditorialResolvedSymbol { + displayName = requireText(displayName, "displayName"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + origin = normalize(origin); + signatures = List.copyOf(Objects.requireNonNull(signatures, "signatures")); + documentation = normalize(documentation); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = normalize(value); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignature.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignature.java new file mode 100644 index 00000000..43bb9a89 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignature.java @@ -0,0 +1,27 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.List; +import java.util.Objects; + +public record PbsEditorialSignature( + String callableName, + String label, + List parameterLabels, + String returnLabel) { + public PbsEditorialSignature { + callableName = requireText(callableName, "callableName"); + label = requireText(label, "label"); + parameterLabels = List.copyOf(Objects.requireNonNull(parameterLabels, "parameterLabels")); + returnLabel = requireText(returnLabel, "returnLabel"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignatureHelp.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignatureHelp.java new file mode 100644 index 00000000..e632e2ed --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSignatureHelp.java @@ -0,0 +1,22 @@ +package p.studio.compiler.pbs.semantics; + +import java.util.List; +import java.util.Objects; + +public record PbsEditorialSignatureHelp( + List signatures, + int activeSignature, + int activeParameter) { + public PbsEditorialSignatureHelp { + signatures = List.copyOf(Objects.requireNonNull(signatures, "signatures")); + if (signatures.isEmpty()) { + throw new IllegalArgumentException("signatures must not be empty"); + } + if (activeSignature < 0 || activeSignature >= signatures.size()) { + throw new IllegalArgumentException("activeSignature must reference one of the provided signatures"); + } + if (activeParameter < 0) { + throw new IllegalArgumentException("activeParameter must be >= 0"); + } + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java new file mode 100644 index 00000000..dc4c839c --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportService.java @@ -0,0 +1,1303 @@ +package p.studio.compiler.pbs.semantics; + +import p.studio.compiler.messages.FESurfaceContext; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.lexer.PbsToken; +import p.studio.compiler.pbs.lexer.PbsTokenKind; +import p.studio.compiler.source.Span; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.*; + +import static p.studio.compiler.pbs.semantics.PbsFlowSemanticSupport.*; + +public final class PbsEditorialSupportService { + private static final Set BUILTIN_TYPE_NAMES = Set.of("int", "float", "bool", "str", "string", "i32", "i64", "f32", "f64"); + private static final List KEYWORDS = List.of( + "fn", "declare", "let", "const", "global", "struct", "service", "host", "contract", "enum", "error", "callback", + "if", "else", "switch", "for", "until", "step", "while", "break", "continue", "return", + "new", "bind", "apply", "handle", "some", "none", "ok", "err", "true", "false"); + + public List completion( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return completion(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public List completion( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final var context = buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + final var scopeState = context.scopeStateAt(offset); + final Integer previousTokenIndex = context.previousSignificantTokenIndexAt(offset); + if (previousTokenIndex != null && context.token(previousTokenIndex).kind() == PbsTokenKind.DOT) { + final TypeView receiverType = context.resolveReceiverTypeBeforeDot(previousTokenIndex, scopeState); + return context.memberCompletions(receiverType); + } + return context.generalCompletions(scopeState); + } + + public Optional hover( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return hover(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public Optional hover( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final var context = buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + final Integer tokenIndex = context.identifierTokenIndexAt(offset); + if (tokenIndex == null) { + return Optional.empty(); + } + final var scopeState = context.scopeStateAt(offset); + final var token = context.token(tokenIndex); + final Integer previousTokenIndex = context.previousSignificantTokenIndex(tokenIndex); + if (previousTokenIndex != null && context.token(previousTokenIndex).kind() == PbsTokenKind.DOT) { + final TypeView receiverType = context.resolveReceiverTypeBeforeDot(previousTokenIndex, scopeState); + return context.resolveMemberHover(receiverType, token.lexeme()); + } + return context.resolveSymbolHover(token.lexeme(), tokenIndex, scopeState); + } + + public Optional signatureHelp( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final int offset) { + return signatureHelp(sourceText, ast, supplementalTopDecls, FESurfaceContext.empty(), offset); + } + + public Optional signatureHelp( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext, + final int offset) { + final var context = buildContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + final var callSite = context.callSiteAt(offset); + if (callSite == null) { + return Optional.empty(); + } + final var scopeState = context.scopeStateAt(offset); + final List signatures = switch (callSite.kind()) { + case FUNCTION -> context.resolveCallableSignatures(callSite.name(), scopeState); + case MEMBER -> { + final TypeView receiverType = context.resolveReceiverTypeBeforeDot(callSite.memberDotTokenIndex(), scopeState); + yield context.resolveMemberSignatures(receiverType, callSite.name()); + } + case CONSTRUCTOR -> context.resolveConstructorSignatures(callSite.name()); + }; + if (signatures.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new PbsEditorialSignatureHelp(signatures, 0, callSite.activeParameter())); + } + + private DocumentContext buildContext( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext) { + return new DocumentContext(sourceText, ast, supplementalTopDecls, feSurfaceContext); + } + + private static final class DocumentContext { + private final String sourceText; + private final PbsAst.File ast; + private final ReadOnlyList editorialSupplementalTopDecls; + private final ReadOnlyList tokens; + private final Model model; + private final PbsFlowTypeOps typeOps = new PbsFlowTypeOps(); + private final PbsFlowExpressionAnalyzer expressionAnalyzer = new PbsFlowExpressionAnalyzer(typeOps); + private final PbsInlineHintSurfaceFormatter typeFormatter = new PbsInlineHintSurfaceFormatter(); + private final Map topLevelSymbols; + private final Map importedSymbols; + private final Map supplementalSymbols; + private final Map aliasedTypeRefs; + + private DocumentContext( + final String sourceText, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final FESurfaceContext feSurfaceContext) { + this.sourceText = sourceText == null ? "" : sourceText; + this.ast = Objects.requireNonNull(ast, "ast"); + final ArrayList expandedSupplementals = new ArrayList<>(supplementalTopDecls == null + ? List.of() + : supplementalTopDecls.asList()); + this.importedSymbols = buildImportedSymbols(expandedSupplementals); + this.editorialSupplementalTopDecls = ReadOnlyList.wrap(expandedSupplementals); + this.tokens = PbsLexer.lex(this.sourceText, FileId.none(), DiagnosticSink.empty()); + this.model = Model.from( + ast, + editorialSupplementalTopDecls, + feSurfaceContext == null ? FESurfaceContext.empty() : feSurfaceContext, + DiagnosticSink.empty()); + this.topLevelSymbols = buildTopLevelSymbols(); + this.supplementalSymbols = buildSupplementalSymbols(); + this.aliasedTypeRefs = buildAliasedTypeRefs(); + } + + private Map buildTopLevelSymbols() { + final Map symbols = new LinkedHashMap<>(); + for (final var topDecl : ast.topDecls()) { + final String name = topDeclName(topDecl); + if (name == null || symbols.containsKey(name)) { + continue; + } + symbols.put(name, new TopDeclSymbol(name, topDecl, "current module")); + } + return Map.copyOf(symbols); + } + + private Map buildImportedSymbols(final ArrayList expandedSupplementals) { + final Map symbols = new LinkedHashMap<>(); + for (final var importDecl : ast.imports()) { + final String moduleLabel = formatModuleRef(importDecl.moduleRef()); + for (final var item : importDecl.items()) { + final String localName = item.alias() == null || item.alias().isBlank() + ? item.name() + : item.alias(); + final PbsAst.TopDecl target = findSupplementalTopDecl(item.name(), expandedSupplementals); + if (target == null) { + continue; + } + symbols.put(localName, new TopDeclSymbol(localName, cloneTopDeclWithName(target, localName), moduleLabel)); + if (!localName.equals(item.name())) { + expandedSupplementals.add(cloneTopDeclWithName(target, localName)); + } + } + } + return Map.copyOf(symbols); + } + + private Map buildSupplementalSymbols() { + final Map symbols = new LinkedHashMap<>(); + for (final var topDecl : editorialSupplementalTopDecls) { + final String name = topDeclName(topDecl); + if (name == null || importedSymbols.containsKey(name) || symbols.containsKey(name)) { + continue; + } + symbols.put(name, new TopDeclSymbol(name, topDecl, "imported context")); + } + return Map.copyOf(symbols); + } + + private Map buildAliasedTypeRefs() { + final Map typeRefs = new HashMap<>(); + for (final var entry : importedSymbols.entrySet()) { + if (entry.getValue().decl() instanceof PbsAst.StructDecl structDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(structDecl.name(), structDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(builtinTypeDecl.name(), builtinTypeDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.ServiceDecl serviceDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(serviceDecl.name(), serviceDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.HostDecl hostDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(hostDecl.name(), hostDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.ContractDecl contractDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(contractDecl.name(), contractDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.EnumDecl enumDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(enumDecl.name(), enumDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.ErrorDecl errorDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(errorDecl.name(), errorDecl.span())); + } else if (entry.getValue().decl() instanceof PbsAst.CallbackDecl callbackDecl) { + typeRefs.put(entry.getKey(), PbsAst.TypeRef.simple(callbackDecl.name(), callbackDecl.span())); + } + } + return Map.copyOf(typeRefs); + } + + private ScopeState scopeStateAt(final int offset) { + final CallableFrame callableFrame = callableFrameAt(offset); + if (callableFrame == null) { + return new ScopeState(new Scope(), null, TypeView.unit(), null, Set.of()); + } + final Scope scope = new Scope(); + for (final var parameter : callableFrame.parameters()) { + scope.bind(parameter.name(), typeOps.typeFromTypeRef(parameter.typeRef(), model, callableFrame.receiverType()), true); + } + final Scope resolvedScope = populateScopeUntilOffset(callableFrame.body(), scope, callableFrame, offset); + return new ScopeState( + resolvedScope, + callableFrame.receiverType(), + callableFrame.returnType(), + callableFrame.resultErrorName(), + callableFrame.parameterNames()); + } + + private Scope populateScopeUntilOffset( + final PbsAst.Block block, + final Scope seedScope, + final CallableFrame frame, + final int offset) { + final Scope scope = seedScope.copy(); + for (final var statement : block.statements()) { + if (statement.span().getStart() >= offset) { + break; + } + if (statement.span().contains(offset)) { + if (statement instanceof PbsAst.IfStatement ifStatement) { + if (ifStatement.thenBlock().span().contains(offset)) { + return populateScopeUntilOffset(ifStatement.thenBlock(), scope, frame, offset); + } + if (ifStatement.elseBlock() != null && ifStatement.elseBlock().span().contains(offset)) { + return populateScopeUntilOffset(ifStatement.elseBlock(), scope, frame, offset); + } + if (ifStatement.elseIf() != null && ifStatement.elseIf().span().contains(offset)) { + final var nestedBlock = new PbsAst.Block(ReadOnlyList.wrap(List.of(ifStatement.elseIf())), null, ifStatement.elseIf().span()); + return populateScopeUntilOffset(nestedBlock, scope, frame, offset); + } + } else if (statement instanceof PbsAst.ForStatement forStatement) { + if (forStatement.body().span().contains(offset)) { + final Scope loopScope = scope.copy(); + loopScope.bind(forStatement.iteratorName(), typeOps.typeFromTypeRef(forStatement.iteratorType(), model, frame.receiverType())); + return populateScopeUntilOffset(forStatement.body(), loopScope, frame, offset); + } + } else if (statement instanceof PbsAst.WhileStatement whileStatement && whileStatement.body().span().contains(offset)) { + return populateScopeUntilOffset(whileStatement.body(), scope, frame, offset); + } + return scope; + } + applyStatementEffects(statement, scope, frame); + } + return scope; + } + + private void applyStatementEffects( + final PbsAst.Statement statement, + final Scope scope, + final CallableFrame frame) { + if (statement instanceof PbsAst.LetStatement letStatement) { + final PbsAst.TypeRef explicitType = aliasExpandedTypeRef(letStatement.explicitType()); + final TypeView expectedType = explicitType == null + ? null + : typeOps.typeFromTypeRef(explicitType, model, frame.receiverType()); + final TypeView inferredType = analyzeExpressionType( + letStatement.initializer(), + scope, + expectedType, + frame.returnType(), + frame.resultErrorName(), + frame.receiverType()); + final TypeView resolvedType = expectedType == null ? inferredType : expectedType; + scope.bind(letStatement.name(), resolvedType, !letStatement.isConst()); + } + } + + private TypeView analyzeExpressionType( + final PbsAst.Expression expression, + final Scope scope, + final TypeView expectedType, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType) { + return expressionAnalyzer.analyzeExpression( + expression, + scope, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + DiagnosticSink.empty(), + ExprUse.VALUE, + true, + this::analyzeNestedBlock).type(); + } + + private TypeView analyzeNestedBlock( + final PbsAst.Block block, + final Scope outerScope, + final TypeView returnType, + final String resultErrorName, + final TypeView receiverType, + final Model ignoredModel, + final DiagnosticSink diagnostics, + final boolean valueContext) { + final Scope scoped = outerScope.copy(); + for (final var statement : block.statements()) { + if (statement instanceof PbsAst.LetStatement letStatement) { + final PbsAst.TypeRef explicitType = aliasExpandedTypeRef(letStatement.explicitType()); + final TypeView expectedType = explicitType == null + ? null + : typeOps.typeFromTypeRef(explicitType, model, receiverType); + final TypeView inferredType = expressionAnalyzer.analyzeExpression( + letStatement.initializer(), + scoped, + expectedType, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + true, + this::analyzeNestedBlock).type(); + scoped.bind(letStatement.name(), expectedType == null ? inferredType : expectedType, !letStatement.isConst()); + } + } + if (block.tailExpression() == null) { + return TypeView.unit(); + } + return expressionAnalyzer.analyzeExpression( + block.tailExpression(), + scoped, + null, + returnType, + resultErrorName, + receiverType, + model, + diagnostics, + ExprUse.VALUE, + valueContext, + this::analyzeNestedBlock).type(); + } + + private CallableFrame callableFrameAt(final int offset) { + for (final var topDecl : ast.topDecls()) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl && functionDecl.span().contains(offset)) { + return new CallableFrame( + functionDecl.parameters(), + functionDecl.body(), + null, + typeOps.callableReturnType( + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + model), + errorName(functionDecl.resultErrorType())); + } + if (topDecl instanceof PbsAst.StructDecl structDecl && structDecl.span().contains(offset)) { + final TypeView receiverType = TypeView.struct(structDecl.name()); + for (final var method : structDecl.methods()) { + if (method.span().contains(offset)) { + return new CallableFrame( + method.parameters(), + method.body(), + receiverType, + typeOps.callableReturnType( + method.returnKind(), + method.returnType(), + method.resultErrorType(), + model), + errorName(method.resultErrorType())); + } + } + for (final var ctor : structDecl.ctors()) { + if (ctor.span().contains(offset)) { + return new CallableFrame( + ctor.parameters(), + ctor.body(), + receiverType, + TypeView.unit(), + null); + } + } + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl && serviceDecl.span().contains(offset)) { + final TypeView receiverType = TypeView.service(serviceDecl.name()); + for (final var method : serviceDecl.methods()) { + if (method.span().contains(offset)) { + return new CallableFrame( + method.parameters(), + method.body(), + receiverType, + typeOps.callableReturnType( + method.returnKind(), + method.returnType(), + method.resultErrorType(), + model), + errorName(method.resultErrorType())); + } + } + } + } + return null; + } + + private List generalCompletions(final ScopeState scopeState) { + final Map completions = new LinkedHashMap<>(); + for (final var keyword : KEYWORDS) { + completions.put(keyword, new PbsEditorialCompletionCandidate(keyword, PbsEditorialSymbolKind.KEYWORD, "", "")); + } + for (final var parameterName : scopeState.parameterNames()) { + final TypeView type = scopeState.scope().resolve(parameterName); + completions.put(parameterName, new PbsEditorialCompletionCandidate( + parameterName, + PbsEditorialSymbolKind.PARAMETER, + formatType(type), + "local parameter")); + } + for (final var entry : scopeState.scope().entries().entrySet()) { + if (scopeState.parameterNames().contains(entry.getKey())) { + continue; + } + completions.put(entry.getKey(), new PbsEditorialCompletionCandidate( + entry.getKey(), + PbsEditorialSymbolKind.LOCAL, + formatType(entry.getValue().type()), + entry.getValue().mutable() ? "local mutable binding" : "local const binding")); + } + for (final var entry : importedSymbols.entrySet()) { + completions.putIfAbsent(entry.getKey(), completionFromTopDecl(entry.getValue())); + } + for (final var entry : supplementalSymbols.entrySet()) { + completions.putIfAbsent(entry.getKey(), completionFromTopDecl(entry.getValue())); + } + for (final var entry : topLevelSymbols.entrySet()) { + completions.putIfAbsent(entry.getKey(), completionFromTopDecl(entry.getValue())); + } + return List.copyOf(completions.values()); + } + + private List memberCompletions(final TypeView receiverType) { + if (receiverType == null) { + return List.of(); + } + final Map completions = new LinkedHashMap<>(); + if (receiverType.kind() == Kind.STRUCT) { + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo != null) { + for (final var field : structInfo.fields().entrySet()) { + completions.put(field.getKey(), new PbsEditorialCompletionCandidate( + field.getKey(), + PbsEditorialSymbolKind.FIELD, + formatType(field.getValue().type()), + receiverType.name())); + } + for (final var method : structInfo.methods().entrySet()) { + completions.put(method.getKey(), new PbsEditorialCompletionCandidate( + method.getKey(), + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(method.getValue()), + receiverType.name())); + } + } + } else if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { + final Map> methods = receiverType.kind() == Kind.SERVICE + ? Optional.ofNullable(model.services.get(receiverType.name())).map(ServiceInfo::methods).orElse(Map.of()) + : Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of()); + for (final var method : methods.entrySet()) { + completions.put(method.getKey(), new PbsEditorialCompletionCandidate( + method.getKey(), + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(method.getValue()), + receiverType.name())); + } + } + return List.copyOf(completions.values()); + } + + private Optional resolveSymbolHover( + final String symbolName, + final int tokenIndex, + final ScopeState scopeState) { + if (scopeState.parameterNames().contains(symbolName)) { + final TypeView type = scopeState.scope().resolve(symbolName); + return Optional.of(new PbsEditorialResolvedSymbol( + symbolName, + PbsEditorialSymbolKind.PARAMETER, + formatType(type), + "local parameter", + List.of(), + "")); + } + final Scope.LocalSymbol localSymbol = scopeState.scope().entries().get(symbolName); + if (localSymbol != null) { + return Optional.of(new PbsEditorialResolvedSymbol( + symbolName, + PbsEditorialSymbolKind.LOCAL, + formatType(localSymbol.type()), + localSymbol.mutable() ? "local mutable binding" : "local const binding", + List.of(), + "")); + } + final Integer previousTokenIndex = previousSignificantTokenIndex(tokenIndex); + if (previousTokenIndex != null && token(previousTokenIndex).kind() == PbsTokenKind.NEW) { + final TopDeclSymbol topDeclSymbol = resolveTopDeclSymbol(symbolName); + if (topDeclSymbol != null) { + return Optional.of(constructorSymbolFromTopDecl(topDeclSymbol, resolveConstructorSignatures(symbolName))); + } + } + final TopDeclSymbol importedSymbol = importedSymbols.get(symbolName); + if (importedSymbol != null) { + return Optional.of(symbolFromTopDecl(importedSymbol, signaturesForTopDecl(importedSymbol.decl()))); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(symbolName); + if (supplementalSymbol != null) { + return Optional.of(symbolFromTopDecl(supplementalSymbol, signaturesForTopDecl(supplementalSymbol.decl()))); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(symbolName); + if (topLevelSymbol != null) { + return Optional.of(symbolFromTopDecl(topLevelSymbol, signaturesForTopDecl(topLevelSymbol.decl()))); + } + if (BUILTIN_TYPE_NAMES.contains(symbolName)) { + return Optional.of(new PbsEditorialResolvedSymbol( + symbolName, + PbsEditorialSymbolKind.BUILTIN_TYPE, + "builtin scalar type", + "", + List.of(), + "")); + } + return Optional.empty(); + } + + private Optional resolveMemberHover( + final TypeView receiverType, + final String memberName) { + if (receiverType == null) { + return Optional.empty(); + } + if (receiverType.kind() == Kind.STRUCT) { + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo == null) { + return Optional.empty(); + } + final StructFieldInfo field = structInfo.fields().get(memberName); + if (field != null) { + return Optional.of(new PbsEditorialResolvedSymbol( + memberName, + PbsEditorialSymbolKind.FIELD, + formatType(field.type()), + receiverType.name(), + List.of(), + "")); + } + final List methods = structInfo.methods().get(memberName); + if (methods != null && !methods.isEmpty()) { + return Optional.of(new PbsEditorialResolvedSymbol( + memberName, + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(methods), + receiverType.name(), + signaturesFromCallables(methods), + "")); + } + } + if (receiverType.kind() == Kind.SERVICE || receiverType.kind() == Kind.CONTRACT) { + final Map> methods = receiverType.kind() == Kind.SERVICE + ? Optional.ofNullable(model.services.get(receiverType.name())).map(ServiceInfo::methods).orElse(Map.of()) + : Optional.ofNullable(model.contracts.get(receiverType.name())).map(ContractInfo::methods).orElse(Map.of()); + final List signatures = methods.get(memberName); + if (signatures != null && !signatures.isEmpty()) { + return Optional.of(new PbsEditorialResolvedSymbol( + memberName, + PbsEditorialSymbolKind.METHOD, + formatCallableDetails(signatures), + receiverType.name(), + signaturesFromCallables(signatures), + "")); + } + } + return Optional.empty(); + } + + private List resolveCallableSignatures( + final String callableName, + final ScopeState scopeState) { + final TopDeclSymbol importedSymbol = importedSymbols.get(callableName); + if (importedSymbol != null) { + return signaturesForTopDecl(importedSymbol.decl()); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(callableName); + if (topLevelSymbol != null) { + return signaturesForTopDecl(topLevelSymbol.decl()); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(callableName); + if (supplementalSymbol != null) { + return signaturesForTopDecl(supplementalSymbol.decl()); + } + return signaturesFromCallables(model.topLevelCallables.getOrDefault(callableName, List.of())); + } + + private List resolveMemberSignatures( + final TypeView receiverType, + final String memberName) { + return resolveMemberHover(receiverType, memberName) + .map(PbsEditorialResolvedSymbol::signatures) + .orElse(List.of()); + } + + private List resolveConstructorSignatures(final String typeName) { + final TopDeclSymbol importedSymbol = importedSymbols.get(typeName); + if (importedSymbol != null) { + return constructorSignaturesForTopDecl(importedSymbol.decl()); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(typeName); + if (topLevelSymbol != null) { + return constructorSignaturesForTopDecl(topLevelSymbol.decl()); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(typeName); + if (supplementalSymbol != null) { + return constructorSignaturesForTopDecl(supplementalSymbol.decl()); + } + return List.of(); + } + + private PbsEditorialResolvedSymbol symbolFromTopDecl( + final TopDeclSymbol topDeclSymbol, + final List signatures) { + final PbsEditorialSymbolKind kind = symbolKind(topDeclSymbol.decl()); + final String detail = signatures.isEmpty() + ? kind.name().toLowerCase(Locale.ROOT).replace('_', ' ') + : signatures.getFirst().label(); + return new PbsEditorialResolvedSymbol( + topDeclSymbol.localName(), + kind, + detail, + topDeclSymbol.origin(), + signatures, + ""); + } + + private PbsEditorialResolvedSymbol constructorSymbolFromTopDecl( + final TopDeclSymbol topDeclSymbol, + final List signatures) { + final String detail = signatures.isEmpty() + ? "constructor" + : signatures.getFirst().label(); + return new PbsEditorialResolvedSymbol( + topDeclSymbol.localName(), + PbsEditorialSymbolKind.CONSTRUCTOR, + detail, + topDeclSymbol.origin(), + signatures, + ""); + } + + private PbsEditorialCompletionCandidate completionFromTopDecl(final TopDeclSymbol topDeclSymbol) { + final List signatures = signaturesForTopDecl(topDeclSymbol.decl()); + final String detail = signatures.isEmpty() + ? symbolKind(topDeclSymbol.decl()).name().toLowerCase(Locale.ROOT).replace('_', ' ') + : signatures.getFirst().label(); + return new PbsEditorialCompletionCandidate( + topDeclSymbol.localName(), + symbolKind(topDeclSymbol.decl()), + detail, + topDeclSymbol.origin()); + } + + private List signaturesForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return List.of(signatureFromFunction(functionDecl.name(), functionDecl.parameters(), functionDecl.returnKind(), functionDecl.returnType(), functionDecl.resultErrorType())); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return List.of(signatureFromFunction(callbackDecl.name(), callbackDecl.parameters(), callbackDecl.returnKind(), callbackDecl.returnType(), callbackDecl.resultErrorType())); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return serviceDecl.methods().stream() + .map(method -> signatureFromFunction(method.name(), method.parameters(), method.returnKind(), method.returnType(), method.resultErrorType())) + .toList(); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return hostDecl.signatures().stream() + .map(signature -> signatureFromFunction(signature.name(), signature.parameters(), signature.returnKind(), signature.returnType(), signature.resultErrorType())) + .toList(); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return contractDecl.signatures().stream() + .map(signature -> signatureFromFunction(signature.name(), signature.parameters(), signature.returnKind(), signature.returnType(), signature.resultErrorType())) + .toList(); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return builtinTypeDecl.signatures().stream() + .map(signature -> signatureFromFunction(signature.name(), signature.parameters(), signature.returnKind(), signature.returnType(), signature.resultErrorType())) + .toList(); + } + return List.of(); + } + + private List constructorSignaturesForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + if (structDecl.ctors().isEmpty()) { + return List.of(new PbsEditorialSignature(structDecl.name(), structDecl.name() + "()", List.of(), "unit")); + } + return structDecl.ctors().stream() + .map(ctor -> signatureFromCtor(structDecl.name(), ctor)) + .toList(); + } + return List.of(); + } + + private List signaturesFromCallables(final List callables) { + if (callables == null || callables.isEmpty()) { + return List.of(); + } + final ArrayList signatures = new ArrayList<>(callables.size()); + for (final var callable : callables) { + final ArrayList parameterLabels = new ArrayList<>(callable.inputTypes().size()); + for (int index = 0; index < callable.inputTypes().size(); index += 1) { + parameterLabels.add("arg" + index + ": " + formatType(callable.inputTypes().get(index))); + } + final String label = callable.name() + "(" + String.join(", ", parameterLabels) + ") -> " + formatType(callable.outputType()); + signatures.add(new PbsEditorialSignature(callable.name(), label, parameterLabels, formatType(callable.outputType()))); + } + return List.copyOf(signatures); + } + + private PbsEditorialSignature signatureFromFunction( + final String callableName, + final ReadOnlyList parameters, + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + final ArrayList parameterLabels = new ArrayList<>(parameters.size()); + for (final var parameter : parameters) { + parameterLabels.add(parameter.name() + ": " + formatTypeRef(aliasExpandedTypeRef(parameter.typeRef()))); + } + final String returnLabel = formatReturnType(returnKind, aliasExpandedTypeRef(returnType), aliasExpandedTypeRef(resultErrorType)); + return new PbsEditorialSignature( + callableName, + callableName + "(" + String.join(", ", parameterLabels) + ") -> " + returnLabel, + parameterLabels, + returnLabel); + } + + private PbsEditorialSignature signatureFromCtor( + final String ownerTypeName, + final PbsAst.CtorDecl ctorDecl) { + final ArrayList parameterLabels = new ArrayList<>(ctorDecl.parameters().size()); + for (final var parameter : ctorDecl.parameters()) { + parameterLabels.add(parameter.name() + ": " + formatTypeRef(aliasExpandedTypeRef(parameter.typeRef()))); + } + return new PbsEditorialSignature( + ownerTypeName, + ownerTypeName + "(" + String.join(", ", parameterLabels) + ")", + parameterLabels, + "unit"); + } + + private String formatCallableDetails(final List callables) { + if (callables == null || callables.isEmpty()) { + return ""; + } + return signaturesFromCallables(callables).getFirst().label(); + } + + private String formatType(final TypeView type) { + final String formatted = typeFormatter.format(type); + return formatted == null ? "unknown" : formatted; + } + + private String formatTypeRef(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return "unit"; + } + return switch (typeRef.kind()) { + case UNIT -> "unit"; + case SELF -> "Self"; + case SIMPLE -> typeRef.name(); + case OPTIONAL -> formatTypeRef(typeRef.inner()) + "?"; + case GROUP -> formatTypeRef(typeRef.inner()); + case NAMED_TUPLE -> { + final ArrayList fields = new ArrayList<>(typeRef.fields().size()); + for (final var field : typeRef.fields()) { + fields.add(field.label() + ": " + formatTypeRef(field.typeRef())); + } + yield "(" + String.join(", ", fields) + ")"; + } + case ERROR -> "unknown"; + }; + } + + private String formatReturnType( + final PbsAst.ReturnKind returnKind, + final PbsAst.TypeRef returnType, + final PbsAst.TypeRef resultErrorType) { + return switch (returnKind) { + case INFERRED_UNIT, EXPLICIT_UNIT -> "unit"; + case PLAIN -> formatTypeRef(returnType); + case RESULT -> "result<" + formatTypeRef(resultErrorType) + ", " + formatTypeRef(returnType) + ">"; + }; + } + + private TypeView resolveReceiverTypeBeforeDot( + final int dotTokenIndex, + final ScopeState scopeState) { + final List segments = resolveChainSegmentsEndingAt(dotTokenIndex - 1); + if (segments.isEmpty()) { + return TypeView.unknown(); + } + TypeView current = resolveBaseChainType(segments.getFirst(), scopeState); + for (int index = 1; index < segments.size(); index += 1) { + current = resolveChainMemberType(current, segments.get(index)); + } + return current; + } + + private TypeView resolveBaseChainType( + final String baseName, + final ScopeState scopeState) { + if ("this".equals(baseName)) { + return scopeState.receiverType() == null ? TypeView.unknown() : scopeState.receiverType(); + } + final TypeView localType = scopeState.scope().resolve(baseName); + if (localType != null) { + return localType; + } + final TopDeclSymbol importedSymbol = importedSymbols.get(baseName); + if (importedSymbol != null) { + return typeForTopDecl(importedSymbol.decl()); + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(baseName); + if (topLevelSymbol != null) { + return typeForTopDecl(topLevelSymbol.decl()); + } + final TopDeclSymbol supplementalSymbol = supplementalSymbols.get(baseName); + if (supplementalSymbol != null) { + return typeForTopDecl(supplementalSymbol.decl()); + } + if ("assets".equals(baseName)) { + return TypeView.assetNamespace("assets"); + } + return TypeView.unknown(); + } + + private TypeView resolveChainMemberType( + final TypeView receiverType, + final String memberName) { + if (receiverType.kind() == Kind.STRUCT) { + final StructInfo structInfo = model.structs.get(receiverType.name()); + if (structInfo == null) { + return TypeView.unknown(); + } + final StructFieldInfo field = structInfo.fields().get(memberName); + return field == null ? TypeView.unknown() : field.type(); + } + if (receiverType.kind() == Kind.ASSET_NAMESPACE) { + final String candidate = receiverType.name() + "." + memberName; + if (model.resolveAssetId(candidate) != null) { + return TypeView.addressable(candidate); + } + if (model.isAssetNamespace(candidate)) { + return TypeView.assetNamespace(candidate); + } + } + return TypeView.unknown(); + } + + private TypeView typeForTopDecl(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return TypeView.struct(structDecl.name()); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return TypeView.struct(builtinTypeDecl.name()); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return TypeView.service(serviceDecl.name()); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return TypeView.service(hostDecl.name()); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return TypeView.contract(contractDecl.name()); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return TypeView.enumType(enumDecl.name()); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return TypeView.error(errorDecl.name()); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return typeOps.typeFromTypeRef(PbsAst.TypeRef.simple(callbackDecl.name(), callbackDecl.span()), model, null); + } + return TypeView.unknown(); + } + + private CallSite callSiteAt(final int offset) { + int depth = 0; + int activeParameter = 0; + for (int index = significantTokenCount() - 1; index >= 0; index -= 1) { + final PbsToken token = significantToken(index); + if (token.start() >= offset) { + continue; + } + if (token.kind() == PbsTokenKind.RIGHT_PAREN) { + depth += 1; + continue; + } + if (token.kind() == PbsTokenKind.LEFT_PAREN) { + if (depth == 0) { + final Integer calleeIndex = previousSignificantTokenIndex(tokenIndex(token)); + if (calleeIndex == null) { + return null; + } + final PbsToken calleeToken = token(calleeIndex); + if (calleeToken.kind() != PbsTokenKind.IDENTIFIER) { + return null; + } + final Integer maybeDotIndex = previousSignificantTokenIndex(calleeIndex); + if (maybeDotIndex != null && token(maybeDotIndex).kind() == PbsTokenKind.DOT) { + return new CallSite(CallSiteKind.MEMBER, calleeToken.lexeme(), activeParameter, maybeDotIndex); + } + if (maybeDotIndex != null && token(maybeDotIndex).kind() == PbsTokenKind.NEW) { + return new CallSite(CallSiteKind.CONSTRUCTOR, calleeToken.lexeme(), activeParameter, -1); + } + return new CallSite(CallSiteKind.FUNCTION, calleeToken.lexeme(), activeParameter, -1); + } + depth -= 1; + continue; + } + if (depth == 0 && token.kind() == PbsTokenKind.COMMA) { + activeParameter += 1; + } + } + return null; + } + + private Integer identifierTokenIndexAt(final int offset) { + for (int index = 0; index < tokens.size(); index += 1) { + final PbsToken token = tokens.get(index); + if (token.kind() == PbsTokenKind.IDENTIFIER && token.start() <= offset && token.end() > offset) { + return index; + } + } + return null; + } + + private Integer previousSignificantTokenIndexAt(final int offset) { + for (int index = tokens.size() - 1; index >= 0; index -= 1) { + final PbsToken token = tokens.get(index); + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + continue; + } + if (token.end() <= offset) { + return index; + } + } + return null; + } + + private Integer previousSignificantTokenIndex(final int index) { + for (int cursor = index - 1; cursor >= 0; cursor -= 1) { + final PbsToken token = tokens.get(cursor); + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + continue; + } + return cursor; + } + return null; + } + + private List resolveChainSegmentsEndingAt(final int endTokenIndex) { + final ArrayList reversedSegments = new ArrayList<>(); + int cursor = endTokenIndex; + boolean expectIdentifier = true; + while (cursor >= 0) { + final PbsToken token = tokens.get(cursor); + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + cursor -= 1; + continue; + } + if (expectIdentifier) { + if (token.kind() == PbsTokenKind.IDENTIFIER) { + reversedSegments.add(token.lexeme()); + expectIdentifier = false; + cursor -= 1; + continue; + } + if (token.kind() == PbsTokenKind.THIS) { + reversedSegments.add("this"); + } + break; + } + if (token.kind() != PbsTokenKind.DOT) { + break; + } + expectIdentifier = true; + cursor -= 1; + } + Collections.reverse(reversedSegments); + return List.copyOf(reversedSegments); + } + + private int tokenIndex(final PbsToken needle) { + for (int index = 0; index < tokens.size(); index += 1) { + if (tokens.get(index) == needle) { + return index; + } + } + return -1; + } + + private int significantTokenCount() { + int count = 0; + for (final var token : tokens) { + if (token.kind() != PbsTokenKind.COMMENT && token.kind() != PbsTokenKind.EOF) { + count += 1; + } + } + return count; + } + + private PbsToken significantToken(final int significantIndex) { + int cursor = 0; + for (final var token : tokens) { + if (token.kind() == PbsTokenKind.COMMENT || token.kind() == PbsTokenKind.EOF) { + continue; + } + if (cursor == significantIndex) { + return token; + } + cursor += 1; + } + throw new IndexOutOfBoundsException("No significant token at index " + significantIndex); + } + + private PbsToken token(final int index) { + return tokens.get(index); + } + + private PbsAst.TypeRef aliasExpandedTypeRef(final PbsAst.TypeRef typeRef) { + if (typeRef == null) { + return null; + } + return switch (typeRef.kind()) { + case SIMPLE -> aliasedTypeRefs.getOrDefault(typeRef.name(), typeRef); + case OPTIONAL -> PbsAst.TypeRef.optional(aliasExpandedTypeRef(typeRef.inner()), typeRef.span()); + case GROUP -> PbsAst.TypeRef.group(aliasExpandedTypeRef(typeRef.inner()), typeRef.span()); + case NAMED_TUPLE -> { + final ArrayList fields = new ArrayList<>(typeRef.fields().size()); + for (final var field : typeRef.fields()) { + fields.add(new PbsAst.NamedTypeField(field.label(), aliasExpandedTypeRef(field.typeRef()), field.span())); + } + yield new PbsAst.TypeRef(PbsAst.TypeRefKind.NAMED_TUPLE, typeRef.name(), ReadOnlyList.wrap(fields), null, typeRef.span()); + } + case UNIT, SELF, ERROR -> typeRef; + }; + } + + private PbsAst.TopDecl findSupplementalTopDecl( + final String name, + final List supplementals) { + for (final var topDecl : supplementals) { + if (name.equals(topDeclName(topDecl))) { + return topDecl; + } + } + return null; + } + + private String topDeclName(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return functionDecl.name(); + } + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return structDecl.name(); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return builtinTypeDecl.name(); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return serviceDecl.name(); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return hostDecl.name(); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return contractDecl.name(); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return callbackDecl.name(); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return enumDecl.name(); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return errorDecl.name(); + } + if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { + return globalDecl.name(); + } + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + return constDecl.name(); + } + return null; + } + + private String formatModuleRef(final PbsAst.ModuleRef moduleRef) { + if (moduleRef == null) { + return ""; + } + if (moduleRef.pathSegments().isEmpty()) { + return "@%s:".formatted(moduleRef.project()); + } + return "@%s:%s".formatted(moduleRef.project(), String.join("/", moduleRef.pathSegments().asList())); + } + + private PbsAst.TopDecl cloneTopDeclWithName( + final PbsAst.TopDecl topDecl, + final String newName) { + if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { + return new PbsAst.FunctionDecl( + newName, + functionDecl.parameters(), + functionDecl.returnKind(), + functionDecl.returnType(), + functionDecl.resultErrorType(), + functionDecl.lifecycleMarker(), + functionDecl.body(), + functionDecl.span()); + } + if (topDecl instanceof PbsAst.StructDecl structDecl) { + return new PbsAst.StructDecl( + newName, + structDecl.fields(), + structDecl.methods(), + structDecl.ctors(), + structDecl.hasBody(), + structDecl.span()); + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { + return new PbsAst.BuiltinTypeDecl( + newName, + builtinTypeDecl.fields(), + builtinTypeDecl.signatures(), + builtinTypeDecl.attributes(), + builtinTypeDecl.span()); + } + if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { + return new PbsAst.ServiceDecl(newName, serviceDecl.methods(), serviceDecl.span()); + } + if (topDecl instanceof PbsAst.HostDecl hostDecl) { + return new PbsAst.HostDecl(newName, hostDecl.signatures(), hostDecl.span()); + } + if (topDecl instanceof PbsAst.ContractDecl contractDecl) { + return new PbsAst.ContractDecl(newName, contractDecl.signatures(), contractDecl.span()); + } + if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { + return new PbsAst.CallbackDecl( + newName, + callbackDecl.parameters(), + callbackDecl.returnKind(), + callbackDecl.returnType(), + callbackDecl.resultErrorType(), + callbackDecl.span()); + } + if (topDecl instanceof PbsAst.EnumDecl enumDecl) { + return new PbsAst.EnumDecl(newName, enumDecl.cases(), enumDecl.span()); + } + if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { + return new PbsAst.ErrorDecl(newName, errorDecl.cases(), errorDecl.span()); + } + if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { + return new PbsAst.GlobalDecl(newName, globalDecl.explicitType(), globalDecl.initializer(), globalDecl.span()); + } + if (topDecl instanceof PbsAst.ConstDecl constDecl) { + return new PbsAst.ConstDecl(newName, constDecl.explicitType(), constDecl.initializer(), constDecl.attributes(), constDecl.span()); + } + return topDecl; + } + + private TopDeclSymbol resolveTopDeclSymbol(final String symbolName) { + final TopDeclSymbol importedSymbol = importedSymbols.get(symbolName); + if (importedSymbol != null) { + return importedSymbol; + } + final TopDeclSymbol topLevelSymbol = topLevelSymbols.get(symbolName); + if (topLevelSymbol != null) { + return topLevelSymbol; + } + return supplementalSymbols.get(symbolName); + } + + private PbsEditorialSymbolKind symbolKind(final PbsAst.TopDecl topDecl) { + if (topDecl instanceof PbsAst.FunctionDecl) { + return PbsEditorialSymbolKind.FUNCTION; + } + if (topDecl instanceof PbsAst.StructDecl) { + return PbsEditorialSymbolKind.STRUCT; + } + if (topDecl instanceof PbsAst.BuiltinTypeDecl) { + return PbsEditorialSymbolKind.BUILTIN_TYPE; + } + if (topDecl instanceof PbsAst.ServiceDecl) { + return PbsEditorialSymbolKind.SERVICE; + } + if (topDecl instanceof PbsAst.HostDecl) { + return PbsEditorialSymbolKind.HOST; + } + if (topDecl instanceof PbsAst.ContractDecl) { + return PbsEditorialSymbolKind.CONTRACT; + } + if (topDecl instanceof PbsAst.CallbackDecl) { + return PbsEditorialSymbolKind.CALLBACK; + } + if (topDecl instanceof PbsAst.EnumDecl) { + return PbsEditorialSymbolKind.ENUM; + } + if (topDecl instanceof PbsAst.ErrorDecl) { + return PbsEditorialSymbolKind.ERROR; + } + if (topDecl instanceof PbsAst.GlobalDecl) { + return PbsEditorialSymbolKind.GLOBAL; + } + if (topDecl instanceof PbsAst.ConstDecl) { + return PbsEditorialSymbolKind.CONST; + } + throw new IllegalArgumentException("Unsupported top declaration: " + topDecl.getClass().getSimpleName()); + } + + private String errorName(final PbsAst.TypeRef typeRef) { + return typeRef == null ? null : typeRef.name(); + } + } + + private record TopDeclSymbol( + String localName, + PbsAst.TopDecl decl, + String origin) { + } + + private record CallableFrame( + ReadOnlyList parameters, + PbsAst.Block body, + TypeView receiverType, + TypeView returnType, + String resultErrorName) { + private Set parameterNames() { + final Set names = new LinkedHashSet<>(); + for (final var parameter : parameters) { + names.add(parameter.name()); + } + return Set.copyOf(names); + } + } + + private record ScopeState( + Scope scope, + TypeView receiverType, + TypeView returnType, + String resultErrorName, + Set parameterNames) { + } + + private enum CallSiteKind { + FUNCTION, + MEMBER, + CONSTRUCTOR + } + + private record CallSite( + CallSiteKind kind, + String name, + int activeParameter, + int memberDotTokenIndex) { + } +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSymbolKind.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSymbolKind.java new file mode 100644 index 00000000..f684dd38 --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsEditorialSymbolKind.java @@ -0,0 +1,21 @@ +package p.studio.compiler.pbs.semantics; + +public enum PbsEditorialSymbolKind { + KEYWORD, + LOCAL, + PARAMETER, + FIELD, + FUNCTION, + METHOD, + CONSTRUCTOR, + STRUCT, + BUILTIN_TYPE, + SERVICE, + HOST, + CONTRACT, + CALLBACK, + ENUM, + ERROR, + GLOBAL, + CONST +} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java index ee33549a..70aeefae 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/pbs/semantics/PbsFlowSemanticSupport.java @@ -584,5 +584,9 @@ final class PbsFlowSemanticSupport { final var symbol = names.get(name); return symbol != null && symbol.mutable(); } + + Map entries() { + return Map.copyOf(names); + } } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java index 27341fff..6aaa8bc7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/services/PBSFrontendPhaseService.java @@ -133,12 +133,14 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { assembly.parsedSourceFiles(), assembly.moduleTable()); final var flowSemanticsValidator = new PbsFlowSemanticsValidator(); + final Map astByFile = new LinkedHashMap<>(); final Map> supplementalTopDeclsByFile = new LinkedHashMap<>(); final Map> inlineHintsByFile = new LinkedHashMap<>(); for (final var entry : importedSemanticContexts.entrySet()) { supplementalTopDeclsByFile.put(entry.getKey(), entry.getValue().supplementalTopDecls()); } for (final var parsedSourceFile : assembly.parsedSourceFiles()) { + astByFile.put(parsedSourceFile.fileId(), parsedSourceFile.ast()); final var importedSemanticContext = importedSemanticContexts.getOrDefault( parsedSourceFile.fileId(), PbsImportedSemanticContext.empty()); @@ -152,11 +154,13 @@ public class PBSFrontendPhaseService implements FrontendPhaseService { } } return new PbsSemanticReadSurface( + Map.copyOf(astByFile), Map.copyOf(supplementalTopDeclsByFile), Map.copyOf(inlineHintsByFile)); } public record PbsSemanticReadSurface( + Map astByFile, Map> supplementalTopDeclsByFile, Map> inlineHintsByFile) { } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css deleted file mode 100644 index 801b2730..00000000 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/resources/themes/pbs/semantic-highlighting.css +++ /dev/null @@ -1,111 +0,0 @@ -.editor-workspace-code-area-type-pbs { - -fx-highlight-fill: #2d4f6b; -} - -.editor-workspace-code-area-type-pbs .text { - -fx-fill: #d9e2ec; -} - -.editor-workspace-code-area-type-pbs .lineno { - -fx-text-fill: #6f8193; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-keyword { - -fx-fill: #569cd6; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-lifecycle { - -fx-fill: #ef50c0; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-function { - -fx-fill: #f2c14e; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-method { - -fx-fill: #f2c14e; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-constructor { - -fx-fill: #ecdcaa; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-struct { - -fx-fill: #4ec9b0; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-contract { - -fx-fill: #78dce8; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-host { - -fx-fill: #b7a2fa; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-builtin-type { - -fx-fill: #8be9fd; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-service { - -fx-fill: #b7a2fa; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-error { - -fx-fill: #ff5b5b; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-enum { - -fx-fill: #56cfe1; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-callback { - -fx-fill: #b7a2fa; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-global { - -fx-fill: #f790fc; - -fx-font-style: italic; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-const { - -fx-fill: #f78c6c; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-implements { - -fx-fill: #a1c181; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-string { - -fx-fill: #00c088; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-number { - -fx-fill: #ff90b0; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-comment { - -fx-fill: #c090b0; - -fx-font-style: italic; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-literal { - -fx-fill: #4fc1ff; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-operator { - -fx-fill: #d4d4d4; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-punctuation { - -fx-fill: #d4d4d4; -} - -.editor-workspace-code-area-type-pbs .text.editor-semantic-pbs-identifier { - -fx-fill: #d4d4d4; -} - -.editor-workspace-status-chip-type-pbs { - -fx-background-color: #2c2140; - -fx-border-color: #a47dff; - -fx-text-fill: #efe6ff; -} diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java index 761a4cff..1d62614f 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java @@ -1,13 +1,14 @@ package p.studio.compiler.pbs; import org.junit.jupiter.api.Test; +import p.studio.compiler.PBSFrontendHostProjections; import p.studio.compiler.PBSDefinitions; +import p.studio.compiler.models.FrontendSemanticToken; -import java.net.URL; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; final class PbsSemanticPresentationContractTest { @@ -18,16 +19,128 @@ final class PbsSemanticPresentationContractTest { assertFalse(presentation.semanticKeys().isEmpty()); assertEquals(PbsSemanticKind.semanticKeys(), presentation.semanticKeys()); - assertEquals(1, presentation.resources().size()); - assertEquals("/themes/pbs/semantic-highlighting.css", presentation.resources().getFirst()); + assertEquals("pbs-default", presentation.defaultThemeId()); + assertEquals(1, presentation.themes().size()); + assertEquals("PBS Default", presentation.defaultTheme().displayName()); + assertEquals("#d9e2ec", presentation.defaultTheme().editorPalette().baseForeground()); + assertEquals("#7fb8ff", presentation.defaultTheme().requireTokenStyle("pbs-keyword").foreground()); + assertEquals("#d8b86a", presentation.defaultTheme().requireTokenStyle("pbs-string").foreground()); + assertEquals("#ff8cc6", presentation.defaultTheme().requireTokenStyle("pbs-number").foreground()); + assertEquals("#7fb8ff", presentation.defaultTheme().requireTokenStyle("pbs-literal").foreground()); + assertEquals("#7ee787", presentation.defaultTheme().requireTokenStyle("pbs-function").foreground()); + assertEquals("#7ee787", presentation.defaultTheme().requireTokenStyle("pbs-method").foreground()); + assertEquals("#7ee787", presentation.defaultTheme().requireTokenStyle("pbs-constructor").foreground()); + assertEquals("#56d4dd", presentation.defaultTheme().requireTokenStyle("pbs-struct").foreground()); + assertTrue(presentation.defaultTheme().requireTokenStyle("pbs-struct").italic()); + assertEquals("#56d4dd", presentation.defaultTheme().requireTokenStyle("pbs-builtin-type").foreground()); + assertFalse(presentation.defaultTheme().requireTokenStyle("pbs-builtin-type").italic()); + assertEquals("#ff8e7a", presentation.defaultTheme().requireTokenStyle("pbs-service").foreground()); + assertEquals("#c297ff", presentation.defaultTheme().requireTokenStyle("pbs-asset").foreground()); + assertTrue(presentation.defaultTheme().requireTokenStyle("pbs-comment").italic()); + assertEquals(PBSFrontendHostProjections.VSCODE_HOST_ID, presentation.requireHostProjection("vscode").hostId()); } @Test - void shouldPublishResolvableSemanticPresentationResources() { - final var resourcePath = PBSDefinitions.PBS.getSemanticPresentation().resources().getFirst(); - final URL resource = PBSDefinitions.class.getResource(resourcePath); + void shouldCoverTheWholePbsSemanticVocabularyInTheDefaultTheme() { + final var presentation = PBSDefinitions.PBS.getSemanticPresentation(); + final var defaultTheme = presentation.defaultTheme(); - assertNotNull(resource, resourcePath); - assertTrue(resource.toExternalForm().endsWith("themes/pbs/semantic-highlighting.css")); + assertEquals( + PbsSemanticKind.semanticKeys(), + defaultTheme.tokenStyles().stream().map(tokenStyle -> tokenStyle.semanticKey()).toList()); + } + + @Test + void shouldCoverTheWholePbsSemanticVocabularyInTheVscodeProjection() { + final var presentation = PBSDefinitions.PBS.getSemanticPresentation(); + final var vscodeProjection = presentation.requireHostProjection(PBSFrontendHostProjections.VSCODE_HOST_ID); + + assertEquals( + PbsSemanticKind.semanticKeys(), + vscodeProjection.tokenProjections().stream().map(tokenProjection -> tokenProjection.semanticKey()).toList()); + assertEquals("keyword", vscodeProjection.requireProjection(PbsSemanticKind.KEYWORD.semanticKey()).hostTokenType()); + assertEquals("type", vscodeProjection.requireProjection(PbsSemanticKind.STRUCT.semanticKey()).fallbackTokenType()); + assertEquals("variable", vscodeProjection.requireProjection(PbsSemanticKind.ASSET.semanticKey()).hostTokenType()); + assertEquals( + java.util.List.of("readonly"), + vscodeProjection.requireProjection(PbsSemanticKind.GLOBAL.semanticKey()).hostTokenModifiers()); + } + + @Test + void shouldPublishPbsOwnedSemanticTokenProvider() { + final List semanticTokens = PBSDefinitions.PBS.getSemanticTokenProvider().tokenize(""" + struct Vec2 {} + service Log {} + host Input {} + enum ResultKind { Ok = 1; } + + fn main() -> void { + const answer: int = 42; + let value = new Vec2(); + value.length(); + Log.info(); + Input.read(); + let kind: ResultKind; + } + """); + + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.BUILTIN_TYPE.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.FUNCTION.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.CONST.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.BUILTIN_TYPE.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.CONSTRUCTOR.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.METHOD.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.SERVICE.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.HOST.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.ENUM.semanticKey()))); + } + + @Test + void shouldClassifyStdlibImportsUsingTheirPublishedBarrelKinds() { + final List semanticTokens = PBSDefinitions.PBS.getSemanticTokenProvider().tokenize(""" + import { Log } from @sdk:log; + import { Composer } from @sdk:composer; + import { Color as Paint } from @core:color; + + fn main() -> void { + Log.info("hello"); + Composer.emit_sprite(0, 0, 0, 0, 0, 0, false, false, 0); + let paint: Paint; + } + """); + + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.SERVICE.semanticKey()))); + assertTrue(semanticTokens.stream().anyMatch(token -> token.semanticKey().equals(PbsSemanticKind.BUILTIN_TYPE.semanticKey()))); + } + + @Test + void shouldClassifyAssetRootedAliasesUsingDedicatedSemanticKind() { + final List semanticTokens = PBSDefinitions.PBS.getSemanticTokenProvider().tokenize(""" + import { Assets } from @sdk:asset; + + fn main() -> void { + Assets.load(assets.ui.atlas2, 3); + } + """); + + final long assetTokenCount = semanticTokens.stream() + .filter(token -> token.semanticKey().equals(PbsSemanticKind.ASSET.semanticKey())) + .count(); + + assertEquals(3, assetTokenCount); + } + + @Test + void shouldClassifyBuiltinTypesExposedByImportedStdlibModules() { + final List semanticTokens = PBSDefinitions.PBS.getSemanticTokenProvider().tokenize(""" + import { Input } from @sdk:input; + + fn main() -> void { + let touch : InputTouch = Input.touch(); + let pad : InputPad = Input.pad(); + } + """); + + assertTrue(semanticTokens.stream().filter(token -> token.semanticKey().equals(PbsSemanticKind.BUILTIN_TYPE.semanticKey())).count() >= 2); } } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java new file mode 100644 index 00000000..b9458adb --- /dev/null +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/semantics/PbsEditorialSupportServiceTest.java @@ -0,0 +1,215 @@ +package p.studio.compiler.pbs.semantics; + +import org.junit.jupiter.api.Test; +import p.studio.compiler.pbs.ast.PbsAst; +import p.studio.compiler.pbs.lexer.PbsLexer; +import p.studio.compiler.pbs.parser.PbsParser; +import p.studio.compiler.source.diagnostics.DiagnosticSink; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.utilities.structures.ReadOnlyList; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +final class PbsEditorialSupportServiceTest { + private final PbsEditorialSupportService service = new PbsEditorialSupportService(); + + @Test + void shouldResolveGeneralCompletionAndHoverAcrossLocalAndImportedSymbols() { + final var source = """ + import { Log } from @sdk:log; + + fn helper(value: int) -> int { + let total = value; + Log.info("hello"); + return total; + } + """; + final var ast = parseOrdinary(source); + final var supplementalTopDecls = supplementalTopDecls(""" + declare service Log { + fn info(message: str) -> void { + return; + } + } + """); + + final var completions = service.completion( + source, + ast, + supplementalTopDecls, + source.indexOf("return total")); + + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("fn") && candidate.kind() == PbsEditorialSymbolKind.KEYWORD)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("value") && candidate.kind() == PbsEditorialSymbolKind.PARAMETER)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("total") && candidate.kind() == PbsEditorialSymbolKind.LOCAL)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("Log") && candidate.kind() == PbsEditorialSymbolKind.SERVICE)); + assertTrue(completions.stream().anyMatch(candidate -> + candidate.label().equals("helper") && candidate.kind() == PbsEditorialSymbolKind.FUNCTION)); + + final var parameterHover = requireHover(source, ast, supplementalTopDecls, "value;"); + assertEquals(PbsEditorialSymbolKind.PARAMETER, parameterHover.kind()); + assertEquals("int", parameterHover.detail()); + + final var importedHover = requireHover(source, ast, supplementalTopDecls, "Log.info"); + assertEquals(PbsEditorialSymbolKind.SERVICE, importedHover.kind()); + assertEquals("@sdk:log", importedHover.origin()); + } + + @Test + void shouldResolveMemberCompletionAndHoverForImportedHostAndBuiltinTypes() { + final var source = """ + import { Input } from @sdk:input; + + fn main() -> void { + let touch: InputTouch = Input.touch(); + Input.touch(); + touch.x_axis(); + } + """; + final var ast = parseOrdinary(source); + final var supplementalTopDecls = supplementalTopDecls(""" + declare builtin type InputTouch( + pub x: int + ) { + fn x_axis() -> int; + } + + declare host Input { + fn touch() -> InputTouch; + fn pad() -> InputPad; + } + + declare builtin type InputPad( + pub id: int + ) { + fn strength() -> int; + } + """); + + final var hostMembers = service.completion( + source, + ast, + supplementalTopDecls, + source.indexOf("touch();")); + + assertTrue(hostMembers.stream().anyMatch(candidate -> + candidate.label().equals("touch") && candidate.kind() == PbsEditorialSymbolKind.METHOD)); + assertTrue(hostMembers.stream().anyMatch(candidate -> + candidate.label().equals("pad") && candidate.kind() == PbsEditorialSymbolKind.METHOD)); + + final var builtinMembers = service.completion( + source, + ast, + supplementalTopDecls, + source.indexOf("x_axis();")); + + assertTrue(builtinMembers.stream().anyMatch(candidate -> + candidate.label().equals("x") && candidate.kind() == PbsEditorialSymbolKind.FIELD)); + assertTrue(builtinMembers.stream().anyMatch(candidate -> + candidate.label().equals("x_axis") && candidate.kind() == PbsEditorialSymbolKind.METHOD)); + + final var builtinTypeHover = requireHover(source, ast, supplementalTopDecls, "InputTouch"); + assertEquals(PbsEditorialSymbolKind.BUILTIN_TYPE, builtinTypeHover.kind()); + + final var hostMethodHover = requireHover(source, ast, supplementalTopDecls, "touch();"); + assertEquals(PbsEditorialSymbolKind.METHOD, hostMethodHover.kind()); + assertEquals("Input", hostMethodHover.origin()); + } + + @Test + void shouldResolveSignatureHelpForFunctionMethodAndConstructorCalls() { + final var source = """ + declare struct Vec() { + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn mix(left: int, right: int) -> int { return left; } + + fn main(vec: Vec) -> void { + mix(1, 2); + vec.blend(1, 2); + new Vec(); + } + """; + final var ast = parseOrdinary(source); + + final var functionHelp = service.signatureHelp( + source, + ast, + ReadOnlyList.empty(), + source.indexOf("2);")); + assertTrue(functionHelp.isPresent()); + assertEquals("mix(left: int, right: int) -> int", functionHelp.orElseThrow().signatures().getFirst().label()); + assertEquals(1, functionHelp.orElseThrow().activeParameter()); + + final var methodHelp = service.signatureHelp( + source, + ast, + ReadOnlyList.empty(), + source.lastIndexOf("2);")); + assertTrue(methodHelp.isPresent()); + assertEquals("blend(arg0: int, arg1: int) -> int", methodHelp.orElseThrow().signatures().getFirst().label()); + assertEquals(1, methodHelp.orElseThrow().activeParameter()); + + final var constructorHelp = service.signatureHelp( + source, + ast, + ReadOnlyList.empty(), + source.indexOf("Vec();") + 4); + assertTrue(constructorHelp.isPresent()); + assertEquals("Vec()", constructorHelp.orElseThrow().signatures().getFirst().label()); + + final var constructorHover = requireHover(source, ast, ReadOnlyList.empty(), "Vec();"); + assertEquals(PbsEditorialSymbolKind.CONSTRUCTOR, constructorHover.kind()); + assertEquals("Vec()", constructorHover.signatures().getFirst().label()); + } + + private PbsEditorialResolvedSymbol requireHover( + final String source, + final PbsAst.File ast, + final ReadOnlyList supplementalTopDecls, + final String needle) { + final int offset = source.indexOf(needle); + assertTrue(offset >= 0, "Expected to find needle: " + needle); + return service.hover(source, ast, supplementalTopDecls, offset) + .orElseThrow(); + } + + private ReadOnlyList supplementalTopDecls(final String... interfaceModuleSources) { + final ArrayList topDecls = new ArrayList<>(); + int fileNumber = 100; + for (final var source : interfaceModuleSources) { + topDecls.addAll(parseInterface(source, new FileId(fileNumber)).topDecls().asList()); + fileNumber += 1; + } + return ReadOnlyList.wrap(topDecls); + } + + private PbsAst.File parseOrdinary(final String source) { + return parse(source, new FileId(0), PbsParser.ParseMode.ORDINARY); + } + + private PbsAst.File parseInterface( + final String source, + final FileId fileId) { + return parse(source, fileId, PbsParser.ParseMode.INTERFACE_MODULE); + } + + private PbsAst.File parse( + final String source, + final FileId fileId, + final PbsParser.ParseMode parseMode) { + final var diagnostics = DiagnosticSink.empty(); + final var tokens = PbsLexer.lex(source, fileId, diagnostics); + final var ast = PbsParser.parse(tokens, fileId, diagnostics, parseMode); + assertFalse(diagnostics.hasErrors(), () -> "Unexpected parse errors: " + diagnostics.stream().toList()); + return ast; + } +} diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java index 661cf7dd..c0f35f7d 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/BuilderPipelineService.java @@ -57,7 +57,7 @@ public class BuilderPipelineService { public AnalysisSnapshot analyze( final BuilderPipelineContext ctx, final LogAggregator logs) { - final var diagnostics = run(ctx, logs, analyses, new ArrayList<>()); + final var diagnostics = run(ctx, logs, analyses, new ArrayList<>(), false); return new AnalysisSnapshot( ctx.resolvedWorkspace.frontendSpec(), diagnostics, @@ -70,7 +70,10 @@ public class BuilderPipelineService { final BuilderPipelineContext ctx, final LogAggregator logs) { final var analysisSnapshot = this.analyze(ctx, logs); - final var diagnostics = run(ctx, logs, compile, new ArrayList<>(analysisSnapshot.diagnostics())); + if (hasErrors(analysisSnapshot.diagnostics())) { + throw new BuildException("issues found on pipeline stage: analyze"); + } + final var diagnostics = run(ctx, logs, compile, new ArrayList<>(analysisSnapshot.diagnostics()), true); return new CompileResult( analysisSnapshot, diagnostics, @@ -84,7 +87,7 @@ public class BuilderPipelineService { final BuilderPipelineContext ctx, final LogAggregator logs) { final var compileResult = this.compile(ctx, logs); - final var diagnostics = run(ctx, logs, build, new ArrayList<>(compileResult.diagnostics())); + final var diagnostics = run(ctx, logs, build, new ArrayList<>(compileResult.diagnostics()), true); return new BuildResult( compileResult, diagnostics, @@ -95,19 +98,32 @@ public class BuilderPipelineService { final BuilderPipelineContext ctx, final LogAggregator logs, final List stages, - final List diagnostics) { + final List diagnostics, + final boolean failFast) { for (final var builderPipelineStage : stages) { final var issues = builderPipelineStage.run(ctx, logs); diagnostics.addAll(issues.asCollection()); printIssues(issues, logs); if (issues.hasErrors()) { - throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName()); + if (failFast) { + throw new BuildException("issues found on pipeline stage: " + builderPipelineStage.getClass().getSimpleName()); + } + break; } } logs.using(log).info("builder pipeline completed successfully"); return List.copyOf(diagnostics); } + private boolean hasErrors(final List diagnostics) { + for (final var diagnostic : diagnostics) { + if (diagnostic.isError()) { + return true; + } + } + return false; + } + private void printIssues( final ReadOnlyCollection issues, final LogAggregator logs) { diff --git a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java index 6a039522..4b370e6a 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/main/java/p/studio/compiler/workspaces/stages/FrontendPhasePipelineStage.java @@ -48,6 +48,11 @@ public class FrontendPhasePipelineStage implements PipelineStage { final var span = diagnostic.getSpan(); issues.report(builder -> builder .error(diagnostic.getSeverity().isError()) + .phase(diagnostic.getPhase().id()) + .code(diagnostic.getCode()) + .fileId(span.getFileId().isNone() ? null : span.getFileId().getId()) + .start(safeToInt(span.getStart())) + .end(safeToInt(span.getEnd())) .message("[BUILD] %s :: %s @ %s:[%d,%d)".formatted( diagnostic.getCode(), diagnostic.getMessage(), @@ -56,4 +61,8 @@ public class FrontendPhasePipelineStage implements PipelineStage { span.getEnd()))); } } + + private int safeToInt(final long value) { + return value > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) Math.max(0L, value); + } } diff --git a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java index 164ececa..eea50a34 100644 --- a/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java +++ b/prometeu-compiler/prometeu-build-pipeline/src/test/java/p/studio/compiler/integration/MainProjectPipelineIntegrationTest.java @@ -3,12 +3,15 @@ package p.studio.compiler.integration; import org.junit.jupiter.api.Test; import p.studio.compiler.messages.BuilderPipelineConfig; import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.compiler.utilities.SourceProviderFactory; import p.studio.compiler.workspaces.BuilderPipelineService; import p.studio.utilities.logs.LogAggregator; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @@ -32,6 +35,36 @@ class MainProjectPipelineIntegrationTest { assertFalse(Files.exists(outputPath), "analyze must not write output: " + outputPath); } + @Test + void analyzeShouldReturnStructuredDiagnosticsForOverlayDocumentWithoutThrowing() throws IOException { + final var projectRoot = projectRoot(); + final var documentPath = projectRoot.resolve("src").resolve("main.pbs").toAbsolutePath().normalize(); + final var logs = bufferedLogs(); + final var invalidOverlay = """ + fn frame() -> void { + let broken: int = ; + } + """; + final var context = BuilderPipelineContext.fromConfig(new BuilderPipelineConfig( + false, + projectRoot.toString(), + "core-v1", + SourceProviderFactory.overlayUtf8(Map.of(documentPath, invalidOverlay)))); + + final var snapshot = assertDoesNotThrow( + () -> BuilderPipelineService.INSTANCE.analyze(context, logs), + () -> "analyze unexpectedly failed for overlay " + documentPath); + + final var documentDiagnostics = snapshot.diagnostics().stream() + .filter(issue -> issue.getFileId() != null) + .filter(issue -> issue.getFileId() >= 0 && issue.getFileId() < snapshot.fileTable().size()) + .filter(issue -> snapshot.fileTable().get(new FileId(issue.getFileId())).getCanonPath().toAbsolutePath().normalize().equals(documentPath)) + .toList(); + + assertFalse(documentDiagnostics.isEmpty(), "overlay analysis must surface diagnostics for " + documentPath); + assertTrue(documentDiagnostics.stream().anyMatch(issue -> issue.isError() && issue.getCode() != null && issue.getStart() != null && issue.getEnd() != null)); + } + @Test void compileShouldProduceInMemoryBytecodeWithoutWritingProgramBytecode() throws IOException { final var projectRoot = projectRoot(); diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendEditorPaletteSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendEditorPaletteSpec.java new file mode 100644 index 00000000..3cd2cfd0 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendEditorPaletteSpec.java @@ -0,0 +1,33 @@ +package p.studio.compiler.models; + +import java.util.Objects; + +public record FrontendEditorPaletteSpec( + String baseForeground, + String selectionBackground, + String activeHighlightBackground, + String lineNumberForeground, + String statusChipBackground, + String statusChipBorder, + String statusChipForeground) { + + public FrontendEditorPaletteSpec { + baseForeground = requireColor(baseForeground, "baseForeground"); + selectionBackground = requireColor(selectionBackground, "selectionBackground"); + activeHighlightBackground = requireColor(activeHighlightBackground, "activeHighlightBackground"); + lineNumberForeground = requireColor(lineNumberForeground, "lineNumberForeground"); + statusChipBackground = requireColor(statusChipBackground, "statusChipBackground"); + statusChipBorder = requireColor(statusChipBorder, "statusChipBorder"); + statusChipForeground = requireColor(statusChipForeground, "statusChipForeground"); + } + + private static String requireColor( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionEntrySpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionEntrySpec.java new file mode 100644 index 00000000..4d66ff4a --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionEntrySpec.java @@ -0,0 +1,38 @@ +package p.studio.compiler.models; + +import java.util.List; +import java.util.Objects; + +public record FrontendHostProjectionEntrySpec( + String semanticKey, + String hostTokenType, + List hostTokenModifiers, + String fallbackTokenType, + List fallbackTokenModifiers) { + + public FrontendHostProjectionEntrySpec { + semanticKey = requireText(semanticKey, "semanticKey"); + hostTokenType = requireText(hostTokenType, "hostTokenType"); + hostTokenModifiers = normalizeList(hostTokenModifiers, "hostTokenModifiers"); + fallbackTokenType = requireText(fallbackTokenType, "fallbackTokenType"); + fallbackTokenModifiers = normalizeList(fallbackTokenModifiers, "fallbackTokenModifiers"); + } + + private static List normalizeList( + final List values, + final String field) { + return List.copyOf(Objects.requireNonNull(values, field).stream() + .map(value -> requireText(value, field + " entry")) + .toList()); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionSpec.java new file mode 100644 index 00000000..826ab68f --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendHostProjectionSpec.java @@ -0,0 +1,48 @@ +package p.studio.compiler.models; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public record FrontendHostProjectionSpec( + String hostId, + List tokenProjections) { + + public FrontendHostProjectionSpec { + hostId = requireText(hostId, "hostId"); + tokenProjections = List.copyOf(Objects.requireNonNull(tokenProjections, "tokenProjections")); + if (tokenProjections.isEmpty()) { + throw new IllegalArgumentException("tokenProjections must not be empty"); + } + final Map bySemanticKey = new LinkedHashMap<>(); + for (final FrontendHostProjectionEntrySpec tokenProjection : tokenProjections) { + final FrontendHostProjectionEntrySpec previous = bySemanticKey.put(tokenProjection.semanticKey(), tokenProjection); + if (previous != null) { + throw new IllegalArgumentException("duplicate host projection for semantic key: " + tokenProjection.semanticKey()); + } + } + } + + public Optional projectionFor(final String semanticKey) { + return tokenProjections.stream() + .filter(tokenProjection -> tokenProjection.semanticKey().equals(semanticKey)) + .findFirst(); + } + + public FrontendHostProjectionEntrySpec requireProjection(final String semanticKey) { + return projectionFor(semanticKey) + .orElseThrow(() -> new IllegalArgumentException("missing host projection for semantic key: " + semanticKey)); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java index 73691d86..69938d91 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java @@ -4,15 +4,117 @@ import java.util.List; import java.util.Objects; public record FrontendSemanticPresentationSpec( - List semanticKeys, - List resources) { + List themes, + String defaultThemeId, + List hostProjections) { public FrontendSemanticPresentationSpec { - semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys")); - resources = List.copyOf(Objects.requireNonNull(resources, "resources")); + themes = List.copyOf(Objects.requireNonNull(themes, "themes")); + defaultThemeId = Objects.requireNonNull(defaultThemeId, "defaultThemeId").trim(); + hostProjections = List.copyOf(Objects.requireNonNull(hostProjections, "hostProjections")); + if (themes.isEmpty()) { + if (!defaultThemeId.isEmpty()) { + throw new IllegalArgumentException("defaultThemeId must be blank when no themes are declared"); + } + if (!hostProjections.isEmpty()) { + throw new IllegalArgumentException("hostProjections must be empty when no themes are declared"); + } + } else { + defaultThemeId = requireText(defaultThemeId, "defaultThemeId"); + final String resolvedDefaultThemeId = defaultThemeId; + if (themes.stream().map(FrontendVisualThemeSpec::themeId).noneMatch(resolvedDefaultThemeId::equals)) { + throw new IllegalArgumentException("defaultThemeId must reference one of the declared themes"); + } + + final List derivedSemanticKeys = themes.stream() + .filter(theme -> theme.themeId().equals(resolvedDefaultThemeId)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("defaultThemeId must resolve to a declared theme")) + .tokenStyles() + .stream() + .map(FrontendTokenStyleSpec::semanticKey) + .toList(); + + for (final FrontendVisualThemeSpec theme : themes) { + final List themeSemanticKeys = theme.tokenStyles().stream() + .map(FrontendTokenStyleSpec::semanticKey) + .toList(); + if (!themeSemanticKeys.equals(derivedSemanticKeys)) { + throw new IllegalArgumentException( + "theme '" + theme.themeId() + "' must publish the same semantic keys as the default theme"); + } + } + + final var hostIds = new java.util.LinkedHashSet(); + for (final FrontendHostProjectionSpec hostProjection : hostProjections) { + if (!hostIds.add(hostProjection.hostId())) { + throw new IllegalArgumentException( + "duplicate host projection for hostId: " + hostProjection.hostId()); + } + final List projectedSemanticKeys = hostProjection.tokenProjections().stream() + .map(FrontendHostProjectionEntrySpec::semanticKey) + .toList(); + if (!projectedSemanticKeys.equals(derivedSemanticKeys)) { + throw new IllegalArgumentException( + "host projection for hostId '" + hostProjection.hostId() + + "' must publish the same semantic keys as the default theme"); + } + for (final FrontendHostProjectionEntrySpec tokenProjection : hostProjection.tokenProjections()) { + if (!derivedSemanticKeys.contains(tokenProjection.semanticKey())) { + throw new IllegalArgumentException( + "host projection for hostId '" + hostProjection.hostId() + "' references unknown semantic key: " + + tokenProjection.semanticKey()); + } + } + } + } } public static FrontendSemanticPresentationSpec empty() { - return new FrontendSemanticPresentationSpec(List.of(), List.of()); + return new FrontendSemanticPresentationSpec(List.of(), "", List.of()); + } + + public FrontendVisualThemeSpec defaultTheme() { + if (themes.isEmpty()) { + throw new IllegalStateException("empty semantic presentation has no default theme"); + } + return themes.stream() + .filter(theme -> theme.themeId().equals(defaultThemeId)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("defaultThemeId must resolve to a declared theme")); + } + + public boolean isEmpty() { + return themes.isEmpty() && defaultThemeId.isEmpty() && hostProjections.isEmpty(); + } + + public List semanticKeys() { + if (themes.isEmpty()) { + return List.of(); + } + return defaultTheme().tokenStyles().stream() + .map(FrontendTokenStyleSpec::semanticKey) + .toList(); + } + + public java.util.Optional hostProjection(final String hostId) { + return hostProjections.stream() + .filter(hostProjection -> hostProjection.hostId().equals(requireText(hostId, "hostId"))) + .findFirst(); + } + + public FrontendHostProjectionSpec requireHostProjection(final String hostId) { + return hostProjection(hostId) + .orElseThrow(() -> new IllegalArgumentException("missing host projection for hostId: " + hostId)); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticToken.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticToken.java new file mode 100644 index 00000000..fef83228 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticToken.java @@ -0,0 +1,29 @@ +package p.studio.compiler.models; + +import java.util.Objects; + +public record FrontendSemanticToken( + int startOffset, + int endOffset, + String semanticKey) { + + public FrontendSemanticToken { + if (startOffset < 0) { + throw new IllegalArgumentException("startOffset must be >= 0"); + } + if (endOffset < startOffset) { + throw new IllegalArgumentException("endOffset must be >= startOffset"); + } + semanticKey = requireText(semanticKey, "semanticKey"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticTokenProvider.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticTokenProvider.java new file mode 100644 index 00000000..6e52f6f3 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticTokenProvider.java @@ -0,0 +1,14 @@ +package p.studio.compiler.models; + +import java.util.List; + +@FunctionalInterface +public interface FrontendSemanticTokenProvider { + FrontendSemanticTokenProvider NOOP = documentText -> List.of(); + + List tokenize(String documentText); + + static FrontendSemanticTokenProvider noop() { + return NOOP; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java index f09e24fc..72a42cab 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSpec.java @@ -17,6 +17,8 @@ public class FrontendSpec { private final List stdlibVersions = List.of(); @Builder.Default private final FrontendSemanticPresentationSpec semanticPresentation = FrontendSemanticPresentationSpec.empty(); + @Builder.Default + private final FrontendSemanticTokenProvider semanticTokenProvider = FrontendSemanticTokenProvider.noop(); public String toString() { return String.format("FrontendSpec(language=%s)", languageId); diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendTokenStyleSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendTokenStyleSpec.java new file mode 100644 index 00000000..5405017c --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendTokenStyleSpec.java @@ -0,0 +1,26 @@ +package p.studio.compiler.models; + +import java.util.Objects; + +public record FrontendTokenStyleSpec( + String semanticKey, + String foreground, + boolean italic, + boolean bold, + boolean underline) { + + public FrontendTokenStyleSpec { + semanticKey = requireText(semanticKey, "semanticKey"); + foreground = requireText(foreground, "foreground"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendVisualThemeSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendVisualThemeSpec.java new file mode 100644 index 00000000..cf8603c2 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendVisualThemeSpec.java @@ -0,0 +1,47 @@ +package p.studio.compiler.models; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public record FrontendVisualThemeSpec( + String themeId, + String displayName, + FrontendEditorPaletteSpec editorPalette, + List tokenStyles) { + + public FrontendVisualThemeSpec { + themeId = requireText(themeId, "themeId"); + displayName = requireText(displayName, "displayName"); + editorPalette = Objects.requireNonNull(editorPalette, "editorPalette"); + tokenStyles = List.copyOf(Objects.requireNonNull(tokenStyles, "tokenStyles")); + if (tokenStyles.isEmpty()) { + throw new IllegalArgumentException("tokenStyles must not be empty"); + } + final Map bySemanticKey = new LinkedHashMap<>(); + for (final FrontendTokenStyleSpec tokenStyle : tokenStyles) { + final FrontendTokenStyleSpec previous = bySemanticKey.put(tokenStyle.semanticKey(), tokenStyle); + if (previous != null) { + throw new IllegalArgumentException("duplicate token style for semantic key: " + tokenStyle.semanticKey()); + } + } + } + + public FrontendTokenStyleSpec requireTokenStyle(final String semanticKey) { + return tokenStyles.stream() + .filter(tokenStyle -> tokenStyle.semanticKey().equals(semanticKey)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("missing token style for semantic key: " + semanticKey)); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/utilities/SourceProviderFactory.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/utilities/SourceProviderFactory.java index 852f0425..6079e30b 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/utilities/SourceProviderFactory.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/utilities/SourceProviderFactory.java @@ -1,8 +1,11 @@ package p.studio.compiler.utilities; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; @FunctionalInterface @@ -24,5 +27,38 @@ public interface SourceProviderFactory { }; } - // more providers can be added here, e.g., in-memory, etc. + static SourceProviderFactory overlayUtf8(final Map overrides) { + final Map bytesByPath = new HashMap<>(); + for (final var entry : Objects.requireNonNull(overrides, "overrides").entrySet()) { + bytesByPath.put( + normalize(entry.getKey()), + Objects.requireNonNull(entry.getValue(), "override text").getBytes(StandardCharsets.UTF_8)); + } + return overlay(bytesByPath, filesystem()); + } + + static SourceProviderFactory overlay( + final Map overrides, + final SourceProviderFactory fallback) { + final Map normalizedOverrides = new HashMap<>(); + for (final var entry : Objects.requireNonNull(overrides, "overrides").entrySet()) { + normalizedOverrides.put( + normalize(entry.getKey()), + Objects.requireNonNull(entry.getValue(), "override bytes").clone()); + } + final SourceProviderFactory safeFallback = Objects.requireNonNull(fallback, "fallback"); + return path -> { + final Path normalizedPath = normalize(path); + final byte[] override = normalizedOverrides.get(normalizedPath); + if (override != null) { + final byte[] immutableBytes = override.clone(); + return () -> immutableBytes; + } + return safeFallback.create(normalizedPath); + }; + } + + private static Path normalize(final Path path) { + return Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); + } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/models/FrontendSemanticPresentationSpecTest.java b/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/models/FrontendSemanticPresentationSpecTest.java new file mode 100644 index 00000000..473facd0 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/test/java/p/studio/compiler/models/FrontendSemanticPresentationSpecTest.java @@ -0,0 +1,100 @@ +package p.studio.compiler.models; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class FrontendSemanticPresentationSpecTest { + + @Test + void shouldExposeHostProjectionByHostId() { + final var presentation = new FrontendSemanticPresentationSpec( + List.of(new FrontendVisualThemeSpec( + "demo-theme", + "Demo Theme", + new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"), + List.of(new FrontendTokenStyleSpec("demo-key", "#eeeeee", false, false, false)))), + "demo-theme", + List.of(new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "demo-key", + "keyword", + List.of("declaration"), + "variable", + List.of()))))); + + final var vscodeProjection = presentation.requireHostProjection("vscode").requireProjection("demo-key"); + assertEquals("keyword", vscodeProjection.hostTokenType()); + assertEquals(List.of("declaration"), vscodeProjection.hostTokenModifiers()); + assertEquals("variable", vscodeProjection.fallbackTokenType()); + } + + @Test + void shouldRejectDuplicateHostIds() { + assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( + List.of(new FrontendVisualThemeSpec( + "demo-theme", + "Demo Theme", + new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"), + List.of(new FrontendTokenStyleSpec("demo-key", "#eeeeee", false, false, false)))), + "demo-theme", + List.of( + new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "demo-key", + "keyword", + List.of(), + "variable", + List.of()))), + new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "demo-key", + "type", + List.of(), + "variable", + List.of())))))); + } + + @Test + void shouldRejectProjectionForUnknownSemanticKey() { + assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( + List.of(new FrontendVisualThemeSpec( + "demo-theme", + "Demo Theme", + new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"), + List.of(new FrontendTokenStyleSpec("demo-key", "#eeeeee", false, false, false)))), + "demo-theme", + List.of(new FrontendHostProjectionSpec( + "vscode", + List.of(new FrontendHostProjectionEntrySpec( + "unknown-key", + "keyword", + List.of(), + "variable", + List.of())))))); + } + + @Test + void shouldRejectThemeSetThatDriftsFromDefaultThemeVocabulary() { + assertThrows(IllegalArgumentException.class, () -> new FrontendSemanticPresentationSpec( + List.of( + new FrontendVisualThemeSpec( + "demo-theme", + "Demo Theme", + new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"), + List.of(new FrontendTokenStyleSpec("demo-key", "#eeeeee", false, false, false))), + new FrontendVisualThemeSpec( + "demo-theme-2", + "Demo Theme 2", + new FrontendEditorPaletteSpec("#111111", "#eeeeee", "#333333", "#777777", "#111111", "#222222", "#eeeeee"), + List.of(new FrontendTokenStyleSpec("other-key", "#eeeeee", false, false, false)))), + "demo-theme", + List.of())); + } +} diff --git a/prometeu-lsp/prometeu-lsp-api/build.gradle.kts b/prometeu-lsp/prometeu-lsp-api/build.gradle.kts index cc7fab83..ef87e22b 100644 --- a/prometeu-lsp/prometeu-lsp-api/build.gradle.kts +++ b/prometeu-lsp/prometeu-lsp-api/build.gradle.kts @@ -3,5 +3,4 @@ plugins { } dependencies { - implementation(project(":prometeu-vfs:prometeu-vfs-api")) } diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/LspService.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/LspService.java deleted file mode 100644 index 7c6ca613..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/LspService.java +++ /dev/null @@ -1,21 +0,0 @@ -package p.studio.lsp; - -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.*; -import p.studio.vfs.VfsProjectDocument; - -public interface LspService extends AutoCloseable { - LspProjectContext projectContext(); - - VfsProjectDocument projectDocumentVfs(); - - LspSessionStateDTO snapshot(); - - LspAnalyzeDocumentResult analyzeDocument(LspAnalyzeDocumentRequest request); - - LspDefinitionResult definition(LspDefinitionRequest request); - - @Override - default void close() { - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/LspServiceFactory.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/LspServiceFactory.java deleted file mode 100644 index 69e6c4f6..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/LspServiceFactory.java +++ /dev/null @@ -1,8 +0,0 @@ -package p.studio.lsp; - -import p.studio.lsp.messages.LspProjectContext; -import p.studio.vfs.VfsProjectDocument; - -public interface LspServiceFactory { - LspService open(LspProjectContext projectContext, VfsProjectDocument vfsProjectDocument); -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/api/LspServerLifecycle.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/api/LspServerLifecycle.java new file mode 100644 index 00000000..564020b8 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/api/LspServerLifecycle.java @@ -0,0 +1,10 @@ +package p.studio.lsp.api; + +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.messages.LspServerConfiguration; + +public interface LspServerLifecycle { + void bootServer(LspServerConfiguration serverConfiguration, LspProjectContext context); + + void shutdownServer(LspProjectContext context); +} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspDefinitionTargetDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspDefinitionTargetDTO.java deleted file mode 100644 index 3eadeee4..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspDefinitionTargetDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package p.studio.lsp.dtos; - -import java.nio.file.Path; -import java.util.Objects; - -public record LspDefinitionTargetDTO( - String name, - Path documentPath, - LspRangeDTO range) { - - public LspDefinitionTargetDTO { - name = Objects.requireNonNull(name, "name"); - documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize(); - range = Objects.requireNonNull(range, "range"); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspDiagnosticDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspDiagnosticDTO.java deleted file mode 100644 index 3eb485e1..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspDiagnosticDTO.java +++ /dev/null @@ -1,24 +0,0 @@ -package p.studio.lsp.dtos; - -import p.studio.lsp.messages.LspDiagnosticSeverity; - -import java.nio.file.Path; -import java.util.Objects; - -public record LspDiagnosticDTO( - Path documentPath, - LspRangeDTO range, - LspDiagnosticSeverity severity, - String phase, - String code, - String message) { - - public LspDiagnosticDTO { - documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize(); - range = Objects.requireNonNull(range, "range"); - severity = Objects.requireNonNull(severity, "severity"); - phase = phase == null ? "" : phase; - code = code == null ? "" : code; - message = Objects.requireNonNull(message, "message"); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspHighlightSpanDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspHighlightSpanDTO.java deleted file mode 100644 index d0b01e3b..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspHighlightSpanDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package p.studio.lsp.dtos; - -import java.util.Objects; - -public record LspHighlightSpanDTO( - LspRangeDTO range, - String semanticKey) { - - public LspHighlightSpanDTO { - range = Objects.requireNonNull(range, "range"); - semanticKey = Objects.requireNonNull(semanticKey, "semanticKey"); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspInlineHintDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspInlineHintDTO.java deleted file mode 100644 index 44e170f8..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspInlineHintDTO.java +++ /dev/null @@ -1,18 +0,0 @@ -package p.studio.lsp.dtos; - -import java.util.Objects; - -public record LspInlineHintDTO( - LspRangeDTO anchor, - String label, - String category) { - - public LspInlineHintDTO { - anchor = Objects.requireNonNull(anchor, "anchor"); - label = Objects.requireNonNull(label, "label").trim(); - category = Objects.requireNonNull(category, "category").trim(); - if (label.isBlank()) { - throw new IllegalArgumentException("label cannot be blank"); - } - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspRangeDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspRangeDTO.java deleted file mode 100644 index 108af2bd..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspRangeDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -package p.studio.lsp.dtos; - -public record LspRangeDTO( - int startOffset, - int endOffset) { - - public LspRangeDTO { - startOffset = Math.max(0, startOffset); - endOffset = Math.max(startOffset, endOffset); - } - - public boolean contains(final int offset) { - return startOffset <= offset && offset < endOffset; - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSemanticPresentationDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSemanticPresentationDTO.java deleted file mode 100644 index d31eaf88..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSemanticPresentationDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package p.studio.lsp.dtos; - -import java.util.List; -import java.util.Objects; - -public record LspSemanticPresentationDTO( - List semanticKeys, - List resources) { - - public LspSemanticPresentationDTO { - semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys")); - resources = List.copyOf(Objects.requireNonNull(resources, "resources")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSessionStateDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSessionStateDTO.java deleted file mode 100644 index 91e758b7..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSessionStateDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package p.studio.lsp.dtos; - -import java.util.List; -import java.util.Objects; - -public record LspSessionStateDTO( - boolean semanticReadReady, - List declaredCapabilities) { - - public LspSessionStateDTO { - declaredCapabilities = List.copyOf(Objects.requireNonNull(declaredCapabilities, "declaredCapabilities")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspStructuralAnchorDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspStructuralAnchorDTO.java deleted file mode 100644 index 334f6f8b..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspStructuralAnchorDTO.java +++ /dev/null @@ -1,18 +0,0 @@ -package p.studio.lsp.dtos; - -import java.util.List; -import java.util.Objects; - -public record LspStructuralAnchorDTO( - LspRangeDTO range, - LspRangeDTO startAnchor, - LspRangeDTO endAnchor, - List children) { - - public LspStructuralAnchorDTO { - range = Objects.requireNonNull(range, "range"); - startAnchor = Objects.requireNonNull(startAnchor, "startAnchor"); - endAnchor = Objects.requireNonNull(endAnchor, "endAnchor"); - children = List.copyOf(Objects.requireNonNull(children, "children")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSymbolDTO.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSymbolDTO.java deleted file mode 100644 index 124b69a0..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/dtos/LspSymbolDTO.java +++ /dev/null @@ -1,23 +0,0 @@ -package p.studio.lsp.dtos; - -import p.studio.lsp.messages.LspSymbolKind; - -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; - -public record LspSymbolDTO( - String name, - LspSymbolKind kind, - Path documentPath, - LspRangeDTO range, - List children) { - - public LspSymbolDTO { - name = Objects.requireNonNull(name, "name"); - kind = Objects.requireNonNull(kind, "kind"); - documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize(); - range = Objects.requireNonNull(range, "range"); - children = List.copyOf(Objects.requireNonNull(children, "children")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEvent.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEvent.java deleted file mode 100644 index 7a218128..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package p.studio.lsp.events; - -import java.util.Objects; - -public record LspEvent( - LspEventKind kind, - String message) { - - public LspEvent { - Objects.requireNonNull(kind, "kind"); - Objects.requireNonNull(message, "message"); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEventKind.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEventKind.java deleted file mode 100644 index 94afdb08..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEventKind.java +++ /dev/null @@ -1,5 +0,0 @@ -package p.studio.lsp.events; - -public enum LspEventKind { - SESSION_READY -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEventSink.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEventSink.java deleted file mode 100644 index eca8e3c5..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/events/LspEventSink.java +++ /dev/null @@ -1,5 +0,0 @@ -package p.studio.lsp.events; - -public interface LspEventSink { - void publish(LspEvent event); -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentRequest.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentRequest.java deleted file mode 100644 index c5d3c5d2..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package p.studio.lsp.messages; - -import java.nio.file.Path; -import java.util.Objects; - -public record LspAnalyzeDocumentRequest( - Path documentPath) { - - public LspAnalyzeDocumentRequest { - documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize(); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java deleted file mode 100644 index 5c647e80..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspAnalyzeDocumentResult.java +++ /dev/null @@ -1,34 +0,0 @@ -package p.studio.lsp.messages; - -import p.studio.lsp.dtos.LspDiagnosticDTO; -import p.studio.lsp.dtos.LspHighlightSpanDTO; -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; - -import java.util.List; -import java.util.Objects; - -public record LspAnalyzeDocumentResult( - LspSessionStateDTO sessionState, - LspSemanticPresentationDTO semanticPresentation, - List diagnostics, - List semanticHighlights, - List inlineHints, - List documentSymbols, - List structuralAnchors, - List workspaceSymbols) { - - public LspAnalyzeDocumentResult { - Objects.requireNonNull(sessionState, "sessionState"); - Objects.requireNonNull(semanticPresentation, "semanticPresentation"); - diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); - semanticHighlights = List.copyOf(Objects.requireNonNull(semanticHighlights, "semanticHighlights")); - inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints")); - documentSymbols = List.copyOf(Objects.requireNonNull(documentSymbols, "documentSymbols")); - structuralAnchors = List.copyOf(Objects.requireNonNull(structuralAnchors, "structuralAnchors")); - workspaceSymbols = List.copyOf(Objects.requireNonNull(workspaceSymbols, "workspaceSymbols")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDefinitionRequest.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDefinitionRequest.java deleted file mode 100644 index af205092..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDefinitionRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package p.studio.lsp.messages; - -import java.nio.file.Path; -import java.util.Objects; - -public record LspDefinitionRequest( - Path documentPath, - int offset) { - - public LspDefinitionRequest { - documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize(); - offset = Math.max(0, offset); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDefinitionResult.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDefinitionResult.java deleted file mode 100644 index 8067702c..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDefinitionResult.java +++ /dev/null @@ -1,19 +0,0 @@ -package p.studio.lsp.messages; - -import p.studio.lsp.dtos.LspDefinitionTargetDTO; - -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; - -public record LspDefinitionResult( - Path documentPath, - int offset, - List targets) { - - public LspDefinitionResult { - documentPath = Objects.requireNonNull(documentPath, "documentPath").toAbsolutePath().normalize(); - offset = Math.max(0, offset); - targets = List.copyOf(Objects.requireNonNull(targets, "targets")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDiagnosticSeverity.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDiagnosticSeverity.java deleted file mode 100644 index 2481f6b1..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspDiagnosticSeverity.java +++ /dev/null @@ -1,6 +0,0 @@ -package p.studio.lsp.messages; - -public enum LspDiagnosticSeverity { - ERROR, - WARNING -} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspProjectContext.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspProjectContext.java index 85b44151..aa9dfcb9 100644 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspProjectContext.java +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspProjectContext.java @@ -4,13 +4,23 @@ import java.nio.file.Path; import java.util.Objects; public record LspProjectContext( - String projectName, + String projectKey, String languageId, - Path rootPath) { + Path projectRoot) { public LspProjectContext { - Objects.requireNonNull(projectName, "projectName"); - Objects.requireNonNull(languageId, "languageId"); - rootPath = Objects.requireNonNull(rootPath, "rootPath").toAbsolutePath().normalize(); + projectKey = requireText(projectKey, "projectKey"); + languageId = requireText(languageId, "languageId"); + projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize(); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; } } diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspServerConfiguration.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspServerConfiguration.java new file mode 100644 index 00000000..9c4344a5 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspServerConfiguration.java @@ -0,0 +1,19 @@ +package p.studio.lsp.messages; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class LspServerConfiguration { + public static final String DEFAULT_HOST = "127.0.0.1"; + public static final int DEFAULT_PORT = 7775; + + @Builder.Default + private final LspServerEndpoint endpoint = new LspServerEndpoint(DEFAULT_HOST, DEFAULT_PORT); + + + public static LspServerConfiguration defaults() { + return LspServerConfiguration.builder().build(); + } +} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspServerEndpoint.java b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspServerEndpoint.java new file mode 100644 index 00000000..d8bdebed --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspServerEndpoint.java @@ -0,0 +1,19 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record LspServerEndpoint( + String host, + int port) { + + public LspServerEndpoint { + final String candidate = Objects.requireNonNull(host, "host").trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException("host must not be blank"); + } + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("port must be between 0 and 65535"); + } + host = candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-api/src/test/java/p/studio/lsp/dtos/LspInlineHintDTOTest.java b/prometeu-lsp/prometeu-lsp-api/src/test/java/p/studio/lsp/dtos/LspInlineHintDTOTest.java deleted file mode 100644 index f4489a17..00000000 --- a/prometeu-lsp/prometeu-lsp-api/src/test/java/p/studio/lsp/dtos/LspInlineHintDTOTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package p.studio.lsp.dtos; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -final class LspInlineHintDTOTest { - @Test - void trimsLabelAndCategory() { - final var hint = new LspInlineHintDTO(new LspRangeDTO(3, 3), " int ", " type "); - - assertEquals("int", hint.label()); - assertEquals("type", hint.category()); - } - - @Test - void rejectsBlankLabels() { - assertThrows(IllegalArgumentException.class, () -> - new LspInlineHintDTO(new LspRangeDTO(1, 1), " ", "type")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts b/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts index 37704dc3..c2b12b6f 100644 --- a/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts +++ b/prometeu-lsp/prometeu-lsp-v1/build.gradle.kts @@ -4,13 +4,11 @@ plugins { dependencies { implementation(project(":prometeu-lsp:prometeu-lsp-api")) - implementation(project(":prometeu-vfs:prometeu-vfs-api")) implementation(project(":prometeu-compiler:prometeu-compiler-core")) implementation(project(":prometeu-compiler:prometeu-frontend-api")) - implementation(project(":prometeu-compiler:prometeu-frontend-registry")) - implementation(project(":prometeu-compiler:prometeu-deps")) implementation(project(":prometeu-compiler:prometeu-build-pipeline")) implementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs")) - - testImplementation(project(":prometeu-vfs:prometeu-vfs-v1")) + implementation(project(":prometeu-compiler:prometeu-frontend-registry")) + implementation(project(":prometeu-compiler:prometeu-deps")) + implementation(libs.lsp4j) } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java deleted file mode 100644 index 73380dbf..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticAnalyseService.java +++ /dev/null @@ -1,27 +0,0 @@ -package p.studio.lsp; - -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.lsp.messages.SemanticSession; - -import java.util.List; - -import static p.studio.lsp.LspSemanticUtilities.normalize; - -class LspSemanticAnalyseService { - LspAnalyzeDocumentResult analyze( - final SemanticSession session, - final LspAnalyzeDocumentRequest request) { - final var normalizedRequestedDocument = normalize(request.documentPath()); - return new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("diagnostics", "symbols", "definition", "highlight", "structural-anchors")), - session.semanticPresentation(), - session.diagnosticsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), - session.semanticHighlightsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), - session.inlineHintsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), - session.documentSymbolsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), - session.structuralAnchorsByDocument().getOrDefault(normalizedRequestedDocument, List.of()), - session.workspaceSymbols()); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticDefinitionService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticDefinitionService.java deleted file mode 100644 index b727c278..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticDefinitionService.java +++ /dev/null @@ -1,46 +0,0 @@ -package p.studio.lsp; - -import p.studio.compiler.pbs.lexer.PbsToken; -import p.studio.compiler.pbs.lexer.PbsTokenKind; -import p.studio.lsp.dtos.LspDefinitionTargetDTO; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspDefinitionRequest; -import p.studio.lsp.messages.LspDefinitionResult; -import p.studio.lsp.messages.SemanticSession; - -import java.nio.file.Path; -import java.util.List; - -import static p.studio.lsp.LspSemanticUtilities.normalize; -import static p.studio.lsp.LspSemanticUtilities.tokenAt; - -class LspSemanticDefinitionService { - - LspDefinitionResult definition( - final SemanticSession session, - final LspDefinitionRequest request) { - final List targets = resolveDefinitionTargets(session, request); - return new LspDefinitionResult(request.documentPath(), request.offset(), targets); - } - - private List resolveDefinitionTargets( - final SemanticSession session, - final LspDefinitionRequest request) { - final Path documentPath = normalize(request.documentPath()); - final List tokens = session.tokensByDocument().getOrDefault(documentPath, List.of()); - final PbsToken activeToken = tokenAt(tokens, request.offset()); - if (activeToken == null || activeToken.kind() != PbsTokenKind.IDENTIFIER) { - return List.of(); - } - final String lexeme = activeToken.lexeme(); - final List sameDocumentMatches = session.symbolsByName().getOrDefault(lexeme, List.of()).stream() - .filter(symbol -> symbol.documentPath().equals(documentPath)) - .toList(); - final List effectiveMatches = sameDocumentMatches.isEmpty() - ? session.symbolsByName().getOrDefault(lexeme, List.of()) - : sameDocumentMatches; - return effectiveMatches.stream() - .map(symbol -> new LspDefinitionTargetDTO(symbol.name(), symbol.documentPath(), symbol.range())) - .toList(); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java deleted file mode 100644 index 0163745c..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticReadPhase.java +++ /dev/null @@ -1,353 +0,0 @@ -package p.studio.lsp; - -import p.studio.compiler.FrontendRegistryService; -import p.studio.compiler.messages.*; -import p.studio.compiler.models.AnalysisSnapshot; -import p.studio.compiler.models.BuilderPipelineContext; -import p.studio.compiler.models.FrontendSpec; -import p.studio.compiler.models.FrontendSemanticPresentationSpec; -import p.studio.compiler.models.SourceHandle; -import p.studio.compiler.pbs.ast.PbsAst; -import p.studio.compiler.pbs.lexer.PbsLexer; -import p.studio.compiler.pbs.parser.PbsParser; -import p.studio.compiler.source.diagnostics.Diagnostic; -import p.studio.compiler.source.diagnostics.DiagnosticSink; -import p.studio.compiler.source.identifiers.FileId; -import p.studio.compiler.services.PBSFrontendPhaseService; -import p.studio.compiler.services.PbsInlineHintSurface; -import p.studio.compiler.workspaces.AssetSurfaceContextLoader; -import p.studio.compiler.workspaces.PipelineStage; -import p.studio.compiler.workspaces.stages.LoadSourcesPipelineStage; -import p.studio.compiler.workspaces.stages.ResolveDepsPipelineStage; -import p.studio.lsp.dtos.LspDiagnosticDTO; -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.lsp.messages.*; -import p.studio.lsp.models.AnalysisRuntimeSnapshot; -import p.studio.lsp.models.SemanticIndex; -import p.studio.utilities.logs.LogAggregator; -import p.studio.vfs.VfsProjectDocument; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import static p.studio.lsp.LspSemanticUtilities.*; - -final class LspSemanticReadPhase { - private final LspSemanticAnalyseService analyseService; - private final LspSemanticDefinitionService definitionService; - - LspSemanticReadPhase() { - this.analyseService = new LspSemanticAnalyseService(); - this.definitionService = new LspSemanticDefinitionService(); - } - - public LspAnalyzeDocumentResult analyze( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument, - final LspAnalyzeDocumentRequest request) { - final var session = buildSession(projectContext, vfsProjectDocument, request.documentPath()); - return analyseService.analyze(session, request); - } - - public LspDefinitionResult definition( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument, - final LspDefinitionRequest request) { - final var session = buildSession(projectContext, vfsProjectDocument, request.documentPath()); - return definitionService.definition(session, request); - } - - public static SemanticSession buildSession( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument, - final Path requestedDocumentPath) { - final BuilderPipelineConfig config = new BuilderPipelineConfig( - false, - projectContext.rootPath().toString(), - "core-v1", - new OverlaySourceProviderFactoryImpl(vfsProjectDocument, requestedDocumentPath)); - final BuilderPipelineContext context = BuilderPipelineContext.fromConfig(config); - final AnalysisRuntimeSnapshot snapshot = runAnalysisStages(context); - return index(snapshot, context, requestedDocumentPath); - } - - private static AnalysisRuntimeSnapshot runAnalysisStages(final BuilderPipelineContext context) { - final LogAggregator logs = LogAggregator.empty(); - final List diagnostics = new ArrayList<>(); - final List stages = List.of( - new ResolveDepsPipelineStage(), - new LoadSourcesPipelineStage()); - for (final PipelineStage stage : stages) { - final BuildingIssueSink stageIssues = stage.run(context, logs); - diagnostics.addAll(stageIssues.asCollection()); - if (stageIssues.hasErrors()) { - break; - } - } - final DiagnosticSink frontendDiagnostics = DiagnosticSink.empty(); - FrontendSpec frontendSpec = FrontendRegistryService.getDefaultFrontendSpec(); - if (context.resolvedWorkspace != null && context.fileTable != null) { - final BuildingIssueSink frontendIssues = runFrontendPhase(context, logs, frontendDiagnostics); - diagnostics.addAll(frontendIssues.asCollection()); - frontendSpec = context.resolvedWorkspace.frontendSpec(); - } - return new AnalysisRuntimeSnapshot( - new AnalysisSnapshot( - frontendSpec, - diagnostics, - context.resolvedWorkspace, - context.fileTable, - context.irBackend), - List.copyOf(frontendDiagnostics.asCollection())); - } - - private static BuildingIssueSink runFrontendPhase( - final BuilderPipelineContext context, - final LogAggregator logs, - final DiagnosticSink frontendDiagnostics) { - final var frontendSpec = context.resolvedWorkspace.frontendSpec(); - final var service = FrontendRegistryService.getFrontendPhaseService(frontendSpec.getLanguageId()); - if (service.isEmpty()) { - return BuildingIssueSink.empty().report(builder -> builder - .error(true) - .message("[BUILD]: unable to find a service for frontend phase: " + frontendSpec.getLanguageId())); - } - final FESurfaceContext feSurfaceContext = new AssetSurfaceContextLoader().load(context.resolvedWorkspace.mainProject().getRootPath()); - final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext( - context.resolvedWorkspace.graph().projectTable(), - context.fileTable, - context.resolvedWorkspace.stack(), - context.resolvedWorkspace.stdlib(), - HostAdmissionContext.permissiveDefault(), - feSurfaceContext); - final BuildingIssueSink issues = BuildingIssueSink.empty(); - context.irBackend = service.get().compile(frontendPhaseContext, frontendDiagnostics, logs, issues); - return issues; - } - - private static SemanticSession index( - final AnalysisRuntimeSnapshot runtimeSnapshot, - final BuilderPipelineContext context, - final Path requestedDocumentPath) { - final AnalysisSnapshot snapshot = runtimeSnapshot.analysisSnapshot(); - final Map> diagnosticsByDocument = diagnosticsByDocument( - snapshot.diagnostics(), - snapshot, - runtimeSnapshot.frontendDiagnostics()); - final SemanticIndex semanticIndex = new SemanticIndex(); - if (snapshot.fileTable() == null) { - return new SemanticSession( - normalize(requestedDocumentPath), - semanticPresentation(snapshot.frontendSpec()), - diagnosticsByDocument, - Map.of(), - Map.of(), - Map.of(), - Map.of(), - List.of(), - Map.of(), - Map.of()); - } - final Map> importedSupplementalTopDeclsByFile = importedSupplementalTopDeclsByFile( - snapshot.frontendSpec(), - context); - final Map> inlineHintsByDocument = inlineHintsByDocument( - snapshot, - snapshot.frontendSpec(), - context); - final List indexedDocuments = new ArrayList<>(); - for (final FileId fileId : snapshot.fileTable()) { - final SourceHandle sourceHandle = snapshot.fileTable().get(fileId); - if (!isSourceRelated(snapshot.frontendSpec(), sourceHandle)) { - continue; - } - final String source = sourceHandle.readUtf8().orElse(""); - final DiagnosticSink diagnostics = DiagnosticSink.empty(); - final var tokens = PbsLexer.lex(source, fileId, diagnostics); - final PbsAst.File ast = PbsParser.parse(tokens, fileId, diagnostics, PbsParser.ParseMode.ORDINARY); - final Path documentPath = sourceHandle.getCanonPath(); - semanticIndex.registerDocument(documentPath, ast, tokens.asList()); - indexedDocuments.add(new IndexedDocument( - fileId, - documentPath, - importedSupplementalTopDeclsByFile.getOrDefault(fileId, List.of()))); - } - for (final IndexedDocument indexedDocument : indexedDocuments) { - semanticIndex.buildHighlights( - indexedDocument.documentPath(), - indexedDocument.visibleImportedTopDecls()); - } - return new SemanticSession( - normalize(requestedDocumentPath), - semanticPresentation(snapshot.frontendSpec()), - diagnosticsByDocument, - semanticIndex.semanticHighlightsByDocument(), - inlineHintsByDocument, - semanticIndex.documentSymbolsByDocument(), - semanticIndex.structuralAnchorsByDocument(), - semanticIndex.workspaceSymbols(), - semanticIndex.symbolsByName(), - semanticIndex.tokensByDocument()); - } - - private static Map> importedSupplementalTopDeclsByFile( - final FrontendSpec frontendSpec, - final BuilderPipelineContext context) { - final var surface = semanticReadSurface(frontendSpec, context); - final Map> byFile = new LinkedHashMap<>(); - for (final var entry : surface.supplementalTopDeclsByFile().entrySet()) { - byFile.put(entry.getKey(), entry.getValue().asList()); - } - return Map.copyOf(byFile); - } - - private static Map> inlineHintsByDocument( - final AnalysisSnapshot snapshot, - final FrontendSpec frontendSpec, - final BuilderPipelineContext context) { - if (snapshot.fileTable() == null) { - return Map.of(); - } - final var surface = semanticReadSurface(frontendSpec, context); - final Map> byDocument = new LinkedHashMap<>(); - for (final var entry : surface.inlineHintsByFile().entrySet()) { - final SourceHandle sourceHandle = snapshot.fileTable().get(entry.getKey()); - final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize(); - final var hints = entry.getValue().asList().stream() - .map(LspSemanticReadPhase::toInlineHintDTO) - .toList(); - if (!hints.isEmpty()) { - byDocument.put(documentPath, hints); - } - } - return Map.copyOf(byDocument); - } - - private static PBSFrontendPhaseService.PbsSemanticReadSurface semanticReadSurface( - final FrontendSpec frontendSpec, - final BuilderPipelineContext context) { - if (context.resolvedWorkspace == null || context.fileTable == null || !"pbs".equals(frontendSpec.getLanguageId())) { - return new PBSFrontendPhaseService.PbsSemanticReadSurface(Map.of(), Map.of()); - } - final FESurfaceContext feSurfaceContext = new AssetSurfaceContextLoader().load(context.resolvedWorkspace.mainProject().getRootPath()); - final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext( - context.resolvedWorkspace.graph().projectTable(), - context.fileTable, - context.resolvedWorkspace.stack(), - context.resolvedWorkspace.stdlib(), - HostAdmissionContext.permissiveDefault(), - feSurfaceContext); - final var diagnostics = DiagnosticSink.empty(); - final var issues = BuildingIssueSink.empty(); - return PBSFrontendPhaseService.semanticReadSurface( - frontendPhaseContext, - diagnostics, - issues); - } - - private static LspInlineHintDTO toInlineHintDTO(final PbsInlineHintSurface hint) { - return new LspInlineHintDTO( - new LspRangeDTO( - (int) hint.anchorSpan().getStart(), - (int) hint.anchorSpan().getEnd()), - hint.label(), - hint.category()); - } - - private record IndexedDocument( - FileId fileId, - Path documentPath, - List visibleImportedTopDecls) { - } - - private static boolean isSourceRelated(final FrontendSpec frontendSpec, final SourceHandle sourceHandle) { - return frontendSpec.getAllowedExtensions().contains(sourceHandle.getExtension()); - } - - private static LspSemanticPresentationDTO semanticPresentation(final FrontendSpec frontendSpec) { - final FrontendSemanticPresentationSpec presentation = frontendSpec.getSemanticPresentation(); - return new LspSemanticPresentationDTO(presentation.semanticKeys(), presentation.resources()); - } - - private static Map> diagnosticsByDocument( - final List issues, - final AnalysisSnapshot snapshot, - final List frontendDiagnostics) { - final Map> diagnosticsByDocument = new LinkedHashMap<>(); - diagnosticIssues(issues, snapshot, diagnosticsByDocument); - diagnosticFrontend(frontendDiagnostics, snapshot, diagnosticsByDocument); - return freezeMapOfLists(diagnosticsByDocument); - } - - private static boolean isValidIssueDiagnostic( - final AnalysisSnapshot snapshot, - final BuildingIssue issue) { - if (snapshot.fileTable() == null || issue.getFileId() == null || issue.getFileId() < 0) { - return false; - } - return issue.getFileId() < snapshot.fileTable().size(); - } - - private static void diagnosticIssues( - final List issues, - final AnalysisSnapshot snapshot, - final Map> diagnosticsByDocument) { - for (final var issue : issues) { - if (!isValidIssueDiagnostic(snapshot, issue)) { - continue; - } - final SourceHandle sourceHandle = snapshot.fileTable().get(new FileId(issue.getFileId())); - final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize(); - diagnosticsByDocument - .computeIfAbsent(documentPath, ignored -> new ArrayList<>()) - .add(new LspDiagnosticDTO( - documentPath, - new LspRangeDTO( - safeOffset(issue.getStart()), - safeEnd(issue.getStart(), issue.getEnd())), - issue.isError() - ? LspDiagnosticSeverity.ERROR - : LspDiagnosticSeverity.WARNING, - issue.getPhase(), - issue.getCode(), - issue.getMessage())); - } - } - - private static boolean isValidFEDiagnostic( - final AnalysisSnapshot snapshot, - final Diagnostic diagnostic) { - return snapshot.fileTable() != null && !diagnostic.getSpan().getFileId().isNone(); - } - - private static void diagnosticFrontend( - final List feDiagnostics, - final AnalysisSnapshot snapshot, - final Map> diagnosticsByDocument) { - for (final var feDiagnostic : feDiagnostics) { - if (!isValidFEDiagnostic(snapshot, feDiagnostic)) { - continue; - } - final SourceHandle sourceHandle = snapshot.fileTable().get(feDiagnostic.getSpan().getFileId()); - final Path documentPath = sourceHandle.getCanonPath().toAbsolutePath().normalize(); - diagnosticsByDocument - .computeIfAbsent(documentPath, ignored -> new ArrayList<>()) - .add(new LspDiagnosticDTO( - documentPath, - new LspRangeDTO( - (int) feDiagnostic.getSpan().getStart(), - (int) feDiagnostic.getSpan().getEnd()), - feDiagnostic.getSeverity().isError() - ? LspDiagnosticSeverity.ERROR - : LspDiagnosticSeverity.WARNING, - feDiagnostic.getPhase().name(), - feDiagnostic.getCode(), - feDiagnostic.getMessage())); - } - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticUtilities.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticUtilities.java deleted file mode 100644 index b3894df1..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspSemanticUtilities.java +++ /dev/null @@ -1,52 +0,0 @@ -package p.studio.lsp; - -import lombok.experimental.UtilityClass; -import p.studio.compiler.pbs.lexer.PbsToken; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -@UtilityClass -public class LspSemanticUtilities { - - public static PbsToken tokenAt(final List tokens, final int offset) { - for (final PbsToken token : tokens) { - if (token.start() <= offset && offset < token.end()) { - return token; - } - } - return null; - } - - public static Path normalize(final Path path) { - final Path normalized = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - try { - return Files.exists(normalized) ? normalized.toRealPath() : normalized; - } catch (IOException ignored) { - return normalized; - } - } - - public static Map> freezeMapOfLists(final Map> mutable) { - final Map> frozen = new LinkedHashMap<>(); - for (final var entry : mutable.entrySet()) { - frozen.put(entry.getKey(), List.copyOf(entry.getValue())); - } - return Map.copyOf(frozen); - } - - public static int safeOffset(final Integer value) { - return value == null ? 0 : Math.max(0, value); - } - - public static int safeEnd(final Integer start, final Integer end) { - final int safeStart = safeOffset(start); - final int safeEnd = safeOffset(end); - return Math.max(safeStart, safeEnd); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspServiceFactoryImpl.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspServiceFactoryImpl.java deleted file mode 100644 index 79abcb93..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspServiceFactoryImpl.java +++ /dev/null @@ -1,17 +0,0 @@ -package p.studio.lsp; - -import p.studio.lsp.messages.LspProjectContext; -import p.studio.vfs.VfsProjectDocument; - -import java.util.Objects; - -public final class LspServiceFactoryImpl implements LspServiceFactory { - @Override - public LspService open( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument) { - return new LspServiceImpl( - Objects.requireNonNull(projectContext, "projectContext"), - Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument")); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspServiceImpl.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspServiceImpl.java deleted file mode 100644 index e85596c3..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/LspServiceImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -package p.studio.lsp; - -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.*; -import p.studio.vfs.VfsProjectDocument; - -import java.util.List; -import java.util.Objects; - -public final class LspServiceImpl implements LspService { - private final LspProjectContext projectContext; - private final VfsProjectDocument vfsProjectDocument; - - private final LspSemanticReadPhase semanticReadPhase; - - public LspServiceImpl( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument) { - this.projectContext = Objects.requireNonNull(projectContext, "projectContext"); - this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); - this.semanticReadPhase = new LspSemanticReadPhase(); - } - - @Override - public LspProjectContext projectContext() { - return projectContext; - } - - @Override - public VfsProjectDocument projectDocumentVfs() { - return vfsProjectDocument; - } - - @Override - public LspSessionStateDTO snapshot() { - return new LspSessionStateDTO( - true, - List.of("diagnostics", "symbols", "definition", "highlight")); - } - - @Override - public LspAnalyzeDocumentResult analyzeDocument(final LspAnalyzeDocumentRequest request) { - return semanticReadPhase.analyze(projectContext, vfsProjectDocument, request); - } - - @Override - public LspDefinitionResult definition(final LspDefinitionRequest request) { - return semanticReadPhase.definition(projectContext, vfsProjectDocument, request); - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/OverlaySourceProviderFactoryImpl.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/OverlaySourceProviderFactoryImpl.java deleted file mode 100644 index e6ef0ac3..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/OverlaySourceProviderFactoryImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -package p.studio.lsp; - -import p.studio.compiler.utilities.SourceProvider; -import p.studio.compiler.utilities.SourceProviderFactory; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.VfsProjectDocument; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Objects; - -final class OverlaySourceProviderFactoryImpl implements SourceProviderFactory { - private final VfsProjectDocument vfsProjectDocument; - private final Path openedDocumentPath; - - OverlaySourceProviderFactoryImpl( - final VfsProjectDocument vfsProjectDocument, - final Path openedDocumentPath) { - this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); - this.openedDocumentPath = openedDocumentPath == null - ? null - : normalize(openedDocumentPath); - } - - @Override - public SourceProvider create(final Path path) { - final Path normalizedPath = normalize(path); - return () -> read(normalizedPath); - } - - private byte[] read(final Path path) throws IOException { - if (openedDocumentPath != null && openedDocumentPath.equals(path)) { - final VfsDocumentOpenResult result = vfsProjectDocument.openDocument(path); - if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) { - return textDocument.content().getBytes(StandardCharsets.UTF_8); - } - } - return Files.readAllBytes(path); - } - - private Path normalize(final Path path) { - final Path normalized = path.toAbsolutePath().normalize(); - try { - return Files.exists(normalized) ? normalized.toRealPath() : normalized; - } catch (IOException ignored) { - return normalized; - } - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletion.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletion.java new file mode 100644 index 00000000..335191cd --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletion.java @@ -0,0 +1,12 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineCompletion( + boolean incomplete, + List items) { + public BaselineCompletion { + items = List.copyOf(Objects.requireNonNull(items, "items")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItem.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItem.java new file mode 100644 index 00000000..6dc80510 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItem.java @@ -0,0 +1,30 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineCompletionItem( + String label, + BaselineCompletionItemKind kind, + String detail, + String documentation) { + public BaselineCompletionItem { + label = requireText(label, "label"); + kind = Objects.requireNonNull(kind, "kind"); + detail = normalize(detail); + documentation = normalize(documentation); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = normalize(value); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java similarity index 61% rename from prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java rename to prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java index 3f65c287..28da0724 100644 --- a/prometeu-lsp/prometeu-lsp-api/src/main/java/p/studio/lsp/messages/LspSymbolKind.java +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineCompletionItemKind.java @@ -1,24 +1,20 @@ package p.studio.lsp.messages; -public enum LspSymbolKind { +public enum BaselineCompletionItemKind { + KEYWORD, + VARIABLE, + FIELD, FUNCTION, METHOD, CONSTRUCTOR, - IF, - SWITCH, - FOR, - WHILE, - HANDLE, - GLOBAL, - CONST, STRUCT, - CONTRACT, - HOST, BUILTIN_TYPE, SERVICE, - ERROR, - ENUM, + HOST, + CONTRACT, CALLBACK, - IMPLEMENTS, - UNKNOWN + ENUM, + ERROR, + GLOBAL, + CONST } diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentAnalysis.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentAnalysis.java new file mode 100644 index 00000000..1c43c4eb --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentAnalysis.java @@ -0,0 +1,12 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineDocumentAnalysis( + List issues) { + + public BaselineDocumentAnalysis { + issues = List.copyOf(Objects.requireNonNull(issues, "issues")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentIssue.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentIssue.java new file mode 100644 index 00000000..7ee61cb9 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineDocumentIssue.java @@ -0,0 +1,29 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineDocumentIssue( + int startLine, + int startCharacter, + int endLine, + int endCharacter, + BaselineIssueSeverity severity, + String source, + String message) { + + public BaselineDocumentIssue { + severity = Objects.requireNonNull(severity, "severity"); + source = requireText(source, "source"); + message = requireText(message, "message"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineEditorPalette.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineEditorPalette.java new file mode 100644 index 00000000..0e638149 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineEditorPalette.java @@ -0,0 +1,33 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineEditorPalette( + String baseForeground, + String selectionBackground, + String activeHighlightBackground, + String lineNumberForeground, + String statusChipBackground, + String statusChipBorder, + String statusChipForeground) { + + public BaselineEditorPalette { + baseForeground = requireText(baseForeground, "baseForeground"); + selectionBackground = requireText(selectionBackground, "selectionBackground"); + activeHighlightBackground = requireText(activeHighlightBackground, "activeHighlightBackground"); + lineNumberForeground = requireText(lineNumberForeground, "lineNumberForeground"); + statusChipBackground = requireText(statusChipBackground, "statusChipBackground"); + statusChipBorder = requireText(statusChipBorder, "statusChipBorder"); + statusChipForeground = requireText(statusChipForeground, "statusChipForeground"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineHover.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineHover.java new file mode 100644 index 00000000..5b795586 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineHover.java @@ -0,0 +1,15 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineHover( + String markdown) { + + public BaselineHover { + final String candidate = Objects.requireNonNull(markdown, "markdown").trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException("markdown must not be blank"); + } + markdown = candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineIssueSeverity.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineIssueSeverity.java new file mode 100644 index 00000000..2624a846 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineIssueSeverity.java @@ -0,0 +1,7 @@ +package p.studio.lsp.messages; + +public enum BaselineIssueSeverity { + INFORMATION, + WARNING, + ERROR +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjection.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjection.java new file mode 100644 index 00000000..ed617665 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjection.java @@ -0,0 +1,27 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSemanticHostProjection( + String hostId, + List tokenProjections) { + + public BaselineSemanticHostProjection { + hostId = requireText(hostId, "hostId"); + tokenProjections = List.copyOf(Objects.requireNonNull(tokenProjections, "tokenProjections")); + if (tokenProjections.isEmpty()) { + throw new IllegalArgumentException("tokenProjections must not be empty"); + } + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjectionEntry.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjectionEntry.java new file mode 100644 index 00000000..406332e8 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticHostProjectionEntry.java @@ -0,0 +1,38 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSemanticHostProjectionEntry( + String semanticKey, + String hostTokenType, + List hostTokenModifiers, + String fallbackTokenType, + List fallbackTokenModifiers) { + + public BaselineSemanticHostProjectionEntry { + semanticKey = requireText(semanticKey, "semanticKey"); + hostTokenType = requireText(hostTokenType, "hostTokenType"); + hostTokenModifiers = normalizeList(hostTokenModifiers, "hostTokenModifiers"); + fallbackTokenType = requireText(fallbackTokenType, "fallbackTokenType"); + fallbackTokenModifiers = normalizeList(fallbackTokenModifiers, "fallbackTokenModifiers"); + } + + private static List normalizeList( + final List values, + final String field) { + return List.copyOf(Objects.requireNonNull(values, field).stream() + .map(value -> requireText(value, field + " entry")) + .toList()); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticToken.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticToken.java new file mode 100644 index 00000000..f7efb7e5 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticToken.java @@ -0,0 +1,33 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineSemanticToken( + int line, + int startCharacter, + int length, + String semanticKey) { + + public BaselineSemanticToken { + if (line < 0) { + throw new IllegalArgumentException("line must be >= 0"); + } + if (startCharacter < 0) { + throw new IllegalArgumentException("startCharacter must be >= 0"); + } + if (length <= 0) { + throw new IllegalArgumentException("length must be > 0"); + } + semanticKey = requireText(semanticKey, "semanticKey"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticTokens.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticTokens.java new file mode 100644 index 00000000..b20f4d35 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSemanticTokens.java @@ -0,0 +1,14 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSemanticTokens( + List legend, + List tokens) { + + public BaselineSemanticTokens { + legend = List.copyOf(Objects.requireNonNull(legend, "legend")); + tokens = List.copyOf(Objects.requireNonNull(tokens, "tokens")); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java new file mode 100644 index 00000000..96a5461c --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineServerDescription.java @@ -0,0 +1,46 @@ +package p.studio.lsp.messages; + +import java.util.Objects; +import java.util.List; + +public record BaselineServerDescription( + String name, + String version, + String frontendLanguageId, + boolean hoverSupported, + boolean completionSupported, + boolean signatureHelpSupported, + List semanticTokenTypes, + List semanticHostProjections, + List visualThemes, + String activeVisualThemeId) { + + public BaselineServerDescription { + name = requireText(name, "name"); + version = requireText(version, "version"); + frontendLanguageId = requireText(frontendLanguageId, "frontendLanguageId"); + semanticTokenTypes = List.copyOf(Objects.requireNonNull(semanticTokenTypes, "semanticTokenTypes")); + semanticHostProjections = List.copyOf(Objects.requireNonNull(semanticHostProjections, "semanticHostProjections")); + visualThemes = List.copyOf(Objects.requireNonNull(visualThemes, "visualThemes")); + activeVisualThemeId = requireText(activeVisualThemeId, "activeVisualThemeId"); + final var hostIds = new java.util.LinkedHashSet(); + for (final BaselineSemanticHostProjection hostProjection : semanticHostProjections) { + if (!hostIds.add(hostProjection.hostId())) { + throw new IllegalArgumentException("duplicate semantic host projection for hostId: " + hostProjection.hostId()); + } + } + if (visualThemes.stream().map(BaselineVisualTheme::themeId).noneMatch(activeVisualThemeId::equals)) { + throw new IllegalArgumentException("activeVisualThemeId must reference one of the declared visual themes"); + } + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignature.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignature.java new file mode 100644 index 00000000..6ba774de --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignature.java @@ -0,0 +1,29 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSignature( + String label, + List parameters, + String documentation) { + public BaselineSignature { + label = requireText(label, "label"); + parameters = List.copyOf(Objects.requireNonNull(parameters, "parameters")); + documentation = normalize(documentation); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = normalize(value); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } + + private static String normalize(final String value) { + return value == null ? "" : value.trim(); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignatureHelp.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignatureHelp.java new file mode 100644 index 00000000..12bb6a4e --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineSignatureHelp.java @@ -0,0 +1,19 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineSignatureHelp( + List signatures, + int activeSignature, + int activeParameter) { + public BaselineSignatureHelp { + signatures = List.copyOf(Objects.requireNonNull(signatures, "signatures")); + if (!signatures.isEmpty() && (activeSignature < 0 || activeSignature >= signatures.size())) { + throw new IllegalArgumentException("activeSignature must reference one of the provided signatures"); + } + if (activeParameter < 0) { + throw new IllegalArgumentException("activeParameter must be >= 0"); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTokenStyle.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTokenStyle.java new file mode 100644 index 00000000..7fbcaab1 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineTokenStyle.java @@ -0,0 +1,26 @@ +package p.studio.lsp.messages; + +import java.util.Objects; + +public record BaselineTokenStyle( + String semanticKey, + String foreground, + boolean italic, + boolean bold, + boolean underline) { + + public BaselineTokenStyle { + semanticKey = requireText(semanticKey, "semanticKey"); + foreground = requireText(foreground, "foreground"); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineVisualTheme.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineVisualTheme.java new file mode 100644 index 00000000..63145a86 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/BaselineVisualTheme.java @@ -0,0 +1,31 @@ +package p.studio.lsp.messages; + +import java.util.List; +import java.util.Objects; + +public record BaselineVisualTheme( + String themeId, + String displayName, + BaselineEditorPalette editorPalette, + List tokenStyles) { + + public BaselineVisualTheme { + themeId = requireText(themeId, "themeId"); + displayName = requireText(displayName, "displayName"); + editorPalette = Objects.requireNonNull(editorPalette, "editorPalette"); + tokenStyles = List.copyOf(Objects.requireNonNull(tokenStyles, "tokenStyles")); + if (tokenStyles.isEmpty()) { + throw new IllegalArgumentException("tokenStyles must not be empty"); + } + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/DocumentPosition.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/DocumentPosition.java new file mode 100644 index 00000000..b3ab68ef --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/DocumentPosition.java @@ -0,0 +1,6 @@ +package p.studio.lsp.messages; + +public record DocumentPosition( + int line, + int character) { +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java deleted file mode 100644 index 3d3f23f9..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/messages/SemanticSession.java +++ /dev/null @@ -1,26 +0,0 @@ -package p.studio.lsp.messages; - -import p.studio.compiler.pbs.lexer.PbsToken; -import p.studio.lsp.dtos.LspDiagnosticDTO; -import p.studio.lsp.dtos.LspHighlightSpanDTO; -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; - -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -public record SemanticSession( - Path requestedDocumentPath, - LspSemanticPresentationDTO semanticPresentation, - Map> diagnosticsByDocument, - Map> semanticHighlightsByDocument, - Map> inlineHintsByDocument, - Map> documentSymbolsByDocument, - Map> structuralAnchorsByDocument, - List workspaceSymbols, - Map> symbolsByName, - Map> tokensByDocument) { -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/AnalysisRuntimeSnapshot.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/AnalysisRuntimeSnapshot.java deleted file mode 100644 index 65a6e234..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/AnalysisRuntimeSnapshot.java +++ /dev/null @@ -1,11 +0,0 @@ -package p.studio.lsp.models; - -import p.studio.compiler.models.AnalysisSnapshot; -import p.studio.compiler.source.diagnostics.Diagnostic; - -import java.util.List; - -public record AnalysisRuntimeSnapshot( - AnalysisSnapshot analysisSnapshot, - List frontendDiagnostics) { -} \ No newline at end of file diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java deleted file mode 100644 index 1649d505..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/models/SemanticIndex.java +++ /dev/null @@ -1,635 +0,0 @@ -package p.studio.lsp.models; - -import p.studio.compiler.pbs.PbsSemanticKind; -import p.studio.compiler.pbs.ast.PbsAst; -import p.studio.compiler.pbs.lexer.PbsToken; -import p.studio.compiler.pbs.lexer.PbsTokenKind; -import p.studio.compiler.source.Span; -import p.studio.lsp.dtos.LspHighlightSpanDTO; -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -import static p.studio.lsp.LspSemanticUtilities.normalize; - -public final class SemanticIndex { - private final Map> semanticHighlightsByDocument = new LinkedHashMap<>(); - private final Map> documentSymbolsByDocument = new LinkedHashMap<>(); - private final Map> structuralAnchorsByDocument = new LinkedHashMap<>(); - private final List workspaceSymbols = new ArrayList<>(); - private final Map> symbolsByName = new LinkedHashMap<>(); - private final Map> tokensByDocument = new LinkedHashMap<>(); - - public void index( - final Path documentPath, - final PbsAst.File ast, - final List tokens) { - index(documentPath, ast, tokens, List.of()); - } - - public void index( - final Path documentPath, - final PbsAst.File ast, - final List tokens, - final List visibleImportedTopDecls) { - registerDocument(documentPath, ast, tokens); - buildHighlights(documentPath, visibleImportedTopDecls); - } - - public void registerDocument( - final Path documentPath, - final PbsAst.File ast, - final List tokens) { - final Path normalizedDocumentPath = normalize(documentPath); - tokensByDocument.put(normalizedDocumentPath, List.copyOf(tokens)); - final List documentSymbols = new ArrayList<>(); - for (final PbsAst.TopDecl topDecl : ast.topDecls()) { - final LspSymbolDTO symbol = symbolForTopDecl(normalizedDocumentPath, topDecl); - if (symbol == null) { - continue; - } - documentSymbols.add(symbol); - workspaceSymbols.add(symbol); - symbolsByName.computeIfAbsent(symbol.name(), ignored -> new ArrayList<>()).add(symbol); - for (final LspSymbolDTO child : symbol.children()) { - symbolsByName.computeIfAbsent(child.name(), ignored -> new ArrayList<>()).add(child); - } - } - documentSymbolsByDocument.put(normalizedDocumentPath, List.copyOf(documentSymbols)); - structuralAnchorsByDocument.put( - normalizedDocumentPath, - structuralAnchors(documentSymbols, tokens)); - } - - public void buildHighlights( - final Path documentPath, - final List visibleImportedTopDecls) { - final Path normalizedDocumentPath = normalize(documentPath); - final List tokens = tokensByDocument.getOrDefault(normalizedDocumentPath, List.of()); - final Map visibleKindsByName = new LinkedHashMap<>(); - for (final var entry : symbolsByName.entrySet()) { - if (entry.getValue().isEmpty()) { - continue; - } - visibleKindsByName.putIfAbsent(entry.getKey(), entry.getValue().getFirst().kind()); - } - for (final LspSymbolDTO symbol : documentSymbolsByDocument.getOrDefault(normalizedDocumentPath, List.of())) { - visibleKindsByName.putIfAbsent(symbol.name(), symbol.kind()); - for (final LspSymbolDTO child : symbol.children()) { - visibleKindsByName.putIfAbsent(child.name(), child.kind()); - } - } - for (final PbsAst.TopDecl topDecl : visibleImportedTopDecls) { - final LspSymbolKind visibleKind = symbolKindForTopDecl(topDecl); - final String visibleName = topDeclName(topDecl); - if (visibleKind == null || visibleName == null || visibleName.isBlank()) { - continue; - } - visibleKindsByName.putIfAbsent(visibleName, visibleKind); - } - semanticHighlightsByDocument.put( - normalizedDocumentPath, - buildSemanticHighlights(tokens, visibleKindsByName)); - } - - private List buildSemanticHighlights( - final List tokens, - final Map visibleKindsByName) { - final List highlights = new ArrayList<>(); - for (int index = 0; index < tokens.size(); index++) { - final PbsToken token = tokens.get(index); - if (token.kind() == PbsTokenKind.EOF) { - continue; - } - final String semanticKey = semanticKey(tokens, index, visibleKindsByName); - if (semanticKey == null || semanticKey.isBlank()) { - continue; - } - highlights.add(new LspHighlightSpanDTO( - new LspRangeDTO(token.start(), token.end()), - semanticKey)); - } - return List.copyOf(highlights); - } - - private String semanticKey( - final List tokens, - final int tokenIndex, - final Map visibleKindsByName) { - final PbsToken token = tokens.get(tokenIndex); - if (isLifecycleAttributeToken(tokens, tokenIndex)) { - return PbsSemanticKind.LIFECYCLE.semanticKey(); - } - final PbsSemanticKind semanticKind = token.kind() == p.studio.compiler.pbs.lexer.PbsTokenKind.IDENTIFIER - ? semanticKindForIdentifier(token.lexeme(), visibleKindsByName) - : PbsSemanticKind.forToken(token); - return semanticKind == null ? null : semanticKind.semanticKey(); - } - - private boolean isLifecycleAttributeToken(final List tokens, final int tokenIndex) { - if (tokenIndex < 0 || tokenIndex >= tokens.size()) { - return false; - } - final PbsToken token = tokens.get(tokenIndex); - return switch (token.kind()) { - case LEFT_BRACKET -> hasLifecycleAttributeWindow(tokens, tokenIndex, tokenIndex + 1, tokenIndex + 2); - case IDENTIFIER -> hasLifecycleAttributeWindow(tokens, tokenIndex - 1, tokenIndex, tokenIndex + 1); - case RIGHT_BRACKET -> hasLifecycleAttributeWindow(tokens, tokenIndex - 2, tokenIndex - 1, tokenIndex); - default -> false; - }; - } - - private boolean hasLifecycleAttributeWindow( - final List tokens, - final int leftBracketIndex, - final int identifierIndex, - final int rightBracketIndex) { - if (leftBracketIndex < 0 || rightBracketIndex >= tokens.size()) { - return false; - } - return tokens.get(leftBracketIndex).kind() == PbsTokenKind.LEFT_BRACKET - && tokens.get(identifierIndex).kind() == PbsTokenKind.IDENTIFIER - && isLifecycleAttributeName(tokens.get(identifierIndex).lexeme()) - && tokens.get(rightBracketIndex).kind() == PbsTokenKind.RIGHT_BRACKET; - } - - private boolean isLifecycleAttributeName(final String lexeme) { - return "Init".equals(lexeme) || "Frame".equals(lexeme); - } - - private PbsSemanticKind semanticKindForIdentifier( - final String lexeme, - final Map visibleKindsByName) { - final LspSymbolKind kind = visibleKindsByName.get(lexeme); - if (kind == null) { - return PbsSemanticKind.IDENTIFIER; - } - return switch (kind) { - case FUNCTION -> PbsSemanticKind.FUNCTION; - case METHOD -> PbsSemanticKind.METHOD; - case CONSTRUCTOR -> PbsSemanticKind.CONSTRUCTOR; - case STRUCT -> PbsSemanticKind.STRUCT; - case CONTRACT -> PbsSemanticKind.CONTRACT; - case HOST -> PbsSemanticKind.HOST; - case BUILTIN_TYPE -> PbsSemanticKind.BUILTIN_TYPE; - case SERVICE -> PbsSemanticKind.SERVICE; - case ERROR -> PbsSemanticKind.ERROR; - case ENUM -> PbsSemanticKind.ENUM; - case CALLBACK -> PbsSemanticKind.CALLBACK; - case GLOBAL -> PbsSemanticKind.GLOBAL; - case CONST -> PbsSemanticKind.CONST; - case IMPLEMENTS -> PbsSemanticKind.IMPLEMENTS; - default -> PbsSemanticKind.IDENTIFIER; - }; - } - - private String topDeclName(final PbsAst.TopDecl topDecl) { - if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { - return functionDecl.name(); - } - if (topDecl instanceof PbsAst.StructDecl structDecl) { - return structDecl.name(); - } - if (topDecl instanceof PbsAst.ContractDecl contractDecl) { - return contractDecl.name(); - } - if (topDecl instanceof PbsAst.HostDecl hostDecl) { - return hostDecl.name(); - } - if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { - return builtinTypeDecl.name(); - } - if (topDecl instanceof PbsAst.ServiceDecl serviceDecl) { - return serviceDecl.name(); - } - if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { - return errorDecl.name(); - } - if (topDecl instanceof PbsAst.EnumDecl enumDecl) { - return enumDecl.name(); - } - if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { - return callbackDecl.name(); - } - if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { - return globalDecl.name(); - } - if (topDecl instanceof PbsAst.ConstDecl constDecl) { - return constDecl.name(); - } - if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) { - return implementsDecl.binderName(); - } - return null; - } - - private LspSymbolKind symbolKindForTopDecl(final PbsAst.TopDecl topDecl) { - if (topDecl instanceof PbsAst.FunctionDecl) { - return LspSymbolKind.FUNCTION; - } - if (topDecl instanceof PbsAst.StructDecl) { - return LspSymbolKind.STRUCT; - } - if (topDecl instanceof PbsAst.ContractDecl) { - return LspSymbolKind.CONTRACT; - } - if (topDecl instanceof PbsAst.HostDecl) { - return LspSymbolKind.HOST; - } - if (topDecl instanceof PbsAst.BuiltinTypeDecl) { - return LspSymbolKind.BUILTIN_TYPE; - } - if (topDecl instanceof PbsAst.ServiceDecl) { - return LspSymbolKind.SERVICE; - } - if (topDecl instanceof PbsAst.ErrorDecl) { - return LspSymbolKind.ERROR; - } - if (topDecl instanceof PbsAst.EnumDecl) { - return LspSymbolKind.ENUM; - } - if (topDecl instanceof PbsAst.CallbackDecl) { - return LspSymbolKind.CALLBACK; - } - if (topDecl instanceof PbsAst.GlobalDecl) { - return LspSymbolKind.GLOBAL; - } - if (topDecl instanceof PbsAst.ConstDecl) { - return LspSymbolKind.CONST; - } - if (topDecl instanceof PbsAst.ImplementsDecl) { - return LspSymbolKind.IMPLEMENTS; - } - return null; - } - - private LspSymbolDTO symbolForTopDecl( - final Path documentPath, - final PbsAst.TopDecl topDecl) { - if (topDecl instanceof PbsAst.FunctionDecl functionDecl) { - return symbol( - documentPath, - functionDecl.name(), - LspSymbolKind.FUNCTION, - functionDecl.span(), - localSymbolsInBlock(documentPath, functionDecl.body())); - } - if (topDecl instanceof PbsAst.StructDecl structDecl) { - return symbol(documentPath, structDecl.name(), LspSymbolKind.STRUCT, structDecl.span(), structChildren(documentPath, structDecl)); - } - if (topDecl instanceof PbsAst.ContractDecl( - String name, p.studio.utilities.structures.ReadOnlyList signatures, Span span - )) { - return symbol(documentPath, name, LspSymbolKind.CONTRACT, span, signatureChildren(documentPath, signatures.asList())); - } - if (topDecl instanceof PbsAst.HostDecl( - String name, p.studio.utilities.structures.ReadOnlyList signatures, Span span - )) { - return symbol(documentPath, name, LspSymbolKind.HOST, span, signatureChildren(documentPath, signatures.asList())); - } - if (topDecl instanceof PbsAst.BuiltinTypeDecl builtinTypeDecl) { - return symbol(documentPath, builtinTypeDecl.name(), LspSymbolKind.BUILTIN_TYPE, builtinTypeDecl.span(), signatureChildren(documentPath, builtinTypeDecl.signatures().asList())); - } - if (topDecl instanceof PbsAst.ServiceDecl( - String name, p.studio.utilities.structures.ReadOnlyList methods, Span span - )) { - return symbol(documentPath, name, LspSymbolKind.SERVICE, span, functionChildren(documentPath, methods.asList())); - } - if (topDecl instanceof PbsAst.ErrorDecl errorDecl) { - return symbol(documentPath, errorDecl.name(), LspSymbolKind.ERROR, errorDecl.span(), List.of()); - } - if (topDecl instanceof PbsAst.EnumDecl enumDecl) { - return symbol(documentPath, enumDecl.name(), LspSymbolKind.ENUM, enumDecl.span(), List.of()); - } - if (topDecl instanceof PbsAst.CallbackDecl callbackDecl) { - return symbol(documentPath, callbackDecl.name(), LspSymbolKind.CALLBACK, callbackDecl.span(), List.of()); - } - if (topDecl instanceof PbsAst.GlobalDecl globalDecl) { - return symbol(documentPath, globalDecl.name(), LspSymbolKind.GLOBAL, globalDecl.span(), List.of()); - } - if (topDecl instanceof PbsAst.ConstDecl constDecl) { - return symbol(documentPath, constDecl.name(), LspSymbolKind.CONST, constDecl.span(), List.of()); - } - if (topDecl instanceof PbsAst.ImplementsDecl implementsDecl) { - return symbol(documentPath, implementsDecl.binderName(), LspSymbolKind.IMPLEMENTS, implementsDecl.span(), functionChildren(documentPath, implementsDecl.methods().asList())); - } - return null; - } - - private List structChildren( - final Path documentPath, - final PbsAst.StructDecl structDecl) { - final List children = new ArrayList<>(functionChildren(documentPath, structDecl.methods().asList())); - for (final PbsAst.CtorDecl ctorDecl : structDecl.ctors()) { - children.add(symbol( - documentPath, - ctorDecl.name(), - LspSymbolKind.CONSTRUCTOR, - ctorDecl.span(), - localSymbolsInBlock(documentPath, ctorDecl.body()))); - } - return List.copyOf(children); - } - - private List functionChildren( - final Path documentPath, - final List functions) { - final List children = new ArrayList<>(); - for (final PbsAst.FunctionDecl functionDecl : functions) { - children.add(symbol( - documentPath, - functionDecl.name(), - LspSymbolKind.METHOD, - functionDecl.span(), - localSymbolsInBlock(documentPath, functionDecl.body()))); - } - return List.copyOf(children); - } - - private List localSymbolsInBlock( - final Path documentPath, - final PbsAst.Block block) { - final List children = new ArrayList<>(); - for (final PbsAst.Statement statement : block.statements().asList()) { - children.addAll(localSymbolsInStatement(documentPath, statement)); - } - if (block.tailExpression() != null) { - children.addAll(localSymbolsInExpression(documentPath, block.tailExpression())); - } - return List.copyOf(children); - } - - private List localSymbolsInStatement( - final Path documentPath, - final PbsAst.Statement statement) { - return switch (statement) { - case PbsAst.LetStatement letStatement -> localSymbolsInExpression(documentPath, letStatement.initializer()); - case PbsAst.AssignStatement assignStatement -> localSymbolsInExpression(documentPath, assignStatement.value()); - case PbsAst.ReturnStatement returnStatement -> localSymbolsInNullableExpression(documentPath, returnStatement.value()); - case PbsAst.IfStatement ifStatement -> List.of(symbol( - documentPath, - "if", - LspSymbolKind.IF, - ifStatement.span(), - children( - localSymbolsInBlock(documentPath, ifStatement.thenBlock()), - ifStatement.elseIf() == null ? List.of() : localSymbolsInStatement(documentPath, ifStatement.elseIf()), - ifStatement.elseBlock() == null ? List.of() : localSymbolsInBlock(documentPath, ifStatement.elseBlock())))); - case PbsAst.ForStatement forStatement -> List.of(symbol( - documentPath, - "for " + forStatement.iteratorName(), - LspSymbolKind.FOR, - forStatement.span(), - localSymbolsInBlock(documentPath, forStatement.body()))); - case PbsAst.WhileStatement whileStatement -> List.of(symbol( - documentPath, - "while", - LspSymbolKind.WHILE, - whileStatement.span(), - localSymbolsInBlock(documentPath, whileStatement.body()))); - case PbsAst.ExpressionStatement expressionStatement -> localSymbolsInExpression(documentPath, expressionStatement.expression()); - case PbsAst.BreakStatement ignored -> List.of(); - case PbsAst.ContinueStatement ignored -> List.of(); - }; - } - - private List localSymbolsInExpression( - final Path documentPath, - final PbsAst.Expression expression) { - return switch (expression) { - case PbsAst.IfExpr ifExpr -> List.of(symbol( - documentPath, - "if", - LspSymbolKind.IF, - ifExpr.span(), - children( - localSymbolsInBlock(documentPath, ifExpr.thenBlock()), - localSymbolsInExpression(documentPath, ifExpr.elseExpression())))); - case PbsAst.SwitchExpr switchExpr -> List.of(symbol( - documentPath, - "switch", - LspSymbolKind.SWITCH, - switchExpr.span(), - childrenFromSwitchArms(documentPath, switchExpr.arms().asList()))); - case PbsAst.HandleExpr handleExpr -> List.of(symbol( - documentPath, - "handle", - LspSymbolKind.HANDLE, - handleExpr.span(), - childrenFromHandleArms(documentPath, handleExpr.arms().asList()))); - case PbsAst.BlockExpr blockExpr -> localSymbolsInBlock(documentPath, blockExpr.block()); - case PbsAst.ElseExpr elseExpr -> children( - localSymbolsInExpression(documentPath, elseExpr.optionalExpression()), - localSymbolsInExpression(documentPath, elseExpr.fallbackExpression())); - case PbsAst.UnaryExpr unaryExpr -> localSymbolsInExpression(documentPath, unaryExpr.expression()); - case PbsAst.BinaryExpr binaryExpr -> children( - localSymbolsInExpression(documentPath, binaryExpr.left()), - localSymbolsInExpression(documentPath, binaryExpr.right())); - case PbsAst.ApplyExpr applyExpr -> children( - localSymbolsInExpression(documentPath, applyExpr.callee()), - localSymbolsInExpression(documentPath, applyExpr.argument())); - case PbsAst.AsExpr asExpr -> localSymbolsInExpression(documentPath, asExpr.expression()); - case PbsAst.CallExpr callExpr -> { - final List> nested = new ArrayList<>(); - nested.add(localSymbolsInExpression(documentPath, callExpr.callee())); - for (final PbsAst.Expression argument : callExpr.arguments().asList()) { - nested.add(localSymbolsInExpression(documentPath, argument)); - } - yield children(nested.toArray(List[]::new)); - } - case PbsAst.MemberExpr memberExpr -> localSymbolsInExpression(documentPath, memberExpr.receiver()); - case PbsAst.PropagateExpr propagateExpr -> localSymbolsInExpression(documentPath, propagateExpr.expression()); - case PbsAst.GroupExpr groupExpr -> localSymbolsInExpression(documentPath, groupExpr.expression()); - case PbsAst.NewExpr newExpr -> { - final List> nested = new ArrayList<>(); - for (final PbsAst.Expression argument : newExpr.arguments().asList()) { - nested.add(localSymbolsInExpression(documentPath, argument)); - } - yield children(nested.toArray(List[]::new)); - } - case PbsAst.BindExpr bindExpr -> localSymbolsInExpression(documentPath, bindExpr.contextExpression()); - case PbsAst.SomeExpr someExpr -> localSymbolsInExpression(documentPath, someExpr.value()); - case PbsAst.OkExpr okExpr -> localSymbolsInExpression(documentPath, okExpr.value()); - case PbsAst.TupleExpr tupleExpr -> { - final List> nested = new ArrayList<>(); - for (final PbsAst.TupleItem item : tupleExpr.items().asList()) { - nested.add(localSymbolsInExpression(documentPath, item.expression())); - } - yield children(nested.toArray(List[]::new)); - } - case PbsAst.IdentifierExpr ignored -> List.of(); - case PbsAst.IntLiteralExpr ignored -> List.of(); - case PbsAst.FloatLiteralExpr ignored -> List.of(); - case PbsAst.BoundedLiteralExpr ignored -> List.of(); - case PbsAst.StringLiteralExpr ignored -> List.of(); - case PbsAst.BoolLiteralExpr ignored -> List.of(); - case PbsAst.ThisExpr ignored -> List.of(); - case PbsAst.NoneExpr ignored -> List.of(); - case PbsAst.ErrExpr ignored -> List.of(); - case PbsAst.UnitExpr ignored -> List.of(); - }; - } - - private List childrenFromSwitchArms( - final Path documentPath, - final List arms) { - final List children = new ArrayList<>(); - for (final PbsAst.SwitchArm arm : arms) { - children.addAll(localSymbolsInBlock(documentPath, arm.block())); - } - return List.copyOf(children); - } - - private List childrenFromHandleArms( - final Path documentPath, - final List arms) { - final List children = new ArrayList<>(); - for (final PbsAst.HandleArm arm : arms) { - children.addAll(localSymbolsInBlock(documentPath, arm.block())); - } - return List.copyOf(children); - } - - private List localSymbolsInNullableExpression( - final Path documentPath, - final PbsAst.Expression expression) { - return expression == null ? List.of() : localSymbolsInExpression(documentPath, expression); - } - - @SafeVarargs - private static List children(final List... parts) { - final List children = new ArrayList<>(); - for (final List part : parts) { - if (part == null || part.isEmpty()) { - continue; - } - children.addAll(part); - } - return List.copyOf(children); - } - - private List signatureChildren( - final Path documentPath, - final List signatures) { - final List children = new ArrayList<>(); - for (final PbsAst.FunctionSignature signature : signatures) { - children.add(symbol(documentPath, signature.name(), LspSymbolKind.METHOD, signature.span(), List.of())); - } - return List.copyOf(children); - } - - private LspSymbolDTO symbol( - final Path documentPath, - final String name, - final LspSymbolKind kind, - final Span span, - final List children) { - return new LspSymbolDTO( - Objects.requireNonNullElse(name, kind.name().toLowerCase()), - kind, - documentPath, - new LspRangeDTO((int) span.getStart(), (int) span.getEnd()), - children); - } - - private List structuralAnchors( - final List documentSymbols, - final List tokens) { - final List anchors = new ArrayList<>(); - for (final LspSymbolDTO symbol : documentSymbols) { - final LspStructuralAnchorDTO anchor = structuralAnchor(symbol, tokens); - if (anchor != null) { - anchors.add(anchor); - } - } - return List.copyOf(anchors); - } - - private LspStructuralAnchorDTO structuralAnchor( - final LspSymbolDTO symbol, - final List tokens) { - final List children = new ArrayList<>(); - for (final LspSymbolDTO child : symbol.children()) { - final LspStructuralAnchorDTO anchorChild = structuralAnchor(child, tokens); - if (anchorChild != null) { - children.add(anchorChild); - } - } - final AnchorPair anchorPair = anchorPair(symbol.range(), tokens); - if (anchorPair == null) { - if (children.isEmpty()) { - return null; - } - return new LspStructuralAnchorDTO(symbol.range(), symbol.range(), symbol.range(), children); - } - return new LspStructuralAnchorDTO(symbol.range(), anchorPair.start(), anchorPair.end(), children); - } - - private AnchorPair anchorPair( - final LspRangeDTO range, - final List tokens) { - PbsToken firstLeftBrace = null; - PbsToken lastRightBrace = null; - for (final PbsToken token : tokens) { - if (token.kind() == PbsTokenKind.EOF) { - continue; - } - if (token.start() < range.startOffset() || token.end() > range.endOffset()) { - continue; - } - if (token.kind() == PbsTokenKind.LEFT_BRACE && firstLeftBrace == null) { - firstLeftBrace = token; - } - if (token.kind() == PbsTokenKind.RIGHT_BRACE) { - lastRightBrace = token; - } - } - if (firstLeftBrace == null || lastRightBrace == null || lastRightBrace.start() < firstLeftBrace.start()) { - return null; - } - return new AnchorPair( - new LspRangeDTO(firstLeftBrace.start(), firstLeftBrace.end()), - new LspRangeDTO(lastRightBrace.start(), lastRightBrace.end())); - } - - public Map> semanticHighlightsByDocument() { - return Map.copyOf(semanticHighlightsByDocument); - } - - public Map> documentSymbolsByDocument() { - return Map.copyOf(documentSymbolsByDocument); - } - - public Map> structuralAnchorsByDocument() { - return Map.copyOf(structuralAnchorsByDocument); - } - - public List workspaceSymbols() { - return List.copyOf(workspaceSymbols); - } - - public Map> symbolsByName() { - final Map> frozen = new LinkedHashMap<>(); - for (final var entry : symbolsByName.entrySet()) { - frozen.put(entry.getKey(), List.copyOf(entry.getValue())); - } - return Map.copyOf(frozen); - } - - public Map> tokensByDocument() { - return Map.copyOf(tokensByDocument); - } - - private record AnchorPair( - LspRangeDTO start, - LspRangeDTO end) { - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java new file mode 100644 index 00000000..f7e7d543 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LanguageServiceBridge.java @@ -0,0 +1,25 @@ +package p.studio.lsp.services; + +import p.studio.lsp.messages.BaselineDocumentAnalysis; +import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineHover; +import p.studio.lsp.messages.BaselineSemanticTokens; +import p.studio.lsp.messages.BaselineSignatureHelp; +import p.studio.lsp.messages.BaselineServerDescription; +import p.studio.lsp.messages.LspProjectContext; + +public interface LanguageServiceBridge { + BaselineServerDescription describeServer(LspProjectContext context); + + BaselineDocumentAnalysis analyzeDocument(LspProjectContext context, String documentUri, String text); + + BaselineCompletion completion(LspProjectContext context, String documentUri, String text, int line, int character); + + BaselineHover hover(LspProjectContext context, String documentUri, String text, int line, int character); + + BaselineSignatureHelp signatureHelp(LspProjectContext context, String documentUri, String text, int line, int character); + + BaselineSemanticTokens semanticTokens(LspProjectContext context, String documentUri, String text); + + String onSave(LspProjectContext context, String documentUri); +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LspServerLifecycleImpl.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LspServerLifecycleImpl.java new file mode 100644 index 00000000..9a85283d --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/LspServerLifecycleImpl.java @@ -0,0 +1,57 @@ +package p.studio.lsp.services; + +import p.studio.lsp.api.LspServerLifecycle; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.messages.LspServerConfiguration; +import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public final class LspServerLifecycleImpl implements LspServerLifecycle { + private final TcpLspServerHostFactory serverHostFactory; + private final CompilerLanguageServiceBridge compilerBridge; + private final ConcurrentMap activeServers; + + public LspServerLifecycleImpl() { + this.compilerBridge = new CompilerLanguageServiceBridge(); + this.serverHostFactory = new TcpLspServerHostFactory(); + this.activeServers = new ConcurrentHashMap<>(); + } + + @Override + public void bootServer(final LspServerConfiguration serverConfiguration, LspProjectContext context) { + final var host = serverHostFactory.create(serverConfiguration, context, compilerBridge); + final var previous = activeServers.putIfAbsent(context.projectKey(), host); + if (previous != null) { + throw new IllegalStateException("LSP server already booted for project " + context.projectKey()); + } + + try { + host.start(); + } catch (RuntimeException runtimeException) { + activeServers.remove(context.projectKey(), host); + closeQuietly(host); + throw runtimeException; + } + } + + @Override + public void shutdownServer(final LspProjectContext project) { + final var safeProject = Objects.requireNonNull(project, "project"); + final var host = activeServers.remove(safeProject.projectKey()); + if (host == null) { + return; + } + closeQuietly(host); + } + + private static void closeQuietly(final TcpLspServerHost host) { + try { + host.close(); + } catch (Exception exception) { + throw new IllegalStateException("failed to close LSP host", exception); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/TcpLspServerHost.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/TcpLspServerHost.java new file mode 100644 index 00000000..0f7edc60 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/TcpLspServerHost.java @@ -0,0 +1,98 @@ +package p.studio.lsp.services; + +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.launch.LSPLauncher; +import org.eclipse.lsp4j.services.LanguageClient; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.messages.LspServerConfiguration; +import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge; +import p.studio.lsp.services.protocol.PrometeuLanguageServer; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public final class TcpLspServerHost { + private final LspServerConfiguration serverConfiguration; + private final LspProjectContext context; + private final CompilerLanguageServiceBridge compilerBridge; + private final ExecutorService clientExecutor; + private volatile boolean closed; + private volatile ServerSocket serverSocket; + private volatile Thread acceptThread; + + public TcpLspServerHost( + final LspServerConfiguration serverConfiguration, + final LspProjectContext context, + final CompilerLanguageServiceBridge compilerBridge) { + this.serverConfiguration = Objects.requireNonNull(serverConfiguration, "serverConfiguration"); + this.context = Objects.requireNonNull(context, "context"); + this.compilerBridge = Objects.requireNonNull(compilerBridge, "compilerBridge"); + this.clientExecutor = Executors.newCachedThreadPool(runnable -> { + final Thread thread = new Thread(runnable); + thread.setName("prometeu-lsp-v1-client"); + thread.setDaemon(true); + return thread; + }); + } + + public synchronized void start() { + if (serverSocket != null) { + return; + } + try { + final InetAddress bindAddress = InetAddress.getByName(serverConfiguration.getEndpoint().host()); + serverSocket = new ServerSocket(serverConfiguration.getEndpoint().port(), 50, bindAddress); + acceptThread = new Thread(this::acceptLoop, "prometeu-lsp-v1-accept"); + acceptThread.setDaemon(true); + acceptThread.start(); + } catch (IOException exception) { + throw new IllegalStateException("failed to start TCP LSP host", exception); + } + } + + private void acceptLoop() { + while (!closed) { + try { + final Socket socket = serverSocket.accept(); + clientExecutor.submit(() -> handleClient(socket)); + } catch (IOException exception) { + if (!closed) { + throw new IllegalStateException("failed to accept LSP client", exception); + } + } + } + } + + private void handleClient(final Socket socket) { + try (socket) { + final var server = new PrometeuLanguageServer(context, compilerBridge); + final Launcher launcher = LSPLauncher.createServerLauncher( + server, + socket.getInputStream(), + socket.getOutputStream()); + server.connect(launcher.getRemoteProxy()); + launcher.startListening().get(); + } catch (Exception exception) { + throw new IllegalStateException("LSP client failed", exception); + } + } + + public synchronized void close() throws Exception { + if (closed) { + return; + } + closed = true; + if (serverSocket != null) { + serverSocket.close(); + } + if (acceptThread != null) { + acceptThread.interrupt(); + } + clientExecutor.shutdownNow(); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/TcpLspServerHostFactory.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/TcpLspServerHostFactory.java new file mode 100644 index 00000000..1aaf9f0f --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/TcpLspServerHostFactory.java @@ -0,0 +1,14 @@ +package p.studio.lsp.services; + +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.messages.LspServerConfiguration; +import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge; + +public final class TcpLspServerHostFactory { + public TcpLspServerHost create( + final LspServerConfiguration serverConfiguration, + final LspProjectContext context, + final CompilerLanguageServiceBridge compilerBridge) { + return new TcpLspServerHost(serverConfiguration, context, compilerBridge); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java new file mode 100644 index 00000000..4e971e0c --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridge.java @@ -0,0 +1,422 @@ +package p.studio.lsp.services.compiler; + +import org.apache.commons.lang3.StringUtils; +import p.studio.compiler.FrontendRegistryService; +import p.studio.compiler.messages.BuilderPipelineConfig; +import p.studio.compiler.messages.BuildingIssueSink; +import p.studio.compiler.messages.FrontendPhaseContext; +import p.studio.compiler.messages.BuildingIssue; +import p.studio.compiler.messages.HostAdmissionContext; +import p.studio.compiler.models.AnalysisSnapshot; +import p.studio.compiler.models.BuilderPipelineContext; +import p.studio.compiler.models.FrontendEditorPaletteSpec; +import p.studio.compiler.models.FrontendHostProjectionEntrySpec; +import p.studio.compiler.models.FrontendHostProjectionSpec; +import p.studio.compiler.models.FrontendSemanticPresentationSpec; +import p.studio.compiler.models.FrontendSemanticToken; +import p.studio.compiler.models.FrontendTokenStyleSpec; +import p.studio.compiler.models.FrontendVisualThemeSpec; +import p.studio.compiler.source.identifiers.FileId; +import p.studio.compiler.services.PBSFrontendPhaseService; +import p.studio.compiler.workspaces.AssetSurfaceContextLoader; +import p.studio.compiler.utilities.SourceProviderFactory; +import p.studio.compiler.workspaces.BuilderPipelineService; +import p.studio.compiler.pbs.semantics.PbsEditorialCompletionCandidate; +import p.studio.compiler.pbs.semantics.PbsEditorialResolvedSymbol; +import p.studio.compiler.pbs.semantics.PbsEditorialSignature; +import p.studio.compiler.pbs.semantics.PbsEditorialSymbolKind; +import p.studio.compiler.pbs.semantics.PbsEditorialSupportService; +import p.studio.lsp.messages.*; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.utilities.logs.LogAggregator; +import p.studio.utilities.structures.ReadOnlyList; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public final class CompilerLanguageServiceBridge implements LanguageServiceBridge { + private final AssetSurfaceContextLoader assetSurfaceContextLoader = new AssetSurfaceContextLoader(); + private final PbsEditorialSupportService editorialSupportService = new PbsEditorialSupportService(); + + @Override + public BaselineServerDescription describeServer(final LspProjectContext context) { + Objects.requireNonNull(context, "context"); + final var frontend = frontendSpec(context); + final var presentation = frontend.getSemanticPresentation(); + return new BaselineServerDescription( + "Prometeu Studio LSP", + "0.1.0", + context.languageId(), + true, + true, + true, + presentation.semanticKeys(), + presentation.hostProjections().stream().map(this::mapSemanticHostProjection).toList(), + presentation.themes().stream().map(this::mapVisualTheme).toList(), + presentation.defaultThemeId()); + } + + @Override + public BaselineDocumentAnalysis analyzeDocument( + final LspProjectContext context, + final String documentUri, + final String text) { + Objects.requireNonNull(context, "context"); + final Path documentPath = normalizeDocumentPath(documentUri); + final String effectiveText = StringUtils.isBlank(text) ? "" : text; + final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText)); + return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText)); + } + + @Override + public BaselineCompletion completion( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + Objects.requireNonNull(context, "context"); + return editorialDocument(context, documentUri, text) + .map(document -> { + final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); + final List candidates = editorialSupportService.completion( + document.text(), + document.ast(), + document.supplementalTopDecls(), + document.feSurfaceContext(), + offset); + return new BaselineCompletion( + false, + candidates.stream().map(this::mapCompletionItem).toList()); + }) + .orElseGet(() -> new BaselineCompletion(false, List.of())); + } + + @Override + public BaselineHover hover( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + Objects.requireNonNull(context, "context"); + return editorialDocument(context, documentUri, text) + .flatMap(document -> { + final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); + return editorialSupportService.hover( + document.text(), + document.ast(), + document.supplementalTopDecls(), + document.feSurfaceContext(), + offset); + }) + .map(symbol -> new BaselineHover(formatHoverMarkdown(symbol))) + .orElseGet(() -> new BaselineHover("No symbol information.")); + } + + @Override + public BaselineSignatureHelp signatureHelp( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + Objects.requireNonNull(context, "context"); + return editorialDocument(context, documentUri, text) + .flatMap(document -> { + final int offset = new DocumentPositionMapper(document.text()).byteOffsetOf(line, character); + return editorialSupportService.signatureHelp( + document.text(), + document.ast(), + document.supplementalTopDecls(), + document.feSurfaceContext(), + offset); + }) + .map(signatureHelp -> new BaselineSignatureHelp( + signatureHelp.signatures().stream() + .map(signature -> new BaselineSignature(signature.label(), signature.parameterLabels(), "")) + .toList(), + signatureHelp.activeSignature(), + signatureHelp.activeParameter())) + .orElseGet(() -> new BaselineSignatureHelp(List.of(), 0, 0)); + } + + @Override + public BaselineSemanticTokens semanticTokens( + final LspProjectContext context, + final String documentUri, + final String text) { + Objects.requireNonNull(context, "context"); + final String effectiveText = text == null ? "" : text; + final var frontend = frontendSpec(context); + final FrontendSemanticPresentationSpec presentation = frontend.getSemanticPresentation(); + final List tokens = frontend.getSemanticTokenProvider().tokenize(effectiveText); + final var positionMapper = new DocumentPositionMapper(effectiveText); + final var semanticTokens = new ArrayList(); + + for (final FrontendSemanticToken token : tokens) { + final DocumentPosition start = positionMapper.positionOf(token.startOffset()); + final DocumentPosition end = positionMapper.positionOf(token.endOffset()); + final int length = Math.max(1, end.character() - start.character()); + semanticTokens.add(new BaselineSemanticToken( + start.line(), + start.character(), + length, + token.semanticKey())); + } + + return new BaselineSemanticTokens(presentation.semanticKeys(), semanticTokens); + } + + @Override + public String onSave( + final LspProjectContext context, + final String documentUri) { + Objects.requireNonNull(context, "context"); + return "Prometeu Studio saw save: " + documentUri; + } + + private AnalysisSnapshot analyzeProject( + final LspProjectContext context, + final Map overlays) { + final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig( + new BuilderPipelineConfig( + false, + context.projectRoot().toString(), + "core-v1", + SourceProviderFactory.overlayUtf8(overlays))); + return BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty()); + } + + private Optional editorialDocument( + final LspProjectContext context, + final String documentUri, + final String text) { + if (!"pbs".equals(context.languageId())) { + return Optional.empty(); + } + final Path documentPath = normalizeDocumentPath(documentUri); + final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig( + new BuilderPipelineConfig( + false, + context.projectRoot().toString(), + "core-v1", + SourceProviderFactory.overlayUtf8(text == null ? Map.of() : Map.of(documentPath, text)))); + BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty()); + final FileId fileId = findFileId(pipelineContext, documentPath); + if (fileId == null) { + return Optional.empty(); + } + final FrontendPhaseContext frontendPhaseContext = new FrontendPhaseContext( + pipelineContext.resolvedWorkspace.graph().projectTable(), + pipelineContext.fileTable, + pipelineContext.resolvedWorkspace.stack(), + pipelineContext.resolvedWorkspace.stdlib(), + HostAdmissionContext.permissiveDefault(), + assetSurfaceContextLoader.load(pipelineContext.resolvedWorkspace.mainProject().getRootPath())); + final var semanticReadSurface = PBSFrontendPhaseService.semanticReadSurface( + frontendPhaseContext, + p.studio.compiler.source.diagnostics.DiagnosticSink.empty(), + BuildingIssueSink.empty()); + final var ast = semanticReadSurface.astByFile().get(fileId); + if (ast == null) { + return Optional.empty(); + } + return Optional.of(new EditorialDocument( + text == null ? readCurrentText(pipelineContext, fileId) : text, + ast, + semanticReadSurface.supplementalTopDeclsByFile().getOrDefault(fileId, ReadOnlyList.empty()), + frontendPhaseContext.feSurfaceContext())); + } + + private FileId findFileId( + final BuilderPipelineContext pipelineContext, + final Path documentPath) { + for (int fileIndex = 0; fileIndex < pipelineContext.fileTable.size(); fileIndex += 1) { + final FileId candidate = new FileId(fileIndex); + final var sourceHandle = pipelineContext.fileTable.get(candidate); + if (sourceHandle != null + && sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) { + return candidate; + } + } + return null; + } + + private String readCurrentText( + final BuilderPipelineContext pipelineContext, + final FileId fileId) { + final var sourceHandle = pipelineContext.fileTable.get(fileId); + if (sourceHandle == null) { + return ""; + } + return sourceHandle.readUtf8().orElse(""); + } + + private List mapDiagnostics( + final AnalysisSnapshot snapshot, + final Path documentPath, + final String text) { + final ArrayList issues = new ArrayList<>(); + final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text); + for (final BuildingIssue diagnostic : snapshot.diagnostics()) { + if (diagnostic.getFileId() == null || diagnostic.getFileId() < 0) { + continue; + } + final int fileIndex = diagnostic.getFileId(); + if (fileIndex >= snapshot.fileTable().size()) { + continue; + } + final var sourceHandle = snapshot.fileTable().get(new FileId(fileIndex)); + if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) { + continue; + } + issues.add(toBaselineIssue(diagnostic, positionMapper)); + } + return List.copyOf(issues); + } + + private BaselineDocumentIssue toBaselineIssue( + final BuildingIssue diagnostic, + final DocumentPositionMapper positionMapper) { + final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart(); + final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd()); + final DocumentPosition start = positionMapper.positionOf(startOffset); + final DocumentPosition end = positionMapper.positionOf(endOffset); + return new BaselineDocumentIssue( + start.line(), + start.character(), + end.line(), + end.character(), + diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING, + diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(), + diagnostic.getMessage()); + } + + private Path normalizeDocumentPath(final String documentUri) { + return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize(); + } + + private BaselineCompletionItem mapCompletionItem(final PbsEditorialCompletionCandidate candidate) { + return new BaselineCompletionItem( + candidate.label(), + mapCompletionKind(candidate.kind()), + candidate.detail(), + candidate.origin()); + } + + private BaselineVisualTheme mapVisualTheme(final FrontendVisualThemeSpec theme) { + return new BaselineVisualTheme( + theme.themeId(), + theme.displayName(), + mapEditorPalette(theme.editorPalette()), + theme.tokenStyles().stream().map(this::mapTokenStyle).toList()); + } + + private BaselineSemanticHostProjection mapSemanticHostProjection(final FrontendHostProjectionSpec hostProjection) { + return new BaselineSemanticHostProjection( + hostProjection.hostId(), + hostProjection.tokenProjections().stream().map(this::mapSemanticHostProjectionEntry).toList()); + } + + private BaselineSemanticHostProjectionEntry mapSemanticHostProjectionEntry(final FrontendHostProjectionEntrySpec tokenProjection) { + return new BaselineSemanticHostProjectionEntry( + tokenProjection.semanticKey(), + tokenProjection.hostTokenType(), + tokenProjection.hostTokenModifiers(), + tokenProjection.fallbackTokenType(), + tokenProjection.fallbackTokenModifiers()); + } + + private BaselineEditorPalette mapEditorPalette(final FrontendEditorPaletteSpec palette) { + return new BaselineEditorPalette( + palette.baseForeground(), + palette.selectionBackground(), + palette.activeHighlightBackground(), + palette.lineNumberForeground(), + palette.statusChipBackground(), + palette.statusChipBorder(), + palette.statusChipForeground()); + } + + private BaselineTokenStyle mapTokenStyle(final FrontendTokenStyleSpec tokenStyle) { + return new BaselineTokenStyle( + tokenStyle.semanticKey(), + tokenStyle.foreground(), + tokenStyle.italic(), + tokenStyle.bold(), + tokenStyle.underline()); + } + + private p.studio.compiler.models.FrontendSpec frontendSpec(final LspProjectContext context) { + return FrontendRegistryService.getFrontendSpec(context.languageId()) + .orElseThrow(() -> new IllegalArgumentException("no frontend registered for languageId: " + context.languageId())); + } + + private BaselineCompletionItemKind mapCompletionKind(final PbsEditorialSymbolKind kind) { + return switch (kind) { + case KEYWORD -> BaselineCompletionItemKind.KEYWORD; + case LOCAL, PARAMETER -> BaselineCompletionItemKind.VARIABLE; + case FIELD -> BaselineCompletionItemKind.FIELD; + case FUNCTION -> BaselineCompletionItemKind.FUNCTION; + case METHOD -> BaselineCompletionItemKind.METHOD; + case CONSTRUCTOR -> BaselineCompletionItemKind.CONSTRUCTOR; + case STRUCT -> BaselineCompletionItemKind.STRUCT; + case BUILTIN_TYPE -> BaselineCompletionItemKind.BUILTIN_TYPE; + case SERVICE -> BaselineCompletionItemKind.SERVICE; + case HOST -> BaselineCompletionItemKind.HOST; + case CONTRACT -> BaselineCompletionItemKind.CONTRACT; + case CALLBACK -> BaselineCompletionItemKind.CALLBACK; + case ENUM -> BaselineCompletionItemKind.ENUM; + case ERROR -> BaselineCompletionItemKind.ERROR; + case GLOBAL -> BaselineCompletionItemKind.GLOBAL; + case CONST -> BaselineCompletionItemKind.CONST; + }; + } + + private String formatHoverMarkdown(final PbsEditorialResolvedSymbol symbol) { + final StringBuilder markdown = new StringBuilder(); + final List signatures = symbol.signatures(); + if (!signatures.isEmpty()) { + markdown.append("```pbs\n"); + for (int index = 0; index < signatures.size(); index += 1) { + if (index > 0) { + markdown.append('\n'); + } + markdown.append(signatures.get(index).label()); + } + markdown.append("\n```"); + } else if (!symbol.detail().isBlank()) { + markdown.append("```pbs\n").append(symbol.displayName()); + if (!symbol.detail().isBlank()) { + markdown.append(" : ").append(symbol.detail()); + } + markdown.append("\n```"); + } + final String kindLabel = symbol.kind().name().toLowerCase(Locale.ROOT).replace('_', ' '); + if (markdown.length() > 0) { + markdown.append("\n\n"); + } + markdown.append("**").append(kindLabel).append("**"); + if (!symbol.origin().isBlank()) { + markdown.append(" from `").append(symbol.origin()).append('`'); + } + if (!symbol.documentation().isBlank()) { + markdown.append("\n\n").append(symbol.documentation()); + } + return markdown.toString(); + } + + private record EditorialDocument( + String text, + p.studio.compiler.pbs.ast.PbsAst.File ast, + ReadOnlyList supplementalTopDecls, + p.studio.compiler.messages.FESurfaceContext feSurfaceContext) { + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java new file mode 100644 index 00000000..449a7482 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/compiler/DocumentPositionMapper.java @@ -0,0 +1,91 @@ +package p.studio.lsp.services.compiler; + +import p.studio.lsp.messages.DocumentPosition; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +final class DocumentPositionMapper { + private final String text; + + DocumentPositionMapper(final String text) { + this.text = Objects.requireNonNull(text, "text"); + } + + DocumentPosition positionOf(final Integer byteOffset) { + if (byteOffset == null || byteOffset <= 0 || text.isEmpty()) { + return new DocumentPosition(0, 0); + } + final int target = Math.min(byteOffset, text.getBytes(StandardCharsets.UTF_8).length); + int utf8Offset = 0; + int line = 0; + int character = 0; + for (int index = 0; index < text.length(); ) { + if (utf8Offset >= target) { + return new DocumentPosition(line, character); + } + final int codePoint = text.codePointAt(index); + final int utf16Width = Character.charCount(codePoint); + final int utf8Width = utf8Length(codePoint); + if (utf8Offset + utf8Width > target) { + return new DocumentPosition(line, character); + } + utf8Offset += utf8Width; + if (codePoint == '\n') { + line++; + character = 0; + } else { + character += utf16Width; + } + index += utf16Width; + } + return new DocumentPosition(line, character); + } + + int byteOffsetOf( + final int line, + final int character) { + if (line <= 0 && character <= 0) { + return 0; + } + int currentLine = 0; + int currentCharacter = 0; + int utf8Offset = 0; + for (int index = 0; index < text.length(); ) { + if (currentLine == line && currentCharacter >= character) { + return utf8Offset; + } + final int codePoint = text.codePointAt(index); + final int utf16Width = Character.charCount(codePoint); + final int utf8Width = utf8Length(codePoint); + if (currentLine == line && currentCharacter + utf16Width > character) { + return utf8Offset; + } + utf8Offset += utf8Width; + if (codePoint == '\n') { + if (currentLine == line) { + return utf8Offset - utf8Width; + } + currentLine += 1; + currentCharacter = 0; + } else { + currentCharacter += utf16Width; + } + index += utf16Width; + } + return utf8Offset; + } + + private int utf8Length(final int codePoint) { + if (codePoint <= 0x7F) { + return 1; + } + if (codePoint <= 0x7FF) { + return 2; + } + if (codePoint <= 0xFFFF) { + return 3; + } + return 4; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/ProjectConnectionValidator.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/ProjectConnectionValidator.java new file mode 100644 index 00000000..9abb9c8a --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/ProjectConnectionValidator.java @@ -0,0 +1,79 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; +import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; +import p.studio.lsp.messages.LspProjectContext; + +import java.net.URI; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +final class ProjectConnectionValidator { + private ProjectConnectionValidator() { + } + + static void validateProjectBinding( + final LspProjectContext project, + final InitializeParams params) { + final LspProjectContext safeProject = Objects.requireNonNull(project, "project"); + final InitializeParams safeParams = Objects.requireNonNull(params, "params"); + + final Path expectedRoot = safeProject.projectRoot().toAbsolutePath().normalize(); + final List clientRoots = extractClientRoots(safeParams); + if (clientRoots.isEmpty()) { + throw mismatch(safeProject, "client did not declare rootUri or workspaceFolders"); + } + + for (final Path clientRoot : clientRoots) { + if (expectedRoot.equals(clientRoot)) { + return; + } + } + + throw mismatch(safeProject, "client roots " + clientRoots + " do not match " + expectedRoot); + } + + private static List extractClientRoots(final InitializeParams params) { + final List roots = new ArrayList<>(); + if (params.getRootUri() != null && !params.getRootUri().isBlank()) { + roots.add(parsePath(params.getRootUri())); + } + if (params.getWorkspaceFolders() != null) { + for (final WorkspaceFolder workspaceFolder : params.getWorkspaceFolders()) { + if (workspaceFolder == null || workspaceFolder.getUri() == null || workspaceFolder.getUri().isBlank()) { + continue; + } + roots.add(parsePath(workspaceFolder.getUri())); + } + } + return List.copyOf(roots); + } + + private static Path parsePath(final String rawUri) { + try { + return Path.of(URI.create(rawUri)).toAbsolutePath().normalize(); + } catch (RuntimeException exception) { + throw new ResponseErrorException(new ResponseError( + ResponseErrorCode.InvalidParams, + "invalid project root uri from client: " + rawUri, + null)); + } + } + + private static ResponseErrorException mismatch( + final LspProjectContext project, + final String detail) { + return new ResponseErrorException(new ResponseError( + ResponseErrorCode.InvalidParams, + "Prometeu Studio LSP is bound to project root " + + project.projectRoot() + + " and rejected this client: " + + detail, + null)); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java new file mode 100644 index 00000000..58b9aff1 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuLanguageServer.java @@ -0,0 +1,110 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.InitializedParams; +import org.eclipse.lsp4j.services.*; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.lsp.services.compiler.CompilerLanguageServiceBridge; +import p.studio.lsp.services.protocol.mapping.Lsp4jProtocolMessageMapper; +import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +public final class PrometeuLanguageServer implements LanguageServer, LanguageClientAware { + private final LspProjectContext project; + private final LanguageServiceBridge languageServiceBridge; + private final ProtocolMessageMapper protocolMessageMapper; + private final TextDocumentService textDocumentService; + private final WorkspaceService workspaceService; + private volatile LanguageClient client; + private volatile boolean shutdownRequested; + + public PrometeuLanguageServer( + final LspProjectContext project, + final CompilerLanguageServiceBridge compilerBridge) { + this(project, + compilerBridge, + new Lsp4jProtocolMessageMapper(), + null, + new PrometeuWorkspaceService()); + } + + PrometeuLanguageServer( + final LspProjectContext project, + final LanguageServiceBridge languageServiceBridge, + final ProtocolMessageMapper protocolMessageMapper, + final TextDocumentService textDocumentService, + final WorkspaceService workspaceService) { + this.project = Objects.requireNonNull(project, "project"); + this.languageServiceBridge = Objects.requireNonNull(languageServiceBridge, "languageServiceBridge"); + this.protocolMessageMapper = Objects.requireNonNull(protocolMessageMapper, "protocolMessageMapper"); + this.textDocumentService = textDocumentService != null + ? textDocumentService + : new PrometeuTextDocumentService(project, languageServiceBridge, protocolMessageMapper); + this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService"); + } + + @Override + public void connect(final LanguageClient client) { + this.client = client; + if (textDocumentService instanceof PrometeuTextDocumentService service) { + service.connect(client); + } + } + + @Override + public CompletableFuture initialize(final InitializeParams params) { + try { + ProjectConnectionValidator.validateProjectBinding(project, params); + } catch (RuntimeException runtimeException) { + final LanguageClient currentClient = client; + if (currentClient != null) { + currentClient.logMessage(protocolMessageMapper + .mapErrorMessage("Failed to initialize Prometeu Studio LSP for project " + project.projectKey() + ": " + runtimeException.getMessage())); + } + return CompletableFuture.failedFuture(runtimeException); + } + return CompletableFuture.completedFuture(protocolMessageMapper + .mapInitializeResult(languageServiceBridge.describeServer(project))); + } + + @Override + public void initialized(final InitializedParams params) { + final LanguageClient currentClient = client; + if (currentClient == null) { + return; + } + currentClient.logMessage(protocolMessageMapper + .mapInfoMessage("Prometeu Studio LSP connected for project " + project.projectKey())); + } + + @Override + public CompletableFuture shutdown() { + shutdownRequested = true; + return CompletableFuture.completedFuture(null); + } + + @Override + public void exit() { + if (!shutdownRequested) { + final LanguageClient currentClient = client; + if (currentClient != null) { + currentClient.logMessage(protocolMessageMapper + .mapWarningMessage("Prometeu Studio LSP received exit before shutdown")); + } + } + } + + @Override + public TextDocumentService getTextDocumentService() { + return textDocumentService; + } + + @Override + public WorkspaceService getWorkspaceService() { + return workspaceService; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java new file mode 100644 index 00000000..ef4b139d --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuTextDocumentService.java @@ -0,0 +1,132 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.TextDocumentService; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentMap; +import java.util.List; + +public final class PrometeuTextDocumentService implements TextDocumentService { + private final LspProjectContext project; + private final LanguageServiceBridge languageServiceBridge; + private final ProtocolMessageMapper protocolMessageMapper; + private final ConcurrentMap documentTextByUri = new ConcurrentHashMap<>(); + private volatile LanguageClient client; + + public PrometeuTextDocumentService( + final LspProjectContext project, + final LanguageServiceBridge languageServiceBridge, + final ProtocolMessageMapper protocolMessageMapper) { + this.project = Objects.requireNonNull(project, "project"); + this.languageServiceBridge = Objects.requireNonNull(languageServiceBridge, "languageServiceBridge"); + this.protocolMessageMapper = Objects.requireNonNull(protocolMessageMapper, "protocolMessageMapper"); + } + + void connect(final LanguageClient client) { + this.client = client; + } + + @Override + public void didOpen(final DidOpenTextDocumentParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = params.getTextDocument().getText(); + documentTextByUri.put(uri, text); + publishAnalysis(uri, text); + } + + @Override + public void didChange(final DidChangeTextDocumentParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = params.getContentChanges().isEmpty() + ? "" + : params.getContentChanges().getFirst().getText(); + documentTextByUri.put(uri, text); + publishAnalysis(uri, text); + } + + @Override + public void didClose(final DidCloseTextDocumentParams params) { + documentTextByUri.remove(params.getTextDocument().getUri()); + final LanguageClient currentClient = client; + if (currentClient == null) { + return; + } + currentClient.publishDiagnostics(protocolMessageMapper.emptyDiagnostics(params.getTextDocument().getUri())); + } + + @Override + public void didSave(final DidSaveTextDocumentParams params) { + final LanguageClient currentClient = client; + if (currentClient == null) { + return; + } + currentClient.logMessage(protocolMessageMapper.mapInfoMessage( + languageServiceBridge.onSave(project, params.getTextDocument().getUri()))); + } + + @Override + public CompletableFuture, CompletionList>> completion(final CompletionParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + return CompletableFuture.completedFuture(Either.forRight(protocolMessageMapper.mapCompletion( + languageServiceBridge.completion( + project, + uri, + text, + params.getPosition().getLine(), + params.getPosition().getCharacter())))); + } + + @Override + public CompletableFuture hover(final HoverParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + return CompletableFuture.completedFuture(protocolMessageMapper.mapHover(languageServiceBridge.hover( + project, + uri, + text, + params.getPosition().getLine(), + params.getPosition().getCharacter()))); + } + + @Override + public CompletableFuture signatureHelp(final SignatureHelpParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.get(uri); + return CompletableFuture.completedFuture(protocolMessageMapper.mapSignatureHelp( + languageServiceBridge.signatureHelp( + project, + uri, + text, + params.getPosition().getLine(), + params.getPosition().getCharacter()))); + } + + @Override + public CompletableFuture semanticTokensFull(final SemanticTokensParams params) { + final String uri = params.getTextDocument().getUri(); + final String text = documentTextByUri.getOrDefault(uri, ""); + return CompletableFuture.completedFuture(protocolMessageMapper.mapSemanticTokens( + languageServiceBridge.semanticTokens(project, uri, text))); + } + + private void publishAnalysis( + final String uri, + final String text) { + final LanguageClient currentClient = client; + if (currentClient == null) { + return; + } + currentClient.publishDiagnostics(protocolMessageMapper.mapDiagnostics( + uri, + languageServiceBridge.analyzeDocument(project, uri, text))); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java new file mode 100644 index 00000000..612f9dd7 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/PrometeuWorkspaceService.java @@ -0,0 +1,15 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.services.WorkspaceService; + +public final class PrometeuWorkspaceService implements WorkspaceService { + @Override + public void didChangeConfiguration(final DidChangeConfigurationParams params) { + } + + @Override + public void didChangeWatchedFiles(final DidChangeWatchedFilesParams params) { + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java new file mode 100644 index 00000000..7b3d548c --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapper.java @@ -0,0 +1,248 @@ +package p.studio.lsp.services.protocol.mapping; + +import org.eclipse.lsp4j.*; +import p.studio.lsp.messages.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class Lsp4jProtocolMessageMapper implements ProtocolMessageMapper { + @Override + public InitializeResult mapInitializeResult(final BaselineServerDescription description) { + final ServerCapabilities capabilities = new ServerCapabilities(); + + final TextDocumentSyncOptions syncOptions = new TextDocumentSyncOptions(); + syncOptions.setOpenClose(true); + syncOptions.setChange(TextDocumentSyncKind.Full); + + final SaveOptions saveOptions = new SaveOptions(); + saveOptions.setIncludeText(true); + syncOptions.setSave(saveOptions); + + capabilities.setTextDocumentSync(syncOptions); + capabilities.setHoverProvider(description.hoverSupported()); + if (description.completionSupported()) { + final CompletionOptions completionOptions = new CompletionOptions(); + completionOptions.setResolveProvider(false); + completionOptions.setTriggerCharacters(List.of(".")); + capabilities.setCompletionProvider(completionOptions); + } + if (description.signatureHelpSupported()) { + final SignatureHelpOptions signatureHelpOptions = new SignatureHelpOptions(); + signatureHelpOptions.setTriggerCharacters(List.of("(", ",")); + signatureHelpOptions.setRetriggerCharacters(List.of(",")); + capabilities.setSignatureHelpProvider(signatureHelpOptions); + } + final SemanticTokensWithRegistrationOptions semanticTokens = new SemanticTokensWithRegistrationOptions(); + semanticTokens.setLegend(new SemanticTokensLegend(description.semanticTokenTypes(), List.of())); + semanticTokens.setFull(true); + capabilities.setSemanticTokensProvider(semanticTokens); + capabilities.setExperimental(Map.of( + "prometeuVisualThemes", + Map.of( + "frontendLanguageId", description.frontendLanguageId(), + "activeVisualThemeId", description.activeVisualThemeId(), + "visualThemes", description.visualThemes().stream().map(this::mapVisualThemePayload).toList()), + "prometeuSemanticHostProjections", + Map.of( + "frontendLanguageId", description.frontendLanguageId(), + "semanticKeys", description.semanticTokenTypes(), + "hostProjections", description.semanticHostProjections().stream().map(this::mapSemanticHostProjectionPayload).toList()))); + + final InitializeResult result = new InitializeResult(capabilities); + final ServerInfo serverInfo = new ServerInfo(); + serverInfo.setName(description.name()); + serverInfo.setVersion(description.version()); + result.setServerInfo(serverInfo); + return result; + } + + @Override + public PublishDiagnosticsParams mapDiagnostics( + final String uri, + final BaselineDocumentAnalysis analysis) { + return new PublishDiagnosticsParams(uri, analysis.issues().stream().map(this::mapDiagnostic).toList()); + } + + @Override + public PublishDiagnosticsParams emptyDiagnostics(final String uri) { + return new PublishDiagnosticsParams(uri, List.of()); + } + + @Override + public Hover mapHover(final BaselineHover hover) { + final MarkupContent content = new MarkupContent(); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(hover.markdown()); + + final Hover result = new Hover(); + result.setContents(content); + return result; + } + + @Override + public CompletionList mapCompletion(final BaselineCompletion completion) { + final CompletionList list = new CompletionList(); + list.setIsIncomplete(completion.incomplete()); + list.setItems(completion.items().stream().map(this::mapCompletionItem).toList()); + return list; + } + + @Override + public SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) { + final SignatureHelp result = new SignatureHelp(); + result.setActiveSignature(signatureHelp.activeSignature()); + result.setActiveParameter(signatureHelp.activeParameter()); + result.setSignatures(signatureHelp.signatures().stream().map(signature -> { + final SignatureInformation information = new SignatureInformation(); + information.setLabel(signature.label()); + if (!signature.documentation().isBlank()) { + final MarkupContent content = new MarkupContent(); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(signature.documentation()); + information.setDocumentation(content); + } + information.setParameters(signature.parameters().stream().map(parameter -> { + final ParameterInformation parameterInformation = new ParameterInformation(); + parameterInformation.setLabel(parameter); + return parameterInformation; + }).toList()); + return information; + }).toList()); + return result; + } + + @Override + public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) { + final List orderedTokens = new ArrayList<>(semanticTokens.tokens()); + orderedTokens.sort(Comparator + .comparingInt(BaselineSemanticToken::line) + .thenComparingInt(BaselineSemanticToken::startCharacter)); + + final List data = new ArrayList<>(); + int previousLine = 0; + int previousCharacter = 0; + for (final BaselineSemanticToken token : orderedTokens) { + final int deltaLine = token.line() - previousLine; + final int deltaStart = deltaLine == 0 + ? token.startCharacter() - previousCharacter + : token.startCharacter(); + final int tokenType = semanticTokens.legend().indexOf(token.semanticKey()); + if (tokenType < 0) { + continue; + } + data.add(deltaLine); + data.add(deltaStart); + data.add(token.length()); + data.add(tokenType); + data.add(0); + previousLine = token.line(); + previousCharacter = token.startCharacter(); + } + return new SemanticTokens(data); + } + + @Override + public MessageParams mapInfoMessage(final String message) { + return new MessageParams(MessageType.Info, message); + } + + @Override + public MessageParams mapErrorMessage(final String message) { + return new MessageParams(MessageType.Error, message); + } + + @Override + public MessageParams mapWarningMessage(String message) { + return new MessageParams(MessageType.Warning, message); + } + + private Diagnostic mapDiagnostic(final BaselineDocumentIssue issue) { + final Diagnostic diagnostic = new Diagnostic(); + diagnostic.setRange(new Range( + new Position(issue.startLine(), issue.startCharacter()), + new Position(issue.endLine(), issue.endCharacter()))); + diagnostic.setSeverity(mapSeverity(issue.severity())); + diagnostic.setSource(issue.source()); + diagnostic.setMessage(issue.message()); + return diagnostic; + } + + private CompletionItem mapCompletionItem(final BaselineCompletionItem item) { + final CompletionItem completionItem = new CompletionItem(); + completionItem.setLabel(item.label()); + completionItem.setKind(mapCompletionItemKind(item.kind())); + if (!item.detail().isBlank()) { + completionItem.setDetail(item.detail()); + } + if (!item.documentation().isBlank()) { + final MarkupContent content = new MarkupContent(); + content.setKind(MarkupKind.MARKDOWN); + content.setValue(item.documentation()); + completionItem.setDocumentation(content); + } + return completionItem; + } + + private CompletionItemKind mapCompletionItemKind(final BaselineCompletionItemKind kind) { + return switch (kind) { + case KEYWORD -> CompletionItemKind.Keyword; + case VARIABLE -> CompletionItemKind.Variable; + case FIELD -> CompletionItemKind.Field; + case FUNCTION -> CompletionItemKind.Function; + case METHOD -> CompletionItemKind.Method; + case CONSTRUCTOR -> CompletionItemKind.Constructor; + case STRUCT, BUILTIN_TYPE -> CompletionItemKind.Struct; + case SERVICE, HOST, CONTRACT -> CompletionItemKind.Interface; + case CALLBACK -> CompletionItemKind.Function; + case ENUM -> CompletionItemKind.Enum; + case ERROR -> CompletionItemKind.EnumMember; + case GLOBAL, CONST -> CompletionItemKind.Constant; + }; + } + + + private DiagnosticSeverity mapSeverity(final BaselineIssueSeverity severity) { + return switch (severity) { + case INFORMATION -> DiagnosticSeverity.Information; + case WARNING -> DiagnosticSeverity.Warning; + case ERROR -> DiagnosticSeverity.Error; + }; + } + + private Map mapVisualThemePayload(final BaselineVisualTheme theme) { + final Map payload = new LinkedHashMap<>(); + payload.put("themeId", theme.themeId()); + payload.put("displayName", theme.displayName()); + payload.put("editorPalette", Map.of( + "baseForeground", theme.editorPalette().baseForeground(), + "selectionBackground", theme.editorPalette().selectionBackground(), + "activeHighlightBackground", theme.editorPalette().activeHighlightBackground(), + "lineNumberForeground", theme.editorPalette().lineNumberForeground(), + "statusChipBackground", theme.editorPalette().statusChipBackground(), + "statusChipBorder", theme.editorPalette().statusChipBorder(), + "statusChipForeground", theme.editorPalette().statusChipForeground())); + payload.put("tokenStyles", theme.tokenStyles().stream().map(tokenStyle -> Map.of( + "semanticKey", tokenStyle.semanticKey(), + "foreground", tokenStyle.foreground(), + "italic", tokenStyle.italic(), + "bold", tokenStyle.bold(), + "underline", tokenStyle.underline())).toList()); + return payload; + } + + private Map mapSemanticHostProjectionPayload(final BaselineSemanticHostProjection hostProjection) { + final Map payload = new LinkedHashMap<>(); + payload.put("hostId", hostProjection.hostId()); + payload.put("tokenProjections", hostProjection.tokenProjections().stream().map(tokenProjection -> Map.of( + "semanticKey", tokenProjection.semanticKey(), + "hostTokenType", tokenProjection.hostTokenType(), + "hostTokenModifiers", tokenProjection.hostTokenModifiers(), + "fallbackTokenType", tokenProjection.fallbackTokenType(), + "fallbackTokenModifiers", tokenProjection.fallbackTokenModifiers())).toList()); + return payload; + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java new file mode 100644 index 00000000..7897adae --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/main/java/p/studio/lsp/services/protocol/mapping/ProtocolMessageMapper.java @@ -0,0 +1,35 @@ +package p.studio.lsp.services.protocol.mapping; + +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SemanticTokens; +import p.studio.lsp.messages.BaselineDocumentAnalysis; +import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineHover; +import p.studio.lsp.messages.BaselineSemanticTokens; +import p.studio.lsp.messages.BaselineSignatureHelp; +import p.studio.lsp.messages.BaselineServerDescription; +import org.eclipse.lsp4j.CompletionList; + +public interface ProtocolMessageMapper { + InitializeResult mapInitializeResult(BaselineServerDescription description); + + PublishDiagnosticsParams mapDiagnostics(String uri, BaselineDocumentAnalysis analysis); + + PublishDiagnosticsParams emptyDiagnostics(String uri); + + Hover mapHover(BaselineHover hover); + + CompletionList mapCompletion(BaselineCompletion completion); + + SignatureHelp mapSignatureHelp(BaselineSignatureHelp signatureHelp); + + SemanticTokens mapSemanticTokens(BaselineSemanticTokens semanticTokens); + + MessageParams mapInfoMessage(String message); + MessageParams mapErrorMessage(String message); + MessageParams mapWarningMessage(String message); +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java deleted file mode 100644 index 69836c6b..00000000 --- a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/LspServiceImplTest.java +++ /dev/null @@ -1,427 +0,0 @@ -package p.studio.lsp; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.dtos.LspDefinitionTargetDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspDefinitionRequest; -import p.studio.vfs.*; -import p.studio.vfs.messages.*; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class LspServiceImplTest { - @TempDir - Path tempDir; - - private static final String OVERLAY_SOURCE = """ - fn helper_call() -> void - { - if true { - helper(); - } - while false { - helper(); - } - let value = switch true { - true: { 1 }, - default: { 2 } - }; - let recovered = handle helper_result() { - _: { - ok(0) - } - }; - } - """; - - private static final String SERVICE_SOURCE = """ - declare service Game { - fn tick(x: int) -> int { return x; } - } - - fn main() -> void { - Game.tick(1); - } - """; - - private static final String SDK_IMPORT_SOURCE = """ - import { Composer } from @sdk:composer; - - fn main() -> void { - Composer.emit_sprite(1, 0, 0, 0, 0, 0, false, false, 0); - } - """; - - private static final String PROJECT_IMPORT_SOURCE = """ - import { Color } from @app:graphics; - - fn main(color: Color) -> Color { - return color; - } - """; - - @Test - void analyzeDocumentUsesVfsOverlayForRequestedDocumentAndFilesystemFallbackForClosedFiles() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - final Path helperFile = projectRoot.resolve("src/helper.pbs"); - Files.writeString(mainFile, "fn broken( -> void {}\n"); - Files.writeString(helperFile, "fn helper() -> void {}\n"); - - final VfsProjectDocument delegate = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - new OverlayVfsProjectDocument(delegate, mainFile, OVERLAY_SOURCE)); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper_call")); - assertEquals("pbs-function", semanticKeyForLexeme(analysis, OVERLAY_SOURCE, "helper")); - assertEquals(List.of("/themes/pbs/semantic-highlighting.css"), analysis.semanticPresentation().resources()); - assertTrue(analysis.semanticPresentation().semanticKeys().contains("pbs-function")); - assertFalse(analysis.inlineHints().isEmpty(), analysis.toString()); - - assertTrue( - analysis.documentSymbols().stream().anyMatch(symbol -> symbol.name().equals("helper_call")), - analysis.toString()); - assertTrue( - flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.IF), - analysis.toString()); - assertTrue( - flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.WHILE), - analysis.toString()); - assertTrue( - flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SWITCH), - analysis.toString()); - assertTrue( - flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> symbol.kind() == p.studio.lsp.messages.LspSymbolKind.HANDLE), - analysis.toString()); - assertFalse(analysis.structuralAnchors().isEmpty(), analysis.toString()); - assertEquals(OVERLAY_SOURCE.indexOf("{"), analysis.structuralAnchors().getFirst().startAnchor().startOffset()); - assertTrue( - flattenAnchors(analysis.structuralAnchors()).stream().anyMatch(anchor -> - anchor.startAnchor().startOffset() == OVERLAY_SOURCE.indexOf("if true {") + "if true ".length()), - analysis.toString()); - assertTrue( - analysis.workspaceSymbols().stream().anyMatch(symbol -> - symbol.name().equals("helper") && symbol.documentPath().equals(normalize(helperFile))), - analysis.toString()); - - final int offset = OVERLAY_SOURCE.indexOf("helper();"); - final var definition = service.definition(new LspDefinitionRequest(mainFile, offset)); - final List targets = definition.targets(); - - assertEquals(2, targets.size()); - assertEquals(normalize(helperFile), targets.getFirst().documentPath()); - assertEquals("helper", targets.getFirst().name()); - } - - @Test - void analyzeDocumentSurfacesDiagnosticsWithoutAbortingSemanticRead() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - Files.writeString(mainFile, """ - fn main( -> void - { - helper(); - } - """); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - vfs); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertFalse(analysis.diagnostics().isEmpty(), analysis.toString()); - assertTrue(analysis.diagnostics().stream().allMatch(diagnostic -> - diagnostic.documentPath().equals(normalize(mainFile)))); - } - - @Test - void analyzeDocumentHighlightsServiceIdentifiersWithServiceSemanticKey() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - Files.writeString(mainFile, SERVICE_SOURCE); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - vfs); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertEquals("pbs-service", semanticKeyForLexeme(analysis, SERVICE_SOURCE, "Game")); - assertTrue(flatten(analysis.documentSymbols()).stream().anyMatch(symbol -> - symbol.kind() == p.studio.lsp.messages.LspSymbolKind.SERVICE && symbol.name().equals("Game"))); - } - - @Test - void analyzeDocumentHighlightsSdkImportedHostIdentifiersWithHostSemanticKey() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - Files.writeString(mainFile, SDK_IMPORT_SOURCE); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - vfs); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertEquals(List.of("pbs-service", "pbs-service"), semanticKeysForLexeme(analysis, SDK_IMPORT_SOURCE, "Composer")); - } - - @Test - void analyzeDocumentHighlightsProjectImportedStructIdentifiersWithStructSemanticKey() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - final Path graphicsDir = projectRoot.resolve("src/graphics"); - final Path graphicsFile = graphicsDir.resolve("types.pbs"); - final Path graphicsBarrel = graphicsDir.resolve("mod.barrel"); - Files.createDirectories(graphicsDir); - Files.writeString(mainFile, PROJECT_IMPORT_SOURCE); - Files.writeString(graphicsFile, "declare struct Color(raw: int);\n"); - Files.writeString(graphicsBarrel, "pub struct Color;\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - vfs); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertEquals(List.of("pbs-struct", "pbs-struct", "pbs-struct"), semanticKeysForLexeme(analysis, PROJECT_IMPORT_SOURCE, "Color")); - } - - @Test - void analyzeDocumentReturnsDedicatedInlineHintTransportSurface() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - final String source = "fn main() -> void { let value = 1; }\n"; - Files.writeString(mainFile, source); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - vfs); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertEquals(1, analysis.inlineHints().size(), analysis.toString()); - assertEquals("int", analysis.inlineHints().getFirst().label()); - assertEquals("type", analysis.inlineHints().getFirst().category()); - assertEquals("value", sourceSlice(source, analysis.inlineHints().getFirst().anchor())); - } - - @Test - void analyzeDocumentPreservesValidInlineHintsUnderPartialDegradation() throws Exception { - final Path projectRoot = createProject(); - final Path mainFile = projectRoot.resolve("src/main.pbs"); - final String source = """ - fn ok() -> void { - let value = 1; - return; - } - - fn broken( -> void { - let missing = 2; - } - """; - Files.writeString(mainFile, source); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext(projectRoot)); - final LspService service = new LspServiceImpl( - new LspProjectContext("Example", "pbs", projectRoot), - vfs); - - final var analysis = service.analyzeDocument(new LspAnalyzeDocumentRequest(mainFile)); - - assertFalse(analysis.diagnostics().isEmpty(), analysis.toString()); - assertEquals(1, analysis.inlineHints().size(), analysis.toString()); - assertEquals("int", analysis.inlineHints().getFirst().label()); - assertEquals("value", sourceSlice(source, analysis.inlineHints().getFirst().anchor())); - } - - private Path createProject() throws Exception { - final Path src = Files.createDirectories(tempDir.resolve("src")); - Files.writeString(tempDir.resolve("prometeu.json"), """ - { - "name": "Example", - "version": "1.0.0", - "language": "pbs", - "stdlib": "1", - "dependencies": [] - } - """); - Files.writeString(src.resolve("mod.barrel"), """ - pub fn helper() -> void; - pub fn helper_result() -> result int; - pub error MyError; - pub service Log; - """); - Files.writeString(src.resolve("helper.pbs"), "fn helper() -> void {}\n"); - Files.writeString(src.resolve("helper_result.pbs"), """ - declare error MyError { - Failed - } - - fn helper_result() -> result int { - return ok(1); - } - """); - Files.writeString(src.resolve("log.pbs"), """ - declare service Log { - fn failure(message: string) -> void; - } - """); - return tempDir; - } - - private static List flatten( - final List symbols) { - final List flattened = new java.util.ArrayList<>(); - for (final var symbol : symbols) { - flattened.add(symbol); - flattened.addAll(flatten(symbol.children())); - } - return flattened; - } - - private static List flattenAnchors( - final List anchors) { - final List flattened = new java.util.ArrayList<>(); - for (final var anchor : anchors) { - flattened.add(anchor); - flattened.addAll(flattenAnchors(anchor.children())); - } - return flattened; - } - - private VfsProjectContext projectContext(final Path projectRoot) { - return new VfsProjectContext("Example", "pbs", projectRoot); - } - - private static Path normalize(final Path path) { - try { - return path.toAbsolutePath().normalize().toRealPath(); - } catch (Exception exception) { - throw new IllegalStateException(exception); - } - } - - private static String semanticKeyForLexeme( - final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis, - final String source, - final String lexeme) { - return semanticKeysForLexeme(analysis, source, lexeme).stream().findFirst().orElseThrow(); - } - - private static List semanticKeysForLexeme( - final p.studio.lsp.messages.LspAnalyzeDocumentResult analysis, - final String source, - final String lexeme) { - return analysis.semanticHighlights().stream() - .filter(highlight -> lexeme.equals(spanContent(source, highlight.range().startOffset(), highlight.range().endOffset()))) - .map(p.studio.lsp.dtos.LspHighlightSpanDTO::semanticKey) - .toList(); - } - - private static String spanContent( - final String source, - final int start, - final int end) { - if (start < 0 || end > source.length() || start >= end) { - return ""; - } - return source.substring(start, end); - } - - private static String sourceSlice( - final String source, - final p.studio.lsp.dtos.LspRangeDTO range) { - return spanContent(source, range.startOffset(), range.endOffset()); - } - - private static final class OverlayVfsProjectDocument implements VfsProjectDocument { - private final VfsProjectDocument delegate; - private final Path overlayPath; - private final String overlayContent; - - private OverlayVfsProjectDocument( - final VfsProjectDocument delegate, - final Path overlayPath, - final String overlayContent) { - this.delegate = delegate; - this.overlayPath = normalize(overlayPath); - this.overlayContent = overlayContent; - } - - @Override - public VfsProjectContext projectContext() { - return delegate.projectContext(); - } - - @Override - public VfsProjectSnapshot snapshot() { - return delegate.snapshot(); - } - - @Override - public VfsProjectSnapshot refresh() { - return delegate.refresh(); - } - - @Override - public VfsProjectSnapshot refresh(final VfsRefreshRequest request) { - return delegate.refresh(request); - } - - @Override - public VfsDocumentOpenResult openDocument(final Path path) { - final Path normalizedPath = normalize(path); - if (!overlayPath.equals(normalizedPath)) { - return delegate.openDocument(normalizedPath); - } - final VfsDocumentAccessContext accessContext = new VfsDocumentAccessContext( - normalizedPath, - "pbs", - true, - VfsDocumentAccessMode.READ_ONLY, - Map.of()); - return new VfsDocumentOpenResult.VfsTextDocument( - normalizedPath, - normalizedPath.getFileName().toString(), - "pbs", - overlayContent, - "LF", - false, - accessContext); - } - - @Override - public void close() { - delegate.close(); - } - - private static Path normalize(final Path path) { - final Path normalized = path.toAbsolutePath().normalize(); - try { - return Files.exists(normalized) ? normalized.toRealPath() : normalized; - } catch (Exception ignored) { - return normalized; - } - } - } -} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java new file mode 100644 index 00000000..319dc6ef --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/compiler/CompilerLanguageServiceBridgeTest.java @@ -0,0 +1,200 @@ +package p.studio.lsp.services.compiler; + +import org.junit.jupiter.api.Test; +import p.studio.lsp.messages.BaselineIssueSeverity; +import p.studio.lsp.messages.LspProjectContext; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CompilerLanguageServiceBridgeTest { + + @Test + void analyzeDocumentReturnsCompilerDiagnosticsForOverlayContent() { + final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) + .resolve("test-projects") + .resolve("main") + .toAbsolutePath() + .normalize(); + final Path documentPath = projectRoot.resolve("src").resolve("main.pbs"); + final String invalidOverlay = """ + fn frame() -> void { + let broken: int = ; + } + """; + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + + final var analysis = bridge.analyzeDocument( + new LspProjectContext("main", "pbs", projectRoot), + documentPath.toUri().toString(), + invalidOverlay); + + assertFalse(analysis.issues().isEmpty(), "analyzeDocument must surface compiler diagnostics"); + assertTrue(analysis.issues().stream().anyMatch(issue -> issue.severity() == BaselineIssueSeverity.ERROR)); + assertTrue(analysis.issues().stream().anyMatch(issue -> issue.startLine() == 1)); + assertTrue(analysis.issues().stream().noneMatch(issue -> issue.message().contains("Compiler-backed baseline"))); + } + + @Test + void semanticTokensReturnsPbsLegendAndTokens() { + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + final var semanticTokens = bridge.semanticTokens( + new LspProjectContext("main", "pbs", Path.of(".")), + Path.of("demo.pbs").toUri().toString(), + """ + struct Vec2 { + fn length() -> int { + return 0; + } + } + + service Log {} + host Input {} + enum ResultKind { Ok = 1; } + + fn main() -> void { + const answer: int = 42; + let value = new Vec2(); + value.length(); + Log.info(); + Input.read(); + let kind: ResultKind; + } + """); + + assertTrue(semanticTokens.legend().contains("pbs-keyword")); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-keyword"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-number"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-struct"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-function"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-method"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-const"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-constructor"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-service"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-host"))); + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-enum"))); + } + + @Test + void semanticTokensClassifyBuiltinTypesExposedByImportedStdlibModules() { + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + + final var semanticTokens = bridge.semanticTokens( + new LspProjectContext("main", "pbs", Path.of(".")), + Path.of("demo.pbs").toUri().toString(), + """ + import { Input } from @sdk:input; + + fn main() -> void { + let touch : InputTouch = Input.touch(); + } + """); + + assertTrue(semanticTokens.tokens().stream().anyMatch(token -> token.semanticKey().equals("pbs-builtin-type"))); + } + + @Test + void completionHoverAndSignatureHelpUseCompilerBackedEditorialResolution() { + final Path projectRoot = findRepoRoot(Path.of("").toAbsolutePath().normalize()) + .resolve("test-projects") + .resolve("main") + .toAbsolutePath() + .normalize(); + final Path documentPath = projectRoot.resolve("src").resolve("main.pbs"); + final String overlay = """ + declare struct Vec() { + fn blend(dx: int, dy: int) -> int { return dx; } + } + + fn mix(left: int, right: int) -> int { return left; } + + fn frame(vec: Vec) -> void { + mix(1, 2); + vec.blend(1, 2); + new Vec(); + } + """; + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + final LspProjectContext context = new LspProjectContext("main", "pbs", projectRoot); + final DocumentPositionMapper mapper = new DocumentPositionMapper(overlay); + final var completionPosition = mapper.positionOf(overlay.indexOf("mix(1, 2);")); + final var hoverPosition = mapper.positionOf(overlay.indexOf("blend(1, 2);")); + final var functionSignaturePosition = mapper.positionOf(overlay.indexOf("2);")); + final var constructorSignaturePosition = mapper.positionOf(overlay.indexOf("Vec();") + 4); + + final var completion = bridge.completion( + context, + documentPath.toUri().toString(), + overlay, + completionPosition.line(), + completionPosition.character()); + assertTrue(completion.items().stream().anyMatch(item -> item.label().equals("mix"))); + assertTrue(completion.items().stream().anyMatch(item -> item.label().equals("vec"))); + + final var hover = bridge.hover( + context, + documentPath.toUri().toString(), + overlay, + hoverPosition.line(), + hoverPosition.character()); + assertTrue(hover.markdown().contains("blend(arg0: int, arg1: int) -> int")); + assertTrue(hover.markdown().contains("**method**")); + + final var functionSignatureHelp = bridge.signatureHelp( + context, + documentPath.toUri().toString(), + overlay, + functionSignaturePosition.line(), + functionSignaturePosition.character()); + assertEquals(1, functionSignatureHelp.activeParameter()); + assertEquals("mix(left: int, right: int) -> int", functionSignatureHelp.signatures().getFirst().label()); + + final var constructorSignatureHelp = bridge.signatureHelp( + context, + documentPath.toUri().toString(), + overlay, + constructorSignaturePosition.line(), + constructorSignaturePosition.character()); + assertEquals("Vec()", constructorSignatureHelp.signatures().getFirst().label()); + } + + @Test + void describeServerPublishesFrontendVisualThemes() { + final CompilerLanguageServiceBridge bridge = new CompilerLanguageServiceBridge(); + + final var description = bridge.describeServer(new LspProjectContext("main", "pbs", Path.of("."))); + + assertEquals("pbs", description.frontendLanguageId()); + assertEquals("pbs-default", description.activeVisualThemeId()); + assertTrue(description.completionSupported()); + assertTrue(description.signatureHelpSupported()); + assertEquals(1, description.semanticHostProjections().size()); + assertEquals("vscode", description.semanticHostProjections().getFirst().hostId()); + assertTrue(description.semanticHostProjections().getFirst().tokenProjections().stream() + .anyMatch(tokenProjection -> tokenProjection.semanticKey().equals("pbs-keyword") + && tokenProjection.hostTokenType().equals("keyword"))); + assertEquals(1, description.visualThemes().size()); + assertEquals("PBS Default", description.visualThemes().getFirst().displayName()); + assertEquals("#d9e2ec", description.visualThemes().getFirst().editorPalette().baseForeground()); + assertTrue(description.visualThemes().getFirst().tokenStyles().stream() + .anyMatch(tokenStyle -> tokenStyle.semanticKey().equals("pbs-keyword") + && tokenStyle.foreground().equals("#7fb8ff"))); + } + + private Path findRepoRoot(final Path start) { + var current = start; + while (current != null) { + if (Files.exists(current.resolve("settings.gradle.kts")) + && Files.exists(current.resolve("test-projects").resolve("main").resolve("prometeu.json"))) { + return current; + } + current = current.getParent(); + } + throw new IllegalStateException("unable to locate repository root from " + start); + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java new file mode 100644 index 00000000..ae67bbb9 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuLanguageServerTest.java @@ -0,0 +1,248 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.InitializedParams; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.junit.jupiter.api.Test; +import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineCompletionItem; +import p.studio.lsp.messages.BaselineCompletionItemKind; +import p.studio.lsp.messages.BaselineDocumentAnalysis; +import p.studio.lsp.messages.BaselineEditorPalette; +import p.studio.lsp.messages.BaselineHover; +import p.studio.lsp.messages.BaselineSemanticHostProjection; +import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; +import p.studio.lsp.messages.BaselineSignature; +import p.studio.lsp.messages.BaselineSignatureHelp; +import p.studio.lsp.messages.BaselineSemanticTokens; +import p.studio.lsp.messages.BaselineServerDescription; +import p.studio.lsp.messages.BaselineTokenStyle; +import p.studio.lsp.messages.BaselineVisualTheme; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; + +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletionException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PrometeuLanguageServerTest { + + @Test + void initializeDelegatesThroughBridgeAndMapper() { + final RecordingBridge bridge = new RecordingBridge(); + final RecordingMapper mapper = new RecordingMapper(); + final InitializeResult expected = new InitializeResult(); + mapper.initializeResult = expected; + + final PrometeuLanguageServer server = new PrometeuLanguageServer( + new LspProjectContext("demo", "pbs", Path.of(".")), + bridge, + mapper, + new NoopTextDocumentService(), + new PrometeuWorkspaceService()); + + final InitializeParams params = new InitializeParams(); + params.setRootUri(Path.of(".").toAbsolutePath().normalize().toUri().toString()); + + final InitializeResult result = server.initialize(params).join(); + + assertSame(expected, result); + assertEquals(1, bridge.describeServerCalls); + assertSame(bridge.serverDescription, mapper.lastServerDescription); + assertEquals("pbs", bridge.serverDescription.frontendLanguageId()); + } + + @Test + void initializeRejectsClientBoundToDifferentProjectRoot() { + final RecordingBridge bridge = new RecordingBridge(); + final RecordingMapper mapper = new RecordingMapper(); + final PrometeuLanguageServer server = new PrometeuLanguageServer( + new LspProjectContext("demo", "pbs", Path.of("/tmp/studio-project")), + bridge, + mapper, + new NoopTextDocumentService(), + new PrometeuWorkspaceService()); + + final InitializeParams params = new InitializeParams(); + params.setRootUri(Path.of("/tmp/another-project").toUri().toString()); + + final CompletionException failure = org.junit.jupiter.api.Assertions.assertThrows( + CompletionException.class, + () -> server.initialize(params).join()); + + final ResponseErrorException responseError = assertInstanceOf(ResponseErrorException.class, failure.getCause()); + assertTrue(responseError.getMessage().contains("/tmp/studio-project")); + assertEquals(0, bridge.describeServerCalls); + } + + private static final class RecordingBridge implements LanguageServiceBridge { + private int describeServerCalls; + private final BaselineServerDescription serverDescription = new BaselineServerDescription( + "demo", + "1", + "pbs", + true, + true, + true, + List.of(), + List.of(new BaselineSemanticHostProjection( + "vscode", + List.of(new BaselineSemanticHostProjectionEntry( + "demo-keyword", + "keyword", + List.of(), + "variable", + List.of())))), + List.of(new BaselineVisualTheme( + "demo-default", + "Demo Default", + new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"), + List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))), + "demo-default"); + + @Override + public BaselineServerDescription describeServer(final LspProjectContext project) { + describeServerCalls++; + return serverDescription; + } + + @Override + public BaselineDocumentAnalysis analyzeDocument(final LspProjectContext project, final String documentUri, final String text) { + return new BaselineDocumentAnalysis(List.of()); + } + + @Override + public BaselineCompletion completion( + final LspProjectContext project, + final String documentUri, + final String text, + final int line, + final int character) { + return new BaselineCompletion(false, List.of(new BaselineCompletionItem("demo", BaselineCompletionItemKind.FUNCTION, "", ""))); + } + + @Override + public BaselineHover hover( + final LspProjectContext project, + final String documentUri, + final String text, + final int line, + final int character) { + return new BaselineHover("hover"); + } + + @Override + public BaselineSignatureHelp signatureHelp( + final LspProjectContext project, + final String documentUri, + final String text, + final int line, + final int character) { + return new BaselineSignatureHelp(List.of(new BaselineSignature("demo()", List.of(), "")), 0, 0); + } + + @Override + public BaselineSemanticTokens semanticTokens(final LspProjectContext context, final String documentUri, final String text) { + return new BaselineSemanticTokens(List.of(), List.of()); + } + + @Override + public String onSave(final LspProjectContext project, final String documentUri) { + return "saved"; + } + } + + private static final class RecordingMapper implements ProtocolMessageMapper { + private InitializeResult initializeResult; + private BaselineServerDescription lastServerDescription; + + @Override + public InitializeResult mapInitializeResult(final BaselineServerDescription description) { + lastServerDescription = description; + return initializeResult; + } + + @Override + public PublishDiagnosticsParams mapDiagnostics(final String uri, final BaselineDocumentAnalysis analysis) { + throw new UnsupportedOperationException(); + } + + @Override + public PublishDiagnosticsParams emptyDiagnostics(final String uri) { + throw new UnsupportedOperationException(); + } + + @Override + public Hover mapHover(final BaselineHover hover) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.CompletionList mapCompletion(final BaselineCompletion completion) { + throw new UnsupportedOperationException(); + } + + @Override + public SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) { + throw new UnsupportedOperationException(); + } + + @Override + public SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) { + throw new UnsupportedOperationException(); + } + + @Override + public MessageParams mapInfoMessage(final String message) { + final MessageParams params = new MessageParams(); + params.setMessage(message); + return params; + } + + @Override + public MessageParams mapErrorMessage(final String message) { + final MessageParams params = new MessageParams(); + params.setMessage(message); + return params; + } + + @Override + public MessageParams mapWarningMessage(final String message) { + final MessageParams params = new MessageParams(); + params.setMessage(message); + return params; + } + } + + private static final class NoopTextDocumentService implements TextDocumentService { + @Override + public void didOpen(final org.eclipse.lsp4j.DidOpenTextDocumentParams params) { + } + + @Override + public void didChange(final org.eclipse.lsp4j.DidChangeTextDocumentParams params) { + } + + @Override + public void didClose(final org.eclipse.lsp4j.DidCloseTextDocumentParams params) { + } + + @Override + public void didSave(final org.eclipse.lsp4j.DidSaveTextDocumentParams params) { + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java new file mode 100644 index 00000000..05908c78 --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/PrometeuTextDocumentServiceTest.java @@ -0,0 +1,212 @@ +package p.studio.lsp.services.protocol; + +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.Hover; +import org.eclipse.lsp4j.HoverParams; +import org.eclipse.lsp4j.MarkupContent; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SignatureHelpParams; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.TextDocumentItem; +import org.junit.jupiter.api.Test; +import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineCompletionItem; +import p.studio.lsp.messages.BaselineCompletionItemKind; +import p.studio.lsp.messages.BaselineDocumentAnalysis; +import p.studio.lsp.messages.BaselineHover; +import p.studio.lsp.messages.BaselineSemanticTokens; +import p.studio.lsp.messages.BaselineServerDescription; +import p.studio.lsp.messages.BaselineSignature; +import p.studio.lsp.messages.BaselineSignatureHelp; +import p.studio.lsp.messages.LspProjectContext; +import p.studio.lsp.services.LanguageServiceBridge; +import p.studio.lsp.services.protocol.mapping.ProtocolMessageMapper; + +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class PrometeuTextDocumentServiceTest { + + @Test + void shouldPassOpenDocumentOverlayToCompletionHoverAndSignatureHelp() { + final var bridge = new RecordingBridge(); + final var mapper = new RecordingMapper(); + final var service = new PrometeuTextDocumentService( + new LspProjectContext("main", "pbs", Path.of(".")), + bridge, + mapper); + final String uri = Path.of("demo.pbs").toUri().toString(); + final String text = """ + fn frame() -> void { + demo(); + } + """; + + final var openParams = new DidOpenTextDocumentParams(new TextDocumentItem(uri, "pbs", 1, text)); + service.didOpen(openParams); + + final var completion = service.completion(new CompletionParams( + new TextDocumentIdentifier(uri), + new Position(1, 2))).join(); + final var completionList = completion.getRight(); + assertSame(mapper.completionList, completionList); + assertEquals(text, bridge.lastCompletionText); + assertEquals(1, bridge.lastCompletionLine); + assertEquals(2, bridge.lastCompletionCharacter); + + final Hover hover = service.hover(new HoverParams( + new TextDocumentIdentifier(uri), + new Position(1, 4))).join(); + final var hoverContent = (MarkupContent) hover.getContents().getRight(); + assertEquals("hover", hoverContent.getValue()); + assertEquals(text, bridge.lastHoverText); + + final SignatureHelp signatureHelp = service.signatureHelp(new SignatureHelpParams( + new TextDocumentIdentifier(uri), + new Position(1, 6))).join(); + assertSame(mapper.signatureHelp, signatureHelp); + assertEquals(text, bridge.lastSignatureHelpText); + assertEquals(1, bridge.lastSignatureHelpLine); + assertEquals(6, bridge.lastSignatureHelpCharacter); + assertTrue(mapper.mappedSignatureHelp.signatures().getFirst().label().contains("demo")); + } + + private static final class RecordingBridge implements LanguageServiceBridge { + private String lastCompletionText; + private int lastCompletionLine; + private int lastCompletionCharacter; + private String lastHoverText; + private String lastSignatureHelpText; + private int lastSignatureHelpLine; + private int lastSignatureHelpCharacter; + + @Override + public BaselineServerDescription describeServer(final LspProjectContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public BaselineDocumentAnalysis analyzeDocument(final LspProjectContext context, final String documentUri, final String text) { + return new BaselineDocumentAnalysis(List.of()); + } + + @Override + public BaselineCompletion completion( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + lastCompletionText = text; + lastCompletionLine = line; + lastCompletionCharacter = character; + return new BaselineCompletion(false, List.of( + new BaselineCompletionItem("demo", BaselineCompletionItemKind.FUNCTION, "", ""))); + } + + @Override + public BaselineHover hover( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + lastHoverText = text; + return new BaselineHover("hover"); + } + + @Override + public BaselineSignatureHelp signatureHelp( + final LspProjectContext context, + final String documentUri, + final String text, + final int line, + final int character) { + lastSignatureHelpText = text; + lastSignatureHelpLine = line; + lastSignatureHelpCharacter = character; + return new BaselineSignatureHelp(List.of( + new BaselineSignature("demo()", List.of(), "")), 0, 0); + } + + @Override + public BaselineSemanticTokens semanticTokens(final LspProjectContext context, final String documentUri, final String text) { + return new BaselineSemanticTokens(List.of(), List.of()); + } + + @Override + public String onSave(final LspProjectContext context, final String documentUri) { + return "saved"; + } + } + + private static final class RecordingMapper implements ProtocolMessageMapper { + private final CompletionList completionList = new CompletionList(false, List.of(new CompletionItem("demo"))); + private final SignatureHelp signatureHelp = new SignatureHelp(); + private BaselineSignatureHelp mappedSignatureHelp; + + @Override + public org.eclipse.lsp4j.InitializeResult mapInitializeResult(final BaselineServerDescription description) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.PublishDiagnosticsParams mapDiagnostics( + final String uri, + final BaselineDocumentAnalysis analysis) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.PublishDiagnosticsParams emptyDiagnostics(final String uri) { + throw new UnsupportedOperationException(); + } + + @Override + public Hover mapHover(final BaselineHover hover) { + final var content = new MarkupContent(); + content.setKind("markdown"); + content.setValue(hover.markdown()); + return new Hover(content); + } + + @Override + public CompletionList mapCompletion(final BaselineCompletion completion) { + return completionList; + } + + @Override + public SignatureHelp mapSignatureHelp(final BaselineSignatureHelp signatureHelp) { + this.mappedSignatureHelp = signatureHelp; + return this.signatureHelp; + } + + @Override + public org.eclipse.lsp4j.SemanticTokens mapSemanticTokens(final BaselineSemanticTokens semanticTokens) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapInfoMessage(final String message) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapErrorMessage(final String message) { + throw new UnsupportedOperationException(); + } + + @Override + public org.eclipse.lsp4j.MessageParams mapWarningMessage(final String message) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java new file mode 100644 index 00000000..78516e5b --- /dev/null +++ b/prometeu-lsp/prometeu-lsp-v1/src/test/java/p/studio/lsp/services/protocol/mapping/Lsp4jProtocolMessageMapperTest.java @@ -0,0 +1,103 @@ +package p.studio.lsp.services.protocol.mapping; + +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.junit.jupiter.api.Test; +import p.studio.lsp.messages.BaselineCompletion; +import p.studio.lsp.messages.BaselineCompletionItem; +import p.studio.lsp.messages.BaselineCompletionItemKind; +import p.studio.lsp.messages.BaselineEditorPalette; +import p.studio.lsp.messages.BaselineSemanticHostProjection; +import p.studio.lsp.messages.BaselineSemanticHostProjectionEntry; +import p.studio.lsp.messages.BaselineSignature; +import p.studio.lsp.messages.BaselineSignatureHelp; +import p.studio.lsp.messages.BaselineServerDescription; +import p.studio.lsp.messages.BaselineTokenStyle; +import p.studio.lsp.messages.BaselineVisualTheme; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class Lsp4jProtocolMessageMapperTest { + + @Test + void initializeResultPublishesCanonicalKeysAndHostProjectionPayload() { + final var mapper = new Lsp4jProtocolMessageMapper(); + final var description = new BaselineServerDescription( + "demo", + "1", + "pbs", + true, + true, + true, + List.of("demo-keyword"), + List.of(new BaselineSemanticHostProjection( + "vscode", + List.of(new BaselineSemanticHostProjectionEntry( + "demo-keyword", + "keyword", + List.of("declaration"), + "variable", + List.of())))), + List.of(new BaselineVisualTheme( + "demo-default", + "Demo Default", + new BaselineEditorPalette("#111111", "#222222", "#333333", "#444444", "#555555", "#666666", "#777777"), + List.of(new BaselineTokenStyle("demo-keyword", "#888888", false, false, false)))), + "demo-default"); + + final InitializeResult result = mapper.mapInitializeResult(description); + + assertEquals(List.of("."), result.getCapabilities().getCompletionProvider().getTriggerCharacters()); + assertEquals(List.of("(", ","), result.getCapabilities().getSignatureHelpProvider().getTriggerCharacters()); + + final var experimental = assertInstanceOf(Map.class, result.getCapabilities().getExperimental()); + final var semanticPayload = assertInstanceOf(Map.class, experimental.get("prometeuSemanticHostProjections")); + assertEquals("pbs", semanticPayload.get("frontendLanguageId")); + assertEquals(List.of("demo-keyword"), semanticPayload.get("semanticKeys")); + + final var hostProjectionPayloads = assertInstanceOf(List.class, semanticPayload.get("hostProjections")); + final var firstHostProjection = assertInstanceOf(Map.class, hostProjectionPayloads.getFirst()); + assertEquals("vscode", firstHostProjection.get("hostId")); + + final var tokenProjectionPayloads = assertInstanceOf(List.class, firstHostProjection.get("tokenProjections")); + final var firstTokenProjection = assertInstanceOf(Map.class, tokenProjectionPayloads.getFirst()); + assertEquals("demo-keyword", firstTokenProjection.get("semanticKey")); + assertEquals("keyword", firstTokenProjection.get("hostTokenType")); + assertEquals(List.of("declaration"), firstTokenProjection.get("hostTokenModifiers")); + assertEquals("variable", firstTokenProjection.get("fallbackTokenType")); + } + + @Test + void completionAndSignatureHelpMapToLspPayloads() { + final var mapper = new Lsp4jProtocolMessageMapper(); + + final var completion = mapper.mapCompletion(new BaselineCompletion( + false, + List.of(new BaselineCompletionItem( + "blend", + BaselineCompletionItemKind.METHOD, + "blend(dx: int, dy: int) -> int", + "method docs")))); + assertEquals(1, completion.getItems().size()); + assertEquals("blend", completion.getItems().getFirst().getLabel()); + assertEquals("blend(dx: int, dy: int) -> int", completion.getItems().getFirst().getDetail()); + final var documentation = assertInstanceOf(Either.class, completion.getItems().getFirst().getDocumentation()); + assertTrue(documentation.isRight()); + + final var signatureHelp = mapper.mapSignatureHelp(new BaselineSignatureHelp( + List.of(new BaselineSignature( + "mix(left: int, right: int) -> int", + List.of("left: int", "right: int"), + "")), + 0, + 1)); + assertEquals(1, signatureHelp.getSignatures().size()); + assertEquals("mix(left: int, right: int) -> int", signatureHelp.getSignatures().getFirst().getLabel()); + assertEquals(1, signatureHelp.getActiveParameter()); + } +} diff --git a/prometeu-studio/build.gradle.kts b/prometeu-studio/build.gradle.kts index 5dadc75d..4993f398 100644 --- a/prometeu-studio/build.gradle.kts +++ b/prometeu-studio/build.gradle.kts @@ -6,7 +6,6 @@ plugins { dependencies { implementation(project(":prometeu-infra")) implementation(project(":prometeu-lsp:prometeu-lsp-api")) - implementation(project(":prometeu-vfs:prometeu-vfs-api")) implementation(project(":prometeu-packer:prometeu-packer-api")) implementation(project(":prometeu-compiler:prometeu-compiler-core")) implementation(project(":prometeu-compiler:prometeu-build-pipeline")) @@ -15,6 +14,7 @@ dependencies { implementation(libs.javafx.controls) implementation(libs.javafx.fxml) implementation(libs.richtextfx) + testImplementation(project(":prometeu-compiler:frontends:prometeu-frontend-pbs")) testImplementation(project(":prometeu-packer:prometeu-packer-v1")) } diff --git a/prometeu-studio/src/main/java/p/studio/Container.java b/prometeu-studio/src/main/java/p/studio/Container.java index a5a226da..8af847e8 100644 --- a/prometeu-studio/src/main/java/p/studio/Container.java +++ b/prometeu-studio/src/main/java/p/studio/Container.java @@ -1,11 +1,10 @@ package p.studio; import com.fasterxml.jackson.databind.ObjectMapper; -import p.studio.lsp.LspServiceFactory; +import p.studio.lsp.api.LspServerLifecycle; import p.studio.lsp.events.StudioEventBus; import p.studio.utilities.ThemeService; import p.studio.utilities.i18n.I18nService; -import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -19,14 +18,12 @@ public interface Container { ObjectMapper getMapper(); - LspServiceFactory getPrometeuLspServiceFactory(); - - ProjectDocumentVfsFactory getProjectDocumentVfsFactory(); - EmbeddedPacker getPacker(); StudioBackgroundTasks getBackgroundTasks(); + LspServerLifecycle getLspServerLifecycle(); + void shutdownContainer(); static void install(Container container) { @@ -66,14 +63,6 @@ public interface Container { return current().getMapper(); } - static LspServiceFactory prometeuLspServiceFactory() { - return current().getPrometeuLspServiceFactory(); - } - - static ProjectDocumentVfsFactory projectDocumentVfsFactory() { - return current().getProjectDocumentVfsFactory(); - } - static EmbeddedPacker packer() { return current().getPacker(); } @@ -82,6 +71,10 @@ public interface Container { return current().getBackgroundTasks(); } + static LspServerLifecycle lspServerLifecycle() { + return current().getLspServerLifecycle(); + } + static void shutdown() { final Container container = Holder.CURRENT.get(); if (container != null) { diff --git a/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java b/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java index c0dbc8ce..32f881b6 100644 --- a/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java +++ b/prometeu-studio/src/main/java/p/studio/projects/ProjectReference.java @@ -2,7 +2,7 @@ package p.studio.projects; import org.apache.commons.lang3.StringUtils; import p.packer.messages.PackerProjectContext; -import p.studio.vfs.messages.VfsProjectContext; +import p.studio.lsp.messages.LspProjectContext; import java.nio.file.Path; @@ -37,7 +37,10 @@ public record ProjectReference( return new PackerProjectContext(name, absoluteRootPath()); } - public VfsProjectContext toVfsProjectContext() { - return new VfsProjectContext(name, languageId, absoluteRootPath()); + public LspProjectContext toLspProjectContext() { + return new LspProjectContext( + rootPath().toAbsolutePath().normalize().toString(), + languageId(), + rootPath()); } } diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java index 68401342..107c344a 100644 --- a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSession.java @@ -1,53 +1,46 @@ package p.studio.projectsessions; import p.studio.execution.StudioExecutionSessionService; -import p.studio.lsp.LspService; +import p.studio.lsp.api.LspServerLifecycle; +import p.studio.projects.ProjectReference; import p.studio.projectstate.ProjectLocalStudioSetup; import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioStateService; -import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; import java.util.Objects; public final class StudioProjectSession implements AutoCloseable { private final ProjectReference projectReference; - private final LspService prometeuLspService; - private final VfsProjectDocument vfsProjectDocument; private final ProjectLocalStudioStateService projectLocalStudioStateService; private final ProjectLocalStudioSetup projectLocalStudioSetup; private final StudioExecutionSessionService executionSessionService; + private final LspServerLifecycle lspServerLifecycle; private ProjectLocalStudioState projectLocalStudioState; private boolean closed; public StudioProjectSession( - final ProjectReference projectReference, - final LspService prometeuLspService, - final VfsProjectDocument vfsProjectDocument) { + final ProjectReference projectReference) { this( projectReference, - prometeuLspService, - vfsProjectDocument, new ProjectLocalStudioStateService(), ProjectLocalStudioSetup.defaults(), new StudioExecutionSessionService(), + null, ProjectLocalStudioState.defaults()); } StudioProjectSession( final ProjectReference projectReference, - final LspService prometeuLspService, - final VfsProjectDocument vfsProjectDocument, final ProjectLocalStudioStateService projectLocalStudioStateService, final ProjectLocalStudioSetup projectLocalStudioSetup, final StudioExecutionSessionService executionSessionService, + final LspServerLifecycle lspServerLifecycle, final ProjectLocalStudioState projectLocalStudioState) { this.projectReference = Objects.requireNonNull(projectReference, "projectReference"); - this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService"); - this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService"); this.projectLocalStudioSetup = Objects.requireNonNull(projectLocalStudioSetup, "projectLocalStudioSetup"); this.executionSessionService = Objects.requireNonNull(executionSessionService, "executionSessionService"); + this.lspServerLifecycle = lspServerLifecycle; this.projectLocalStudioState = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState"); } @@ -55,14 +48,6 @@ public final class StudioProjectSession implements AutoCloseable { return projectReference; } - public VfsProjectDocument projectDocumentVfs() { - return vfsProjectDocument; - } - - public LspService prometeuLspService() { - return prometeuLspService; - } - public ProjectLocalStudioState projectLocalStudioState() { return projectLocalStudioState; } @@ -96,25 +81,24 @@ public final class StudioProjectSession implements AutoCloseable { failure = runtimeException; } try { - prometeuLspService.close(); + shutdownLspServer(); } catch (RuntimeException runtimeException) { if (failure == null) { failure = runtimeException; } else { failure.addSuppressed(runtimeException); } - } - try { - vfsProjectDocument.close(); - } catch (RuntimeException runtimeException) { - if (failure == null) { - failure = runtimeException; - } else { - failure.addSuppressed(runtimeException); + } finally { + if (failure != null) { + throw failure; } } - if (failure != null) { - throw failure; - } + } + + private void shutdownLspServer() { + if (lspServerLifecycle == null) { + return; + } + lspServerLifecycle.shutdownServer(projectReference.toLspProjectContext()); } } diff --git a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java index cedb35c5..2004d2ab 100644 --- a/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java +++ b/prometeu-studio/src/main/java/p/studio/projectsessions/StudioProjectSessionFactory.java @@ -1,60 +1,46 @@ package p.studio.projectsessions; import p.studio.execution.StudioExecutionSessionService; -import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.LspServiceFactory; +import p.studio.lsp.api.LspServerLifecycle; +import p.studio.lsp.messages.LspServerConfiguration; +import p.studio.projects.ProjectReference; import p.studio.projectstate.ProjectLocalStudioSetupService; import p.studio.projectstate.ProjectLocalStudioStateService; -import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.ProjectDocumentVfsFactory; import java.util.Objects; public final class StudioProjectSessionFactory { - private final LspServiceFactory lspServiceFactory; - private final ProjectDocumentVfsFactory projectDocumentVfsFactory; private final ProjectLocalStudioStateService projectLocalStudioStateService; private final ProjectLocalStudioSetupService projectLocalStudioSetupService; + private final LspServerLifecycle lspServerLifecycle; - public StudioProjectSessionFactory( - final LspServiceFactory lspServiceFactory, - final ProjectDocumentVfsFactory projectDocumentVfsFactory) { + public StudioProjectSessionFactory(final LspServerLifecycle lspServerLifecycle) { this( - lspServiceFactory, - projectDocumentVfsFactory, new ProjectLocalStudioStateService(), - new ProjectLocalStudioSetupService()); + new ProjectLocalStudioSetupService(), + Objects.requireNonNull(lspServerLifecycle, "lspServerLifecycle")); } StudioProjectSessionFactory( - final LspServiceFactory lspServiceFactory, - final ProjectDocumentVfsFactory projectDocumentVfsFactory, final ProjectLocalStudioStateService projectLocalStudioStateService, - final ProjectLocalStudioSetupService projectLocalStudioSetupService) { - this.lspServiceFactory = Objects.requireNonNull(lspServiceFactory, "prometeuLspServiceFactory"); - this.projectDocumentVfsFactory = Objects.requireNonNull(projectDocumentVfsFactory, "projectDocumentVfsFactory"); + final ProjectLocalStudioSetupService projectLocalStudioSetupService, + final LspServerLifecycle lspServerLifecycle) { this.projectLocalStudioStateService = Objects.requireNonNull(projectLocalStudioStateService, "projectLocalStudioStateService"); this.projectLocalStudioSetupService = Objects.requireNonNull(projectLocalStudioSetupService, "projectLocalStudioSetupService"); + this.lspServerLifecycle = Objects.requireNonNull(lspServerLifecycle, "lspServerLifecycle"); } public StudioProjectSession open(final ProjectReference projectReference) { final ProjectReference target = Objects.requireNonNull(projectReference, "projectReference"); - final VfsProjectDocument vfsProjectDocument = projectDocumentVfsFactory.open(target.toVfsProjectContext()); + final var setup = projectLocalStudioSetupService.load(target); + final var state = projectLocalStudioStateService.load(target); + lspServerLifecycle.bootServer(LspServerConfiguration.defaults(), target.toLspProjectContext()); return new StudioProjectSession( target, - lspServiceFactory.open(lspProjectContext(target), vfsProjectDocument), - vfsProjectDocument, projectLocalStudioStateService, - projectLocalStudioSetupService.load(target), + setup, new StudioExecutionSessionService(), - projectLocalStudioStateService.load(target)); - } - - private LspProjectContext lspProjectContext(final ProjectReference projectReference) { - return new LspProjectContext( - projectReference.name(), - projectReference.languageId(), - projectReference.rootPath()); + lspServerLifecycle, + state); } } diff --git a/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java b/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java index 7bc6278d..120b6c36 100644 --- a/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java +++ b/prometeu-studio/src/main/java/p/studio/projectstate/ProjectLocalStudioState.java @@ -3,57 +3,48 @@ package p.studio.projectstate; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public record ProjectLocalStudioState( int version, ShellLayout shellLayout, - OpenShellState openShellState, - EditorRestorationState editorRestoration) { + OpenShellState openShellState) { public static final int CURRENT_VERSION = 1; public ProjectLocalStudioState { version = version <= 0 ? CURRENT_VERSION : version; shellLayout = shellLayout == null ? ShellLayout.defaults() : shellLayout.normalize(); openShellState = openShellState == null ? OpenShellState.defaults() : openShellState.normalize(); - editorRestoration = editorRestoration == null ? EditorRestorationState.defaults() : editorRestoration.normalize(); } public static ProjectLocalStudioState defaults() { return new ProjectLocalStudioState( CURRENT_VERSION, ShellLayout.defaults(), - OpenShellState.defaults(), - EditorRestorationState.defaults()); + OpenShellState.defaults()); } public ProjectLocalStudioState withShellLayout(final ShellLayout nextShellLayout) { - return new ProjectLocalStudioState(version, nextShellLayout, openShellState, editorRestoration); + return new ProjectLocalStudioState(version, nextShellLayout, openShellState); } public ProjectLocalStudioState withOpenShellState(final OpenShellState nextOpenShellState) { - return new ProjectLocalStudioState(version, shellLayout, nextOpenShellState, editorRestoration); - } - - public ProjectLocalStudioState withEditorRestoration(final EditorRestorationState nextEditorRestoration) { - return new ProjectLocalStudioState(version, shellLayout, openShellState, nextEditorRestoration); + return new ProjectLocalStudioState(version, shellLayout, nextOpenShellState); } @JsonIgnoreProperties(ignoreUnknown = true) - public record ShellLayout(EditorLayoutState editorLayout, AssetsLayoutState assetsLayout) { + public record ShellLayout(AssetsLayoutState assetsLayout) { public ShellLayout { - editorLayout = editorLayout == null ? EditorLayoutState.defaults() : editorLayout.normalize(); assetsLayout = assetsLayout == null ? AssetsLayoutState.defaults() : assetsLayout.normalize(); } public static ShellLayout defaults() { - return new ShellLayout(EditorLayoutState.defaults(), AssetsLayoutState.defaults()); + return new ShellLayout(AssetsLayoutState.defaults()); } private ShellLayout normalize() { - return new ShellLayout(editorLayout, assetsLayout); + return new ShellLayout(assetsLayout); } } @@ -76,6 +67,9 @@ public record ProjectLocalStudioState( public record OpenShellState(String selectedWorkspaceId) { public OpenShellState { selectedWorkspaceId = normalizeText(selectedWorkspaceId); + if ("EDITOR".equals(selectedWorkspaceId)) { + selectedWorkspaceId = "ASSETS"; + } } public static OpenShellState defaults() { @@ -87,80 +81,6 @@ public record ProjectLocalStudioState( } } - @JsonIgnoreProperties(ignoreUnknown = true) - public record EditorLayoutState( - List contentSplitDividers, - List leftColumnDividers, - List rightColumnDividers, - DockPanelState outlinePanel, - DockPanelState helperPanel, - List navigatorExpandedPaths) { - public EditorLayoutState { - contentSplitDividers = normalizeDividerPositions(contentSplitDividers); - leftColumnDividers = normalizeDividerPositions(leftColumnDividers); - rightColumnDividers = normalizeDividerPositions(rightColumnDividers); - outlinePanel = outlinePanel == null ? DockPanelState.defaults() : outlinePanel.normalize(); - helperPanel = helperPanel == null ? DockPanelState.defaults() : helperPanel.normalize(); - navigatorExpandedPaths = normalizePaths(navigatorExpandedPaths); - } - - public static EditorLayoutState defaults() { - return new EditorLayoutState( - List.of(), - List.of(), - List.of(), - DockPanelState.defaults(), - DockPanelState.defaults(), - List.of()); - } - - private EditorLayoutState normalize() { - return new EditorLayoutState( - contentSplitDividers, - leftColumnDividers, - rightColumnDividers, - outlinePanel, - helperPanel, - navigatorExpandedPaths); - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public record DockPanelState(boolean expanded, Double dividerPosition) { - public DockPanelState { - dividerPosition = normalizeDividerPosition(dividerPosition); - } - - public static DockPanelState defaults() { - return new DockPanelState(true, null); - } - - private DockPanelState normalize() { - return new DockPanelState(expanded, dividerPosition); - } - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public record EditorRestorationState(List openTabPaths, String activeTabPath) { - public EditorRestorationState { - openTabPaths = normalizePaths(openTabPaths); - activeTabPath = normalizeText(activeTabPath); - if (activeTabPath != null && !openTabPaths.contains(activeTabPath)) { - final List expanded = new ArrayList<>(openTabPaths); - expanded.add(activeTabPath); - openTabPaths = List.copyOf(expanded); - } - } - - public static EditorRestorationState defaults() { - return new EditorRestorationState(List.of(), null); - } - - private EditorRestorationState normalize() { - return new EditorRestorationState(openTabPaths, activeTabPath); - } - } - private static String normalizeText(final String value) { if (value == null) { return null; @@ -169,20 +89,6 @@ public record ProjectLocalStudioState( return trimmed.isEmpty() ? null : trimmed; } - private static List normalizePaths(final List values) { - if (values == null || values.isEmpty()) { - return List.of(); - } - final LinkedHashSet normalized = new LinkedHashSet<>(); - for (final String value : values) { - final String text = normalizeText(value); - if (text != null) { - normalized.add(text); - } - } - return List.copyOf(normalized); - } - private static List normalizeDividerPositions(final List values) { if (values == null || values.isEmpty()) { return List.of(); @@ -196,11 +102,4 @@ public record ProjectLocalStudioState( } return List.copyOf(normalized); } - - private static Double normalizeDividerPosition(final Double value) { - if (value == null || value.isNaN() || value.isInfinite()) { - return null; - } - return Math.max(0.0d, Math.min(1.0d, value)); - } } diff --git a/prometeu-studio/src/main/java/p/studio/utilities/FrontendVisualThemeCssRenderer.java b/prometeu-studio/src/main/java/p/studio/utilities/FrontendVisualThemeCssRenderer.java new file mode 100644 index 00000000..24fef5ab --- /dev/null +++ b/prometeu-studio/src/main/java/p/studio/utilities/FrontendVisualThemeCssRenderer.java @@ -0,0 +1,69 @@ +package p.studio.utilities; + +import p.studio.compiler.models.FrontendSemanticPresentationSpec; +import p.studio.compiler.models.FrontendTokenStyleSpec; +import p.studio.compiler.models.FrontendVisualThemeSpec; + +import java.util.Objects; + +public final class FrontendVisualThemeCssRenderer { + + public String renderDefaultTheme( + final String languageId, + final FrontendSemanticPresentationSpec presentation) { + Objects.requireNonNull(presentation, "presentation"); + return renderTheme(languageId, presentation.defaultTheme()); + } + + public String renderTheme( + final String languageId, + final FrontendVisualThemeSpec theme) { + final String normalizedLanguageId = requireText(languageId, "languageId"); + final FrontendVisualThemeSpec resolvedTheme = Objects.requireNonNull(theme, "theme"); + final String editorSelector = ".editor-workspace-code-area-type-" + normalizedLanguageId; + final String statusChipSelector = ".editor-workspace-status-chip-type-" + normalizedLanguageId; + final StringBuilder css = new StringBuilder(); + + css.append(editorSelector).append(" {\n") + .append(" -fx-highlight-fill: ").append(resolvedTheme.editorPalette().activeHighlightBackground()).append(";\n") + .append("}\n\n") + .append(editorSelector).append(" .text {\n") + .append(" -fx-fill: ").append(resolvedTheme.editorPalette().baseForeground()).append(";\n") + .append("}\n\n") + .append(editorSelector).append(" .lineno {\n") + .append(" -fx-text-fill: ").append(resolvedTheme.editorPalette().lineNumberForeground()).append(";\n") + .append("}\n\n"); + + for (final FrontendTokenStyleSpec tokenStyle : resolvedTheme.tokenStyles()) { + css.append(editorSelector).append(" .text.editor-semantic-").append(tokenStyle.semanticKey()).append(" {\n") + .append(" -fx-fill: ").append(tokenStyle.foreground()).append(";\n"); + if (tokenStyle.italic()) { + css.append(" -fx-font-style: italic;\n"); + } + if (tokenStyle.bold()) { + css.append(" -fx-font-weight: bold;\n"); + } + if (tokenStyle.underline()) { + css.append(" -fx-underline: true;\n"); + } + css.append("}\n\n"); + } + + css.append(statusChipSelector).append(" {\n") + .append(" -fx-background-color: ").append(resolvedTheme.editorPalette().statusChipBackground()).append(";\n") + .append(" -fx-border-color: ").append(resolvedTheme.editorPalette().statusChipBorder()).append(";\n") + .append(" -fx-text-fill: ").append(resolvedTheme.editorPalette().statusChipForeground()).append(";\n") + .append("}\n"); + return css.toString(); + } + + private static String requireText( + final String value, + final String field) { + final String candidate = Objects.requireNonNull(value, field).trim(); + if (candidate.isEmpty()) { + throw new IllegalArgumentException(field + " must not be blank"); + } + return candidate; + } +} diff --git a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java index 6eb889dc..b423f17b 100644 --- a/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java +++ b/prometeu-studio/src/main/java/p/studio/utilities/i18n/I18n.java @@ -78,38 +78,6 @@ public enum I18n { TOOLBAR_STOP("toolbar.stop"), TOOLBAR_EXPORT("toolbar.export"), - WORKSPACE_CODE("workspace.code"), - CODE_EDITOR_NAVIGATOR_TITLE("codeEditor.navigator.title"), - CODE_EDITOR_NAVIGATOR_REFRESH("codeEditor.navigator.refresh"), - CODE_EDITOR_NAVIGATOR_REVEAL_ACTIVE("codeEditor.navigator.revealActive"), - CODE_EDITOR_NAVIGATOR_PLACEHOLDER("codeEditor.navigator.placeholder"), - CODE_EDITOR_NAVIGATOR_DETAIL("codeEditor.navigator.detail"), - CODE_EDITOR_OUTLINE_TITLE("codeEditor.outline.title"), - CODE_EDITOR_OUTLINE_PLACEHOLDER("codeEditor.outline.placeholder"), - CODE_EDITOR_OUTLINE_SYMBOLS("codeEditor.outline.symbols"), - CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS("codeEditor.outline.emptySymbols"), - CODE_EDITOR_HELPER_TITLE("codeEditor.helper.title"), - CODE_EDITOR_HELPER_PLACEHOLDER("codeEditor.helper.placeholder"), - CODE_EDITOR_HELPER_DIAGNOSTICS("codeEditor.helper.diagnostics"), - CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS("codeEditor.helper.emptyDiagnostics"), - CODE_EDITOR_TABS_PLACEHOLDER("codeEditor.tabs.placeholder"), - CODE_EDITOR_TABS_OVERFLOW("codeEditor.tabs.overflow"), - CODE_EDITOR_STATUS_BREADCRUMB("codeEditor.status.breadcrumb"), - CODE_EDITOR_STATUS_POSITION("codeEditor.status.position"), - CODE_EDITOR_STATUS_LINE_SEPARATOR("codeEditor.status.lineSeparator"), - CODE_EDITOR_STATUS_INDENTATION("codeEditor.status.indentation"), - CODE_EDITOR_STATUS_LANGUAGE("codeEditor.status.language"), - CODE_EDITOR_COMMAND_SAVE("codeEditor.command.save"), - CODE_EDITOR_COMMAND_SAVE_ALL("codeEditor.command.saveAll"), - CODE_EDITOR_CLOSE_DIRTY_TITLE("codeEditor.closeDirty.title"), - CODE_EDITOR_CLOSE_DIRTY_MESSAGE("codeEditor.closeDirty.message"), - CODE_EDITOR_CLOSE_DIRTY_SAVE("codeEditor.closeDirty.save"), - CODE_EDITOR_CLOSE_DIRTY_DISCARD("codeEditor.closeDirty.discard"), - CODE_EDITOR_CLOSE_DIRTY_CANCEL("codeEditor.closeDirty.cancel"), - CODE_EDITOR_WARNING_FRONTEND_READ_ONLY("codeEditor.warning.frontendReadOnly"), - CODE_EDITOR_UNSUPPORTED_FILE_TITLE("codeEditor.unsupportedFile.title"), - CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE("codeEditor.unsupportedFile.message"), - WORKSPACE_SHIPPER("workspace.shipper"), WORKSPACE_SHIPPER_LOGS("workspace.shipper.logs"), WORKSPACE_SHIPPER_BUTTON_RUN("workspace.shipper.button.run"), diff --git a/prometeu-studio/src/main/java/p/studio/window/MainView.java b/prometeu-studio/src/main/java/p/studio/window/MainView.java index 5a1a89d8..3fa406e6 100644 --- a/prometeu-studio/src/main/java/p/studio/window/MainView.java +++ b/prometeu-studio/src/main/java/p/studio/window/MainView.java @@ -15,7 +15,6 @@ import p.studio.workspaces.WorkspaceId; import p.studio.workspaces.assets.AssetWorkspace; import p.studio.workspaces.builder.ShipperWorkspace; import p.studio.workspaces.debug.DebugWorkspace; -import p.studio.workspaces.editor.EditorWorkspace; import java.util.List; @@ -24,7 +23,6 @@ public final class MainView extends BorderPane { private final ProjectReference projectReference; private final StudioProjectSession projectSession; private final AssetWorkspace assetWorkspace; - private final EditorWorkspace editorWorkspace; private final StudioWorkspaceRailControl workspaceRail; private final StudioPlayStopCoordinator playStopCoordinator; private boolean initializing; @@ -50,23 +48,14 @@ public final class MainView extends BorderPane { assetWorkspace = new AssetWorkspace(projectReference); host.register(assetWorkspace); - editorWorkspace = new EditorWorkspace( - projectReference, - projectSession.projectDocumentVfs(), - projectSession.prometeuLspService(), - projectSetup.editorIndentation()); - host.register(editorWorkspace); assetWorkspace.restoreProjectLocalState(persistedState); - editorWorkspace.restoreProjectLocalState(persistedState); assetWorkspace.setStateChangedAction(this::persistProjectLocalState); - editorWorkspace.setStateChangedAction(this::persistProjectLocalState); host.register(new DebugWorkspace(projectReference, projectSession.executionSession())); host.register(new ShipperWorkspace(projectReference)); workspaceRail = new StudioWorkspaceRailControl<>( List.of( new StudioWorkspaceRailItem<>(WorkspaceId.ASSETS, "📦", Container.i18n().bind(I18n.WORKSPACE_ASSETS)), - new StudioWorkspaceRailItem<>(WorkspaceId.EDITOR, "📝", Container.i18n().bind(I18n.WORKSPACE_CODE)), new StudioWorkspaceRailItem<>(WorkspaceId.DEBUG, "🎮", Container.i18n().bind(I18n.WORKSPACE_DEBUG)), new StudioWorkspaceRailItem<>(WorkspaceId.SHIPPER, "⚙️", Container.i18n().bind(I18n.WORKSPACE_SHIPPER)) ), @@ -102,14 +91,11 @@ public final class MainView extends BorderPane { projectSession.replaceProjectLocalStudioState( current.withOpenShellState(new ProjectLocalStudioState.OpenShellState(selectedWorkspace.name())) .withShellLayout(new ProjectLocalStudioState.ShellLayout( - editorWorkspace.captureLayoutState(), - assetWorkspace.captureLayoutState())) - .withEditorRestoration(editorWorkspace.captureRestorationState())); + assetWorkspace.captureLayoutState()))); } public void reapplyProjectLocalLayout() { assetWorkspace.reapplyPendingLayoutState(); - editorWorkspace.reapplyPendingLayoutState(); } public void close() { diff --git a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java index e198201b..be2b6321 100644 --- a/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java +++ b/prometeu-studio/src/main/java/p/studio/window/StudioWindowCoordinator.java @@ -37,9 +37,7 @@ public final class StudioWindowCoordinator { this.projectCatalogService = new ProjectCatalogService(resolveDefaultProjectsRoot()); this.knownProjectsService = new KnownProjectsService(resolveKnownProjectsStorage(), projectCatalogService); this.windowStateService = new WindowStateService(resolveWindowStateStorage()); - this.projectSessionFactory = new StudioProjectSessionFactory( - Container.prometeuLspServiceFactory(), - Container.projectDocumentVfsFactory()); + this.projectSessionFactory = new StudioProjectSessionFactory(Container.lspServerLifecycle()); this.launcherView = new ProjectLauncherView( knownProjectsService, projectCatalogService, diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java b/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java index 8f0b043e..c3c88b44 100644 --- a/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java +++ b/prometeu-studio/src/main/java/p/studio/workspaces/WorkspaceId.java @@ -1,7 +1,6 @@ package p.studio.workspaces; public enum WorkspaceId { - EDITOR, ASSETS, SHIPPER, DEBUG diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java deleted file mode 100644 index 9e24dded..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightOwner.java +++ /dev/null @@ -1,6 +0,0 @@ -package p.studio.workspaces.editor; - -enum EditorDocumentHighlightOwner { - LOCAL, - LSP -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java deleted file mode 100644 index 6323aacf..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingResult.java +++ /dev/null @@ -1,20 +0,0 @@ -package p.studio.workspaces.editor; - -import org.fxmisc.richtext.model.StyleSpans; -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -record EditorDocumentHighlightingResult( - EditorDocumentHighlightOwner owner, - StyleSpans> styleSpans, - List inlineHints) { - - EditorDocumentHighlightingResult { - owner = Objects.requireNonNull(owner, "owner"); - styleSpans = Objects.requireNonNull(styleSpans, "styleSpans"); - inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints")); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java deleted file mode 100644 index c5cf550a..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouter.java +++ /dev/null @@ -1,32 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSemanticHighlighting; - -final class EditorDocumentHighlightingRouter { - private EditorDocumentHighlightingRouter() { - } - - static EditorDocumentHighlightingResult route( - final EditorOpenFileBuffer fileBuffer, - final EditorDocumentPresentation presentation, - final LspAnalyzeDocumentResult analysis) { - final var localHighlighting = presentation.highlight(fileBuffer.content()); - final var inlineHints = EditorDocumentInlineHintRouter.route(fileBuffer, analysis); - if (fileBuffer.frontendDocument() - && analysis != null - && presentation.supportsSemanticHighlighting() - && !analysis.semanticHighlights().isEmpty()) { - return new EditorDocumentHighlightingResult( - EditorDocumentHighlightOwner.LSP, - EditorDocumentSemanticHighlighting.overlay( - localHighlighting, - EditorDocumentSemanticHighlighting.highlight(fileBuffer.content(), analysis.semanticHighlights())), - inlineHints); - } - return new EditorDocumentHighlightingResult( - EditorDocumentHighlightOwner.LOCAL, - localHighlighting, - inlineHints); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentInlineHintRouter.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentInlineHintRouter.java deleted file mode 100644 index 84065142..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentInlineHintRouter.java +++ /dev/null @@ -1,20 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; - -import java.util.List; - -final class EditorDocumentInlineHintRouter { - private EditorDocumentInlineHintRouter() { - } - - static List route( - final EditorOpenFileBuffer fileBuffer, - final LspAnalyzeDocumentResult analysis) { - if (!fileBuffer.frontendDocument() || analysis == null || analysis.inlineHints().isEmpty()) { - return List.of(); - } - return analysis.inlineHints(); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java deleted file mode 100644 index 2b7dd55f..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentation.java +++ /dev/null @@ -1,30 +0,0 @@ -package p.studio.workspaces.editor; - -import org.fxmisc.richtext.model.StyleSpans; -import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -record EditorDocumentPresentation( - String styleKey, - List stylesheetUrls, - List semanticKeys, - EditorDocumentSyntaxHighlighting syntaxHighlighting) { - - EditorDocumentPresentation { - styleKey = Objects.requireNonNull(styleKey, "styleKey"); - stylesheetUrls = List.copyOf(Objects.requireNonNull(stylesheetUrls, "stylesheetUrls")); - semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys")); - syntaxHighlighting = Objects.requireNonNull(syntaxHighlighting, "syntaxHighlighting"); - } - - StyleSpans> highlight(final String content) { - return syntaxHighlighting.highlight(content); - } - - boolean supportsSemanticHighlighting() { - return !semanticKeys.isEmpty() && !stylesheetUrls.isEmpty(); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java deleted file mode 100644 index 846b280c..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistry.java +++ /dev/null @@ -1,122 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.compiler.FrontendRegistryService; -import p.studio.compiler.models.FrontendSemanticPresentationSpec; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.vfs.messages.VfsDocumentTypeIds; -import p.studio.workspaces.editor.syntaxhighlight.EditorDocumentSyntaxHighlighting; - -import java.net.URL; -import java.util.Locale; -import java.util.Objects; -import java.util.Optional; -import java.util.logging.Logger; - -final class EditorDocumentPresentationRegistry { - private static final Logger LOGGER = Logger.getLogger(EditorDocumentPresentationRegistry.class.getName()); - private static final EditorDocumentPresentation TEXT_PRESENTATION = new EditorDocumentPresentation( - "text", - java.util.List.of(), - java.util.List.of(), - EditorDocumentSyntaxHighlighting.plainText()); - private static final EditorDocumentPresentation JSON_PRESENTATION = new EditorDocumentPresentation( - "json", - java.util.List.of(stylesheet("presentations/json.css")), - java.util.List.of(), - EditorDocumentSyntaxHighlighting.json()); - private static final EditorDocumentPresentation BASH_PRESENTATION = new EditorDocumentPresentation( - "bash", - java.util.List.of(stylesheet("presentations/bash.css")), - java.util.List.of(), - EditorDocumentSyntaxHighlighting.bash()); - - EditorDocumentPresentation resolve(final String typeId) { - return resolve(typeId, null); - } - - EditorDocumentPresentation resolve( - final String typeId, - final LspSemanticPresentationDTO semanticPresentation) { - final String normalizedTypeId = normalize(typeId); - if (normalizedTypeId.isBlank()) { - return TEXT_PRESENTATION; - } - if (FrontendRegistryService.getFrontendSpec(normalizedTypeId).isPresent()) { - return frontendPresentation(normalizedTypeId, semanticPresentation); - } - if (VfsDocumentTypeIds.JSON.equals(normalizedTypeId)) { - return JSON_PRESENTATION; - } - if (VfsDocumentTypeIds.BASH.equals(normalizedTypeId)) { - return BASH_PRESENTATION; - } - return TEXT_PRESENTATION; - } - - private EditorDocumentPresentation frontendPresentation( - final String normalizedTypeId, - final LspSemanticPresentationDTO semanticPresentation) { - final var frontendSpec = FrontendRegistryService.getFrontendSpec(normalizedTypeId).orElseThrow(); - final FrontendSemanticPresentationSpec frontendPresentationSpec = frontendSpec.getSemanticPresentation(); - final java.util.List stylesheetUrls = resolveFrontendStylesheets( - semanticPresentation == null ? java.util.List.of() : semanticPresentation.resources(), - frontendPresentationSpec.resources()); - final java.util.List semanticKeys = semanticPresentation == null - ? frontendPresentationSpec.semanticKeys() - : semanticPresentation.semanticKeys(); - return new EditorDocumentPresentation( - normalizedTypeId, - stylesheetUrls, - semanticKeys, - frontendSyntaxHighlighting(normalizedTypeId)); - } - - private String normalize(final String typeId) { - return typeId == null ? "" : typeId.trim().toLowerCase(Locale.ROOT); - } - - private static String stylesheet(final String relativePath) { - return java.util.Objects.requireNonNull( - EditorDocumentPresentationRegistry.class.getResource("/themes/editor/" + relativePath), - "missing editor presentation stylesheet: " + relativePath) - .toExternalForm(); - } - - private static Optional resourceStylesheet(final String resourcePath) { - final String normalizedPath = normalizeResourcePath(resourcePath); - final URL resource = EditorDocumentPresentationRegistry.class.getResource(normalizedPath); - if (resource == null) { - LOGGER.fine("missing frontend semantic presentation resource: " + resourcePath); - return Optional.empty(); - } - return Optional.of(resource.toExternalForm()); - } - - private static java.util.List resolveFrontendStylesheets( - final java.util.List primaryResources, - final java.util.List fallbackResources) { - final java.util.List primary = primaryResources.stream() - .map(EditorDocumentPresentationRegistry::resourceStylesheet) - .flatMap(Optional::stream) - .toList(); - if (!primary.isEmpty()) { - return primary; - } - return fallbackResources.stream() - .map(EditorDocumentPresentationRegistry::resourceStylesheet) - .flatMap(Optional::stream) - .toList(); - } - - private static EditorDocumentSyntaxHighlighting frontendSyntaxHighlighting(final String normalizedTypeId) { - return EditorDocumentSyntaxHighlighting.plainText(); - } - - private static String normalizeResourcePath(final String resourcePath) { - final String normalized = Objects.requireNonNull(resourcePath, "resourcePath").trim(); - if (normalized.isEmpty()) { - throw new IllegalArgumentException("resourcePath cannot be blank"); - } - return normalized.startsWith("/") ? normalized : "/" + normalized; - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationStyles.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationStyles.java deleted file mode 100644 index cf0bedcb..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentPresentationStyles.java +++ /dev/null @@ -1,28 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.scene.Node; -import javafx.scene.control.Label; - -final class EditorDocumentPresentationStyles { - private static final String CODE_AREA_TYPE_CLASS_PREFIX = "editor-workspace-code-area-type-"; - private static final String STATUS_CHIP_TYPE_CLASS_PREFIX = "editor-workspace-status-chip-type-"; - - private EditorDocumentPresentationStyles() { - } - - static void applyToCodeArea(final Node node, final EditorDocumentPresentation presentation) { - replaceTypeClass(node, CODE_AREA_TYPE_CLASS_PREFIX, presentation.styleKey()); - } - - static void applyToStatusChip(final Label label, final EditorDocumentPresentation presentation) { - replaceTypeClass(label, STATUS_CHIP_TYPE_CLASS_PREFIX, presentation.styleKey()); - } - - private static void replaceTypeClass( - final Node node, - final String prefix, - final String styleKey) { - node.getStyleClass().removeIf(styleClass -> styleClass.startsWith(prefix)); - node.getStyleClass().add(prefix + styleKey); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java deleted file mode 100644 index d079e7fc..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideGraphicFactory.java +++ /dev/null @@ -1,91 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.scene.Node; -import javafx.scene.canvas.Canvas; -import javafx.scene.canvas.GraphicsContext; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import javafx.scene.paint.Color; - -final class EditorDocumentScopeGuideGraphicFactory { - private static final double COLUMN_WIDTH = 10.0; - private static final double CANVAS_PADDING = 6.0; - private static final double CONTAINER_X = CANVAS_PADDING + (COLUMN_WIDTH / 2.0); - private static final double SCOPE_X = CANVAS_PADDING + COLUMN_WIDTH + (COLUMN_WIDTH / 2.0); - private static final double GUIDE_CAP = 4.0; - private static final Color CONTAINER_COLOR = Color.web("#5f7896"); - private static final Color SCOPE_COLOR = Color.web("#8f72ff"); - - private EditorDocumentScopeGuideGraphicFactory() { - } - - static Node create( - final Node lineNumberNode, - final int paragraphIndex, - final EditorDocumentScopeGuideModel model, - final EditorDocumentScopeGuideModel.ActiveGuides activeGuides) { - final var container = new HBox(lineNumberNode); - final var segments = model.segmentsForLine(paragraphIndex, activeGuides); - container.getChildren().add(new ScopeGuideCanvas(segments)); - return container; - } - - private static final class ScopeGuideCanvas extends Region { - private final Canvas canvas = new Canvas(); - private final java.util.List segments; - - private ScopeGuideCanvas( - final java.util.List segments) { - this.segments = segments; - getStyleClass().add("editor-workspace-scope-guide-gutter"); - getChildren().add(canvas); - setMinWidth(computeGuideWidth()); - setPrefWidth(computeGuideWidth()); - setMaxWidth(computeGuideWidth()); - } - - @Override - protected double computePrefWidth(final double height) { - return computeGuideWidth(); - } - - @Override - protected void layoutChildren() { - final double width = snapSizeX(getWidth()); - final double height = Math.max(1.0, snapSizeY(getHeight())); - canvas.setWidth(width); - canvas.setHeight(height); - draw(width, height); - } - - private void draw(final double width, final double height) { - final GraphicsContext graphics = canvas.getGraphicsContext2D(); - graphics.clearRect(0, 0, width, height); - if (segments.isEmpty()) { - return; - } - final double centerY = Math.floor(height / 2.0); - for (final var segment : segments) { - final boolean container = segment.role() == EditorDocumentScopeGuideModel.GuideRole.ACTIVE_CONTAINER; - final double x = container ? CONTAINER_X : SCOPE_X; - graphics.setLineWidth(container ? 1.0 : 1.4); - graphics.setStroke(container ? CONTAINER_COLOR : SCOPE_COLOR); - switch (segment.kind()) { - case START -> { - graphics.strokeLine(x, centerY, x, height); - graphics.strokeLine(x, centerY, x + GUIDE_CAP, centerY); - } - case CONTINUE -> graphics.strokeLine(x, 0, x, height); - case END -> { - graphics.strokeLine(x, 0, x, centerY); - graphics.strokeLine(x, centerY, x + GUIDE_CAP, centerY); - } - } - } - } - - private static double computeGuideWidth() { - return CANVAS_PADDING * 2.0 + (COLUMN_WIDTH * 2.0); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java deleted file mode 100644 index b38a6706..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModel.java +++ /dev/null @@ -1,340 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; - -final class EditorDocumentScopeGuideModel { - private static final EditorDocumentScopeGuideModel EMPTY = - new EditorDocumentScopeGuideModel(List.of(), List.of(0), 0); - - private final List ranges; - private final List lineStarts; - private final int contentLength; - - private EditorDocumentScopeGuideModel( - final List ranges, - final List lineStarts, - final int contentLength) { - this.ranges = ranges; - this.lineStarts = lineStarts; - this.contentLength = contentLength; - } - - static EditorDocumentScopeGuideModel empty() { - return EMPTY; - } - - static EditorDocumentScopeGuideModel from( - final String content, - final List symbols) { - return fromRanges( - content, - symbols, - 0, - null, - (symbol, depth, parentId, nextId, lineStarts, contentLength) -> { - if (symbol.kind() == LspSymbolKind.UNKNOWN) { - return null; - } - final LspRangeDTO range = symbol.range(); - return toGuideRange( - lineStarts, - contentLength, - range, - new LspRangeDTO(range.startOffset(), Math.min(range.endOffset(), range.startOffset() + 1)), - new LspRangeDTO(Math.max(range.startOffset(), range.endOffset() - 1), range.endOffset()), - depth, - parentId, - nextId[0]); - }, - LspSymbolDTO::children); - } - - static EditorDocumentScopeGuideModel fromStructuralAnchors( - final String content, - final List structuralAnchors) { - return fromRanges( - content, - structuralAnchors, - 0, - null, - (anchor, depth, parentId, nextId, lineStarts, contentLength) -> toGuideRange( - lineStarts, - contentLength, - anchor.range(), - anchor.startAnchor(), - anchor.endAnchor(), - depth, - parentId, - nextId[0]), - LspStructuralAnchorDTO::children); - } - - private static EditorDocumentScopeGuideModel fromRanges( - final String content, - final List roots, - final int initialDepth, - final Integer initialParentId, - final RangeFactory rangeFactory, - final ChildAccessor childAccessor) { - final List lineStarts = lineStarts(content); - final List ranges = new ArrayList<>(); - final int[] nextId = new int[] {1}; - for (final T root : roots) { - appendRangeNode( - ranges, - lineStarts, - content.length(), - root, - initialDepth, - initialParentId, - nextId, - rangeFactory, - childAccessor); - } - ranges.sort(Comparator - .comparingInt(GuideRange::startOffset) - .thenComparingInt(GuideRange::depth) - .thenComparingInt(GuideRange::endOffset)); - return new EditorDocumentScopeGuideModel(List.copyOf(ranges), lineStarts, content.length()); - } - - ActiveGuides resolveActiveGuides(final int caretOffset) { - if (ranges.isEmpty()) { - return ActiveGuides.empty(); - } - final int probeOffset = clampCaretOffset(caretOffset); - GuideRange activeScope = null; - for (final GuideRange range : ranges) { - if (!contains(range, probeOffset)) { - continue; - } - if (activeScope == null - || span(range) < span(activeScope) - || (span(range) == span(activeScope) && range.depth() > activeScope.depth())) { - activeScope = range; - } - } - if (activeScope == null) { - return ActiveGuides.empty(); - } - final GuideRange activeContainer = activeScope.parentId() == null - ? null - : findById(activeScope.parentId()); - return new ActiveGuides(activeContainer, activeScope); - } - - List segmentsForLine( - final int lineIndex, - final ActiveGuides activeGuides) { - if (lineIndex < 0 || lineIndex >= totalLines() || activeGuides.isEmpty()) { - return List.of(); - } - final List segments = new ArrayList<>(2); - appendSegments(segments, lineIndex, activeGuides.activeContainer(), GuideRole.ACTIVE_CONTAINER); - appendSegments(segments, lineIndex, activeGuides.activeScope(), GuideRole.ACTIVE_SCOPE); - return List.copyOf(segments); - } - - int totalLines() { - return lineStarts.size(); - } - - private static void appendRangeNode( - final List ranges, - final List lineStarts, - final int contentLength, - final T node, - final int depth, - final Integer parentId, - final int[] nextId, - final RangeFactory rangeFactory, - final ChildAccessor childAccessor) { - Integer currentParentId = parentId; - final GuideRange range = rangeFactory.create(node, depth, parentId, nextId, lineStarts, contentLength); - if (range != null) { - ranges.add(range); - currentParentId = range.id(); - nextId[0]++; - } - for (final T child : childAccessor.children(node)) { - appendRangeNode(ranges, lineStarts, contentLength, child, depth + 1, currentParentId, nextId, rangeFactory, childAccessor); - } - } - - private static GuideRange toGuideRange( - final List lineStarts, - final int contentLength, - final LspRangeDTO range, - final LspRangeDTO startAnchor, - final LspRangeDTO endAnchor, - final int depth, - final Integer parentId, - final int id) { - final int startOffset = clamp(range.startOffset(), 0, contentLength); - final int endOffset = Math.max(startOffset, clamp(range.endOffset(), 0, contentLength)); - final int startAnchorOffset = clamp(startAnchor.startOffset(), startOffset, endOffset); - final int endAnchorOffset = clamp(Math.max(startAnchorOffset, endAnchor.startOffset()), startOffset, endOffset); - final int startLine = lineForOffset(lineStarts, startAnchorOffset); - final int inclusiveEndOffset = Math.max(startOffset, Math.max(startOffset, endOffset) - 1); - final int inclusiveEndAnchorOffset = Math.max( - startAnchorOffset, - Math.min(Math.max(startOffset, endOffset) - 1, Math.max(endAnchorOffset, startAnchorOffset))); - final int endLine = lineForOffset(lineStarts, clamp(inclusiveEndAnchorOffset, 0, contentLength)); - if (endLine <= startLine) { - return null; - } - return new GuideRange(id, depth, startOffset, endOffset, startAnchorOffset, endAnchorOffset, startLine, endLine, parentId); - } - - private GuideRange findById(final int id) { - for (final GuideRange range : ranges) { - if (range.id() == id) { - return range; - } - } - return null; - } - - private int clampCaretOffset(final int offset) { - if (contentLength <= 0) { - return 0; - } - return clamp(offset, 0, contentLength - 1); - } - - private static boolean contains(final GuideRange range, final int offset) { - return offset >= range.startOffset() && offset < range.endOffset(); - } - - private static int span(final GuideRange range) { - return range.endOffset() - range.startOffset(); - } - - private static void appendSegments( - final List segments, - final int lineIndex, - final GuideRange range, - final GuideRole role) { - if (range == null || lineIndex < range.startLine() || lineIndex > range.endLine()) { - return; - } - final GuideSegmentKind kind; - if (lineIndex == range.startLine()) { - kind = GuideSegmentKind.START; - } else if (lineIndex == range.endLine()) { - kind = GuideSegmentKind.END; - } else { - kind = GuideSegmentKind.CONTINUE; - } - segments.add(new ActiveGuideSegment(role, kind)); - } - - private static List lineStarts(final String content) { - if (content.isEmpty()) { - return List.of(0); - } - final List starts = new ArrayList<>(); - starts.add(0); - for (int index = 0; index < content.length(); index++) { - if (content.charAt(index) == '\n') { - starts.add(index + 1); - } - } - return Collections.unmodifiableList(starts); - } - - private static int lineForOffset(final List lineStarts, final int offset) { - int low = 0; - int high = lineStarts.size() - 1; - while (low <= high) { - final int mid = (low + high) >>> 1; - final int start = lineStarts.get(mid); - final int nextStart = mid + 1 < lineStarts.size() ? lineStarts.get(mid + 1) : Integer.MAX_VALUE; - if (offset < start) { - high = mid - 1; - continue; - } - if (offset >= nextStart) { - low = mid + 1; - continue; - } - return mid; - } - return Math.max(0, Math.min(lineStarts.size() - 1, low)); - } - - private static int clamp(final int value, final int minimum, final int maximum) { - return Math.max(minimum, Math.min(maximum, value)); - } - - enum GuideRole { - ACTIVE_CONTAINER, - ACTIVE_SCOPE - } - - enum GuideSegmentKind { - START, - CONTINUE, - END - } - - record GuideRange( - int id, - int depth, - int startOffset, - int endOffset, - int startAnchorOffset, - int endAnchorOffset, - int startLine, - int endLine, - Integer parentId) { - } - - record ActiveGuides( - GuideRange activeContainer, - GuideRange activeScope) { - private static final ActiveGuides EMPTY = new ActiveGuides(null, null); - - static ActiveGuides empty() { - return EMPTY; - } - - boolean isEmpty() { - return activeScope == null; - } - } - - record ActiveGuideSegment( - GuideRole role, - GuideSegmentKind kind) { - ActiveGuideSegment { - Objects.requireNonNull(role, "role"); - Objects.requireNonNull(kind, "kind"); - } - } - - @FunctionalInterface - private interface ChildAccessor { - List children(T node); - } - - @FunctionalInterface - private interface RangeFactory { - GuideRange create( - T node, - int depth, - Integer parentId, - int[] nextId, - List lineStarts, - int contentLength); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java deleted file mode 100644 index ba8ef5ef..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorHelperPanel.java +++ /dev/null @@ -1,96 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.VBox; -import p.studio.Container; -import p.studio.controls.WorkspaceDockPane; -import p.studio.lsp.dtos.LspDiagnosticDTO; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.List; - -public final class EditorHelperPanel extends WorkspaceDockPane { - public static final double COLLAPSED_HEIGHT = 34.0; - public static final double MINIMUM_EXPANDED_HEIGHT = 120.0; - public static final double DEFAULT_EXPANDED_HEIGHT = 180.0; - private final Label summary = new Label(); - private final Label diagnosticsTitle = new Label(); - private final VBox diagnosticsBox = new VBox(6); - - public EditorHelperPanel() { - super( - Container.i18n().bind(I18n.CODE_EDITOR_HELPER_TITLE), - COLLAPSED_HEIGHT, - MINIMUM_EXPANDED_HEIGHT, - DEFAULT_EXPANDED_HEIGHT, - false, - "editor-workspace-helper-pane", - "editor-workspace-helper-panel"); - - summary.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-helper-summary"); - summary.setWrapText(true); - diagnosticsTitle.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_DIAGNOSTICS)); - diagnosticsTitle.getStyleClass().add("editor-workspace-outline-section-title"); - diagnosticsBox.getStyleClass().add("editor-workspace-outline-list"); - - final var content = new VBox(10, summary, diagnosticsTitle, diagnosticsBox); - content.getStyleClass().add("editor-workspace-panel-content"); - content.setPadding(new Insets(10, 16, 14, 16)); - final var scrollPane = new ScrollPane(content); - scrollPane.setFitToWidth(true); - scrollPane.getStyleClass().add("editor-workspace-outline-scroll"); - setDockContent(scrollPane); - showPlaceholder(); - } - - public void showPlaceholder() { - summary.textProperty().unbind(); - summary.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_HELPER_PLACEHOLDER)); - diagnosticsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS)); - } - - public void showSemanticReadResult( - final Path documentPath, - final List diagnostics) { - summary.textProperty().unbind(); - summary.setText(documentPath.getFileName() + " • semantic read-only"); - rebuildDiagnostics(diagnostics); - } - - private void rebuildDiagnostics(final List diagnostics) { - diagnosticsBox.getChildren().clear(); - if (diagnostics.isEmpty()) { - diagnosticsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_HELPER_EMPTY_DIAGNOSTICS)); - return; - } - for (final LspDiagnosticDTO diagnostic : diagnostics) { - final var label = new Label(formatDiagnostic(diagnostic)); - label.setWrapText(true); - label.getStyleClass().addAll( - "editor-workspace-outline-item", - diagnostic.severity().name().equals("ERROR") - ? "editor-workspace-outline-diagnostic-error" - : "editor-workspace-outline-diagnostic-warning"); - diagnosticsBox.getChildren().add(label); - } - } - - private Label placeholderLabel(final I18n key) { - final var label = new Label(); - label.textProperty().bind(Container.i18n().bind(key)); - label.setWrapText(true); - label.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-item"); - return label; - } - - private String formatDiagnostic(final LspDiagnosticDTO diagnostic) { - return "%s [%d,%d) %s".formatted( - diagnostic.severity().name(), - diagnostic.range().startOffset(), - diagnostic.range().endOffset(), - diagnostic.message()); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintLayout.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintLayout.java deleted file mode 100644 index bab8e530..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintLayout.java +++ /dev/null @@ -1,72 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Optional; - -final class EditorInlineHintLayout { - static final double HORIZONTAL_GAP = 6.0; - - private EditorInlineHintLayout() { - } - - static List layout( - final List inlineHints, - final AnchorBoundsResolver anchorBoundsResolver) { - final var placements = new ArrayList(); - for (final var inlineHint : inlineHints) { - final var anchorBounds = anchorBoundsResolver.resolve(inlineHint); - if (anchorBounds.isEmpty()) { - continue; - } - final var bounds = anchorBounds.orElseThrow(); - placements.add(new Placement( - inlineHint, - displayText(inlineHint), - categoryStyleClass(inlineHint), - bounds.maxX() + HORIZONTAL_GAP, - bounds.minY(), - bounds.height())); - } - return List.copyOf(placements); - } - - static String displayText(final LspInlineHintDTO inlineHint) { - return ": %s".formatted(inlineHint.label()); - } - - static String categoryStyleClass(final LspInlineHintDTO inlineHint) { - final var normalizedCategory = inlineHint.category() - .toLowerCase(Locale.ROOT) - .replaceAll("[^a-z0-9]+", "-") - .replaceAll("^-+", "") - .replaceAll("-+$", ""); - return normalizedCategory.isBlank() - ? "editor-inline-hint-generic" - : "editor-inline-hint-" + normalizedCategory; - } - - @FunctionalInterface - interface AnchorBoundsResolver { - Optional resolve(LspInlineHintDTO inlineHint); - } - - record AnchorBounds( - double minX, - double minY, - double maxX, - double height) { - } - - record Placement( - LspInlineHintDTO inlineHint, - String displayText, - String categoryStyleClass, - double screenX, - double screenY, - double anchorHeight) { - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintOverlay.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintOverlay.java deleted file mode 100644 index 039e716b..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintOverlay.java +++ /dev/null @@ -1,124 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.application.Platform; -import javafx.geometry.Bounds; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.Pane; -import org.fxmisc.richtext.CodeArea; -import org.reactfx.Subscription; -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -final class EditorInlineHintOverlay { - private final CodeArea codeArea; - private final Pane overlayPane = new Pane(); - private final Map labelsByHint = new LinkedHashMap<>(); - private final Subscription viewportSubscription; - private List inlineHints = List.of(); - - EditorInlineHintOverlay(final CodeArea codeArea) { - this.codeArea = Objects.requireNonNull(codeArea, "codeArea"); - overlayPane.setMouseTransparent(true); - overlayPane.setPickOnBounds(false); - overlayPane.getStyleClass().add("editor-inline-hint-layer"); - codeArea.estimatedScrollXProperty().addListener((ignored, previous, current) -> refresh()); - codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) -> refresh()); - codeArea.layoutBoundsProperty().addListener((ignored, previous, current) -> refresh()); - codeArea.textProperty().addListener((ignored, previous, current) -> refresh()); - overlayPane.layoutBoundsProperty().addListener((ignored, previous, current) -> refresh()); - overlayPane.sceneProperty().addListener((ignored, previous, current) -> scheduleRefresh()); - viewportSubscription = codeArea.viewportDirtyEvents().subscribe(ignored -> refresh()); - } - - Node node() { - return overlayPane; - } - - void setInlineHints(final List inlineHints) { - this.inlineHints = List.copyOf(Objects.requireNonNull(inlineHints, "inlineHints")); - rebuildLabels(); - scheduleRefresh(); - } - - void clear() { - setInlineHints(List.of()); - } - - void refreshLater() { - scheduleRefresh(); - } - - void dispose() { - viewportSubscription.unsubscribe(); - } - - private void rebuildLabels() { - labelsByHint.clear(); - overlayPane.getChildren().clear(); - for (final var inlineHint : inlineHints) { - final var label = new Label(EditorInlineHintLayout.displayText(inlineHint)); - label.setManaged(false); - label.setMouseTransparent(true); - label.setPickOnBounds(false); - label.getStyleClass().add("editor-inline-hint"); - label.getStyleClass().add(EditorInlineHintLayout.categoryStyleClass(inlineHint)); - labelsByHint.put(inlineHint, label); - overlayPane.getChildren().add(label); - } - } - - private void scheduleRefresh() { - Platform.runLater(this::refresh); - } - - private void refresh() { - if (overlayPane.getScene() == null || inlineHints.isEmpty()) { - overlayPane.getChildren().forEach(node -> node.setVisible(false)); - return; - } - final var placements = EditorInlineHintLayout.layout(inlineHints, this::resolveAnchorBounds); - final var placedHints = new java.util.HashSet(); - for (final var placement : placements) { - final var label = labelsByHint.get(placement.inlineHint()); - if (label == null) { - continue; - } - label.applyCss(); - label.autosize(); - final Point2D localPoint = overlayPane.screenToLocal(placement.screenX(), placement.screenY()); - if (!Double.isFinite(localPoint.getX()) || !Double.isFinite(localPoint.getY())) { - label.setVisible(false); - continue; - } - final double layoutY = localPoint.getY() + Math.max(0.0d, (placement.anchorHeight() - label.getHeight()) / 2.0d); - label.relocate(localPoint.getX(), layoutY); - label.setVisible(true); - placedHints.add(placement.inlineHint()); - } - labelsByHint.forEach((inlineHint, label) -> { - if (!placedHints.contains(inlineHint)) { - label.setVisible(false); - } - }); - } - - private Optional resolveAnchorBounds(final LspInlineHintDTO inlineHint) { - if (codeArea.getLength() == 0) { - return Optional.empty(); - } - final int anchorOffset = Math.max(0, Math.min(codeArea.getLength(), inlineHint.anchor().endOffset())); - final Optional characterBounds = codeArea.getCharacterBoundsOnScreen(anchorOffset, anchorOffset); - return characterBounds.map(bounds -> new EditorInlineHintLayout.AnchorBounds( - bounds.getMinX(), - bounds.getMinY(), - bounds.getMaxX(), - bounds.getHeight())); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintProjection.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintProjection.java deleted file mode 100644 index 63f010ac..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorInlineHintProjection.java +++ /dev/null @@ -1,159 +0,0 @@ -package p.studio.workspaces.editor; - -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyleSpansBuilder; -import org.fxmisc.richtext.model.StyleSpan; -import p.studio.lsp.dtos.LspInlineHintDTO; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; - -final class EditorInlineHintProjection { - private final String displayText; - private final StyleSpans> displayStyles; - private final List protectedRanges; - - private EditorInlineHintProjection( - final String displayText, - final StyleSpans> displayStyles, - final List protectedRanges) { - this.displayText = Objects.requireNonNull(displayText, "displayText"); - this.displayStyles = Objects.requireNonNull(displayStyles, "displayStyles"); - this.protectedRanges = new ArrayList<>(Objects.requireNonNull(protectedRanges, "protectedRanges")); - } - - static EditorInlineHintProjection create( - final String sourceText, - final StyleSpans> sourceStyles, - final List inlineHints) { - Objects.requireNonNull(sourceText, "sourceText"); - Objects.requireNonNull(sourceStyles, "sourceStyles"); - Objects.requireNonNull(inlineHints, "inlineHints"); - if (inlineHints.isEmpty()) { - return new EditorInlineHintProjection(sourceText, sourceStyles, List.of()); - } - - final var sortedHints = inlineHints.stream() - .sorted(Comparator.comparingInt(hint -> hint.anchor().endOffset())) - .toList(); - final var displayText = new StringBuilder(sourceText.length() + (sortedHints.size() * 8)); - final var displayStyles = new StyleSpansBuilder>(); - final var protectedRanges = new ArrayList(); - - int sourceOffset = 0; - int displayOffset = 0; - for (final var inlineHint : sortedHints) { - final int anchorOffset = Math.max(0, Math.min(sourceText.length(), inlineHint.anchor().endOffset())); - if (anchorOffset > sourceOffset) { - displayText.append(sourceText, sourceOffset, anchorOffset); - appendSourceStyles(displayStyles, sourceStyles.subView(sourceOffset, anchorOffset)); - displayOffset += anchorOffset - sourceOffset; - sourceOffset = anchorOffset; - } - - final String hintText = " " + EditorInlineHintLayout.displayText(inlineHint); - displayText.append(hintText); - displayStyles.add(hintClasses(inlineHint), hintText.length()); - protectedRanges.add(new ProtectedRange(displayOffset, displayOffset + hintText.length())); - displayOffset += hintText.length(); - } - - if (sourceOffset < sourceText.length()) { - displayText.append(sourceText, sourceOffset, sourceText.length()); - appendSourceStyles(displayStyles, sourceStyles.subView(sourceOffset, sourceText.length())); - } - return new EditorInlineHintProjection(displayText.toString(), displayStyles.create(), protectedRanges); - } - - String displayText() { - return displayText; - } - - StyleSpans> displayStyles() { - return displayStyles; - } - - String stripDecorations(final String currentDisplayText) { - Objects.requireNonNull(currentDisplayText, "currentDisplayText"); - if (protectedRanges.isEmpty()) { - return currentDisplayText; - } - final var sourceText = new StringBuilder(currentDisplayText); - for (int index = protectedRanges.size() - 1; index >= 0; index--) { - final var range = protectedRanges.get(index); - final int start = Math.max(0, Math.min(sourceText.length(), range.start())); - final int end = Math.max(start, Math.min(sourceText.length(), range.end())); - sourceText.delete(start, end); - } - return sourceText.toString(); - } - - void applyDisplayChange(final int position, final int removedLength, final int insertedLength) { - final int delta = insertedLength - removedLength; - for (int index = 0; index < protectedRanges.size(); index++) { - final var range = protectedRanges.get(index); - if (range.end() <= position) { - continue; - } - if (range.start() >= position + removedLength) { - protectedRanges.set(index, range.shift(delta)); - continue; - } - protectedRanges.set(index, range); - } - } - - boolean containsOffset(final int offset) { - return protectedRanges.stream().anyMatch(range -> range.contains(offset)); - } - - boolean touchesRange(final int start, final int end) { - if (start == end) { - return containsOffset(start); - } - return protectedRanges.stream().anyMatch(range -> range.intersects(start, end)); - } - - int clampCaret(final int offset, final boolean preferRightBoundary) { - for (final var range : protectedRanges) { - if (!range.contains(offset)) { - continue; - } - return preferRightBoundary ? range.end() : range.start(); - } - return offset; - } - - private static void appendSourceStyles( - final StyleSpansBuilder> builder, - final StyleSpans> styles) { - for (final StyleSpan> span : styles) { - builder.add(span.getStyle(), span.getLength()); - } - } - - private static Collection hintClasses(final LspInlineHintDTO inlineHint) { - final var classes = new LinkedHashSet(); - classes.add("editor-inline-hint"); - classes.add(EditorInlineHintLayout.categoryStyleClass(inlineHint)); - return List.copyOf(classes); - } - - record ProtectedRange(int start, int end) { - boolean contains(final int offset) { - return offset >= start && offset < end; - } - - boolean intersects(final int rangeStart, final int rangeEnd) { - return rangeStart < end && rangeEnd > start; - } - - ProtectedRange shift(final int delta) { - return new ProtectedRange(start + delta, end + delta); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileBuffer.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileBuffer.java deleted file mode 100644 index 62e491f7..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileBuffer.java +++ /dev/null @@ -1,37 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.vfs.messages.VfsDocumentAccessMode; - -import java.nio.file.Path; -import java.util.Objects; - -public record EditorOpenFileBuffer( - Path path, - String tabLabel, - String typeId, - String content, - String lineSeparator, - boolean frontendDocument, - VfsDocumentAccessMode accessMode, - boolean dirty) { - public EditorOpenFileBuffer { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - tabLabel = Objects.requireNonNull(tabLabel, "tabLabel"); - typeId = Objects.requireNonNull(typeId, "typeId"); - content = Objects.requireNonNull(content, "content"); - lineSeparator = Objects.requireNonNull(lineSeparator, "lineSeparator"); - Objects.requireNonNull(accessMode, "accessMode"); - } - - public boolean editable() { - return accessMode == VfsDocumentAccessMode.EDITABLE; - } - - public boolean readOnly() { - return !editable(); - } - - public boolean saveEnabled() { - return editable() && dirty; - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java deleted file mode 100644 index b1e4bdb0..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOpenFileSession.java +++ /dev/null @@ -1,112 +0,0 @@ -package p.studio.workspaces.editor; - -import p.studio.projectstate.ProjectLocalStudioState; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public final class EditorOpenFileSession { - private final List openFiles = new ArrayList<>(); - private Path activePath; - - public void open(final EditorOpenFileBuffer fileBuffer) { - final var buffer = Objects.requireNonNull(fileBuffer, "fileBuffer"); - final var existingIndex = indexOf(buffer.path()); - if (existingIndex < 0) { - openFiles.add(buffer); - } else { - openFiles.set(existingIndex, buffer); - } - activePath = buffer.path(); - } - - public void activate(final Path path) { - final var normalizedPath = normalize(path); - if (find(normalizedPath).isPresent()) { - activePath = normalizedPath; - } - } - - public void close(final Path path) { - final int index = indexOf(path); - if (index < 0) { - return; - } - final Path normalizedPath = normalize(path); - openFiles.remove(index); - if (openFiles.isEmpty()) { - activePath = null; - return; - } - if (!normalizedPath.equals(activePath)) { - return; - } - final int nextIndex = Math.min(index, openFiles.size() - 1); - activePath = openFiles.get(nextIndex).path(); - } - - public List openFiles() { - return List.copyOf(openFiles); - } - - public Optional activeFile() { - if (activePath == null) { - return Optional.empty(); - } - return find(activePath); - } - - public Optional file(final Path path) { - return find(normalize(path)); - } - - public boolean hasDirtyEditableFiles() { - return openFiles.stream().anyMatch(EditorOpenFileBuffer::saveEnabled); - } - - public boolean hasSaveableActiveFile() { - return activeFile().map(EditorOpenFileBuffer::saveEnabled).orElse(false); - } - - public boolean isEmpty() { - return openFiles.isEmpty(); - } - - public void clear() { - openFiles.clear(); - activePath = null; - } - - public ProjectLocalStudioState.EditorRestorationState restorationState() { - return new ProjectLocalStudioState.EditorRestorationState( - openFiles.stream() - .map(buffer -> buffer.path().toString()) - .toList(), - activePath == null ? null : activePath.toString()); - } - - private Optional find(final Path path) { - final int index = indexOf(path); - if (index < 0) { - return Optional.empty(); - } - return Optional.of(openFiles.get(index)); - } - - private int indexOf(final Path path) { - final var normalizedPath = normalize(path); - for (int index = 0; index < openFiles.size(); index++) { - if (openFiles.get(index).path().equals(normalizedPath)) { - return index; - } - } - return -1; - } - - private Path normalize(final Path path) { - return Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java deleted file mode 100644 index 2432a6d6..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorOutlinePanel.java +++ /dev/null @@ -1,116 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.ScrollPane; -import javafx.scene.layout.VBox; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; -import p.studio.Container; -import p.studio.controls.WorkspaceDockPane; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.List; - -public final class EditorOutlinePanel extends WorkspaceDockPane { - private static final double COLLAPSED_HEIGHT = 34.0; - private static final double MINIMUM_EXPANDED_HEIGHT = 120.0; - private static final double DEFAULT_HEIGHT = 180.0; - private final Label summary = new Label(); - private final VBox symbolsBox = new VBox(6); - - public EditorOutlinePanel() { - super( - Container.i18n().bind(I18n.CODE_EDITOR_OUTLINE_TITLE), - COLLAPSED_HEIGHT, - MINIMUM_EXPANDED_HEIGHT, - DEFAULT_HEIGHT, - true, - "editor-workspace-outline-panel"); - - summary.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-summary"); - summary.setWrapText(true); - - final var symbolsTitle = sectionTitle(I18n.CODE_EDITOR_OUTLINE_SYMBOLS); - - symbolsBox.getStyleClass().add("editor-workspace-outline-list"); - - final var content = new VBox(10, - summary, - symbolsTitle, - symbolsBox); - content.getStyleClass().add("editor-workspace-panel-content"); - content.setPadding(new Insets(10, 16, 14, 16)); - final var scrollPane = new ScrollPane(content); - scrollPane.setFitToWidth(true); - scrollPane.getStyleClass().add("editor-workspace-outline-scroll"); - setDockContent(scrollPane); - showPlaceholder(); - } - - public void showPlaceholder() { - summary.textProperty().unbind(); - summary.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_OUTLINE_PLACEHOLDER)); - symbolsBox.getChildren().setAll(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); - } - - public void showSemanticReadResult( - final Path documentPath, - final List symbols) { - summary.textProperty().unbind(); - summary.setText(documentPath.getFileName() + " • semantic read-only"); - rebuildSymbols(symbols); - } - - private void rebuildSymbols(final List symbols) { - symbolsBox.getChildren().clear(); - int visible = 0; - for (final LspSymbolDTO symbol : symbols) { - visible += appendSymbol(symbol, 0); - } - if (visible == 0) { - symbolsBox.getChildren().add(placeholderLabel(I18n.CODE_EDITOR_OUTLINE_EMPTY_SYMBOLS)); - } - } - - private int appendSymbol(final LspSymbolDTO symbol, final int depth) { - int visible = 0; - int childDepth = depth; - if (showInOutline(symbol.kind())) { - final var label = new Label(symbol.name() + " • " + symbol.kind().name().toLowerCase()); - label.setWrapText(true); - label.setPadding(new Insets(0, 0, 0, depth * 12)); - label.getStyleClass().add("editor-workspace-outline-item"); - symbolsBox.getChildren().add(label); - visible++; - childDepth++; - } - for (final LspSymbolDTO child : symbol.children()) { - visible += appendSymbol(child, childDepth); - } - return visible; - } - - private boolean showInOutline(final LspSymbolKind kind) { - return switch (kind) { - case IF, SWITCH, FOR, WHILE, HANDLE -> false; - default -> true; - }; - } - - private Label sectionTitle(final I18n key) { - final var label = new Label(); - label.textProperty().bind(Container.i18n().bind(key)); - label.getStyleClass().add("editor-workspace-outline-section-title"); - return label; - } - - private Label placeholderLabel(final I18n key) { - final var label = new Label(); - label.textProperty().bind(Container.i18n().bind(key)); - label.setWrapText(true); - label.getStyleClass().addAll("editor-workspace-placeholder", "editor-workspace-outline-item"); - return label; - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorProjectNavigatorPanel.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorProjectNavigatorPanel.java deleted file mode 100644 index bbcb13a8..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorProjectNavigatorPanel.java +++ /dev/null @@ -1,337 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; -import javafx.scene.control.TreeCell; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import p.studio.Container; -import p.studio.utilities.i18n.I18n; -import p.studio.vfs.messages.VfsProjectNode; -import p.studio.vfs.messages.VfsProjectSnapshot; - -import java.nio.file.Path; -import java.util.LinkedHashSet; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; - -public final class EditorProjectNavigatorPanel extends BorderPane { - private final Button revealActiveButton = new Button(); - private final Button refreshButton = new Button(); - private final TreeView treeView = new TreeView<>(); - private final Label emptyState = emptyLabel(); - private final StackPane content = new StackPane(); - private Runnable revealActiveFileAction = () -> { }; - private Runnable refreshAction = () -> { }; - private Consumer fileSelectionAction = node -> { }; - private Set preferredExpandedPaths = Set.of(); - - public EditorProjectNavigatorPanel() { - getStyleClass().addAll("editor-workspace-panel", "editor-workspace-navigator-panel"); - setPadding(new Insets(14, 16, 14, 16)); - setTop(buildHeader()); - configureTreeView(); - content.getStyleClass().add("editor-workspace-panel-content"); - content.getChildren().addAll(treeView, emptyState); - setCenter(content); - showEmptyState(true); - } - - private HBox buildHeader() { - final var title = new Label(); - title.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_NAVIGATOR_TITLE)); - title.getStyleClass().add("editor-workspace-panel-title"); - - revealActiveButton.setFocusTraversable(false); - revealActiveButton.setDisable(true); - revealActiveButton.getStyleClass().addAll( - "studio-button", - "studio-button-icon", - "editor-workspace-header-icon-button"); - revealActiveButton.setGraphic(EditorWorkspaceIcons.target()); - revealActiveButton.setTooltip(new Tooltip(Container.i18n().text(I18n.CODE_EDITOR_NAVIGATOR_REVEAL_ACTIVE))); - revealActiveButton.setOnAction(event -> revealActiveFileAction.run()); - - refreshButton.setFocusTraversable(false); - refreshButton.getStyleClass().addAll( - "studio-button", - "studio-button-icon", - "editor-workspace-header-icon-button"); - refreshButton.setGraphic(EditorWorkspaceIcons.refresh()); - refreshButton.setTooltip(new Tooltip(Container.i18n().text(I18n.CODE_EDITOR_NAVIGATOR_REFRESH))); - refreshButton.setOnAction(event -> refreshAction.run()); - - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - final var header = new HBox(8, title, spacer, revealActiveButton, refreshButton); - header.getStyleClass().add("editor-workspace-panel-header"); - return header; - } - - private void configureTreeView() { - treeView.getStyleClass().add("editor-workspace-tree"); - treeView.setShowRoot(true); - treeView.setFocusTraversable(false); - treeView.setCellFactory(ignored -> new TreeCell<>() { - @Override - protected void updateItem(final VfsProjectNode item, final boolean empty) { - super.updateItem(item, empty); - getStyleClass().remove("editor-workspace-tree-cell-tagged"); - getStyleClass().remove("editor-workspace-tree-cell-root"); - if (empty || item == null) { - setText(null); - setGraphic(null); - return; - } - setText(labelFor(item, getTreeItem())); - setGraphic(iconFor(item, getTreeItem())); - if (isRoot(getTreeItem())) { - getStyleClass().add("editor-workspace-tree-cell-root"); - } - if (item.taggedSourceRoot() || isInsideTaggedSourceRoot(getTreeItem())) { - getStyleClass().add("editor-workspace-tree-cell-tagged"); - } - } - }); - treeView.setOnMouseClicked(event -> { - if (event.getClickCount() != 2) { - return; - } - final TreeItem current = treeView.getSelectionModel().getSelectedItem(); - if (current == null || current.getValue() == null || current.getValue().directory()) { - return; - } - fileSelectionAction.accept(current.getValue()); - }); - } - - public void setRefreshAction(final Runnable refreshAction) { - this.refreshAction = Objects.requireNonNull(refreshAction, "refreshAction"); - } - - public void setRevealActiveFileAction(final Runnable revealActiveFileAction) { - this.revealActiveFileAction = Objects.requireNonNull(revealActiveFileAction, "revealActiveFileAction"); - } - - public void setRevealActiveFileAvailable(final boolean available) { - revealActiveButton.setDisable(!available); - } - - public void setFileSelectionAction(final Consumer fileSelectionAction) { - this.fileSelectionAction = Objects.requireNonNull(fileSelectionAction, "fileSelectionAction"); - } - - public void setSnapshot(final VfsProjectSnapshot snapshot) { - final var currentSelection = selectedPath(); - final var expandedPaths = mergeExpandedPaths(); - final var rootItem = buildTreeItem(Objects.requireNonNull(snapshot, "snapshot").rootNode(), expandedPaths, true); - treeView.setRoot(rootItem); - restoreSelection(rootItem, currentSelection); - showEmptyState(false); - } - - public List expandedPathStrings() { - return captureExpandedPaths(treeView.getRoot()).stream() - .map(Path::toString) - .toList(); - } - - public void restoreExpandedPaths(final List expandedPaths) { - final LinkedHashSet restored = new LinkedHashSet<>(); - if (expandedPaths != null) { - for (final String expandedPath : expandedPaths) { - if (expandedPath == null || expandedPath.isBlank()) { - continue; - } - try { - restored.add(Path.of(expandedPath).toAbsolutePath().normalize()); - } catch (RuntimeException ignored) { - // Ignore malformed persisted paths and continue with the remaining ones. - } - } - } - preferredExpandedPaths = Set.copyOf(restored); - final TreeItem rootItem = treeView.getRoot(); - if (rootItem != null) { - final var currentSelection = selectedPath(); - treeView.setRoot(buildTreeItem(rootItem.getValue(), preferredExpandedPaths, true)); - restoreSelection(treeView.getRoot(), currentSelection); - } - } - - public void revealPath(final Path path) { - final var normalizedPath = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - final var rootItem = treeView.getRoot(); - if (rootItem == null) { - return; - } - findTreeItem(rootItem, normalizedPath).ifPresent(item -> { - expandAncestors(item); - treeView.getSelectionModel().select(item); - treeView.scrollTo(Math.max(treeView.getRow(item) - 2, 0)); - }); - } - - private TreeItem buildTreeItem( - final VfsProjectNode node, - final Set expandedPaths, - final boolean isRoot) { - final TreeItem item = node.directory() - ? new DirectoryTreeItem(node) - : new TreeItem<>(node); - item.setExpanded(isRoot || expandedPaths.contains(node.path())); - node.children().stream() - .map(child -> buildTreeItem(child, expandedPaths, false)) - .forEach(item.getChildren()::add); - return item; - } - - private Set captureExpandedPaths(final TreeItem item) { - final var expandedPaths = new HashSet(); - if (item == null) { - return expandedPaths; - } - collectExpandedPaths(item, expandedPaths); - return expandedPaths; - } - - private void collectExpandedPaths(final TreeItem item, final Set expandedPaths) { - final var value = item.getValue(); - if (value != null && item.isExpanded()) { - expandedPaths.add(value.path()); - } - item.getChildren().forEach(child -> collectExpandedPaths(child, expandedPaths)); - } - - private Set mergeExpandedPaths() { - final LinkedHashSet merged = new LinkedHashSet<>(preferredExpandedPaths); - merged.addAll(captureExpandedPaths(treeView.getRoot())); - return Set.copyOf(merged); - } - - private Optional selectedPath() { - return Optional.ofNullable(treeView.getSelectionModel().getSelectedItem()) - .map(TreeItem::getValue) - .map(VfsProjectNode::path); - } - - private void restoreSelection(final TreeItem rootItem, final Optional selectedPath) { - treeView.getSelectionModel().clearSelection(); - if (selectedPath.isEmpty()) { - return; - } - findTreeItem(rootItem, selectedPath.orElseThrow()) - .ifPresent(item -> treeView.getSelectionModel().select(item)); - } - - private Optional> findTreeItem(final TreeItem item, final Path path) { - if (item == null || item.getValue() == null) { - return Optional.empty(); - } - if (item.getValue().path().equals(path)) { - return Optional.of(item); - } - for (final var child : item.getChildren()) { - final var match = findTreeItem(child, path); - if (match.isPresent()) { - return match; - } - } - return Optional.empty(); - } - - private void showEmptyState(final boolean empty) { - treeView.setVisible(!empty); - treeView.setManaged(!empty); - emptyState.setVisible(empty); - emptyState.setManaged(empty); - } - - private void expandAncestors(final TreeItem item) { - TreeItem current = item; - while (current != null) { - current.setExpanded(true); - current = current.getParent(); - } - } - - private Node iconFor(final VfsProjectNode node, final TreeItem treeItem) { - if (node.directory()) { - if (isBuildTone(node)) { - return EditorWorkspaceIcons.folderBuild(); - } - if (node.taggedSourceRoot() || isInsideTaggedSourceRoot(treeItem)) { - return EditorWorkspaceIcons.folderSource(); - } - return EditorWorkspaceIcons.folderDefault(); - } - if ("prometeu.json".equals(node.displayName())) { - return EditorWorkspaceIcons.cog(); - } - - return EditorWorkspaceIcons.file(); - } - - private String labelFor(final VfsProjectNode node, final TreeItem treeItem) { - if (!isRoot(treeItem)) { - return node.displayName(); - } - final var directoryName = Optional.ofNullable(node.path().getFileName()) - .map(Path::toString) - .filter(name -> !name.isBlank()) - .orElse(node.displayName()); - return directoryName + " (" + node.displayName() + ")"; - } - - private boolean isInsideTaggedSourceRoot(final TreeItem item) { - var current = item == null ? null : item.getParent(); - while (current != null && current.getValue() != null) { - if (current.getValue().taggedSourceRoot()) { - return true; - } - current = current.getParent(); - } - return false; - } - - private boolean isRoot(final TreeItem item) { - return item != null && item.getParent() == null; - } - - private boolean isBuildTone(final VfsProjectNode node) { - return node.directory() - && ("build".equals(node.displayName()) || "cartridge".equals(node.displayName())); - } - - private Label emptyLabel() { - final var placeholder = new Label(); - placeholder.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_NAVIGATOR_PLACEHOLDER)); - placeholder.getStyleClass().add("editor-workspace-placeholder"); - placeholder.setWrapText(true); - return placeholder; - } - - private static final class DirectoryTreeItem extends TreeItem { - private DirectoryTreeItem(final VfsProjectNode node) { - super(node); - } - - @Override - public boolean isLeaf() { - return false; - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java deleted file mode 100644 index dde4b197..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorStatusBar.java +++ /dev/null @@ -1,218 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import p.studio.Container; -import p.studio.projects.ProjectReference; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.Optional; - -public final class EditorStatusBar extends HBox { - private final HBox breadcrumb = new HBox(6); - private final Label position = new Label(); - private final Label lineSeparator = new Label(); - private final Label indentation = new Label(); - private final Label language = new Label(); - private final StackPane accessMode = new StackPane(); - - public EditorStatusBar() { - setAlignment(Pos.CENTER_LEFT); - setSpacing(8); - getStyleClass().add("editor-workspace-status-bar"); - - breadcrumb.getStyleClass().add("editor-workspace-status-breadcrumb"); - breadcrumb.setAlignment(Pos.CENTER_LEFT); - - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - - styleChip(position); - position.getStyleClass().add("editor-workspace-status-chip-position"); - styleChip(lineSeparator); - lineSeparator.getStyleClass().add("editor-workspace-status-chip-line-separator"); - styleChip(indentation); - indentation.getStyleClass().add("editor-workspace-status-chip-indentation"); - styleChip(language); - language.getStyleClass().add("editor-workspace-status-chip-language"); - accessMode.getStyleClass().addAll("editor-workspace-status-chip", "editor-workspace-status-chip-access-mode"); - - getChildren().addAll( - breadcrumb, - spacer, - position, - lineSeparator, - indentation, - language, - accessMode - ); - - } - - public void showFile( - final ProjectReference projectReference, - final EditorOpenFileBuffer fileBuffer, - final EditorDocumentPresentation presentation, - final String indentationPolicyLabel) { - showBreadcrumb(projectReference, fileBuffer.path()); - showMetadata(true); - showPosition(fileBuffer.editable(), 1, 1); - showDocumentFormatting(fileBuffer.lineSeparator(), indentationPolicyLabel); - setText(language, fileBuffer.typeId()); - EditorDocumentPresentationStyles.applyToStatusChip(language, presentation); - showAccessMode(fileBuffer.readOnly()); - } - - public void showPlaceholder(final EditorDocumentPresentation presentation) { - breadcrumb.getChildren().clear(); - showMetadata(false); - showPosition(false, 1, 1); - bindDefault(lineSeparator, I18n.CODE_EDITOR_STATUS_LINE_SEPARATOR); - bindDefault(indentation, I18n.CODE_EDITOR_STATUS_INDENTATION); - bindDefault(language, I18n.CODE_EDITOR_STATUS_LANGUAGE); - EditorDocumentPresentationStyles.applyToStatusChip(language, presentation); - accessMode.getChildren().clear(); - accessMode.getStyleClass().removeAll("editor-workspace-status-chip-editable", "editor-workspace-status-chip-read-only"); - } - - public void showCaretPosition(final int line, final int column) { - if (!position.isManaged()) { - return; - } - setText(position, line + ":" + column); - } - - public void showDocumentFormatting(final String separator, final String indentationPolicyLabel) { - setText(lineSeparator, separator); - setText(indentation, indentationPolicyLabel); - } - - private void bindDefault(final Label label, final I18n key) { - bindText(label, key); - } - - private void bindText(final Label label, final I18n key) { - label.textProperty().unbind(); - label.textProperty().bind(Container.i18n().bind(key)); - } - - private void showBreadcrumb(final ProjectReference projectReference, final Path filePath) { - breadcrumb.getChildren().clear(); - breadcrumb.getChildren().add(segment( - rootLabel(projectReference), - EditorWorkspaceIcons.folderDefault(), - true)); - - final var relativePath = projectReference.rootPath() - .toAbsolutePath() - .normalize() - .relativize(filePath.toAbsolutePath().normalize()); - - boolean insideSourceTree = false; - Path accumulated = projectReference.rootPath().toAbsolutePath().normalize(); - for (int index = 0; index < relativePath.getNameCount(); index++) { - final var part = relativePath.getName(index).toString(); - accumulated = accumulated.resolve(part); - final boolean isLast = index == relativePath.getNameCount() - 1; - if ("src".equals(part)) { - insideSourceTree = true; - } - breadcrumb.getChildren().add(separator()); - breadcrumb.getChildren().add(segment( - part, - iconForSegment(part, isLast, insideSourceTree), - false)); - } - } - - private String rootLabel(final ProjectReference projectReference) { - return Optional.ofNullable(projectReference.rootPath().getFileName()) - .map(Path::toString) - .filter(name -> !name.isBlank()) - .orElse(projectReference.name()); - } - - private HBox segment(final String text, final Node icon, final boolean root) { - final var label = new Label(text); - label.getStyleClass().add("editor-workspace-status-breadcrumb-text"); - - final var box = new HBox(6, icon, label); - box.setAlignment(Pos.CENTER_LEFT); - box.getStyleClass().add("editor-workspace-status-breadcrumb-segment"); - if (root) { - box.getStyleClass().add("editor-workspace-status-breadcrumb-segment-root"); - } - return box; - } - - private Label separator() { - final var label = new Label(">"); - label.getStyleClass().add("editor-workspace-status-breadcrumb-separator"); - return label; - } - - private Node iconForSegment(final String name, final boolean isFile, final boolean insideSourceTree) { - if (isFile) { - if ("prometeu.json".equals(name)) { - return EditorWorkspaceIcons.cog(); - } - return EditorWorkspaceIcons.file(); - } - - if ("build".equals(name) || "cartridge".equals(name)) { - return EditorWorkspaceIcons.folderBuild(); - } - if ("src".equals(name) || insideSourceTree) { - return EditorWorkspaceIcons.folderSource(); - } - return EditorWorkspaceIcons.folderDefault(); - } - - private void setText(final Label label, final String text) { - label.textProperty().unbind(); - label.setText(text); - } - - private void showMetadata(final boolean visible) { - setVisibleManaged(lineSeparator, visible); - setVisibleManaged(indentation, visible); - setVisibleManaged(language, visible); - setVisibleManaged(accessMode, visible); - } - - private void showPosition(final boolean visible, final int line, final int column) { - setVisibleManaged(position, visible); - if (!visible) { - bindDefault(position, I18n.CODE_EDITOR_STATUS_POSITION); - return; - } - setText(position, line + ":" + column); - } - - private void showAccessMode(final boolean readOnlyMode) { - accessMode.getChildren().setAll(readOnlyMode - ? EditorWorkspaceIcons.lockClosed() - : EditorWorkspaceIcons.lockOpen()); - accessMode.getStyleClass().removeAll("editor-workspace-status-chip-editable", "editor-workspace-status-chip-read-only"); - accessMode.getStyleClass().add(readOnlyMode - ? "editor-workspace-status-chip-read-only" - : "editor-workspace-status-chip-editable"); - } - - private void setVisibleManaged(final Node node, final boolean visible) { - node.setVisible(visible); - node.setManaged(visible); - } - - private void styleChip(final Label label) { - if (!label.getStyleClass().contains("editor-workspace-status-chip")) { - label.getStyleClass().add("editor-workspace-status-chip"); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java deleted file mode 100644 index 84dc590e..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorTabStrip.java +++ /dev/null @@ -1,252 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.css.PseudoClass; -import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.CustomMenuItem; -import javafx.scene.control.Label; -import javafx.scene.control.MenuButton; -import javafx.scene.control.MenuItem; -import javafx.scene.control.OverrunStyle; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.SVGPath; -import p.studio.Container; -import p.studio.utilities.i18n.I18n; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -public final class EditorTabStrip extends HBox { - private static final PseudoClass PRESSED_PSEUDO_CLASS = PseudoClass.getPseudoClass("pressed"); - private static final double TAB_WIDTH_HINT = 176.0; - private static final double TAB_HEIGHT_HINT = 34.0; - private static final double STRIP_HEIGHT_HINT = 50.0; - private static final double OVERFLOW_WIDTH_HINT = 128.0; - private static final double OVERFLOW_MENU_WIDTH_HINT = 180.0; - - private final MenuButton overflowButton = new MenuButton(); - private final Region spacer = new Region(); - private final List openFiles = new ArrayList<>(); - private Consumer tabSelectionAction = path -> { }; - private Consumer tabCloseAction = path -> { }; - private Path activePath; - - public EditorTabStrip() { - setAlignment(Pos.CENTER_LEFT); - setSpacing(10); - getStyleClass().add("editor-workspace-tab-strip"); - setMinHeight(STRIP_HEIGHT_HINT); - setPrefHeight(STRIP_HEIGHT_HINT); - setMaxHeight(STRIP_HEIGHT_HINT); - HBox.setHgrow(spacer, Priority.ALWAYS); - - overflowButton.textProperty().bind(Container.i18n().bind(I18n.CODE_EDITOR_TABS_OVERFLOW)); - overflowButton.setFocusTraversable(false); - overflowButton.getStyleClass().addAll( - "studio-button", - "studio-button-secondary", - "editor-workspace-tab-overflow"); - applyOverflowMetrics(); - widthProperty().addListener((ignored, previous, current) -> rebuild()); - rebuild(); - } - - public void setTabSelectionAction(final Consumer tabSelectionAction) { - this.tabSelectionAction = Objects.requireNonNull(tabSelectionAction, "tabSelectionAction"); - } - - public void setTabCloseAction(final Consumer tabCloseAction) { - this.tabCloseAction = Objects.requireNonNull(tabCloseAction, "tabCloseAction"); - } - - public void showOpenFiles(final List files, final Path activePath) { - this.openFiles.clear(); - this.openFiles.addAll(Objects.requireNonNull(files, "files")); - this.activePath = activePath == null ? null : activePath.toAbsolutePath().normalize(); - rebuild(); - } - - private void rebuild() { - getChildren().clear(); - if (openFiles.isEmpty()) { - overflowButton.getItems().clear(); - getChildren().add(spacer); - overflowButton.setVisible(false); - overflowButton.setManaged(false); - return; - } - - final int visibleCount = visibleCount(); - final int activeIndex = activeIndex(); - final int startIndex = Math.max(0, Math.min(activeIndex - visibleCount + 1, openFiles.size() - visibleCount)); - final int endIndex = Math.min(openFiles.size(), startIndex + visibleCount); - - for (int index = startIndex; index < endIndex; index++) { - final var fileBuffer = openFiles.get(index); - final var tabButton = new Button(fileBuffer.tabLabel()); - final var tabLabel = new Label(fileBuffer.tabLabel()); - final var dirtyIndicator = new StackPane(); - final var closeChip = new StackPane(); - final var closeIcon = new SVGPath(); - final var tabContent = new HBox(); - tabButton.setFocusTraversable(false); - tabButton.getStyleClass().addAll( - "studio-button", - "studio-button-secondary", - "editor-workspace-tab-button"); - tabButton.getStyleClass().add( - fileBuffer.readOnly() - ? "editor-workspace-tab-button-read-only" - : "editor-workspace-tab-button-editable"); - tabLabel.getStyleClass().add("editor-workspace-tab-label"); - tabLabel.setTextOverrun(OverrunStyle.ELLIPSIS); - dirtyIndicator.getStyleClass().add("editor-workspace-tab-dirty-indicator"); - closeChip.getStyleClass().add("editor-workspace-tab-close-chip"); - closeIcon.setContent("M 3 3 L 9 9 M 9 3 L 3 9"); - closeIcon.getStyleClass().add("editor-workspace-tab-close-icon"); - closeIcon.setMouseTransparent(true); - closeChip.getChildren().add(closeIcon); - tabContent.getStyleClass().add("editor-workspace-tab-content"); - tabContent.setAlignment(Pos.CENTER_LEFT); - HBox.setHgrow(tabLabel, Priority.ALWAYS); - dirtyIndicator.setManaged(fileBuffer.dirty()); - dirtyIndicator.setVisible(fileBuffer.dirty()); - tabContent.getChildren().addAll(tabLabel, dirtyIndicator, closeChip); - tabButton.setText(null); - tabButton.setGraphic(tabContent); - applyTabMetrics(tabButton, tabLabel, dirtyIndicator, closeChip); - if (fileBuffer.path().equals(activePath)) { - tabButton.getStyleClass().add("editor-workspace-tab-button-active"); - } - tabButton.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path())); - configureCloseChip(closeChip, fileBuffer.path()); - getChildren().add(tabButton); - } - - getChildren().add(spacer); - final boolean hasOverflow = openFiles.size() > visibleCount; - overflowButton.getItems().setAll(buildOverflowItems(startIndex, endIndex)); - overflowButton.setVisible(hasOverflow); - overflowButton.setManaged(hasOverflow); - overflowButton.setDisable(!hasOverflow); - getChildren().add(overflowButton); - } - - private List buildOverflowItems(final int startIndex, final int endIndex) { - final var items = new ArrayList(); - for (int index = 0; index < openFiles.size(); index++) { - if (index >= startIndex && index < endIndex) { - continue; - } - final var fileBuffer = openFiles.get(index); - final var label = new Label(fileBuffer.tabLabel()); - label.getStyleClass().add("editor-workspace-tab-overflow-item"); - label.setMinWidth(OVERFLOW_MENU_WIDTH_HINT); - label.setPrefWidth(OVERFLOW_MENU_WIDTH_HINT); - label.setMaxWidth(OVERFLOW_MENU_WIDTH_HINT); - label.setMinHeight(TAB_HEIGHT_HINT); - label.setPrefHeight(TAB_HEIGHT_HINT); - label.setMaxHeight(TAB_HEIGHT_HINT); - label.setAlignment(Pos.CENTER_LEFT); - label.setTextOverrun(OverrunStyle.ELLIPSIS); - - final var item = new CustomMenuItem(label, true); - item.getStyleClass().add("editor-workspace-tab-overflow-menu-item"); - item.setOnAction(event -> tabSelectionAction.accept(fileBuffer.path())); - items.add(item); - } - return items; - } - - private int activeIndex() { - if (activePath == null) { - return Math.max(0, openFiles.size() - 1); - } - for (int index = 0; index < openFiles.size(); index++) { - if (openFiles.get(index).path().equals(activePath)) { - return index; - } - } - return Math.max(0, openFiles.size() - 1); - } - - private int visibleCount() { - final double availableWidth = getWidth() > 0 ? getWidth() : 760; - final double usableWidth = Math.max(availableWidth - OVERFLOW_WIDTH_HINT, TAB_WIDTH_HINT); - return Math.max(1, (int) Math.floor(usableWidth / TAB_WIDTH_HINT)); - } - - private void applyTabMetrics( - final Button button, - final Label tabLabel, - final StackPane dirtyIndicator, - final StackPane closeChip) { - button.setMinWidth(TAB_WIDTH_HINT); - button.setPrefWidth(TAB_WIDTH_HINT); - button.setMaxWidth(TAB_WIDTH_HINT); - button.setMinHeight(TAB_HEIGHT_HINT); - button.setPrefHeight(TAB_HEIGHT_HINT); - button.setMaxHeight(TAB_HEIGHT_HINT); - button.setMnemonicParsing(false); - tabLabel.setMinWidth(0); - tabLabel.setPrefWidth(TAB_WIDTH_HINT - 30); - tabLabel.setMaxWidth(Double.MAX_VALUE); - dirtyIndicator.setMinWidth(8); - dirtyIndicator.setPrefWidth(8); - dirtyIndicator.setMaxWidth(8); - dirtyIndicator.setMinHeight(8); - dirtyIndicator.setPrefHeight(8); - dirtyIndicator.setMaxHeight(8); - closeChip.setMinWidth(14); - closeChip.setPrefWidth(14); - closeChip.setMaxWidth(14); - closeChip.setMinHeight(14); - closeChip.setPrefHeight(14); - closeChip.setMaxHeight(14); - } - - private void applyOverflowMetrics() { - overflowButton.setMinWidth(OVERFLOW_WIDTH_HINT); - overflowButton.setPrefWidth(OVERFLOW_WIDTH_HINT); - overflowButton.setMaxWidth(OVERFLOW_WIDTH_HINT); - overflowButton.setMinHeight(TAB_HEIGHT_HINT); - overflowButton.setPrefHeight(TAB_HEIGHT_HINT); - overflowButton.setMaxHeight(TAB_HEIGHT_HINT); - overflowButton.setMnemonicParsing(false); - } - - private void configureCloseChip(final StackPane closeChip, final Path filePath) { - closeChip.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { - event.consume(); - if (event.getButton() == MouseButton.PRIMARY && closeChip.contains(event.getX(), event.getY())) { - closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, true); - } - }); - closeChip.addEventFilter(MouseEvent.MOUSE_DRAGGED, event -> { - event.consume(); - closeChip.pseudoClassStateChanged( - PRESSED_PSEUDO_CLASS, - event.isPrimaryButtonDown() && closeChip.contains(event.getX(), event.getY())); - }); - closeChip.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { - event.consume(); - final boolean shouldClose = event.getButton() == MouseButton.PRIMARY - && closeChip.getPseudoClassStates().contains(PRESSED_PSEUDO_CLASS) - && closeChip.contains(event.getX(), event.getY()); - closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, false); - if (shouldClose) { - tabCloseAction.accept(filePath); - } - }); - closeChip.addEventFilter(MouseEvent.MOUSE_EXITED, event -> - closeChip.pseudoClassStateChanged(PRESSED_PSEUDO_CLASS, false)); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java deleted file mode 100644 index 615c205c..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWarningStrip.java +++ /dev/null @@ -1,103 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.geometry.Pos; -import javafx.scene.control.Button; -import javafx.scene.control.Label; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.Region; - -import java.util.List; -import java.util.Objects; - -public final class EditorWarningStrip extends HBox { - private final Label message = new Label(); - private final Label counter = new Label(); - private final Button previousButton = new Button("‹"); - private final Button nextButton = new Button("›"); - private final Button closeButton = new Button("×"); - - private List warnings = List.of(); - private int activeIndex; - private boolean dismissed; - - public EditorWarningStrip() { - setAlignment(Pos.CENTER_LEFT); - getStyleClass().add("editor-workspace-warning"); - - message.setWrapText(true); - message.setMaxWidth(Double.MAX_VALUE); - HBox.setHgrow(message, Priority.ALWAYS); - - counter.getStyleClass().add("editor-workspace-warning-counter"); - - previousButton.getStyleClass().add("editor-workspace-warning-button"); - nextButton.getStyleClass().add("editor-workspace-warning-button"); - closeButton.getStyleClass().add("editor-workspace-warning-button"); - - previousButton.setFocusTraversable(false); - nextButton.setFocusTraversable(false); - closeButton.setFocusTraversable(false); - - previousButton.setOnAction(event -> showIndex(activeIndex - 1)); - nextButton.setOnAction(event -> showIndex(activeIndex + 1)); - closeButton.setOnAction(event -> dismiss()); - - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - getChildren().addAll(message, spacer, counter, previousButton, nextButton, closeButton); - setVisible(false); - setManaged(false); - } - - public void showWarnings(final List warningItems) { - warnings = List.copyOf(Objects.requireNonNull(warningItems, "warningItems")); - activeIndex = 0; - dismissed = false; - refresh(); - } - - public void clearWarnings() { - warnings = List.of(); - activeIndex = 0; - dismissed = false; - refresh(); - } - - private void dismiss() { - dismissed = true; - refresh(); - } - - private void showIndex(final int candidateIndex) { - if (warnings.isEmpty()) { - return; - } - activeIndex = Math.floorMod(candidateIndex, warnings.size()); - refresh(); - } - - private void refresh() { - final boolean visible = !dismissed && !warnings.isEmpty(); - setVisible(visible); - setManaged(visible); - if (!visible) { - message.setText(""); - counter.setText(""); - return; - } - - final WarningItem warning = warnings.get(activeIndex); - message.setText(warning.message()); - counter.setText(warnings.size() == 1 ? "1/1" : (activeIndex + 1) + "/" + warnings.size()); - final boolean canNavigate = warnings.size() > 1; - previousButton.setDisable(!canNavigate); - nextButton.setDisable(!canNavigate); - } - - public record WarningItem(String message) { - public WarningItem { - message = Objects.requireNonNull(message, "message"); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java deleted file mode 100644 index cf9d3cc7..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspace.java +++ /dev/null @@ -1,784 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.application.Platform; -import javafx.scene.Node; -import javafx.scene.control.Alert; -import javafx.scene.control.Button; -import javafx.scene.control.ButtonBar; -import javafx.scene.control.ButtonType; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyCodeCombination; -import javafx.scene.input.KeyCombination; -import javafx.scene.input.KeyEvent; -import javafx.scene.control.SplitPane; -import javafx.scene.layout.*; -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.CodeArea; -import org.fxmisc.richtext.LineNumberFactory; -import org.reactfx.Subscription; -import p.studio.lsp.LspService; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.projectstate.ProjectLocalStudioSetup; -import p.studio.projectstate.ProjectLocalStudioState; -import p.studio.projects.ProjectReference; -import p.studio.utilities.i18n.I18n; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.messages.VfsProjectNode; -import p.studio.workspaces.Workspace; -import p.studio.workspaces.WorkspaceId; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.IntFunction; - -public final class EditorWorkspace extends Workspace { - private static final int CARET_SCROLL_CONTEXT_LINES = 2; - private static final KeyCombination SAVE_SHORTCUT = new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN); - private static final KeyCombination SAVE_ALL_SHORTCUT = new KeyCodeCombination( - KeyCode.S, - KeyCombination.SHORTCUT_DOWN, - KeyCombination.SHIFT_DOWN); - private final BorderPane root = new BorderPane(); - private final CodeArea codeArea = new CodeArea(); - private final VirtualizedScrollPane codeScroller = new VirtualizedScrollPane<>(codeArea); - private final Button saveButton = new Button(); - private final Button saveAllButton = new Button(); - private final EditorWarningStrip warningStrip = new EditorWarningStrip(); - private final EditorProjectNavigatorPanel navigatorPanel = new EditorProjectNavigatorPanel(); - private final EditorOutlinePanel outlinePanel = new EditorOutlinePanel(); - private final EditorHelperPanel helperPanel = new EditorHelperPanel(); - private final EditorStatusBar statusBar = new EditorStatusBar(); - private final EditorTabStrip tabStrip = new EditorTabStrip(); - private final EditorDocumentPresentationRegistry presentationRegistry = new EditorDocumentPresentationRegistry(); - private final LspService prometeuLspService; - private final VfsProjectDocument vfsProjectDocument; - private final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup; - private final EditorOpenFileSession openFileSession = new EditorOpenFileSession(); - private final Subscription inlineHintChangeSubscription; - private final List activePresentationStylesheets = new ArrayList<>(); - private final IntFunction lineNumberFactory = LineNumberFactory.get(codeArea); - private EditorDocumentScopeGuideModel scopeGuideModel = EditorDocumentScopeGuideModel.empty(); - private EditorDocumentScopeGuideModel.ActiveGuides activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty(); - private EditorInlineHintProjection inlineHintProjection = EditorInlineHintProjection.create( - "", - presentationRegistry.resolve("text").highlight(""), - List.of()); - private boolean syncingEditor; - private boolean applyingPendingLayoutState; - private int activeGuideParagraph = -1; - private boolean pendingCaretContextRestore; - private Runnable stateChangedAction = () -> { }; - private ProjectLocalStudioState.EditorLayoutState pendingEditorLayoutState = ProjectLocalStudioState.EditorLayoutState.defaults(); - private SplitPane contentSplit; - private SplitPane leftColumnSplit; - private SplitPane rightColumnSplit; - - public EditorWorkspace( - final ProjectReference projectReference, - final VfsProjectDocument vfsProjectDocument, - final LspService prometeuLspService, - final ProjectLocalStudioSetup.EditorIndentationSetup indentationSetup) { - super(projectReference); - this.vfsProjectDocument = Objects.requireNonNull(vfsProjectDocument, "vfsProjectDocument"); - this.prometeuLspService = Objects.requireNonNull(prometeuLspService, "prometeuLspService"); - this.indentationSetup = Objects.requireNonNull(indentationSetup, "indentationSetup"); - root.getStyleClass().add("editor-workspace"); - refreshParagraphGraphics(); - codeArea.setEditable(false); - codeArea.setWrapText(false); - codeArea.textProperty().addListener((ignored, previous, current) -> syncActiveDocumentToVfs(current)); - codeArea.caretPositionProperty().addListener((ignored, previous, current) -> { - final int caretOffset = current == null ? 0 : current.intValue(); - updateActiveGuides(caretOffset, codeArea.getCurrentParagraph()); - refreshStatusBarCaret(); - }); - codeArea.estimatedScrollYProperty().addListener((ignored, previous, current) -> - restoreCaretScrollContextIfNeeded(previous, current)); - codeArea.getStyleClass().add("editor-workspace-code-area"); - codeArea.addEventFilter(KeyEvent.KEY_PRESSED, this::guardInlineHintMutation); - codeArea.addEventFilter(KeyEvent.KEY_TYPED, this::guardInlineHintMutation); - root.addEventFilter(KeyEvent.KEY_PRESSED, this::handleWorkspaceShortcuts); - inlineHintChangeSubscription = codeArea.plainTextChanges().subscribe(change -> { - if (syncingEditor) { - return; - } - inlineHintProjection.applyDisplayChange( - change.getPosition(), - change.getRemoved().length(), - change.getInserted().length()); - }); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentationRegistry.resolve("text")); - configureCommandBar(); - configureWarning(); - showEditorPlaceholder(); - navigatorPanel.setRefreshAction(this::refreshNavigator); - navigatorPanel.setRevealActiveFileAction(this::revealActiveFileInNavigator); - navigatorPanel.setFileSelectionAction(this::openNode); - tabStrip.setTabSelectionAction(path -> { - openFileSession.activate(path); - notifyStateChanged(); - renderSession(); - }); - tabStrip.setTabCloseAction(this::requestCloseFile); - - root.setCenter(buildLayout()); - statusBar.showPlaceholder(presentationRegistry.resolve("text")); - } - - @Override public WorkspaceId workspaceId() { return WorkspaceId.EDITOR; } - @Override public I18n title() { return I18n.WORKSPACE_CODE; } - @Override public Node rootNode() { return root; } - - @Override - public void load() { - applyEditorLayoutState(pendingEditorLayoutState); - schedulePendingLayoutReapply(); - refreshNavigator(); - } - - @Override - public void unLoad() { - inlineHintChangeSubscription.unsubscribe(); - } - - public CodeArea codeArea() { return codeArea; } - - public void setStateChangedAction(final Runnable stateChangedAction) { - this.stateChangedAction = Objects.requireNonNull(stateChangedAction, "stateChangedAction"); - } - - public void restoreProjectLocalState(final ProjectLocalStudioState projectLocalStudioState) { - final ProjectLocalStudioState state = Objects.requireNonNull(projectLocalStudioState, "projectLocalStudioState"); - pendingEditorLayoutState = state.shellLayout().editorLayout(); - reapplyPendingLayoutState(); - schedulePendingLayoutReapply(); - restoreEditorState(state.editorRestoration()); - } - - public void reapplyPendingLayoutState() { - applyEditorLayoutState(pendingEditorLayoutState); - } - - public ProjectLocalStudioState.EditorLayoutState captureLayoutState() { - return new ProjectLocalStudioState.EditorLayoutState( - dividerPositions(contentSplit), - dividerPositions(leftColumnSplit), - dividerPositions(rightColumnSplit), - dockPanelState(outlinePanel), - dockPanelState(helperPanel), - navigatorPanel.expandedPathStrings()); - } - - public ProjectLocalStudioState.EditorRestorationState captureRestorationState() { - return openFileSession.restorationState(); - } - - private void refreshNavigator() { - navigatorPanel.setSnapshot(vfsProjectDocument.refresh()); - } - - private void openNode(final VfsProjectNode node) { - final VfsDocumentOpenResult result = vfsProjectDocument.openDocument(node.path()); - if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFile(textDocument); - return; - } - showUnsupportedFileModal(node.path()); - } - - private void openFile(final VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFileSession.open(bufferFrom(textDocument)); - notifyStateChanged(); - renderSession(); - } - - private void renderSession() { - final var activeFile = openFileSession.activeFile(); - navigatorPanel.setRevealActiveFileAvailable(activeFile.isPresent()); - tabStrip.showOpenFiles( - openFileSession.openFiles(), - activeFile.map(EditorOpenFileBuffer::path).orElse(null)); - if (activeFile.isEmpty()) { - showEditorPlaceholder(); - final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); - applyPresentationStylesheets(presentation); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); - statusBar.showPlaceholder(presentation); - outlinePanel.showPlaceholder(); - return; - } - - final var fileBuffer = activeFile.orElseThrow(); - final LspAnalyzeDocumentResult analysis = fileBuffer.frontendDocument() - ? prometeuLspService.analyzeDocument(new LspAnalyzeDocumentRequest(fileBuffer.path())) - : null; - final EditorDocumentPresentation presentation = presentationRegistry.resolve( - fileBuffer.typeId(), - analysis == null ? null : analysis.semanticPresentation()); - final EditorDocumentHighlightingResult highlighting = EditorDocumentHighlightingRouter.route( - fileBuffer, - presentation, - analysis); - scopeGuideModel = guidesFor(fileBuffer, analysis); - applyPresentationStylesheets(presentation); - syncingEditor = true; - try { - inlineHintProjection = EditorInlineHintProjection.create( - fileBuffer.content(), - highlighting.styleSpans(), - highlighting.inlineHints()); - codeArea.replaceText(inlineHintProjection.displayText()); - codeArea.setStyleSpans(0, inlineHintProjection.displayStyles()); - codeArea.moveTo(0); - } finally { - syncingEditor = false; - } - activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition()); - activeGuideParagraph = codeArea.getCurrentParagraph(); - refreshParagraphGraphics(); - Platform.runLater(() -> { - if (fileBuffer.editable()) { - codeArea.requestFocus(); - } - }); - codeArea.setEditable(fileBuffer.editable()); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); - refreshCommandSurfaces(fileBuffer); - statusBar.showFile(projectReference, fileBuffer, presentation, indentationSetup.statusLabel()); - refreshStatusBarCaret(); - refreshSemanticOutline(fileBuffer, analysis); - } - - private void revealActiveFileInNavigator() { - openFileSession.activeFile() - .map(EditorOpenFileBuffer::path) - .ifPresent(navigatorPanel::revealPath); - } - - private void showUnsupportedFileModal(final Path path) { - final var alert = new Alert(Alert.AlertType.INFORMATION); - if (root.getScene() != null) { - alert.initOwner(root.getScene().getWindow()); - } - alert.setHeaderText(null); - alert.setTitle(p.studio.Container.i18n().text(I18n.CODE_EDITOR_UNSUPPORTED_FILE_TITLE)); - alert.setContentText(p.studio.Container.i18n().format(I18n.CODE_EDITOR_UNSUPPORTED_FILE_MESSAGE, path.getFileName())); - alert.showAndWait(); - } - - private void showEditorPlaceholder() { - final EditorDocumentPresentation presentation = presentationRegistry.resolve("text"); - scopeGuideModel = EditorDocumentScopeGuideModel.empty(); - activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty(); - activeGuideParagraph = -1; - refreshParagraphGraphics(); - applyPresentationStylesheets(presentation); - syncingEditor = true; - try { - inlineHintProjection = EditorInlineHintProjection.create("", presentation.highlight(""), List.of()); - codeArea.replaceText(""); - codeArea.setStyleSpans(0, inlineHintProjection.displayStyles()); - codeArea.moveTo(0); - } finally { - syncingEditor = false; - } - codeArea.setEditable(false); - EditorDocumentPresentationStyles.applyToCodeArea(codeArea, presentation); - saveButton.setDisable(true); - saveAllButton.setDisable(true); - warningStrip.clearWarnings(); - outlinePanel.showPlaceholder(); - helperPanel.showPlaceholder(); - } - - private void applyPresentationStylesheets(final EditorDocumentPresentation presentation) { - root.getStylesheets().removeAll(activePresentationStylesheets); - activePresentationStylesheets.clear(); - activePresentationStylesheets.addAll(presentation.stylesheetUrls()); - root.getStylesheets().addAll(activePresentationStylesheets); - } - - private void refreshParagraphGraphics() { - preserveViewport(() -> codeArea.setParagraphGraphicFactory(paragraphIndex -> EditorDocumentScopeGuideGraphicFactory.create( - lineNumberFactory.apply(paragraphIndex), - paragraphIndex, - scopeGuideModel, - activeGuides))); - } - - private VBox buildLayout() { - contentSplit = new SplitPane(buildLeftColumn(), buildRightColumn()); - contentSplit.setDividerPositions(0.30); - contentSplit.getStyleClass().add("editor-workspace-split"); - observeStatefulSplitPane(contentSplit); - - final var layout = new VBox(12, buildCommandBar(), contentSplit, statusBar); - layout.getStyleClass().add("editor-workspace-layout"); - VBox.setVgrow(contentSplit, Priority.ALWAYS); - return layout; - } - - private SplitPane buildLeftColumn() { - leftColumnSplit = outlinePanel.createBottomDockLayout(navigatorPanel, "editor-workspace-left-split"); - final var leftColumn = leftColumnSplit; - leftColumn.getStyleClass().add("editor-workspace-left-column"); - observeStatefulSplitPane(leftColumnSplit); - observeDockPane(outlinePanel); - return leftColumn; - } - - private VBox buildCenterColumn() { - final var editorViewport = new StackPane(codeScroller); - editorViewport.getStyleClass().add("editor-workspace-editor-viewport"); - final var editorSurface = new VBox(0, warningStrip, editorViewport); - editorSurface.getStyleClass().add("editor-workspace-editor-surface"); - codeScroller.getStyleClass().add("editor-workspace-code-scroller"); - VBox.setVgrow(editorViewport, Priority.ALWAYS); - final var centerColumn = new VBox(12, tabStrip, editorSurface); - centerColumn.getStyleClass().add("editor-workspace-center-column"); - VBox.setVgrow(editorSurface, Priority.ALWAYS); - return centerColumn; - } - - private SplitPane buildRightColumn() { - rightColumnSplit = helperPanel.createBottomDockLayout(buildCenterColumn(), "editor-workspace-right-split"); - observeStatefulSplitPane(rightColumnSplit); - observeDockPane(helperPanel); - return rightColumnSplit; - } - - private void configureCommandBar() { - saveButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE)); - saveAllButton.textProperty().bind(p.studio.Container.i18n().bind(I18n.CODE_EDITOR_COMMAND_SAVE_ALL)); - saveButton.getStyleClass().addAll("studio-button", "studio-button-primary", "editor-workspace-command-button"); - saveAllButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "editor-workspace-command-button"); - saveButton.setFocusTraversable(false); - saveAllButton.setFocusTraversable(false); - saveButton.setDisable(true); - saveAllButton.setDisable(true); - saveButton.setOnAction(event -> saveActiveFile()); - saveAllButton.setOnAction(event -> saveAllFiles()); - } - - private void configureWarning() { - warningStrip.clearWarnings(); - } - - private HBox buildCommandBar() { - final var spacer = new Region(); - HBox.setHgrow(spacer, Priority.ALWAYS); - final var commandBar = new HBox(8, saveButton, saveAllButton, spacer); - commandBar.getStyleClass().add("editor-workspace-command-bar"); - return commandBar; - } - - private void syncActiveDocumentToVfs(final String content) { - if (syncingEditor) { - return; - } - openFileSession.activeFile() - .filter(EditorOpenFileBuffer::editable) - .ifPresent(activeFile -> { - final String sourceContent = inlineHintProjection.stripDecorations(content); - final VfsDocumentOpenResult.VfsTextDocument updatedDocument = vfsProjectDocument.updateDocument(activeFile.path(), sourceContent); - openFileSession.open(bufferFrom(updatedDocument)); - pendingCaretContextRestore = true; - refreshEditableHighlighting(updatedDocument); - refreshEditableSemanticState(updatedDocument); - statusBar.showDocumentFormatting(updatedDocument.lineSeparator(), indentationSetup.statusLabel()); - tabStrip.showOpenFiles( - openFileSession.openFiles(), - openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null)); - refreshCommandSurfaces(openFileSession.activeFile().orElseThrow()); - }); - } - - private void refreshEditableHighlighting(final VfsDocumentOpenResult.VfsTextDocument updatedDocument) { - final EditorDocumentPresentation presentation = presentationRegistry.resolve(updatedDocument.typeId()); - inlineHintProjection = EditorInlineHintProjection.create( - updatedDocument.content(), - presentation.highlight(updatedDocument.content()), - List.of()); - preserveViewport(() -> codeArea.setStyleSpans(0, inlineHintProjection.displayStyles())); - } - - private void refreshEditableSemanticState(final VfsDocumentOpenResult.VfsTextDocument updatedDocument) { - final EditorOpenFileBuffer fileBuffer = openFileSession.activeFile().orElse(null); - if (fileBuffer == null || !fileBuffer.path().equals(updatedDocument.path())) { - return; - } - if (!updatedDocument.accessContext().frontendDocument()) { - scopeGuideModel = EditorDocumentScopeGuideModel.empty(); - activeGuides = EditorDocumentScopeGuideModel.ActiveGuides.empty(); - activeGuideParagraph = -1; - refreshParagraphGraphics(); - outlinePanel.showPlaceholder(); - helperPanel.showPlaceholder(); - return; - } - final LspAnalyzeDocumentResult analysis = prometeuLspService.analyzeDocument( - new LspAnalyzeDocumentRequest(updatedDocument.path())); - scopeGuideModel = guidesFor(fileBuffer, analysis); - activeGuides = scopeGuideModel.resolveActiveGuides(codeArea.getCaretPosition()); - activeGuideParagraph = codeArea.getCurrentParagraph(); - refreshParagraphGraphics(); - refreshSemanticOutline(fileBuffer, analysis); - } - - private void preserveViewport(final Runnable action) { - final int caretPosition = codeArea.getCaretPosition(); - final double scrollX = codeArea.estimatedScrollXProperty().getValue(); - final double scrollY = codeArea.estimatedScrollYProperty().getValue(); - action.run(); - codeArea.moveTo(Math.max(0, Math.min(caretPosition, codeArea.getLength()))); - Platform.runLater(() -> { - codeArea.scrollXToPixel(scrollX); - codeArea.scrollYToPixel(scrollY); - }); - } - - private void restoreCaretScrollContextIfNeeded(final Number previous, final Number current) { - if (!pendingCaretContextRestore) { - return; - } - final double previousValue = previous == null ? Double.NaN : previous.doubleValue(); - final double currentValue = current == null ? Double.NaN : current.doubleValue(); - if (!Double.isFinite(previousValue) || !Double.isFinite(currentValue) || Double.compare(previousValue, currentValue) == 0) { - return; - } - pendingCaretContextRestore = false; - Platform.runLater(() -> codeArea.showParagraphAtTop( - Math.max(0, codeArea.getCurrentParagraph() - CARET_SCROLL_CONTEXT_LINES))); - } - - private void saveActiveFile() { - openFileSession.activeFile() - .filter(EditorOpenFileBuffer::saveEnabled) - .ifPresent(activeFile -> { - vfsProjectDocument.saveDocument(activeFile.path()); - reloadOpenFilesFromVfs(activeFile.path()); - renderSession(); - }); - } - - private void saveAllFiles() { - if (!openFileSession.hasDirtyEditableFiles()) { - return; - } - vfsProjectDocument.saveAllDocuments(); - reloadOpenFilesFromVfs(openFileSession.activeFile().map(EditorOpenFileBuffer::path).orElse(null)); - renderSession(); - } - - private void requestCloseFile(final Path path) { - final var fileBuffer = openFileSession.file(path).orElse(null); - if (fileBuffer == null) { - return; - } - if (fileBuffer.dirty() && !confirmDirtyFileClose(fileBuffer)) { - return; - } - openFileSession.close(path); - notifyStateChanged(); - renderSession(); - } - - private boolean confirmDirtyFileClose(final EditorOpenFileBuffer fileBuffer) { - final var alert = new Alert(Alert.AlertType.CONFIRMATION); - if (root.getScene() != null) { - alert.initOwner(root.getScene().getWindow()); - } - final var saveButtonType = new ButtonType( - p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_SAVE), - ButtonBar.ButtonData.YES); - final var discardButtonType = new ButtonType( - p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_DISCARD), - ButtonBar.ButtonData.NO); - final var cancelButtonType = new ButtonType( - p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_CANCEL), - ButtonBar.ButtonData.CANCEL_CLOSE); - alert.setTitle(p.studio.Container.i18n().text(I18n.CODE_EDITOR_CLOSE_DIRTY_TITLE)); - alert.setHeaderText(null); - alert.setContentText(p.studio.Container.i18n().format( - I18n.CODE_EDITOR_CLOSE_DIRTY_MESSAGE, - fileBuffer.tabLabel())); - alert.getButtonTypes().setAll(saveButtonType, discardButtonType, cancelButtonType); - final var result = alert.showAndWait().orElse(cancelButtonType); - if (result == saveButtonType) { - vfsProjectDocument.saveDocument(fileBuffer.path()); - return true; - } - if (result == discardButtonType) { - vfsProjectDocument.discardDocument(fileBuffer.path()); - return true; - } - return false; - } - - private void handleWorkspaceShortcuts(final KeyEvent event) { - if (SAVE_ALL_SHORTCUT.match(event)) { - if (!saveAllButton.isDisabled()) { - saveAllFiles(); - } - event.consume(); - return; - } - if (SAVE_SHORTCUT.match(event)) { - if (!saveButton.isDisabled()) { - saveActiveFile(); - } - event.consume(); - } - } - - private void reloadOpenFilesFromVfs(final Path activePath) { - final var openPaths = openFileSession.openFiles().stream() - .map(EditorOpenFileBuffer::path) - .toList(); - for (final var path : openPaths) { - final var result = vfsProjectDocument.openDocument(path); - if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFileSession.open(bufferFrom(textDocument)); - } - } - if (activePath != null) { - openFileSession.activate(activePath); - } - } - - private void refreshCommandSurfaces(final EditorOpenFileBuffer fileBuffer) { - saveButton.setDisable(!fileBuffer.saveEnabled()); - saveAllButton.setDisable(!openFileSession.hasDirtyEditableFiles()); - warningStrip.clearWarnings(); - } - - private EditorOpenFileBuffer bufferFrom(final VfsDocumentOpenResult.VfsTextDocument textDocument) { - return new EditorOpenFileBuffer( - textDocument.path(), - textDocument.documentName(), - textDocument.typeId(), - textDocument.content(), - textDocument.lineSeparator(), - textDocument.accessContext().frontendDocument(), - textDocument.accessContext().accessMode(), - textDocument.dirty()); - } - - private void refreshSemanticOutline( - final EditorOpenFileBuffer fileBuffer, - final LspAnalyzeDocumentResult analysis) { - if (!fileBuffer.frontendDocument() || analysis == null) { - outlinePanel.showPlaceholder(); - helperPanel.showPlaceholder(); - return; - } - outlinePanel.showSemanticReadResult(fileBuffer.path(), analysis.documentSymbols()); - helperPanel.showSemanticReadResult(fileBuffer.path(), analysis.diagnostics()); - } - - private EditorDocumentScopeGuideModel guidesFor( - final EditorOpenFileBuffer fileBuffer, - final LspAnalyzeDocumentResult analysis) { - if (!fileBuffer.frontendDocument() || analysis == null) { - return EditorDocumentScopeGuideModel.empty(); - } - if (!analysis.structuralAnchors().isEmpty()) { - return EditorDocumentScopeGuideModel.fromStructuralAnchors( - fileBuffer.content(), - analysis.structuralAnchors()); - } - return EditorDocumentScopeGuideModel.from(fileBuffer.content(), analysis.documentSymbols()); - } - - private void updateActiveGuides(final int caretOffset, final int currentParagraph) { - final EditorDocumentScopeGuideModel.ActiveGuides next = scopeGuideModel.resolveActiveGuides(caretOffset); - if (Objects.equals(activeGuides, next) && activeGuideParagraph == currentParagraph) { - return; - } - final boolean guidesChanged = !Objects.equals(activeGuides, next); - activeGuides = next; - final boolean paragraphChanged = activeGuideParagraph != currentParagraph; - activeGuideParagraph = currentParagraph; - if (!paragraphChanged && !guidesChanged) { - return; - } - refreshParagraphGraphics(); - } - - private void refreshStatusBarCaret() { - openFileSession.activeFile() - .filter(EditorOpenFileBuffer::editable) - .ifPresent(ignored -> statusBar.showCaretPosition( - codeArea.getCurrentParagraph() + 1, - codeArea.getCaretColumn() + 1)); - } - - private void guardInlineHintMutation(final KeyEvent event) { - if (syncingEditor || !codeArea.isEditable()) { - return; - } - final int selectionStart = codeArea.getSelection().getStart(); - final int selectionEnd = codeArea.getSelection().getEnd(); - if (selectionStart != selectionEnd && inlineHintProjection.touchesRange(selectionStart, selectionEnd)) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(selectionStart, false)); - return; - } - final int caret = codeArea.getCaretPosition(); - if (event.getEventType() == KeyEvent.KEY_TYPED) { - if (inlineHintProjection.containsOffset(caret)) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(caret, true)); - } - return; - } - if (event.getCode() == KeyCode.TAB) { - event.consume(); - codeArea.replaceSelection(indentationSetup.tabInsertion()); - return; - } - if (event.getCode() == KeyCode.BACK_SPACE && inlineHintProjection.containsOffset(Math.max(0, caret - 1))) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(Math.max(0, caret - 1), false)); - return; - } - if (event.getCode() == KeyCode.DELETE && inlineHintProjection.containsOffset(caret)) { - event.consume(); - codeArea.moveTo(inlineHintProjection.clampCaret(caret, true)); - } - } - - private void restoreEditorState(final ProjectLocalStudioState.EditorRestorationState editorRestorationState) { - openFileSession.clear(); - for (final String pathText : editorRestorationState.openTabPaths()) { - tryRestoreOpenFile(pathText); - } - if (editorRestorationState.activeTabPath() != null) { - try { - openFileSession.activate(Path.of(editorRestorationState.activeTabPath())); - } catch (RuntimeException ignored) { - // Fall back to the currently active restored tab. - } - } - renderSession(); - } - - private void tryRestoreOpenFile(final String pathText) { - try { - final Path path = Path.of(pathText).toAbsolutePath().normalize(); - final VfsDocumentOpenResult result = vfsProjectDocument.openDocument(path); - if (result instanceof VfsDocumentOpenResult.VfsTextDocument textDocument) { - openFileSession.open(bufferFrom(textDocument)); - } - } catch (RuntimeException ignored) { - // Invalid or unavailable persisted tabs are skipped and the editor falls back to the remaining state. - } - } - - private List dividerPositions(final SplitPane splitPane) { - if (splitPane == null || splitPane.getDividers().isEmpty()) { - return List.of(); - } - return splitPane.getDividers().stream() - .map(SplitPane.Divider::getPosition) - .toList(); - } - - private void applyDividerPositions(final SplitPane splitPane, final List positions) { - if (splitPane == null || positions == null || positions.isEmpty()) { - return; - } - final double[] values = positions.stream().mapToDouble(Double::doubleValue).toArray(); - splitPane.setDividerPositions(values); - } - - private ProjectLocalStudioState.DockPanelState dockPanelState(final p.studio.controls.WorkspaceDockPane dockPane) { - final p.studio.controls.WorkspaceDockPane.DockState state = dockPane.captureDockState(); - return new ProjectLocalStudioState.DockPanelState(state.expanded(), state.dividerPosition()); - } - - private void restoreDockPanelState( - final p.studio.controls.WorkspaceDockPane dockPane, - final ProjectLocalStudioState.DockPanelState dockPanelState) { - if (dockPanelState == null) { - return; - } - final double dividerPosition = dockPanelState.dividerPosition() == null - ? dockPane.captureDockState().dividerPosition() - : dockPanelState.dividerPosition(); - dockPane.restoreDockState(new p.studio.controls.WorkspaceDockPane.DockState( - dockPanelState.expanded(), - dividerPosition)); - } - - private void observeStatefulSplitPane(final SplitPane splitPane) { - splitPane.getDividers().forEach(divider -> - divider.positionProperty().addListener((ignored, previous, current) -> { - if (applyingPendingLayoutState) { - return; - } - notifyStateChanged(); - })); - splitPane.getDividers().addListener((javafx.collections.ListChangeListener) change -> { - while (change.next()) { - if (change.wasAdded()) { - change.getAddedSubList().forEach(divider -> - divider.positionProperty().addListener((ignored, previous, current) -> { - if (applyingPendingLayoutState) { - return; - } - notifyStateChanged(); - })); - } - } - }); - } - - private void observeDockPane(final p.studio.controls.WorkspaceDockPane dockPane) { - dockPane.expandedProperty().addListener((ignored, previous, current) -> { - if (applyingPendingLayoutState) { - return; - } - notifyStateChanged(); - }); - } - - private void notifyStateChanged() { - pendingEditorLayoutState = captureLayoutState(); - stateChangedAction.run(); - } - - private void schedulePendingLayoutReapply() { - if (contentSplit == null) { - return; - } - if (contentSplit.getWidth() > 0) { - Platform.runLater(this::reapplyPendingLayoutState); - return; - } - final javafx.beans.value.ChangeListener[] listenerRef = new javafx.beans.value.ChangeListener[1]; - listenerRef[0] = (ignored, previous, current) -> { - if (current == null || current.doubleValue() <= 0) { - return; - } - contentSplit.widthProperty().removeListener(listenerRef[0]); - Platform.runLater(this::reapplyPendingLayoutState); - }; - contentSplit.widthProperty().addListener(listenerRef[0]); - } - - private void applyEditorLayoutState(final ProjectLocalStudioState.EditorLayoutState editorLayoutState) { - applyingPendingLayoutState = true; - applyDividerPositions(contentSplit, editorLayoutState.contentSplitDividers()); - applyDividerPositions(leftColumnSplit, editorLayoutState.leftColumnDividers()); - applyDividerPositions(rightColumnSplit, editorLayoutState.rightColumnDividers()); - restoreDockPanelState(outlinePanel, editorLayoutState.outlinePanel()); - restoreDockPanelState(helperPanel, editorLayoutState.helperPanel()); - navigatorPanel.restoreExpandedPaths(editorLayoutState.navigatorExpandedPaths()); - Platform.runLater(() -> applyingPendingLayoutState = false); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java deleted file mode 100644 index 15d28072..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/EditorWorkspaceIcons.java +++ /dev/null @@ -1,85 +0,0 @@ -package p.studio.workspaces.editor; - -import javafx.scene.Node; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.SVGPath; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public final class EditorWorkspaceIcons { - private static final Pattern SVG_PATH_PATTERN = Pattern.compile("]*\\sd=\"([^\"]+)\""); - private static final String FOLDER_PATH = "M1.5 4.5A1.5 1.5 0 0 1 3 3h3.2l1.1 1.4H13A1.5 1.5 0 0 1 14.5 5.9V12A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12z"; - private static final String FILE_PATH = "M4 1.5h5l3 3V13A1.5 1.5 0 0 1 10.5 14.5h-6A1.5 1.5 0 0 1 3 13V3A1.5 1.5 0 0 1 4.5 1.5zm4.7 1.2V5h2.3z"; - private static final String COG_PATH = "M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.24-.68-2.64.72-1.96 1.96l.17.31c.446.816.023 1.84-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1c.895.265 1.318 1.289.872 2.105l-.17.31c-.68 1.24.72 2.64 1.96 1.96l.31-.17a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.24.68 2.64-.72 1.96-1.96l-.17-.31a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.68-1.24-.72-2.64-1.96-1.96l-.31.17a1.464 1.464 0 0 1-2.105-.872z"; - private static final String REFRESH_PATH = "M8 2.2a5.8 5.8 0 0 1 4.3 1.9V2.8h1.2v3.8H9.7V5.4h1.8A4.6 4.6 0 1 0 12 11h1.3A5.9 5.9 0 1 1 8 2.2z"; - private static final String TARGET_PATH = "M7.25 1.5h1.5v1.9a4.85 4.85 0 0 1 3.85 3.85h1.9v1.5h-1.9a4.85 4.85 0 0 1-3.85 3.85v1.9h-1.5v-1.9A4.85 4.85 0 0 1 3.4 8.75H1.5v-1.5h1.9A4.85 4.85 0 0 1 7.25 3.4zm.75 3.25A3.25 3.25 0 1 0 8 11.25 3.25 3.25 0 0 0 8 4.75zm0 1.65a1.6 1.6 0 1 1 0 3.2 1.6 1.6 0 0 1 0-3.2z"; - - private EditorWorkspaceIcons() { - } - - public static Node folderDefault() { - return icon(FOLDER_PATH, "editor-workspace-icon-folder-default"); - } - - public static Node folderSource() { - return icon(FOLDER_PATH, "editor-workspace-icon-folder-source"); - } - - public static Node folderBuild() { - return icon(FOLDER_PATH, "editor-workspace-icon-folder-build"); - } - - public static Node file() { - return icon(FILE_PATH, "editor-workspace-icon-file"); - } - - public static Node cog() { - return icon(COG_PATH, "editor-workspace-icon-cog"); - } - - public static Node refresh() { - return icon(REFRESH_PATH, "editor-workspace-icon-refresh"); - } - - public static Node target() { - return icon(TARGET_PATH, "editor-workspace-icon-target"); - } - - public static Node lockClosed() { - return icon(iconPathFromResource("/icons/editor/lock-closed.svg"), "editor-workspace-icon-lock-closed"); - } - - public static Node lockOpen() { - return icon(iconPathFromResource("/icons/editor/lock-open.svg"), "editor-workspace-icon-lock-open"); - } - - private static Node icon(final String path, final String toneClass) { - final var shape = new SVGPath(); - shape.setContent(path); - shape.getStyleClass().addAll("editor-workspace-icon-shape", toneClass); - - final var icon = new StackPane(shape); - icon.getStyleClass().add("editor-workspace-icon"); - return icon; - } - - private static String iconPathFromResource(final String resourcePath) { - try (InputStream inputStream = EditorWorkspaceIcons.class.getResourceAsStream(resourcePath)) { - if (inputStream == null) { - throw new IllegalStateException("Missing icon resource: " + resourcePath); - } - final String svg = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - final Matcher matcher = SVG_PATH_PATTERN.matcher(svg); - if (!matcher.find()) { - throw new IllegalStateException("Missing in icon resource: " + resourcePath); - } - return matcher.group(1); - } catch (IOException exception) { - throw new IllegalStateException("Unable to read icon resource: " + resourcePath, exception); - } - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentHighlightToken.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentHighlightToken.java deleted file mode 100644 index fc1c8c9d..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentHighlightToken.java +++ /dev/null @@ -1,25 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -public record EditorDocumentHighlightToken( - String groupName, - Collection styleClasses) { - - public EditorDocumentHighlightToken(final String groupName, final String styleClass) { - this(groupName, List.of(styleClass)); - } - - public EditorDocumentHighlightToken { - groupName = Objects.requireNonNull(groupName, "groupName"); - styleClasses = List.copyOf(Objects.requireNonNull(styleClasses, "styleClasses")); - if (groupName.isBlank()) { - throw new IllegalArgumentException("groupName cannot be blank"); - } - if (styleClasses.isEmpty()) { - throw new IllegalArgumentException("styleClasses cannot be empty"); - } - } -} \ No newline at end of file diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java deleted file mode 100644 index 89dd1487..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSemanticHighlighting.java +++ /dev/null @@ -1,64 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyleSpansBuilder; -import p.studio.lsp.dtos.LspHighlightSpanDTO; - -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashSet; -import java.util.List; - -public final class EditorDocumentSemanticHighlighting { - private EditorDocumentSemanticHighlighting() { - } - - public static StyleSpans> highlight( - final String content, - final List semanticHighlights) { - final StyleSpansBuilder> builder = new StyleSpansBuilder<>(); - final List orderedHighlights = semanticHighlights.stream() - .sorted(Comparator.comparingInt(highlight -> highlight.range().startOffset())) - .toList(); - int cursor = 0; - for (final LspHighlightSpanDTO highlight : orderedHighlights) { - final int start = Math.max(cursor, highlight.range().startOffset()); - final int end = Math.min(content.length(), highlight.range().endOffset()); - if (start > cursor) { - builder.add(Collections.emptyList(), start - cursor); - } - if (end > start) { - builder.add(List.of("editor-semantic-" + highlight.semanticKey()), end - start); - cursor = end; - } - } - if (cursor < content.length()) { - builder.add(Collections.emptyList(), content.length() - cursor); - } - if (content.isEmpty()) { - builder.add(Collections.emptyList(), 0); - } - return builder.create(); - } - - public static StyleSpans> overlay( - final StyleSpans> baseHighlighting, - final StyleSpans> semanticHighlighting) { - return baseHighlighting.overlay(semanticHighlighting, EditorDocumentSemanticHighlighting::mergeStyles); - } - - private static Collection mergeStyles( - final Collection baseStyles, - final Collection semanticStyles) { - if (baseStyles.isEmpty()) { - return semanticStyles; - } - if (semanticStyles.isEmpty()) { - return baseStyles; - } - final LinkedHashSet merged = new LinkedHashSet<>(baseStyles); - merged.addAll(semanticStyles); - return List.copyOf(merged); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java deleted file mode 100644 index 92271b9b..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlighting.java +++ /dev/null @@ -1,59 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyleSpansBuilder; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public record EditorDocumentSyntaxHighlighting( - Pattern tokenPattern, - List tokens) { - - public EditorDocumentSyntaxHighlighting { - tokens = List.copyOf(Objects.requireNonNull(tokens, "tokens")); - if (tokenPattern == null && !tokens.isEmpty()) { - throw new IllegalArgumentException("plain text highlighting cannot define tokens"); - } - if (tokenPattern != null && tokens.isEmpty()) { - throw new IllegalArgumentException("token-based highlighting requires tokens"); - } - } - - public static EditorDocumentSyntaxHighlighting plainText() { return EditorDocumentSyntaxHighlightingPlainText.PLAIN_TEXT; } - public static EditorDocumentSyntaxHighlighting json() { return EditorDocumentSyntaxHighlightingJson.JSON; } - public static EditorDocumentSyntaxHighlighting bash() { return EditorDocumentSyntaxHighlightingBash.BASH; } - - public StyleSpans> highlight(final String content) { - final StyleSpansBuilder> builder = new StyleSpansBuilder<>(); - if (tokenPattern == null) { - builder.add(Collections.emptyList(), content.length()); - return builder.create(); - } - - final Matcher matcher = tokenPattern.matcher(content); - int lastMatchEnd = 0; - - while (matcher.find()) { - builder.add(Collections.emptyList(), matcher.start() - lastMatchEnd); - builder.add(styleClassFor(matcher), matcher.end() - matcher.start()); - lastMatchEnd = matcher.end(); - } - - builder.add(Collections.emptyList(), content.length() - lastMatchEnd); - return builder.create(); - } - - private Collection styleClassFor(final Matcher matcher) { - for (final EditorDocumentHighlightToken token : tokens) { - if (matcher.group(token.groupName()) != null) { - return token.styleClasses(); - } - } - throw new IllegalStateException("matched token without registered style"); - } -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingBash.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingBash.java deleted file mode 100644 index 3623b37e..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingBash.java +++ /dev/null @@ -1,28 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.List; -import java.util.regex.Pattern; - -public class EditorDocumentSyntaxHighlightingBash { - public static final EditorDocumentSyntaxHighlighting BASH = new EditorDocumentSyntaxHighlighting( - Pattern.compile( - "(?^#![^\\n]*)" - + "|(?(?m)(?\"(?:\\\\.|[^\"\\\\])*\"|'(?:\\\\.|[^'\\\\])*')" - + "|(?\\$\\{?[A-Za-z_][A-Za-z0-9_]*}?|\\$[0-9@*#?!$-])" - + "|(?(?m)^[ \\t]*test\\b)" - + "|(?(?m)^[ \\t]*[A-Za-z_./-][A-Za-z0-9_./-]*)" - + "|(?\\b(?:if|then|else|elif|fi|for|while|until|do|done|case|esac|in|function|select|time)\\b)" - + "|(?\\b(?:echo|printf|read|cd|pwd|export|local|readonly|unset|return|shift|source|trap|exit)\\b)" - + "|(?\\|\\||&&|;;|<<-?|>>|[|&;<>~=(){}\\[\\]])"), - List.of( - new EditorDocumentHighlightToken("SHEBANG", "editor-syntax-bash-shebang"), - new EditorDocumentHighlightToken("COMMENT", "editor-syntax-bash-comment"), - new EditorDocumentHighlightToken("STRING", "editor-syntax-bash-string"), - new EditorDocumentHighlightToken("VARIABLE", "editor-syntax-bash-variable"), - new EditorDocumentHighlightToken("TESTBUILTIN", "editor-syntax-bash-builtin"), - new EditorDocumentHighlightToken("KEYWORD", "editor-syntax-bash-keyword"), - new EditorDocumentHighlightToken("BUILTIN", "editor-syntax-bash-builtin"), - new EditorDocumentHighlightToken("COMMAND", "editor-syntax-bash-command"), - new EditorDocumentHighlightToken("OPERATOR", "editor-syntax-bash-operator"))); -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingJson.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingJson.java deleted file mode 100644 index 58f803e8..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingJson.java +++ /dev/null @@ -1,22 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.List; -import java.util.regex.Pattern; - -public class EditorDocumentSyntaxHighlightingJson { - public static final EditorDocumentSyntaxHighlighting JSON = new EditorDocumentSyntaxHighlighting( - Pattern.compile( - "(?\"(?:\\\\.|[^\"\\\\])*\"(?=\\s*:))" - + "|(?\"(?:\\\\.|[^\"\\\\])*\")" - + "|(?-?\\b\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)" - + "|(?\\b(?:true|false)\\b)" - + "|(?\\bnull\\b)" - + "|(?[\\{\\}\\[\\],:])"), - List.of( - new EditorDocumentHighlightToken("KEY", "editor-syntax-json-key"), - new EditorDocumentHighlightToken("STRING", "editor-syntax-json-string"), - new EditorDocumentHighlightToken("NUMBER", "editor-syntax-json-number"), - new EditorDocumentHighlightToken("BOOLEAN", "editor-syntax-json-boolean"), - new EditorDocumentHighlightToken("NULL", "editor-syntax-json-null"), - new EditorDocumentHighlightToken("PUNCTUATION", "editor-syntax-json-punctuation"))); -} diff --git a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingPlainText.java b/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingPlainText.java deleted file mode 100644 index 1f13091b..00000000 --- a/prometeu-studio/src/main/java/p/studio/workspaces/editor/syntaxhighlight/EditorDocumentSyntaxHighlightingPlainText.java +++ /dev/null @@ -1,9 +0,0 @@ -package p.studio.workspaces.editor.syntaxhighlight; - -import java.util.List; - -public class EditorDocumentSyntaxHighlightingPlainText { - public static final EditorDocumentSyntaxHighlighting PLAIN_TEXT = new EditorDocumentSyntaxHighlighting( - null, - List.of()); -} diff --git a/prometeu-studio/src/main/resources/i18n/messages.properties b/prometeu-studio/src/main/resources/i18n/messages.properties index d0bb1aa4..0df5cfe4 100644 --- a/prometeu-studio/src/main/resources/i18n/messages.properties +++ b/prometeu-studio/src/main/resources/i18n/messages.properties @@ -3,7 +3,7 @@ app.projectTitle=Prometeu Studio - {0} shield.loadingProject=Loading project {0}... shield.version=Version {0} shield.status.indexing=Indexing sources and restoring services... -shield.status.restoring=Restoring workspace surfaces and editor state... +shield.status.restoring=Restoring workspace surfaces and project state... shield.phase.indexing=Indexing shield.phase.preparing=Preparing UI shield.phase.restoring=Restoring State @@ -69,37 +69,6 @@ wizard.error.indentationRequired=Indentation width must be selected. toolbar.play=Play toolbar.stop=Stop toolbar.export=Export -workspace.code=Code -codeEditor.navigator.title=Project Navigator -codeEditor.navigator.refresh=Refresh -codeEditor.navigator.revealActive=Reveal active file -codeEditor.navigator.placeholder=Project navigation lands in the next implementation slice. -codeEditor.navigator.detail=This first shell reserves the full navigator surface, its refresh action, and the left-column composition without wiring project-tree data yet. -codeEditor.outline.title=Outline -codeEditor.outline.placeholder=Open a frontend document to inspect semantic symbols. -codeEditor.outline.symbols=Symbols -codeEditor.outline.emptySymbols=No semantic symbols are currently available for the active frontend document. -codeEditor.helper.title=Editor Helper -codeEditor.helper.placeholder=Open a frontend document to inspect semantic diagnostics. -codeEditor.helper.diagnostics=Diagnostics -codeEditor.helper.emptyDiagnostics=No diagnostics for the active frontend document. -codeEditor.tabs.placeholder=no-file-open.txt -codeEditor.tabs.overflow=More -codeEditor.status.breadcrumb=proj > src > file.pbs -codeEditor.status.position=L:C -codeEditor.status.lineSeparator=LF -codeEditor.status.indentation=Spaces: 4 -codeEditor.status.language=Text -codeEditor.command.save=Save -codeEditor.command.saveAll=Save All -codeEditor.closeDirty.title=Unsaved changes -codeEditor.closeDirty.message=Save changes to {0} before closing? -codeEditor.closeDirty.save=Save -codeEditor.closeDirty.discard=Discard -codeEditor.closeDirty.cancel=Cancel -codeEditor.warning.frontendReadOnly=This frontend file is read-only in this wave. It cannot be edited or saved yet. -codeEditor.unsupportedFile.title=Unsupported file -codeEditor.unsupportedFile.message=This file is not supported in this wave: {0} workspace.shipper=Shipper workspace.shipper.logs=Logs workspace.shipper.button.run=Build diff --git a/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties b/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties index 71ec4f02..5e00b3ed 100644 --- a/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties +++ b/prometeu-studio/src/main/resources/i18n/messages_pt_BR.properties @@ -12,7 +12,6 @@ toolbar.play=Executar toolbar.stop=Parar toolbar.export=Exportar -workspace.code=Cdigo workspace.shipper=Empacotador workspace.shipper.logs=Logs workspace.shipper.button.run=Construir diff --git a/prometeu-studio/src/main/resources/themes/default-prometeu.css b/prometeu-studio/src/main/resources/themes/default-prometeu.css index 3e5910ab..c98780ff 100644 --- a/prometeu-studio/src/main/resources/themes/default-prometeu.css +++ b/prometeu-studio/src/main/resources/themes/default-prometeu.css @@ -352,24 +352,6 @@ -fx-background-color: #17191d; } -.editor-workspace { - -fx-background-color: #15181d; -} - -.editor-workspace-layout { - -fx-padding: 16; - -fx-background-color: #15181d; -} - -.editor-workspace-split { - -fx-background-color: transparent; -} - -.editor-workspace-split > .split-pane-divider { - -fx-background-color: transparent; - -fx-padding: 0 8 0 8; -} - .workspace-dock-split { -fx-background-color: transparent; } @@ -386,544 +368,6 @@ -fx-padding: 0; } -.editor-workspace-left-column, -.editor-workspace-center-column { - -fx-spacing: 12; -} - -.editor-workspace-panel { - -fx-background-color: #1b1f25; - -fx-background-radius: 12; - -fx-border-radius: 12; - -fx-border-color: #2a313c; -} - -.editor-workspace-panel-header { - -fx-alignment: center-left; -} - -.editor-workspace-header-icon-button { - -fx-background-color: transparent; - -fx-border-color: transparent; - -fx-background-radius: 4; - -fx-border-radius: 4; - -fx-padding: 6; -} - -.editor-workspace-header-icon-button:hover { - -fx-background-color: #243243; - -fx-border-color: #556f8b; -} - -.editor-workspace-panel-content { - -fx-padding: 6 0 0 0; -} - -.editor-workspace-panel-title { - -fx-text-fill: #eff5fb; - -fx-font-size: 14px; - -fx-font-weight: bold; -} - -.editor-workspace-placeholder { - -fx-text-fill: #c3cfdb; - -fx-font-size: 12px; -} - -.editor-workspace-placeholder-detail { - -fx-text-fill: #8896a7; - -fx-font-size: 11px; -} - -.editor-workspace-navigator-panel { - -fx-min-height: 320px; -} - -.editor-workspace-tree { - -fx-background-color: transparent; - -fx-border-color: transparent; -} - -.editor-workspace-tree .tree-cell { - -fx-background-color: transparent; - -fx-text-fill: #d6e0ea; - -fx-font-size: 14px; - -fx-padding: 4 6 4 4; -} - -.editor-workspace-tree .tree-cell:selected { - -fx-background-color: #24415e; - -fx-text-fill: #ffffff; -} - -.editor-workspace-tree .tree-disclosure-node { - -fx-padding: 0 4 0 0; -} - -.editor-workspace-tree .tree-disclosure-node .arrow { - -fx-background-color: #8ea4ba; -} - -.editor-workspace-tree .tree-cell:selected .tree-disclosure-node .arrow { - -fx-background-color: #e8f3ff; -} - -.editor-workspace-tree-cell-tagged { - -fx-font-weight: bold; -} - -.editor-workspace-tree-cell-root { - -fx-font-weight: bold; - -fx-text-fill: #f4f8fc; -} - -.editor-workspace-icon { - -fx-min-width: 14; - -fx-min-height: 14; - -fx-pref-width: 14; - -fx-pref-height: 14; - -fx-max-width: 14; - -fx-max-height: 14; - -fx-alignment: center; -} - -.editor-workspace-icon-shape { - -fx-scale-x: 0.88; - -fx-scale-y: 0.88; -} - -.editor-workspace-icon-folder-default { - -fx-fill: #8792a1; -} - -.editor-workspace-icon-folder-source { - -fx-fill: #4ea0ef; -} - -.editor-workspace-icon-folder-build { - -fx-fill: #d88a3d; -} - -.editor-workspace-icon-file { - -fx-fill: #b0bac6; -} - -.editor-workspace-icon-cog { - -fx-fill: #c6d0da; -} - -.editor-workspace-icon-refresh { - -fx-fill: #eff5fb; -} - -.editor-workspace-icon-target { - -fx-fill: #eff5fb; -} - -.editor-workspace-icon-lock-closed { - -fx-fill: #f6d78f; -} - -.editor-workspace-icon-lock-open { - -fx-fill: #bde7c7; -} - -.editor-workspace-outline-panel { - -fx-min-height: 0; -} - -.editor-workspace-outline-scroll { - -fx-background-color: transparent; - -fx-background-insets: 0; -} - -.editor-workspace-outline-scroll > .viewport { - -fx-background-color: transparent; -} - -.editor-workspace-outline-summary { - -fx-text-fill: #dce6f0; -} - -.editor-workspace-outline-section-title { - -fx-font-size: 11px; - -fx-font-weight: 700; - -fx-text-fill: #8fb1d2; - -fx-padding: 4 0 0 0; -} - -.editor-workspace-outline-list { - -fx-spacing: 6; -} - -.editor-workspace-outline-item { - -fx-text-fill: #d7e2ec; - -fx-font-size: 12px; -} - -.editor-workspace-outline-diagnostic-error { - -fx-text-fill: #ff9a9a; -} - -.editor-workspace-outline-diagnostic-warning { - -fx-text-fill: #f6d78f; -} - -.editor-workspace-tab-strip { - -fx-padding: 8 12 8 12; - -fx-background-color: #1b1f25; - -fx-background-radius: 0; - -fx-border-radius: 0; - -fx-border-color: #2a313c; -} - -.editor-workspace-code-area { - -fx-background-color: #171c22; - -fx-font-size: 15px; - -fx-highlight-fill: #26405c; - -fx-highlight-text-fill: #eef4fb; -} - -.editor-workspace-tab-button { - -fx-background-radius: 0; - -fx-border-radius: 0; - -fx-padding: 0 8 0 12; - -fx-alignment: center-left; - -fx-font-size: 12px; - -fx-background-color: #20262f; - -fx-border-color: #343f4d; - -fx-border-width: 1; - -fx-text-fill: #d6dde6; -} - -.editor-workspace-tab-content { - -fx-spacing: 8; - -fx-alignment: center-left; -} - -.editor-workspace-tab-label { - -fx-alignment: center-left; - -fx-font-size: 12px; -} - -.editor-workspace-tab-dirty-indicator { - -fx-background-color: #d9e3ef; - -fx-background-radius: 999; - -fx-border-radius: 999; - -fx-opacity: 0.9; -} - -.editor-workspace-tab-close-chip { - -fx-alignment: center; - -fx-background-color: #131820; - -fx-background-radius: 999; - -fx-border-color: #485667; - -fx-border-radius: 999; - -fx-border-width: 1; - -fx-padding: 0; - -fx-cursor: hand; -} - -.editor-workspace-tab-close-icon { - -fx-fill: transparent; - -fx-stroke: #cfd8e2; - -fx-stroke-width: 1.1; -} - -.editor-workspace-tab-button-active { - -fx-background-color: #16283d; - -fx-border-color: #8fc4f2 #516579 #516579 #516579; - -fx-border-width: 3 1 1 1; - -fx-text-fill: #ffffff; - -fx-font-weight: bold; -} - -.editor-workspace-tab-button-read-only { - -fx-background-color: #252b34; - -fx-border-color: #47515d; - -fx-text-fill: #d9dee5; -} - -.editor-workspace-tab-button-read-only .editor-workspace-tab-label { - -fx-text-fill: #d9dee5; -} - -.editor-workspace-tab-button-read-only .editor-workspace-tab-dirty-indicator { - -fx-background-color: #d9dee5; -} - -.editor-workspace-tab-button-read-only .editor-workspace-tab-close-icon { - -fx-stroke: #d9dee5; -} - -.editor-workspace-tab-button-read-only .editor-workspace-tab-close-chip { - -fx-background-color: #171c23; - -fx-border-color: #4f5b68; -} - -.editor-workspace-tab-button-read-only:hover { - -fx-background-color: #2b323d; - -fx-border-color: #5b6878; - -fx-text-fill: #eff4fa; -} - -.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-label { - -fx-text-fill: #eff4fa; -} - -.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-dirty-indicator { - -fx-background-color: #eff4fa; -} - -.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip { - -fx-background-color: #1c232c; - -fx-border-color: #6b7989; -} - -.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip:hover { - -fx-background-color: #2a3440; - -fx-border-color: #90a2b5; -} - -.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip:hover .editor-workspace-tab-close-icon { - -fx-stroke: #f7fbff; -} - -.editor-workspace-tab-button-read-only:hover .editor-workspace-tab-close-chip:pressed { - -fx-background-color: #364351; - -fx-border-color: #a8bbce; -} - -.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active { - -fx-background-color: #16283d; - -fx-border-color: #8fc4f2 #516579 #516579 #516579; - -fx-border-width: 3 1 1 1; - -fx-text-fill: #ffffff; - -fx-font-weight: bold; -} - -.editor-workspace-tab-button-read-only.editor-workspace-tab-button-active:hover { - -fx-background-color: #1c3148; - -fx-border-color: #a7d7ff #5c738b #5c738b #5c738b; -} - -.editor-workspace-tab-button-editable { - -fx-background-color: #203226; - -fx-border-color: #4d6f58; - -fx-text-fill: #e8f6eb; -} - -.editor-workspace-tab-button-editable .editor-workspace-tab-label { - -fx-text-fill: #e8f6eb; -} - -.editor-workspace-tab-button-editable .editor-workspace-tab-dirty-indicator { - -fx-background-color: #b9f0c7; -} - -.editor-workspace-tab-button-editable .editor-workspace-tab-close-icon { - -fx-stroke: #e8f6eb; -} - -.editor-workspace-tab-button-editable .editor-workspace-tab-close-chip { - -fx-background-color: #15211a; - -fx-border-color: #557363; -} - -.editor-workspace-tab-button-editable:hover { - -fx-background-color: #29412f; - -fx-border-color: #6f957a; - -fx-text-fill: #f4fff5; -} - -.editor-workspace-tab-button-editable:hover .editor-workspace-tab-label { - -fx-text-fill: #f4fff5; -} - -.editor-workspace-tab-button-editable:hover .editor-workspace-tab-dirty-indicator { - -fx-background-color: #f4fff5; -} - -.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip { - -fx-background-color: #192720; - -fx-border-color: #789886; -} - -.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip:hover { - -fx-background-color: #27382d; - -fx-border-color: #9fc0ac; -} - -.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip:hover .editor-workspace-tab-close-icon { - -fx-stroke: #ffffff; -} - -.editor-workspace-tab-button-editable:hover .editor-workspace-tab-close-chip:pressed { - -fx-background-color: #314739; - -fx-border-color: #b2d2bf; -} - -.editor-workspace-tab-button-editable.editor-workspace-tab-button-active { - -fx-background-color: #1d3a2a; - -fx-border-color: #8ad3a2 #587464 #587464 #587464; - -fx-border-width: 3 1 1 1; - -fx-text-fill: #ffffff; - -fx-font-weight: bold; -} - -.editor-workspace-tab-button-active .editor-workspace-tab-label { - -fx-text-fill: #ffffff; - -fx-font-weight: bold; -} - -.editor-workspace-tab-button-active .editor-workspace-tab-dirty-indicator { - -fx-background-color: #ffffff; -} - -.editor-workspace-tab-button-active .editor-workspace-tab-close-icon { - -fx-stroke: #ffffff; -} - -.editor-workspace-tab-button-active .editor-workspace-tab-close-chip { - -fx-background-color: rgba(255, 255, 255, 0.08); - -fx-border-color: rgba(255, 255, 255, 0.22); -} - -.editor-workspace-tab-button-active:hover .editor-workspace-tab-close-chip:hover { - -fx-background-color: rgba(255, 255, 255, 0.18); - -fx-border-color: rgba(255, 255, 255, 0.42); -} - -.editor-workspace-tab-button-active:hover .editor-workspace-tab-close-chip:pressed { - -fx-background-color: rgba(255, 255, 255, 0.28); - -fx-border-color: rgba(255, 255, 255, 0.56); -} - -.editor-workspace-tab-button-editable.editor-workspace-tab-button-active:hover { - -fx-background-color: #234532; - -fx-border-color: #a5efbd #688676 #688676 #688676; -} - - -.editor-workspace-tab-overflow { - -fx-background-radius: 0; - -fx-border-radius: 0; - -fx-padding: 0 12 0 12; - -fx-min-width: 128; - -fx-pref-width: 128; - -fx-max-width: 128; - -fx-min-height: 34; - -fx-pref-height: 34; - -fx-max-height: 34; -} - -.editor-workspace-tab-overflow-item { - -fx-text-fill: #d9e2eb; - -fx-font-size: 12px; - -fx-padding: 0 10 0 10; - -fx-background-color: transparent; -} - -.editor-workspace-code-area .content { - -fx-background-color: #171c22; -} - -.editor-workspace-code-area .paragraph-box { - -fx-background-color: #171c22; -} - -.editor-workspace-code-area .lineno { - -fx-background-color: #12161c; - -fx-text-fill: #6f7a86; - -fx-padding: 0 12 0 12; - -fx-font-family: "JetBrains Mono Medium", "JetBrains Mono", "Iosevka", "Cascadia Mono", "IBM Plex Mono", monospace; - -fx-font-size: 14px; -} - -.editor-workspace-code-area .text { - -fx-fill: #f2f6fb; - -fx-font-family: "JetBrains Mono Medium", "JetBrains Mono", "Iosevka", "Cascadia Mono", "IBM Plex Mono", monospace; - -fx-font-size: 14px; -} - -.editor-workspace-code-area .caret { - -fx-stroke: #ffd27a; -} - -.editor-workspace-editor-surface { - -fx-background-color: #171c22; - -fx-border-color: #2a313c; - -fx-border-width: 1; -} - -.editor-workspace-editor-viewport { - -fx-background-color: transparent; -} - -.editor-workspace-code-area .text.editor-inline-hint { - -fx-fill: #707070; - -rtfx-background-color: #1f242f; - -fx-font-family: "JetBrains Mono Medium", "JetBrains Mono", "Iosevka", "Cascadia Mono", "IBM Plex Mono", monospace; - -fx-font-size: 13px; -} - -.editor-workspace-code-area .text.editor-inline-hint-type { - -fx-fill: #707070; - -rtfx-background-color: #1f242f; -} - -.editor-workspace-code-area-type-text .text { - -fx-fill: #eef3f8; -} - -.editor-workspace-command-bar { - -fx-padding: 10 12 0 12; - -fx-alignment: center-left; -} - -.editor-workspace-command-button { - -fx-alignment: center; - -fx-pref-width: 118px; - -fx-min-width: 118px; -} - -.editor-workspace-warning { - -fx-alignment: center-left; - -fx-spacing: 8; - -fx-padding: 8 12 8 12; - -fx-background-color: #3b2a10; - -fx-border-color: #8f6730; - -fx-border-width: 0 0 1 0; - -fx-text-fill: #f7ddb0; - -fx-font-size: 12px; -} - -.editor-workspace-warning-counter { - -fx-text-fill: #f3d8a7; - -fx-font-size: 11px; - -fx-font-weight: 700; -} - -.editor-workspace-warning-button { - -fx-min-width: 24; - -fx-pref-width: 24; - -fx-max-width: 24; - -fx-min-height: 24; - -fx-pref-height: 24; - -fx-max-height: 24; - -fx-padding: 0; - -fx-background-color: #4b3413; - -fx-border-color: #9a7440; - -fx-text-fill: #f7ddb0; - -fx-font-size: 11px; - -fx-background-radius: 4; - -fx-border-radius: 4; -} - -.editor-workspace-warning-button:hover { - -fx-background-color: #5a4018; - -fx-border-color: #b38749; -} .workspace-dock-pane { -fx-collapsible: true; @@ -960,13 +404,6 @@ -fx-border-insets: 0; } -.editor-workspace-helper-panel { - -fx-min-height: 0; -} - -.editor-workspace-helper-summary { - -fx-text-fill: #dce6f0; -} .workspace-dock-pane-collapsed { -fx-background-insets: 0; @@ -984,107 +421,6 @@ -fx-border-insets: 0; } -.editor-workspace-status-bar { - -fx-padding: 6 12 6 12; - -fx-background-color: #1b1f25; - -fx-background-radius: 12; - -fx-border-radius: 12; - -fx-border-color: #2a313c; - -fx-min-height: 40; - -fx-pref-height: 40; - -fx-max-height: 40; -} - -.editor-workspace-status-breadcrumb { - -fx-alignment: center-left; -} - -.editor-workspace-status-breadcrumb-segment { - -fx-alignment: center-left; - -fx-padding: 0 2 0 0; -} - -.editor-workspace-status-breadcrumb-segment-root { - -fx-font-weight: bold; -} - -.editor-workspace-status-breadcrumb-text { - -fx-text-fill: #d4dde7; - -fx-font-size: 12px; -} - -.editor-workspace-status-breadcrumb-segment-root .editor-workspace-status-breadcrumb-text { - -fx-text-fill: #f1f7fd; - -fx-font-weight: bold; -} - -.editor-workspace-status-breadcrumb-separator { - -fx-text-fill: #6f7e8e; - -fx-font-size: 12px; -} - -.editor-workspace-status-chip { - -fx-background-color: #11151b; - -fx-background-radius: 4; - -fx-border-radius: 4; - -fx-border-color: #2a313c; - -fx-padding: 0 8 0 8; - -fx-text-fill: #c5d2de; - -fx-font-size: 11px; - -fx-alignment: center; - -fx-min-height: 28; - -fx-pref-height: 28; - -fx-max-height: 28; -} - -.editor-workspace-status-chip-type-text { - -fx-background-color: #11151b; - -fx-border-color: #2a313c; - -fx-text-fill: #c5d2de; -} - -.editor-workspace-status-chip-position { - -fx-min-width: 44; - -fx-pref-width: 44; - -fx-max-width: 44; -} - -.editor-workspace-status-chip-line-separator { - -fx-min-width: 44; - -fx-pref-width: 44; - -fx-max-width: 44; -} - -.editor-workspace-status-chip-indentation { - -fx-min-width: 84; - -fx-pref-width: 84; - -fx-max-width: 84; -} - -.editor-workspace-status-chip-language { - -fx-min-width: 72; - -fx-pref-width: 72; - -fx-max-width: 72; -} - -.editor-workspace-status-chip-access-mode { - -fx-min-width: 36; - -fx-pref-width: 36; - -fx-max-width: 36; - -fx-padding: 0; -} - -.editor-workspace-status-chip-read-only { - -fx-background-color: #3b2a10; - -fx-border-color: #8f6730; - -fx-text-fill: #f7ddb0; -} - -.editor-workspace-status-chip-editable { - -fx-background-color: #203226; - -fx-border-color: #5a8567; - -fx-text-fill: #ebfff0; -} .assets-workspace-split { -fx-background-color: transparent; diff --git a/prometeu-studio/src/main/resources/themes/editor/presentations/bash.css b/prometeu-studio/src/main/resources/themes/editor/presentations/bash.css deleted file mode 100644 index a3e2c6a9..00000000 --- a/prometeu-studio/src/main/resources/themes/editor/presentations/bash.css +++ /dev/null @@ -1,39 +0,0 @@ -.editor-workspace-code-area-type-bash .text { - -fx-fill: #edf2f7; -} - -.editor-workspace-code-area-type-bash .lineno { - -fx-text-fill: #788595; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-shebang { - -fx-fill: #f0ae63; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-comment { - -fx-fill: #7d8a98; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-string { - -fx-fill: #dcbf88; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-variable { - -fx-fill: #8fd4ff; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-keyword { - -fx-fill: #d7a6ff; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-builtin { - -fx-fill: #9fe2a0; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-command { - -fx-fill: #7fc1ff; -} - -.editor-workspace-code-area-type-bash .text.editor-syntax-bash-operator { - -fx-fill: #c7d3de; -} diff --git a/prometeu-studio/src/main/resources/themes/editor/presentations/json.css b/prometeu-studio/src/main/resources/themes/editor/presentations/json.css deleted file mode 100644 index fa180ac1..00000000 --- a/prometeu-studio/src/main/resources/themes/editor/presentations/json.css +++ /dev/null @@ -1,41 +0,0 @@ -.editor-workspace-code-area-type-json { - -fx-highlight-fill: #204766; -} - -.editor-workspace-code-area-type-json .text { - -fx-fill: #dff3ff; -} - -.editor-workspace-code-area-type-json .lineno { - -fx-text-fill: #7e92a6; -} - -.editor-workspace-code-area-type-json .text.editor-syntax-json-key { - -fx-fill: #8fd4ff; -} - -.editor-workspace-code-area-type-json .text.editor-syntax-json-string { - -fx-fill: #d9c48f; -} - -.editor-workspace-code-area-type-json .text.editor-syntax-json-number { - -fx-fill: #9ee39f; -} - -.editor-workspace-code-area-type-json .text.editor-syntax-json-boolean { - -fx-fill: #f3a6d6; -} - -.editor-workspace-code-area-type-json .text.editor-syntax-json-null { - -fx-fill: #c89cff; -} - -.editor-workspace-code-area-type-json .text.editor-syntax-json-punctuation { - -fx-fill: #c8d5e2; -} - -.editor-workspace-status-chip-type-json { - -fx-background-color: #132433; - -fx-border-color: #4e88b8; - -fx-text-fill: #e2f4ff; -} diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java index 42419295..96739885 100644 --- a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionFactoryTest.java @@ -1,43 +1,29 @@ package p.studio.projectsessions; import org.junit.jupiter.api.Test; +import p.studio.lsp.api.LspServerLifecycle; import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.LspService; -import p.studio.lsp.LspServiceFactory; +import p.studio.lsp.messages.LspServerConfiguration; +import p.studio.projects.ProjectReference; import p.studio.projectstate.ProjectLocalStudioSetup; import p.studio.projectstate.ProjectLocalStudioSetupService; import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioStateService; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.lsp.messages.LspDefinitionRequest; -import p.studio.lsp.messages.LspDefinitionResult; -import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.ProjectDocumentVfsFactory; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.messages.VfsProjectContext; -import p.studio.vfs.messages.VfsProjectSnapshot; -import p.studio.vfs.messages.VfsRefreshRequest; import java.nio.file.Path; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.*; final class StudioProjectSessionFactoryTest { @Test - void openCreatesProjectSessionBackedByProjectScopedVfs() { - final RecordingLspFactory lspFactory = new RecordingLspFactory(); - final RecordingVfsFactory vfsFactory = new RecordingVfsFactory(); + void openCreatesProjectSessionAndBootsLspServer() { final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService(); + final RecordingLspServerLifecycle lspServerLifecycle = new RecordingLspServerLifecycle(); final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory( - lspFactory, - vfsFactory, stateService, - setupService); + setupService, + lspServerLifecycle); final ProjectReference projectReference = new ProjectReference( "Example", "1.0.0", @@ -48,19 +34,33 @@ final class StudioProjectSessionFactoryTest { final StudioProjectSession session = sessionFactory.open(projectReference); assertSame(projectReference, session.projectReference()); - assertSame(vfsFactory.vfs, session.projectDocumentVfs()); - assertSame(lspFactory.service, session.prometeuLspService()); - assertEquals("Example", vfsFactory.capturedContext.projectName()); - assertEquals("pbs", vfsFactory.capturedContext.languageId()); - assertEquals(projectReference.rootPath().toAbsolutePath().normalize(), vfsFactory.capturedContext.rootPath()); - assertEquals("Example", lspFactory.capturedContext.projectName()); - assertEquals("pbs", lspFactory.capturedContext.languageId()); - assertSame(vfsFactory.vfs, lspFactory.capturedVfs); assertSame(stateService.loadedState, session.projectLocalStudioState()); assertSame(setupService.loadedSetup, session.projectLocalStudioSetup()); assertEquals("IDLE", session.executionSession().snapshot().state().name()); assertSame(projectReference, stateService.loadedProjectReference); assertSame(projectReference, setupService.loadedProjectReference); + assertEquals(1, lspServerLifecycle.bootCalls); + assertEquals(projectReference.languageId(), lspServerLifecycle.lastProjectContext.languageId()); + assertEquals(projectReference.rootPath().toAbsolutePath().normalize(), lspServerLifecycle.lastProjectContext.projectRoot()); + } + + @Test + void openSurfacesLspBootFailure() { + final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); + final RecordingProjectLocalStudioSetupService setupService = new RecordingProjectLocalStudioSetupService(); + final StudioProjectSessionFactory sessionFactory = new StudioProjectSessionFactory( + stateService, + setupService, + new FailingLspServerLifecycle()); + + final IllegalStateException failure = assertThrows(IllegalStateException.class, () -> sessionFactory.open(new ProjectReference( + "Example", + "1.0.0", + "pbs", + 1, + Path.of("/tmp/example")))); + + assertEquals("boom", failure.getMessage()); } private static final class RecordingProjectLocalStudioStateService extends ProjectLocalStudioStateService { @@ -68,8 +68,7 @@ final class StudioProjectSessionFactoryTest { private final ProjectLocalStudioState loadedState = new ProjectLocalStudioState( ProjectLocalStudioState.CURRENT_VERSION, ProjectLocalStudioState.ShellLayout.defaults(), - new ProjectLocalStudioState.OpenShellState("EDITOR"), - ProjectLocalStudioState.EditorRestorationState.defaults()); + new ProjectLocalStudioState.OpenShellState("ASSETS")); @Override public ProjectLocalStudioState load(final ProjectReference projectReference) { @@ -91,83 +90,29 @@ final class StudioProjectSessionFactoryTest { } } - private static final class RecordingVfsFactory implements ProjectDocumentVfsFactory { - private VfsProjectContext capturedContext; - private final VfsProjectDocument vfs = new NoOpVfsProjectDocument(); + private static final class RecordingLspServerLifecycle implements LspServerLifecycle { + private int bootCalls; + private LspProjectContext lastProjectContext; @Override - public VfsProjectDocument open(VfsProjectContext projectContext) { - this.capturedContext = projectContext; - return vfs; + public void bootServer(LspServerConfiguration serverConfiguration, LspProjectContext context) { + bootCalls++; + lastProjectContext = context; + } + + @Override + public void shutdownServer(final LspProjectContext project) { } } - private static final class RecordingLspFactory implements LspServiceFactory { - private LspProjectContext capturedContext; - private VfsProjectDocument capturedVfs; - private final LspService service = new NoOpPrometeuLspService(); - + private static final class FailingLspServerLifecycle implements LspServerLifecycle { @Override - public LspService open( - final LspProjectContext projectContext, - final VfsProjectDocument vfsProjectDocument) { - this.capturedContext = projectContext; - this.capturedVfs = vfsProjectDocument; - return service; - } - } - - private static final class NoOpVfsProjectDocument implements VfsProjectDocument { - @Override - public VfsProjectContext projectContext() { - throw new UnsupportedOperationException(); + public void bootServer(LspServerConfiguration serverConfiguration, LspProjectContext context) { + throw new IllegalStateException("boom"); } @Override - public VfsProjectSnapshot snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh(VfsRefreshRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public VfsDocumentOpenResult openDocument(Path path) { - throw new UnsupportedOperationException(); - } - } - - private static final class NoOpPrometeuLspService implements LspService { - @Override - public LspProjectContext projectContext() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectDocument projectDocumentVfs() { - throw new UnsupportedOperationException(); - } - - @Override - public LspSessionStateDTO snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public LspAnalyzeDocumentResult analyzeDocument(final LspAnalyzeDocumentRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public LspDefinitionResult definition(final LspDefinitionRequest request) { - throw new UnsupportedOperationException(); + public void shutdownServer(final LspProjectContext project) { } } } diff --git a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java index 5b731bc8..037f3b83 100644 --- a/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectsessions/StudioProjectSessionTest.java @@ -2,22 +2,13 @@ package p.studio.projectsessions; import org.junit.jupiter.api.Test; import p.studio.execution.StudioExecutionSessionService; +import p.studio.lsp.api.LspServerLifecycle; import p.studio.lsp.messages.LspProjectContext; -import p.studio.lsp.LspService; +import p.studio.lsp.messages.LspServerConfiguration; +import p.studio.projects.ProjectReference; import p.studio.projectstate.ProjectLocalStudioSetup; import p.studio.projectstate.ProjectLocalStudioState; import p.studio.projectstate.ProjectLocalStudioStateService; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentRequest; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.lsp.messages.LspDefinitionRequest; -import p.studio.lsp.messages.LspDefinitionResult; -import p.studio.projects.ProjectReference; -import p.studio.vfs.VfsProjectDocument; -import p.studio.vfs.VfsDocumentOpenResult; -import p.studio.vfs.messages.VfsProjectContext; -import p.studio.vfs.messages.VfsProjectSnapshot; -import p.studio.vfs.messages.VfsRefreshRequest; import java.nio.file.Path; @@ -26,43 +17,35 @@ import static org.junit.jupiter.api.Assertions.assertSame; final class StudioProjectSessionTest { @Test - void closeDelegatesToUnderlyingServicesOnlyOnce() { - final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument(); - final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs); + void closeSavesStateOnlyOnce() { final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); final StudioProjectSession session = new StudioProjectSession( projectReference(), - lsp, - vfs, stateService, ProjectLocalStudioSetup.defaults(), new StudioExecutionSessionService(), + null, ProjectLocalStudioState.defaults()); session.close(); session.close(); - assertEquals(1, lsp.closeCalls); - assertEquals(1, vfs.closeCalls); assertEquals(1, stateService.saveCalls); assertEquals(projectReference(), stateService.savedProjectReference); } @Test void replaceProjectLocalStudioStateUpdatesTheSavedSnapshot() { - final CountingVfsProjectDocument vfs = new CountingVfsProjectDocument(); - final CountingPrometeuLspService lsp = new CountingPrometeuLspService(vfs); final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); final StudioProjectSession session = new StudioProjectSession( projectReference(), - lsp, - vfs, stateService, ProjectLocalStudioSetup.defaults(), new StudioExecutionSessionService(), + null, ProjectLocalStudioState.defaults()); final ProjectLocalStudioState nextState = ProjectLocalStudioState.defaults() - .withOpenShellState(new ProjectLocalStudioState.OpenShellState("EDITOR")); + .withOpenShellState(new ProjectLocalStudioState.OpenShellState("ASSETS")); session.replaceProjectLocalStudioState(nextState); session.close(); @@ -70,6 +53,24 @@ final class StudioProjectSessionTest { assertSame(nextState, stateService.savedState); } + @Test + void closeShutsDownLspServerOnlyOnce() { + final RecordingProjectLocalStudioStateService stateService = new RecordingProjectLocalStudioStateService(); + final RecordingLspServerLifecycle lspServerLifecycle = new RecordingLspServerLifecycle(); + final StudioProjectSession session = new StudioProjectSession( + projectReference(), + stateService, + ProjectLocalStudioSetup.defaults(), + new StudioExecutionSessionService(), + lspServerLifecycle, + ProjectLocalStudioState.defaults()); + + session.close(); + session.close(); + + assertEquals(1, lspServerLifecycle.shutdownCalls); + } + private ProjectReference projectReference() { return new ProjectReference("Example", "1.0.0", "pbs", 1, Path.of("/tmp/example")); } @@ -87,76 +88,17 @@ final class StudioProjectSessionTest { } } - private static final class CountingVfsProjectDocument implements VfsProjectDocument { - private int closeCalls; + private static final class RecordingLspServerLifecycle implements LspServerLifecycle { + private int shutdownCalls; @Override - public VfsProjectContext projectContext() { + public void bootServer(LspServerConfiguration serverConfiguration, LspProjectContext context) { throw new UnsupportedOperationException(); } @Override - public VfsProjectSnapshot snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectSnapshot refresh(VfsRefreshRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public VfsDocumentOpenResult openDocument(Path path) { - throw new UnsupportedOperationException(); - } - - @Override - public void close() { - closeCalls++; - } - } - - private static final class CountingPrometeuLspService implements LspService { - private final VfsProjectDocument vfsProjectDocument; - private int closeCalls; - - private CountingPrometeuLspService(final VfsProjectDocument vfsProjectDocument) { - this.vfsProjectDocument = vfsProjectDocument; - } - - @Override - public LspProjectContext projectContext() { - throw new UnsupportedOperationException(); - } - - @Override - public VfsProjectDocument projectDocumentVfs() { - return vfsProjectDocument; - } - - @Override - public LspSessionStateDTO snapshot() { - throw new UnsupportedOperationException(); - } - - @Override - public LspAnalyzeDocumentResult analyzeDocument(final LspAnalyzeDocumentRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public LspDefinitionResult definition(final LspDefinitionRequest request) { - throw new UnsupportedOperationException(); - } - - @Override - public void close() { - closeCalls++; + public void shutdownServer(final LspProjectContext project) { + shutdownCalls++; } } } diff --git a/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java b/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java index 259b2f85..023b5d90 100644 --- a/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java +++ b/prometeu-studio/src/test/java/p/studio/projectstate/ProjectLocalStudioStateServiceTest.java @@ -23,22 +23,8 @@ final class ProjectLocalStudioStateServiceTest { final ProjectLocalStudioState state = new ProjectLocalStudioState( ProjectLocalStudioState.CURRENT_VERSION, new ProjectLocalStudioState.ShellLayout( - new ProjectLocalStudioState.EditorLayoutState( - List.of(0.25d), - List.of(0.80d), - List.of(0.70d), - new ProjectLocalStudioState.DockPanelState(true, 0.80d), - new ProjectLocalStudioState.DockPanelState(false, 0.70d), - List.of( - project.rootPath().resolve("src").toString(), - project.rootPath().resolve("src/game").toString())), new ProjectLocalStudioState.AssetsLayoutState(List.of(0.42d))), - new ProjectLocalStudioState.OpenShellState("EDITOR"), - new ProjectLocalStudioState.EditorRestorationState( - List.of( - project.rootPath().resolve("src/main.pbs").toString(), - project.rootPath().resolve("README.md").toString()), - project.rootPath().resolve("src/main.pbs").toString())); + new ProjectLocalStudioState.OpenShellState("ASSETS")); service.save(project, state); @@ -72,6 +58,43 @@ final class ProjectLocalStudioStateServiceTest { assertEquals(ProjectLocalStudioState.defaults(), service.load(project)); } + @Test + void legacyEditorStateLoadsWithAssetsFallbackAndIgnoresEditorFields() throws Exception { + final ProjectLocalStudioStateService service = new ProjectLocalStudioStateService(); + final ProjectReference project = project("main"); + Files.createDirectories(ProjectStudioPaths.studioRoot(project)); + Files.writeString( + ProjectStudioPaths.statePath(project), + """ + { + "version": 1, + "shellLayout": { + "editorLayout": { + "contentSplitDividers": [0.2, 0.8] + }, + "assetsLayout": { + "contentSplitDividers": [0.42] + } + }, + "openShellState": { + "selectedWorkspaceId": "EDITOR" + }, + "editorRestoration": { + "openTabPaths": ["src/main.pbs"], + "activeTabPath": "src/main.pbs" + } + } + """); + + assertEquals( + new ProjectLocalStudioState( + ProjectLocalStudioState.CURRENT_VERSION, + new ProjectLocalStudioState.ShellLayout( + new ProjectLocalStudioState.AssetsLayoutState(List.of(0.42d))), + new ProjectLocalStudioState.OpenShellState("ASSETS")), + service.load(project)); + } + private ProjectReference project(final String name) { final Path projectRoot = tempDir.resolve(name); return new ProjectReference("Main", "1.0.0", "pbs", 1, projectRoot); diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java deleted file mode 100644 index 26b9d405..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentHighlightingRouterTest.java +++ /dev/null @@ -1,315 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspHighlightSpanDTO; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspSessionStateDTO; -import p.studio.lsp.messages.LspAnalyzeDocumentResult; -import p.studio.vfs.messages.VfsDocumentAccessMode; - -import java.nio.file.Path; -import java.util.Collection; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class EditorDocumentHighlightingRouterTest { - @Test - void frontendDocumentsUseLspOwnedHighlightsWhenSemanticSpansExist() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void {}", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-keyword", "pbs-function"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of( - new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword"), - new LspHighlightSpanDTO(new LspRangeDTO(3, 7), "pbs-function")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); - } - - @Test - void nonFrontendDocumentsStayOnLocalHighlighting() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/prometeu.json"), - "prometeu.json", - "json", - "{\n \"name\": \"Example\"\n}\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO(List.of(), List.of()), - List.of(), - List.of(new LspHighlightSpanDTO(new LspRangeDTO(0, 1), "fe-punctuation")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("json"), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - } - - @Test - void frontendDocumentsFallbackToLocalPbsSyntaxWhenSemanticPresentationResourcesAreUnavailable() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void {}", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-keyword", "pbs-function"), - List.of("/themes/pbs/missing.css")), - List.of(), - List.of( - new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword"), - new LspHighlightSpanDTO(new LspRangeDTO(3, 7), "pbs-function")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); - } - - @Test - void frontendServiceHighlightsProjectToDedicatedServiceCssClass() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "declare service Game {}", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-service"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of(new LspHighlightSpanDTO(new LspRangeDTO(16, 20), "pbs-service")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); - } - - @Test - void frontendDocumentsKeepLocalSyntaxForRangesWithoutSemanticCoverage() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void { Game.tick(1); }", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-keyword", "pbs-service", "pbs-function"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of( - new LspHighlightSpanDTO(new LspRangeDTO(0, 2), "pbs-keyword"), - new LspHighlightSpanDTO(new LspRangeDTO(3, 7), "pbs-function"), - new LspHighlightSpanDTO(new LspRangeDTO(19, 23), "pbs-service")), - List.of(), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LSP, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-keyword")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-service")); - assertTrue(containsStyle(result.styleSpans(), "editor-semantic-pbs-function")); - } - - @Test - void frontendDocumentsPreserveTransportedInlineHintsEvenWithoutSemanticHighlightCoverage() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/src/main.pbs"), - "main.pbs", - "pbs", - "fn main() -> void { let value = 1; }", - "LF", - true, - VfsDocumentAccessMode.READ_ONLY, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO( - List.of("pbs-function"), - List.of("/themes/pbs/semantic-highlighting.css")), - List.of(), - List.of(), - List.of(new p.studio.lsp.dtos.LspInlineHintDTO(new LspRangeDTO(24, 29), "int", "type")), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("pbs", analysis.semanticPresentation()), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertEquals(1, result.inlineHints().size()); - assertEquals("int", result.inlineHints().getFirst().label()); - } - - @Test - void nonFrontendDocumentsDiscardTransportedInlineHints() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer fileBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/prometeu.json"), - "prometeu.json", - "json", - "{\n \"name\": \"Example\"\n}\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - - final LspAnalyzeDocumentResult analysis = new LspAnalyzeDocumentResult( - new LspSessionStateDTO(true, List.of("highlight")), - new LspSemanticPresentationDTO(List.of(), List.of()), - List.of(), - List.of(), - List.of(new p.studio.lsp.dtos.LspInlineHintDTO(new LspRangeDTO(0, 1), "ignored", "type")), - List.of(), - List.of(), - List.of()); - - final EditorDocumentHighlightingResult result = EditorDocumentHighlightingRouter.route( - fileBuffer, - registry.resolve("json"), - analysis); - - assertEquals(EditorDocumentHighlightOwner.LOCAL, result.owner()); - assertTrue(result.inlineHints().isEmpty()); - } - - @Test - void bashBuiltinTestOnlyHighlightsInCommandPosition() { - final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - final EditorOpenFileBuffer commandBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/run.sh"), - "run.sh", - "bash", - "test -f ./file\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - final EditorOpenFileBuffer plainTextBuffer = new EditorOpenFileBuffer( - Path.of("/tmp/example/run.sh"), - "run.sh", - "bash", - "echo \"$test\"\n", - "LF", - false, - VfsDocumentAccessMode.EDITABLE, - false); - - final EditorDocumentHighlightingResult commandResult = EditorDocumentHighlightingRouter.route( - commandBuffer, - registry.resolve("bash"), - null); - final EditorDocumentHighlightingResult plainTextResult = EditorDocumentHighlightingRouter.route( - plainTextBuffer, - registry.resolve("bash"), - null); - - assertTrue(containsStyle(commandResult.styleSpans(), "editor-syntax-bash-builtin")); - assertFalse(containsStyle(plainTextResult.styleSpans(), "editor-syntax-bash-builtin")); - } - - private boolean containsStyle( - final org.fxmisc.richtext.model.StyleSpans> styleSpans, - final String styleClass) { - for (int index = 0; index < styleSpans.getSpanCount(); index++) { - if (styleSpans.getStyleSpan(index).getStyle().contains(styleClass)) { - return true; - } - } - return false; - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java deleted file mode 100644 index 13450c3b..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentPresentationRegistryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspSemanticPresentationDTO; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class EditorDocumentPresentationRegistryTest { - private final EditorDocumentPresentationRegistry registry = new EditorDocumentPresentationRegistry(); - - @Test - void resolvesFrontendTypeIdsToFrontendPresentation() { - final EditorDocumentPresentation presentation = registry.resolve( - "pbs", - new LspSemanticPresentationDTO( - java.util.List.of("pbs-keyword", "pbs-service"), - java.util.List.of("/themes/pbs/semantic-highlighting.css"))); - - assertEquals("pbs", presentation.styleKey()); - assertEquals(java.util.List.of("pbs-keyword", "pbs-service"), presentation.semanticKeys()); - assertEquals(1, presentation.stylesheetUrls().size()); - assertTrue(presentation.stylesheetUrls().getFirst().endsWith("/themes/pbs/semantic-highlighting.css")); - } - - @Test - void missingFrontendResourcesFallbackToFrontendOwnedPresentationResources() { - final EditorDocumentPresentation presentation = registry.resolve( - "pbs", - new LspSemanticPresentationDTO( - java.util.List.of("pbs-keyword"), - java.util.List.of("/themes/pbs/missing.css"))); - - assertEquals("pbs", presentation.styleKey()); - assertEquals(java.util.List.of("pbs-keyword"), presentation.semanticKeys()); - assertEquals(1, presentation.stylesheetUrls().size()); - assertTrue(presentation.highlight("fn main() -> void {}").getStyleSpan(0).getStyle().isEmpty()); - } - - @Test - void resolvesJsonTypeIdsToJsonPresentation() { - final EditorDocumentPresentation presentation = registry.resolve("json"); - - assertEquals("json", presentation.styleKey()); - assertEquals(1, presentation.stylesheetUrls().size()); - } - - @Test - void resolvesBashTypeIdsToBashPresentation() { - final EditorDocumentPresentation presentation = registry.resolve("bash"); - - assertEquals("bash", presentation.styleKey()); - assertEquals(1, presentation.stylesheetUrls().size()); - } - - @Test - void fallsBackToTextPresentationForUnknownTypeIds() { - assertEquals("text", registry.resolve("markdown").styleKey()); - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java deleted file mode 100644 index b5efe311..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorDocumentScopeGuideModelTest.java +++ /dev/null @@ -1,166 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspRangeDTO; -import p.studio.lsp.dtos.LspStructuralAnchorDTO; -import p.studio.lsp.dtos.LspSymbolDTO; -import p.studio.lsp.messages.LspSymbolKind; - -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -final class EditorDocumentScopeGuideModelTest { - @Test - void selectsActiveScopeAsSmallestContainingRange() { - final String content = """ - struct Hero { - fn attack() { - let value = 1 - } - } - - enum Kind { - Unit - } - """; - final LspSymbolDTO attack = symbol(content, "fn attack()", "}", LspSymbolKind.METHOD, List.of()); - final LspSymbolDTO hero = new LspSymbolDTO( - "struct Hero", - LspSymbolKind.STRUCT, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO( - content.indexOf("struct Hero"), - content.indexOf("\n\nenum Kind")), - List.of(attack)); - final LspSymbolDTO kind = symbol(content, "enum Kind", "}\n", LspSymbolKind.ENUM, List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(hero, kind)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("let value")); - - assertEquals(content.indexOf("fn attack()"), guides.activeScope().startOffset()); - assertEquals(content.indexOf("struct Hero"), guides.activeContainer().startOffset()); - } - - @Test - void omitsActiveContainerWhenScopeHasNoParent() { - final String content = """ - struct Hero { - hp: Int - } - """; - final LspSymbolDTO symbol = new LspSymbolDTO( - "struct Hero", - LspSymbolKind.STRUCT, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(0, content.length()), - List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(symbol)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("hp")); - - assertEquals(content.indexOf("struct Hero"), guides.activeScope().startOffset()); - assertNull(guides.activeContainer()); - } - - @Test - void exposesAtMostTwoActiveSegmentsPerLine() { - final String content = """ - struct Hero { - fn attack() { - let value = 1 - } - } - """; - final LspSymbolDTO attack = symbol(content, "fn attack()", "}", LspSymbolKind.METHOD, List.of()); - final LspSymbolDTO hero = new LspSymbolDTO( - "struct Hero", - LspSymbolKind.STRUCT, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(0, content.length()), - List.of(attack)); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(hero)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("let value")); - - assertSegments(model, 1, guides, "ACTIVE_CONTAINER:CONTINUE", "ACTIVE_SCOPE:START"); - assertSegments(model, 2, guides, "ACTIVE_CONTAINER:CONTINUE", "ACTIVE_SCOPE:CONTINUE"); - assertSegments(model, 3, guides, "ACTIVE_CONTAINER:CONTINUE", "ACTIVE_SCOPE:END"); - } - - @Test - void ignoresSingleLineAndUnknownSymbols() { - final String content = """ - if ready { - value() - } - """; - final LspSymbolDTO symbol = new LspSymbolDTO( - "if ready", - LspSymbolKind.UNKNOWN, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(0, content.length()), - List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.from(content, List.of(symbol)); - - assertEquals(EditorDocumentScopeGuideModel.ActiveGuides.empty(), model.resolveActiveGuides(content.indexOf("value"))); - } - - @Test - void structuralAnchorsDriveGuideStartAndEndLines() { - final String content = """ - fn main( - value: int - ) -> void - { - helper(); - } - """; - final LspStructuralAnchorDTO anchor = new LspStructuralAnchorDTO( - new LspRangeDTO(content.indexOf("fn main"), content.length()), - new LspRangeDTO(content.indexOf("{"), content.indexOf("{") + 1), - new LspRangeDTO(content.indexOf("}"), content.indexOf("}") + 1), - List.of()); - - final EditorDocumentScopeGuideModel model = EditorDocumentScopeGuideModel.fromStructuralAnchors(content, List.of(anchor)); - final EditorDocumentScopeGuideModel.ActiveGuides guides = model.resolveActiveGuides(content.indexOf("helper")); - - assertSegments(model, 0, guides); - assertSegments(model, 1, guides); - assertSegments(model, 2, guides); - assertSegments(model, 3, guides, "ACTIVE_SCOPE:START"); - assertSegments(model, 4, guides, "ACTIVE_SCOPE:CONTINUE"); - assertSegments(model, 5, guides, "ACTIVE_SCOPE:END"); - } - - private static LspSymbolDTO symbol( - final String content, - final String startMarker, - final String endMarker, - final LspSymbolKind kind, - final List children) { - final int start = content.indexOf(startMarker); - final int end = content.indexOf(endMarker, start) + endMarker.length(); - return new LspSymbolDTO( - startMarker, - kind, - Path.of("/tmp/example/main.pbs"), - new LspRangeDTO(start, end), - children); - } - - private static void assertSegments( - final EditorDocumentScopeGuideModel model, - final int line, - final EditorDocumentScopeGuideModel.ActiveGuides guides, - final String... expectedKinds) { - assertEquals( - List.of(expectedKinds), - model.segmentsForLine(line, guides).stream() - .map(segment -> segment.role().name() + ":" + segment.kind().name()) - .toList()); - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorInlineHintLayoutTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorInlineHintLayoutTest.java deleted file mode 100644 index 7bcc9c23..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorInlineHintLayoutTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.lsp.dtos.LspInlineHintDTO; -import p.studio.lsp.dtos.LspRangeDTO; - -import java.util.List; -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -final class EditorInlineHintLayoutTest { - @Test - void layoutCreatesDecorativePlacementsForHintsWithResolvedAnchors() { - final var hint = new LspInlineHintDTO(new LspRangeDTO(10, 15), "int", "type"); - - final var placements = EditorInlineHintLayout.layout( - List.of(hint), - ignored -> Optional.of(new EditorInlineHintLayout.AnchorBounds(20.0d, 8.0d, 40.0d, 16.0d))); - - assertEquals(1, placements.size()); - assertEquals(": int", placements.getFirst().displayText()); - assertEquals("editor-inline-hint-type", placements.getFirst().categoryStyleClass()); - assertEquals(46.0d, placements.getFirst().screenX()); - assertEquals(8.0d, placements.getFirst().screenY()); - assertEquals(16.0d, placements.getFirst().anchorHeight()); - } - - @Test - void layoutKeepsValidHintsWhenOnlySomeAnchorsResolve() { - final var stable = new LspInlineHintDTO(new LspRangeDTO(10, 15), "int", "type"); - final var missing = new LspInlineHintDTO(new LspRangeDTO(30, 35), "Player", "type"); - - final var placements = EditorInlineHintLayout.layout( - List.of(stable, missing), - hint -> hint == stable - ? Optional.of(new EditorInlineHintLayout.AnchorBounds(12.0d, 4.0d, 28.0d, 14.0d)) - : Optional.empty()); - - assertEquals(1, placements.size()); - assertEquals(stable, placements.getFirst().inlineHint()); - assertEquals(": int", placements.getFirst().displayText()); - } -} diff --git a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorOpenFileSessionTest.java b/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorOpenFileSessionTest.java deleted file mode 100644 index c793092e..00000000 --- a/prometeu-studio/src/test/java/p/studio/workspaces/editor/EditorOpenFileSessionTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package p.studio.workspaces.editor; - -import org.junit.jupiter.api.Test; -import p.studio.vfs.messages.VfsDocumentAccessMode; - -import java.nio.file.Path; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -final class EditorOpenFileSessionTest { - - @Test - void openAddsNewFileAndMarksItActive() { - final var session = new EditorOpenFileSession(); - final var file = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "fn main(): void\n"); - - session.open(file); - - assertEquals(1, session.openFiles().size()); - assertEquals(file.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void openDoesNotDuplicateExistingTab() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b"); - - session.open(first); - session.open(second); - - assertEquals(1, session.openFiles().size()); - assertEquals(first.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - assertEquals("b", session.activeFile().orElseThrow().content()); - } - - @Test - void activateSwitchesTheActiveTabWithinTheCurrentSession() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("src/other.pbs"), "other.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "b"); - - session.open(first); - session.open(second); - session.activate(first.path()); - - assertEquals(first.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void reportsSaveStateForDirtyEditableFilesOnly() { - final var session = new EditorOpenFileSession(); - session.open(fileBuffer(Path.of("notes.txt"), "notes.txt", VfsDocumentAccessMode.EDITABLE, true, false, "alpha")); - - assertTrue(session.hasSaveableActiveFile()); - assertTrue(session.hasDirtyEditableFiles()); - } - - @Test - void frontendEditableFilesParticipateInSaveStateWhenDirty() { - final var session = new EditorOpenFileSession(); - session.open(fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, true, true, "fn main(): void")); - - assertTrue(session.hasSaveableActiveFile()); - assertTrue(session.hasDirtyEditableFiles()); - assertTrue(session.activeFile().orElseThrow().frontendDocument()); - assertTrue(session.activeFile().orElseThrow().editable()); - } - - @Test - void closeRemovesInactiveTabWithoutChangingTheActiveTab() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("README.md"), "README.md", VfsDocumentAccessMode.EDITABLE, false, false, "b"); - - session.open(first); - session.open(second); - session.close(first.path()); - - assertEquals(1, session.openFiles().size()); - assertEquals(second.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void closePromotesTheNextTabWhenTheActiveTabIsClosed() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("README.md"), "README.md", VfsDocumentAccessMode.EDITABLE, false, false, "b"); - final var third = fileBuffer(Path.of("notes.txt"), "notes.txt", VfsDocumentAccessMode.EDITABLE, false, false, "c"); - - session.open(first); - session.open(second); - session.open(third); - session.activate(second.path()); - session.close(second.path()); - - assertEquals(List.of( - first.path().toAbsolutePath().normalize(), - third.path().toAbsolutePath().normalize()), - session.openFiles().stream().map(EditorOpenFileBuffer::path).toList()); - assertEquals(third.path().toAbsolutePath().normalize(), session.activeFile().orElseThrow().path()); - } - - @Test - void exportsRestorationStateFromOpenTabsAndActiveTab() { - final var session = new EditorOpenFileSession(); - final var first = fileBuffer(Path.of("src/main.pbs"), "main.pbs", VfsDocumentAccessMode.EDITABLE, false, true, "a"); - final var second = fileBuffer(Path.of("README.md"), "README.md", VfsDocumentAccessMode.EDITABLE, false, false, "b"); - - session.open(first); - session.open(second); - session.activate(first.path()); - - final var restorationState = session.restorationState(); - - assertEquals(List.of( - first.path().toAbsolutePath().normalize().toString(), - second.path().toAbsolutePath().normalize().toString()), restorationState.openTabPaths()); - assertEquals(first.path().toAbsolutePath().normalize().toString(), restorationState.activeTabPath()); - } - - private EditorOpenFileBuffer fileBuffer( - final Path path, - final String tabLabel, - final VfsDocumentAccessMode accessMode, - final boolean dirty, - final boolean frontendDocument, - final String content) { - return new EditorOpenFileBuffer( - path, - tabLabel, - frontendDocument ? "pbs" : "text", - content, - "LF", - frontendDocument, - accessMode, - dirty); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/build.gradle.kts b/prometeu-vfs/prometeu-vfs-api/build.gradle.kts deleted file mode 100644 index a6775d6e..00000000 --- a/prometeu-vfs/prometeu-vfs-api/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("gradle.java-library-conventions") -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/ProjectDocumentVfsFactory.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/ProjectDocumentVfsFactory.java deleted file mode 100644 index fe309951..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/ProjectDocumentVfsFactory.java +++ /dev/null @@ -1,7 +0,0 @@ -package p.studio.vfs; - -import p.studio.vfs.messages.VfsProjectContext; - -public interface ProjectDocumentVfsFactory { - VfsProjectDocument open(VfsProjectContext projectContext); -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/VfsDocumentOpenResult.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/VfsDocumentOpenResult.java deleted file mode 100644 index 4eee7b40..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/VfsDocumentOpenResult.java +++ /dev/null @@ -1,40 +0,0 @@ -package p.studio.vfs; - -import p.studio.vfs.messages.VfsDocumentAccessContext; -import p.studio.vfs.messages.VfsUnsupportedReason; - -import java.nio.file.Path; -import java.util.Objects; - -public sealed interface VfsDocumentOpenResult permits VfsDocumentOpenResult.VfsUnsupportedDocument, VfsDocumentOpenResult.VfsTextDocument { - Path path(); - - record VfsUnsupportedDocument( - Path path, - VfsUnsupportedReason reason) implements VfsDocumentOpenResult { - - public VfsUnsupportedDocument { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - Objects.requireNonNull(reason, "reason"); - } - } - - record VfsTextDocument( - Path path, - String documentName, - String typeId, - String content, - String lineSeparator, - boolean dirty, - VfsDocumentAccessContext accessContext) implements VfsDocumentOpenResult { - - public VfsTextDocument { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - Objects.requireNonNull(documentName, "documentName"); - Objects.requireNonNull(typeId, "typeId"); - Objects.requireNonNull(content, "content"); - Objects.requireNonNull(lineSeparator, "lineSeparator"); - Objects.requireNonNull(accessContext, "accessContext"); - } - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/VfsProjectDocument.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/VfsProjectDocument.java deleted file mode 100644 index 73c5fdbb..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/VfsProjectDocument.java +++ /dev/null @@ -1,47 +0,0 @@ -package p.studio.vfs; - -import p.studio.vfs.messages.*; - -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -public interface VfsProjectDocument extends AutoCloseable { - VfsProjectContext projectContext(); - - VfsProjectSnapshot snapshot(); - - VfsProjectSnapshot refresh(); - - VfsProjectSnapshot refresh(VfsRefreshRequest request); - - VfsDocumentOpenResult openDocument(Path path); - - default VfsDocumentAccessContext accessContext(final Path path) { - throw new UnsupportedOperationException("Document access context is not supported by this VFS implementation."); - } - - default VfsDocumentAccessContext updateAccessContext(final Path path, final Map attributes) { - throw new UnsupportedOperationException("Document access context updates are not supported by this VFS implementation."); - } - - default VfsDocumentOpenResult.VfsTextDocument updateDocument(final Path path, final String content) { - throw new UnsupportedOperationException("Document mutation is not supported by this VFS implementation."); - } - - default VfsDocumentSaveResult saveDocument(final Path path) { - throw new UnsupportedOperationException("Document save is not supported by this VFS implementation."); - } - - default VfsDocumentOpenResult.VfsTextDocument discardDocument(final Path path) { - throw new UnsupportedOperationException("Document discard is not supported by this VFS implementation."); - } - - default List saveAllDocuments() { - return List.of(); - } - - @Override - default void close() { - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentAccessContext.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentAccessContext.java deleted file mode 100644 index 40fb7cdb..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentAccessContext.java +++ /dev/null @@ -1,20 +0,0 @@ -package p.studio.vfs.messages; - -import java.nio.file.Path; -import java.util.Map; -import java.util.Objects; - -public record VfsDocumentAccessContext( - Path path, - String typeId, - boolean frontendDocument, - VfsDocumentAccessMode accessMode, - Map attributes) { - - public VfsDocumentAccessContext { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - Objects.requireNonNull(typeId, "typeId"); - Objects.requireNonNull(accessMode, "accessMode"); - attributes = Map.copyOf(Objects.requireNonNull(attributes, "attributes")); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentAccessMode.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentAccessMode.java deleted file mode 100644 index 1ebbc417..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentAccessMode.java +++ /dev/null @@ -1,6 +0,0 @@ -package p.studio.vfs.messages; - -public enum VfsDocumentAccessMode { - READ_ONLY, - EDITABLE -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentSaveResult.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentSaveResult.java deleted file mode 100644 index 5ac04114..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentSaveResult.java +++ /dev/null @@ -1,16 +0,0 @@ -package p.studio.vfs.messages; - -import java.nio.file.Path; -import java.util.Objects; - -public record VfsDocumentSaveResult( - Path path, - String typeId, - VfsDocumentSaveStatus status) { - - public VfsDocumentSaveResult { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - Objects.requireNonNull(typeId, "typeId"); - Objects.requireNonNull(status, "status"); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentSaveStatus.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentSaveStatus.java deleted file mode 100644 index 2a8ad4d4..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentSaveStatus.java +++ /dev/null @@ -1,7 +0,0 @@ -package p.studio.vfs.messages; - -public enum VfsDocumentSaveStatus { - SAVED, - NO_CHANGES, - READ_ONLY -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentTypeIds.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentTypeIds.java deleted file mode 100644 index f664d59a..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsDocumentTypeIds.java +++ /dev/null @@ -1,11 +0,0 @@ -package p.studio.vfs.messages; - -public final class VfsDocumentTypeIds { - public static final String TEXT = "text"; - public static final String JSON = "json"; - public static final String NDJSON = "ndjson"; - public static final String BASH = "bash"; - - private VfsDocumentTypeIds() { - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectContext.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectContext.java deleted file mode 100644 index 81ecd2fb..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectContext.java +++ /dev/null @@ -1,16 +0,0 @@ -package p.studio.vfs.messages; - -import java.nio.file.Path; -import java.util.Objects; - -public record VfsProjectContext( - String projectName, - String languageId, - Path rootPath) { - - public VfsProjectContext { - Objects.requireNonNull(projectName, "projectName"); - Objects.requireNonNull(languageId, "languageId"); - rootPath = Objects.requireNonNull(rootPath, "rootPath").toAbsolutePath().normalize(); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectNode.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectNode.java deleted file mode 100644 index 271bdadc..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectNode.java +++ /dev/null @@ -1,19 +0,0 @@ -package p.studio.vfs.messages; - -import java.nio.file.Path; -import java.util.List; -import java.util.Objects; - -public record VfsProjectNode( - Path path, - String displayName, - boolean directory, - boolean taggedSourceRoot, - List children) { - - public VfsProjectNode { - path = Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - Objects.requireNonNull(displayName, "displayName"); - children = List.copyOf(Objects.requireNonNull(children, "children")); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectSnapshot.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectSnapshot.java deleted file mode 100644 index 7995d13b..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsProjectSnapshot.java +++ /dev/null @@ -1,14 +0,0 @@ -package p.studio.vfs.messages; - -import java.nio.file.Path; -import java.util.Objects; - -public record VfsProjectSnapshot( - Path projectRoot, - VfsProjectNode rootNode) { - - public VfsProjectSnapshot { - projectRoot = Objects.requireNonNull(projectRoot, "projectRoot").toAbsolutePath().normalize(); - Objects.requireNonNull(rootNode, "rootNode"); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsRefreshRequest.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsRefreshRequest.java deleted file mode 100644 index 955c0cf4..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsRefreshRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package p.studio.vfs.messages; - -import java.nio.file.Path; -import java.util.Objects; - -public record VfsRefreshRequest(Path targetPath) { - public VfsRefreshRequest { - targetPath = Objects.requireNonNull(targetPath, "targetPath").toAbsolutePath().normalize(); - } -} diff --git a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsUnsupportedReason.java b/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsUnsupportedReason.java deleted file mode 100644 index beeee78a..00000000 --- a/prometeu-vfs/prometeu-vfs-api/src/main/java/p/studio/vfs/messages/VfsUnsupportedReason.java +++ /dev/null @@ -1,10 +0,0 @@ -package p.studio.vfs.messages; - -public enum VfsUnsupportedReason { - OUTSIDE_PROJECT, - NOT_FOUND, - NOT_A_FILE, - NO_HANDLER, - BINARY_CONTENT, - INVALID_UTF8 -} diff --git a/prometeu-vfs/prometeu-vfs-v1/build.gradle.kts b/prometeu-vfs/prometeu-vfs-v1/build.gradle.kts deleted file mode 100644 index 4a0f4a96..00000000 --- a/prometeu-vfs/prometeu-vfs-v1/build.gradle.kts +++ /dev/null @@ -1,9 +0,0 @@ -plugins { - id("gradle.java-library-conventions") -} - -dependencies { - implementation(project(":prometeu-vfs:prometeu-vfs-api")) - implementation(project(":prometeu-compiler:prometeu-compiler-core")) - implementation(project(":prometeu-compiler:prometeu-frontend-registry")) -} diff --git a/prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemProjectDocumentVfsFactory.java b/prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemProjectDocumentVfsFactory.java deleted file mode 100644 index a60f52ed..00000000 --- a/prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemProjectDocumentVfsFactory.java +++ /dev/null @@ -1,12 +0,0 @@ -package p.studio.vfs; - -import p.studio.vfs.messages.VfsProjectContext; - -import java.util.Objects; - -public final class FilesystemProjectDocumentVfsFactory implements ProjectDocumentVfsFactory { - @Override - public VfsProjectDocument open(final VfsProjectContext projectContext) { - return new FilesystemVfsProjectDocument(Objects.requireNonNull(projectContext, "projectContext")); - } -} diff --git a/prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemVfsProjectDocument.java b/prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemVfsProjectDocument.java deleted file mode 100644 index feac1f57..00000000 --- a/prometeu-vfs/prometeu-vfs-v1/src/main/java/p/studio/vfs/FilesystemVfsProjectDocument.java +++ /dev/null @@ -1,466 +0,0 @@ -package p.studio.vfs; - -import p.studio.compiler.FrontendRegistryService; -import p.studio.compiler.models.FrontendSpec; -import p.studio.vfs.messages.*; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.nio.ByteBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.CodingErrorAction; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -final class FilesystemVfsProjectDocument implements VfsProjectDocument { - private final VfsProjectContext projectContext; - private final Map editableSnapshots = new LinkedHashMap<>(); - private final Map> accessContextAttributes = new HashMap<>(); - private VfsProjectSnapshot snapshot; - - FilesystemVfsProjectDocument(final VfsProjectContext projectContext) { - this.projectContext = Objects.requireNonNull(projectContext, "projectContext"); - this.snapshot = buildSnapshot(); - } - - @Override - public VfsProjectContext projectContext() { - return projectContext; - } - - @Override - public VfsProjectSnapshot snapshot() { - return snapshot; - } - - @Override - public VfsProjectSnapshot refresh() { - snapshot = buildSnapshot(); - return snapshot; - } - - @Override - public VfsProjectSnapshot refresh(final VfsRefreshRequest request) { - final Path refreshTarget = refreshRootFor(Objects.requireNonNull(request, "request").targetPath()); - if (refreshTarget.equals(projectContext.rootPath())) { - return refresh(); - } - - final Set taggedRoots = taggedSourceRoots(); - final String displayName = refreshTarget.getFileName() == null - ? projectContext.projectName() - : refreshTarget.getFileName().toString(); - final VfsProjectNode refreshedNode = buildNode(refreshTarget, displayName, taggedRoots); - snapshot = new VfsProjectSnapshot( - projectContext.rootPath(), - replaceNode(snapshot.rootNode(), refreshedNode)); - return snapshot; - } - - @Override - public VfsDocumentOpenResult openDocument(final Path path) { - try { - final var supportedDocument = requireSupportedDocument(path); - return openSupportedDocument(supportedDocument); - } catch (UnsupportedDocumentException unsupportedDocumentException) { - return new VfsDocumentOpenResult.VfsUnsupportedDocument( - unsupportedDocumentException.path(), - unsupportedDocumentException.reason()); - } - } - - @Override - public VfsDocumentAccessContext accessContext(final Path path) { - return accessContextFor(requireSupportedDocument(path)); - } - - @Override - public VfsDocumentAccessContext updateAccessContext(final Path path, final Map attributes) { - final var supportedDocument = requireSupportedDocument(path); - accessContextAttributes.put(supportedDocument.path(), Map.copyOf(Objects.requireNonNull(attributes, "attributes"))); - return accessContextFor(supportedDocument); - } - - @Override - public VfsDocumentOpenResult.VfsTextDocument updateDocument(final Path path, final String content) { - final var supportedDocument = requireSupportedDocument(path); - if (supportedDocument.accessMode() != VfsDocumentAccessMode.EDITABLE) { - throw new IllegalStateException("Document is hard read-only: " + supportedDocument.path()); - } - final var normalizedPath = supportedDocument.path(); - final var snapshot = editableSnapshots.computeIfAbsent( - normalizedPath, - ignored -> new EditableDocumentSnapshot( - supportedDocument.content(), - supportedDocument.content(), - supportedDocument.lineSeparator())); - snapshot.content = Objects.requireNonNull(content, "content"); - snapshot.lineSeparator = lineSeparator(snapshot.content); - return toVfsTextDocument(supportedDocument.withContent(snapshot.content, snapshot.lineSeparator), snapshot.isDirty(), accessContextFor(supportedDocument)); - } - - @Override - public VfsDocumentSaveResult saveDocument(final Path path) { - final var supportedDocument = requireSupportedDocument(path); - final var accessContext = accessContextFor(supportedDocument); - if (accessContext.accessMode() != VfsDocumentAccessMode.EDITABLE) { - return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.READ_ONLY); - } - final var snapshot = editableSnapshots.computeIfAbsent( - supportedDocument.path(), - ignored -> new EditableDocumentSnapshot( - supportedDocument.content(), - supportedDocument.content(), - supportedDocument.lineSeparator())); - if (!snapshot.isDirty()) { - return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.NO_CHANGES); - } - writeUtf8(supportedDocument.path(), snapshot.content); - snapshot.savedContent = snapshot.content; - snapshot.lineSeparator = lineSeparator(snapshot.content); - return new VfsDocumentSaveResult(supportedDocument.path(), supportedDocument.typeId(), VfsDocumentSaveStatus.SAVED); - } - - @Override - public VfsDocumentOpenResult.VfsTextDocument discardDocument(final Path path) { - final var supportedDocument = requireSupportedDocument(path); - editableSnapshots.remove(supportedDocument.path()); - return toVfsTextDocument(supportedDocument, false, accessContextFor(supportedDocument)); - } - - @Override - public List saveAllDocuments() { - return editableSnapshots.keySet().stream() - .map(this::saveDocument) - .toList(); - } - - private SupportedDocument requireSupportedDocument(final Path path) { - final Path normalizedPath = normalize(path); - if (!normalizedPath.startsWith(projectContext.rootPath())) { - throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.OUTSIDE_PROJECT); - } - if (!Files.exists(normalizedPath)) { - throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.NOT_FOUND); - } - if (!Files.isRegularFile(normalizedPath)) { - throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.NOT_A_FILE); - } - - final byte[] bytes = readAllBytes(normalizedPath); - if (containsNulByte(bytes)) { - throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.BINARY_CONTENT); - } - - final String content; - try { - content = StandardCharsets.UTF_8.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT) - .decode(ByteBuffer.wrap(bytes)) - .toString(); - } catch (CharacterCodingException codingException) { - throw new UnsupportedDocumentException(normalizedPath, VfsUnsupportedReason.INVALID_UTF8, codingException); - } - - final DocumentKind documentKind = documentKind(normalizedPath, content); - return new SupportedDocument( - normalizedPath, - normalizedPath.getFileName().toString(), - documentKind.typeId(), - documentKind.frontendDocument(), - documentKind.accessMode(), - content, - lineSeparator(content)); - } - - private VfsDocumentOpenResult openSupportedDocument(final SupportedDocument supportedDocument) { - final var accessContext = accessContextFor(supportedDocument); - if (supportedDocument.accessMode() == VfsDocumentAccessMode.EDITABLE) { - final var snapshot = editableSnapshots.computeIfAbsent( - supportedDocument.path(), - ignored -> new EditableDocumentSnapshot( - supportedDocument.content(), - supportedDocument.content(), - supportedDocument.lineSeparator())); - return toVfsTextDocument( - supportedDocument.withContent(snapshot.content, snapshot.lineSeparator), - snapshot.isDirty(), - accessContext); - } - return toVfsTextDocument(supportedDocument, false, accessContext); - } - - private VfsDocumentOpenResult.VfsTextDocument toVfsTextDocument( - final SupportedDocument supportedDocument, - final boolean dirty, - final VfsDocumentAccessContext accessContext) { - return new VfsDocumentOpenResult.VfsTextDocument( - supportedDocument.path(), - supportedDocument.documentName(), - supportedDocument.typeId(), - supportedDocument.content(), - supportedDocument.lineSeparator(), - dirty, - accessContext); - } - - private VfsProjectSnapshot buildSnapshot() { - return new VfsProjectSnapshot( - projectContext.rootPath(), - buildNode(projectContext.rootPath(), projectContext.projectName(), taggedSourceRoots())); - } - - private Set taggedSourceRoots() { - return FrontendRegistryService.getFrontendSpec(projectContext.languageId()) - .stream() - .flatMap(frontendSpec -> frontendSpec.getSourceRoots().stream()) - .map(projectContext.rootPath()::resolve) - .map(Path::toAbsolutePath) - .map(Path::normalize) - .collect(Collectors.toSet()); - } - - private DocumentKind documentKind(final Path path, final String content) { - final String extension = extensionOf(path); - if ("json".equals(extension)) { - return new DocumentKind(VfsDocumentTypeIds.JSON, false, VfsDocumentAccessMode.EDITABLE); - } - if ("ndjson".equals(extension)) { - return new DocumentKind(VfsDocumentTypeIds.NDJSON, false, VfsDocumentAccessMode.EDITABLE); - } - if (isBashDocument(path, extension, content)) { - return new DocumentKind(VfsDocumentTypeIds.BASH, false, VfsDocumentAccessMode.EDITABLE); - } - if (isFrontendSourceDocument(extension)) { - return new DocumentKind(projectContext.languageId(), true, VfsDocumentAccessMode.EDITABLE); - } - return new DocumentKind(VfsDocumentTypeIds.TEXT, false, VfsDocumentAccessMode.EDITABLE); - } - - private VfsProjectNode buildNode( - final Path path, - final String displayName, - final Set taggedRoots) { - if (!Files.isDirectory(path)) { - return new VfsProjectNode(path, displayName, false, taggedRoots.contains(path), List.of()); - } - - final List children; - try (Stream stream = Files.list(path)) { - children = stream - .sorted(nodeComparator()) - .map(child -> buildNode(child, child.getFileName().toString(), taggedRoots)) - .toList(); - } catch (IOException ioException) { - throw new UncheckedIOException(ioException); - } - - return new VfsProjectNode(path, displayName, true, taggedRoots.contains(path), children); - } - - private VfsProjectNode replaceNode(final VfsProjectNode currentNode, final VfsProjectNode replacementNode) { - if (currentNode.path().equals(replacementNode.path())) { - return replacementNode; - } - if (!replacementNode.path().startsWith(currentNode.path())) { - return currentNode; - } - if (!currentNode.directory()) { - return currentNode; - } - - final List replacedChildren = currentNode.children() - .stream() - .map(child -> replaceNode(child, replacementNode)) - .sorted(projectNodeComparator()) - .toList(); - return new VfsProjectNode( - currentNode.path(), - currentNode.displayName(), - true, - currentNode.taggedSourceRoot(), - replacedChildren); - } - - private Path refreshRootFor(final Path targetPath) { - final Path normalizedTarget = normalize(targetPath); - if (!normalizedTarget.startsWith(projectContext.rootPath())) { - throw new IllegalArgumentException("Refresh target must stay within the project root."); - } - if (Files.exists(normalizedTarget)) { - return normalizedTarget; - } - final Path parent = normalizedTarget.getParent(); - if (parent == null || !parent.startsWith(projectContext.rootPath())) { - return projectContext.rootPath(); - } - return parent; - } - - private Comparator nodeComparator() { - return Comparator - .comparing((Path path) -> !Files.isDirectory(path)) - .thenComparing(path -> path.getFileName().toString(), String.CASE_INSENSITIVE_ORDER); - } - - private Comparator projectNodeComparator() { - return Comparator - .comparing((VfsProjectNode node) -> !node.directory()) - .thenComparing(VfsProjectNode::displayName, String.CASE_INSENSITIVE_ORDER); - } - - private Path normalize(final Path path) { - return Objects.requireNonNull(path, "path").toAbsolutePath().normalize(); - } - - private boolean isFrontendSourceDocument(final String extension) { - if (extension.isBlank()) { - return false; - } - return FrontendRegistryService.getFrontendSpec(projectContext.languageId()) - .map(FrontendSpec::getAllowedExtensions) - .stream() - .flatMap(allowedExtensions -> allowedExtensions.stream()) - .anyMatch(allowedExtension -> allowedExtension.equalsIgnoreCase(extension)); - } - - private boolean isBashDocument(final Path path, final String extension, final String content) { - if (Set.of("sh", "bash", "bashrc", "bash_profile").contains(extension)) { - return true; - } - final String fileName = path.getFileName().toString().toLowerCase(Locale.ROOT); - if (".bashrc".equals(fileName) || ".bash_profile".equals(fileName)) { - return true; - } - final int firstLineBreak = content.indexOf('\n'); - final String firstLine = firstLineBreak >= 0 ? content.substring(0, firstLineBreak) : content; - return firstLine.startsWith("#!") && (firstLine.contains("bash") || firstLine.contains("/sh")); - } - - private String extensionOf(final Path path) { - final String fileName = path.getFileName().toString(); - final int dot = fileName.lastIndexOf('.'); - if (dot < 0 || dot == fileName.length() - 1) { - return ""; - } - return fileName.substring(dot + 1).toLowerCase(Locale.ROOT); - } - - private boolean containsNulByte(final byte[] bytes) { - for (final byte value : bytes) { - if (value == 0) { - return true; - } - } - return false; - } - - private String lineSeparator(final String content) { - return content.contains("\r\n") ? "CRLF" : "LF"; - } - - private byte[] readAllBytes(final Path path) { - try { - return Files.readAllBytes(path); - } catch (IOException ioException) { - throw new UncheckedIOException(ioException); - } - } - - private void writeUtf8(final Path path, final String content) { - try { - Files.writeString(path, content, StandardCharsets.UTF_8); - } catch (IOException ioException) { - throw new UncheckedIOException(ioException); - } - } - - private VfsDocumentAccessContext accessContextFor(final SupportedDocument supportedDocument) { - final var attributes = accessContextAttributes.computeIfAbsent(supportedDocument.path(), ignored -> Map.of()); - return new VfsDocumentAccessContext( - supportedDocument.path(), - supportedDocument.typeId(), - supportedDocument.frontendDocument(), - supportedDocument.accessMode(), - attributes); - } - - private record DocumentKind( - String typeId, - boolean frontendDocument, - VfsDocumentAccessMode accessMode) { - } - - private record SupportedDocument( - Path path, - String documentName, - String typeId, - boolean frontendDocument, - VfsDocumentAccessMode accessMode, - String content, - String lineSeparator) { - - private SupportedDocument withContent(final String updatedContent, final String updatedLineSeparator) { - return new SupportedDocument( - path, - documentName, - typeId, - frontendDocument, - accessMode, - updatedContent, - updatedLineSeparator); - } - } - - private static final class EditableDocumentSnapshot { - private String savedContent; - private String content; - private String lineSeparator; - - private EditableDocumentSnapshot( - final String savedContent, - final String content, - final String lineSeparator) { - this.savedContent = savedContent; - this.content = content; - this.lineSeparator = lineSeparator; - } - - private boolean isDirty() { - return !Objects.equals(savedContent, content); - } - } - - private static final class UnsupportedDocumentException extends IllegalArgumentException { - private final Path path; - private final VfsUnsupportedReason reason; - - private UnsupportedDocumentException(final Path path, final VfsUnsupportedReason reason) { - super(reason.name() + ": " + path); - this.path = path; - this.reason = reason; - } - - private UnsupportedDocumentException( - final Path path, - final VfsUnsupportedReason reason, - final Throwable cause) { - super(reason.name() + ": " + path, cause); - this.path = path; - this.reason = reason; - } - - private Path path() { - return path; - } - - private VfsUnsupportedReason reason() { - return reason; - } - } -} diff --git a/prometeu-vfs/prometeu-vfs-v1/src/test/java/p/studio/vfs/FilesystemVfsProjectDocumentTest.java b/prometeu-vfs/prometeu-vfs-v1/src/test/java/p/studio/vfs/FilesystemVfsProjectDocumentTest.java deleted file mode 100644 index f619f924..00000000 --- a/prometeu-vfs/prometeu-vfs-v1/src/test/java/p/studio/vfs/FilesystemVfsProjectDocumentTest.java +++ /dev/null @@ -1,237 +0,0 @@ -package p.studio.vfs; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import p.studio.vfs.messages.*; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -final class FilesystemVfsProjectDocumentTest { - @TempDir - Path tempDir; - - @Test - void snapshotIncludesHiddenFilesOrdersFoldersFirstAndTagsSourceRoots() throws Exception { - Files.createDirectories(tempDir.resolve("src")); - Files.createDirectories(tempDir.resolve("assets")); - Files.writeString(tempDir.resolve(".env"), "TOKEN=1\n"); - Files.writeString(tempDir.resolve("README.md"), "# project\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsProjectSnapshot snapshot = vfs.snapshot(); - - assertEquals("Example", snapshot.rootNode().displayName()); - assertEquals("assets", snapshot.rootNode().children().get(0).displayName()); - assertEquals("src", snapshot.rootNode().children().get(1).displayName()); - assertEquals(".env", snapshot.rootNode().children().get(2).displayName()); - assertTrue(snapshot.rootNode().children().get(1).taggedSourceRoot()); - } - - @Test - void openDocumentReturnsTextDocumentForUtf8TextFile() throws Exception { - final Path file = tempDir.resolve("main.pbs"); - Files.writeString(file, "fn main(): void\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentOpenResult result = vfs.openDocument(file); - final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result); - - assertEquals("main.pbs", document.documentName()); - assertEquals("pbs", document.typeId()); - assertEquals("LF", document.lineSeparator()); - assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode()); - assertTrue(document.accessContext().frontendDocument()); - assertTrue(document.content().contains("fn main()")); - } - - @Test - void openDocumentClassifiesJsonFilesWithJsonTypeIdentifier() throws Exception { - final Path file = tempDir.resolve("prometeu.json"); - Files.writeString(file, "{\n \"name\": \"Example\"\n}\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentOpenResult result = vfs.openDocument(file); - final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result); - - assertEquals(VfsDocumentTypeIds.JSON, document.typeId()); - assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode()); - } - - @Test - void openDocumentClassifiesNdjsonFilesWithJsonTypeIdentifier() throws Exception { - final Path file = tempDir.resolve("events.ndjson"); - Files.writeString(file, "{\"event\":\"start\"}\n{\"event\":\"done\"}\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentOpenResult result = vfs.openDocument(file); - final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result); - - assertEquals(VfsDocumentTypeIds.NDJSON, document.typeId()); - } - - @Test - void openDocumentClassifiesBashScriptsWithBashTypeIdentifier() throws Exception { - final Path file = tempDir.resolve("script.sh"); - Files.writeString(file, "#!/usr/bin/env bash\nprintf \"hi\"\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentOpenResult result = vfs.openDocument(file); - final var document = assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, result); - - assertEquals(VfsDocumentTypeIds.BASH, document.typeId()); - assertEquals(VfsDocumentAccessMode.EDITABLE, document.accessContext().accessMode()); - } - - @Test - void frontendDocumentsUseEditableInMemorySnapshotsUntilSavePersistsThem() throws Exception { - final Path file = tempDir.resolve("main.pbs"); - Files.writeString(file, "fn main(): void\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - final var updated = vfs.updateDocument(file, "fn main(): int\n"); - - assertTrue(updated.dirty()); - assertEquals("fn main(): int\n", updated.content()); - assertEquals("fn main(): void\n", Files.readString(file)); - assertEquals("fn main(): int\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content()); - - final VfsDocumentSaveResult saveResult = vfs.saveDocument(file); - - assertEquals(VfsDocumentSaveStatus.SAVED, saveResult.status()); - assertEquals("fn main(): int\n", Files.readString(file)); - assertFalse(assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).dirty()); - } - - @Test - void editableDocumentsUseInMemorySnapshotsUntilSavePersistsThem() throws Exception { - final Path file = tempDir.resolve("notes.txt"); - Files.writeString(file, "alpha\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final var updated = vfs.updateDocument(file, "beta\n"); - - assertTrue(updated.dirty()); - assertEquals("beta\n", updated.content()); - assertEquals("alpha\n", Files.readString(file)); - assertEquals("beta\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content()); - - final VfsDocumentSaveResult saveResult = vfs.saveDocument(file); - - assertEquals(VfsDocumentSaveStatus.SAVED, saveResult.status()); - assertEquals("beta\n", Files.readString(file)); - assertFalse(assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).dirty()); - } - - @Test - void discardDocumentDropsDirtySnapshotAndRestoresFilesystemState() throws Exception { - final Path file = tempDir.resolve("notes.txt"); - Files.writeString(file, "alpha\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - vfs.updateDocument(file, "beta\n"); - - final var discarded = vfs.discardDocument(file); - - assertFalse(discarded.dirty()); - assertEquals("alpha\n", discarded.content()); - assertEquals("alpha\n", Files.readString(file)); - assertEquals("alpha\n", assertInstanceOf(VfsDocumentOpenResult.VfsTextDocument.class, vfs.openDocument(file)).content()); - } - - @Test - void saveAllDocumentsPersistsEditableDirtySnapshotsIncludingFrontendDocuments() throws Exception { - final Path editable = tempDir.resolve("notes.txt"); - final Path frontend = tempDir.resolve("main.pbs"); - Files.writeString(editable, "alpha\n"); - Files.writeString(frontend, "fn main(): void\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - vfs.updateDocument(editable, "beta\n"); - vfs.updateDocument(frontend, "fn main(): int\n"); - - final var results = vfs.saveAllDocuments(); - - assertEquals(2, results.size()); - assertEquals(editable.toAbsolutePath().normalize(), results.get(0).path()); - assertEquals(VfsDocumentSaveStatus.SAVED, results.get(0).status()); - assertEquals(frontend.toAbsolutePath().normalize(), results.get(1).path()); - assertEquals(VfsDocumentSaveStatus.SAVED, results.get(1).status()); - assertEquals("beta\n", Files.readString(editable)); - assertEquals("fn main(): int\n", Files.readString(frontend)); - } - - @Test - void accessContextCanBeReadAndMutatedWithoutChangingCanonicalPolicy() throws Exception { - final Path file = tempDir.resolve("prometeu.json"); - Files.writeString(file, "{\n \"name\": \"Example\"\n}\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentAccessContext initialContext = vfs.accessContext(file); - final VfsDocumentAccessContext updatedContext = vfs.updateAccessContext(file, Map.of("statusBar", "future-toggle")); - - assertEquals(VfsDocumentAccessMode.EDITABLE, initialContext.accessMode()); - assertEquals(VfsDocumentTypeIds.JSON, initialContext.typeId()); - assertEquals(Map.of("statusBar", "future-toggle"), updatedContext.attributes()); - assertEquals(initialContext.accessMode(), updatedContext.accessMode()); - assertEquals(initialContext.typeId(), updatedContext.typeId()); - } - - @Test - void openDocumentRejectsBinaryLikeFiles() throws Exception { - final Path file = tempDir.resolve("sprite.bin"); - Files.write(file, new byte[]{0x01, 0x00, 0x02}); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentOpenResult result = vfs.openDocument(file); - final var unsupported = assertInstanceOf(VfsDocumentOpenResult.VfsUnsupportedDocument.class, result); - - assertEquals(VfsUnsupportedReason.BINARY_CONTENT, unsupported.reason()); - } - - @Test - void openDocumentRejectsPathsOutsideProjectScope() throws Exception { - final Path outsideFile = tempDir.getParent().resolve("outside.pbs"); - Files.writeString(outsideFile, "fn stray(): void\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - - final VfsDocumentOpenResult result = vfs.openDocument(outsideFile); - final var unsupported = assertInstanceOf(VfsDocumentOpenResult.VfsUnsupportedDocument.class, result); - - assertEquals(VfsUnsupportedReason.OUTSIDE_PROJECT, unsupported.reason()); - } - - @Test - void targetedRefreshUpdatesOnlyTheRequestedSubtreeInTheSnapshot() throws Exception { - final Path src = Files.createDirectories(tempDir.resolve("src")); - Files.createDirectories(tempDir.resolve("assets")); - Files.writeString(src.resolve("main.pbs"), "fn main(): void\n"); - - final VfsProjectDocument vfs = new FilesystemProjectDocumentVfsFactory().open(projectContext()); - Files.writeString(src.resolve("later.pbs"), "fn later(): void\n"); - - final VfsProjectSnapshot refreshedSnapshot = vfs.refresh(new VfsRefreshRequest(src)); - final VfsProjectNode srcNode = refreshedSnapshot.rootNode().children().get(1); - - assertEquals("src", srcNode.displayName()); - assertEquals(2, srcNode.children().size()); - assertEquals("later.pbs", srcNode.children().get(0).displayName()); - assertEquals("main.pbs", srcNode.children().get(1).displayName()); - } - - private VfsProjectContext projectContext() { - return new VfsProjectContext("Example", "pbs", tempDir); - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0df55951..a3801c38 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,13 +5,13 @@ plugins { rootProject.name = "prometeu-studio" include("prometeu-infra") -include("prometeu-vfs:prometeu-vfs-api") -include("prometeu-vfs:prometeu-vfs-v1") -include("prometeu-lsp:prometeu-lsp-api") -include("prometeu-lsp:prometeu-lsp-v1") + include("prometeu-packer:prometeu-packer-api") include("prometeu-packer:prometeu-packer-v1") +include("prometeu-lsp:prometeu-lsp-api") +include("prometeu-lsp:prometeu-lsp-v1") + include("prometeu-compiler:frontends:prometeu-frontend-pbs") include("prometeu-compiler:prometeu-compiler-core") include("prometeu-compiler:prometeu-build-pipeline") diff --git a/test-projects/main/.vscode/settings.json b/test-projects/main/.vscode/settings.json new file mode 100644 index 00000000..57fd1504 --- /dev/null +++ b/test-projects/main/.vscode/settings.json @@ -0,0 +1,121 @@ +{ + "editor.semanticTokenColorCustomizations": { + "enabled": true, + "rules": { + "pbs-comment:pbs": { + "foreground": "#8b949e", + "italic": true + }, + "pbs-string:pbs": "#d8b86a", + "pbs-number:pbs": "#ff8cc6", + "pbs-literal:pbs": "#7fb8ff", + "pbs-lifecycle:pbs": { + "foreground": "#ff7b72", + "bold": true + }, + "pbs-keyword:pbs": "#7fb8ff", + "pbs-operator:pbs": "#c9d1d9", + "pbs-punctuation:pbs": "#8b949e", + "pbs-function:pbs": { + "foreground": "#7ee787", + "bold": true + }, + "pbs-method:pbs": "#7ee787", + "pbs-constructor:pbs": { + "foreground": "#7ee787", + "bold": true + }, + "pbs-struct:pbs": { + "foreground": "#56d4dd", + "italic": true + }, + "pbs-contract:pbs": { + "foreground": "#79c0ff", + "underline": true + }, + "pbs-host:pbs": "#c297ff", + "pbs-builtin-type:pbs": "#56d4dd", + "pbs-service:pbs": { + "foreground": "#ff8e7a", + "bold": true + }, + "pbs-error:pbs": { + "foreground": "#ff7b72", + "underline": true + }, + "pbs-enum:pbs": { + "foreground": "#56d4dd", + "bold": true + }, + "pbs-callback:pbs": "#ffb3e6", + "pbs-global:pbs": { + "foreground": "#f778ba", + "italic": true + }, + "pbs-const:pbs": { + "foreground": "#ffab70", + "bold": true + }, + "pbs-implements:pbs": "#a5d6ff", + "pbs-identifier:pbs": "#d4d4d4", + "comment:pbs": { + "foreground": "#8b949e", + "italic": true + }, + "string:pbs": "#d8b86a", + "number:pbs": "#ff8cc6", + "enumMember:pbs": "#7fb8ff", + "variable:pbs": "#7fb8ff", + "function.async:pbs": { + "foreground": "#ff7b72", + "bold": true + }, + "variable.async:pbs": { + "foreground": "#ff7b72", + "bold": true + }, + "keyword:pbs": "#7fb8ff", + "operator:pbs": "#c9d1d9", + "function:pbs": { + "foreground": "#7ee787", + "bold": true + }, + "method:pbs": "#7ee787", + "class.declaration:pbs": { + "foreground": "#7ee787", + "bold": true + }, + "type.declaration:pbs": { + "foreground": "#7ee787", + "bold": true + }, + "struct:pbs": { + "foreground": "#56d4dd", + "italic": true + }, + "type:pbs": { + "foreground": "#56d4dd", + "italic": true + }, + "interface:pbs": { + "foreground": "#79c0ff", + "underline": true + }, + "class.defaultLibrary:pbs": "#c297ff", + "type.defaultLibrary:pbs": "#c297ff", + "variable.defaultLibrary:pbs": "#56d4dd", + "enum:pbs": { + "foreground": "#ff7b72", + "underline": true + }, + "variable.readonly:pbs": "#c297ff", + "pbs-asset:pbs": "#c297ff", + "string.readonly:pbs": "#c297ff" + } + }, + "workbench.colorCustomizations": { + "editor.foreground": "#d9e2ec", + "editor.selectionBackground": "#2f3f5b", + "editorLineNumber.foreground": "#6b7f99" + } +} \ No newline at end of file diff --git a/test-projects/main/assets/.prometeu/cache.json b/test-projects/main/assets/.prometeu/cache.json index ef4e0d61..174130e2 100644 --- a/test-projects/main/assets/.prometeu/cache.json +++ b/test-projects/main/assets/.prometeu/cache.json @@ -24,14 +24,14 @@ "last_modified" : 1774359772403, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -42,14 +42,14 @@ "last_modified" : 1774359772405, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -60,14 +60,14 @@ "last_modified" : 1774359772406, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -78,14 +78,14 @@ "last_modified" : 1774359772408, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -96,14 +96,14 @@ "last_modified" : 1774359772409, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -114,14 +114,14 @@ "last_modified" : 1774359772411, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -132,14 +132,14 @@ "last_modified" : 1774359772412, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -150,14 +150,14 @@ "last_modified" : 1774359772413, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -168,14 +168,14 @@ "last_modified" : 1774359772415, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -186,14 +186,14 @@ "last_modified" : 1774359772416, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -204,14 +204,14 @@ "last_modified" : 1774359772417, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -222,14 +222,14 @@ "last_modified" : 1774359772418, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -240,14 +240,14 @@ "last_modified" : 1774359772420, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -258,14 +258,14 @@ "last_modified" : 1774359772421, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -276,14 +276,14 @@ "last_modified" : 1774359772423, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -294,14 +294,14 @@ "last_modified" : 1774359772424, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -312,14 +312,14 @@ "last_modified" : 1774359772425, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -330,14 +330,14 @@ "last_modified" : 1774359772426, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -348,14 +348,14 @@ "last_modified" : 1774359772428, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -366,14 +366,14 @@ "last_modified" : 1774359772429, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -384,14 +384,14 @@ "last_modified" : 1774359772430, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -402,14 +402,14 @@ "last_modified" : 1774359772431, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -420,14 +420,14 @@ "last_modified" : 1774359772432, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -438,14 +438,14 @@ "last_modified" : 1774359772433, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -456,14 +456,14 @@ "last_modified" : 1774359772435, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -474,14 +474,14 @@ "last_modified" : 1774359772436, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -492,14 +492,14 @@ "last_modified" : 1774359772437, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -510,14 +510,14 @@ "last_modified" : 1774359772438, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -528,14 +528,14 @@ "last_modified" : 1774359772439, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -546,14 +546,14 @@ "last_modified" : 1774359772441, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -564,14 +564,14 @@ "last_modified" : 1774359772442, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -582,14 +582,14 @@ "last_modified" : 1774359772443, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -600,14 +600,14 @@ "last_modified" : 1774359772444, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -618,14 +618,14 @@ "last_modified" : 1774359772445, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -636,14 +636,14 @@ "last_modified" : 1774359772448, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -654,14 +654,14 @@ "last_modified" : 1774359772449, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -672,14 +672,14 @@ "last_modified" : 1774359772450, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -690,14 +690,14 @@ "last_modified" : 1774359772452, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -708,14 +708,14 @@ "last_modified" : 1774359772453, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -726,14 +726,14 @@ "last_modified" : 1774359772454, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -744,14 +744,14 @@ "last_modified" : 1774359772455, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -762,14 +762,14 @@ "last_modified" : 1774359772456, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -780,14 +780,14 @@ "last_modified" : 1774359772458, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -798,14 +798,14 @@ "last_modified" : 1774359772459, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -816,14 +816,14 @@ "last_modified" : 1774359772460, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -834,14 +834,14 @@ "last_modified" : 1774359772461, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -852,14 +852,14 @@ "last_modified" : 1774359772463, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -870,14 +870,14 @@ "last_modified" : 1774359772464, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -888,14 +888,14 @@ "last_modified" : 1774359772465, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -906,14 +906,14 @@ "last_modified" : 1774359772467, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -924,14 +924,14 @@ "last_modified" : 1774359772468, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -942,14 +942,14 @@ "last_modified" : 1774359772470, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -960,14 +960,14 @@ "last_modified" : 1774359772474, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -978,14 +978,14 @@ "last_modified" : 1774359772476, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -996,14 +996,14 @@ "last_modified" : 1774359772478, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1014,14 +1014,14 @@ "last_modified" : 1774359772479, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1032,14 +1032,14 @@ "last_modified" : 1774359772481, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1050,14 +1050,14 @@ "last_modified" : 1774359772482, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1068,14 +1068,14 @@ "last_modified" : 1774359772483, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1086,14 +1086,14 @@ "last_modified" : 1774359772484, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1104,14 +1104,14 @@ "last_modified" : 1774359772485, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1122,14 +1122,14 @@ "last_modified" : 1774359772487, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1140,14 +1140,14 @@ "last_modified" : 1774359772488, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1158,14 +1158,14 @@ "last_modified" : 1774359772489, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1176,14 +1176,14 @@ "last_modified" : 1774359772491, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1194,14 +1194,14 @@ "last_modified" : 1774359772492, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1212,14 +1212,14 @@ "last_modified" : 1774359772493, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1230,14 +1230,14 @@ "last_modified" : 1774359772494, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1248,14 +1248,14 @@ "last_modified" : 1774359772495, "fingerprint" : "eeb7cc665386299403b7be1c272375d5077f3647c587f8033aa8c9275f840562", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1266,14 +1266,14 @@ "last_modified" : 1775797786950, "fingerprint" : "0526996900bdeef23c72fe987fb45800f11895946dcd5f0f7f8390c879069a37", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674, -1736296, -13905598, -11518505, -14439577, -2467509 ], + "convertedRgb565" : [ -122, -7149, 11912, 20986, 9548, -9527 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQECAgIBAQEBAQEBAQEBAQEBAQEBAwMDAwMDAQEBAQEBAgICAgICAgIBAQEBAQEBAQEBAQMDAwMDAwMBAQEBAQICAgICAgICAQEBAQEBAQEBAQEBAQMDAwMDAQEBAQEBAgICAgICAgEBAQEBAQQEBAQBAQEBAQEBAQEBAQEBAQEBAgICAgIBAQEBAQEEBAQEBAQBAQEBAQEBAQEBAQEBAQEBAgECAQEBAQEBAQQEBAQEAQEBAQEBAAAAAAAAAAABAQECAQEBAQEBAQEEBAQEBAEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQUBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQUFBQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEFBQUFBQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBBQUFBQUBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBBQUFAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQYGBgYBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBBgYGBgEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEGBgYGAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQYGBgYBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQYGAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674, -1736296, -13905598, -11518505, -14439577, -2467509 ], - "convertedRgb565" : [ -122, -7149, 11912, 20986, 9548, -9527 ] } }, "diagnostics" : [ ] @@ -1314,14 +1314,14 @@ "last_modified" : 1775797786770, "fingerprint" : "aa7d241deabcebe29a6096e14eaf16fdc06cf06380c11a507620b00fc7bff094", "metadata" : { + "palette" : { + "originalArgb8888" : [ -265674 ], + "convertedRgb565" : [ -122 ] + }, "tile" : { "width" : 16, "height" : 16, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAAAAAAEBAQEBAQEBAQEBAQAAAAABAQEBAQEBAQEBAQEAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAABAQEBAQEAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAABAQEBAQEBAQEAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAABAQEBAQEBAQEAAAAAAAAAAQEBAQEBAQEBAAAAAAAAAAEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -265674 ], - "convertedRgb565" : [ -122 ] } }, "diagnostics" : [ ] @@ -1344,14 +1344,14 @@ "last_modified" : 1775797786709, "fingerprint" : "6ca635a7906067b6a2a8460a8562718c445783e6df18b9f2c80c83c5ac569db6", "metadata" : { + "palette" : { + "originalArgb8888" : [ -130562, -14145496, -2043712, -460560, -13082456, -6252424, -10977088 ], + "convertedRgb565" : [ -2017, 10565, -6504, -34, 15125, -23345, 23576 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQICAgICAgICAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAwMDAwMDAgAAAAAAAAAAAAACAgIAAAAAAAAAAAAAAAIEBQUFBQUDAgAAAAAAAgICAgMDAwIAAAAAAAAAAAAAAgQFAwYGBgUDAgICAgIDAwMDBQUFAwIAAAAAAAAAAAACBAUDBgYGBgUEBAQEAwUFBQUGAwMFBAICAAAAAAAAAAIEBQMGBgYGAwUFBwcFBgYGBgMDAwMFBAQCAgAAAAAAAgQFAwYGBgMDAwQEBAMGBgYGBgMDAwMHBwQDAgAAAAACBAUDBgYGAwMDAwQEAwYGBgYGAwMDBAQEBQUDAgIAAAIEBQMGBgYDAwMDBAQDBgYGBgYDAwMDBAQDAwUGAgAAAgQFAwYGBgMDAwMEBAMGBgYGBQUFBQUEBAUFBgIAAAACBAUDBgYGAwMDAwQEAwYGBQUGAwMDAwcHAwYCAAAAAAIEBQMFBQUFBQMDBAQDBgUGBgICAgICBAQCAgAAAAAAAgQFBQYGBgYDBQMEBAMFBgICAAAAAAACAgAAAAAAAAACBAMDBgICAgIDBQcHBQYCAAAAAAAAAAAAAAAAAAAAAgIDAwMCAAAAAAIEBAMDAgAAAAAAAAAAAAAAAAAAAAICAgICAgAAAAAAAAICAgIAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -130562, -14145496, -2043712, -460560, -13082456, -6252424, -10977088 ], - "convertedRgb565" : [ -2017, 10565, -6504, -34, 15125, -23345, 23576 ] } }, "diagnostics" : [ { @@ -1396,14 +1396,14 @@ "last_modified" : 1775797786706, "fingerprint" : "7feab8c868bb446afd3dd7250e70504fb0ca228f463650d2dd7c525144e0b321", "metadata" : { + "palette" : { + "originalArgb8888" : [ -16777216, -2109505, -526353, -395279, -130562, -13082456, -10911807, -6318217, -6186631, -6187143, -6318215, -13148249, -13016663, -14080215 ], + "convertedRgb565" : [ 0, -8585, -2115, -66, -2017, 15125, 23544, -25426, -23345, -23377, -25425, 13044, 15125, 10533 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgMDBAEBAQEBBQUFAAUFBQUFBQUBAQAAAAAAAAAAAAECBgcHBAIICAkBBQUFBQUFBQEBAQICAQEAAAAFBQAAAQIGAwMHBgYGBgIBBQUFBQEBCgIIBgYDBAEBBQUFAAABAgYDAwIJCQsJBgIBAQEBCAgMDA0JAgcHAwMBBQUAAAECBgMDAgsLCwsLBgMDAwIGBgkCCwILAgIHAwEFBQAAAQIGAwMCCQkJCQkCBgcHBgsLCwsCCwICAwMHAwEFAAABAgYDAgkJCQkJCQICAwMCCQkJAgkGBgYHBwcDDgUAAAECBwMCCQkJCQkJAgIDAgkJCQkGBgsCAwMDAwQEAQAAAQIHAwIJCQkJCQkCAgMCCQkJBgkLAQEBAQEBAQQBAAABAgcDAgkJCQkJAgICAwILCQYJAQEFBQUFBQUFAQEAAAECBwMCCQkJCQYGAgIDAgkGCwEFBQUFBQUFBQAAAAAAAQIHAwIJCQYGCQIGBgcMBgkBBQUFBQUFBQAAAAAAAAABAgcGBgYGCQsBAQIDBAIKAQUFBQUFBQAAAAAAAAAAAQECAwIJCQkBAQAAAQEBAQEAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -16777216, -2109505, -526353, -395279, -130562, -13082456, -10911807, -6318217, -6186631, -6187143, -6318215, -13148249, -13016663, -14080215 ], - "convertedRgb565" : [ 0, -8585, -2115, -66, -2017, 15125, 23544, -25426, -23345, -23377, -25425, 13044, 15125, 10533 ] } }, "diagnostics" : [ ] @@ -1414,14 +1414,14 @@ "last_modified" : 1775797786710, "fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQICAwMBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAQBAgIDAwMCAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQUFBAQBAgIDAgICAQAAAAAAAAAAAAAAAAAAAAAAAAABBQEFBAUBAgICAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAABBgEFBQUBAgECAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAEGBwEFBAEBCAECAwEAAAAAAAAAAAAAAAAAAAAAAAAAAQgBBwEEAQgJAQICAQAAAAAAAAAAAAAAAAAAAAAAAAEICAEHAQQBCQYBAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAEGCQkBBQEGAQUBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBgYBBgEBAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQcBAQEBAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECAwECAwIBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAQcHBwECAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAEBAQEBAQECAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgEJCgoBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEJCQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEKAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQELCwoKAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1432,14 +1432,14 @@ "last_modified" : 1775797786711, "fingerprint" : "e2130efbb9643b41a4fb62619f2713402f9622c7db040fe18d929609ef70b89a", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQECAgMDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQQEAQICAwMDAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAEFBQQEAQICAwICAgEAAAAAAAAAAAAAAAAAAAAAAAAAAQUBBQQFAQICAgIDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQYBBQUFAQIBAgIDAQAAAAAAAAAAAAAAAAAAAAAAAAABBgcBBQQBAQgBAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAEIAQcBBAEICQECAgEAAAAAAAAAAAAAAAAAAAAAAAABCAgBBwEEAQkGAQICAQAAAAAAAAAAAAAAAAAAAAAAAAABBgkJAQUBBgEFAQIBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQYGAQYBAQEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEHAQEBAQECAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgEBBwMBAwMBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBCQoBBwEDAgEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAgEJCQoBAgIBAQIBAAAAAAAAAAAAAAAAAAAAAAAAAAECAgEBAQICAQECAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgECAQECAgEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQICAgEBAQoKAQAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQELCwoBAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQsLAQEAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1450,14 +1450,14 @@ "last_modified" : 1775797786713, "fingerprint" : "9216dfc6a6226fac398e9f781550d23fbfaa65bd377cdda69187149beca114ec", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQECAgMDAQEAAAEBAAAAAAAAAAAAAAAAAAAAAAABBAQEAQICAwMDAgEBAwEAAAAAAAAAAAAAAAAAAAAAAAEFBQQEAQICAwICAQIDAQAAAAAAAAAAAAAAAAAAAAAAAAEBBQQFAQICAgICAwEBAAAAAAAAAAAAAAAAAAAAAAAAAQYBBQUFAQIBAgICAQAAAAAAAAAAAAAAAAAAAAAAAAABBgcBBQQFAQgBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAEIAQcBBAUBCQEBBQUBAAAAAAAAAAAAAAAAAAAAAAABCAgBBwgBBAUBAQUBAAAAAAAAAAAAAAAAAAAAAAAAAAABBgkJBgYBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQYGBgYBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQMBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEJCgEHAwECAwEBAQAAAAAAAAAAAAAAAAAAAAAAAAABAQkKAQcCAQIDAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAEKAQEBAQECAgEBAgELAQAAAAAAAAAAAAAAAAAAAAAAAQEKAQICAQEBAgIBAQoLAQAAAAAAAAAAAAAAAAAAAAAAAQEBAQECAgICAQEAAQEAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1468,14 +1468,14 @@ "last_modified" : 1775797786712, "fingerprint" : "ebc4b7ca7bb1455288681c5d71424f60f658a44cb343d0c3934329cec676c867", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQICAwMBAQEBAwEAAAAAAAAAAAAAAAAAAAAAAAABBAQBAgIDAwMCAQMDAQAAAAAAAAAAAAAAAAAAAAAAAQUFBAQBAgIDAgICAwIBAAAAAAAAAAAAAAAAAAAAAAABBQEFBAUBAgICAgMCAQAAAAAAAAAAAAAAAAAAAAAAAAABBgEFBQUBAgECAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAEGBwEFBAUBCAEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQgBBwEEBQEJAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEICAEHCQEEBQEFAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBCQkGBgEBAQUBAAAAAAAAAAAAAAAAAAAAAAAAAAABCQoBAQEGBgEBAQUBAAAAAAAAAAAAAAAAAAAAAAAAAAEKCgEHAgEBAQMBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBwICAQIDAwEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQECAgMBAQIBAAAAAAAAAAAAAAAAAAAAAAAAAQoBAQIBAgICAgECAQEAAAAAAAAAAAAAAAAAAAAAAAABCgoBAgIBAQEBAgELAQAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQICAgIBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1486,14 +1486,14 @@ "last_modified" : 1775797786718, "fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQICAwMBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAQBAgIDAwMCAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQUFBAQBAgIDAgICAQAAAAAAAAAAAAAAAAAAAAAAAAABBQEFBAUBAgICAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAABBgEFBQUBAgECAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAEGBwEFBAEBCAECAwEAAAAAAAAAAAAAAAAAAAAAAAAAAQgBBwEEAQgJAQICAQAAAAAAAAAAAAAAAAAAAAAAAAEICAEHAQQBCQYBAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAEGCQkBBQEGAQUBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBgYBBgEBAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQcBAQEBAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECAwECAwIBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAQcHBwECAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAEBAQEBAQECAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgEJCgoBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEJCQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEKAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQELCwoKAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1504,14 +1504,14 @@ "last_modified" : 1775797786717, "fingerprint" : "1ba8ce75c445396334737143e0cfa56c87a1fb64cca8ad0962192594849249eb", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQICAwMBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAQBAgIDAwMCAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQUFBAQBAgIDAgICAQAAAAAAAAAAAAAAAAAAAAAAAAABBQEFBAUBAgICAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAABBgEFBQUBAgECAgMBAAAAAAAAAAAAAAAAAAAAAAAAAAEGBwEFBAEBCAECAwEAAAAAAAAAAAAAAAAAAAAAAAAAAQgBBwEEAQgJAQICAQAAAAAAAAAAAAAAAAAAAAAAAAEICAEHAQQBCQYBAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAEGCQkBBQEGAQUBAgEAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBgYBAQEBAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQcBAQEDAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAwMBAgcHAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQMDAwIBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBAIDAgEKCgECAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAECAQEBAQkJAQIBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAgICAQECAQoBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQICAgEKCgEAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBCgoKAQAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092 ] } }, "diagnostics" : [ ] @@ -1522,14 +1522,14 @@ "last_modified" : 1775797786714, "fingerprint" : "5e0954447699c6d5eac7550f1a10cf520efcddad0b9fbd9a716434da9d9550d4", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQICAwMBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEEBAQBAgIDAwMCAQABAQAAAAAAAAAAAAAAAAAAAAAAAQUFBAQBAgIDAgIBAQMBAAAAAAAAAAAAAAAAAAAAAAAAAQEFBAUBAgICAgIDAwEAAAAAAAAAAAAAAAAAAAAAAAABBgEFBQUBAgECAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAEGBwEFBAUBCAEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAQgBBwEEBQEJAQEFBQEAAAAAAAAAAAAAAAAAAAAAAAEICAEHCAEEBQEBBQEBAAAAAAAAAAAAAAAAAAAAAAAAAAEGCQkGBgEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBgYGAQIDAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCQEBAQEBAQEHAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBBwcDAgEKCgEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAgIBAwMCAQkJAQEAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQIBAgMCAQEBCgEAAAAAAAAAAAAAAAAAAAAAAAABCwsKAQIBAQECAgEKAQAAAAAAAAAAAAAAAAAAAAAAAAABCwsKAQICAgIBCgoBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1540,14 +1540,14 @@ "last_modified" : 1775797786716, "fingerprint" : "6104ba7f216a937994d478ed07766ec28f48ad72fdd6620508535a1f877b8ddb", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], + "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] + }, "tile" : { "width" : 32, "height" : 32, "paletteIndices" : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBAAAAAAEBAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQICAwMBAQEBAwEAAAAAAAAAAAAAAAAAAAAAAAABBAQBAgIDAwMCAQMDAQAAAAAAAAAAAAAAAAAAAAAAAQUFBAQBAgIDAgICAwIBAAAAAAAAAAAAAAAAAAAAAAABBQEFBAUBAgICAgMCAQAAAAAAAAAAAAAAAAAAAAAAAAABBgEFBQUBAgECAgIBAAAAAAAAAAAAAAAAAAAAAAAAAAEGBwEFBAUBCAEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQgBBwEEBQEJAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEICAEHCQEEBQEFAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEGCQkGBgEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAABAQEBBgYGAQIDAgEBAAAAAAAAAAAAAAAAAAAAAAAAAAEJAQEBAQEBAgIBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBBwcDAgIBAQoKAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQQBAwMDAgIBCQkBAAAAAAAAAAAAAAAAAAAAAAAAAAEBAgECAwMCAQEBAQEAAAAAAAAAAAAAAAAAAAAAAAABCwYBAgEBAQECAgEKAQAAAAAAAAAAAAAAAAAAAAAAAAELCgYBAgICAgEBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAEKCgoBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQEBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBAQEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQEBAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "palette" : { - "originalArgb8888" : [ -14671840, -11431408, -9326048, -802016, -3964416, -5086944, -789517, -810399, -1863087, -10403552, -5078447 ], - "convertedRgb565" : [ 8452, 21634, 30084, -2556, -15360, -19708, -2146, -2804, -7030, 25092, -19446 ] } }, "diagnostics" : [ ] @@ -1602,14 +1602,14 @@ "last_modified" : 1776965154257, "fingerprint" : "8771c3e294d0edbc8dd3aaa6995a6db32166ae6bfb3e063170cc79b298d17e71", "metadata" : { + "palette" : { + "originalArgb8888" : [ -12019640 ], + "convertedRgb565" : [ 19657 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -12019640 ], - "convertedRgb565" : [ 19657 ] } }, "diagnostics" : [ ] @@ -1620,14 +1620,14 @@ "last_modified" : 1776965154257, "fingerprint" : "5d81eef8be13043330b42b546e10beed8ba00dfaa42df67477a8778c2b9484d7", "metadata" : { + "palette" : { + "originalArgb8888" : [ -12019640, -12552128, -14129088, -14133200, -14145496, -7835584, -5730208, -10465232 ], + "convertedRgb565" : [ 19657, 17352, 11080, 10950, 10565, -29816, -21364, 25222 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAQEBAQEBAQEBAQICAgECAgICAgICAgMEBQUFBQQDBAUGBgYGBQMEBQYGBwcIAwQFCAYGCAgBAwUFCAgIBw==" - }, - "palette" : { - "originalArgb8888" : [ -12019640, -12552128, -14129088, -14133200, -14145496, -7835584, -5730208, -10465232 ], - "convertedRgb565" : [ 19657, 17352, 11080, 10950, 10565, -29816, -21364, 25222 ] } }, "diagnostics" : [ ] @@ -1638,14 +1638,14 @@ "last_modified" : 1776965154258, "fingerprint" : "3250840c4fe08c134ea966ce7c8fcaccae2c2a45660d7173c5e262a2e5076548", "metadata" : { + "palette" : { + "originalArgb8888" : [ -12552128, -14133200, -14145496, -10465232, -5730208, -7835584 ], + "convertedRgb565" : [ 17352, 10950, 10565, 25222, -21364, -29816 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAQECAwMBAQEBAgMEBQEBAQIDBAUFAQIDAwQEBAQDAwYFBQUFBQQGBQUGBgYGBQUFBgYFBQUFBgYGBQUGBg==" - }, - "palette" : { - "originalArgb8888" : [ -12552128, -14133200, -14145496, -10465232, -5730208, -7835584 ], - "convertedRgb565" : [ 17352, 10950, 10565, 25222, -21364, -29816 ] } }, "diagnostics" : [ ] @@ -1656,14 +1656,14 @@ "last_modified" : 1776965154258, "fingerprint" : "fe3ab2b7eb3b10a042de14484d53e718e6b505edb1ad19053668565bfe3a8f25", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14145496, -14133200, -12552128, -5730208, -10465232, -7835584 ], + "convertedRgb565" : [ 10565, 10950, 17352, -21364, 25222, -29816 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQECAwMDAwMEBQECAwMDAwQEBQECAwMDBQUFBQEBAgMEBAQEBAYBAQYGBgYEBAYFBAQEBgYEBAQGBgQEBgYGBA==" - }, - "palette" : { - "originalArgb8888" : [ -14145496, -14133200, -12552128, -5730208, -10465232, -7835584 ], - "convertedRgb565" : [ 10565, 10950, 17352, -21364, 25222, -29816 ] } }, "diagnostics" : [ ] @@ -1674,14 +1674,14 @@ "last_modified" : 1776965154259, "fingerprint" : "7fcbf90e1e9a69d6ce3be80ea1901d66c9992f68dc72e4c636dcd2ad52fa2898", "metadata" : { + "palette" : { + "originalArgb8888" : [ -12019640, -12552128, -14133200, -14145496, -14129088, -7835584, -10465232, -5730208 ], + "convertedRgb565" : [ 19657, 17352, 10950, 10565, 11080, -29816, 25222, -21364 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAQEBAQECAgIBAQEBAQICAgICAgIBAwQEBAQDBQIEBgYGBgQDBQcICAYGBAMFBwcGBgcEAwUIBwcHBAQFAQ==" - }, - "palette" : { - "originalArgb8888" : [ -12019640, -12552128, -14133200, -14145496, -14129088, -7835584, -10465232, -5730208 ], - "convertedRgb565" : [ 19657, 17352, 10950, 10565, 11080, -29816, 25222, -21364 ] } }, "diagnostics" : [ ] @@ -1692,14 +1692,14 @@ "last_modified" : 1776965154259, "fingerprint" : "451fc8d646049944d5aa7c7dc254636eec0151aaff6e2051059304b9d6ebc31e", "metadata" : { + "palette" : { + "originalArgb8888" : [ -12019640, -14129088, -14133200, -14145496, -10465232, -5730208, -7835584 ], + "convertedRgb565" : [ 19657, 11080, 10950, 10565, 25222, -21364, -29816 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQIDBAQFBgYBAQIDBAYGBwEBAQMEBgcHAQEBBAQGBwcBAQQFBAYHBwEEBwUEBgcHBAcGBQQGBwcEBwYFBAYHBw==" - }, - "palette" : { - "originalArgb8888" : [ -12019640, -14129088, -14133200, -14145496, -10465232, -5730208, -7835584 ], - "convertedRgb565" : [ 19657, 11080, 10950, 10565, 25222, -21364, -29816 ] } }, "diagnostics" : [ ] @@ -1710,14 +1710,14 @@ "last_modified" : 1776965154260, "fingerprint" : "aa757df4600823ec6655235a1081061777124ccee74b31481bbe402e53635f53", "metadata" : { + "palette" : { + "originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ], + "convertedRgb565" : [ -29816, -21364, 25222, 10565 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQECAwMBAQEBAgIBAwMBAgICAQEBAwMCAgIBAQIDBAMCAgEBAgMEBAICAQECAwQEAgIBAQIDBAMCAgIBAwQDAg==" - }, - "palette" : { - "originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ], - "convertedRgb565" : [ -29816, -21364, 25222, 10565 ] } }, "diagnostics" : [ ] @@ -1728,14 +1728,14 @@ "last_modified" : 1776965154260, "fingerprint" : "38162157f5685cce25b835d3a2caee3465fd2ec3ee5db2a5089d7d5ae5ea3bb7", "metadata" : { + "palette" : { + "originalArgb8888" : [ -7835584, -10465232, -5730208, -14145496 ], + "convertedRgb565" : [ -29816, 25222, -21364, 10565 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAgIDAQEDAQICAQMDAQMCAgEBAQMDAgQCAwEBAwMEBAIDAQEDAwQEAgMBAQMDAgQCAwEBAwMDAgQCAQMDAw==" - }, - "palette" : { - "originalArgb8888" : [ -7835584, -10465232, -5730208, -14145496 ], - "convertedRgb565" : [ -29816, 25222, -21364, 10565 ] } }, "diagnostics" : [ ] @@ -1746,14 +1746,14 @@ "last_modified" : 1776965154260, "fingerprint" : "8cab5b3d553af9ed371bd19fc624763accc90e5d926d300ba31dd8d9d6bab803", "metadata" : { + "palette" : { + "originalArgb8888" : [ -5730208, -10465232, -14145496, -14133200, -14129088, -12019640, -7835584 ], + "convertedRgb565" : [ -21364, 25222, 10565, 10950, 11080, 19657, -29816 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQECAwMEBQYHAQEDBAUGBgcHAQMEBgYGBwcBAwMGBgYHBwEDAgMGBgcHAQMCBwMGBwcBAwIBBwMHBwEDAgEHAw==" - }, - "palette" : { - "originalArgb8888" : [ -5730208, -10465232, -14145496, -14133200, -14129088, -12019640, -7835584 ], - "convertedRgb565" : [ -21364, 25222, 10565, 10950, 11080, 19657, -29816 ] } }, "diagnostics" : [ ] @@ -1764,14 +1764,14 @@ "last_modified" : 1776965154261, "fingerprint" : "d9ff5a6df0f65f5fcfb8f22376beeb266d429e2c6e9c82f51c12bc6ee4c3fcdc", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14145496, -10465232, -7835584, -5730208, -14133200, -14129088, -12019640 ], + "convertedRgb565" : [ 10565, 25222, -29816, -21364, 10950, 11080, 19657 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQIDAgIBBAMFAQECAgEEBAUGBQECAgEEBwYFBQECAQEHBwYGBQEBAgcHBwYFAQMDBwcHBwEBAwMHBwcFAQICAw==" - }, - "palette" : { - "originalArgb8888" : [ -14145496, -10465232, -7835584, -5730208, -14133200, -14129088, -12019640 ], - "convertedRgb565" : [ 10565, 25222, -29816, -21364, 10950, 11080, 19657 ] } }, "diagnostics" : [ ] @@ -1782,14 +1782,14 @@ "last_modified" : 1776965154261, "fingerprint" : "b9ff45c77f7779a6acebcc537c2ddac0e27e91162b656a04dc4772ec5f24fc26", "metadata" : { + "palette" : { + "originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ], + "convertedRgb565" : [ -29816, -21364, 25222, 10565 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQICAwMBAQIBAQMDAgIBAQIBAQEBAgIBAgICAgEBAgIEAwMCAgEBAQMEAwMCAgEBAQMEAwMDAgIBAwMEAwMDAw==" - }, - "palette" : { - "originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ], - "convertedRgb565" : [ -29816, -21364, 25222, 10565 ] } }, "diagnostics" : [ ] @@ -1800,14 +1800,14 @@ "last_modified" : 1776965154261, "fingerprint" : "43f370324fedccda888d49409e2a64f182f08b4143b4bdd5bafaf498feebd21a", "metadata" : { + "palette" : { + "originalArgb8888" : [ -5730208, -7835584, -10465232, -14145496 ], + "convertedRgb565" : [ -21364, -29816, 25222, 10565 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQICAwMBAQICAgEBAwMCAgIBAQICAgIBAQECAgEBAQECAgIBAQMDBAICAQEDAwQDAQEDAwMEAwIDAwMDBAMDAg==" - }, - "palette" : { - "originalArgb8888" : [ -5730208, -7835584, -10465232, -14145496 ], - "convertedRgb565" : [ -21364, -29816, 25222, 10565 ] } }, "diagnostics" : [ ] @@ -1818,14 +1818,14 @@ "last_modified" : 1776965154262, "fingerprint" : "cb047e655f4e196f22ca0048149df5dcb959f895faeaeb94920182d4a2d16b38", "metadata" : { + "palette" : { + "originalArgb8888" : [ -7835584, -5730208, -14145496, -10465232, -14133200, -14129088, -12019640 ], + "convertedRgb565" : [ -29816, -21364, 10565, 25222, 10950, 11080, 19657 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQIDBAQBBAMCAgMEBAMDBQIDBAQDBQYFAwMEAwUFBgcEAwMFBgYHBwEBAwUGBwcHAQEDAwcHBwcBBAQDBQcHBw==" - }, - "palette" : { - "originalArgb8888" : [ -7835584, -5730208, -14145496, -10465232, -14133200, -14129088, -12019640 ], - "convertedRgb565" : [ -29816, -21364, 10565, 25222, 10950, 11080, 19657 ] } }, "diagnostics" : [ ] @@ -1836,14 +1836,14 @@ "last_modified" : 1776965154262, "fingerprint" : "25eba17b67dd5159f5fb9d3edd531d54a44d1c76f997385840dac53517b98886", "metadata" : { + "palette" : { + "originalArgb8888" : [ -12019640, -14145496, -7835584, -10465232, -14133200, -14129088 ], + "convertedRgb565" : [ 19657, 10565, -29816, 25222, 10950, 11080 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAgMDBAQBAQIDAwMDBAEBAgMDAwMEAQECAwMEAgQBAQIEAgUFAgEBBQUGBQYFAQEBAQEGAQYBAQEBAQEBAQ==" - }, - "palette" : { - "originalArgb8888" : [ -12019640, -14145496, -7835584, -10465232, -14133200, -14129088 ], - "convertedRgb565" : [ 19657, 10565, -29816, 25222, 10950, 11080 ] } }, "diagnostics" : [ ] @@ -1854,14 +1854,14 @@ "last_modified" : 1776965154262, "fingerprint" : "32ca96c14290de8009fd2db623f55514bed2345b7d5881a80dd3fe6235012854", "metadata" : { + "palette" : { + "originalArgb8888" : [ -10465232, -14145496, -14133200, -7835584, -14129088, -12019640 ], + "convertedRgb565" : [ 25222, 10565, 10950, -29816, 11080, 19657 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQEBAgEBAgICAQICAQIBAQICAwICAQEEAgMDAwIBAQQDAwUDAwIBAQUFBgUFAgEEBgYGBgYDAgEGBgYGBgYDAg==" - }, - "palette" : { - "originalArgb8888" : [ -10465232, -14145496, -14133200, -7835584, -14129088, -12019640 ], - "convertedRgb565" : [ 25222, 10565, 10950, -29816, 11080, 19657 ] } }, "diagnostics" : [ ] @@ -1872,14 +1872,14 @@ "last_modified" : 1776965154263, "fingerprint" : "7a093fe00c805bcf38d4facff14a6d4c2b8baeaa2d553fa8d3db9f2d76b32f3b", "metadata" : { + "palette" : { + "originalArgb8888" : [ -14145496, -10465232, -7835584, -14133200, -14129088, -12019640 ], + "convertedRgb565" : [ 10565, 25222, -29816, 10950, 11080, 19657 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQECAgECAgICAgECAQECAQMCAgEBBAEBAwICAQQEBAECAgEEBAUEBAMCAQUFBgUFAgEEBgYGBgYBBAYGBgYGBg==" - }, - "palette" : { - "originalArgb8888" : [ -14145496, -10465232, -7835584, -14133200, -14129088, -12019640 ], - "convertedRgb565" : [ 10565, 25222, -29816, 10950, 11080, 19657 ] } }, "diagnostics" : [ ] @@ -1890,14 +1890,14 @@ "last_modified" : 1776965154263, "fingerprint" : "77c5277220421226d16f58b1bcfb4eaf157fa4f6c740c5aaebefaf88fb91c22e", "metadata" : { + "palette" : { + "originalArgb8888" : [ -10465232, -7835584, -14145496, -12019640, -14133200, -14129088 ], + "convertedRgb565" : [ 25222, -29816, 10565, 19657, 10950, 11080 ] + }, "tile" : { "width" : 8, "height" : 8, "paletteIndices" : "AQECAgMEBAQBAgICAgMEBAECAgICAwQEAQMBAgIDBAQDBQUDAQMEBAUGBQYFBQQEBgQGBAQEBAQEBAQEBAQEBA==" - }, - "palette" : { - "originalArgb8888" : [ -10465232, -7835584, -14145496, -12019640, -14133200, -14129088 ], - "convertedRgb565" : [ 25222, -29816, 10565, 19657, 10950, 11080 ] } }, "diagnostics" : [ ] @@ -1909,7 +1909,7 @@ "relative_path" : "scene-bank.pack-request.json", "mime_type" : "application/json", "size" : 1313159, - "last_modified" : 1777630380963, + "last_modified" : 1778159580103, "fingerprint" : "31b7a0019dc08d84af95893b349839ba2975e0f8094d8eafdc1b4c48841709f8", "metadata" : { "scenePackRequest" : { diff --git a/test-projects/main/src/main.pbs b/test-projects/main/src/main.pbs index ce88eaf8..1cdf6722 100644 --- a/test-projects/main/src/main.pbs +++ b/test-projects/main/src/main.pbs @@ -15,6 +15,8 @@ declare global camera_mult: int = 1; declare const MAX_FRAMES: int = 4; declare const CAMERA_SPEED: int = 1; +declare struct Bla (a: int) {} + [Init] fn init() -> void { @@ -25,6 +27,7 @@ fn init() -> void camera_x = 0; camera_mult = 1; Composer.bind_scene(0); + let bla: Bla = new Bla(1); } [Frame] @@ -55,9 +58,8 @@ fn frame() -> void } } - - let touch : InputTouch = Input.touch(); + let pad : InputPad = Input.pad(); if (touch.button().released()) { @@ -93,15 +95,15 @@ fn frame() -> void let b : int = 15; let total : int = a + b; - if (Input.pad().a().pressed()) + if (pad.a().pressed()) { total += 25; } - else if (Input.pad().b().pressed()) + else if (pad.b().pressed()) { total += 5; } - else if (Input.pad().x().pressed()) + else if (pad.x().pressed()) { total -= 10; } diff --git a/tools/vscode-extension/.gitignore b/tools/vscode-extension/.gitignore new file mode 100644 index 00000000..12ee8905 --- /dev/null +++ b/tools/vscode-extension/.gitignore @@ -0,0 +1,38 @@ +# Dependencies +node_modules/ + +# Build output +out/ +dist/ + +# VS Code package output +*.vsix + +# TypeScript +*.tsbuildinfo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Test / coverage +coverage/ +.vscode-test/ + +# Environment / local config +.env +.env.* +!.env.example + +# OS files +.DS_Store +Thumbs.db + +# Editor local state +.vscode/settings.json +.vscode/*.log + +# AI +debug \ No newline at end of file diff --git a/tools/vscode-extension/.vscode/launch.json b/tools/vscode-extension/.vscode/launch.json new file mode 100644 index 00000000..24c92b0e --- /dev/null +++ b/tools/vscode-extension/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Prometeu VS Code Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "npm: compile" + } + ] +} \ No newline at end of file diff --git a/tools/vscode-extension/.vscode/tasks.json b/tools/vscode-extension/.vscode/tasks.json new file mode 100644 index 00000000..53ec0d6a --- /dev/null +++ b/tools/vscode-extension/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "compile", + "group": "build", + "problemMatcher": "$tsc", + "label": "npm: compile" + } + ] +} \ No newline at end of file diff --git a/tools/vscode-extension/README.md b/tools/vscode-extension/README.md new file mode 100644 index 00000000..7c647b1e --- /dev/null +++ b/tools/vscode-extension/README.md @@ -0,0 +1,155 @@ +# Prometeu VS Code Extension + +VS Code bridge for Prometeu Studio. + +This extension does **not** start the language server itself. It connects to the Prometeu Studio LSP server running locally. + +```text +VS Code Extension + -> vscode-languageclient + -> TCP 127.0.0.1:7775 + -> Prometeu Studio Java/LSP4J +``` + +## Requirements + +* Node.js +* npm +* VS Code +* Prometeu Studio running with the embedded LSP server enabled + +The Studio LSP server must be listening on: + +```text +127.0.0.1:7775 +``` + +## Install dependencies + +From this directory: + +```bash +npm install +``` + +## Compile + +```bash +npm run compile +``` + +This generates: + +```text +out/extension.js +``` + +## Run in development mode + +Open this extension folder in VS Code: + +```bash +code . +``` + +Press: + +```text +F5 +``` + +This opens a second VS Code window called **Extension Development Host**. + +In that second window, open a Prometeu project and create/open a file like: + +```text +main.pbs +``` + +or: + +```text +main.barrel +``` + +Expected output in: + +```text +View -> Output -> Prometeu LSP +``` + +```text +Prometeu VS Code extension activated. +Connecting to Prometeu Studio LSP at 127.0.0.1:7775... +TCP connection established with Prometeu Studio. +Prometeu Studio LSP client started. +[Info] Hello from Prometeu Studio LSP +``` + +## Run commands manually + +In the Extension Development Host window: + +```text +Cmd+Shift+P +Prometeu: Connect to Studio LSP +``` + +To restart the client: + +```text +Cmd+Shift+P +Prometeu: Restart Studio LSP +``` + +## Package as VSIX + +```bash +npm run compile +vsce package --allow-missing-repository +``` + +This creates a file like: + +```text +prometeu-vscode-extension-0.0.1.vsix +``` + +## Install locally + +```bash +code --install-extension prometeu-vscode-extension-0.0.1.vsix --force +``` + +Then open a Prometeu project normally: + +```bash +code /path/to/prometeu-project +``` + +With Prometeu Studio running, opening `.pbs` or `.barrel` files should activate the extension and connect to the Studio LSP server. + +## Configuration + +Default settings: + +```json +{ + "prometeu.studio.host": "127.0.0.1", + "prometeu.studio.port": 7775 +} +``` + +These can be changed in VS Code settings if needed. + +## Current status + +The extension currently provides only the minimal LSP client bridge: + +* registers `.pbs` +* registers `.barrel` +* connects to Prometeu Studio over TCP +* starts a VS Code `LanguageClient` +* receives LSP messages from the Java/LSP4J server + +The Studio remains the source of trut diff --git a/tools/vscode-extension/out/extension.js b/tools/vscode-extension/out/extension.js new file mode 100644 index 00000000..f967e639 --- /dev/null +++ b/tools/vscode-extension/out/extension.js @@ -0,0 +1,338 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.activate = activate; +exports.deactivate = deactivate; +const net = __importStar(require("node:net")); +const vscode = __importStar(require("vscode")); +const node_1 = require("vscode-languageclient/node"); +let client; +let output; +let reconnectTimer; +let reconnectContext; +let stoppingClient = false; +let lastAppliedThemeSignature; +const RECONNECT_DELAY_MS = 2000; +const VSCODE_HOST_ID = "vscode"; +function activate(context) { + reconnectContext = context; + output = vscode.window.createOutputChannel("Prometeu LSP"); + output.appendLine("Prometeu VS Code extension activated."); + const connectCommand = vscode.commands.registerCommand("prometeu.connectStudioLsp", async () => { + await startClient(context); + }); + const restartCommand = vscode.commands.registerCommand("prometeu.restartStudioLsp", async () => { + await stopClient(); + await startClient(context); + }); + context.subscriptions.push(output, connectCommand, restartCommand); + void startClient(context); +} +async function deactivate() { + await stopClient(); +} +async function startClient(context) { + clearReconnectTimer(); + if (client) { + output.appendLine("Prometeu LSP client is already running."); + return; + } + const config = vscode.workspace.getConfiguration("prometeu.studio"); + const host = config.get("host", "127.0.0.1"); + const port = config.get("port", 7775); + output.show(true); + output.appendLine(`Connecting to Prometeu Studio LSP at ${host}:${port}...`); + const serverOptions = async () => { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host, port }); + let settled = false; + const connectTimeout = setTimeout(() => { + if (settled) { + return; + } + settled = true; + socket.destroy(); + reject(new Error("Connection timed out.")); + }, 3000); + socket.once("connect", () => { + if (settled) { + return; + } + settled = true; + clearTimeout(connectTimeout); + output.appendLine("TCP connection established with Prometeu Studio."); + resolve({ + reader: socket, + writer: socket + }); + }); + socket.once("error", (error) => { + if (settled) { + return; + } + settled = true; + clearTimeout(connectTimeout); + reject(error); + }); + }); + }; + const clientOptions = { + documentSelector: [ + { + scheme: "file", + language: "pbs" + } + ], + synchronize: { + fileEvents: [ + vscode.workspace.createFileSystemWatcher("**/*.pbs") + ] + }, + initializationOptions: { + clientKind: "vscode", + prometeuClientVersion: context.extension.packageJSON.version + }, + outputChannel: output, + errorHandler: { + error: (error) => { + output.appendLine(`Prometeu LSP transport error: ${error.message}`); + return { action: node_1.ErrorAction.Continue }; + }, + closed: () => { + output.appendLine("Prometeu LSP transport closed."); + return { action: node_1.CloseAction.DoNotRestart }; + } + } + }; + const nextClient = new node_1.LanguageClient("prometeuStudioLsp", "Prometeu Studio LSP", serverOptions, clientOptions); + client = nextClient; + nextClient.onDidChangeState((event) => { + output.appendLine(`Prometeu LSP state changed: ${node_1.State[event.oldState]} -> ${node_1.State[event.newState]}`); + if (event.newState !== node_1.State.Stopped) { + return; + } + if (client === nextClient) { + client = undefined; + } + if (!stoppingClient) { + scheduleReconnect(); + } + }); + try { + await nextClient.start(); + await applyPrometeuVisualTheme(nextClient.initializeResult); + output.appendLine("Prometeu Studio LSP client started."); + vscode.window.showInformationMessage("Connected to Prometeu Studio LSP."); + } + catch (error) { + if (client === nextClient) { + client = undefined; + } + const message = error instanceof Error ? error.message : String(error); + output.appendLine(`Failed to start Prometeu LSP client: ${message}`); + vscode.window.showErrorMessage(`Could not connect to Prometeu Studio LSP: ${message}`); + scheduleReconnect(); + } +} +async function stopClient() { + clearReconnectTimer(); + if (!client) { + return; + } + const currentClient = client; + client = undefined; + stoppingClient = true; + output.appendLine("Stopping Prometeu Studio LSP client..."); + try { + await currentClient.stop(); + output.appendLine("Prometeu Studio LSP client stopped."); + } + catch (error) { + const message = error instanceof Error ? error.message : String(error); + output.appendLine(`Failed to stop Prometeu LSP client cleanly: ${message}`); + } + finally { + stoppingClient = false; + } +} +function scheduleReconnect() { + if (!reconnectContext) { + return; + } + if (reconnectTimer || client) { + return; + } + output.appendLine(`Scheduling Prometeu LSP reconnect in ${RECONNECT_DELAY_MS}ms...`); + reconnectTimer = setTimeout(() => { + reconnectTimer = undefined; + void startClient(reconnectContext); + }, RECONNECT_DELAY_MS); +} +function clearReconnectTimer() { + if (!reconnectTimer) { + return; + } + clearTimeout(reconnectTimer); + reconnectTimer = undefined; +} +async function applyPrometeuVisualTheme(initializeResult) { + const visualThemesPayload = extractPrometeuVisualThemesPayload(initializeResult); + if (!visualThemesPayload) { + output.appendLine("Prometeu visual theme payload not present in initialize result."); + return; + } + const semanticProjectionPayload = extractPrometeuSemanticHostProjectionsPayload(initializeResult); + const vscodeProjection = selectHostProjection(semanticProjectionPayload, VSCODE_HOST_ID); + if (!semanticProjectionPayload) { + output.appendLine("Prometeu semantic host projection payload not present in initialize result."); + } + else if (!vscodeProjection) { + output.appendLine("Prometeu semantic host projection payload does not declare a vscode projection."); + } + const activeTheme = visualThemesPayload.visualThemes.find((theme) => theme.themeId === visualThemesPayload.activeVisualThemeId); + if (!activeTheme) { + output.appendLine(`Prometeu visual theme '${visualThemesPayload.activeVisualThemeId}' was not declared by the server.`); + return; + } + const themeSignature = JSON.stringify({ + languageId: visualThemesPayload.frontendLanguageId, + themeId: activeTheme.themeId, + editorPalette: activeTheme.editorPalette, + tokenStyles: activeTheme.tokenStyles, + semanticProjection: vscodeProjection ?? null + }); + if (themeSignature === lastAppliedThemeSignature) { + output.appendLine(`Prometeu visual theme '${activeTheme.themeId}' is already applied.`); + return; + } + const configurationTarget = vscode.workspace.workspaceFolders?.length + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + const configuration = vscode.workspace.getConfiguration(); + const existingSemanticCustomizations = configuration.get("editor.semanticTokenColorCustomizations") ?? {}; + const semanticRules = { + ...(readObject(existingSemanticCustomizations.rules)), + ...buildSemanticTokenRules(visualThemesPayload.frontendLanguageId, activeTheme, vscodeProjection) + }; + await configuration.update("editor.semanticTokenColorCustomizations", { + ...existingSemanticCustomizations, + enabled: true, + rules: semanticRules + }, configurationTarget); + const existingWorkbenchColors = configuration.get("workbench.colorCustomizations") ?? {}; + await configuration.update("workbench.colorCustomizations", { + ...existingWorkbenchColors, + "editor.foreground": activeTheme.editorPalette.baseForeground, + "editor.selectionBackground": activeTheme.editorPalette.selectionBackground, + "editorLineNumber.foreground": activeTheme.editorPalette.lineNumberForeground + }, configurationTarget); + lastAppliedThemeSignature = themeSignature; + output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${visualThemesPayload.frontendLanguageId}'.`); +} +function extractPrometeuVisualThemesPayload(initializeResult) { + if (!initializeResult || typeof initializeResult !== "object") { + return undefined; + } + const capabilities = readObject(initializeResult.capabilities); + const experimental = readObject(capabilities.experimental); + const payload = readObject(experimental.prometeuVisualThemes); + if (typeof payload.frontendLanguageId !== "string" || typeof payload.activeVisualThemeId !== "string") { + return undefined; + } + if (!Array.isArray(payload.visualThemes)) { + return undefined; + } + return payload; +} +function extractPrometeuSemanticHostProjectionsPayload(initializeResult) { + if (!initializeResult || typeof initializeResult !== "object") { + return undefined; + } + const capabilities = readObject(initializeResult.capabilities); + const experimental = readObject(capabilities.experimental); + const payload = readObject(experimental.prometeuSemanticHostProjections); + if (typeof payload.frontendLanguageId !== "string" || !Array.isArray(payload.semanticKeys) || !Array.isArray(payload.hostProjections)) { + return undefined; + } + return payload; +} +function selectHostProjection(payload, hostId) { + if (!payload) { + return undefined; + } + return payload.hostProjections.find((projection) => projection.hostId === hostId); +} +function buildSemanticTokenRules(languageId, theme, hostProjection) { + const rules = {}; + const projectionBySemanticKey = new Map((hostProjection?.tokenProjections ?? []).map((projection) => [projection.semanticKey, projection])); + for (const tokenStyle of theme.tokenStyles) { + const styleValue = !tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline + ? tokenStyle.foreground + : { + foreground: tokenStyle.foreground, + ...(tokenStyle.italic ? { italic: true } : {}), + ...(tokenStyle.bold ? { bold: true } : {}), + ...(tokenStyle.underline ? { underline: true } : {}) + }; + rules[`${tokenStyle.semanticKey}:${languageId}`] = styleValue; + const projection = projectionBySemanticKey.get(tokenStyle.semanticKey); + if (!projection) { + continue; + } + const projectedSelector = buildSemanticSelector(languageId, projection.hostTokenType, projection.hostTokenModifiers); + if (!(projectedSelector in rules)) { + rules[projectedSelector] = styleValue; + } + const fallbackSelector = buildSemanticSelector(languageId, projection.fallbackTokenType, projection.fallbackTokenModifiers); + if (!(fallbackSelector in rules)) { + rules[fallbackSelector] = styleValue; + } + } + return rules; +} +function buildSemanticSelector(languageId, tokenType, modifiers) { + const normalizedModifiers = [...modifiers].sort(); + if (normalizedModifiers.length === 0) { + return `${tokenType}:${languageId}`; + } + return `${tokenType}.${normalizedModifiers.join(".")}:${languageId}`; +} +function readObject(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + return value; +} +//# sourceMappingURL=extension.js.map \ No newline at end of file diff --git a/tools/vscode-extension/package-lock.json b/tools/vscode-extension/package-lock.json new file mode 100644 index 00000000..6cc8d203 --- /dev/null +++ b/tools/vscode-extension/package-lock.json @@ -0,0 +1,139 @@ +{ + "name": "prometeu-vscode-extension", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prometeu-vscode-extension", + "version": "0.1.0", + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.90.0", + "typescript": "^5.0.0" + }, + "engines": { + "vscode": "^1.90.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.118.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.118.0.tgz", + "integrity": "sha512-Ah6eTlqDcwIMELEVwQMO++rJAFBRz/oLluLD/vWdYrH1KuI9kfpaM+7pg0OvvascgcJy+ghLCERAYouM4QbzGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/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/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", + "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", + "license": "MIT", + "dependencies": { + "minimatch": "^5.1.0", + "semver": "^7.3.7", + "vscode-languageserver-protocol": "3.17.5" + }, + "engines": { + "vscode": "^1.82.0" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + } + } +} diff --git a/tools/vscode-extension/package.json b/tools/vscode-extension/package.json new file mode 100644 index 00000000..823a02a4 --- /dev/null +++ b/tools/vscode-extension/package.json @@ -0,0 +1,72 @@ +{ + "name": "prometeu-vscode-extension", + "displayName": "Prometeu VS Code Extension", + "description": "VS Code bridge for Prometeu Studio.", + "version": "0.1.0", + "private": true, + "engines": { + "vscode": "^1.90.0" + }, + "categories": [ + "Programming Languages" + ], + "activationEvents": [ + "onLanguage:pbs" + ], + "main": "./out/extension.js", + "contributes": { + "languages": [ + { + "id": "pbs", + "aliases": [ + "PBS", + "Prometeu Base Script" + ], + "extensions": [ + ".pbs", + ".barrel" + ] + } + ], + "commands": [ + { + "command": "prometeu.connectStudioLsp", + "title": "Prometeu: Connect to Studio LSP" + }, + { + "command": "prometeu.restartStudioLsp", + "title": "Prometeu: Restart Studio LSP" + } + ], + "configuration": { + "title": "Prometeu", + "properties": { + "prometeu.studio.host": { + "type": "string", + "default": "127.0.0.1", + "description": "Prometeu Studio LSP host." + }, + "prometeu.studio.port": { + "type": "number", + "default": 7775, + "description": "Prometeu Studio LSP TCP port." + } + } + }, + "configurationDefaults": { + "editor.semanticHighlighting.enabled": true + } + }, + "scripts": { + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.90.0", + "typescript": "^5.0.0" + } +} diff --git a/tools/vscode-extension/src/extension.ts b/tools/vscode-extension/src/extension.ts new file mode 100644 index 00000000..090ae8d6 --- /dev/null +++ b/tools/vscode-extension/src/extension.ts @@ -0,0 +1,447 @@ +import * as net from "node:net"; +import * as vscode from "vscode"; + +import { + CloseAction, + ErrorAction, + LanguageClient, + LanguageClientOptions, + ServerOptions, + State, + StreamInfo +} from "vscode-languageclient/node"; + +let client: LanguageClient | undefined; +let output: vscode.OutputChannel; +let reconnectTimer: NodeJS.Timeout | undefined; +let reconnectContext: vscode.ExtensionContext | undefined; +let stoppingClient = false; +let lastAppliedThemeSignature: string | undefined; + +const RECONNECT_DELAY_MS = 2000; + +type PrometeuTokenStyle = { + semanticKey: string; + foreground: string; + italic: boolean; + bold: boolean; + underline: boolean; +}; + +type PrometeuEditorPalette = { + baseForeground: string; + selectionBackground: string; + activeHighlightBackground: string; + lineNumberForeground: string; + statusChipBackground: string; + statusChipBorder: string; + statusChipForeground: string; +}; + +type PrometeuVisualTheme = { + themeId: string; + displayName: string; + editorPalette: PrometeuEditorPalette; + tokenStyles: PrometeuTokenStyle[]; +}; + +type PrometeuVisualThemesPayload = { + frontendLanguageId: string; + activeVisualThemeId: string; + visualThemes: PrometeuVisualTheme[]; +}; + +type PrometeuSemanticHostProjectionEntry = { + semanticKey: string; + hostTokenType: string; + hostTokenModifiers: string[]; + fallbackTokenType: string; + fallbackTokenModifiers: string[]; +}; + +type PrometeuSemanticHostProjection = { + hostId: string; + tokenProjections: PrometeuSemanticHostProjectionEntry[]; +}; + +type PrometeuSemanticHostProjectionsPayload = { + frontendLanguageId: string; + semanticKeys: string[]; + hostProjections: PrometeuSemanticHostProjection[]; +}; + +const VSCODE_HOST_ID = "vscode"; + +export function activate(context: vscode.ExtensionContext): void { + reconnectContext = context; + output = vscode.window.createOutputChannel("Prometeu LSP"); + output.appendLine("Prometeu VS Code extension activated."); + + const connectCommand = vscode.commands.registerCommand( + "prometeu.connectStudioLsp", + async () => { + await startClient(context); + } + ); + + const restartCommand = vscode.commands.registerCommand( + "prometeu.restartStudioLsp", + async () => { + await stopClient(); + await startClient(context); + } + ); + + context.subscriptions.push(output, connectCommand, restartCommand); + + void startClient(context); +} + +export async function deactivate(): Promise { + await stopClient(); +} + +async function startClient(context: vscode.ExtensionContext): Promise { + clearReconnectTimer(); + + if (client) { + output.appendLine("Prometeu LSP client is already running."); + return; + } + + const config = vscode.workspace.getConfiguration("prometeu.studio"); + + const host = config.get("host", "127.0.0.1"); + const port = config.get("port", 7775); + + output.show(true); + output.appendLine(`Connecting to Prometeu Studio LSP at ${host}:${port}...`); + + const serverOptions: ServerOptions = async (): Promise => { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host, port }); + + let settled = false; + + const connectTimeout = setTimeout(() => { + if (settled) { + return; + } + + settled = true; + socket.destroy(); + reject(new Error("Connection timed out.")); + }, 3000); + + socket.once("connect", () => { + if (settled) { + return; + } + + settled = true; + clearTimeout(connectTimeout); + + output.appendLine("TCP connection established with Prometeu Studio."); + + resolve({ + reader: socket, + writer: socket + }); + }); + + socket.once("error", (error) => { + if (settled) { + return; + } + + settled = true; + clearTimeout(connectTimeout); + + reject(error); + }); + }); + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [ + { + scheme: "file", + language: "pbs" + } + ], + synchronize: { + fileEvents: [ + vscode.workspace.createFileSystemWatcher("**/*.pbs") + ] + }, + initializationOptions: { + clientKind: "vscode", + prometeuClientVersion: context.extension.packageJSON.version + }, + outputChannel: output, + errorHandler: { + error: (error) => { + output.appendLine(`Prometeu LSP transport error: ${error.message}`); + return { action: ErrorAction.Continue }; + }, + closed: () => { + output.appendLine("Prometeu LSP transport closed."); + return { action: CloseAction.DoNotRestart }; + } + } + }; + + const nextClient = new LanguageClient( + "prometeuStudioLsp", + "Prometeu Studio LSP", + serverOptions, + clientOptions + ); + client = nextClient; + nextClient.onDidChangeState((event) => { + output.appendLine(`Prometeu LSP state changed: ${State[event.oldState]} -> ${State[event.newState]}`); + if (event.newState !== State.Stopped) { + return; + } + if (client === nextClient) { + client = undefined; + } + if (!stoppingClient) { + scheduleReconnect(); + } + }); + + try { + await nextClient.start(); + await applyPrometeuVisualTheme(nextClient.initializeResult); + + output.appendLine("Prometeu Studio LSP client started."); + vscode.window.showInformationMessage("Connected to Prometeu Studio LSP."); + } catch (error) { + if (client === nextClient) { + client = undefined; + } + + const message = error instanceof Error ? error.message : String(error); + + output.appendLine(`Failed to start Prometeu LSP client: ${message}`); + vscode.window.showErrorMessage(`Could not connect to Prometeu Studio LSP: ${message}`); + scheduleReconnect(); + } +} + +async function stopClient(): Promise { + clearReconnectTimer(); + if (!client) { + return; + } + + const currentClient = client; + client = undefined; + stoppingClient = true; + + output.appendLine("Stopping Prometeu Studio LSP client..."); + + try { + await currentClient.stop(); + output.appendLine("Prometeu Studio LSP client stopped."); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + output.appendLine(`Failed to stop Prometeu LSP client cleanly: ${message}`); + } finally { + stoppingClient = false; + } +} + +function scheduleReconnect(): void { + if (!reconnectContext) { + return; + } + if (reconnectTimer || client) { + return; + } + output.appendLine(`Scheduling Prometeu LSP reconnect in ${RECONNECT_DELAY_MS}ms...`); + reconnectTimer = setTimeout(() => { + reconnectTimer = undefined; + void startClient(reconnectContext!); + }, RECONNECT_DELAY_MS); +} + +function clearReconnectTimer(): void { + if (!reconnectTimer) { + return; + } + clearTimeout(reconnectTimer); + reconnectTimer = undefined; +} + +async function applyPrometeuVisualTheme(initializeResult: unknown): Promise { + const visualThemesPayload = extractPrometeuVisualThemesPayload(initializeResult); + if (!visualThemesPayload) { + output.appendLine("Prometeu visual theme payload not present in initialize result."); + return; + } + const semanticProjectionPayload = extractPrometeuSemanticHostProjectionsPayload(initializeResult); + const vscodeProjection = selectHostProjection(semanticProjectionPayload, VSCODE_HOST_ID); + if (!semanticProjectionPayload) { + output.appendLine("Prometeu semantic host projection payload not present in initialize result."); + } else if (!vscodeProjection) { + output.appendLine("Prometeu semantic host projection payload does not declare a vscode projection."); + } + + const activeTheme = visualThemesPayload.visualThemes.find((theme) => theme.themeId === visualThemesPayload.activeVisualThemeId); + if (!activeTheme) { + output.appendLine(`Prometeu visual theme '${visualThemesPayload.activeVisualThemeId}' was not declared by the server.`); + return; + } + + const themeSignature = JSON.stringify({ + languageId: visualThemesPayload.frontendLanguageId, + themeId: activeTheme.themeId, + editorPalette: activeTheme.editorPalette, + tokenStyles: activeTheme.tokenStyles, + semanticProjection: vscodeProjection ?? null + }); + if (themeSignature === lastAppliedThemeSignature) { + output.appendLine(`Prometeu visual theme '${activeTheme.themeId}' is already applied.`); + return; + } + + const configurationTarget = vscode.workspace.workspaceFolders?.length + ? vscode.ConfigurationTarget.Workspace + : vscode.ConfigurationTarget.Global; + const configuration = vscode.workspace.getConfiguration(); + + const existingSemanticCustomizations = configuration.get>("editor.semanticTokenColorCustomizations") ?? {}; + const semanticRules = { + ...(readObject(existingSemanticCustomizations.rules)), + ...buildSemanticTokenRules(visualThemesPayload.frontendLanguageId, activeTheme, vscodeProjection) + }; + + await configuration.update( + "editor.semanticTokenColorCustomizations", + { + ...existingSemanticCustomizations, + enabled: true, + rules: semanticRules + }, + configurationTarget + ); + + const existingWorkbenchColors = configuration.get>("workbench.colorCustomizations") ?? {}; + await configuration.update( + "workbench.colorCustomizations", + { + ...existingWorkbenchColors, + "editor.foreground": activeTheme.editorPalette.baseForeground, + "editor.selectionBackground": activeTheme.editorPalette.selectionBackground, + "editorLineNumber.foreground": activeTheme.editorPalette.lineNumberForeground + }, + configurationTarget + ); + + lastAppliedThemeSignature = themeSignature; + output.appendLine(`Applied Prometeu visual theme '${activeTheme.themeId}' for language '${visualThemesPayload.frontendLanguageId}'.`); +} + +function extractPrometeuVisualThemesPayload(initializeResult: unknown): PrometeuVisualThemesPayload | undefined { + if (!initializeResult || typeof initializeResult !== "object") { + return undefined; + } + + const capabilities = readObject((initializeResult as { capabilities?: unknown }).capabilities); + const experimental = readObject(capabilities.experimental); + const payload = readObject(experimental.prometeuVisualThemes); + + if (typeof payload.frontendLanguageId !== "string" || typeof payload.activeVisualThemeId !== "string") { + return undefined; + } + if (!Array.isArray(payload.visualThemes)) { + return undefined; + } + + return payload as unknown as PrometeuVisualThemesPayload; +} + +function extractPrometeuSemanticHostProjectionsPayload(initializeResult: unknown): PrometeuSemanticHostProjectionsPayload | undefined { + if (!initializeResult || typeof initializeResult !== "object") { + return undefined; + } + + const capabilities = readObject((initializeResult as { capabilities?: unknown }).capabilities); + const experimental = readObject(capabilities.experimental); + const payload = readObject(experimental.prometeuSemanticHostProjections); + + if (typeof payload.frontendLanguageId !== "string" || !Array.isArray(payload.semanticKeys) || !Array.isArray(payload.hostProjections)) { + return undefined; + } + + return payload as unknown as PrometeuSemanticHostProjectionsPayload; +} + +function selectHostProjection( + payload: PrometeuSemanticHostProjectionsPayload | undefined, + hostId: string +): PrometeuSemanticHostProjection | undefined { + if (!payload) { + return undefined; + } + return payload.hostProjections.find((projection) => projection.hostId === hostId); +} + +function buildSemanticTokenRules( + languageId: string, + theme: PrometeuVisualTheme, + hostProjection: PrometeuSemanticHostProjection | undefined +): Record { + const rules: Record = {}; + const projectionBySemanticKey = new Map( + (hostProjection?.tokenProjections ?? []).map((projection) => [projection.semanticKey, projection]) + ); + for (const tokenStyle of theme.tokenStyles) { + const styleValue = !tokenStyle.italic && !tokenStyle.bold && !tokenStyle.underline + ? tokenStyle.foreground + : { + foreground: tokenStyle.foreground, + ...(tokenStyle.italic ? { italic: true } : {}), + ...(tokenStyle.bold ? { bold: true } : {}), + ...(tokenStyle.underline ? { underline: true } : {}) + }; + rules[`${tokenStyle.semanticKey}:${languageId}`] = styleValue; + + const projection = projectionBySemanticKey.get(tokenStyle.semanticKey); + if (!projection) { + continue; + } + + const projectedSelector = buildSemanticSelector(languageId, projection.hostTokenType, projection.hostTokenModifiers); + if (!(projectedSelector in rules)) { + rules[projectedSelector] = styleValue; + } + + const fallbackSelector = buildSemanticSelector(languageId, projection.fallbackTokenType, projection.fallbackTokenModifiers); + if (!(fallbackSelector in rules)) { + rules[fallbackSelector] = styleValue; + } + } + return rules; +} + +function buildSemanticSelector( + languageId: string, + tokenType: string, + modifiers: string[] +): string { + const normalizedModifiers = [...modifiers].sort(); + if (normalizedModifiers.length === 0) { + return `${tokenType}:${languageId}`; + } + return `${tokenType}.${normalizedModifiers.join(".")}:${languageId}`; +} + +function readObject(value: unknown): Record { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return {}; + } + return value as Record; +} diff --git a/tools/vscode-extension/tsconfig.json b/tools/vscode-extension/tsconfig.json new file mode 100644 index 00000000..5864bac8 --- /dev/null +++ b/tools/vscode-extension/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": [ + "ES2022" + ], + "outDir": "out", + "rootDir": "src", + "sourceMap": true, + "strict": true, + "moduleResolution": "Node16", + "esModuleInterop": true, + "skipLibCheck": true + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} \ No newline at end of file