blob: 83b987b8bf1f99ebd3ac6f971e55f6d6979de3ff [file] [log] [blame]
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. 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:collection/collection.dart';
import 'package:path/path.dart' as path;
/// Provides access to both global and resource-specific client configuration.
///
/// Resource-specific config is currently only supported at the WorkspaceFolder
/// level so when looking up config for a resource, the nearest WorkspaceFolders
/// config will be used.
class LspClientConfiguration {
/// Global settings for the workspace.
///
/// Used as a fallback for resource settings if no specific config is found
/// for the resource.
LspGlobalClientConfiguration _globalSettings =
LspGlobalClientConfiguration({});
/// Settings for each resource.
///
/// Keys are string paths without trailing path separators (eg. 'C:\foo').
final Map<String, LspResourceClientConfiguration> _resourceSettings =
<String, LspResourceClientConfiguration>{};
/// Pattern for stripping trailing slashes that may be been provided by the
/// client (in WorkspaceFolder URIs) for consistent comparisons.
final _trailingSlashPattern = RegExp(r'[\/]+$');
/// Returns the global configuration for the whole workspace.
LspGlobalClientConfiguration get global => _globalSettings;
/// Returns whether or not the provided new configuration changes any values
/// that would require analysis roots to be updated.
bool affectsAnalysisRoots(LspGlobalClientConfiguration otherConfig) {
return _globalSettings.analysisExcludedFolders !=
otherConfig.analysisExcludedFolders;
}
/// Returns config for a given resource.
///
/// Because we only support config at the WorkspaceFolder level, this is done
/// by finding the nearest WorkspaceFolder to [resourcePath] and using config
/// for that.
///
/// If no specific config is available, returns [global].
LspResourceClientConfiguration forResource(String resourcePath) {
final workspaceFolder = _getWorkspaceFolderPath(resourcePath);
if (workspaceFolder == null) {
return _globalSettings;
}
return _resourceSettings[_normaliseFolderPath(workspaceFolder)] ??
_globalSettings;
}
/// Replaces all previously known configuration with updated values from the
/// client.
void replace(
Map<String, Object?> globalConfig,
Map<String, Map<String, Object?>> workspaceFolderConfig,
) {
_globalSettings = LspGlobalClientConfiguration(globalConfig);
_resourceSettings
..clear()
..addAll(workspaceFolderConfig.map(
(key, value) => MapEntry(
_normaliseFolderPath(key),
LspResourceClientConfiguration(value, _globalSettings),
),
));
}
/// Gets the path for the WorkspaceFolder closest to [resourcePath].
String? _getWorkspaceFolderPath(String resourcePath) {
final candidates = _resourceSettings.keys
.where((wfPath) =>
wfPath == _normaliseFolderPath(resourcePath) ||
path.isWithin(wfPath, resourcePath))
.toList();
candidates.sort((a, b) => -a.length.compareTo(b.length));
return candidates.firstOrNull;
}
/// Normalises a folder path to never have a trailing path separator.
String _normaliseFolderPath(String path) =>
path.replaceAll(_trailingSlashPattern, '');
}
/// Wraps the client (editor) configuration to provide stronger typing and
/// handling of default values where a setting has not been supplied.
///
/// Settings in this class are only allowed to be configured at the workspace
/// level (they will be ignored at the resource level).
class LspGlobalClientConfiguration extends LspResourceClientConfiguration {
LspGlobalClientConfiguration(Map<String, Object?> settings)
: super(settings, null);
List<String> get analysisExcludedFolders {
// This setting is documented as a string array, but because editors are
// unlikely to provide validation, support single strings for convenience.
final value = _settings['analysisExcludedFolders'];
if (value is String) {
return [value];
} else if (value is List && value.every((s) => s is String)) {
return value.cast<String>();
} else {
return const [];
}
}
/// Whether methods/functions in completion should include parens and argument
/// placeholders when used in an invocation context.
bool get completeFunctionCalls =>
_settings['completeFunctionCalls'] as bool? ?? false;
/// A preview flag for enabling commit characters for completions.
///
/// This is a temporary setting to allow this feature to be tested without
/// defaulting to on for everybody.
bool get previewCommitCharacters =>
_settings['previewCommitCharacters'] as bool? ?? false;
/// Whether diagnostics should be generated for all TODO comments.
bool get showAllTodos =>
_settings['showTodos'] is bool ? _settings['showTodos'] as bool : false;
/// A specific set of TODO comments that should generate diagnostics.
///
/// Codes are all forced UPPERCASE regardless of what the client supplies.
///
/// [showAllTodos] should be checked first, as this will return an empty
/// set if `showTodos` is a boolean.
Set<String> get showTodoTypes => _settings['showTodos'] is List
? (_settings['showTodos'] as List)
.cast<String>()
.map((kind) => kind.toUpperCase())
.toSet()
: const {};
}
/// Wraps the client (editor) configuration for a specific resource.
///
/// Settings in this class are only allowed to be configured either for a
/// resource or for the whole workspace.
///
/// Right now, we treat "resource" to always mean a WorkspaceFolder since no
/// known editors allow per-file configuration and it allows us to keep the
/// settings cached, invalidated only when WorkspaceFolders change.
class LspResourceClientConfiguration {
final Map<String, Object?> _settings;
final LspResourceClientConfiguration? _fallback;
LspResourceClientConfiguration(this._settings, this._fallback);
/// Whether to enable the SDK formatter.
///
/// If this setting is `false`, the formatter will be unregistered with the
/// client.
bool get enableSdkFormatter =>
_settings['enableSdkFormatter'] as bool? ??
_fallback?.enableSdkFormatter ??
true;
/// The line length used when formatting documents.
///
/// If null, the formatters default will be used.
int? get lineLength =>
_settings['lineLength'] as int? ?? _fallback?.lineLength;
/// Whether to rename files when renaming classes inside them where the file
/// and class name match.
///
/// Values are "always", "prompt", "never". Any other values should be treated
/// like "never".
String get renameFilesWithClasses =>
_settings['renameFilesWithClasses'] as String? ?? 'never';
}