diff --git a/packages/devtools_app/lib/src/shared/managers/survey.dart b/packages/devtools_app/lib/src/shared/managers/survey.dart index b275fa688f4..271bd95f9de 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:devtools_shared/devtools_shared.dart'; 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 @@ class DevToolsSurvey { /// 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? devEnvironments; } @@ -250,10 +257,70 @@ extension ShowSurveyExtension on DevToolsSurvey { 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 27191349d24..e1d3bbc6ce6 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 @@ class PreferencesController extends DisposableController // 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 7bd99f33216..dfa86b0fb81 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/shared.dart'; 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 params) { static DevToolsQueryParams empty() => DevToolsQueryParams({}); @@ -50,12 +51,58 @@ extension type DevToolsQueryParams(Map params) { 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 @@ extension type DevToolsQueryParams(Map params) { 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 eae789f69b6..9dfbb9fc946 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 @@ List generateDeviceDescription( /// This method should be public, because it is used by g3 specific code. List 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 da6dda6a7eb..e96b008a756 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 @@ class _PropertyEditorFooter extends StatelessWidget { 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 @@ class _PropertyEditorFooter extends StatelessWidget { ), ); } +} - 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 eee3fe8ab58..44a5e05ed92 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 @@ void main() { 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 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 9f70cf598d4..1d926a24178 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { 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 @@ void main() { ); } }); + + 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 00000000000..8a9a44b9e18 --- /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, + ); + }); + }); +}