Establish buildable modules and shared host-service boundaries

This commit is contained in:
2026-11-12 07:49:06 +01:00
parent 1326b18746
commit 306266ae40
41 changed files with 1980 additions and 23 deletions
+2
View File
@@ -4,3 +4,5 @@ mvnw text eol=lf
mvnw.cmd text eol=crlf mvnw.cmd text eol=crlf
*.md text eol=lf *.md text eol=lf
*.py text eol=lf *.py text eol=lf
gradlew text eol=lf
gradlew.bat text eol=crlf
+4 -1
View File
@@ -1,5 +1,6 @@
# Build output and local designer state # Build output and local designer state
**/target/ /target/
/*/target/
**/build/ **/build/
.gradle/ .gradle/
.neitzel-fx-designer/ .neitzel-fx-designer/
@@ -12,3 +13,5 @@
!.env.example !.env.example
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
.intellijPlatform/
+7 -13
View File
@@ -4,35 +4,29 @@ Shared Scene Builder Kit designer for NeitzelLib FX, with planned standalone and
## Current state ## 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. 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 ## 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 ```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: 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/`.
```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.
## Implementation references ## Implementation references
- [Initial project contract](docs/initial-project-contract.md): module responsibilities, configuration, snapshots, runtimes, preview sessions and communication. - [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. - [Decision provenance](docs/provenance.md): workspace planning and captured prototype evidence.
- [Third-party notices](THIRD-PARTY-NOTICES.md): Maven wrapper provenance. - [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`. 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.
+3 -1
View File
@@ -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). `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.
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>designer-contracts</artifactId>
<properties>
<module.java.release>21</module.java.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>module-dependency-boundary</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.intellij:*</exclude>
<exclude>com.jetbrains:*</exclude>
<exclude>com.jetbrains.intellij.platform:*</exclude>
<exclude>com.gluonhq.scenebuilder:*</exclude>
<exclude>org.openjfx:*</exclude>
<exclude>de.neitzel.lib:*</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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");
}
}
@@ -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<Replacement> 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();
}
}
@@ -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());
}
}
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>designer-core</artifactId>
<properties>
<module.java.release>21</module.java.release>
</properties>
<dependencies>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-contracts</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>module-dependency-boundary</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.intellij:*</exclude>
<exclude>com.jetbrains:*</exclude>
<exclude>com.jetbrains.intellij.platform:*</exclude>
<exclude>com.gluonhq.scenebuilder:*</exclude>
<exclude>org.openjfx:*</exclude>
<exclude>de.neitzel.lib:*</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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<Key, HostServices.RuntimeInstallation> installations;
public ConfiguredRuntimes(Map<Key, HostServices.RuntimeInstallation> installations) {
this.installations = Map.copyOf(installations);
}
public CompletionStage<HostServices.RuntimeInstallation> 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);
}
}
@@ -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<DocumentSnapshot> snapshot();
CompletionStage<DocumentSnapshot> apply(EditBatch edits);
CompletionStage<DocumentSnapshot> undo();
CompletionStage<DocumentSnapshot> redo();
CompletionStage<DocumentSnapshot> save(long expectedRevision);
/** Releases this handle; does not save, discard or close unrelated host documents. */
@Override
void close();
}
@@ -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<DocumentSession> 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<RuntimeInstallation> 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<Void> open(Location location);
}
public enum Severity {
INFO,
WARNING,
ERROR
}
public record Diagnostic(
String operation, Severity severity, String message, Optional<Location> 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<String> command, Map<String, String> 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<BuildResult> 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<BuildOutput> output) throws java.io.IOException;
}
}
@@ -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<HostServices.BuildOutput> 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<HostServices.BuildResult>();
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<HostServices.BuildResult> 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<HostServices.BuildOutput> 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. */
}
});
}
}
@@ -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<AutoCloseable> resources = new ArrayDeque<>();
private boolean closed;
public synchronized <T extends AutoCloseable> 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;
}
}
@@ -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<Integer>();
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<Integer>();
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<HostServices.BuildOutput>();
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");
}
}
}
+35
View File
@@ -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<JavaCompile>().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") }
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2g
org.gradle.configuration-cache=false
Binary file not shown.
@@ -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
Vendored Executable
+248
View File
@@ -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" "$@"
+93
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
pluginManagement { repositories { gradlePluginPortal(); mavenCentral() } }
rootProject.name = "designer-intellij"
@@ -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<Path, State> states = new HashMap<>();
private final Set<Handle> 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 <T> CompletionStage<T> edt(Callable<T> action) {
var future = new CompletableFuture<T>();
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<DocumentSession> 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 <T> CompletionStage<T> run(Callable<T> action) {
return edt(
() -> {
if (released) throw new IllegalStateException("Document handle closed");
return action.call();
});
}
public CompletionStage<DocumentSnapshot> snapshot() {
return run(() -> IntellijDocuments.this.snapshot(s));
}
public CompletionStage<DocumentSnapshot> 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<DocumentSnapshot> undo() {
return history(false);
}
public CompletionStage<DocumentSnapshot> redo() {
return history(true);
}
private CompletionStage<DocumentSnapshot> 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<DocumentSnapshot> 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;
});
}
}
@@ -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<Void>();
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();
}
}
@@ -0,0 +1,5 @@
<idea-plugin require-restart="true">
<id>de.neitzel.fxdesigner</id><name>Neitzel FX Designer</name><vendor>Neitzel</vendor>
<description>Module and host-service foundation. The FXML designer UI is not registered yet.</description>
<depends>com.intellij.modules.platform</depends>
</idea-plugin>
@@ -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);
}
}
}
+58
View File
@@ -0,0 +1,58 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>designer-kit-ui</artifactId>
<properties>
<module.java.release>25</module.java.release>
</properties>
<dependencies>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.gluonhq.scenebuilder</groupId>
<artifactId>kit</artifactId>
<version>26.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>module-dependency-boundary</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.intellij:*</exclude>
<exclude>com.jetbrains:*</exclude>
<exclude>com.jetbrains.intellij.platform:*</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>designer-preview-api</artifactId>
<properties>
<module.java.release>21</module.java.release>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>module-dependency-boundary</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.intellij:*</exclude>
<exclude>com.jetbrains:*</exclude>
<exclude>com.jetbrains.intellij.platform:*</exclude>
<exclude>com.gluonhq.scenebuilder:*</exclude>
<exclude>org.openjfx:*</exclude>
<exclude>de.neitzel.lib:*</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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);
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>designer-standalone</artifactId>
<properties>
<module.java.release>25</module.java.release>
</properties>
<dependencies>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-kit-ui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>module-dependency-boundary</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.intellij:*</exclude>
<exclude>com.jetbrains:*</exclude>
<exclude>com.jetbrains.intellij.platform:*</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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<Path, State> documents = new HashMap<>();
private static final class State {
Path file;
String text, saved;
long revision;
ArrayDeque<String> undo = new ArrayDeque<>(), redo = new ArrayDeque<>();
}
private <T> CompletionStage<T> submit(Callable<T> action) {
var future = new CompletableFuture<T>();
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<DocumentSession> 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 <T> CompletionStage<T> run(Callable<T> action) {
return submit(
() -> {
if (closed) throw new IllegalStateException("Document handle closed");
return action.call();
});
}
public CompletionStage<DocumentSnapshot> snapshot() {
return run(() -> FileDocuments.snapshot(s));
}
public CompletionStage<DocumentSnapshot> 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<DocumentSnapshot> undo() {
return history(s.undo, s.redo);
}
public CompletionStage<DocumentSnapshot> redo() {
return history(s.redo, s.undo);
}
private CompletionStage<DocumentSnapshot> history(
ArrayDeque<String> from, ArrayDeque<String> 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<DocumentSnapshot> 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();
}
}
@@ -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();
}
}
@@ -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());
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" ?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version>
</parent>
<artifactId>designer-target-tools</artifactId>
<properties>
<module.java.release>21</module.java.release>
</properties>
<dependencies>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-contracts</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>de.neitzel.fxdesigner</groupId>
<artifactId>designer-preview-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>de.neitzel.lib</groupId>
<artifactId>fx</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<executions>
<execution>
<id>module-dependency-boundary</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<searchTransitive>true</searchTransitive>
<excludes>
<exclude>com.intellij:*</exclude>
<exclude>com.jetbrains:*</exclude>
<exclude>com.jetbrains.intellij.platform:*</exclude>
<exclude>com.gluonhq.scenebuilder:*</exclude>
</excludes>
</bannedDependencies>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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());
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ Decision provenance: [accepted workspace decisions](provenance.md). Execution or
## Modules and dependency direction ## 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 | | Module/directory | Responsibility | Boundary |
| --- | --- | --- | | --- | --- | --- |
+50
View File
@@ -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.
+4 -2
View File
@@ -1,6 +1,6 @@
# NeitzelFXDesigner repository bootstrap plan # 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 ## Starting-point evidence before bootstrap
@@ -16,7 +16,7 @@ Status: repository foundation implemented by the initial commit; work package 2
## Ordered work packages ## 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 ### 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 ### 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. 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. 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.
+16 -2
View File
@@ -7,6 +7,11 @@
<artifactId>neitzel-fx-designer</artifactId> <artifactId>neitzel-fx-designer</artifactId>
<version>0.1.0-SNAPSHOT</version> <version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<modules>
<module>designer-contracts</module><module>designer-preview-api</module>
<module>designer-core</module><module>designer-kit-ui</module>
<module>designer-standalone</module><module>designer-target-tools</module>
</modules>
<name>Neitzel FX Designer</name> <name>Neitzel FX Designer</name>
<description>Repository foundation for the shared Kit designer and its standalone and IntelliJ hosts.</description> <description>Repository foundation for the shared Kit designer and its standalone and IntelliJ hosts.</description>
<url>https://gitea.neitzel.de/konrad/NeitzelFXDesigner</url> <url>https://gitea.neitzel.de/konrad/NeitzelFXDesigner</url>
@@ -20,8 +25,17 @@
<url>https://gitea.neitzel.de/konrad/NeitzelFXDesigner</url> <url>https://gitea.neitzel.de/konrad/NeitzelFXDesigner</url>
<tag>HEAD</tag> <tag>HEAD</tag>
</scm> </scm>
<!-- Module and bytecode baselines are established in bootstrap work package 3. --> <dependencyManagement><dependencies>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.12.2</version><scope>test</scope></dependency>
</dependencies></dependencyManagement>
<build> <build>
<pluginManagement><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version><configuration><release>${module.java.release}</release></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.4</version><configuration><testFailureIgnore>false</testFailureIgnore></configuration></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.3.1</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.4.2</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-install-plugin</artifactId><version>3.1.2</version></plugin>
</plugins></pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@@ -34,7 +48,7 @@
<configuration> <configuration>
<rules> <rules>
<requireMavenVersion><version>[3.9.11,3.9.12)</version></requireMavenVersion> <requireMavenVersion><version>[3.9.11,3.9.12)</version></requireMavenVersion>
<requireJavaVersion><version>[21,)</version></requireJavaVersion> <requireJavaVersion><version>[25,)</version></requireJavaVersion>
</rules> </rules>
</configuration> </configuration>
</execution> </execution>
+35 -3
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/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 from pathlib import Path
import argparse
import zipfile
import json import json
import os import os
import re import re
@@ -8,8 +10,38 @@ import subprocess
import sys import sys
ROOT = Path(__file__).resolve().parent.parent 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") 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"))]: 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") content = document.read_text(encoding="utf-8")
for example in re.findall(r"```json\n(.*?)\n```", content, re.S): 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] local = document.parent / target.split("#", 1)[0]
if not local.exists(): if not local.exists():
sys.exit(f"Broken local link in {document.relative_to(ROOT)}: {target}") 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")