Connect minimal hosts to snapshots, target workers and shared Kit editing
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.openapi.fileEditor.*;
|
||||
import com.intellij.openapi.project.*;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
|
||||
public final class DesignerEditorProvider implements FileEditorProvider, DumbAware {
|
||||
public boolean accept(Project project, VirtualFile file) {
|
||||
return !file.isDirectory() && "fxml".equalsIgnoreCase(file.getExtension());
|
||||
}
|
||||
|
||||
public FileEditor createEditor(Project project, VirtualFile file) {
|
||||
return new DesignerFileEditor(project, file);
|
||||
}
|
||||
|
||||
public String getEditorTypeId() {
|
||||
return "neitzel-fx-designer";
|
||||
}
|
||||
|
||||
public FileEditorPolicy getPolicy() {
|
||||
return FileEditorPolicy.PLACE_AFTER_DEFAULT_EDITOR;
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.fileEditor.*;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.UserDataHolderBase;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import de.neitzel.fxdesigner.kit.DesignerPane;
|
||||
import de.neitzel.fxdesigner.workflow.HostLaunch;
|
||||
import java.awt.BorderLayout;
|
||||
import java.beans.*;
|
||||
import java.nio.file.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import javax.swing.*;
|
||||
|
||||
/** IDE-owned FXML remains available in its normal text editor when designer setup fails. */
|
||||
public final class DesignerFileEditor extends UserDataHolderBase implements FileEditor {
|
||||
private final VirtualFile file;
|
||||
private final JPanel panel = new JPanel(new BorderLayout());
|
||||
private final JLabel status = new JLabel("Checking exclusive JavaFX setup…");
|
||||
private final PropertyChangeSupport changes = new PropertyChangeSupport(this);
|
||||
private volatile boolean disposed;
|
||||
private IntellijHost host;
|
||||
private DesignerPane pane;
|
||||
private JFXPanel bridge;
|
||||
private Timer gate;
|
||||
|
||||
DesignerFileEditor(Project project, VirtualFile file) {
|
||||
this.file = file;
|
||||
panel.add(status, BorderLayout.NORTH);
|
||||
ApplicationManager.getApplication()
|
||||
.executeOnPooledThread(
|
||||
() -> {
|
||||
try {
|
||||
ToolkitGate.check(project, file);
|
||||
Path settings =
|
||||
Path.of(
|
||||
System.getProperty(
|
||||
"neitzel.designer.launch",
|
||||
Path.of(
|
||||
project.getBasePath(),
|
||||
".neitzel-fx-designer",
|
||||
"host.properties")
|
||||
.toString()));
|
||||
var launch = HostLaunch.load(settings);
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (disposed) return;
|
||||
try {
|
||||
host =
|
||||
new IntellijHost(
|
||||
project,
|
||||
launch.runtimes(),
|
||||
d -> SwingUtilities.invokeLater(() -> status.setText(d.message())));
|
||||
var previous = Thread.currentThread().getContextClassLoader();
|
||||
try {
|
||||
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
|
||||
bridge = new JFXPanel();
|
||||
} finally {
|
||||
Thread.currentThread().setContextClassLoader(previous);
|
||||
}
|
||||
panel.add(bridge, BorderLayout.CENTER);
|
||||
panel.revalidate();
|
||||
Platform.setImplicitExit(false);
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
if (disposed) return;
|
||||
try {
|
||||
Thread.currentThread()
|
||||
.setContextClassLoader(getClass().getClassLoader());
|
||||
pane =
|
||||
new DesignerPane(
|
||||
host.services(), launch, Path.of(file.getPath()));
|
||||
bridge.setScene(new Scene(pane, 1200, 800));
|
||||
SwingUtilities.invokeLater(
|
||||
() ->
|
||||
status.setText(
|
||||
"Neitzel FX Designer · JavaFX 26 · native IDE"
|
||||
+ " documents"));
|
||||
} catch (Exception e) {
|
||||
failed(e);
|
||||
}
|
||||
});
|
||||
gate =
|
||||
new Timer(
|
||||
500,
|
||||
e -> {
|
||||
if (!ToolkitGate.allowed()) {
|
||||
status.setText(
|
||||
"JavaFX configuration changed; restart required");
|
||||
Platform.runLater(
|
||||
() -> {
|
||||
if (pane != null) {
|
||||
pane.close();
|
||||
pane.setDisable(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
gate.start();
|
||||
} catch (Exception e) {
|
||||
failed(e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
failed(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void failed(Throwable error) {
|
||||
SwingUtilities.invokeLater(
|
||||
() ->
|
||||
status.setText(
|
||||
"Designer unavailable: "
|
||||
+ error.getMessage()
|
||||
+ ". Source editing remains available."));
|
||||
}
|
||||
|
||||
public JComponent getComponent() {
|
||||
return panel;
|
||||
}
|
||||
|
||||
public JComponent getPreferredFocusedComponent() {
|
||||
return bridge == null ? panel : bridge;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "Neitzel Designer";
|
||||
}
|
||||
|
||||
public boolean isModified() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return !disposed && file.isValid();
|
||||
}
|
||||
|
||||
public VirtualFile getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public void setState(FileEditorState state) {}
|
||||
|
||||
public void addPropertyChangeListener(PropertyChangeListener listener) {
|
||||
changes.addPropertyChangeListener(listener);
|
||||
}
|
||||
|
||||
public void removePropertyChangeListener(PropertyChangeListener listener) {
|
||||
changes.removePropertyChangeListener(listener);
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
disposed = true;
|
||||
if (gate != null) gate.stop();
|
||||
if (pane != null)
|
||||
Platform.runLater(
|
||||
() ->
|
||||
pane.flush()
|
||||
.whenComplete(
|
||||
(v, error) -> {
|
||||
pane.close();
|
||||
bridge.setScene(null);
|
||||
if (host != null) host.close();
|
||||
}));
|
||||
else if (host != null) host.close();
|
||||
}
|
||||
}
|
||||
+43
-4
@@ -23,6 +23,14 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
* store.
|
||||
*/
|
||||
public final class IntellijDocuments implements HostServices.Documents, AutoCloseable {
|
||||
private final java.util.concurrent.CopyOnWriteArrayList<java.util.function.Consumer<URI>>
|
||||
changes = new java.util.concurrent.CopyOnWriteArrayList<>();
|
||||
|
||||
public AutoCloseable onChange(java.util.function.Consumer<URI> listener) {
|
||||
changes.add(listener);
|
||||
return () -> changes.remove(listener);
|
||||
}
|
||||
|
||||
private final Project project;
|
||||
private final com.intellij.openapi.Disposable lifetime;
|
||||
private final Map<Path, State> states = new HashMap<>();
|
||||
@@ -40,6 +48,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
||||
this.project = Objects.requireNonNull(project);
|
||||
lifetime = this::disposeResources;
|
||||
Disposer.register(project, lifetime);
|
||||
EditorFactory.getInstance()
|
||||
.getEventMulticaster()
|
||||
.addDocumentListener(
|
||||
new DocumentListener() {
|
||||
public void documentChanged(DocumentEvent event) {
|
||||
var file = FileDocumentManager.getInstance().getFile(event.getDocument());
|
||||
if (file != null && file.isInLocalFileSystem())
|
||||
changes.forEach(listener -> listener.accept(canonicalUri(file)));
|
||||
}
|
||||
},
|
||||
lifetime);
|
||||
}
|
||||
|
||||
private <T> CompletionStage<T> edt(Callable<T> action) {
|
||||
@@ -57,18 +76,19 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
||||
}
|
||||
});
|
||||
if (ApplicationManager.getApplication().isDispatchThread()) task.run();
|
||||
else ApplicationManager.getApplication().invokeLater(task, ModalityState.any());
|
||||
else ApplicationManager.getApplication().invokeLater(task, ModalityState.nonModal());
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletionStage<DocumentSession> open(URI uri) {
|
||||
return edt(
|
||||
() -> {
|
||||
Path path = Path.of(uri).toRealPath();
|
||||
Path supplied = Path.of(uri).toAbsolutePath().normalize();
|
||||
Path path = supplied.toRealPath();
|
||||
State state = states.get(path);
|
||||
if (state == null) {
|
||||
state = new State();
|
||||
state.file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(path);
|
||||
state.file = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(supplied);
|
||||
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");
|
||||
@@ -90,6 +110,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
||||
});
|
||||
}
|
||||
|
||||
public CompletionStage<List<DocumentSnapshot>> snapshots() {
|
||||
return edt(
|
||||
() -> {
|
||||
unsaved().toCompletableFuture().join();
|
||||
for (var file : FileEditorManager.getInstance(project).getOpenFiles())
|
||||
if (file.isInLocalFileSystem() && "fxml".equalsIgnoreCase(file.getExtension()))
|
||||
open(Path.of(file.getPath()).toUri()).toCompletableFuture().join().close();
|
||||
return states.values().stream().map(this::snapshot).toList();
|
||||
});
|
||||
}
|
||||
|
||||
public CompletionStage<List<DocumentSnapshot>> unsaved() {
|
||||
return edt(
|
||||
() -> {
|
||||
@@ -110,9 +141,17 @@ public final class IntellijDocuments implements HostServices.Documents, AutoClos
|
||||
});
|
||||
}
|
||||
|
||||
private static URI canonicalUri(VirtualFile file) {
|
||||
try {
|
||||
return Path.of(file.getPath()).toRealPath().toUri();
|
||||
} catch (java.io.IOException e) {
|
||||
return Path.of(file.getPath()).toAbsolutePath().normalize().toUri();
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentSnapshot snapshot(State s) {
|
||||
return new DocumentSnapshot(
|
||||
Path.of(s.file.getPath()).toUri(),
|
||||
canonicalUri(s.file),
|
||||
s.revision.get(),
|
||||
s.document.getText(),
|
||||
s.document.isWritable() && s.file.isWritable(),
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.openapi.application.*;
|
||||
import com.intellij.openapi.fileEditor.FileEditorProvider;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.registry.*;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import java.nio.file.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* Qualified exclusive JavaFX 26 setup. Never initializes Glass when bundled embedding is enabled.
|
||||
*/
|
||||
final class ToolkitGate {
|
||||
private static boolean initialized;
|
||||
private static volatile boolean revoked;
|
||||
|
||||
static synchronized void check(Project project, VirtualFile file) throws Exception {
|
||||
if (revoked) throw new IllegalStateException("JavaFX configuration changed; restart IntelliJ");
|
||||
if (!ApplicationInfo.getInstance().getBuild().asString().endsWith("263.7307.200"))
|
||||
throw new IllegalStateException("Designer requires the qualified IntelliJ 263.7307.200 SDK");
|
||||
if (Registry.is("embed.scene.builder"))
|
||||
throw new IllegalStateException(
|
||||
"Disable embed.scene.builder in IntelliJ Registry and restart. Keep JavaFX/FXML language"
|
||||
+ " support enabled.");
|
||||
var provider =
|
||||
FileEditorProvider.EP_FILE_EDITOR_PROVIDER.getExtensionList().stream()
|
||||
.filter(p -> p.getEditorTypeId().equals("JavaFX-Scene-Builder"))
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() -> new IllegalStateException("Bundled Scene Builder contract unavailable"));
|
||||
if (ReadAction.compute(() -> provider.accept(project, file)))
|
||||
throw new IllegalStateException("Bundled Scene Builder still accepts FXML; restart IntelliJ");
|
||||
if (initialized) return;
|
||||
Path log = Files.createTempFile("designer-native-", ".txt");
|
||||
try {
|
||||
var process =
|
||||
new ProcessBuilder(
|
||||
Path.of(System.getProperty("java.home"), "bin", "jcmd").toString(),
|
||||
Long.toString(ProcessHandle.current().pid()),
|
||||
"VM.dynlibs")
|
||||
.redirectErrorStream(true)
|
||||
.redirectOutput(log.toFile())
|
||||
.start();
|
||||
if (!process.waitFor(15, TimeUnit.SECONDS)) {
|
||||
process.destroyForcibly();
|
||||
throw new IllegalStateException("Cannot inspect loaded JavaFX libraries");
|
||||
}
|
||||
if (process.exitValue() != 0 || Files.readString(log).contains("libglass."))
|
||||
throw new IllegalStateException(
|
||||
"JavaFX already loaded or native inspection unavailable; restart with bundled Scene"
|
||||
+ " Builder disabled");
|
||||
} finally {
|
||||
Files.deleteIfExists(log);
|
||||
}
|
||||
Registry.get("embed.scene.builder")
|
||||
.addListener(
|
||||
new RegistryValueListener() {
|
||||
public void afterValueChanged(RegistryValue value) {
|
||||
revoked = true;
|
||||
}
|
||||
},
|
||||
ApplicationManager.getApplication());
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
static boolean allowed() {
|
||||
return !revoked && !Registry.is("embed.scene.builder");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
<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>
|
||||
<description>Shared Kit designer with isolated target previews and IDE-owned FXML documents.</description>
|
||||
<depends>com.intellij.modules.platform</depends>
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<fileEditorProvider implementation="de.neitzel.fxdesigner.intellij.DesignerEditorProvider"/>
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package de.neitzel.fxdesigner.intellij;
|
||||
|
||||
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess;
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import de.neitzel.fxdesigner.host.*;
|
||||
import de.neitzel.fxdesigner.workflow.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/** Same real build/socket/draft contract as standalone, using actual IDE Documents and commands. */
|
||||
public final class ConnectedWorkflowTest extends BasePlatformTestCase {
|
||||
public void testClasspathAndModulePath() throws Exception {
|
||||
String fixtures = System.getProperty("designer.workflow.fixtures");
|
||||
if (fixtures == null) return;
|
||||
Path root = Path.of(System.getProperty("designer.repository")),
|
||||
java = Path.of(System.getProperty("designer.target.java"));
|
||||
var runtimes =
|
||||
new ConfiguredRuntimes(
|
||||
Map.of(
|
||||
new ConfiguredRuntimes.Key("java21", HostServices.RuntimeRole.BUILD),
|
||||
new HostServices.RuntimeInstallation(java, 21),
|
||||
new ConfiguredRuntimes.Key("java21", HostServices.RuntimeRole.TARGET),
|
||||
new HostServices.RuntimeInstallation(java, 21)));
|
||||
VfsRootAccess.allowRootAccess(
|
||||
getTestRootDisposable(), Path.of(fixtures).toRealPath().toString(), root.toString());
|
||||
try (var host =
|
||||
new IntellijHost(getProject(), runtimes, d -> System.out.println(d.message()))) {
|
||||
var future =
|
||||
CompletableFuture.supplyAsync(
|
||||
() -> {
|
||||
try {
|
||||
int count = 0;
|
||||
for (String mode : List.of("classpath", "modulepath"))
|
||||
count +=
|
||||
new ConnectedWorkflowContract()
|
||||
.run(host.services(), root, Path.of(fixtures, mode));
|
||||
return count;
|
||||
} catch (Exception e) {
|
||||
throw new CompletionException(e);
|
||||
}
|
||||
});
|
||||
long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(5);
|
||||
while (!future.isDone() && System.nanoTime() < deadline) {
|
||||
UIUtil.dispatchAllInvocationEvents();
|
||||
Thread.sleep(10);
|
||||
}
|
||||
assertEquals(32, (int) future.get(1, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
-2
@@ -9,7 +9,8 @@ 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());
|
||||
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();
|
||||
@@ -48,7 +49,8 @@ public final class IntellijDocumentsTest extends BasePlatformTestCase {
|
||||
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());
|
||||
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 =
|
||||
@@ -69,4 +71,42 @@ public final class IntellijDocumentsTest extends BasePlatformTestCase {
|
||||
Files.deleteIfExists(file);
|
||||
}
|
||||
}
|
||||
|
||||
public void testKeepsNativeDocumentOpenedThroughAlias() throws Exception {
|
||||
Path directory = Files.createTempDirectory("designer-alias-");
|
||||
Path real = directory.resolve("real");
|
||||
Files.createDirectory(real);
|
||||
Path alias = directory.resolve("alias");
|
||||
Files.createSymbolicLink(alias, real);
|
||||
Path file = real.resolve("View.fxml");
|
||||
Files.writeString(file, "before");
|
||||
com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess.allowRootAccess(
|
||||
getTestRootDisposable(), directory.toRealPath().toString(), directory.toString());
|
||||
var virtual =
|
||||
com.intellij.openapi.vfs.LocalFileSystem.getInstance()
|
||||
.refreshAndFindFileByNioFile(alias.resolve("View.fxml"));
|
||||
var nativeDocument =
|
||||
com.intellij.openapi.fileEditor.FileDocumentManager.getInstance().getDocument(virtual);
|
||||
try (var docs = new IntellijDocuments(getProject());
|
||||
var handle = docs.open(alias.resolve("View.fxml").toUri()).toCompletableFuture().get()) {
|
||||
var before = handle.snapshot().toCompletableFuture().get();
|
||||
var after =
|
||||
handle
|
||||
.apply(
|
||||
new EditBatch(
|
||||
before.revision(),
|
||||
"Alias edit",
|
||||
List.of(new EditBatch.Replacement(0, before.text().length(), "after"))))
|
||||
.toCompletableFuture()
|
||||
.get();
|
||||
assertEquals("after", nativeDocument.getText());
|
||||
assertEquals(file.toRealPath().toUri(), after.document());
|
||||
handle.save(after.revision()).toCompletableFuture().get();
|
||||
} finally {
|
||||
Files.deleteIfExists(alias);
|
||||
Files.deleteIfExists(file);
|
||||
Files.deleteIfExists(real);
|
||||
Files.deleteIfExists(directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user