// 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, { this.version, this.edition, required FileSystem fileSystem}) { 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 != null ? ', 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); }