blob: 85537222e0ceb3228a51535607b885b733a09efa [file] [log] [blame]
// 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') == true) {
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);
}