From 306266ae40eb889b7bf4130df69b5fbd371db08e Mon Sep 17 00:00:00 2001 From: Konrad Neitzel Date: Thu, 12 Nov 2026 07:49:06 +0100 Subject: [PATCH] Establish buildable modules and shared host-service boundaries --- .gitattributes | 2 + .gitignore | 5 +- README.md | 20 +- THIRD-PARTY-NOTICES.md | 4 +- designer-contracts/pom.xml | 51 ++++ .../contracts/DocumentSnapshot.java | 14 + .../fxdesigner/contracts/EditBatch.java | 40 +++ .../fxdesigner/contracts/EditBatchTest.java | 37 +++ designer-core/pom.xml | 56 ++++ .../fxdesigner/host/ConfiguredRuntimes.java | 27 ++ .../fxdesigner/host/DocumentSession.java | 23 ++ .../neitzel/fxdesigner/host/HostServices.java | 109 ++++++++ .../fxdesigner/host/ProcessBuilds.java | 81 ++++++ .../neitzel/fxdesigner/host/SessionScope.java | 38 +++ .../fxdesigner/host/HostServicesTest.java | 94 +++++++ designer-intellij/build.gradle.kts | 35 +++ designer-intellij/gradle.properties | 2 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 45457 bytes .../gradle/wrapper/gradle-wrapper.properties | 9 + designer-intellij/gradlew | 248 ++++++++++++++++++ designer-intellij/gradlew.bat | 93 +++++++ designer-intellij/settings.gradle.kts | 2 + .../intellij/IntellijDocuments.java | 213 +++++++++++++++ .../fxdesigner/intellij/IntellijHost.java | 68 +++++ .../src/main/resources/META-INF/plugin.xml | 5 + .../intellij/IntellijDocumentsTest.java | 72 +++++ designer-kit-ui/pom.xml | 58 ++++ .../de/neitzel/fxdesigner/kit/KitView.java | 30 +++ designer-preview-api/pom.xml | 51 ++++ .../fxdesigner/preview/ExampleDescriptor.java | 12 + designer-standalone/pom.xml | 53 ++++ .../fxdesigner/standalone/FileDocuments.java | 153 +++++++++++ .../fxdesigner/standalone/StandaloneHost.java | 33 +++ .../standalone/FileDocumentsTest.java | 70 +++++ designer-target-tools/pom.xml | 64 +++++ .../fxdesigner/target/TargetToolInfo.java | 17 ++ docs/initial-project-contract.md | 2 +- docs/module-host-boundaries.md | 50 ++++ docs/repository-bootstrap-plan.md | 6 +- pom.xml | 18 +- scripts/check-bootstrap.py | 38 ++- 41 files changed, 1980 insertions(+), 23 deletions(-) create mode 100644 designer-contracts/pom.xml create mode 100644 designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/DocumentSnapshot.java create mode 100644 designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/EditBatch.java create mode 100644 designer-contracts/src/test/java/de/neitzel/fxdesigner/contracts/EditBatchTest.java create mode 100644 designer-core/pom.xml create mode 100644 designer-core/src/main/java/de/neitzel/fxdesigner/host/ConfiguredRuntimes.java create mode 100644 designer-core/src/main/java/de/neitzel/fxdesigner/host/DocumentSession.java create mode 100644 designer-core/src/main/java/de/neitzel/fxdesigner/host/HostServices.java create mode 100644 designer-core/src/main/java/de/neitzel/fxdesigner/host/ProcessBuilds.java create mode 100644 designer-core/src/main/java/de/neitzel/fxdesigner/host/SessionScope.java create mode 100644 designer-core/src/test/java/de/neitzel/fxdesigner/host/HostServicesTest.java create mode 100644 designer-intellij/build.gradle.kts create mode 100644 designer-intellij/gradle.properties create mode 100644 designer-intellij/gradle/wrapper/gradle-wrapper.jar create mode 100644 designer-intellij/gradle/wrapper/gradle-wrapper.properties create mode 100755 designer-intellij/gradlew create mode 100644 designer-intellij/gradlew.bat create mode 100644 designer-intellij/settings.gradle.kts create mode 100644 designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijDocuments.java create mode 100644 designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijHost.java create mode 100644 designer-intellij/src/main/resources/META-INF/plugin.xml create mode 100644 designer-intellij/src/test/java/de/neitzel/fxdesigner/intellij/IntellijDocumentsTest.java create mode 100644 designer-kit-ui/pom.xml create mode 100644 designer-kit-ui/src/main/java/de/neitzel/fxdesigner/kit/KitView.java create mode 100644 designer-preview-api/pom.xml create mode 100644 designer-preview-api/src/main/java/de/neitzel/fxdesigner/preview/ExampleDescriptor.java create mode 100644 designer-standalone/pom.xml create mode 100644 designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/FileDocuments.java create mode 100644 designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/StandaloneHost.java create mode 100644 designer-standalone/src/test/java/de/neitzel/fxdesigner/standalone/FileDocumentsTest.java create mode 100644 designer-target-tools/pom.xml create mode 100644 designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/TargetToolInfo.java create mode 100644 docs/module-host-boundaries.md diff --git a/.gitattributes b/.gitattributes index ba049f8..456fdb4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,5 @@ mvnw text eol=lf mvnw.cmd text eol=crlf *.md text eol=lf *.py text eol=lf +gradlew text eol=lf +gradlew.bat text eol=crlf diff --git a/.gitignore b/.gitignore index 37e1326..ae40462 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Build output and local designer state -**/target/ +/target/ +/*/target/ **/build/ .gradle/ .neitzel-fx-designer/ @@ -12,3 +13,5 @@ !.env.example __pycache__/ *.py[cod] + +.intellijPlatform/ diff --git a/README.md b/README.md index ce7fe7e..06df75d 100644 --- a/README.md +++ b/README.md @@ -4,35 +4,29 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with planned standalone and ## Current state -This initial repository contains the accepted contracts, bootstrap plan, project instructions and a pinned Maven build foundation (`de.neitzel.fxdesigner:neitzel-fx-designer:0.1.0-SNAPSHOT`). It currently has **no application sources, implemented modules, runnable designer or packaged IntelliJ plugin**. The root POM validates the build environment; it does not yet build a UI. The module layout in the contract is planned work. +The repository builds six shared Maven modules and a separate IntelliJ adapter plugin. Document transactions, native undo, build execution/cancellation, explicit runtime mappings and lifecycle services have initial implementations and tests. The shared UI exposes a Kit construction seam. There is **no runnable designer application or registered IntelliJ designer editor yet**. See [module and host boundaries](docs/module-host-boundaries.md) for the implemented scope and limitations. The Component builder fix is integrated in NeitzelLib main. A [verified local development snapshot](docs/runtime/neitzellib-baseline.md) is recorded with source/artifact identity; a corrected released version is still unestablished. NeitzelLib sources and prototype implementations are not copied here. ## Validate the foundation -Requirements: JDK 21 or newer, Python 3.10 or newer, and network access on the first Maven/dependency download. Select the intended JDK through `JAVA_HOME`. +Requirements: JDK 25 for builds, JDK 21 for target linkage validation, Python 3.10+, the recorded local NeitzelLib artifacts, and IntelliJ IDEA SDK build **263.7307.200**. Downloads require network access. Set `JAVA_HOME` to JDK 25 and supply explicit local paths: ```sh -python3 scripts/check-bootstrap.py +python3 scripts/check-bootstrap.py --idea-home "$IDEA_HOME" --target-java-home "$TARGET_JAVA_HOME" ``` -This runs Maven's clean/verify lifecycle through the pinned wrapper and validates local documentation links and illustrative JSON. Maven is pinned to **3.9.11** using Apache Maven Wrapper **3.3.4**. No system Maven installation is needed for this command. On Windows the check invokes `mvnw.cmd`; validation has initially been performed on macOS only. +On macOS `IDEA_HOME` points to the IDE application's `Contents` directory. The current SDK check uses its macOS product-info layout; other platforms are not qualified. The command checks runtime artifact hashes, clean-builds/installs the Maven modules, tests the adapters, checks bytecode baselines, runs target linkage under the supplied target JDK, builds the plugin ZIP, and validates documentation. It does not install or publish NeitzelLib. -For just the Maven foundation: - -```sh -./mvnw --batch-mode --no-transfer-progress clean verify -``` - -The JDK requirement above is for the bootstrap, not a declaration that all future modules or JavaFX 26 hosts run on Java 21. Target/host and bytecode baselines are established independently in the implementation plan. +Maven is pinned to **3.9.11** (Wrapper **3.3.4**); the separate IntelliJ build uses **Gradle 9.1.0** and IntelliJ Platform Gradle Plugin **2.20.1**. Wrapper distributions have pinned checksums. Shared-module-only validation is `./mvnw -B -ntp clean verify`; it does not qualify IntelliJ packaging. Plugin output is under `designer-intellij/build/distributions/`. ## Implementation references - [Initial project contract](docs/initial-project-contract.md): module responsibilities, configuration, snapshots, runtimes, preview sessions and communication. -- [Repository bootstrap plan](docs/repository-bootstrap-plan.md): ordered work packages and acceptance gates. Next: establish the buildable module and host-service boundaries; the corrected local NeitzelLib baseline is prepared. +- [Repository bootstrap plan](docs/repository-bootstrap-plan.md): ordered work packages and acceptance gates. Next: make configuration and compatibility executable. - [Decision provenance](docs/provenance.md): workspace planning and captured prototype evidence. - [Third-party notices](THIRD-PARTY-NOTICES.md): Maven wrapper provenance. The canonical checkout is `/Volumes/PlanckSSD/KI/java/NeitzelFXDesigner`. Its Git remote is `ssh://git@gitea.neitzel.de:2222/konrad/NeitzelFXDesigner.git` and its default branch is `main`. -Validate the installed corrected development runtime separately with `python3 scripts/check-neitzellib-artifact.py`. This requires the recorded local artifacts; the repository foundation check does not install or publish NeitzelLib. +Validate the installed corrected development runtime separately with `python3 scripts/check-neitzellib-artifact.py`. This requires the recorded local artifacts; the full check also verifies these hashes. diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 68eed62..5eaacbf 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -2,4 +2,6 @@ `mvnw`, `mvnw.cmd` and `.mvn/wrapper/maven-wrapper.properties` are generated using Apache Maven Wrapper Plugin 3.3.4 (only-script distribution). The wrapper scripts retain their Apache Software Foundation copyright/license headers. See the [Apache Maven Wrapper documentation](https://maven.apache.org/tools/wrapper/) and [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). -No Scene Builder Kit, IntelliJ, JavaFX, NeitzelLib or prototype source/binary is included in this bootstrap. Track provenance and required notices when dependencies or patches are introduced; this notice does not assign a license to original NeitzelFXDesigner code. +The Gradle wrapper in `designer-intellij/` is generated by Gradle 9.1.0; its scripts and JAR retain upstream notices (Apache License 2.0). The wrapper distribution checksum is pinned. Build dependencies are resolved from upstream artifact repositories, not copied source trees. + +The shared UI depends on Scene Builder Kit 26.0.0 and OpenJFX 26. The IntelliJ adapter builds against the locally supplied IntelliJ SDK with IntelliJ Platform Gradle Plugin 2.20.1. Target tools depend on the separately recorded NeitzelLib development artifacts. Preserve the upstream license/notice resources in packaged dependencies. Product distribution and any patched Kit source import require their separate notice/provenance review; this foundation does not assign a license to original NeitzelFXDesigner code. diff --git a/designer-contracts/pom.xml b/designer-contracts/pom.xml new file mode 100644 index 0000000..70919da --- /dev/null +++ b/designer-contracts/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + de.neitzel.fxdesigner + neitzel-fx-designer + 0.1.0-SNAPSHOT + + designer-contracts + + 21 + + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + module-dependency-boundary + + enforce + + + + + true + + com.intellij:* + com.jetbrains:* + com.jetbrains.intellij.platform:* + com.gluonhq.scenebuilder:* + org.openjfx:* + de.neitzel.lib:* + + + + + + + + + + diff --git a/designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/DocumentSnapshot.java b/designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/DocumentSnapshot.java new file mode 100644 index 0000000..d4b11e9 --- /dev/null +++ b/designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/DocumentSnapshot.java @@ -0,0 +1,14 @@ +package de.neitzel.fxdesigner.contracts; + +import java.net.URI; +import java.util.Objects; + +/** Immutable host-owned draft; revision ordering is independent of native undo stamps. */ +public record DocumentSnapshot( + URI document, long revision, String text, boolean writable, boolean dirty) { + public DocumentSnapshot { + Objects.requireNonNull(document); + Objects.requireNonNull(text); + if (revision < 0) throw new IllegalArgumentException("Negative revision"); + } +} diff --git a/designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/EditBatch.java b/designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/EditBatch.java new file mode 100644 index 0000000..a483620 --- /dev/null +++ b/designer-contracts/src/main/java/de/neitzel/fxdesigner/contracts/EditBatch.java @@ -0,0 +1,40 @@ +package de.neitzel.fxdesigner.contracts; + +import java.util.*; + +/** One named undo transaction. All UTF-16 ranges address the same expected revision. */ +public record EditBatch(long expectedRevision, String name, List replacements) { + public record Replacement(int start, int end, String text) { + public Replacement { + if (start < 0 || end < start) throw new IllegalArgumentException("Invalid range"); + Objects.requireNonNull(text); + } + } + + public EditBatch { + if (expectedRevision < 0) throw new IllegalArgumentException("Negative revision"); + if (name == null || name.isBlank()) throw new IllegalArgumentException("Missing command name"); + replacements = List.copyOf(replacements); + } + + /** Validates every range before computing a result; never partially edits a host draft. */ + public String applyTo(DocumentSnapshot draft) { + if (!draft.writable()) throw new IllegalStateException("Document is read-only"); + if (draft.revision() != expectedRevision) + throw new ConcurrentModificationException("Stale document revision"); + var ordered = new ArrayList<>(replacements); + ordered.sort(Comparator.comparingInt(Replacement::start)); + int previousEnd = -1; + for (var r : ordered) { + if (r.end() > draft.text().length() || r.start() < previousEnd) + throw new IllegalArgumentException("Overlapping/out-of-bounds edits"); + previousEnd = r.end(); + } + var result = new StringBuilder(draft.text()); + for (int i = ordered.size() - 1; i >= 0; i--) { + var r = ordered.get(i); + result.replace(r.start(), r.end(), r.text()); + } + return result.toString(); + } +} diff --git a/designer-contracts/src/test/java/de/neitzel/fxdesigner/contracts/EditBatchTest.java b/designer-contracts/src/test/java/de/neitzel/fxdesigner/contracts/EditBatchTest.java new file mode 100644 index 0000000..d630906 --- /dev/null +++ b/designer-contracts/src/test/java/de/neitzel/fxdesigner/contracts/EditBatchTest.java @@ -0,0 +1,37 @@ +package de.neitzel.fxdesigner.contracts; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; +import java.util.*; +import org.junit.jupiter.api.Test; + +class EditBatchTest { + private DocumentSnapshot draft(long r, boolean writable) { + return new DocumentSnapshot(URI.create("file:/sample.fxml"), r, "abcdef", writable, false); + } + + @Test + void multipleRangesUseOriginalOffsets() { + var b = + new EditBatch( + 4, + "Rename", + List.of(new EditBatch.Replacement(1, 2, "LONG"), new EditBatch.Replacement(4, 6, "!"))); + assertEquals("aLONGcd!", b.applyTo(draft(4, true))); + } + + @Test + void rejectsStaleReadOnlyAndOverlappingTransactions() { + var b = new EditBatch(3, "Change", List.of(new EditBatch.Replacement(0, 2, "x"))); + assertThrows(ConcurrentModificationException.class, () -> b.applyTo(draft(4, true))); + assertThrows(IllegalStateException.class, () -> b.applyTo(draft(3, false))); + var overlap = + new EditBatch( + 3, + "Bad", + List.of(new EditBatch.Replacement(1, 4, ""), new EditBatch.Replacement(3, 5, ""))); + assertThrows(IllegalArgumentException.class, () -> overlap.applyTo(draft(3, true))); + assertEquals("abcdef", draft(3, true).text()); + } +} diff --git a/designer-core/pom.xml b/designer-core/pom.xml new file mode 100644 index 0000000..7e817f5 --- /dev/null +++ b/designer-core/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + de.neitzel.fxdesigner + neitzel-fx-designer + 0.1.0-SNAPSHOT + + designer-core + + 21 + + + + de.neitzel.fxdesigner + designer-contracts + ${project.version} + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + module-dependency-boundary + + enforce + + + + + true + + com.intellij:* + com.jetbrains:* + com.jetbrains.intellij.platform:* + com.gluonhq.scenebuilder:* + org.openjfx:* + de.neitzel.lib:* + + + + + + + + + + diff --git a/designer-core/src/main/java/de/neitzel/fxdesigner/host/ConfiguredRuntimes.java b/designer-core/src/main/java/de/neitzel/fxdesigner/host/ConfiguredRuntimes.java new file mode 100644 index 0000000..16d4edf --- /dev/null +++ b/designer-core/src/main/java/de/neitzel/fxdesigner/host/ConfiguredRuntimes.java @@ -0,0 +1,27 @@ +package de.neitzel.fxdesigner.host; + +import java.util.*; +import java.util.concurrent.*; + +/** + * Explicit local mappings; no fallback to the host JVM. Discovery/requirements arrive with + * configuration. + */ +public final class ConfiguredRuntimes implements HostServices.Runtimes { + public record Key(String logicalName, HostServices.RuntimeRole role) {} + + private final Map installations; + + public ConfiguredRuntimes(Map installations) { + this.installations = Map.copyOf(installations); + } + + public CompletionStage resolve( + String name, HostServices.RuntimeRole role) { + var found = installations.get(new Key(name, role)); + return found == null + ? CompletableFuture.failedFuture( + new IllegalStateException("Configure runtime " + name + " for " + role)) + : CompletableFuture.completedFuture(found); + } +} diff --git a/designer-core/src/main/java/de/neitzel/fxdesigner/host/DocumentSession.java b/designer-core/src/main/java/de/neitzel/fxdesigner/host/DocumentSession.java new file mode 100644 index 0000000..cce1122 --- /dev/null +++ b/designer-core/src/main/java/de/neitzel/fxdesigner/host/DocumentSession.java @@ -0,0 +1,23 @@ +package de.neitzel.fxdesigner.host; + +import de.neitzel.fxdesigner.contracts.*; +import java.util.concurrent.CompletionStage; + +/** + * Host draft/undo authority. Futures complete after host-thread work; never block its UI thread. + */ +public interface DocumentSession extends AutoCloseable { + CompletionStage snapshot(); + + CompletionStage apply(EditBatch edits); + + CompletionStage undo(); + + CompletionStage redo(); + + CompletionStage save(long expectedRevision); + + /** Releases this handle; does not save, discard or close unrelated host documents. */ + @Override + void close(); +} diff --git a/designer-core/src/main/java/de/neitzel/fxdesigner/host/HostServices.java b/designer-core/src/main/java/de/neitzel/fxdesigner/host/HostServices.java new file mode 100644 index 0000000..a8ea1c8 --- /dev/null +++ b/designer-core/src/main/java/de/neitzel/fxdesigner/host/HostServices.java @@ -0,0 +1,109 @@ +package de.neitzel.fxdesigner.host; + +import java.net.URI; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; + +/** Host integrations supplied to each designer session, with no host SDK types. */ +public record HostServices( + Documents documents, + Builds builds, + Runtimes runtimes, + Navigation navigation, + Diagnostics diagnostics, + SessionScope lifecycle) { + public HostServices { + Objects.requireNonNull(documents); + Objects.requireNonNull(builds); + Objects.requireNonNull(runtimes); + Objects.requireNonNull(navigation); + Objects.requireNonNull(diagnostics); + Objects.requireNonNull(lifecycle); + } + + public interface Documents { + CompletionStage open(URI document); + } + + public enum RuntimeRole { + BUILD, + TARGET + } + + public record RuntimeInstallation(Path javaHome, int javaFeature) { + public RuntimeInstallation { + Objects.requireNonNull(javaHome); + if (javaFeature < 1) throw new IllegalArgumentException("Invalid Java feature"); + } + } + + public interface Runtimes { + CompletionStage resolve(String logicalName, RuntimeRole role); + } + + public record Location(URI document, int offset) { + public Location { + Objects.requireNonNull(document); + if (offset < 0) throw new IllegalArgumentException("Negative offset"); + } + } + + public interface Navigation { + CompletionStage open(Location location); + } + + public enum Severity { + INFO, + WARNING, + ERROR + } + + public record Diagnostic( + String operation, Severity severity, String message, Optional location) { + public Diagnostic { + Objects.requireNonNull(operation); + Objects.requireNonNull(severity); + Objects.requireNonNull(message); + Objects.requireNonNull(location); + } + } + + public interface Diagnostics { + void report(Diagnostic diagnostic); + } + + public record BuildRequest( + Path directory, List command, Map environment) { + public BuildRequest { + Objects.requireNonNull(directory); + command = List.copyOf(command); + environment = Map.copyOf(environment); + if (command.isEmpty()) throw new IllegalArgumentException("Empty build command"); + } + } + + public record BuildOutput(boolean stderr, String line) {} + + public record BuildResult(int exitCode, boolean cancelled) {} + + public interface BuildRun extends AutoCloseable { + CompletionStage completion(); + + void cancel(); + + @Override + default void close() { + cancel(); + } + } + + /** + * Executes an already-resolved build plan; Maven resolution/snapshot capture belong to later + * steps. + */ + public interface Builds { + BuildRun start(BuildRequest request, Consumer output) throws java.io.IOException; + } +} diff --git a/designer-core/src/main/java/de/neitzel/fxdesigner/host/ProcessBuilds.java b/designer-core/src/main/java/de/neitzel/fxdesigner/host/ProcessBuilds.java new file mode 100644 index 0000000..a0a910f --- /dev/null +++ b/designer-core/src/main/java/de/neitzel/fxdesigner/host/ProcessBuilds.java @@ -0,0 +1,81 @@ +package de.neitzel.fxdesigner.host; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +/** Shared process executor used by both adapters; host output is supplied explicitly. */ +public final class ProcessBuilds implements HostServices.Builds { + @Override + public HostServices.BuildRun start( + HostServices.BuildRequest request, Consumer output) + throws IOException { + var builder = new ProcessBuilder(request.command()).directory(request.directory().toFile()); + builder.environment().putAll(request.environment()); + var process = builder.start(); + var cancelled = new AtomicBoolean(); + var completion = new CompletableFuture(); + var out = pump(process.getInputStream(), false, output); + var err = pump(process.getErrorStream(), true, output); + Thread.ofVirtual() + .start( + () -> { + try { + int exit = process.waitFor(); + out.join(); + err.join(); + completion.complete(new HostServices.BuildResult(exit, cancelled.get())); + } catch (Exception e) { + completion.completeExceptionally(e); + } + }); + return new HostServices.BuildRun() { + public CompletionStage completion() { + return completion; + } + + public void cancel() { + if (!process.isAlive()) return; + cancelled.set(true); + var descendants = process.descendants().toList(); + descendants.forEach(ProcessHandle::destroy); + process.destroy(); + Thread.ofVirtual() + .start( + () -> { + try { + if (!process.waitFor(2, TimeUnit.SECONDS)) process.destroyForcibly(); + descendants.stream() + .filter(ProcessHandle::isAlive) + .forEach(ProcessHandle::destroyForcibly); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + process.destroyForcibly(); + } + }); + } + }; + } + + private static Thread pump( + InputStream stream, boolean stderr, Consumer output) { + return Thread.ofVirtual() + .start( + () -> { + try (var reader = + new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) { + for (String line; (line = reader.readLine()) != null; ) { + try { + output.accept(new HostServices.BuildOutput(stderr, line)); + } catch (RuntimeException ignored) { + /* A UI callback cannot stop draining the process pipe. */ + } + } + } catch (IOException ignored) { + /* Process cancellation may close the pipe. */ + } + }); + } +} diff --git a/designer-core/src/main/java/de/neitzel/fxdesigner/host/SessionScope.java b/designer-core/src/main/java/de/neitzel/fxdesigner/host/SessionScope.java new file mode 100644 index 0000000..5a34798 --- /dev/null +++ b/designer-core/src/main/java/de/neitzel/fxdesigner/host/SessionScope.java @@ -0,0 +1,38 @@ +package de.neitzel.fxdesigner.host; + +import java.util.ArrayDeque; + +/** Session-local resource ownership, closed once in reverse acquisition order. */ +public final class SessionScope implements AutoCloseable { + private final ArrayDeque resources = new ArrayDeque<>(); + private boolean closed; + + public synchronized T own(T resource) { + if (closed) { + try { + resource.close(); + } catch (Exception e) { + throw new IllegalStateException("Closed scope cleanup failed", e); + } + throw new IllegalStateException("Session closed"); + } + resources.push(resource); + return resource; + } + + @Override + public synchronized void close() { + if (closed) return; + closed = true; + RuntimeException failure = null; + while (!resources.isEmpty()) { + try { + resources.pop().close(); + } catch (Exception e) { + if (failure == null) failure = new IllegalStateException("Session cleanup failed"); + failure.addSuppressed(e); + } + } + if (failure != null) throw failure; + } +} diff --git a/designer-core/src/test/java/de/neitzel/fxdesigner/host/HostServicesTest.java b/designer-core/src/test/java/de/neitzel/fxdesigner/host/HostServicesTest.java new file mode 100644 index 0000000..23c78d9 --- /dev/null +++ b/designer-core/src/test/java/de/neitzel/fxdesigner/host/HostServicesTest.java @@ -0,0 +1,94 @@ +package de.neitzel.fxdesigner.host; + +import static org.junit.jupiter.api.Assertions.*; + +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; +import org.junit.jupiter.api.Test; + +class HostServicesTest { + @Test + void scopesAreIndependentAndCloseInReverseOrder() { + var seen = new ArrayList(); + var first = new SessionScope(); + var other = new SessionScope(); + first.own(() -> seen.add(1)); + first.own(() -> seen.add(2)); + other.own(() -> seen.add(3)); + first.close(); + first.close(); + assertEquals(List.of(2, 1), seen); + other.close(); + assertEquals(List.of(2, 1, 3), seen); + } + + @Test + void cleanupContinuesAfterFailureAndLateResourcesClose() { + var scope = new SessionScope(); + var seen = new ArrayList(); + scope.own(() -> seen.add(1)); + scope.own( + () -> { + throw new Exception("broken"); + }); + assertThrows(IllegalStateException.class, scope::close); + assertEquals(List.of(1), seen); + assertThrows(IllegalStateException.class, () -> scope.own(() -> seen.add(2))); + assertEquals(List.of(1, 2), seen); + } + + @Test + void runtimeRolesNeverFallBack() { + var runtime = new HostServices.RuntimeInstallation(Path.of("/explicit"), 21); + var r = + new ConfiguredRuntimes( + Map.of(new ConfiguredRuntimes.Key("java", HostServices.RuntimeRole.TARGET), runtime)); + assertEquals( + runtime, r.resolve("java", HostServices.RuntimeRole.TARGET).toCompletableFuture().join()); + assertThrows( + CompletionException.class, + () -> r.resolve("java", HostServices.RuntimeRole.BUILD).toCompletableFuture().join()); + } + + @Test + void buildCapturesBothStreamsAndCancellation() throws Exception { + String java = Path.of(System.getProperty("java.home"), "bin", "java").toString(); + var lines = new CopyOnWriteArrayList(); + var builds = new ProcessBuilds(); + var run = + builds.start( + new HostServices.BuildRequest( + Path.of("."), + List.of(java, "-cp", System.getProperty("java.class.path"), Child.class.getName()), + Map.of()), + lines::add); + var done = run.completion().toCompletableFuture().get(10, TimeUnit.SECONDS); + assertEquals(0, done.exitCode()); + assertFalse(done.cancelled()); + assertTrue(lines.stream().anyMatch(x -> x.stderr() && x.line().equals("error output"))); + assertTrue(lines.stream().anyMatch(x -> !x.stderr() && x.line().equals("normal output"))); + var pending = + builds.start( + new HostServices.BuildRequest( + Path.of("."), + List.of( + java, + "-cp", + System.getProperty("java.class.path"), + Child.class.getName(), + "wait"), + Map.of()), + x -> {}); + pending.cancel(); + assertTrue(pending.completion().toCompletableFuture().get(10, TimeUnit.SECONDS).cancelled()); + } + + public static class Child { + public static void main(String[] args) throws Exception { + if (args.length > 0) Thread.sleep(30000); + System.out.println("normal output"); + System.err.println("error output"); + } + } +} diff --git a/designer-intellij/build.gradle.kts b/designer-intellij/build.gradle.kts new file mode 100644 index 0000000..270253b --- /dev/null +++ b/designer-intellij/build.gradle.kts @@ -0,0 +1,35 @@ +import org.jetbrains.intellij.platform.gradle.TestFrameworkType +plugins { + java + id("org.jetbrains.intellij.platform") version "2.20.1" +} +group = "de.neitzel.fxdesigner" +version = "0.1.0-SNAPSHOT" +repositories { + mavenLocal { content { includeGroup("de.neitzel.fxdesigner") } } + mavenCentral() + intellijPlatform { defaultRepositories() } +} +java { sourceCompatibility = JavaVersion.VERSION_25; targetCompatibility = JavaVersion.VERSION_25 } +tasks.withType().configureEach { options.release.set(25) } +val fxPlatform = if (System.getProperty("os.arch") == "aarch64") "mac-aarch64" else "mac" +dependencies { + implementation("de.neitzel.fxdesigner:designer-kit-ui:0.1.0-SNAPSHOT") + intellijPlatform { + local(providers.gradleProperty("ideaHome").orElse(providers.environmentVariable("IDEA_HOME"))) + testFramework(TestFrameworkType.Platform) + } + // Maven platform profiles are not selected by Gradle: include the qualified macOS natives explicitly. + listOf("base", "controls", "fxml", "graphics", "media", "swing", "web").forEach { + runtimeOnly("org.openjfx:javafx-$it:26:$fxPlatform") + } + testImplementation("junit:junit:4.13.2") +} +intellijPlatform { + pluginConfiguration { + name = "Neitzel FX Designer" + ideaVersion { sinceBuild = "263.7307.200"; untilBuild = "263.*" } + } + instrumentCode = false +} +tasks.test { useJUnit(); jvmArgs("-Djava.awt.headless=true") } diff --git a/designer-intellij/gradle.properties b/designer-intellij/gradle.properties new file mode 100644 index 0000000..f12ad90 --- /dev/null +++ b/designer-intellij/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx2g +org.gradle.configuration-cache=false diff --git a/designer-intellij/gradle/wrapper/gradle-wrapper.jar b/designer-intellij/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8bdaf60c75ab801e22807dde59e12a8735a34077 GIT binary patch literal 45457 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 c82=K%QXLKFx%@O{wJjEi6Y56o#$)Bpeg literal 0 HcmV?d00001 diff --git a/designer-intellij/gradle/wrapper/gradle-wrapper.properties b/designer-intellij/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e593595 --- /dev/null +++ b/designer-intellij/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,9 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists + +distributionSha256Sum=a17ddd85a26b6a7f5ddb71ff8b05fc5104c0202c6e64782429790c933686c806 diff --git a/designer-intellij/gradlew b/designer-intellij/gradlew new file mode 100755 index 0000000..adff685 --- /dev/null +++ b/designer-intellij/gradlew @@ -0,0 +1,248 @@ +#!/bin/sh + +# +# Copyright © 2015 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/designer-intellij/gradlew.bat b/designer-intellij/gradlew.bat new file mode 100644 index 0000000..c4bdd3a --- /dev/null +++ b/designer-intellij/gradlew.bat @@ -0,0 +1,93 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/designer-intellij/settings.gradle.kts b/designer-intellij/settings.gradle.kts new file mode 100644 index 0000000..d7edddc --- /dev/null +++ b/designer-intellij/settings.gradle.kts @@ -0,0 +1,2 @@ +pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } } +rootProject.name = "designer-intellij" diff --git a/designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijDocuments.java b/designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijDocuments.java new file mode 100644 index 0000000..3745694 --- /dev/null +++ b/designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijDocuments.java @@ -0,0 +1,213 @@ +package de.neitzel.fxdesigner.intellij; + +import com.intellij.openapi.application.*; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.command.undo.UndoManager; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.event.*; +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.*; +import de.neitzel.fxdesigner.contracts.*; +import de.neitzel.fxdesigner.host.*; +import java.net.URI; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Adapts actual IntelliJ Documents and native commands/history; never creates a competing undo + * store. + */ +public final class IntellijDocuments implements HostServices.Documents, AutoCloseable { + private final Project project; + private final com.intellij.openapi.Disposable lifetime; + private final Map states = new HashMap<>(); + private final Set handles = new HashSet<>(); + private boolean closed; + + private static final class State { + VirtualFile file; + Document document; + String disk; + AtomicLong revision = new AtomicLong(); + } + + public IntellijDocuments(Project project) { + this.project = Objects.requireNonNull(project); + lifetime = this::disposeResources; + Disposer.register(project, lifetime); + } + + private CompletionStage edt(Callable action) { + var future = new CompletableFuture(); + Runnable task = + () -> + WriteIntentReadAction.run( + () -> { + try { + if (closed || project.isDisposed() || Disposer.isDisposed(lifetime)) + throw new IllegalStateException("Designer host disposed"); + future.complete(action.call()); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + if (ApplicationManager.getApplication().isDispatchThread()) task.run(); + else ApplicationManager.getApplication().invokeLater(task, ModalityState.any()); + return future; + } + + public CompletionStage open(URI uri) { + return edt( + () -> { + Path path = Path.of(uri).toRealPath(); + State state = states.get(path); + if (state == null) { + state = new State(); + state.file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path); + if (state.file == null) throw new IllegalArgumentException("Missing file: " + path); + state.document = FileDocumentManager.getInstance().getDocument(state.file); + if (state.document == null) throw new IllegalArgumentException("Not a text document"); + state.disk = Files.readString(path); + var observed = state; + state.document.addDocumentListener( + new DocumentListener() { + @Override + public void documentChanged(DocumentEvent e) { + observed.revision.incrementAndGet(); + } + }, + lifetime); + states.put(path, state); + } + var handle = new Handle(state); + handles.add(handle); + return handle; + }); + } + + private DocumentSnapshot snapshot(State s) { + return new DocumentSnapshot( + Path.of(s.file.getPath()).toUri(), + s.revision.get(), + s.document.getText(), + s.document.isWritable() && s.file.isWritable(), + FileDocumentManager.getInstance().isDocumentUnsaved(s.document)); + } + + private final class Handle implements DocumentSession { + private final State s; + private boolean released; + private Editor editor; + + Handle(State s) { + this.s = s; + } + + private CompletionStage run(Callable action) { + return edt( + () -> { + if (released) throw new IllegalStateException("Document handle closed"); + return action.call(); + }); + } + + public CompletionStage snapshot() { + return run(() -> IntellijDocuments.this.snapshot(s)); + } + + public CompletionStage apply(EditBatch edits) { + return run( + () -> { + edits.applyTo(IntellijDocuments.this.snapshot(s)); + var ranges = new ArrayList<>(edits.replacements()); + ranges.sort(Comparator.comparingInt(EditBatch.Replacement::start)); + Collections.reverse(ranges); + WriteCommandAction.runWriteCommandAction( + project, + edits.name(), + java.util.UUID.randomUUID().toString(), + () -> { + for (var range : ranges) + s.document.replaceString(range.start(), range.end(), range.text()); + }); + return IntellijDocuments.this.snapshot(s); + }); + } + + public CompletionStage undo() { + return history(false); + } + + public CompletionStage redo() { + return history(true); + } + + private CompletionStage history(boolean redo) { + return run( + () -> { + if (!s.document.isWritable() || !s.file.isWritable()) + throw new IllegalStateException("Read-only document"); + if (editor == null) + editor = EditorFactory.getInstance().createEditor(s.document, project); + var textEditor = TextEditorProvider.getInstance().getTextEditor(editor); + var undo = UndoManager.getInstance(project); + if (redo) { + if (undo.isRedoAvailable(textEditor)) undo.redo(textEditor); + } else if (undo.isUndoAvailable(textEditor)) undo.undo(textEditor); + return IntellijDocuments.this.snapshot(s); + }); + } + + public CompletionStage save(long revision) { + return run( + () -> { + if (s.revision.get() != revision) + throw new ConcurrentModificationException("Stale save"); + if (!s.document.isWritable() || !s.file.isWritable()) + throw new IllegalStateException("Read-only document"); + Path file = Path.of(s.file.getPath()); + if (!Files.readString(file).equals(s.disk)) + throw new ConcurrentModificationException("External change; draft retained"); + FileDocumentManager.getInstance().saveDocument(s.document); + if (FileDocumentManager.getInstance().isDocumentUnsaved(s.document)) + throw new IllegalStateException("IDE did not save document"); + s.disk = Files.readString(file); + return IntellijDocuments.this.snapshot(s); + }); + } + + private void release() { + if (released) return; + released = true; + if (editor != null) EditorFactory.getInstance().releaseEditor(editor); + handles.remove(this); + } + + public void close() { + edt( + () -> { + release(); + return null; + }); + } + } + + private void disposeResources() { + for (var h : List.copyOf(handles)) h.release(); + closed = true; + states.clear(); + } + + public void close() { + edt( + () -> { + Disposer.dispose(lifetime); + return null; + }); + } +} diff --git a/designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijHost.java b/designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijHost.java new file mode 100644 index 0000000..da6b5de --- /dev/null +++ b/designer-intellij/src/main/java/de/neitzel/fxdesigner/intellij/IntellijHost.java @@ -0,0 +1,68 @@ +package de.neitzel.fxdesigner.intellij; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.WriteIntentReadAction; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.vfs.LocalFileSystem; +import de.neitzel.fxdesigner.host.*; +import java.nio.file.Path; +import java.util.concurrent.*; + +/** + * IDE composition; shared build execution exposes output/cancellation to IDE presentation + * callbacks. + */ +public final class IntellijHost implements AutoCloseable { + private final SessionScope scope = new SessionScope(); + private final HostServices services; + + public IntellijHost( + Project project, HostServices.Runtimes runtimes, HostServices.Diagnostics diagnostics) { + var docs = scope.own(new IntellijDocuments(project)); + var builds = new ProcessBuilds(); + services = + new HostServices( + docs, + (request, output) -> scope.own(builds.start(request, output)), + runtimes, + location -> { + var result = new CompletableFuture(); + ApplicationManager.getApplication() + .invokeLater( + () -> + WriteIntentReadAction.run( + () -> { + try { + if (project.isDisposed()) + throw new IllegalStateException("Project disposed"); + var file = + LocalFileSystem.getInstance() + .refreshAndFindFileByNioFile( + Path.of(location.document())); + if (file == null) + throw new IllegalArgumentException( + "Navigation file unavailable"); + new OpenFileDescriptor(project, file, location.offset()) + .navigate(true); + result.complete(null); + } catch (Throwable e) { + result.completeExceptionally(e); + } + })); + return result; + }, + diagnostics, + scope); + Disposer.register(project, this::close); + } + + public HostServices services() { + return services; + } + + public void close() { + scope.close(); + } +} diff --git a/designer-intellij/src/main/resources/META-INF/plugin.xml b/designer-intellij/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..5561ddb --- /dev/null +++ b/designer-intellij/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,5 @@ + + de.neitzel.fxdesignerNeitzel FX DesignerNeitzel + Module and host-service foundation. The FXML designer UI is not registered yet. + com.intellij.modules.platform + diff --git a/designer-intellij/src/test/java/de/neitzel/fxdesigner/intellij/IntellijDocumentsTest.java b/designer-intellij/src/test/java/de/neitzel/fxdesigner/intellij/IntellijDocumentsTest.java new file mode 100644 index 0000000..caccb52 --- /dev/null +++ b/designer-intellij/src/test/java/de/neitzel/fxdesigner/intellij/IntellijDocumentsTest.java @@ -0,0 +1,72 @@ +package de.neitzel.fxdesigner.intellij; + +import com.intellij.testFramework.fixtures.BasePlatformTestCase; +import de.neitzel.fxdesigner.contracts.*; +import java.nio.file.*; +import java.util.*; + +public final class IntellijDocumentsTest extends BasePlatformTestCase { + public void testNativeDocumentTransactionUndoAndIndependentHandles() throws Exception { + Path file = Files.createTempFile("designer-document-", ".fxml"); + Files.writeString(file, "abcdef"); + com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(getTestRootDisposable(), file.toRealPath().toString()); + try (var docs = new IntellijDocuments(getProject())) { + var a = docs.open(file.toUri()).toCompletableFuture().get(); + var b = docs.open(file.toUri()).toCompletableFuture().get(); + var initial = a.snapshot().toCompletableFuture().get(); + var changed = + a.apply( + new EditBatch( + initial.revision(), + "Component properties", + List.of( + new EditBatch.Replacement(0, 1, "A"), + new EditBatch.Replacement(5, 6, "F")))) + .toCompletableFuture() + .get(); + assertEquals("AbcdeF", b.snapshot().toCompletableFuture().get().text()); + a.close(); + var undone = b.undo().toCompletableFuture().get(); + assertEquals("abcdef", undone.text()); + assertTrue(undone.revision() > changed.revision()); + try { + b.apply(new EditBatch(initial.revision(), "stale", List.of())).toCompletableFuture().get(); + fail("Stale edit accepted"); + } catch (java.util.concurrent.ExecutionException expected) { + assertInstanceOf(expected.getCause(), ConcurrentModificationException.class); + } + var redone = b.redo().toCompletableFuture().get(); + assertEquals("AbcdeF", redone.text()); + b.save(redone.revision()).toCompletableFuture().get(); + assertEquals("AbcdeF", Files.readString(file)); + b.close(); + } finally { + Files.deleteIfExists(file); + } + } + + public void testExternalChangesDoNotLoseDraft() throws Exception { + Path file = Files.createTempFile("designer-conflict-", ".fxml"); + Files.writeString(file, "old"); + com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(getTestRootDisposable(), file.toRealPath().toString()); + try (var docs = new IntellijDocuments(getProject())) { + var d = docs.open(file.toUri()).toCompletableFuture().get(); + var changed = + d.apply(new EditBatch(0, "edit", List.of(new EditBatch.Replacement(0, 3, "draft")))) + .toCompletableFuture() + .get(); + Files.writeString(file, "external"); + try { + d.save(changed.revision()).toCompletableFuture().get(); + fail("Conflict accepted"); + } catch (java.util.concurrent.ExecutionException expected) { + assertInstanceOf(expected.getCause(), ConcurrentModificationException.class); + } + assertEquals("draft", d.snapshot().toCompletableFuture().get().text()); + assertEquals("external", Files.readString(file)); + d.close(); + } finally { + Files.deleteIfExists(file); + } + } +} diff --git a/designer-kit-ui/pom.xml b/designer-kit-ui/pom.xml new file mode 100644 index 0000000..f74f8e8 --- /dev/null +++ b/designer-kit-ui/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + de.neitzel.fxdesigner + neitzel-fx-designer + 0.1.0-SNAPSHOT + + designer-kit-ui + + 25 + + + + de.neitzel.fxdesigner + designer-core + ${project.version} + + + com.gluonhq.scenebuilder + kit + 26.0.0 + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + module-dependency-boundary + + enforce + + + + + true + + com.intellij:* + com.jetbrains:* + com.jetbrains.intellij.platform:* + + + + + + + + + + diff --git a/designer-kit-ui/src/main/java/de/neitzel/fxdesigner/kit/KitView.java b/designer-kit-ui/src/main/java/de/neitzel/fxdesigner/kit/KitView.java new file mode 100644 index 0000000..94b9f59 --- /dev/null +++ b/designer-kit-ui/src/main/java/de/neitzel/fxdesigner/kit/KitView.java @@ -0,0 +1,30 @@ +package de.neitzel.fxdesigner.kit; + +import com.oracle.javafx.scenebuilder.kit.editor.EditorController; +import de.neitzel.fxdesigner.host.HostServices; +import java.util.Objects; +import javafx.application.Platform; + +/** + * Shared Kit instance. Host setup/toolkit checks must complete before construction on the FX + * thread. + */ +public final class KitView { + private final HostServices host; + private final EditorController editor; + + public KitView(HostServices host) { + if (!Platform.isFxApplicationThread()) + throw new IllegalStateException("Kit requires the FX thread"); + this.host = Objects.requireNonNull(host); + this.editor = new EditorController(); + } + + public EditorController editor() { + return editor; + } + + public HostServices host() { + return host; + } +} diff --git a/designer-preview-api/pom.xml b/designer-preview-api/pom.xml new file mode 100644 index 0000000..1f23697 --- /dev/null +++ b/designer-preview-api/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + de.neitzel.fxdesigner + neitzel-fx-designer + 0.1.0-SNAPSHOT + + designer-preview-api + + 21 + + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + module-dependency-boundary + + enforce + + + + + true + + com.intellij:* + com.jetbrains:* + com.jetbrains.intellij.platform:* + com.gluonhq.scenebuilder:* + org.openjfx:* + de.neitzel.lib:* + + + + + + + + + + diff --git a/designer-preview-api/src/main/java/de/neitzel/fxdesigner/preview/ExampleDescriptor.java b/designer-preview-api/src/main/java/de/neitzel/fxdesigner/preview/ExampleDescriptor.java new file mode 100644 index 0000000..3c8be9d --- /dev/null +++ b/designer-preview-api/src/main/java/de/neitzel/fxdesigner/preview/ExampleDescriptor.java @@ -0,0 +1,12 @@ +package de.neitzel.fxdesigner.preview; + +import java.util.Objects; + +/** Non-executing example metadata. Factory/session signatures await real target fixtures. */ +public record ExampleDescriptor(String id, String label, String modelType) { + public ExampleDescriptor { + if (id == null || id.isBlank()) throw new IllegalArgumentException("Missing example ID"); + Objects.requireNonNull(label); + Objects.requireNonNull(modelType); + } +} diff --git a/designer-standalone/pom.xml b/designer-standalone/pom.xml new file mode 100644 index 0000000..e5d66b0 --- /dev/null +++ b/designer-standalone/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + de.neitzel.fxdesigner + neitzel-fx-designer + 0.1.0-SNAPSHOT + + designer-standalone + + 25 + + + + de.neitzel.fxdesigner + designer-kit-ui + ${project.version} + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + module-dependency-boundary + + enforce + + + + + true + + com.intellij:* + com.jetbrains:* + com.jetbrains.intellij.platform:* + + + + + + + + + + diff --git a/designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/FileDocuments.java b/designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/FileDocuments.java new file mode 100644 index 0000000..6b64065 --- /dev/null +++ b/designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/FileDocuments.java @@ -0,0 +1,153 @@ +package de.neitzel.fxdesigner.standalone; + +import de.neitzel.fxdesigner.contracts.*; +import de.neitzel.fxdesigner.host.*; +import java.net.URI; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; + +/** + * Standalone draft store with shared per-file history; all IO/history operations are serialized off + * UI threads. + */ +public final class FileDocuments implements HostServices.Documents, AutoCloseable { + private final ExecutorService executor = + Executors.newSingleThreadExecutor(Thread.ofVirtual().factory()); + private final Map documents = new HashMap<>(); + + private static final class State { + Path file; + String text, saved; + long revision; + ArrayDeque undo = new ArrayDeque<>(), redo = new ArrayDeque<>(); + } + + private CompletionStage submit(Callable action) { + var future = new CompletableFuture(); + try { + executor.submit( + () -> { + try { + future.complete(action.call()); + } catch (Throwable e) { + future.completeExceptionally(e); + } + }); + } catch (RejectedExecutionException e) { + future.completeExceptionally(new IllegalStateException("Host closed", e)); + } + return future; + } + + public CompletionStage open(URI uri) { + return submit( + () -> { + Path file = Path.of(uri).toRealPath(); + var state = documents.get(file); + if (state == null) { + state = new State(); + state.file = file; + state.text = state.saved = Files.readString(file); + documents.put(file, state); + } + return new Handle(state); + }); + } + + private static DocumentSnapshot snapshot(State s) { + return new DocumentSnapshot( + s.file.toUri(), s.revision, s.text, Files.isWritable(s.file), !s.text.equals(s.saved)); + } + + private final class Handle implements DocumentSession { + private final State s; + private volatile boolean closed; + + Handle(State s) { + this.s = s; + } + + private CompletionStage run(Callable action) { + return submit( + () -> { + if (closed) throw new IllegalStateException("Document handle closed"); + return action.call(); + }); + } + + public CompletionStage snapshot() { + return run(() -> FileDocuments.snapshot(s)); + } + + public CompletionStage apply(EditBatch edits) { + return run( + () -> { + String updated = edits.applyTo(FileDocuments.snapshot(s)); + if (!updated.equals(s.text)) { + s.undo.push(s.text); + s.redo.clear(); + s.text = updated; + s.revision++; + } + return FileDocuments.snapshot(s); + }); + } + + public CompletionStage undo() { + return history(s.undo, s.redo); + } + + public CompletionStage redo() { + return history(s.redo, s.undo); + } + + private CompletionStage history( + ArrayDeque from, ArrayDeque to) { + return run( + () -> { + if (!Files.isWritable(s.file)) throw new IllegalStateException("Read-only document"); + if (!from.isEmpty()) { + to.push(s.text); + s.text = from.pop(); + s.revision++; + } + return FileDocuments.snapshot(s); + }); + } + + public CompletionStage save(long revision) { + return run( + () -> { + if (s.revision != revision) throw new ConcurrentModificationException("Stale save"); + if (!Files.isWritable(s.file)) throw new IllegalStateException("Read-only document"); + if (!Files.readString(s.file).equals(s.saved)) + throw new ConcurrentModificationException("External change; draft retained"); + Path temp = Files.createTempFile(s.file.getParent(), "designer-save-", ".tmp"); + try { + Files.writeString(temp, s.text); + if (Files.getFileAttributeView(s.file, java.nio.file.attribute.PosixFileAttributeView.class) != null) { + Files.setPosixFilePermissions(temp, Files.getPosixFilePermissions(s.file)); + } + Files.move( + temp, + s.file, + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); + } finally { + Files.deleteIfExists(temp); + } + s.saved = s.text; + return FileDocuments.snapshot(s); + }); + } + + public void close() { + closed = true; + } + } + + public void close() { + executor.shutdown(); + } +} diff --git a/designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/StandaloneHost.java b/designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/StandaloneHost.java new file mode 100644 index 0000000..8656f5b --- /dev/null +++ b/designer-standalone/src/main/java/de/neitzel/fxdesigner/standalone/StandaloneHost.java @@ -0,0 +1,33 @@ +package de.neitzel.fxdesigner.standalone; + +import de.neitzel.fxdesigner.host.*; + +/** Host composition with explicit native navigation, diagnostic and runtime integrations. */ +public final class StandaloneHost implements AutoCloseable { + private final SessionScope scope = new SessionScope(); + private final FileDocuments documents = scope.own(new FileDocuments()); + private final HostServices services; + + public StandaloneHost( + HostServices.Runtimes runtimes, + HostServices.Navigation navigation, + HostServices.Diagnostics diagnostics) { + var builds = new ProcessBuilds(); + services = + new HostServices( + documents, + (request, output) -> scope.own(builds.start(request, output)), + runtimes, + navigation, + diagnostics, + scope); + } + + public HostServices services() { + return services; + } + + public void close() { + scope.close(); + } +} diff --git a/designer-standalone/src/test/java/de/neitzel/fxdesigner/standalone/FileDocumentsTest.java b/designer-standalone/src/test/java/de/neitzel/fxdesigner/standalone/FileDocumentsTest.java new file mode 100644 index 0000000..a6f088c --- /dev/null +++ b/designer-standalone/src/test/java/de/neitzel/fxdesigner/standalone/FileDocumentsTest.java @@ -0,0 +1,70 @@ +package de.neitzel.fxdesigner.standalone; + +import static org.junit.jupiter.api.Assertions.*; + +import de.neitzel.fxdesigner.contracts.*; +import java.nio.file.*; +import java.util.*; +import java.util.concurrent.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class FileDocumentsTest { + @TempDir Path folder; + + @Test + void sharedDraftHistorySurvivesClosingOneHandleAndUndoNeverReusesRevision() throws Exception { + Path file = folder.resolve("View.fxml"); + Files.writeString(file, "abcdef"); + var permissions = Files.getPosixFilePermissions(file); + try (var docs = new FileDocuments()) { + var a = docs.open(file.toUri()).toCompletableFuture().get(); + var b = docs.open(file.toUri()).toCompletableFuture().get(); + var start = a.snapshot().toCompletableFuture().get(); + var changed = + a.apply( + new EditBatch( + start.revision(), + "two edits", + List.of( + new EditBatch.Replacement(0, 1, "A"), + new EditBatch.Replacement(5, 6, "F")))) + .toCompletableFuture() + .get(); + assertEquals("AbcdeF", b.snapshot().toCompletableFuture().get().text()); + a.close(); + var undone = b.undo().toCompletableFuture().get(); + assertEquals("abcdef", undone.text()); + assertTrue(undone.revision() > changed.revision()); + assertThrows( + ExecutionException.class, + () -> + b.apply(new EditBatch(start.revision(), "stale", List.of())) + .toCompletableFuture() + .get()); + var redone = b.redo().toCompletableFuture().get(); + b.save(redone.revision()).toCompletableFuture().get(); + assertEquals("AbcdeF", Files.readString(file)); + assertEquals(permissions, Files.getPosixFilePermissions(file)); + assertFalse(b.snapshot().toCompletableFuture().get().dirty()); + } + } + + @Test + void externalChangeBlocksSaveWithoutLosingDraft() throws Exception { + Path file = folder.resolve("View.fxml"); + Files.writeString(file, "old"); + try (var docs = new FileDocuments()) { + var d = docs.open(file.toUri()).toCompletableFuture().get(); + var changed = + d.apply(new EditBatch(0, "edit", List.of(new EditBatch.Replacement(0, 3, "draft")))) + .toCompletableFuture() + .get(); + Files.writeString(file, "external"); + assertThrows( + ExecutionException.class, () -> d.save(changed.revision()).toCompletableFuture().get()); + assertEquals("external", Files.readString(file)); + assertEquals("draft", d.snapshot().toCompletableFuture().get().text()); + } + } +} diff --git a/designer-target-tools/pom.xml b/designer-target-tools/pom.xml new file mode 100644 index 0000000..b0485f7 --- /dev/null +++ b/designer-target-tools/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + de.neitzel.fxdesigner + neitzel-fx-designer + 0.1.0-SNAPSHOT + + designer-target-tools + + 21 + + + + de.neitzel.fxdesigner + designer-contracts + ${project.version} + + + de.neitzel.fxdesigner + designer-preview-api + ${project.version} + + + de.neitzel.lib + fx + 1.0-SNAPSHOT + + + org.junit.jupiter + junit-jupiter + test + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + module-dependency-boundary + + enforce + + + + + true + + com.intellij:* + com.jetbrains:* + com.jetbrains.intellij.platform:* + com.gluonhq.scenebuilder:* + + + + + + + + + + diff --git a/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/TargetToolInfo.java b/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/TargetToolInfo.java new file mode 100644 index 0000000..0df0335 --- /dev/null +++ b/designer-target-tools/src/main/java/de/neitzel/fxdesigner/target/TargetToolInfo.java @@ -0,0 +1,17 @@ +package de.neitzel.fxdesigner.target; + +import de.neitzel.fx.view.ViewLoader; +import de.neitzel.fxdesigner.contracts.DocumentSnapshot; +import de.neitzel.fxdesigner.preview.ExampleDescriptor; + +/** Target-side linkage smoke entry point; it is not yet a discovery/rendering worker. */ +public final class TargetToolInfo { + private TargetToolInfo() {} + + public static void main(String[] args) { + System.out.println("target-java=" + Runtime.version().feature()); + System.out.println("loader=" + ViewLoader.class.getName()); + System.out.println("preview-api=" + ExampleDescriptor.class.getName()); + System.out.println("contracts=" + DocumentSnapshot.class.getName()); + } +} diff --git a/docs/initial-project-contract.md b/docs/initial-project-contract.md index df97145..5d629e0 100644 --- a/docs/initial-project-contract.md +++ b/docs/initial-project-contract.md @@ -16,7 +16,7 @@ Decision provenance: [accepted workspace decisions](provenance.md). Execution or ## Modules and dependency direction -Names are proposed, while the responsibility boundaries are accepted. +The artifact names below are established by work package 3. See [implemented boundaries and baselines](module-host-boundaries.md); the broader responsibilities remain the target architecture. | Module/directory | Responsibility | Boundary | | --- | --- | --- | diff --git a/docs/module-host-boundaries.md b/docs/module-host-boundaries.md new file mode 100644 index 0000000..c5b11f1 --- /dev/null +++ b/docs/module-host-boundaries.md @@ -0,0 +1,50 @@ +# Buildable module and host-service boundaries + +Work package 3 implements a buildable service foundation. It does not register an IntelliJ editor, launch a standalone designer, initialize JavaFX inside IntelliJ, or implement target discovery/rendering. The earlier prototypes remain separate evidence. + +## Artifacts + +| Artifact | Java release | Dependencies/responsibility | +| --- | --- | --- | +| designer-contracts | 21 | Immutable draft and edit DTOs; no framework dependencies | +| designer-preview-api | 21 | Provisional example descriptor; no framework dependencies | +| designer-core | 21 | Contracts; host interfaces, runtime mappings, process builds, lifecycle scope | +| designer-kit-ui | 25 | Core and Scene Builder Kit 26.0.0 (JavaFX 26); FX-thread construction seam | +| designer-standalone | 25 | Shared Kit UI; file-backed drafts and undo adapter | +| designer-target-tools | 21 | Contracts, preview API, recorded NeitzelLib snapshot; linkage smoke entry point | +| designer-intellij | 25 | Shared Kit UI; actual IDE Documents, commands, navigation, project disposal | + +```mermaid +flowchart LR + IDEA[IntelliJ adapter] --> UI[Shared Kit UI] + Standalone[Standalone adapter] --> UI + UI --> Core[Core / HostServices] + Core --> Contracts[Contracts] + Target[Target tools: separate process] --> Contracts + Target --> Preview[Preview API] + Target --> Library[NeitzelLib] +``` + +The Maven reactor enforces transitive dependency exclusions: contracts/core/preview cannot acquire JavaFX, Kit, IntelliJ or NeitzelLib; target tools cannot acquire Kit or IntelliJ; other shared modules cannot acquire IntelliJ. The orchestration script verifies compiled class versions. There are no JPMS descriptors yet. Module-path qualification and public preview lifecycle signatures remain work package 6. + +## Service contract + +`HostServices` supplies document access, builds, explicit runtime resolution, navigation, diagnostics and an owned `SessionScope`. Core imports neither host implementation. Each host owns its services; closing one handle does not close sibling handles or implicitly save a document. + +Document operations return completion stages. Callers must not block UI threads waiting for them. Standalone IO/history runs on a serialized executor; IntelliJ operations marshal to its EDT with write-intent read access and use native write commands. Revisions are monotonically increasing identities, not undo indices. Every edit batch addresses one revision with UTF-16 offsets; all ranges are validated before any mutation. One batch is one undo command. Closing the IDE adapter releases its listeners/editors, while the native Document remains IDE-owned. + +Save checks the expected revision and compares disk content with the captured baseline. Conflicts preserve both draft and external content. This is initial conflict detection, **not** atomic coordination with arbitrary external writers: another writer can race the check. IntelliJ autosave/external reload reconciliation and complete encoding/line-ending fidelity need the later document integration gate. Standalone currently reads/writes UTF-8 and retains history for the lifetime of its host. + +Build execution accepts an already resolved command/environment/working directory, drains both output streams and supports cancellation of the process and observed descendants, with forced termination after a bounded grace period. The lifecycle scope closes owned resources once in reverse order and continues cleanup after individual failures. Runtime lookup requires an explicit name/role mapping and has no fallback JVM. Navigation and diagnostics remain host callbacks. Build planning, immutable snapshots, worker authentication and owner-death IPC are not implemented by this process runner. + +## Validation and packaging + +Run the single command documented in [README](../README.md). Shared modules use Maven 3.9.11; IntelliJ packaging uses Gradle 9.1.0 and IntelliJ Platform Gradle Plugin 2.20.1 against local SDK build 263.7307.200. The plugin range is restricted to the 263 branch. This is build/test evidence for that exact SDK, not binary compatibility evidence for every future build in the range. + +Tests exercise atomic multi-range edits, stale and invalid requests, independent handles, standalone/native IDE undo and redo, disk conflicts, separate runtime roles, process output/cancellation and scope cleanup. The Java 21 target smoke checks linkage to the recorded library, contracts and preview descriptor; it does not initialize a Model or JavaFX rendering. Plugin ZIP creation and project configuration verification do not replace Plugin Verifier, manual IDE interaction, accessibility, or JavaFX exclusivity qualification. + +The next work package makes configuration/context resolution and compatibility executable. The first full two-host workflow remains work package 7. + +Validation on 2026-11-12: eight Maven tests and two native IntelliJ tests passed, Java 21 linkage passed, and the clean plugin build passed. A deliberately injected JavaFX dependency in contracts was rejected by Maven Enforcer and then removed. The plugin archive is checked for host JavaFX implementation JARs and exclusion of target artifacts. Gradle needs explicit macOS classifier dependencies because it does not select Maven's OpenJFX platform profiles. Other OS packaging is not qualified yet. + +The plugin configuration check warns against an `until-build` restriction; this foundation intentionally retains `263.*` rather than implying untested forward compatibility. No Plugin Verifier compatibility run is claimed. diff --git a/docs/repository-bootstrap-plan.md b/docs/repository-bootstrap-plan.md index 8e30122..4d2b7e5 100644 --- a/docs/repository-bootstrap-plan.md +++ b/docs/repository-bootstrap-plan.md @@ -1,6 +1,6 @@ # NeitzelFXDesigner repository bootstrap plan -Status: repository foundation implemented by the initial commit; work package 2 has a verified development artifact; work packages 3–7 remain planned. Consolidated 2026-11-12. Scope is the ordered implementation start for the [accepted initial contract](initial-project-contract.md); the initial commit creates the foundation only and does not publish a library release. +Status: repository foundation implemented by the initial commit; work package 2 has a verified development artifact; work package 3 implements the module/service foundation; work packages 4–7 remain planned. Consolidated 2026-11-12. Scope is the ordered implementation start for the [accepted initial contract](initial-project-contract.md); the initial commit creates the foundation only and does not publish a library release. ## Starting-point evidence before bootstrap @@ -16,7 +16,7 @@ Status: repository foundation implemented by the initial commit; work package 2 ## Ordered work packages -Work package 1 supplies this repository, documentation, pinned wrapper and `python3 scripts/check-bootstrap.py`. Its remote-main and workspace-registration checks are verified during publication. Work package 2 is now integrated with a verified local development artifact; the remaining implementation packages are not implemented. +Work package 1 supplies this repository, documentation, pinned wrapper and `python3 scripts/check-bootstrap.py`. Its remote-main and workspace-registration checks are verified during publication. Work package 2 is now integrated with a verified local development artifact; work package 3 is documented in [module and host boundaries](module-host-boundaries.md). ### 1. Bootstrap the independent repository and workspace registration @@ -48,6 +48,8 @@ Establish the first release containing the fix through actual artifact/source ev ### 3. Establish buildable module and host-service boundaries +Implemented: six shared modules, separate pinned IntelliJ adapter build, minimal host services and regression checks. See [scope and validation](module-host-boundaries.md). This does not complete the first end-to-end designer workflow. + Create the contract's shared artifacts and two host adapters. Define interfaces for document drafts/edit transactions/undo, build execution/output/cancellation, navigation, diagnostics, runtime resolution and lifecycle. Keep UI and IDE dependencies out of contracts/preview artifacts. Verify dependency rules with build checks. Build shared libraries/fixtures using the Maven structure; verify and pin the actual IntelliJ SDK packaging toolchain, using a separate adapter build if required. Provide one documented orchestration entry point. Retain independent standalone/plugin packaging gates. Select per-artifact Java bytecode baselines from real target requirements rather than propagating the host JDK into every artifact. diff --git a/pom.xml b/pom.xml index a819627..d684bfd 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,11 @@ neitzel-fx-designer 0.1.0-SNAPSHOT pom + + designer-contractsdesigner-preview-api + designer-coredesigner-kit-ui + designer-standalonedesigner-target-tools + Neitzel FX Designer Repository foundation for the shared Kit designer and its standalone and IntelliJ hosts. https://gitea.neitzel.de/konrad/NeitzelFXDesigner @@ -20,8 +25,17 @@ https://gitea.neitzel.de/konrad/NeitzelFXDesigner HEAD - + + org.junit.jupiterjunit-jupiter5.12.2test + + + org.apache.maven.pluginsmaven-compiler-plugin3.14.1${module.java.release} + org.apache.maven.pluginsmaven-surefire-plugin3.5.4false + org.apache.maven.pluginsmaven-resources-plugin3.3.1 + org.apache.maven.pluginsmaven-jar-plugin3.4.2 + org.apache.maven.pluginsmaven-install-plugin3.1.2 + org.apache.maven.plugins @@ -34,7 +48,7 @@ [3.9.11,3.9.12) - [21,) + [25,) diff --git a/scripts/check-bootstrap.py b/scripts/check-bootstrap.py index a4e84f9..7b9edfc 100644 --- a/scripts/check-bootstrap.py +++ b/scripts/check-bootstrap.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 -"""Validate the current POM-only foundation and portable local documentation.""" +"""Build shared modules and the pinned IDE adapter; verify target bytecode and docs.""" from pathlib import Path +import argparse +import zipfile import json import os import re @@ -8,8 +10,38 @@ import subprocess import sys ROOT = Path(__file__).resolve().parent.parent +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("--idea-home", default=os.environ.get("IDEA_HOME"), required=not os.environ.get("IDEA_HOME")) +parser.add_argument("--target-java-home", default=os.environ.get("TARGET_JAVA_HOME"), required=not os.environ.get("TARGET_JAVA_HOME")) +args = parser.parse_args() +idea = Path(args.idea_home).resolve() +info = json.loads((idea / "Resources/product-info.json").read_text()) +if info["buildNumber"] != "263.7307.200": + sys.exit("Expected IntelliJ SDK build 263.7307.200") +def run(command): + subprocess.run([str(x) for x in command], cwd=ROOT, check=True) wrapper = ROOT / ("mvnw.cmd" if os.name == "nt" else "mvnw") -subprocess.run([str(wrapper), "--batch-mode", "--no-transfer-progress", "clean", "verify"], cwd=ROOT, check=True) +run([sys.executable, ROOT / "scripts/check-neitzellib-artifact.py"]) +run([wrapper, "-B", "-ntp", "clean", "install"]) +run([wrapper, "-B", "-ntp", "-pl", "designer-target-tools", "org.apache.maven.plugins:maven-dependency-plugin:3.6.1:build-classpath", "-DincludeScope=runtime", "-Dmdep.outputFile=target/runtime-classpath.txt"]) +for module in ("contracts", "preview-api", "core", "kit-ui", "standalone", "target-tools"): + name = "designer-" + module + expected = 69 if module in ("kit-ui", "standalone") else 65 + with zipfile.ZipFile(ROOT / name / "target" / (name + "-0.1.0-SNAPSHOT.jar")) as jar: + classes = [n for n in jar.namelist() if n.endswith(".class")] + assert classes, name + assert all(int.from_bytes(jar.read(n)[6:8], "big") == expected for n in classes), name +classpath = str(ROOT / "designer-target-tools/target/classes") + os.pathsep + (ROOT / "designer-target-tools/target/runtime-classpath.txt").read_text().strip() +target_java = Path(args.target_java_home) / "bin/java" +version = subprocess.run([str(target_java), "-version"], capture_output=True, text=True, check=True) +if not re.search(r'version "21[.\"]', version.stderr): + sys.exit("Target linkage gate requires Java 21") +run([target_java, "-cp", classpath, "de.neitzel.fxdesigner.target.TargetToolInfo"]) +run([ROOT / "designer-intellij" / ("gradlew.bat" if os.name == "nt" else "gradlew"), "-p", "designer-intellij", "--no-daemon", "-PideaHome=" + str(idea), "clean", "test", "buildPlugin", "verifyPluginProjectConfiguration"]) +with zipfile.ZipFile(next((ROOT / "designer-intellij/build/distributions").glob("*.zip"))) as plugin: + names = plugin.namelist() + assert any("javafx-graphics-26-mac" in n for n in names), "Missing host JavaFX implementation" + assert not any("designer-target-tools" in n or "designer-preview-api" in n or "/fx-1.0" in n for n in names), "Target dependency in IDE host" for document in [ROOT / "README.md", ROOT / "AGENTS.md", ROOT / "THIRD-PARTY-NOTICES.md", *sorted((ROOT / "docs").rglob("*.md"))]: content = document.read_text(encoding="utf-8") for example in re.findall(r"```json\n(.*?)\n```", content, re.S): @@ -20,4 +52,4 @@ for document in [ROOT / "README.md", ROOT / "AGENTS.md", ROOT / "THIRD-PARTY-NOT local = document.parent / target.split("#", 1)[0] if not local.exists(): sys.exit(f"Broken local link in {document.relative_to(ROOT)}: {target}") -print("PASS Maven foundation, local document links and JSON examples (no application tests yet)") +print("PASS module builds, adapter tests, bytecode baselines, target linkage, plugin packaging and documentation")