Use dashTool and dashIdeName to identify the launching IDE (#10037) Use dashTool and dashIdeName to identify the launching IDE IDE integrations now pass the `dashTool` and `dashIdeName` query parameters when they launch or embed DevTools, alongside the legacy `ide` parameter: * `dashTool` is the canonical label of the Dash tool that hosts DevTools, such as `vscode-plugins` or `intellij-plugins`. It is shared by every variant of an IDE that uses the same plugin; VS Code, Cursor and Antigravity all pass `vscode-plugins`. * `dashIdeName` is the name of the IDE, such as `Visual Studio Code`, `Cursor` or `Android Studio`. Its values are open-ended, so it is only suitable for display and diagnostics. The legacy `ide` parameter (`VSCode`, `Android-Studio`, `IntelliJ-IDEA`) is an ad-hoc vocabulary that cannot describe new IDEs or forks of IDEs, and it is expected to be removed once IDE integrations have passed `dashTool` and `dashIdeName` for long enough (at least two years). This change moves DevTools over to the new parameters, while remaining compatible with IDE integrations that only pass `ide`: * DevToolsQueryParams gains `dashTool`, `dashIdeName`, `ideDisplayName` (`dashIdeName`, falling back to `ide`) and `isVsCode`. `isVsCode` is true when `dashTool` is `vscode-plugins` or when `ide` is `VSCode`. The `ide` getter is documented as legacy, with a TODO to remove it. * The two checks of `ide == 'VSCode'` now use `isVsCode`: the choice of docs that the Property Editor links to, and the decision not to notify users that DevTools fell back from WebAssembly to JavaScript when embedded in VS Code, which disables cross-origin isolation. * A survey's `devEnvironments` may now list `dashTool` labels (like `vscode-plugins`) as well as the legacy `ide` values (like `VSCode`). The legacy `VSCode` environment also matches VS Code forks. The matching moves to `meetsEnvironmentRequirementFor`, which takes the query parameters as an argument so that it can be unit tested. Older versions of DevTools only know the legacy values, so surveys that need to reach them should keep listing those. * The environment details that DevTools pre-fills into bug reports use `ideDisplayName`, so they name the actual IDE (like Cursor). Analytics are not changed by this commit. `Event.devtoolsEvent` in `package:unified_analytics` does not accept `dashTool` or `dashIdeName` yet, so DevTools continues to report only the legacy `ide` value. That needs a change to `unified_analytics`, and follows once it is published. Part of https://github.com/flutter/devtools/issues/9811
diff --git a/packages/devtools_app/lib/src/shared/managers/survey.dart b/packages/devtools_app/lib/src/shared/managers/survey.dart index b275fa6..271bd95 100644 --- a/packages/devtools_app/lib/src/shared/managers/survey.dart +++ b/packages/devtools_app/lib/src/shared/managers/survey.dart
@@ -8,10 +8,12 @@ import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import 'package:logging/logging.dart'; +import 'package:unified_analytics/unified_analytics.dart' as ua; import '../analytics/analytics.dart' as ga; import '../development_helpers.dart'; import '../globals.dart'; +import '../primitives/query_parameters.dart'; import '../primitives/utils.dart'; import '../server/server.dart' as server; import '../utils/utils.dart'; @@ -232,7 +234,12 @@ /// If [devEnvironments] is null, the survey can be shown to any platform. /// /// The possible values for this list correspond to the possible values of - /// `_ideLaunched` from [shared/analytics/_analytics_web.dart]. + /// `_ideLaunched` from [shared/analytics/_analytics_web.dart]. They can also + /// be the labels of the Dash tools that IDEs pass in the `dashTool` query + /// parameter (e.g. 'vscode-plugins', 'intellij-plugins'), which are shared by + /// every variant of an IDE (e.g. VS Code, Cursor and Antigravity). Versions + /// of DevTools that predate the `dashTool` query parameter only recognize the + /// former. final List<String>? devEnvironments; } @@ -250,10 +257,70 @@ devToolsVersion, ).isSupported(minSupportedVersion: minDevToolsVersion!); - bool get meetsEnvironmentRequirement => - devEnvironments == null || - ga.ideLaunched.isEmpty || - devEnvironments!.contains(ga.ideLaunched); + /// Whether this survey should be shown in the development environment that + /// the current DevTools session was launched from. + /// + /// See [meetsEnvironmentRequirementFor] for how [devEnvironments] are matched. + bool get meetsEnvironmentRequirement => meetsEnvironmentRequirementFor( + ideLaunched: ga.ideLaunched, + queryParams: DevToolsQueryParams.load(), + ); + + /// Whether this survey should be shown in the development environment that + /// DevTools was launched from, which is described by [ideLaunched] and + /// [queryParams]. + /// + /// A survey that lists [devEnvironments] is shown if it lists the legacy + /// [ideLaunched] value, or the `dashTool` of the IDE integration that + /// launched DevTools. The legacy and `dashTool` environment names for VS Code + /// ('VSCode' and 'vscode-plugins') and for IntelliJ/Android Studio + /// ('Android-Studio', 'IntelliJ-IDEA', 'intellij-plugins' and + /// 'android-studio-plugins') match bidirectionally, including for VS Code + /// forks (such as Cursor or Antigravity) that do not pass 'VSCode' as their + /// legacy IDE value. + @visibleForTesting + bool meetsEnvironmentRequirementFor({ + required String ideLaunched, + required DevToolsQueryParams queryParams, + }) { + final environments = devEnvironments; + if (environments == null || ideLaunched.isEmpty) return true; + + final dashTool = queryParams.dashTool; + final isVsCode = + queryParams.isVsCode || + ideLaunched == DevToolsQueryParams.legacyVsCodeIdeValue; + if (isVsCode && + (environments.contains(DevToolsQueryParams.legacyVsCodeIdeValue) || + environments.contains(ua.DashTool.vscodePlugins.label))) { + return true; + } + + final isIntelliJOrAndroidStudio = + dashTool == ua.DashTool.intellijPlugins.label || + dashTool == ua.DashTool.androidStudioPlugins.label || + ideLaunched == DevToolsQueryParams.legacyAndroidStudioIdeValue || + ideLaunched == DevToolsQueryParams.legacyIntelliJIdeValue; + if (isIntelliJOrAndroidStudio && + (environments.contains( + DevToolsQueryParams.legacyAndroidStudioIdeValue, + ) || + environments.contains(DevToolsQueryParams.legacyIntelliJIdeValue) || + environments.contains(ua.DashTool.intellijPlugins.label) || + environments.contains(ua.DashTool.androidStudioPlugins.label))) { + return true; + } + + // When an IDE integration passes `dashTool` without the legacy `ide` + // parameter, `ga.ideLaunched` defaults to 'CLI'. Ignore that fallback so + // IDE launches do not match 'CLI' surveys. + final isIdeDashTool = isVsCode || isIntelliJOrAndroidStudio; + final matchesIdeLaunched = + (!isIdeDashTool || queryParams.ide != null) && + environments.contains(ideLaunched); + return matchesIdeLaunched || + (dashTool != null && environments.contains(dashTool)); + } bool get shouldShow => meetsDateRequirement &&
diff --git a/packages/devtools_app/lib/src/shared/preferences/preferences.dart b/packages/devtools_app/lib/src/shared/preferences/preferences.dart index 2719134..e1d3bbc 100644 --- a/packages/devtools_app/lib/src/shared/preferences/preferences.dart +++ b/packages/devtools_app/lib/src/shared/preferences/preferences.dart
@@ -231,7 +231,7 @@ // default. This is because cross-origin-isolation is disabled by VS // Code. See https://github.com/microsoft/vscode/issues/186614. final embeddedInVsCode = - queryParams.embedMode.embedded && queryParams.ide == 'VSCode'; + queryParams.embedMode.embedded && queryParams.isVsCode; if (!embeddedInVsCode) { notificationService.push( 'Something went wrong when trying to load DevTools with WebAssembly. '
diff --git a/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart b/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart index 7bd99f3..dfa86b0 100644 --- a/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart +++ b/packages/devtools_app/lib/src/shared/primitives/query_parameters.dart
@@ -6,6 +6,7 @@ import 'package:devtools_app_shared/ui.dart'; import 'package:devtools_app_shared/utils.dart'; import 'package:devtools_shared/devtools_shared.dart'; +import 'package:unified_analytics/unified_analytics.dart' as ua; extension type DevToolsQueryParams(Map<String, String?> params) { static DevToolsQueryParams empty() => DevToolsQueryParams({}); @@ -50,12 +51,58 @@ String? get appSizeTestFilePath => params[AppSizeApi.testAppSizeFilePropertyName]; + // TODO(https://github.com/flutter/devtools/issues/9811): Remove this legacy + // parameter once IDE integrations have been passing dashTool and dashIdeName + // for long enough (at least two years) to allow users ample time to upgrade. /// The IDE that DevTools is embedded in or was launched from. + /// + /// This is a legacy value (for example `VSCode`, `Android-Studio` or + /// `IntelliJ-IDEA`) that IDE integrations continue to pass for the benefit of + /// versions of DevTools that predate [dashTool] and [dashIdeName]. Prefer + /// those parameters, and use [isVsCode] to check for VS Code. String? get ide => params[ideKey]; /// The feature of the IDE that DevTools was opened from. String? get ideFeature => params[ideFeatureKey]; + /// The canonical label of the Dash tool, such as an IDE plugin, that DevTools + /// is embedded in or was launched from (for example `vscode-plugins` or + /// `intellij-plugins`). + /// + /// This corresponds to the `DASH__TOOL` environment variable that IDEs set + /// for the processes they spawn. It is shared by every variant of an IDE + /// that uses the same plugin (for example VS Code, Cursor and Antigravity). + /// See [ua.DashTool] for the labels of the known tools. + String? get dashTool => params[dashToolKey]; + + /// The name of the IDE that DevTools is embedded in or was launched from (for + /// example `Visual Studio Code`, `Cursor`, `Antigravity` or `Android Studio`). + /// + /// This corresponds to the `DASH__IDE_NAME` environment variable that IDEs + /// set for the processes they spawn. The possible values are open-ended, + /// because new IDEs, and forks of existing IDEs, choose their own names. So + /// this should only be used for display and diagnostics, and not to decide + /// how DevTools behaves. Use [dashTool] or [isVsCode] for that. + String? get dashIdeName => params[dashIdeNameKey]; + + /// The best available name of the IDE that DevTools is embedded in or was + /// launched from, for display and diagnostics (for example in bug reports). + /// + /// This is [dashIdeName], which distinguishes forks of an IDE (like Cursor) + /// from the IDE itself, falling back to the legacy [ide] for IDE integrations + /// that do not pass [dashIdeName]. + String? get ideDisplayName => dashIdeName ?? ide; + + /// Whether DevTools is embedded in or was launched from VS Code, or a fork of + /// VS Code such as Cursor or Antigravity. + /// + /// This is determined by [dashTool], which is the same for every variant of + /// VS Code. The legacy [ide] is checked as well, for IDE integrations that do + /// not pass [dashTool]. + bool get isVsCode => + dashTool == ua.DashTool.vscodePlugins.label || + ide == legacyVsCodeIdeValue; + /// Keys for theming values that an IDE may pass in the embedded DevTools URI. IdeThemeQueryParams get ideThemeParams => IdeThemeQueryParams(params); @@ -81,6 +128,18 @@ static const inspectorRefKey = 'inspectorRef'; static const ideKey = 'ide'; static const ideFeatureKey = 'ideFeature'; + static const dashToolKey = 'dashTool'; + static const dashIdeNameKey = 'dashIdeName'; + + /// The value of the legacy [ide] parameter that VS Code, and the forks of VS + /// Code that use the same extension, pass. + static const legacyVsCodeIdeValue = 'VSCode'; + + /// The value of the legacy [ide] parameter that Android Studio passes. + static const legacyAndroidStudioIdeValue = 'Android-Studio'; + + /// The value of the legacy [ide] parameter that IntelliJ IDEA passes. + static const legacyIntelliJIdeValue = 'IntelliJ-IDEA'; /// Query parameter key to determine whether to use dart2wasm or dart2js. ///
diff --git a/packages/devtools_app/lib/src/shared/utils/utils.dart b/packages/devtools_app/lib/src/shared/utils/utils.dart index eae789f..9dfbb9f 100644 --- a/packages/devtools_app/lib/src/shared/utils/utils.dart +++ b/packages/devtools_app/lib/src/shared/utils/utils.dart
@@ -127,7 +127,7 @@ /// This method should be public, because it is used by g3 specific code. List<String> issueLinkDetails() { - final ide = DevToolsQueryParams.load().ide; + final ide = DevToolsQueryParams.load().ideDisplayName; final issueDescriptionItems = [ '<-- Please describe your problem here. Be sure to include repro steps. -->', '___', // This will create a separator in the rendered markdown.
diff --git a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart index da6dda6..e96b008 100644 --- a/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart +++ b/packages/devtools_app/lib/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart
@@ -140,7 +140,9 @@ Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; - final documentationLink = _documentationLink(); + final documentationLink = propertyEditorDocumentationLink( + DevToolsQueryParams.load(), + ); return Container( decoration: BoxDecoration( color: colorScheme.surface, @@ -165,15 +167,17 @@ ), ); } +} - String? _documentationLink() { - final queryParams = DevToolsQueryParams.load(); - final isEmbedded = queryParams.embedMode.embedded; - if (!isEmbedded) return null; - const uriPrefix = 'https://docs.flutter.dev/tools/'; - const uriHash = '#property-editor'; - return '$uriPrefix${queryParams.ide == 'VSCode' ? 'vs-code' : 'android-studio'}$uriHash'; - } +/// Returns the Property Editor documentation link for the IDE described by +/// [queryParams], or `null` if DevTools is not embedded in an IDE. +@visibleForTesting +String? propertyEditorDocumentationLink(DevToolsQueryParams queryParams) { + final isEmbedded = queryParams.embedMode.embedded; + if (!isEmbedded) return null; + const uriPrefix = 'https://docs.flutter.dev/tools/'; + const uriHash = '#property-editor'; + return '$uriPrefix${queryParams.isVsCode ? 'vs-code' : 'android-studio'}$uriHash'; } class _DocsLink extends StatelessWidget {
diff --git a/packages/devtools_app/test/shared/managers/survey_test.dart b/packages/devtools_app/test/shared/managers/survey_test.dart index eee3fe8..44a5e05 100644 --- a/packages/devtools_app/test/shared/managers/survey_test.dart +++ b/packages/devtools_app/test/shared/managers/survey_test.dart
@@ -4,6 +4,7 @@ import 'package:devtools_app/devtools_app.dart'; import 'package:devtools_app/src/shared/managers/survey.dart'; +import 'package:devtools_app/src/shared/primitives/query_parameters.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -135,6 +136,291 @@ expect(vsCodeOnlySurvey.meetsEnvironmentRequirement, isTrue); expect(intelliJSurvey.meetsEnvironmentRequirement, isFalse); }); + + group('meetsEnvironmentRequirementFor', () { + final vsCodeSurvey = DevToolsSurvey.fromJson({ + 'devEnvironments': ['VSCode'], + }); + final intelliJSurvey = DevToolsSurvey.fromJson({ + 'devEnvironments': ['Android-Studio', 'IntelliJ-IDEA'], + }); + final vsCodeDashToolSurvey = DevToolsSurvey.fromJson({ + 'devEnvironments': ['vscode-plugins'], + }); + final intelliJDashToolSurvey = DevToolsSurvey.fromJson({ + 'devEnvironments': ['intellij-plugins'], + }); + final cliSurvey = DevToolsSurvey.fromJson({ + 'devEnvironments': ['CLI'], + }); + + bool meetsRequirement( + DevToolsSurvey survey, { + required String ideLaunched, + Map<String, String?> queryParams = const {}, + }) => survey.meetsEnvironmentRequirementFor( + ideLaunched: ideLaunched, + queryParams: DevToolsQueryParams(queryParams), + ); + + test('matches the legacy ide value', () { + // IDE integrations that predate the dashTool parameter. + const vsCodeParams = {DevToolsQueryParams.ideKey: 'VSCode'}; + expect( + meetsRequirement( + vsCodeSurvey, + ideLaunched: 'VSCode', + queryParams: vsCodeParams, + ), + isTrue, + ); + expect( + meetsRequirement( + intelliJSurvey, + ideLaunched: 'VSCode', + queryParams: vsCodeParams, + ), + isFalse, + ); + + const androidStudioParams = { + DevToolsQueryParams.ideKey: 'Android-Studio', + }; + expect( + meetsRequirement( + intelliJSurvey, + ideLaunched: 'Android-Studio', + queryParams: androidStudioParams, + ), + isTrue, + ); + expect( + meetsRequirement( + vsCodeSurvey, + ideLaunched: 'Android-Studio', + queryParams: androidStudioParams, + ), + isFalse, + ); + }); + + test('matches the dashTool', () { + const vsCodeParams = { + DevToolsQueryParams.ideKey: 'VSCode', + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Visual Studio Code', + }; + expect( + meetsRequirement( + vsCodeDashToolSurvey, + ideLaunched: 'VSCode', + queryParams: vsCodeParams, + ), + isTrue, + ); + expect( + meetsRequirement( + intelliJDashToolSurvey, + ideLaunched: 'VSCode', + queryParams: vsCodeParams, + ), + isFalse, + ); + + // Android Studio and IntelliJ share the same dashTool. + const androidStudioParams = { + DevToolsQueryParams.ideKey: 'Android-Studio', + DevToolsQueryParams.dashToolKey: 'intellij-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Android Studio', + }; + expect( + meetsRequirement( + intelliJDashToolSurvey, + ideLaunched: 'Android-Studio', + queryParams: androidStudioParams, + ), + isTrue, + ); + expect( + meetsRequirement( + vsCodeDashToolSurvey, + ideLaunched: 'Android-Studio', + queryParams: androidStudioParams, + ), + isFalse, + ); + }); + + test( + 'matches dashTool environments when only the legacy ide value is passed', + () { + const vsCodeParams = {DevToolsQueryParams.ideKey: 'VSCode'}; + expect( + meetsRequirement( + vsCodeDashToolSurvey, + ideLaunched: 'VSCode', + queryParams: vsCodeParams, + ), + isTrue, + ); + expect( + meetsRequirement( + intelliJDashToolSurvey, + ideLaunched: 'VSCode', + queryParams: vsCodeParams, + ), + isFalse, + ); + + for (final ide in ['Android-Studio', 'IntelliJ-IDEA']) { + final params = {DevToolsQueryParams.ideKey: ide}; + expect( + meetsRequirement( + intelliJDashToolSurvey, + ideLaunched: ide, + queryParams: params, + ), + isTrue, + reason: ide, + ); + expect( + meetsRequirement( + vsCodeDashToolSurvey, + ideLaunched: ide, + queryParams: params, + ), + isFalse, + reason: ide, + ); + } + }, + ); + + test('matches VS Code forks for the legacy VSCode environment', () { + // A fork does not have to pass 'VSCode' as its legacy ide value. + const cursorParams = { + DevToolsQueryParams.ideKey: 'Cursor', + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Cursor', + }; + expect( + meetsRequirement( + vsCodeSurvey, + ideLaunched: 'Cursor', + queryParams: cursorParams, + ), + isTrue, + ); + expect( + meetsRequirement( + intelliJSurvey, + ideLaunched: 'Cursor', + queryParams: cursorParams, + ), + isFalse, + ); + + // Once IDE integrations stop passing the legacy ide parameter, the + // launch environment defaults to 'CLI'. + const dashToolOnlyParams = { + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + }; + expect( + meetsRequirement( + vsCodeSurvey, + ideLaunched: 'CLI', + queryParams: dashToolOnlyParams, + ), + isTrue, + ); + expect( + meetsRequirement( + intelliJSurvey, + ideLaunched: 'CLI', + queryParams: dashToolOnlyParams, + ), + isFalse, + ); + expect( + meetsRequirement( + cliSurvey, + ideLaunched: 'CLI', + queryParams: dashToolOnlyParams, + ), + isFalse, + ); + + for (final dashTool in [ + 'intellij-plugins', + 'android-studio-plugins', + ]) { + final intelliJDashToolOnlyParams = { + DevToolsQueryParams.dashToolKey: dashTool, + }; + expect( + meetsRequirement( + intelliJSurvey, + ideLaunched: 'CLI', + queryParams: intelliJDashToolOnlyParams, + ), + isTrue, + reason: dashTool, + ); + expect( + meetsRequirement( + vsCodeSurvey, + ideLaunched: 'CLI', + queryParams: intelliJDashToolOnlyParams, + ), + isFalse, + reason: dashTool, + ); + expect( + meetsRequirement( + cliSurvey, + ideLaunched: 'CLI', + queryParams: intelliJDashToolOnlyParams, + ), + isFalse, + reason: dashTool, + ); + } + }); + + test('does not match other environments', () { + for (final survey in [ + vsCodeSurvey, + intelliJSurvey, + vsCodeDashToolSurvey, + intelliJDashToolSurvey, + ]) { + expect(meetsRequirement(survey, ideLaunched: 'CLI'), isFalse); + } + expect(meetsRequirement(cliSurvey, ideLaunched: 'CLI'), isTrue); + expect( + meetsRequirement( + cliSurvey, + ideLaunched: 'CLI', + queryParams: {DevToolsQueryParams.dashToolKey: 'flutter-tool'}, + ), + isTrue, + ); + }); + + test('matches any environment when none is specified or known', () { + final anyEnvironmentSurvey = DevToolsSurvey.fromJson({}); + expect( + meetsRequirement(anyEnvironmentSurvey, ideLaunched: 'CLI'), + isTrue, + ); + + // The launch environment is not known until the analytics + // dimensions are computed. + for (final survey in [vsCodeSurvey, vsCodeDashToolSurvey]) { + expect(meetsRequirement(survey, ideLaunched: ''), isTrue); + } + }); + }); }); }); }
diff --git a/packages/devtools_app/test/shared/primitives/query_parameters_test.dart b/packages/devtools_app/test/shared/primitives/query_parameters_test.dart index 9f70cf5..1d926a2 100644 --- a/packages/devtools_app/test/shared/primitives/query_parameters_test.dart +++ b/packages/devtools_app/test/shared/primitives/query_parameters_test.dart
@@ -24,6 +24,8 @@ AppSizeApi.testAppSizeFilePropertyName: '/path/to/test/file.json', DevToolsQueryParams.ideKey: 'Android-Studio', DevToolsQueryParams.ideFeatureKey: 'onDebugAutomatic', + DevToolsQueryParams.dashToolKey: 'intellij-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Android Studio', DevToolsQueryParams.legacyPageKey: 'memory', // IdeThemeQueryParams values 'embedMode': 'one', @@ -47,6 +49,10 @@ expect(params.appSizeTestFilePath, '/path/to/test/file.json'); expect(params.ide, 'Android-Studio'); expect(params.ideFeature, 'onDebugAutomatic'); + expect(params.dashTool, 'intellij-plugins'); + expect(params.dashIdeName, 'Android Studio'); + expect(params.ideDisplayName, 'Android Studio'); + expect(params.isVsCode, false); expect(params.legacyPage, 'memory'); expect(params.ideThemeParams.params, isNotEmpty); expect(params.embedMode, EmbedMode.embedOne); @@ -68,6 +74,10 @@ expect(params.appSizeTestFilePath, isNull); expect(params.ide, isNull); expect(params.ideFeature, isNull); + expect(params.dashTool, isNull); + expect(params.dashIdeName, isNull); + expect(params.ideDisplayName, isNull); + expect(params.isVsCode, false); expect(params.legacyPage, isNull); expect(params.ideThemeParams.params, isEmpty); expect(params.embedMode, EmbedMode.none); @@ -83,6 +93,8 @@ AppSizeApi.testAppSizeFilePropertyName: '/path/to/test/file.json', DevToolsQueryParams.ideKey: 'Android-Studio', DevToolsQueryParams.ideFeatureKey: 'onDebugAutomatic', + DevToolsQueryParams.dashToolKey: 'intellij-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Android Studio', DevToolsQueryParams.legacyPageKey: 'memory', // IdeThemeQueryParams values 'embedMode': 'one', @@ -99,6 +111,8 @@ expect(params.appSizeTestFilePath, '/path/to/test/file.json'); expect(params.ide, 'Android-Studio'); expect(params.ideFeature, 'onDebugAutomatic'); + expect(params.dashTool, 'intellij-plugins'); + expect(params.dashIdeName, 'Android Studio'); expect(params.legacyPage, 'memory'); expect(params.ideThemeParams.params, isNotEmpty); expect(params.embedMode, EmbedMode.embedOne); @@ -125,6 +139,8 @@ expect(params.appSizeTestFilePath, '/path/to/test/file.json'); expect(params.ide, 'Android-Studio'); expect(params.ideFeature, 'onDebugAutomatic'); + expect(params.dashTool, 'intellij-plugins'); + expect(params.dashIdeName, 'Android Studio'); expect(params.legacyPage, 'memory'); expect(params.ideThemeParams.params, isNotEmpty); expect(params.embedMode, EmbedMode.embedMany); @@ -156,5 +172,100 @@ ); } }); + + test('parses the dashTool and dashIdeName that IDEs pass in the URL', () { + // Dart-Code percent-encodes spaces, and flutter-intellij encodes them as + // '+'. Both should be decoded. + final vsCodeParams = DevToolsQueryParams.fromUrl( + 'http://localhost:9101/#/inspector?ide=VSCode&dashTool=vscode-plugins' + '&dashIdeName=Visual%20Studio%20Code', + ); + expect(vsCodeParams.ide, 'VSCode'); + expect(vsCodeParams.dashTool, 'vscode-plugins'); + expect(vsCodeParams.dashIdeName, 'Visual Studio Code'); + + final intelliJParams = DevToolsQueryParams.fromUrl( + 'http://localhost:9101/?ide=Android-Studio&dashTool=intellij-plugins' + '&dashIdeName=Android+Studio', + ); + expect(intelliJParams.ide, 'Android-Studio'); + expect(intelliJParams.dashTool, 'intellij-plugins'); + expect(intelliJParams.dashIdeName, 'Android Studio'); + }); + + group('isVsCode', () { + test('is true for the vscode-plugins dashTool', () { + final params = DevToolsQueryParams({ + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Visual Studio Code', + }); + expect(params.isVsCode, true); + }); + + test('is true for VS Code forks, which share the dashTool', () { + final params = DevToolsQueryParams({ + // A fork does not have to use the legacy 'VSCode' ide value. + DevToolsQueryParams.ideKey: 'Cursor', + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + DevToolsQueryParams.dashIdeNameKey: 'Cursor', + }); + expect(params.isVsCode, true); + }); + + test('falls back to the legacy ide parameter', () { + final params = DevToolsQueryParams({ + DevToolsQueryParams.ideKey: 'VSCode', + }); + expect(params.isVsCode, true); + }); + + test('is false for IntelliJ and Android Studio', () { + // Both pass the intellij-plugins dashTool, and are told apart by their + // dashIdeName. + for (final (ide, dashIdeName) in const [ + ('IntelliJ-IDEA', 'IntelliJ Community'), + ('Android-Studio', 'Android Studio'), + ]) { + final params = DevToolsQueryParams({ + DevToolsQueryParams.ideKey: ide, + DevToolsQueryParams.dashToolKey: 'intellij-plugins', + DevToolsQueryParams.dashIdeNameKey: dashIdeName, + }); + expect(params.isVsCode, false, reason: dashIdeName); + } + }); + + test('is false for IDEs that only pass the legacy ide parameter', () { + for (final ide in ['IntelliJ-IDEA', 'Android-Studio']) { + final params = DevToolsQueryParams({DevToolsQueryParams.ideKey: ide}); + expect(params.isVsCode, false, reason: ide); + } + }); + + test('is false when DevTools is not launched from an IDE', () { + expect(DevToolsQueryParams.empty().isVsCode, false); + }); + }); + + group('ideDisplayName', () { + test('prefers dashIdeName, which identifies VS Code forks', () { + final params = DevToolsQueryParams({ + DevToolsQueryParams.ideKey: 'VSCode', + DevToolsQueryParams.dashIdeNameKey: 'Cursor', + }); + expect(params.ideDisplayName, 'Cursor'); + }); + + test('falls back to the legacy ide parameter', () { + final params = DevToolsQueryParams({ + DevToolsQueryParams.ideKey: 'Android-Studio', + }); + expect(params.ideDisplayName, 'Android-Studio'); + }); + + test('is null when DevTools is not launched from an IDE', () { + expect(DevToolsQueryParams.empty().ideDisplayName, isNull); + }); + }); }); }
diff --git a/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_panel_test.dart b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_panel_test.dart new file mode 100644 index 0000000..8a9a44b --- /dev/null +++ b/packages/devtools_app/test/standalone_ui/ide_shared/property_editor/property_editor_panel_test.dart
@@ -0,0 +1,73 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app/src/shared/primitives/query_parameters.dart'; +import 'package:devtools_app/src/standalone_ui/ide_shared/property_editor/property_editor_panel.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('propertyEditorDocumentationLink', () { + const embedModeKey = 'embedMode'; + const embedOneValue = 'one'; + const vsCodeLink = 'https://docs.flutter.dev/tools/vs-code#property-editor'; + const androidStudioLink = + 'https://docs.flutter.dev/tools/android-studio#property-editor'; + + test('is null when not embedded', () { + expect( + propertyEditorDocumentationLink( + DevToolsQueryParams({ + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + }), + ), + isNull, + ); + }); + + test('links to VS Code docs for dashTool=vscode-plugins', () { + expect( + propertyEditorDocumentationLink( + DevToolsQueryParams({ + embedModeKey: embedOneValue, + DevToolsQueryParams.dashToolKey: 'vscode-plugins', + }), + ), + vsCodeLink, + ); + }); + + test('links to VS Code docs for legacy ide=VSCode', () { + expect( + propertyEditorDocumentationLink( + DevToolsQueryParams({ + embedModeKey: embedOneValue, + DevToolsQueryParams.ideKey: 'VSCode', + }), + ), + vsCodeLink, + ); + }); + + test('links to Android Studio docs for dashTool=intellij-plugins', () { + expect( + propertyEditorDocumentationLink( + DevToolsQueryParams({ + embedModeKey: embedOneValue, + DevToolsQueryParams.dashToolKey: 'intellij-plugins', + }), + ), + androidStudioLink, + ); + }); + + test('defaults to Android Studio docs when no IDE is specified', () { + expect( + propertyEditorDocumentationLink( + DevToolsQueryParams({embedModeKey: embedOneValue}), + ), + androidStudioLink, + ); + }); + }); +}