vscode.dart 11.1 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, { this.version, this.edition, required FileSystem fileSystem}) {
22

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return results;
  }

  @override
  String toString() =>
286
      'VS Code ($version)${_extensionVersion != null ? ', Flutter ($_extensionVersion)' : ''}';
287

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

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

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

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

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