| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:meta/meta.dart'; |
| import 'package:process/process.dart'; |
| |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/platform.dart'; |
| import '../base/user_messages.dart' hide userMessages; |
| import '../base/version.dart'; |
| import '../convert.dart'; |
| import '../doctor_validator.dart'; |
| import '../ios/plist_parser.dart'; |
| import 'intellij.dart'; |
| |
| const String _ultimateEditionTitle = 'IntelliJ IDEA Ultimate Edition'; |
| const String _ultimateEditionId = 'IntelliJIdea'; |
| const String _communityEditionTitle = 'IntelliJ IDEA Community Edition'; |
| const String _communityEditionId = 'IdeaIC'; |
| |
| /// A doctor validator for both Intellij and Android Studio. |
| abstract class IntelliJValidator extends DoctorValidator { |
| IntelliJValidator(super.title, this.installPath, { |
| required FileSystem fileSystem, |
| required UserMessages userMessages, |
| }) : _fileSystem = fileSystem, |
| _userMessages = userMessages; |
| |
| final String installPath; |
| final FileSystem _fileSystem; |
| final UserMessages _userMessages; |
| |
| String get version; |
| |
| String? get pluginsPath; |
| |
| static const Map<String, String> _idToTitle = <String, String>{ |
| _ultimateEditionId: _ultimateEditionTitle, |
| _communityEditionId: _communityEditionTitle, |
| }; |
| |
| static final Version kMinIdeaVersion = Version(2017, 1, 0); |
| |
| /// Create a [DoctorValidator] for each installation of Intellij. |
| /// |
| /// On platforms other than macOS, Linux, and Windows this returns an |
| /// empty list. |
| static Iterable<DoctorValidator> installedValidators({ |
| required FileSystem fileSystem, |
| required Platform platform, |
| required UserMessages userMessages, |
| required PlistParser plistParser, |
| required ProcessManager processManager, |
| }) { |
| final FileSystemUtils fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform); |
| if (platform.isWindows) { |
| return IntelliJValidatorOnWindows.installed( |
| fileSystem: fileSystem, |
| fileSystemUtils: fileSystemUtils, |
| platform: platform, |
| userMessages: userMessages, |
| ); |
| } |
| if (platform.isLinux) { |
| return IntelliJValidatorOnLinux.installed( |
| fileSystem: fileSystem, |
| fileSystemUtils: fileSystemUtils, |
| userMessages: userMessages, |
| ); |
| } |
| if (platform.isMacOS) { |
| return IntelliJValidatorOnMac.installed( |
| fileSystem: fileSystem, |
| fileSystemUtils: fileSystemUtils, |
| userMessages: userMessages, |
| plistParser: plistParser, |
| processManager: processManager, |
| ); |
| } |
| return <DoctorValidator>[]; |
| } |
| |
| @override |
| Future<ValidationResult> validate() async { |
| final List<ValidationMessage> messages = <ValidationMessage>[]; |
| |
| if (pluginsPath == null) { |
| messages.add(const ValidationMessage.error('Invalid IntelliJ version number.')); |
| } else { |
| messages.add(ValidationMessage(_userMessages.intellijLocation(installPath))); |
| |
| final IntelliJPlugins plugins = IntelliJPlugins(pluginsPath!, fileSystem: _fileSystem); |
| plugins.validatePackage( |
| messages, |
| <String>['flutter-intellij', 'flutter-intellij.jar'], |
| 'Flutter', |
| IntelliJPlugins.kIntellijFlutterPluginUrl, |
| minVersion: IntelliJPlugins.kMinFlutterPluginVersion, |
| ); |
| plugins.validatePackage( |
| messages, |
| <String>['Dart'], |
| 'Dart', |
| IntelliJPlugins.kIntellijDartPluginUrl, |
| ); |
| |
| if (_hasIssues(messages)) { |
| messages.add(ValidationMessage(_userMessages.intellijPluginInfo)); |
| } |
| |
| _validateIntelliJVersion(messages, kMinIdeaVersion); |
| } |
| |
| return ValidationResult( |
| _hasIssues(messages) ? ValidationType.partial : ValidationType.installed, |
| messages, |
| statusInfo: _userMessages.intellijStatusInfo(version), |
| ); |
| } |
| |
| bool _hasIssues(List<ValidationMessage> messages) { |
| return messages.any((ValidationMessage message) => message.isError); |
| } |
| |
| void _validateIntelliJVersion(List<ValidationMessage> messages, Version minVersion) { |
| // Ignore unknown versions. |
| if (minVersion == Version.unknown) { |
| return; |
| } |
| |
| final Version? installedVersion = Version.parse(version); |
| if (installedVersion == null) { |
| return; |
| } |
| |
| if (installedVersion < minVersion) { |
| messages.add(ValidationMessage.error(_userMessages.intellijMinimumVersion(minVersion.toString()))); |
| } |
| } |
| } |
| |
| /// A windows specific implementation of the intellij validator. |
| class IntelliJValidatorOnWindows extends IntelliJValidator { |
| IntelliJValidatorOnWindows(String title, this.version, String installPath, this.pluginsPath, { |
| required FileSystem fileSystem, |
| required UserMessages userMessages, |
| }) : super(title, installPath, fileSystem: fileSystem, userMessages: userMessages); |
| |
| @override |
| final String version; |
| |
| @override |
| final String pluginsPath; |
| |
| static Iterable<DoctorValidator> installed({ |
| required FileSystem fileSystem, |
| required FileSystemUtils fileSystemUtils, |
| required Platform platform, |
| required UserMessages userMessages, |
| }) { |
| final List<DoctorValidator> validators = <DoctorValidator>[]; |
| if (fileSystemUtils.homeDirPath == null) { |
| return validators; |
| } |
| |
| void addValidator(String title, String version, String installPath, String pluginsPath) { |
| final IntelliJValidatorOnWindows validator = IntelliJValidatorOnWindows( |
| title, |
| version, |
| installPath, |
| pluginsPath, |
| fileSystem: fileSystem, |
| userMessages: userMessages, |
| ); |
| for (int index = 0; index < validators.length; index += 1) { |
| final DoctorValidator other = validators[index]; |
| if (other is IntelliJValidatorOnWindows && validator.installPath == other.installPath) { |
| if (validator.version.compareTo(other.version) > 0) { |
| validators[index] = validator; |
| } |
| return; |
| } |
| } |
| validators.add(validator); |
| } |
| |
| // before IntelliJ 2019 |
| final Directory homeDir = fileSystem.directory(fileSystemUtils.homeDirPath); |
| for (final Directory dir in homeDir.listSync().whereType<Directory>()) { |
| final String name = fileSystem.path.basename(dir.path); |
| IntelliJValidator._idToTitle.forEach((String id, String title) { |
| if (name.startsWith('.$id')) { |
| final String version = name.substring(id.length + 1); |
| String? installPath; |
| try { |
| installPath = fileSystem.file(fileSystem.path.join(dir.path, 'system', '.home')).readAsStringSync(); |
| } on FileSystemException { |
| // ignored |
| } |
| if (installPath != null && fileSystem.isDirectorySync(installPath)) { |
| final String pluginsPath = fileSystem.path.join(dir.path, 'config', 'plugins'); |
| addValidator(title, version, installPath, pluginsPath); |
| } |
| } |
| }); |
| } |
| |
| // after IntelliJ 2020 |
| if (!platform.environment.containsKey('LOCALAPPDATA')) { |
| return validators; |
| } |
| final Directory cacheDir = fileSystem.directory(fileSystem.path.join(platform.environment['LOCALAPPDATA']!, 'JetBrains')); |
| if (!cacheDir.existsSync()) { |
| return validators; |
| } |
| for (final Directory dir in cacheDir.listSync().whereType<Directory>()) { |
| final String name = fileSystem.path.basename(dir.path); |
| IntelliJValidator._idToTitle.forEach((String id, String title) { |
| if (name.startsWith(id)) { |
| final String version = name.substring(id.length); |
| String? installPath; |
| try { |
| installPath = fileSystem.file(fileSystem.path.join(dir.path, '.home')).readAsStringSync(); |
| } on FileSystemException { |
| // ignored |
| } |
| if (installPath != null && fileSystem.isDirectorySync(installPath)) { |
| String pluginsPath; |
| if (fileSystem.isDirectorySync('$installPath.plugins')) { |
| // IntelliJ 2020.3 |
| pluginsPath = '$installPath.plugins'; |
| addValidator(title, version, installPath, pluginsPath); |
| } else if (platform.environment.containsKey('APPDATA')) { |
| final String pluginsPathInAppData = fileSystem.path.join( |
| platform.environment['APPDATA']!, 'JetBrains', name, 'plugins'); |
| if (fileSystem.isDirectorySync(pluginsPathInAppData)) { |
| // IntelliJ 2020.1 ~ 2020.2 |
| pluginsPath = pluginsPathInAppData; |
| addValidator(title, version, installPath, pluginsPath); |
| } |
| } |
| } |
| } |
| }); |
| } |
| return validators; |
| } |
| } |
| |
| /// A linux specific implementation of the intellij validator. |
| class IntelliJValidatorOnLinux extends IntelliJValidator { |
| IntelliJValidatorOnLinux(String title, this.version, String installPath, this.pluginsPath, { |
| required FileSystem fileSystem, |
| required UserMessages userMessages, |
| }) : super(title, installPath, fileSystem: fileSystem, userMessages: userMessages); |
| |
| @override |
| final String version; |
| |
| @override |
| final String pluginsPath; |
| |
| static Iterable<DoctorValidator> installed({ |
| required FileSystem fileSystem, |
| required FileSystemUtils fileSystemUtils, |
| required UserMessages userMessages, |
| }) { |
| final List<DoctorValidator> validators = <DoctorValidator>[]; |
| final String? homeDirPath = fileSystemUtils.homeDirPath; |
| if (homeDirPath == null) { |
| return validators; |
| } |
| |
| void addValidator(String title, String version, String installPath, String pluginsPath) { |
| final IntelliJValidatorOnLinux validator = IntelliJValidatorOnLinux( |
| title, |
| version, |
| installPath, |
| pluginsPath, |
| fileSystem: fileSystem, |
| userMessages: userMessages, |
| ); |
| for (int index = 0; index < validators.length; index += 1) { |
| final DoctorValidator other = validators[index]; |
| if (other is IntelliJValidatorOnLinux && validator.installPath == other.installPath) { |
| if (validator.version.compareTo(other.version) > 0) { |
| validators[index] = validator; |
| } |
| return; |
| } |
| } |
| validators.add(validator); |
| } |
| |
| // before IntelliJ 2019 |
| final Directory homeDir = fileSystem.directory(homeDirPath); |
| for (final Directory dir in homeDir.listSync().whereType<Directory>()) { |
| final String name = fileSystem.path.basename(dir.path); |
| IntelliJValidator._idToTitle.forEach((String id, String title) { |
| if (name.startsWith('.$id')) { |
| final String version = name.substring(id.length + 1); |
| String? installPath; |
| try { |
| installPath = fileSystem.file(fileSystem.path.join(dir.path, 'system', '.home')).readAsStringSync(); |
| } on FileSystemException { |
| // ignored |
| } |
| if (installPath != null && fileSystem.isDirectorySync(installPath)) { |
| final String pluginsPath = fileSystem.path.join(dir.path, 'config', 'plugins'); |
| addValidator(title, version, installPath, pluginsPath); |
| } |
| } |
| }); |
| } |
| // after IntelliJ 2020 ~ |
| final Directory cacheDir = fileSystem.directory(fileSystem.path.join(homeDirPath, '.cache', 'JetBrains')); |
| if (!cacheDir.existsSync()) { |
| return validators; |
| } |
| for (final Directory dir in cacheDir.listSync().whereType<Directory>()) { |
| final String name = fileSystem.path.basename(dir.path); |
| IntelliJValidator._idToTitle.forEach((String id, String title) { |
| if (name.startsWith(id)) { |
| final String version = name.substring(id.length); |
| String? installPath; |
| try { |
| installPath = fileSystem.file(fileSystem.path.join(dir.path, '.home')).readAsStringSync(); |
| } on FileSystemException { |
| // ignored |
| } |
| if (installPath != null && fileSystem.isDirectorySync(installPath)) { |
| final String pluginsPathInUserHomeDir = fileSystem.path.join( |
| homeDirPath, |
| '.local', |
| 'share', |
| 'JetBrains', |
| name); |
| if (installPath.contains(fileSystem.path.join('JetBrains','Toolbox','apps'))) { |
| // via JetBrains ToolBox app |
| final String pluginsPathInInstallDir = '$installPath.plugins'; |
| if (fileSystem.isDirectorySync(pluginsPathInUserHomeDir)) { |
| // after 2020.2.x |
| final String pluginsPath = pluginsPathInUserHomeDir; |
| addValidator(title, version, installPath, pluginsPath); |
| } else if (fileSystem.isDirectorySync(pluginsPathInInstallDir)) { |
| // only 2020.1.X |
| final String pluginsPath = pluginsPathInInstallDir; |
| addValidator(title, version, installPath, pluginsPath); |
| } |
| } else { |
| // via tar.gz |
| final String pluginsPath = pluginsPathInUserHomeDir; |
| addValidator(title, version, installPath, pluginsPath); |
| } |
| } |
| } |
| }); |
| } |
| return validators; |
| } |
| } |
| |
| /// A macOS specific implementation of the intellij validator. |
| class IntelliJValidatorOnMac extends IntelliJValidator { |
| IntelliJValidatorOnMac(String title, this.id, String installPath, { |
| required FileSystem fileSystem, |
| required UserMessages userMessages, |
| required PlistParser plistParser, |
| required String? homeDirPath, |
| }) : _plistParser = plistParser, |
| _homeDirPath = homeDirPath, |
| super(title, installPath, fileSystem: fileSystem, userMessages: userMessages); |
| |
| final String id; |
| final PlistParser _plistParser; |
| final String? _homeDirPath; |
| |
| static const Map<String, String> _dirNameToId = <String, String>{ |
| 'IntelliJ IDEA.app': _ultimateEditionId, |
| 'IntelliJ IDEA Ultimate.app': _ultimateEditionId, |
| 'IntelliJ IDEA CE.app': _communityEditionId, |
| }; |
| |
| static Iterable<DoctorValidator> installed({ |
| required FileSystem fileSystem, |
| required FileSystemUtils fileSystemUtils, |
| required UserMessages userMessages, |
| required PlistParser plistParser, |
| required ProcessManager processManager, |
| }) { |
| final List<DoctorValidator> validators = <DoctorValidator>[]; |
| final String? homeDirPath = fileSystemUtils.homeDirPath; |
| final List<String> installPaths = <String>[ |
| '/Applications', |
| if (homeDirPath != null) |
| fileSystem.path.join(homeDirPath, 'Applications'), |
| ]; |
| |
| void checkForIntelliJ(Directory dir) { |
| final String name = fileSystem.path.basename(dir.path); |
| _dirNameToId.forEach((String dirName, String id) { |
| if (name == dirName) { |
| assert(IntelliJValidator._idToTitle.containsKey(id)); |
| final String title = IntelliJValidator._idToTitle[id]!; |
| validators.add(IntelliJValidatorOnMac( |
| title, |
| id, |
| dir.path, |
| fileSystem: fileSystem, |
| userMessages: userMessages, |
| plistParser: plistParser, |
| homeDirPath: homeDirPath, |
| )); |
| } |
| }); |
| } |
| |
| try { |
| final Iterable<Directory> installDirs = installPaths |
| .map(fileSystem.directory) |
| .map<List<FileSystemEntity>>((Directory dir) => dir.existsSync() ? dir.listSync() : <FileSystemEntity>[]) |
| .expand<FileSystemEntity>((List<FileSystemEntity> mappedDirs) => mappedDirs) |
| .whereType<Directory>(); |
| for (final Directory dir in installDirs) { |
| checkForIntelliJ(dir); |
| if (!dir.path.endsWith('.app')) { |
| for (final FileSystemEntity subdirectory in dir.listSync()) { |
| if (subdirectory is Directory) { |
| checkForIntelliJ(subdirectory); |
| } |
| } |
| } |
| } |
| |
| // Query Spotlight for unexpected installation locations. |
| String ceSpotlightResult = ''; |
| String ultimateSpotlightResult = ''; |
| try { |
| final ProcessResult ceQueryResult = processManager.runSync(<String>[ |
| 'mdfind', |
| 'kMDItemCFBundleIdentifier="com.jetbrains.intellij.ce"', |
| ]); |
| ceSpotlightResult = ceQueryResult.stdout as String; |
| final ProcessResult ultimateQueryResult = processManager.runSync(<String>[ |
| 'mdfind', |
| 'kMDItemCFBundleIdentifier="com.jetbrains.intellij*"', |
| ]); |
| ultimateSpotlightResult = ultimateQueryResult.stdout as String; |
| } on ProcessException { |
| // The Spotlight query is a nice-to-have, continue checking known installation locations. |
| } |
| |
| for (final String installPath in LineSplitter.split(ceSpotlightResult)) { |
| if (!validators.whereType<IntelliJValidatorOnMac>().any((IntelliJValidatorOnMac e) => e.installPath == installPath)) { |
| validators.add(IntelliJValidatorOnMac( |
| _communityEditionTitle, |
| _communityEditionId, |
| installPath, |
| fileSystem: fileSystem, |
| userMessages: userMessages, |
| plistParser: plistParser, |
| homeDirPath: homeDirPath, |
| )); |
| } |
| } |
| |
| for (final String installPath in LineSplitter.split(ultimateSpotlightResult)) { |
| if (!validators.whereType<IntelliJValidatorOnMac>().any((IntelliJValidatorOnMac e) => e.installPath == installPath)) { |
| validators.add(IntelliJValidatorOnMac( |
| _ultimateEditionTitle, |
| _ultimateEditionId, |
| installPath, |
| fileSystem: fileSystem, |
| userMessages: userMessages, |
| plistParser: plistParser, |
| homeDirPath: homeDirPath, |
| )); |
| } |
| } |
| } on FileSystemException catch (e) { |
| validators.add(ValidatorWithResult( |
| userMessages.intellijMacUnknownResult, |
| ValidationResult(ValidationType.missing, <ValidationMessage>[ |
| ValidationMessage.error(e.message), |
| ]), |
| )); |
| } |
| return validators; |
| } |
| |
| @visibleForTesting |
| String get plistFile { |
| _plistFile ??= _fileSystem.path.join(installPath, 'Contents', 'Info.plist'); |
| return _plistFile!; |
| } |
| String? _plistFile; |
| |
| @override |
| String get version { |
| return _version ??= _plistParser.getStringValueFromFile( |
| plistFile, |
| PlistParser.kCFBundleShortVersionStringKey, |
| ) ?? 'unknown'; |
| } |
| String? _version; |
| |
| @override |
| String? get pluginsPath { |
| if (_pluginsPath != null) { |
| return _pluginsPath!; |
| } |
| |
| final String? altLocation = _plistParser |
| .getStringValueFromFile(plistFile, 'JetBrainsToolboxApp'); |
| |
| if (altLocation != null) { |
| _pluginsPath = '$altLocation.plugins'; |
| return _pluginsPath!; |
| } |
| |
| final List<String> split = version.split('.'); |
| if (split.length < 2) { |
| return null; |
| } |
| final String major = split[0]; |
| final String minor = split[1]; |
| |
| final String? homeDirPath = _homeDirPath; |
| if (homeDirPath != null) { |
| String pluginsPath = _fileSystem.path.join( |
| homeDirPath, |
| 'Library', |
| 'Application Support', |
| 'JetBrains', |
| '$id$major.$minor', |
| 'plugins', |
| ); |
| // Fallback to legacy location from < 2020. |
| if (!_fileSystem.isDirectorySync(pluginsPath)) { |
| pluginsPath = _fileSystem.path.join( |
| homeDirPath, |
| 'Library', |
| 'Application Support', |
| '$id$major.$minor', |
| ); |
| } |
| _pluginsPath = pluginsPath; |
| } |
| |
| return _pluginsPath; |
| } |
| String? _pluginsPath; |
| } |