vscode.dart 11.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6 7
import 'package:meta/meta.dart';
import 'package:process/process.dart';

8
import '../base/file_system.dart';
9
import '../base/io.dart';
10
import '../base/platform.dart';
11
import '../base/utils.dart';
12
import '../base/version.dart';
13
import '../convert.dart';
14
import '../doctor_validator.dart';
15

16 17 18 19
const String extensionIdentifier = 'Dart-Code.flutter';
const String extensionMarketplaceUrl =
  'https://marketplace.visualstudio.com/items?itemName=$extensionIdentifier';

20
class VsCode {
21
  VsCode._(this.directory, this.extensionDirectory, { Version? version, this.edition, required FileSystem fileSystem})
22
      : version = version ?? Version.unknown {
23

24
    if (!fileSystem.isDirectorySync(directory)) {
25
      _validationMessages.add(ValidationMessage.error('VS Code not found at $directory'));
26
      return;
27 28
    } else {
      _validationMessages.add(ValidationMessage('VS Code at $directory'));
29 30 31 32
    }

    // If the extensions directory doesn't exist at all, the listSync()
    // below will fail, so just bail out early.
33 34 35 36 37
    const ValidationMessage notInstalledMessage = ValidationMessage(
      'Flutter extension can be installed from:',
      contextUrl: extensionMarketplaceUrl,
    );
    if (!fileSystem.isDirectorySync(extensionDirectory)) {
38
      _validationMessages.add(notInstalledMessage);
39 40 41 42
      return;
    }

    // Check for presence of extension.
43
    final String extensionIdentifierLower = extensionIdentifier.toLowerCase();
44
    final Iterable<FileSystemEntity> extensionDirs = fileSystem
45 46
        .directory(extensionDirectory)
        .listSync()
47 48
        .whereType<Directory>()
        .where((Directory d) => d.basename.toLowerCase().startsWith(extensionIdentifierLower));
49 50 51 52

    if (extensionDirs.isNotEmpty) {
      final FileSystemEntity extensionDir = extensionDirs.first;

53
      _extensionVersion = Version.parse(
54
          extensionDir.basename.substring('$extensionIdentifier-'.length));
55 56 57
      _validationMessages.add(ValidationMessage('Flutter extension version $_extensionVersion'));
    } else {
      _validationMessages.add(notInstalledMessage);
58 59 60
    }
  }

61 62 63
  factory VsCode.fromDirectory(
    String installPath,
    String extensionDirectory, {
64 65
    String? edition,
    required FileSystem fileSystem,
66
  }) {
67
    final String packageJsonPath =
68
        fileSystem.path.join(installPath, 'resources', 'app', 'package.json');
69 70
    final String? versionString = _getVersionFromPackageJson(packageJsonPath, fileSystem);
    Version? version;
71
    if (versionString != null) {
72
      version = Version.parse(versionString);
73
    }
74
    return VsCode._(installPath, extensionDirectory, version: version, edition: edition, fileSystem: fileSystem);
75 76
  }

77 78 79
  final String directory;
  final String extensionDirectory;
  final Version version;
80
  final String? edition;
81

82
  Version? _extensionVersion;
83
  final List<ValidationMessage> _validationMessages = <ValidationMessage>[];
84

85
  String get productName => 'VS Code${edition != null ? ', $edition' : ''}';
86

87
  Iterable<ValidationMessage> get validationMessages => _validationMessages;
88

89 90 91
  static List<VsCode> allInstalled(
    FileSystem fileSystem,
    Platform platform,
92
    ProcessManager processManager,
93 94
  ) {
    if (platform.isMacOS) {
95
      return _installedMacOS(fileSystem, platform, processManager);
96
    }
97 98
    if (platform.isWindows) {
      return _installedWindows(fileSystem, platform);
99
    }
100
    if (platform.isLinux) {
101
      return _installedLinux(fileSystem, platform);
102 103 104
    }
    // VS Code isn't supported on the other platforms.
    return <VsCode>[];
105 106 107 108 109 110 111 112 113 114
  }

  // 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
115
  static List<VsCode> _installedMacOS(FileSystem fileSystem, Platform platform, ProcessManager processManager) {
116
    final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath;
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138

    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(
139
        fileSystem.path.join('/Applications', 'Visual Studio Code.app', 'Contents'),
140 141
        '.vscode',
      ),
142
      if (homeDirPath != null)
143
        VsCodeInstallLocation(
144 145 146 147 148 149 150
          fileSystem.path.join(
            homeDirPath,
            'Applications',
            'Visual Studio Code.app',
            'Contents',
          ),
          '.vscode',
151
        ),
152
      VsCodeInstallLocation(
153
        fileSystem.path.join('/Applications', 'Visual Studio Code - Insiders.app', 'Contents'),
154 155
        '.vscode-insiders',
      ),
156
      if (homeDirPath != null)
157
        VsCodeInstallLocation(
158 159 160 161 162 163 164
          fileSystem.path.join(
            homeDirPath,
            'Applications',
            'Visual Studio Code - Insiders.app',
            'Contents',
          ),
          '.vscode-insiders',
165
        ),
166 167 168 169 170 171 172 173 174 175 176
      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);
177 178 179 180 181
  }

  // Windows:
  //   $programfiles(x86)\Microsoft VS Code
  //   $programfiles(x86)\Microsoft VS Code Insiders
182 183 184
  // User install:
  //   $localappdata\Programs\Microsoft VS Code
  //   $localappdata\Programs\Microsoft VS Code Insiders
185
  // TODO(dantup): Confirm these are correct for 64bit
186 187 188 189 190
  //   $programfiles\Microsoft VS Code
  //   $programfiles\Microsoft VS Code Insiders
  // Windows Extensions:
  //   $HOME/.vscode/extensions
  //   $HOME/.vscode-insiders/extensions
191 192 193 194
  static List<VsCode> _installedWindows(
    FileSystem fileSystem,
    Platform platform,
  ) {
195 196 197
    final String? progFiles86 = platform.environment['programfiles(x86)'];
    final String? progFiles = platform.environment['programfiles'];
    final String? localAppData = platform.environment['localappdata'];
198

199
    final List<VsCodeInstallLocation> searchLocations = <VsCodeInstallLocation>[
200
      if (localAppData != null)
201
        VsCodeInstallLocation(
202 203 204 205
          fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code'),
          '.vscode',
        ),
      if (progFiles86 != null)
206 207
        ...<VsCodeInstallLocation>[
          VsCodeInstallLocation(
208 209 210 211
            fileSystem.path.join(progFiles86, 'Microsoft VS Code'),
            '.vscode',
            edition: '32-bit edition',
          ),
212
          VsCodeInstallLocation(
213 214 215 216 217 218
            fileSystem.path.join(progFiles86, 'Microsoft VS Code Insiders'),
            '.vscode-insiders',
            edition: '32-bit edition',
          ),
        ],
      if (progFiles != null)
219 220
        ...<VsCodeInstallLocation>[
          VsCodeInstallLocation(
221 222 223 224
            fileSystem.path.join(progFiles, 'Microsoft VS Code'),
            '.vscode',
            edition: '64-bit edition',
          ),
225
          VsCodeInstallLocation(
226 227 228 229 230 231
            fileSystem.path.join(progFiles, 'Microsoft VS Code Insiders'),
            '.vscode-insiders',
            edition: '64-bit edition',
          ),
        ],
      if (localAppData != null)
232
        VsCodeInstallLocation(
233 234 235 236
          fileSystem.path.join(localAppData, r'Programs\Microsoft VS Code Insiders'),
          '.vscode-insiders',
        ),
    ];
237
    return _findInstalled(searchLocations, fileSystem, platform);
238 239 240 241
  }

  // Linux:
  //   /usr/share/code/bin/code
242
  //   /snap/code/current
243 244 245 246
  //   /usr/share/code-insiders/bin/code-insiders
  // Linux Extensions:
  //   $HOME/.vscode/extensions
  //   $HOME/.vscode-insiders/extensions
247
  static List<VsCode> _installedLinux(FileSystem fileSystem, Platform platform) {
248 249
    return _findInstalled(<VsCodeInstallLocation>[
      const VsCodeInstallLocation('/usr/share/code', '.vscode'),
250
      const VsCodeInstallLocation('/snap/code/current', '.vscode'),
251
      const VsCodeInstallLocation(
252 253 254
        '/usr/share/code-insiders',
        '.vscode-insiders',
      ),
255
    ], fileSystem, platform);
256 257
  }

258
  static List<VsCode> _findInstalled(
259
    Iterable<VsCodeInstallLocation> allLocations,
260
    FileSystem fileSystem,
261
    Platform platform,
262
  ) {
263 264
    final List<VsCode> results = <VsCode>[];

265
    for (final VsCodeInstallLocation searchLocation in allLocations) {
266 267
      final String? homeDirPath = FileSystemUtils(fileSystem: fileSystem, platform: platform).homeDirPath;
      if (homeDirPath != null && fileSystem.isDirectorySync(searchLocation.installPath)) {
268
        final String extensionDirectory = fileSystem.path.join(
269
          homeDirPath,
270 271 272 273 274 275 276
          searchLocation.extensionsFolder,
          'extensions',
        );
        results.add(VsCode.fromDirectory(
          searchLocation.installPath,
          extensionDirectory,
          edition: searchLocation.edition,
277
          fileSystem: fileSystem,
278
        ));
279 280 281 282 283 284 285 286
      }
    }

    return results;
  }

  @override
  String toString() =>
287
      'VS Code ($version)${_extensionVersion != Version.unknown ? ', Flutter ($_extensionVersion)' : ''}';
288

289
  static String? _getVersionFromPackageJson(String packageJsonPath, FileSystem fileSystem) {
290
    if (!fileSystem.isFileSync(packageJsonPath)) {
291
      return null;
292
    }
293
    final String jsonString = fileSystem.file(packageJsonPath).readAsStringSync();
294
    try {
295
      final Map<String, dynamic>? jsonObject = castStringKeyedMap(json.decode(jsonString));
296
      if (jsonObject?.containsKey('version') ?? false) {
297 298
        return jsonObject!['version'] as String;
      }
299
    } on FormatException {
300 301
      return null;
    }
302
    return null;
303 304
  }
}
305

306 307 308 309
@immutable
@visibleForTesting
class VsCodeInstallLocation {
  const VsCodeInstallLocation(
310 311 312
    this.installPath,
    this.extensionsFolder, {
    this.edition,
313
  });
314

315 316
  final String installPath;
  final String extensionsFolder;
317
  final String? edition;
318 319 320 321 322 323

  @override
  bool operator ==(Object other) {
    return other is VsCodeInstallLocation &&
        other.installPath == installPath &&
        other.extensionsFolder == extensionsFolder &&
324
        other.edition == edition;
325 326 327 328
  }

  @override
  // Lowest bit is for isInsiders boolean.
329
  int get hashCode => Object.hash(installPath, extensionsFolder, edition);
330
}