flutter / devtools

Performance tools for Flutter
https://flutter.dev/docs/development/tools/devtools/
BSD 3-Clause "New" or "Revised" License
1.59k stars 327 forks source link

Respond to `ThemeChanged` event from IDEs #8336

Closed helin24 closed 1 month ago

helin24 commented 1 month ago

Addresses https://github.com/flutter/flutter-intellij/issues/7626

Sep-26-2024 11-50-20

DanTup commented 1 month ago

I haven't figured it out yet, but when I use this version of DevTools, the sidebar no longer loads in VS Code (it shows a spinner indefinitely - though there are no errors in the console). If I revert to the last commit this was based on, everything works fine.

I'll keep debugging, but if anyone has any ideas what could impact this (nothing in the code looks related to me!), please let me know!

DanTup commented 1 month ago

I see the issue... we're creating a second DtdEditorClient on the same DTD connection. DtdEditorClient expects to have a one-to-one relationship with a DTD connection, because it calls things like streamListen('Service') and if there is another user of the connection, it may have already subscribed to that.

To fix this specific issue, we could ensure the theme manager uses the same DtdEditorClient that the sidebar is using - however I think there may be some additional issues we need to deal with here - which is if DevTools is going to have a single connection to DTD and we may want to subscribe to different kinds of services, we may need a single central class that manages tracking which services are available (since we can only subscribe to the Service stream once, and if the DtdEditorClient has done it, then other code won't be able to do it and won't know if their services are available).

DanTup commented 1 month ago

Some ideas:

  1. DtdEditorClient always creates its own DTD connection from the URI, regardless of anything else
  2. Create a single DtdEditorClient in DTDManager that can be used in both places (this fixes the current issue, but doesn't make DTD generally easier to consume multiple services from)
  3. Create a wrapped version of DartToolingDaemon that supports multiple subscribers to _dtd.onEvent('Service') by tracking the active services internally, and when there's a new subscriber, it re-sends events for the active services and then forwards any new incoming events
  4. Similar to 3, but instead of trying to emulate the streams directly, the class just exposes the list of current services and has strongly-typed events for services being registered/unregistered (and DtdEditorClient uses them instead of directly interacting with the service stream at all)
helin24 commented 1 month ago

@DanTup thanks for the debugging and thoughts here!

I'm inclined for this PR to do:

  1. Create a single DtdEditorClient in DTDManager that can be used in both places (this fixes the current issue, but doesn't make DTD generally easier to consume multiple services from)

It seems odd to create multiple DTD connections for one service (DevTools), and it sounds like options 3 and 4 warrant more consideration outside the scope of this change. What do you think? Also @kenzieschmoll

DanTup commented 1 month ago

@helin24 I started trying to share one, but then found that DtdEditorClient.event is also a single-listener stream, but both the sidebar and theme manager try to listen on it. Making it broadcast could result in lost events depending on the ordering.

So as a simple fix to avoid holding this PR up, I suggest we just use DTD directly here (don't use the editor client). It's a tiny change and I can look at refactoring DtdEditorClient later (it will be easier when we get rid of the PostMessage implementation, since they share interfaces, and I think we should do that after the next branch for stable).

Here's the diff that uses DTD directly:

PS D:\Dev\Google\devtools> git diff
diff --git a/packages/devtools_app/lib/src/framework/theme_manager.dart b/packages/devtools_app/lib/src/framework/theme_manager.dart
index f79a96b8f..c41bec15d 100644
--- a/packages/devtools_app/lib/src/framework/theme_manager.dart
+++ b/packages/devtools_app/lib/src/framework/theme_manager.dart
@@ -10,7 +10,6 @@ import 'package:dtd/dtd.dart';
 import 'package:logging/logging.dart';

 import '../service/editor/api_classes.dart';
-import '../service/editor/editor_client.dart';
 import '../shared/globals.dart';

 final _log = Logger('theme_manager');
@@ -18,17 +17,16 @@ final _log = Logger('theme_manager');
 /// Manages changes in theme settings from an editor/IDE.
 class EditorThemeManager extends DisposableController
     with AutoDisposeControllerMixin {
-  EditorThemeManager(DartToolingDaemon dtd)
-      : editorClient = DtdEditorClient(dtd);
+  EditorThemeManager(this.dtd);

-  final DtdEditorClient editorClient;
+  final DartToolingDaemon dtd;

   void listenForThemeChanges() {
     autoDisposeStreamSubscription(
-      editorClient.event.listen((event) {
-        if (event is ThemeChangedEvent) {
+      dtd.onEvent(editorStreamName).listen((event) {
+        if (event.kind == EditorEventKind.themeChanged.toString()) {
           final currentTheme = getIdeTheme();
-          final newTheme = event.theme;
+          final newTheme = ThemeChangedEvent.fromJson(event.data).theme;

           if (currentTheme.isDarkMode != newTheme.isDarkMode) {
             updateQueryParameter(
DanTup commented 1 month ago

@helin24 @kenzieschmoll btw, just as an FYI in case it comes up... while testing this in VS Code I found a weird bug where query string params (for colors + font size) were being lost. It turns out to be that something in Flutter is decoding encoded #s in the URL causing anything after them to be treated as a client-side fragment any time we update the URL in updateQueryParameter.

I've filed https://github.com/flutter/flutter/issues/155992 about it, but as a workaround, it turns out we will parse hex colours from the querystring with or without the hashes, so I'm updating VS Code to instead of doing (%23 is a percent-encoded #):

&backgroundColor=%23000000

Just doing:

&backgroundColor=000000

This fixes the issue I saw (which is that the background color for dark mode was always black, even when the theme had a slightly different dark shade). If IntelliJ also passes background/foreground colors, it may be worth ensuring it doesn't include the # to avoid any issue arising from that too.

DanTup commented 1 month ago

@helin24 we'll need to also add the .catch for streamListen in DtdEditorClient, like this:

https://github.com/DanTup/devtools/commit/b062884104a3498f1b1e10f785e94a36016975f8

(I think you should be able to easily apply this locally with git fetch https://github.com/DanTup/devtools && git cherry-pick b062884104a3498f1b1e10f785e94a36016975f8)

With this PR + that change, the sidebar is working fine in VS Code.

(I also fixed that we were using editorServiceName instead of editorStreamName in that code - the values are the same so it doesn't change anything, but it was a little misleading).

helin24 commented 1 month ago

@DanTup thanks for the patch! I applied it with copy and paste because I got this error from git: fatal: bad object b062884104a3498f1b1e10f785e94a36016975f8 Maybe something related to forks? 🤷