diff --git a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java index e46e528211..d68c10fa9c 100644 --- a/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java +++ b/flutter-idea/src/io/flutter/devtools/AbstractDevToolsViewFactory.java @@ -6,8 +6,11 @@ package io.flutter.devtools; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.openapi.wm.ex.ToolWindowManagerListener; +import com.intellij.util.messages.MessageBusConnection; import io.flutter.FlutterUtils; import io.flutter.actions.RefreshToolWindowAction; import io.flutter.run.daemon.DevToolsInstance; @@ -42,7 +45,10 @@ public abstract DevToolsUrl getDevToolsUrl(@NotNull Project project, @NotNull FlutterSdkVersion flutterSdkVersion, @NotNull DevToolsInstance instance); - protected void doAfterBrowserOpened(@NotNull Project project, @NotNull EmbeddedBrowser browser) {} + protected void doAfterBrowserOpened(@NotNull Project project, @NotNull EmbeddedBrowser browser) { + } + + private boolean devToolsLoadedInBrowser = false; @Override public Object isApplicableAsync(@NotNull Project project, @NotNull Continuation $completion) { @@ -98,19 +104,36 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo } // Final case: + loadDevToolsInEmbeddedBrowser(project, toolWindow, flutterSdkVersion); + + // Finally, listen for the panel to be reopened and potentially reload DevTools. + maybeReloadDevToolsWhenVisible(project, toolWindow, flutterSdkVersion); + } + + private void loadDevToolsInEmbeddedBrowser(@NotNull Project project, + @NotNull ToolWindow toolWindow, + @NotNull FlutterSdkVersion flutterSdkVersion) { + viewUtils.presentLabel(toolWindow, "Loading " + getToolWindowTitle() + "..."); + AsyncUtils.whenCompleteUiThread( DevToolsService.getInstance(project).getDevToolsInstance(), (instance, error) -> { // Skip displaying if the project has been closed. if (!project.isOpen()) { + viewUtils.presentLabel(toolWindow, "Project is closed."); return; } + // Show a message if DevTools started with an error. + final String restartDevToolsMessage = "Try switching to another Flutter panel and back again to restart the server."; if (error != null) { + viewUtils.presentLabels(toolWindow, List.of("Flutter DevTools start-up failed.", restartDevToolsMessage)); return; } + // Show a message if there is no DevTools yet. if (instance == null) { + viewUtils.presentLabels(toolWindow, List.of("Flutter DevTools does not exist.", restartDevToolsMessage)); return; } @@ -122,14 +145,30 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo .ifPresent(embeddedBrowser -> { embeddedBrowser.openPanel(toolWindow, getToolWindowTitle(), devToolsUrl, System.out::println); + devToolsLoadedInBrowser = true; doAfterBrowserOpened(project, embeddedBrowser); + // The "refresh" action refreshes the embedded browser, not the panel. + // Therefore, we only show it once we have an embedded browser. + toolWindow.setTitleActions(List.of(new RefreshToolWindowAction(getToolWindowId()))); }); }); } ); + } - // TODO(helin24): It may be better to add this to the gear actions or to attach as a mouse event on individual tabs within a tool - // window, but I wasn't able to get either working immediately. - toolWindow.setTitleActions(List.of(new RefreshToolWindowAction(getToolWindowId()))); + private void maybeReloadDevToolsWhenVisible(@NotNull Project project, + @NotNull ToolWindow toolWindow, @NotNull FlutterSdkVersion flutterSdkVersion) { + MessageBusConnection connection = project.getMessageBus().connect(); + connection.subscribe(ToolWindowManagerListener.TOPIC, new ToolWindowManagerListener() { + @Override + public void toolWindowShown(@NotNull ToolWindow activatedToolWindow) { + if (activatedToolWindow.getId().equals(getToolWindowId())) { + if (!devToolsLoadedInBrowser) { + loadDevToolsInEmbeddedBrowser(project, toolWindow, flutterSdkVersion); + } + } + } + }); + Disposer.register(toolWindow.getDisposable(), connection); } -} \ No newline at end of file +} diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java new file mode 100644 index 0000000000..fc1f723097 --- /dev/null +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsServerTask.java @@ -0,0 +1,390 @@ +/* + * Copyright 2025 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +package io.flutter.run.daemon; + +import com.google.common.collect.ImmutableList; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.intellij.execution.ExecutionException; +import com.intellij.execution.configurations.GeneralCommandLine; +import com.intellij.execution.process.ProcessAdapter; +import com.intellij.execution.process.ProcessEvent; +import com.intellij.execution.process.ProcessHandler; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.project.ProjectManagerListener; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Version; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.registry.Registry; +import com.jetbrains.lang.dart.ide.devtools.DartDevToolsService; +import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; +import com.jetbrains.lang.dart.sdk.DartSdk; +import io.flutter.FlutterMessages; +import io.flutter.FlutterUtils; +import io.flutter.bazel.Workspace; +import io.flutter.bazel.WorkspaceCache; +import io.flutter.dart.DtdUtils; +import io.flutter.sdk.FlutterSdk; +import io.flutter.sdk.FlutterSdkUtil; +import io.flutter.utils.JsonUtils; +import io.flutter.utils.MostlySilentColoredProcessHandler; +import io.flutter.utils.OpenApiUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +class DevToolsServerTask extends Task.Backgroundable { + private @NotNull static final Logger LOG = Logger.getInstance(DevToolsServerTask.class); + public @NotNull static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; + public @NotNull static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; + private @NotNull final Project project; + private @NotNull final AtomicReference> devToolsFutureRef; + + private @Nullable DaemonApi daemonApi; + private @Nullable ProcessHandler process; + + public DevToolsServerTask(@NotNull Project project, + @NotNull String title, + @NotNull AtomicReference> devToolsFutureRef) { + super(project, title, true); + this.project = project; + this.devToolsFutureRef = devToolsFutureRef; + } + + @Override + public void run(@NotNull ProgressIndicator progressIndicator) { + try { + progressIndicator.setFraction(30); + progressIndicator.setText2("Init"); + + // If DevTools is not supported, start the daemon instead. + final boolean dartDevToolsSupported = dartSdkSupportsDartDevTools(); + if (!dartDevToolsSupported) { + LOG.info("Starting the DevTools daemon."); + progressIndicator.setFraction(60); + progressIndicator.setText2("Daemon set-up"); + setUpWithDaemon(); + return; + } + + // If we are in a Bazel workspace, start the server. + // Note: This is only for internal usages. + final WorkspaceCache workspaceCache = WorkspaceCache.getInstance(project); + if (workspaceCache.isBazel()) { + progressIndicator.setFraction(60); + progressIndicator.setText2("Starting server"); + setUpWithDart(createCommand(workspaceCache.get().getRoot().getPath(), workspaceCache.get().getDevToolsScript(), + ImmutableList.of("--machine"))); + return; + } + + // This is only for development to check integration with a locally run DevTools server. + // To enable, follow the instructions in: + // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools + final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); + if (!localDevToolsDir.isEmpty()) { + LOG.info("Starting local DevTools server at: " + localDevToolsDir); + progressIndicator.setFraction(60); + progressIndicator.setText2("Starting local server"); + setUpLocalServer(localDevToolsDir); + } + + // Wait for the Dart Plugin to start the DevTools server. + final CompletableFuture devToolsFuture = checkForDartPluginInitiatedDevToolsWithRetries(progressIndicator); + devToolsFuture.whenComplete((devTools, error) -> { + if (error != null) { + cancelWithError(new Exception(error)); + } + else { + devToolsFutureRef.get().complete(devTools); + } + }); + } + catch (java.util.concurrent.ExecutionException | InterruptedException e) { + cancelWithError(e); + } + } + + private Boolean dartSdkSupportsDartDevTools() { + final DartSdk dartSdk = DartSdk.getDartSdk(project); + if (dartSdk != null) { + final Version version = Version.parseVersion(dartSdk.getVersion()); + assert version != null; + return version.compareTo(2, 15, 0) >= 0; + } + return false; + } + + @Override + public void onThrowable(@NotNull Throwable error) { + cancelWithError(new Exception(error)); + } + + private void setUpLocalServer(@NotNull String localDevToolsDir) throws java.util.concurrent.ExecutionException, InterruptedException { + final DtdUtils dtdUtils = new DtdUtils(); + final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); + final String dtdUri = dtdService.getUri(); + + final List args = new ArrayList<>(); + args.add("serve"); + args.add("--machine"); + args.add("--dtd-uri=" + dtdUri); + final String localDevToolsArgs = Registry.stringValue(LOCAL_DEVTOOLS_ARGS); + if (!localDevToolsArgs.isEmpty()) { + args.addAll(Arrays.stream(localDevToolsArgs.split(" ")).toList()); + } + + setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); + } + + private void setUpInDevMode(@NotNull GeneralCommandLine command) { + try { + this.process = new MostlySilentColoredProcessHandler(command); + this.process.addProcessListener(new ProcessAdapter() { + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + final String text = event.getText().trim(); + + // Keep this printout so developers can see DevTools startup output in idea.log. + System.out.println("DevTools startup: " + text); + tryParseStartupText(text); + } + }); + process.startNotify(); + } + catch (ExecutionException e) { + cancelWithError(e); + } + } + + private void setUpWithDart(GeneralCommandLine command) { + try { + this.process = new MostlySilentColoredProcessHandler(command); + this.process.addProcessListener(new ProcessAdapter() { + @Override + public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { + tryParseStartupText(event.getText().trim()); + } + }); + process.startNotify(); + + ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { + @Override + public void projectClosing(@NotNull Project project) { + devToolsFutureRef.set(null); + process.destroyProcess(); + } + }); + } + catch (ExecutionException e) { + cancelWithError(e); + } + } + + private void setUpWithDaemon() { + try { + final GeneralCommandLine command = chooseCommand(project); + if (command == null) { + cancelWithError("Unable to find daemon command for project"); + return; + } + this.process = new MostlySilentColoredProcessHandler(command); + daemonApi = new DaemonApi(process); + daemonApi.listen(process, new DevToolsService.DevToolsServiceListener()); + daemonApi.devToolsServe().thenAccept((DaemonApi.DevToolsAddress address) -> { + if (!project.isOpen()) { + // We should skip starting DevTools (and doing any UI work) if the project has been closed. + return; + } + if (address == null) { + cancelWithError("DevTools address was null"); + } + else { + devToolsFutureRef.get().complete(new DevToolsInstance(address.host, address.port)); + } + }); + } + catch (ExecutionException e) { + cancelWithError(e); + } + + ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { + @Override + public void projectClosing(@NotNull Project project) { + devToolsFutureRef.set(null); + + try { + daemonApi.daemonShutdown().get(5, TimeUnit.SECONDS); + } + catch (InterruptedException | java.util.concurrent.ExecutionException | TimeoutException e) { + LOG.error("DevTools daemon did not shut down normally: " + e); + if (!process.isProcessTerminated()) { + process.destroyProcess(); + } + } + } + }); + } + + private CompletableFuture checkForDartPluginInitiatedDevToolsWithRetries(@NotNull ProgressIndicator progressIndicator) + throws InterruptedException { + progressIndicator.setText2("Waiting for server with DTD"); + final CompletableFuture devToolsFuture = new CompletableFuture<>(); + + final long msBetweenRetries = 1500; + int retries = 0; + while (retries < 10) { + retries++; + final double currentProgress = progressIndicator.getFraction(); + progressIndicator.setFraction(Math.min(currentProgress + 10, 95)); + + final @Nullable DevToolsInstance devTools = createDevToolsInstanceFromDartPluginUri(); + if (devTools != null) { + LOG.debug("Dart plugin DevTools ready after " + retries + " tries."); + devToolsFuture.complete(devTools); + return devToolsFuture; + } + else { + LOG.debug("Dart plugin DevTools not ready after " + retries + " tries."); + Thread.sleep(msBetweenRetries); + } + } + + devToolsFuture.completeExceptionally(new Exception("Timed out waiting for Dart plugin to start DevTools.")); + return devToolsFuture; + } + + private @Nullable DevToolsInstance createDevToolsInstanceFromDartPluginUri() { + final String dartPluginUri = DartDevToolsService.getInstance(project).getDevToolsHostAndPort(); + if (dartPluginUri == null) { + return null; + } + + String[] parts = dartPluginUri.split(":"); + String host = parts[0]; + Integer port = Integer.parseInt(parts[1]); + if (host == null || port == null) { + return null; + } + + return new DevToolsInstance(host, port); + } + + private void tryParseStartupText(@NotNull String text) { + if (text.startsWith("{") && text.endsWith("}")) { + try { + final JsonElement element = JsonUtils.parseString(text); + + final JsonObject obj = element.getAsJsonObject(); + + if (Objects.equals(JsonUtils.getStringMember(obj, "event"), "server.started")) { + final JsonObject params = obj.getAsJsonObject("params"); + final String host = JsonUtils.getStringMember(params, "host"); + final int port = JsonUtils.getIntMember(params, "port"); + + if (port != -1) { + devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); + } + else { + cancelWithError("DevTools port was invalid"); + } + } + } + catch (JsonSyntaxException e) { + cancelWithError(e); + } + } + } + + private static GeneralCommandLine chooseCommand(@NotNull final Project project) { + // Use daemon script if this is a bazel project. + final Workspace workspace = WorkspaceCache.getInstance(project).get(); + if (workspace != null) { + final String script = workspace.getDaemonScript(); + if (script != null) { + return createCommand(workspace.getRoot().getPath(), script, ImmutableList.of()); + } + } + + // Otherwise, use the Flutter SDK. + final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); + if (sdk == null) { + return null; + } + + try { + final String path = FlutterSdkUtil.pathToFlutterTool(sdk.getHomePath()); + return createCommand(sdk.getHomePath(), path, ImmutableList.of("daemon")); + } + catch (ExecutionException e) { + FlutterUtils.warn(LOG, "Unable to calculate command to start Flutter daemon", e); + return null; + } + } + + private static @NotNull GeneralCommandLine createCommand(String workDir, String command, List arguments) { + final GeneralCommandLine result = new GeneralCommandLine().withWorkDirectory(workDir); + result.setCharset(StandardCharsets.UTF_8); + result.setExePath(FileUtil.toSystemDependentName(command)); + result.withEnvironment(FlutterSdkUtil.FLUTTER_HOST_ENV, (new FlutterSdkUtil()).getFlutterHostEnvValue()); + + for (String argument : arguments) { + result.addParameter(argument); + } + + return result; + } + + private void cancelWithError(String message) { + cancelWithError(new Exception(message)); + } + + private void cancelWithError(Exception exception) { + final String errorTitle = "DevTools server start-up failure."; + FlutterUtils.warn(LOG, errorTitle, exception); + devToolsFutureRef.get().completeExceptionally(new Exception(errorTitle)); + showErrorNotification(errorTitle, exception.getMessage()); + throw new ProcessCanceledException(exception); + } + + private void showErrorNotification(String errorTitle, String errorDetails) { + OpenApiUtils.safeInvokeLater(() -> { + final Notification notification = new Notification(FlutterMessages.FLUTTER_NOTIFICATION_GROUP_ID, + "DevTools", + errorTitle + " " + errorDetails, + NotificationType.WARNING); + + notification.addAction(new AnAction("Dismiss") { + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + notification.expire(); + } + }); + Notifications.Bus.notify(notification, project); + }); + } +} diff --git a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java index 4a27890a8a..f22dc87c84 100644 --- a/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java +++ b/flutter-idea/src/io/flutter/run/daemon/DevToolsService.java @@ -5,63 +5,33 @@ */ package io.flutter.run.daemon; -import com.google.common.collect.ImmutableList; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import com.intellij.execution.ExecutionException; -import com.intellij.execution.configurations.GeneralCommandLine; -import com.intellij.execution.process.ProcessAdapter; -import com.intellij.execution.process.ProcessEvent; -import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.process.ProcessOutput; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator; import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.project.ProjectManagerListener; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.Version; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.util.registry.Registry; -import com.jetbrains.lang.dart.ide.devtools.DartDevToolsService; -import com.jetbrains.lang.dart.ide.toolingDaemon.DartToolingDaemonService; -import com.jetbrains.lang.dart.sdk.DartSdk; -import io.flutter.FlutterUtils; -import io.flutter.bazel.Workspace; -import io.flutter.bazel.WorkspaceCache; import io.flutter.console.FlutterConsoles; -import io.flutter.dart.DtdUtils; import io.flutter.sdk.FlutterCommand; import io.flutter.sdk.FlutterSdk; -import io.flutter.sdk.FlutterSdkUtil; -import io.flutter.utils.JsonUtils; -import io.flutter.utils.MostlySilentColoredProcessHandler; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; public class DevToolsService { private static final Logger LOG = Logger.getInstance(DevToolsService.class); - public static final String LOCAL_DEVTOOLS_DIR = "flutter.local.devtools.dir"; - public static final String LOCAL_DEVTOOLS_ARGS = "flutter.local.devtools.args"; - - private static class DevToolsServiceListener implements DaemonEvent.Listener { + protected static class DevToolsServiceListener implements DaemonEvent.Listener { } @NotNull private final Project project; - private DaemonApi daemonApi; - private ProcessHandler process; - private AtomicReference> devToolsFutureRef = new AtomicReference<>(null); + + @Nullable private DevToolsServerTask devToolsServerTask; + + @Nullable private BackgroundableProcessIndicator devToolsServerProgressIndicator; + + @NotNull private AtomicReference> devToolsFutureRef = new AtomicReference<>(null); @NotNull public static DevToolsService getInstance(@NotNull final Project project) { @@ -105,203 +75,54 @@ public CompletableFuture getDevToolsInstanceWithForcedRestart( if (futureInstance == null) { devToolsFutureRef.set(new CompletableFuture<>()); - startServer(); + startServer(true); } else if (!futureInstance.isDone()) { futureInstance.cancel(true); devToolsFutureRef.set(new CompletableFuture<>()); - startServer(); + startServer(true); } return devToolsFutureRef.get(); } private void startServer() { - ApplicationManager.getApplication().executeOnPooledThread(() -> { - final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); + startServer(false); + } - boolean dartDevToolsSupported = false; - final DartSdk dartSdk = DartSdk.getDartSdk(project); - if (dartSdk != null) { - final Version version = Version.parseVersion(dartSdk.getVersion()); - assert version != null; - dartDevToolsSupported = version.compareTo(2, 15, 0) >= 0; + private void startServer(boolean forceRestart) { + if (forceRestart) { + // If this is a force-restart request and the previous DevTools server is still running, cancel it before starting another. + if (devToolsServerProgressIndicator != null && devToolsServerProgressIndicator.isRunning()) { + devToolsServerProgressIndicator.cancel(); } - - if (dartDevToolsSupported) { - // This condition means we can use `dart devtools` to start. - final WorkspaceCache workspaceCache = WorkspaceCache.getInstance(project); - if (workspaceCache.isBazel()) { - // This is only for internal usages. - setUpWithDart(createCommand(workspaceCache.get().getRoot().getPath(), workspaceCache.get().getDevToolsScript(), - ImmutableList.of("--machine"))); - } - else { - final String localDevToolsDir = Registry.stringValue(LOCAL_DEVTOOLS_DIR); - if (!localDevToolsDir.isEmpty()) { - // This is only for development to check integration with a locally run DevTools server. - // To enable, follow the instructions in: - // https://github.com/flutter/flutter-intellij/blob/master/CONTRIBUTING.md#developing-with-local-devtools - final DtdUtils dtdUtils = new DtdUtils(); - try { - final DartToolingDaemonService dtdService = dtdUtils.readyDtdService(project).get(); - final String dtdUri = dtdService.getUri(); - - final List args = new ArrayList<>(); - args.add("serve"); - args.add("--machine"); - args.add("--dtd-uri=" + dtdUri); - final String localDevToolsArgs = Registry.stringValue(LOCAL_DEVTOOLS_ARGS); - if (!localDevToolsArgs.isEmpty()) { - args.addAll(Arrays.stream(localDevToolsArgs.split(" ")).toList()); - } - - setUpInDevMode(createCommand(localDevToolsDir, "dt", args)); - } - catch (InterruptedException | java.util.concurrent.ExecutionException e) { - throw new RuntimeException(e); - } - return; - } - - // The Dart plugin should start DevTools with DTD, so try to use this instance of DevTools before trying to start another. - final String dartPluginUri = DartDevToolsService.getInstance(project).getDevToolsHostAndPort(); - if (dartPluginUri != null) { - String[] parts = dartPluginUri.split(":"); - String host = parts[0]; - Integer port = Integer.parseInt(parts[1]); - if (host != null && port != null) { - devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); - return; - } - } - - setUpWithDart(createCommand(DartSdk.getDartSdk(project).getHomePath(), - DartSdk.getDartSdk(project).getHomePath() + File.separatorChar + "bin" + File.separatorChar + "dart", - ImmutableList.of("devtools", "--machine"))); + } else { + // If this is not a force-restart request, do not start a new DevTools server if one is already running, or if we have a + // DevTools instance. + if (devToolsServerProgressIndicator != null) { + if (devToolsServerProgressIndicator.isRunning() || devToolsInstanceExists()) { + return; + } else { + devToolsServerProgressIndicator.cancel(); } } - else { - setUpWithDaemon(); - } - }); - } - - private void setUpInDevMode(GeneralCommandLine command) { - try { - this.process = new MostlySilentColoredProcessHandler(command); - this.process.addProcessListener(new ProcessAdapter() { - @Override - public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { - final String text = event.getText().trim(); - - // Keep this printout so developers can see DevTools startup output in idea.log. - System.out.println("DevTools startup: " + text); - tryParseStartupText(text); - } - }); - process.startNotify(); - } - catch (ExecutionException e) { - logExceptionAndComplete(e); } - } - - private void setUpWithDart(GeneralCommandLine command) { - try { - this.process = new MostlySilentColoredProcessHandler(command); - this.process.addProcessListener(new ProcessAdapter() { - @Override - public void onTextAvailable(@NotNull ProcessEvent event, @NotNull Key outputType) { - tryParseStartupText(event.getText().trim()); - } - }); - process.startNotify(); - - ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { - @Override - public void projectClosing(@NotNull Project project) { - devToolsFutureRef.set(null); - process.destroyProcess(); - } - }); - } - catch (ExecutionException e) { - logExceptionAndComplete(e); - } - } - - private void tryParseStartupText(@NotNull String text) { - if (text.startsWith("{") && text.endsWith("}")) { - try { - final JsonElement element = JsonUtils.parseString(text); - - final JsonObject obj = element.getAsJsonObject(); - - if (Objects.equals(JsonUtils.getStringMember(obj, "event"), "server.started")) { - final JsonObject params = obj.getAsJsonObject("params"); - final String host = JsonUtils.getStringMember(params, "host"); - final int port = JsonUtils.getIntMember(params, "port"); - if (port != -1) { - devToolsFutureRef.get().complete(new DevToolsInstance(host, port)); - } - else { - logExceptionAndComplete("DevTools port was invalid"); - } - } - } - catch (JsonSyntaxException e) { - logExceptionAndComplete(e); - } - } + // Start the DevTools server. + devToolsServerTask = new DevToolsServerTask(project, "Starting DevTools", devToolsFutureRef); + devToolsServerProgressIndicator = new BackgroundableProcessIndicator(project, devToolsServerTask); + ProgressManager.getInstance() + .runProcessWithProgressAsynchronously( + devToolsServerTask, devToolsServerProgressIndicator); } - private void setUpWithDaemon() { - try { - final GeneralCommandLine command = chooseCommand(project); - if (command == null) { - logExceptionAndComplete("Unable to find daemon command for project"); - return; - } - this.process = new MostlySilentColoredProcessHandler(command); - daemonApi = new DaemonApi(process); - daemonApi.listen(process, new DevToolsServiceListener()); - daemonApi.devToolsServe().thenAccept((DaemonApi.DevToolsAddress address) -> { - if (!project.isOpen()) { - // We should skip starting DevTools (and doing any UI work) if the project has been closed. - return; - } - if (address == null) { - logExceptionAndComplete("DevTools address was null"); - } - else { - devToolsFutureRef.get().complete(new DevToolsInstance(address.host, address.port)); - } - }); - } - catch (ExecutionException e) { - logExceptionAndComplete(e); + private boolean devToolsInstanceExists() { + if (devToolsFutureRef != null) { + final CompletableFuture devToolsFuture = devToolsFutureRef.get(); + return devToolsFuture != null && devToolsFuture.isDone() && !devToolsFuture.isCompletedExceptionally(); } - - ProjectManager.getInstance().addProjectManagerListener(project, new ProjectManagerListener() { - @Override - public void projectClosing(@NotNull Project project) { - devToolsFutureRef.set(null); - - try { - daemonApi.daemonShutdown().get(5, TimeUnit.SECONDS); - } - catch (InterruptedException | java.util.concurrent.ExecutionException | TimeoutException e) { - LOG.error("DevTools daemon did not shut down normally: " + e); - if (!process.isProcessTerminated()) { - process.destroyProcess(); - } - } - } - }); + return false; } - private CompletableFuture pubActivateDevTools(FlutterSdk sdk) { final FlutterCommand command = sdk.flutterPub(null, "global", "activate", "devtools"); @@ -338,44 +159,6 @@ private void logExceptionAndComplete(Exception exception) { future.completeExceptionally(exception); } } - - private static GeneralCommandLine chooseCommand(@NotNull final Project project) { - // Use daemon script if this is a bazel project. - final Workspace workspace = WorkspaceCache.getInstance(project).get(); - if (workspace != null) { - final String script = workspace.getDaemonScript(); - if (script != null) { - return createCommand(workspace.getRoot().getPath(), script, ImmutableList.of()); - } - } - - // Otherwise, use the Flutter SDK. - final FlutterSdk sdk = FlutterSdk.getFlutterSdk(project); - if (sdk == null) { - return null; - } - - try { - final String path = FlutterSdkUtil.pathToFlutterTool(sdk.getHomePath()); - return createCommand(sdk.getHomePath(), path, ImmutableList.of("daemon")); - } - catch (ExecutionException e) { - FlutterUtils.warn(LOG, "Unable to calculate command to start Flutter daemon", e); - return null; - } - } - - private static GeneralCommandLine createCommand(String workDir, String command, List arguments) { - final GeneralCommandLine result = new GeneralCommandLine().withWorkDirectory(workDir); - result.setCharset(StandardCharsets.UTF_8); - result.setExePath(FileUtil.toSystemDependentName(command)); - result.withEnvironment(FlutterSdkUtil.FLUTTER_HOST_ENV, (new FlutterSdkUtil()).getFlutterHostEnvValue()); - - for (String argument : arguments) { - result.addParameter(argument); - } - - return result; - } } + diff --git a/flutter-idea/src/io/flutter/view/ViewUtils.java b/flutter-idea/src/io/flutter/view/ViewUtils.java index 0b1541a866..b5bd434bcc 100644 --- a/flutter-idea/src/io/flutter/view/ViewUtils.java +++ b/flutter-idea/src/io/flutter/view/ViewUtils.java @@ -23,7 +23,7 @@ public class ViewUtils { public void presentLabel(ToolWindow toolWindow, String text) { - final JBLabel label = new JBLabel(text, SwingConstants.CENTER); + final JBLabel label = new JBLabel(wrapWithHtml(text), SwingConstants.CENTER); label.setForeground(UIUtil.getLabelDisabledForeground()); replacePanelLabel(toolWindow, label); } @@ -39,7 +39,7 @@ public void presentLabels(@NotNull ToolWindow toolWindow, @NotNull List labelsPanel.setBorder(JBUI.Borders.empty()); // Use padding on individual labels if needed for (String text : labels) { - final JBLabel label = new JBLabel(text, SwingConstants.CENTER); + final JBLabel label = new JBLabel(wrapWithHtml(text), SwingConstants.CENTER); label.setForeground(UIUtil.getLabelDisabledForeground()); // Add padding to each label for spacing label.setBorder(JBUI.Borders.empty(2, 0)); @@ -59,13 +59,13 @@ public void presentClickableLabel(ToolWindow toolWindow, List labels for (LabelInput input : labels) { if (input.listener == null) { - final JLabel descriptionLabel = new JLabel("" + input.text + ""); + final JLabel descriptionLabel = new JLabel(wrapWithHtml(input.text)); descriptionLabel.setBorder(JBUI.Borders.empty(5)); descriptionLabel.setHorizontalAlignment(SwingConstants.CENTER); panel.add(descriptionLabel, BorderLayout.NORTH); } else { - final LinkLabel linkLabel = new LinkLabel<>("" + input.text + "", null); + final LinkLabel linkLabel = new LinkLabel<>(wrapWithHtml(input.text), null); linkLabel.setBorder(JBUI.Borders.empty(5)); linkLabel.setListener(input.listener, null); linkLabel.setHorizontalAlignment(SwingConstants.CENTER); @@ -92,4 +92,9 @@ public void replacePanelLabel(ToolWindow toolWindow, JComponent label) { contentManager.addContent(content); }); } + + private String wrapWithHtml(String text) { + // Wrapping with HTML tags ensures the text wraps and is not cut off. + return "" + text + ""; + } }