blob: 2ec92a7f03fd98e4ef0ad2c48290fe2e54ea5580 [file] [log] [blame]
// Copyright (c) 2021, 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:analysis_server/lsp_protocol/protocol.dart';
import 'package:analysis_server/src/lsp/error_or.dart';
import 'package:analysis_server/src/lsp/handlers/handlers.dart';
import 'package:analysis_server/src/lsp/mapping.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer/src/dart/analysis/session_helper.dart';
import 'package:analyzer/src/dart/ast/utilities.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
/// Handles textDocument/colorPresentation.
///
/// This request is sent by the client if it allowed the user to pick a color
/// using a color picker (in a location returned by textDocument/documentColor)
/// and needs a representation of this color, including the edits to insert it
/// into the source file.
class DocumentColorPresentationHandler extends SharedMessageHandler<
ColorPresentationParams, List<ColorPresentation>> {
DocumentColorPresentationHandler(super.server);
@override
Method get handlesMessage => Method.textDocument_colorPresentation;
@override
LspJsonHandler<ColorPresentationParams> get jsonHandler =>
ColorPresentationParams.jsonHandler;
@override
bool get requiresTrustedCaller => false;
@override
Future<ErrorOr<List<ColorPresentation>>> handle(
ColorPresentationParams params,
MessageInfo message,
CancellationToken token,
) async {
if (!isDartDocument(params.textDocument)) {
return success([]);
}
var path = pathOfDoc(params.textDocument);
var unit = await path.mapResult(requireResolvedUnit);
return unit.mapResult((unit) => _getPresentations(params, unit));
}
/// Converts individual 0-255 ARGB values into a single int value as
/// 0xAARRGGBB as used by the dart:ui Color class.
int _colorValueForComponents(int alpha, int red, int green, int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
}
/// Creates a [ColorPresentation] for inserting code to produce a dart:ui
/// or Flutter Color at [editRange].
///
/// [colorType] is the Type of the Color class whose constructor will be
/// called. This will be replaced into [editRange] and any required import
/// statement will produce additional edits.
///
/// [label] is the visible label shown to the user and should roughly reflect
/// the code that will be inserted.
///
/// [invocationString] is written immediately after [colorType] in [editRange].
Future<ColorPresentation> _createColorPresentation({
required ResolvedUnitResult unit,
required SourceRange editRange,
required InterfaceElement colorType,
required String label,
required String invocationString,
required bool includeConstKeyword,
}) async {
var builder = ChangeBuilder(session: unit.session);
await builder.addDartFileEdit(unit.path, (builder) {
builder.addReplacement(editRange, (builder) {
if (includeConstKeyword) {
builder.write('const ');
}
builder.writeType(colorType.thisType);
builder.write(invocationString);
});
});
// We can only apply changes to the same file, so filter any change from the
// builder to only include this file, otherwise we may corrupt the users
// source (although hopefully we don't produce edits for other files).
var editsForThisFile = builder.sourceChange.edits
.where((edit) => edit.file == unit.path)
.expand((edit) => edit.edits)
.toList();
// LSP requires that we separate the main edit (changing the color code)
// from anything else (imports).
var mainEdit =
editsForThisFile.singleWhere((edit) => edit.offset == editRange.offset);
var otherEdits =
editsForThisFile.where((edit) => edit.offset != editRange.offset);
return ColorPresentation(
label: label,
textEdit: toTextEdit(unit.lineInfo, mainEdit),
additionalTextEdits: otherEdits.isNotEmpty
? otherEdits.map((edit) => toTextEdit(unit.lineInfo, edit)).toList()
: null,
);
}
/// Builds a list of valid color presentations for the requested color.
Future<ErrorOr<List<ColorPresentation>>> _getPresentations(
ColorPresentationParams params,
ResolvedUnitResult unit,
) async {
// If this file is outside of analysis roots, we cannot build edits for it
// so return null to signal to the client that it should not try to modify
// the source.
var analysisContext = unit.session.analysisContext;
if (!analysisContext.contextRoot.isAnalyzed(unit.path)) {
return success([]);
}
// The values in LSP are decimals 0-1 so should be scaled up to 255 that
// we use internally (except for opacity is which 0-1).
var alpha = (params.color.alpha * 255).toInt();
var red = (params.color.red * 255).toInt();
var green = (params.color.green * 255).toInt();
var blue = (params.color.blue * 255).toInt();
var opacity = params.color.alpha;
var editStart = toOffset(unit.lineInfo, params.range.start);
var editEnd = toOffset(unit.lineInfo, params.range.end);
return (editStart, editEnd).mapResults((editStart, editEnd) async {
var editRange = SourceRange(editStart, editEnd - editStart);
var sessionHelper = AnalysisSessionHelper(unit.session);
var colorType = await sessionHelper.getFlutterClass('Color');
if (colorType == null) {
// If we can't find the class (perhaps because this isn't a Flutter
// project) we will not include any results. In theory the client should
// not be calling this request in that case.
return success([]);
}
var requiresConstKeyword =
_willRequireConstKeyword(editRange.offset, unit);
var colorValue = _colorValueForComponents(alpha, red, green, blue);
var colorValueHex =
'0x${colorValue.toRadixString(16).toUpperCase().padLeft(8, '0')}';
var colorFromARGB = await _createColorPresentation(
unit: unit,
editRange: editRange,
colorType: colorType,
label: 'Color.fromARGB($alpha, $red, $green, $blue)',
invocationString: '.fromARGB($alpha, $red, $green, $blue)',
includeConstKeyword: requiresConstKeyword,
);
var colorFromRGBO = await _createColorPresentation(
unit: unit,
editRange: editRange,
colorType: colorType,
label: 'Color.fromRGBO($red, $green, $blue, $opacity)',
invocationString: '.fromRGBO($red, $green, $blue, $opacity)',
includeConstKeyword: requiresConstKeyword,
);
var colorDefault = await _createColorPresentation(
unit: unit,
editRange: editRange,
colorType: colorType,
label: 'Color($colorValueHex)',
invocationString: '($colorValueHex)',
includeConstKeyword: requiresConstKeyword,
);
return success([
colorFromARGB,
colorFromRGBO,
colorDefault,
]);
});
}
/// Checks whether a `const` keyword is required in front of inserted
/// constructor calls to preserve existing semantics.
///
/// `const` should be inserted if the existing expression is constant but
/// we are not already in a constant context.
bool _willRequireConstKeyword(int offset, ResolvedUnitResult unit) {
var node = NodeLocator2(offset).searchWithin(unit.unit);
if (node is! Expression) {
return false;
}
// `const` is unnecessary if we're in a constant context.
if (node.inConstantContext) {
return false;
}
if (node is InstanceCreationExpression) {
return node.isConst;
} else if (node is SimpleIdentifier) {
var parent = node.parent;
var staticElement = parent is PrefixedIdentifier
? parent.staticElement
: node.staticElement;
var target = staticElement is PropertyAccessorElement
? staticElement.variable2
: staticElement;
return target is ConstVariableElement;
} else {
return false;
}
}
}