| // 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/utils.dart'; |
| import '../base/version.dart'; |
| import '../convert.dart'; |
| import '../doctor_validator.dart'; |
| |
| const String extensionIdentifier = 'Dart-Code.flutter'; |
| const String extensionMarketplaceUrl = |
| 'https://marketplace.visualstudio.com/items?itemName=$extensionIdentifier'; |
| |
| class VsCode { |
| VsCode._(this.directory, this.extensionDirectory, { Version? version, this.edition, required FileSystem fileSystem}) |
| : version = version ?? Version.unknown { |
| |
| if (!fileSystem.isDirectorySync(directory)) { |
| _validationMessages.add(ValidationMessage.error('VS Code not found at $directory')); |
| return; |
| } else { |
| _validationMessages.add(ValidationMessage('VS Code at $directory')); |
| } |
| |
| // If the extensions directory doesn't exist at all, the listSync() |
| // below will fail, so just bail out early. |
| const ValidationMessage notInstalledMessage = ValidationMessage( |
| 'Flutter extension can be installed from:', |
| contextUrl: extensionMarketplaceUrl, |
| ); |
| if (!fileSystem.isDirectorySync(extensionDirectory)) { |
| _validationMessages.add(notInstalledMessage); |
| return; |
| } |
| |
| // Check for presence of extension. |
| final String extensionIdentifierLower = extensionIdentifier.toLowerCase(); |
| final Iterable<FileSystemEntity> extensionDirs = fileSystem |
| .directory(extensionDirectory) |
| .listSync() |
| .whereType<Directory>() |
| .where((Directory d) => d.basename.toLowerCase().startsWith(extensionIdentifierLower)); |
| |
| if (extensionDirs.isNotEmpty) { |
| final FileSystemEntity extensionDir = extensionDirs.first; |
| |
| _extensionVersion = Version.parse( |
| extensionDir.basename.substring('$extensionIdentifier-'.length)); |
| _validationMessages.add(ValidationMessage('Flutter extension version $_extensionVersion')); |
| } else { |
| _validationMessages.add(notInstalledMessage); |
| } |
| } |
| |
| factory VsCode.fromDirectory( |
| String installPath, |
| String extensionDirectory, { |
| String? edition, |
| required FileSystem fileSystem, |
| }) { |
| final String packageJsonPath = |
| fileSystem.path.join(installPath, 'resources', 'app', 'package.json'); |
| final String? versionString = _getVersionFromPackageJson(packageJsonPath, fileSystem); |
| Version? version; |
| if (versionString != null) { |
| version = Version.parse(versionString); |
| } |
| return VsCode._(installPath, extensionDirectory, version: version, edition: edition, fileSystem: fileSystem); |
| } |
| |
| final String directory; |
| final String extensionDirectory; |
| final Version version; |
| final String? edition; |
| |
| Version? _extensionVersion; |
| final List<ValidationMessage> _validationMessages = <ValidationMessage>[]; |
| |
| String get productName => 'VS Code${edition != null ? ', $edition' : ''}'; |
| |
| Iterable<ValidationMessage> get validationMessages => _validationMessages; |
| |
| static List<VsCode> allInstalled( |
| FileSystem fileSystem, |
| Platform platform, |
| ProcessManager processManager, |
| ) { |
| if (platform.isMacOS) { |
| return _installedMacOS(fileSystem, platform, processManager); |
| } |
| if (platform.isWindows) { |
| return _installedWindows(fileSystem, platform); |
| } |
| if (platform.isLinux) { |
| return _installedLinux(fileSystem, platform); |
| } |
| // VS Code isn't supported on the other platforms. |
| return <VsCode>[]; |
| } |
| |
| // macOS: |
| // /Applications/Visual Studio Code.app/Contents/ |
| // /Applications/Visual Studio Code - Insiders.app/Contents/ |
| // $HOME/Applications/Visual Studio Code.app/Contents/ |
| // $HOME/Applications/Visual Studio Code - Insiders.app/Contents/ |
| // macOS Extensions: |
| // $HOME/.vscode/extensions |
| // $HOME/.vscode-insiders/extensions |
| static List<VsCode> _installedMacOS(FileSystem fileSystem, Platform platform, ProcessManager processManager) { |
| final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath; |
| |
| String vsCodeSpotlightResult = ''; |
| String vsCodeInsiderSpotlightResult = ''; |
| // Query Spotlight for unexpected installation locations. |
| try { |
| final ProcessResult vsCodeSpotlightQueryResult = processManager.runSync(<String>[ |
| 'mdfind', |
| 'kMDItemCFBundleIdentifier="com.microsoft.VSCode"', |
| ]); |
| vsCodeSpotlightResult = vsCodeSpotlightQueryResult.stdout as String; |
| final ProcessResult vsCodeInsidersSpotlightQueryResult = processManager.runSync(<String>[ |
| 'mdfind', |
| 'kMDItemCFBundleIdentifier="com.microsoft.VSCodeInsiders"', |
| ]); |
| vsCodeInsiderSpotlightResult = vsCodeInsidersSpotlightQueryResult.stdout as String; |
| } on ProcessException { |
| // The Spotlight query is a nice-to-have, continue checking known installation locations. |
| } |
| |
| // De-duplicated set. |
| return _findInstalled(<VsCodeInstallLocation>{ |
| VsCodeInstallLocation( |
| fileSystem.path.join('/Applications', 'Visual Studio Code.app', 'Contents'), |
| '.vscode', |
| ), |
| if (homeDirPath != null) |
| VsCodeInstallLocation( |
| fileSystem.path.join( |
| homeDirPath, |
| 'Applications', |
| 'Visual Studio Code.app', |
| 'Contents', |
| ), |
| '.vscode', |
| ), |
| VsCodeInstallLocation( |
| fileSystem.path.join('/Applications', 'Visual Studio Code - Insiders.app', 'Contents'), |
| '.vscode-insiders', |
| ), |
| if (homeDirPath != null) |
| VsCodeInstallLocation( |
| fileSystem.path.join( |
| homeDirPath, |
| 'Applications', |
| 'Visual Studio Code - Insiders.app', |
| 'Contents', |
| ), |
| '.vscode-insiders', |
| ), |
| for (final String vsCodePath in LineSplitter.split(vsCodeSpotlightResult)) |
| VsCodeInstallLocation( |
| fileSystem.path.join(vsCodePath, 'Contents'), |
| '.vscode', |
| ), |
| for (final String vsCodeInsidersPath in LineSplitter.split(vsCodeInsiderSpotlightResult)) |
| VsCodeInstallLocation( |
| fileSystem.path.join(vsCodeInsidersPath, 'Contents'), |
| '.vscode-insiders', |
| ), |
| }, fileSystem, platform); |
| } |
| |
| // Windows: |
| // $programfiles(x86)\Microsoft VS Code |
| // $programfiles(x86)\Microsoft VS Code Insiders |
| // User install: |
| // $localappdata\Programs\Microsoft VS Code |
| // $localappdata\Programs\Microsoft VS Code Insiders |
| // TODO(dantup): Confirm these are correct for 64bit |
| // $programfiles\Microsoft VS Code |
| // $programfiles\Microsoft VS Code Insiders |
| // Windows Extensions: |
| // $HOME/.vscode/extensions |
| // $HOME/.vscode-insiders/extensions |
| static List<VsCode> _installedWindows( |
| FileSystem fileSystem, |
| Platform platform, |
| ) { |
| final String? progFiles86 = platform.environment['programfiles(x86)']; |
| final String? progFiles = platform.environment['programfiles']; |
| final String? localAppData = platform.environment['localappdata']; |
| |
| final List<VsCodeInstallLocation> searchLocations = <VsCodeInstallLocation>[ |
| if (localAppData != null) |
| VsCodeInstallLocation( |
| fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code'), |
| '.vscode', |
| ), |
| if (progFiles86 != null) |
| ...<VsCodeInstallLocation>[ |
| VsCodeInstallLocation( |
| fileSystem.path.join(progFiles86, 'Microsoft VS Code'), |
| '.vscode', |
| edition: '32-bit edition', |
| ), |
| VsCodeInstallLocation( |
| fileSystem.path.join(progFiles86, 'Microsoft VS Code Insiders'), |
| '.vscode-insiders', |
| edition: '32-bit edition', |
| ), |
| ], |
| if (progFiles != null) |
| ...<VsCodeInstallLocation>[ |
| VsCodeInstallLocation( |
| fileSystem.path.join(progFiles, 'Microsoft VS Code'), |
| '.vscode', |
| edition: '64-bit edition', |
| ), |
| VsCodeInstallLocation( |
| fileSystem.path.join(progFiles, 'Microsoft VS Code Insiders'), |
| '.vscode-insiders', |
| edition: '64-bit edition', |
| ), |
| ], |
| if (localAppData != null) |
| VsCodeInstallLocation( |
| fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code Insiders'), |
| '.vscode-insiders', |
| ), |
| ]; |
| return _findInstalled(searchLocations, fileSystem, platform); |
| } |
| |
| // Linux: |
| // /usr/share/code/bin/code |
| // /snap/code/current |
| // /usr/share/code-insiders/bin/code-insiders |
| // Linux Extensions: |
| // $HOME/.vscode/extensions |
| // $HOME/.vscode-insiders/extensions |
| static List<VsCode> _installedLinux(FileSystem fileSystem, Platform platform) { |
| return _findInstalled(<VsCodeInstallLocation>[ |
| const VsCodeInstallLocation('/usr/share/code', '.vscode'), |
| const VsCodeInstallLocation('/snap/code/current', '.vscode'), |
| const VsCodeInstallLocation( |
| '/usr/share/code-insiders', |
| '.vscode-insiders', |
| ), |
| ], fileSystem, platform); |
| } |
| |
| static List<VsCode> _findInstalled( |
| Iterable<VsCodeInstallLocation> allLocations, |
| FileSystem fileSystem, |
| Platform platform, |
| ) { |
| final List<VsCode> results = <VsCode>[]; |
| |
| for (final VsCodeInstallLocation searchLocation in allLocations) { |
| final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath; |
| if (homeDirPath != null && fileSystem.isDirectorySync(searchLocation.installPath)) { |
| final String extensionDirectory = fileSystem.path.join( |
| homeDirPath, |
| searchLocation.extensionsFolder, |
| 'extensions', |
| ); |
| results.add(VsCode.fromDirectory( |
| searchLocation.installPath, |
| extensionDirectory, |
| edition: searchLocation.edition, |
| fileSystem: fileSystem, |
| )); |
| } |
| } |
| |
| return results; |
| } |
| |
| @override |
| String toString() => |
| 'VS Code ($version)${_extensionVersion != Version.unknown ? ', Flutter ($_extensionVersion)' : ''}'; |
| |
| static String? _getVersionFromPackageJson(String packageJsonPath, FileSystem fileSystem) { |
| if (!fileSystem.isFileSync(packageJsonPath)) { |
| return null; |
| } |
| final String jsonString = fileSystem.file(packageJsonPath).readAsStringSync(); |
| try { |
| final Map<String, dynamic>? jsonObject = castStringKeyedMap(json.decode(jsonString)); |
| if (jsonObject?.containsKey('version') ?? false) { |
| return jsonObject!['version'] as String; |
| } |
| } on FormatException { |
| return null; |
| } |
| return null; |
| } |
| } |
| |
| @immutable |
| @visibleForTesting |
| class VsCodeInstallLocation { |
| const VsCodeInstallLocation( |
| this.installPath, |
| this.extensionsFolder, { |
| this.edition, |
| }); |
| |
| final String installPath; |
| final String extensionsFolder; |
| final String? edition; |
| |
| @override |
| bool operator ==(Object other) { |
| return other is VsCodeInstallLocation && |
| other.installPath == installPath && |
| other.extensionsFolder == extensionsFolder && |
| other.edition == edition; |
| } |
| |
| @override |
| // Lowest bit is for isInsiders boolean. |
| int get hashCode => Object.hash(installPath, extensionsFolder, edition); |
| } |