blob: 1b8e0247ad02579217c1b158202f30dc8eeb1687 [file] [log] [blame]
// Copyright (c) 2019, 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 'dart:collection';
import 'dart:math';
import 'package:analysis_server/lsp_protocol/protocol_custom_generated.dart'
as lsp;
import 'package:analysis_server/lsp_protocol/protocol_generated.dart' as lsp;
import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
import 'package:analysis_server/lsp_protocol/protocol_special.dart' as lsp;
import 'package:analysis_server/src/lsp/constants.dart' as lsp;
import 'package:analysis_server/src/lsp/constants.dart';
import 'package:analysis_server/src/lsp/dartdoc.dart';
import 'package:analysis_server/src/lsp/lsp_analysis_server.dart' as lsp;
import 'package:analysis_server/src/lsp/source_edits.dart';
import 'package:analysis_server/src/protocol_server.dart' as server
hide AnalysisError;
import 'package:analysis_server/src/search/workspace_symbols.dart' as server
show DeclarationKind;
import 'package:analyzer/dart/analysis/results.dart' as server;
import 'package:analyzer/diagnostic/diagnostic.dart' as analyzer;
import 'package:analyzer/error/error.dart' as server;
import 'package:analyzer/source/line_info.dart' as server;
import 'package:analyzer/source/source_range.dart' as server;
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/services/available_declarations.dart';
import 'package:analyzer/src/services/available_declarations.dart' as dec;
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
import 'package:analyzer_plugin/utilities/pair.dart';
import 'package:meta/meta.dart';
const diagnosticTagsForErrorCode = <server.ErrorCode, List<lsp.DiagnosticTag>>{
HintCode.DEAD_CODE: [lsp.DiagnosticTag.Unnecessary],
HintCode.DEPRECATED_MEMBER_USE: [lsp.DiagnosticTag.Deprecated],
HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE: [
lsp.DiagnosticTag.Deprecated
],
HintCode.DEPRECATED_MEMBER_USE_FROM_SAME_PACKAGE_WITH_MESSAGE: [
lsp.DiagnosticTag.Deprecated
],
HintCode.DEPRECATED_MEMBER_USE_WITH_MESSAGE: [lsp.DiagnosticTag.Deprecated],
};
const languageSourceName = 'dart';
lsp.Either2<String, lsp.MarkupContent> asStringOrMarkupContent(
List<lsp.MarkupKind> preferredFormats, String content) {
if (content == null) {
return null;
}
return preferredFormats == null
? lsp.Either2<String, lsp.MarkupContent>.t1(content)
: lsp.Either2<String, lsp.MarkupContent>.t2(
_asMarkup(preferredFormats, content));
}
/// Builds an LSP snippet string with supplied ranges as tabstops.
String buildSnippetStringWithTabStops(
String text,
List<int> offsetLengthPairs,
) {
text ??= '';
offsetLengthPairs ??= const [];
String escape(String input) => input.replaceAllMapped(
RegExp(r'[$}\\]'), // Replace any of $ } \
(c) => '\\${c[0]}', // Prefix with a backslash
);
// Snippets syntax is documented in the LSP spec:
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax
//
// $1, $2, etc. are used for tab stops and ${1:foo} inserts a placeholder of foo.
final output = [];
var offset = 0;
// When there's only a single tabstop, it should be ${0} as this is treated
// specially as the final cursor position (if we use 1, the editor will insert
// a 0 at the end of the string which is not what we expect).
// When there are multiple, start with ${1} since these are placeholders the
// user can tab through and the editor-inserted ${0} at the end is expected.
var tabStopNumber = offsetLengthPairs.length <= 2 ? 0 : 1;
for (var i = 0; i < offsetLengthPairs.length; i += 2) {
final pairOffset = offsetLengthPairs[i];
final pairLength = offsetLengthPairs[i + 1];
// Add any text that came before this tabstop to the result.
output.add(escape(text.substring(offset, pairOffset)));
// Add this tabstop
final tabStopText =
escape(text.substring(pairOffset, pairOffset + pairLength));
output.add('\${${tabStopNumber++}:$tabStopText}');
offset = pairOffset + pairLength;
}
// Add any remaining text that was after the last tabstop.
output.add(escape(text.substring(offset)));
return output.join('');
}
/// Note: This code will fetch the version of each document being modified so
/// it's important to call this immediately after computing edits to ensure
/// the document is not modified before the version number is read.
lsp.WorkspaceEdit createWorkspaceEdit(
lsp.LspAnalysisServer server, List<server.SourceFileEdit> edits) {
return toWorkspaceEdit(
server.clientCapabilities?.workspace,
edits
.map((e) => FileEditInformation(
server.getVersionedDocumentIdentifier(e.file),
server.getLineInfo(e.file),
e.edits,
// fileStamp == 1 is used by the server to indicate the file needs creating.
newFile: e.fileStamp == -1,
))
.toList());
}
lsp.CompletionItemKind declarationKindToCompletionItemKind(
HashSet<lsp.CompletionItemKind> clientSupportedCompletionKinds,
dec.DeclarationKind kind,
) {
bool isSupported(lsp.CompletionItemKind kind) =>
clientSupportedCompletionKinds.contains(kind);
List<lsp.CompletionItemKind> getKindPreferences() {
switch (kind) {
case dec.DeclarationKind.CLASS:
case dec.DeclarationKind.CLASS_TYPE_ALIAS:
case dec.DeclarationKind.MIXIN:
return const [lsp.CompletionItemKind.Class];
case dec.DeclarationKind.CONSTRUCTOR:
return const [lsp.CompletionItemKind.Constructor];
case dec.DeclarationKind.ENUM:
case dec.DeclarationKind.ENUM_CONSTANT:
return const [lsp.CompletionItemKind.Enum];
case dec.DeclarationKind.FUNCTION:
return const [lsp.CompletionItemKind.Function];
case dec.DeclarationKind.FUNCTION_TYPE_ALIAS:
return const [lsp.CompletionItemKind.Class];
case dec.DeclarationKind.GETTER:
return const [lsp.CompletionItemKind.Property];
case dec.DeclarationKind.SETTER:
return const [lsp.CompletionItemKind.Property];
case dec.DeclarationKind.VARIABLE:
return const [lsp.CompletionItemKind.Variable];
default:
return const [];
}
}
return getKindPreferences().firstWhere(isSupported, orElse: () => null);
}
lsp.SymbolKind declarationKindToSymbolKind(
HashSet<lsp.SymbolKind> clientSupportedSymbolKinds,
server.DeclarationKind kind,
) {
bool isSupported(lsp.SymbolKind kind) =>
clientSupportedSymbolKinds.contains(kind);
List<lsp.SymbolKind> getKindPreferences() {
switch (kind) {
case server.DeclarationKind.CLASS:
case server.DeclarationKind.CLASS_TYPE_ALIAS:
return const [lsp.SymbolKind.Class];
case server.DeclarationKind.CONSTRUCTOR:
return const [lsp.SymbolKind.Constructor];
case server.DeclarationKind.ENUM:
return const [lsp.SymbolKind.Enum];
case server.DeclarationKind.ENUM_CONSTANT:
return const [lsp.SymbolKind.EnumMember, lsp.SymbolKind.Enum];
case server.DeclarationKind.FIELD:
return const [lsp.SymbolKind.Field];
case server.DeclarationKind.FUNCTION:
return const [lsp.SymbolKind.Function];
case server.DeclarationKind.FUNCTION_TYPE_ALIAS:
return const [lsp.SymbolKind.Class];
case server.DeclarationKind.GETTER:
return const [lsp.SymbolKind.Property];
case server.DeclarationKind.METHOD:
return const [lsp.SymbolKind.Method];
case server.DeclarationKind.MIXIN:
return const [lsp.SymbolKind.Class];
case server.DeclarationKind.SETTER:
return const [lsp.SymbolKind.Property];
case server.DeclarationKind.VARIABLE:
return const [lsp.SymbolKind.Variable];
default:
// Assert that we only get here if kind=null. If it's anything else
// then we're missing a mapping from above.
assert(kind == null, 'Unexpected declaration kind $kind');
return const [];
}
}
// LSP requires we specify *some* kind, so in the case where the above code doesn't
// match we'll just have to send a value to avoid a crash.
return getKindPreferences()
.firstWhere(isSupported, orElse: () => lsp.SymbolKind.Obj);
}
lsp.CompletionItem declarationToCompletionItem(
lsp.CompletionClientCapabilities completionCapabilities,
HashSet<lsp.CompletionItemKind> supportedCompletionItemKinds,
String file,
int offset,
server.IncludedSuggestionSet includedSuggestionSet,
Library library,
Map<String, int> tagBoosts,
server.LineInfo lineInfo,
dec.Declaration declaration,
int replacementOffset,
int insertLength,
int replacementLength, {
@required bool includeCommitCharacters,
@required bool completeFunctionCalls,
}) {
final supportsSnippets =
completionCapabilities?.completionItem?.snippetSupport == true;
String completion;
switch (declaration.kind) {
case DeclarationKind.ENUM_CONSTANT:
completion = '${declaration.parent.name}.${declaration.name}';
break;
case DeclarationKind.GETTER:
case DeclarationKind.FIELD:
completion = declaration.parent != null &&
declaration.parent.name != null &&
declaration.parent.name.isNotEmpty
? '${declaration.parent.name}.${declaration.name}'
: declaration.name;
break;
case DeclarationKind.CONSTRUCTOR:
completion = declaration.parent.name;
if (declaration.name.isNotEmpty) {
completion += '.${declaration.name}';
}
break;
default:
completion = declaration.name;
break;
}
// By default, label is the same as the completion text, but may be added to
// later (parens/snippets).
var label = completion;
// isCallable is used to suffix the label with parens so it's clear the item
// is callable.
final declarationKind = declaration.kind;
final isCallable = declarationKind == DeclarationKind.CONSTRUCTOR ||
declarationKind == DeclarationKind.FUNCTION ||
declarationKind == DeclarationKind.METHOD;
if (isCallable) {
label += declaration.parameterNames?.isNotEmpty ?? false ? '(…)' : '()';
}
final insertTextInfo = _buildInsertText(
supportsSnippets: supportsSnippets,
includeCommitCharacters: includeCommitCharacters,
completeFunctionCalls: completeFunctionCalls,
isCallable: isCallable,
// For SuggestionSets, we don't have a CompletionKind to check if it's
// an invocation, but since they do not show in show/hide combinators
// we can assume if an item is callable it's probably being used in a context
// that can invoke it.
isInvocation: isCallable,
defaultArgumentListString: declaration.defaultArgumentListString,
defaultArgumentListTextRanges: declaration.defaultArgumentListTextRanges,
completion: completion,
selectionOffset: 0,
selectionLength: 0,
);
final insertText = insertTextInfo.first;
final insertTextFormat = insertTextInfo.last;
final isMultilineCompletion = insertText.contains('\n');
final supportsDeprecatedFlag =
completionCapabilities?.completionItem?.deprecatedSupport == true;
final supportedTags =
completionCapabilities?.completionItem?.tagSupport?.valueSet ?? const [];
final supportsDeprecatedTag =
supportedTags.contains(lsp.CompletionItemTag.Deprecated);
final supportsAsIsInsertMode = completionCapabilities
?.completionItem?.insertTextModeSupport?.valueSet
?.contains(InsertTextMode.asIs) ==
true;
final completionKind = declarationKindToCompletionItemKind(
supportedCompletionItemKinds, declaration.kind);
var relevanceBoost = 0;
if (declaration.relevanceTags != null) {
declaration.relevanceTags.forEach(
(t) => relevanceBoost = max(relevanceBoost, tagBoosts[t] ?? 0));
}
final itemRelevance = includedSuggestionSet.relevance + relevanceBoost;
// Because we potentially send thousands of these items, we should minimise
// the generated JSON as much as possible - for example using nulls in place
// of empty lists/false where possible.
return lsp.CompletionItem(
label: label,
kind: completionKind,
tags: nullIfEmpty([
if (supportsDeprecatedTag && declaration.isDeprecated)
lsp.CompletionItemTag.Deprecated
]),
commitCharacters:
includeCommitCharacters ? lsp.dartCompletionCommitCharacters : null,
detail: getDeclarationCompletionDetail(declaration, completionKind,
supportsDeprecatedFlag || supportsDeprecatedTag),
deprecated:
supportsDeprecatedFlag && declaration.isDeprecated ? true : null,
// Relevance is a number, highest being best. LSP does text sort so subtract
// from a large number so that a text sort will result in the correct order.
// 555 -> 999455
// 10 -> 999990
// 1 -> 999999
sortText: (1000000 - itemRelevance).toString(),
filterText: completion != label
? completion
: null, // filterText uses label if not set
insertText: insertText != label
? insertText
: null, // insertText uses label if not set
insertTextFormat: insertTextFormat != lsp.InsertTextFormat.PlainText
? insertTextFormat
: null, // Defaults to PlainText if not supplied
insertTextMode: supportsAsIsInsertMode && isMultilineCompletion
? InsertTextMode.asIs
: null,
// data, used for completionItem/resolve.
data: lsp.DartCompletionItemResolutionInfo(
file: file,
offset: offset,
libId: includedSuggestionSet.id,
displayUri: includedSuggestionSet.displayUri ?? library.uri?.toString(),
rOffset: replacementOffset,
iLength: insertLength,
rLength: replacementLength),
);
}
lsp.CompletionItemKind elementKindToCompletionItemKind(
HashSet<lsp.CompletionItemKind> clientSupportedCompletionKinds,
server.ElementKind kind,
) {
bool isSupported(lsp.CompletionItemKind kind) =>
clientSupportedCompletionKinds.contains(kind);
List<lsp.CompletionItemKind> getKindPreferences() {
switch (kind) {
case server.ElementKind.CLASS:
case server.ElementKind.CLASS_TYPE_ALIAS:
return const [lsp.CompletionItemKind.Class];
case server.ElementKind.COMPILATION_UNIT:
return const [lsp.CompletionItemKind.Module];
case server.ElementKind.CONSTRUCTOR:
case server.ElementKind.CONSTRUCTOR_INVOCATION:
return const [lsp.CompletionItemKind.Constructor];
case server.ElementKind.ENUM:
case server.ElementKind.ENUM_CONSTANT:
return const [lsp.CompletionItemKind.Enum];
case server.ElementKind.FIELD:
return const [lsp.CompletionItemKind.Field];
case server.ElementKind.FILE:
return const [lsp.CompletionItemKind.File];
case server.ElementKind.FUNCTION:
return const [lsp.CompletionItemKind.Function];
case server.ElementKind.FUNCTION_TYPE_ALIAS:
return const [lsp.CompletionItemKind.Class];
case server.ElementKind.GETTER:
return const [lsp.CompletionItemKind.Property];
case server.ElementKind.LABEL:
// There isn't really a good CompletionItemKind for labels so we'll
// just use the Text option.
return const [lsp.CompletionItemKind.Text];
case server.ElementKind.LIBRARY:
return const [lsp.CompletionItemKind.Module];
case server.ElementKind.LOCAL_VARIABLE:
return const [lsp.CompletionItemKind.Variable];
case server.ElementKind.METHOD:
return const [lsp.CompletionItemKind.Method];
case server.ElementKind.MIXIN:
return const [lsp.CompletionItemKind.Class];
case server.ElementKind.PARAMETER:
case server.ElementKind.PREFIX:
return const [lsp.CompletionItemKind.Variable];
case server.ElementKind.SETTER:
return const [lsp.CompletionItemKind.Property];
case server.ElementKind.TOP_LEVEL_VARIABLE:
return const [lsp.CompletionItemKind.Variable];
case server.ElementKind.TYPE_PARAMETER:
return const [
lsp.CompletionItemKind.TypeParameter,
lsp.CompletionItemKind.Variable,
];
case server.ElementKind.UNIT_TEST_GROUP:
case server.ElementKind.UNIT_TEST_TEST:
return const [lsp.CompletionItemKind.Method];
default:
return const [];
}
}
return getKindPreferences().firstWhere(isSupported, orElse: () => null);
}
lsp.SymbolKind elementKindToSymbolKind(
HashSet<lsp.SymbolKind> clientSupportedSymbolKinds,
server.ElementKind kind,
) {
bool isSupported(lsp.SymbolKind kind) =>
clientSupportedSymbolKinds.contains(kind);
List<lsp.SymbolKind> getKindPreferences() {
switch (kind) {
case server.ElementKind.CLASS:
case server.ElementKind.CLASS_TYPE_ALIAS:
return const [lsp.SymbolKind.Class];
case server.ElementKind.COMPILATION_UNIT:
return const [lsp.SymbolKind.Module];
case server.ElementKind.CONSTRUCTOR:
case server.ElementKind.CONSTRUCTOR_INVOCATION:
return const [lsp.SymbolKind.Constructor];
case server.ElementKind.ENUM:
return const [lsp.SymbolKind.Enum];
case server.ElementKind.ENUM_CONSTANT:
return const [lsp.SymbolKind.EnumMember, lsp.SymbolKind.Enum];
case server.ElementKind.EXTENSION:
return const [lsp.SymbolKind.Namespace];
case server.ElementKind.FIELD:
return const [lsp.SymbolKind.Field];
case server.ElementKind.FILE:
return const [lsp.SymbolKind.File];
case server.ElementKind.FUNCTION:
case server.ElementKind.FUNCTION_INVOCATION:
return const [lsp.SymbolKind.Function];
case server.ElementKind.FUNCTION_TYPE_ALIAS:
return const [lsp.SymbolKind.Class];
case server.ElementKind.GETTER:
return const [lsp.SymbolKind.Property];
case server.ElementKind.LABEL:
// There isn't really a good SymbolKind for labels so we'll
// just use the Null option.
return const [lsp.SymbolKind.Null];
case server.ElementKind.LIBRARY:
return const [lsp.SymbolKind.Namespace];
case server.ElementKind.LOCAL_VARIABLE:
return const [lsp.SymbolKind.Variable];
case server.ElementKind.METHOD:
return const [lsp.SymbolKind.Method];
case server.ElementKind.MIXIN:
return const [lsp.SymbolKind.Class];
case server.ElementKind.PARAMETER:
case server.ElementKind.PREFIX:
return const [lsp.SymbolKind.Variable];
case server.ElementKind.SETTER:
return const [lsp.SymbolKind.Property];
case server.ElementKind.TOP_LEVEL_VARIABLE:
return const [lsp.SymbolKind.Variable];
case server.ElementKind.TYPE_PARAMETER:
return const [
lsp.SymbolKind.TypeParameter,
lsp.SymbolKind.Variable,
];
case server.ElementKind.UNIT_TEST_GROUP:
case server.ElementKind.UNIT_TEST_TEST:
return const [lsp.SymbolKind.Method];
default:
// Assert that we only get here if kind=null. If it's anything else
// then we're missing a mapping from above.
assert(kind == null, 'Unexpected element kind $kind');
return const [];
}
}
// LSP requires we specify *some* kind, so in the case where the above code doesn't
// match we'll just have to send a value to avoid a crash.
return getKindPreferences()
.firstWhere(isSupported, orElse: () => lsp.SymbolKind.Obj);
}
String getCompletionDetail(
server.CompletionSuggestion suggestion,
lsp.CompletionItemKind completionKind,
bool clientSupportsDeprecated,
) {
final hasElement = suggestion.element != null;
final hasParameters = hasElement &&
suggestion.element.parameters != null &&
suggestion.element.parameters.isNotEmpty;
final hasReturnType = hasElement &&
suggestion.element.returnType != null &&
suggestion.element.returnType.isNotEmpty;
final hasParameterType =
suggestion.parameterType != null && suggestion.parameterType.isNotEmpty;
final prefix = clientSupportsDeprecated || !suggestion.isDeprecated
? ''
: '(Deprecated) ';
if (completionKind == lsp.CompletionItemKind.Property) {
// Setters appear as methods with one arg but they also cause getters to not
// appear in the completion list, so displaying them as setters is misleading.
// To avoid this, always show only the return type, whether it's a getter
// or a setter.
return prefix +
(suggestion.element.kind == server.ElementKind.GETTER
? suggestion.element.returnType
// Don't assume setters always have parameters
// See https://github.com/dart-lang/sdk/issues/27747
: suggestion.element.parameters != null &&
suggestion.element.parameters.isNotEmpty
// Extract the type part from '(MyType value)`
? suggestion.element.parameters.substring(
1, suggestion.element.parameters.lastIndexOf(' '))
: '');
} else if (hasParameters && hasReturnType) {
return '$prefix${suggestion.element.parameters} → ${suggestion.element.returnType}';
} else if (hasReturnType) {
return '$prefix${suggestion.element.returnType}';
} else if (hasParameterType) {
return '$prefix${suggestion.parameterType}';
} else {
return prefix.isNotEmpty ? prefix : null;
}
}
String getDeclarationCompletionDetail(
dec.Declaration declaration,
lsp.CompletionItemKind completionKind,
bool clientSupportsDeprecated,
) {
final hasParameters =
declaration.parameters != null && declaration.parameters.isNotEmpty;
final hasReturnType =
declaration.returnType != null && declaration.returnType.isNotEmpty;
final prefix = clientSupportsDeprecated || !declaration.isDeprecated
? ''
: '(Deprecated) ';
if (completionKind == lsp.CompletionItemKind.Property) {
// Setters appear as methods with one arg but they also cause getters to not
// appear in the completion list, so displaying them as setters is misleading.
// To avoid this, always show only the return type, whether it's a getter
// or a setter.
var suffix = '';
if (declaration.kind == dec.DeclarationKind.GETTER) {
suffix = declaration.returnType;
} else {
// Don't assume setters always have parameters
// See https://github.com/dart-lang/sdk/issues/27747
if (declaration.parameters != null && declaration.parameters.isNotEmpty) {
// Extract the type part from `(MyType value)`, if there is a type.
var spaceIndex = declaration.parameters.lastIndexOf(' ');
if (spaceIndex > 0) {
suffix = declaration.parameters.substring(1, spaceIndex);
}
}
}
return prefix + suffix;
} else if (hasParameters && hasReturnType) {
return '$prefix${declaration.parameters} → ${declaration.returnType}';
} else if (hasReturnType) {
return '$prefix${declaration.returnType}';
} else {
return prefix.isNotEmpty ? prefix : null;
}
}
List<lsp.DiagnosticTag> getDiagnosticTags(
HashSet<lsp.DiagnosticTag> supportedTags, server.AnalysisError error) {
if (supportedTags == null) {
return null;
}
final tags = diagnosticTagsForErrorCode[error.errorCode]
?.where(supportedTags.contains)
?.toList();
return tags != null && tags.isNotEmpty ? tags : null;
}
bool isDartDocument(lsp.TextDocumentIdentifier doc) =>
doc?.uri?.endsWith('.dart');
lsp.Location navigationTargetToLocation(
String targetFilePath,
server.NavigationTarget target,
server.LineInfo targetLineInfo,
) {
if (targetLineInfo == null) {
return null;
}
return lsp.Location(
uri: Uri.file(targetFilePath).toString(),
range: toRange(targetLineInfo, target.offset, target.length),
);
}
lsp.LocationLink navigationTargetToLocationLink(
server.NavigationRegion region,
server.LineInfo regionLineInfo,
String targetFilePath,
server.NavigationTarget target,
server.LineInfo targetLineInfo,
) {
if (regionLineInfo == null || targetLineInfo == null) {
return null;
}
final nameRange = toRange(targetLineInfo, target.offset, target.length);
final codeRange = target.codeOffset != null && target.codeLength != null
? toRange(targetLineInfo, target.codeOffset, target.codeLength)
: nameRange;
return lsp.LocationLink(
originSelectionRange: toRange(regionLineInfo, region.offset, region.length),
targetUri: Uri.file(targetFilePath).toString(),
targetRange: codeRange,
targetSelectionRange: nameRange,
);
}
List<T> nullIfEmpty<T>(List<T> items) => items.isEmpty ? null : items;
/// Returns the file system path for a TextDocumentIdentifier.
ErrorOr<String> pathOfDoc(lsp.TextDocumentIdentifier doc) =>
pathOfUri(Uri.tryParse(doc?.uri));
/// Returns the file system path for a TextDocumentItem.
ErrorOr<String> pathOfDocItem(lsp.TextDocumentItem doc) =>
pathOfUri(Uri.tryParse(doc?.uri));
/// Returns the file system path for a file URI.
ErrorOr<String> pathOfUri(Uri uri) {
if (uri == null) {
return ErrorOr<String>.error(ResponseError(
code: lsp.ServerErrorCodes.InvalidFilePath,
message: 'Document URI was not supplied',
));
}
final isValidFileUri = (uri?.isScheme('file') ?? false);
if (!isValidFileUri) {
return ErrorOr<String>.error(ResponseError(
code: lsp.ServerErrorCodes.InvalidFilePath,
message: 'URI was not a valid file:// URI',
data: uri.toString(),
));
}
try {
return ErrorOr<String>.success(uri.toFilePath());
} catch (e) {
// Even if tryParse() works and file == scheme, toFilePath() can throw on
// Windows if there are invalid characters.
return ErrorOr<String>.error(ResponseError(
code: lsp.ServerErrorCodes.InvalidFilePath,
message: 'File URI did not contain a valid file path',
data: uri.toString()));
}
}
lsp.Diagnostic pluginToDiagnostic(
server.LineInfo Function(String) getLineInfo,
plugin.AnalysisError error,
) {
List<lsp.DiagnosticRelatedInformation> relatedInformation;
if (error.contextMessages != null && error.contextMessages.isNotEmpty) {
relatedInformation = error.contextMessages
.map((message) =>
pluginToDiagnosticRelatedInformation(getLineInfo, message))
.toList();
}
var message = error.message;
if (error.correction != null) {
message = '$message\n${error.correction}';
}
var lineInfo = getLineInfo(error.location.file);
return lsp.Diagnostic(
range: toRange(lineInfo, error.location.offset, error.location.length),
severity: pluginToDiagnosticSeverity(error.severity),
code: error.code,
source: languageSourceName,
message: message,
relatedInformation: relatedInformation,
);
}
lsp.DiagnosticRelatedInformation pluginToDiagnosticRelatedInformation(
server.LineInfo Function(String) getLineInfo,
plugin.DiagnosticMessage message) {
var file = message.location.file;
var lineInfo = getLineInfo(file);
return lsp.DiagnosticRelatedInformation(
location: lsp.Location(
uri: Uri.file(file).toString(),
range: toRange(
lineInfo,
message.location.offset,
message.location.length,
),
),
message: message.message);
}
lsp.DiagnosticSeverity pluginToDiagnosticSeverity(
plugin.AnalysisErrorSeverity severity) {
switch (severity) {
case plugin.AnalysisErrorSeverity.ERROR:
return lsp.DiagnosticSeverity.Error;
case plugin.AnalysisErrorSeverity.WARNING:
return lsp.DiagnosticSeverity.Warning;
case plugin.AnalysisErrorSeverity.INFO:
return lsp.DiagnosticSeverity.Information;
// Note: LSP also supports "Hint", but they won't render in things like the
// VS Code errors list as they're apparently intended to communicate
// non-visible diagnostics back (for example, if you wanted to grey out
// unreachable code without producing an item in the error list).
default:
throw 'Unknown AnalysisErrorSeverity: $severity';
}
}
lsp.Location searchResultToLocation(
server.SearchResult result, server.LineInfo lineInfo) {
final location = result.location;
if (lineInfo == null) {
return null;
}
return lsp.Location(
uri: Uri.file(result.location.file).toString(),
range: toRange(lineInfo, location.offset, location.length),
);
}
lsp.CompletionItemKind suggestionKindToCompletionItemKind(
HashSet<lsp.CompletionItemKind> clientSupportedCompletionKinds,
server.CompletionSuggestionKind kind,
String label,
) {
bool isSupported(lsp.CompletionItemKind kind) =>
clientSupportedCompletionKinds.contains(kind);
List<lsp.CompletionItemKind> getKindPreferences() {
switch (kind) {
case server.CompletionSuggestionKind.ARGUMENT_LIST:
return const [lsp.CompletionItemKind.Variable];
case server.CompletionSuggestionKind.IMPORT:
// For package/relative URIs, we can send File/Folder kinds for better icons.
if (!label.startsWith('dart:')) {
return label.endsWith('.dart')
? const [
lsp.CompletionItemKind.File,
lsp.CompletionItemKind.Module,
]
: const [
lsp.CompletionItemKind.Folder,
lsp.CompletionItemKind.Module,
];
}
return const [lsp.CompletionItemKind.Module];
case server.CompletionSuggestionKind.IDENTIFIER:
return const [lsp.CompletionItemKind.Variable];
case server.CompletionSuggestionKind.INVOCATION:
return const [lsp.CompletionItemKind.Method];
case server.CompletionSuggestionKind.KEYWORD:
return const [lsp.CompletionItemKind.Keyword];
case server.CompletionSuggestionKind.NAMED_ARGUMENT:
return const [lsp.CompletionItemKind.Variable];
case server.CompletionSuggestionKind.OPTIONAL_ARGUMENT:
return const [lsp.CompletionItemKind.Variable];
case server.CompletionSuggestionKind.PARAMETER:
return const [lsp.CompletionItemKind.Value];
case server.CompletionSuggestionKind.PACKAGE_NAME:
return const [lsp.CompletionItemKind.Module];
default:
return const [];
}
}
return getKindPreferences().firstWhere(isSupported, orElse: () => null);
}
lsp.ClosingLabel toClosingLabel(
server.LineInfo lineInfo, server.ClosingLabel label) =>
lsp.ClosingLabel(
range: toRange(lineInfo, label.offset, label.length),
label: label.label);
lsp.CodeActionKind toCodeActionKind(String id, lsp.CodeActionKind fallback) {
if (id == null) {
return fallback;
}
// Dart fixes and assists start with "dart.assist." and "dart.fix." but in LSP
// we want to use the predefined prefixes for CodeActions.
final newId = id
.replaceAll('dart.assist', lsp.CodeActionKind.Refactor.toString())
.replaceAll('dart.fix', lsp.CodeActionKind.QuickFix.toString())
.replaceAll(
'analysisOptions.assist', lsp.CodeActionKind.Refactor.toString())
.replaceAll(
'analysisOptions.fix', lsp.CodeActionKind.QuickFix.toString());
return lsp.CodeActionKind(newId);
}
lsp.CompletionItem toCompletionItem(
lsp.CompletionClientCapabilities completionCapabilities,
HashSet<lsp.CompletionItemKind> supportedCompletionItemKinds,
server.LineInfo lineInfo,
server.CompletionSuggestion suggestion,
int replacementOffset,
int insertLength,
int replacementLength, {
@required bool includeCommitCharacters,
@required bool completeFunctionCalls,
Object resolutionData,
}) {
// Build separate display and filter labels. Displayed labels may have additional
// info appended (for example '(...)' on callables) that should not be included
// in filterText.
var label = suggestion.displayText ?? suggestion.completion;
final filterText = label;
// Trim any trailing comma from the (displayed) label.
if (label.endsWith(',')) {
label = label.substring(0, label.length - 1);
}
// isCallable is used to suffix the label with parens so it's clear the item
// is callable.
//
// isInvocation means the location at which it's used is an invocation (and
// therefore it is appropriate to include the parens/parameters in the
// inserted text).
//
// In the case of show combinators, the parens will still be shown to indicate
// functions but they should not be included in the completions.
final elementKind = suggestion.element?.kind;
final isCallable = elementKind == server.ElementKind.CONSTRUCTOR ||
elementKind == server.ElementKind.FUNCTION ||
elementKind == server.ElementKind.METHOD;
final isInvocation =
suggestion.kind == server.CompletionSuggestionKind.INVOCATION;
if (suggestion.displayText == null && isCallable) {
label += suggestion.parameterNames?.isNotEmpty ?? false ? '(…)' : '()';
}
final supportsDeprecatedFlag =
completionCapabilities?.completionItem?.deprecatedSupport == true;
final supportedTags =
completionCapabilities?.completionItem?.tagSupport?.valueSet ?? const [];
final supportsDeprecatedTag =
supportedTags.contains(lsp.CompletionItemTag.Deprecated);
final formats = completionCapabilities?.completionItem?.documentationFormat;
final supportsSnippets =
completionCapabilities?.completionItem?.snippetSupport == true;
final supportsInsertReplace =
completionCapabilities?.completionItem?.insertReplaceSupport == true;
final supportsAsIsInsertMode = completionCapabilities
?.completionItem?.insertTextModeSupport?.valueSet
?.contains(InsertTextMode.asIs) ==
true;
final completionKind = suggestion.element != null
? elementKindToCompletionItemKind(
supportedCompletionItemKinds, suggestion.element.kind)
: suggestionKindToCompletionItemKind(
supportedCompletionItemKinds, suggestion.kind, label);
final insertTextInfo = _buildInsertText(
supportsSnippets: supportsSnippets,
includeCommitCharacters: includeCommitCharacters,
completeFunctionCalls: completeFunctionCalls,
isCallable: isCallable,
isInvocation: isInvocation,
defaultArgumentListString: suggestion.defaultArgumentListString,
defaultArgumentListTextRanges: suggestion.defaultArgumentListTextRanges,
completion: suggestion.completion,
selectionOffset: suggestion.selectionOffset,
selectionLength: suggestion.selectionLength,
);
final insertText = insertTextInfo.first;
final insertTextFormat = insertTextInfo.last;
final isMultilineCompletion = insertText.contains('\n');
// Because we potentially send thousands of these items, we should minimise
// the generated JSON as much as possible - for example using nulls in place
// of empty lists/false where possible.
return lsp.CompletionItem(
label: label,
kind: completionKind,
tags: nullIfEmpty([
if (supportsDeprecatedTag && suggestion.isDeprecated)
lsp.CompletionItemTag.Deprecated
]),
commitCharacters:
includeCommitCharacters ? dartCompletionCommitCharacters : null,
data: resolutionData,
detail: getCompletionDetail(suggestion, completionKind,
supportsDeprecatedFlag || supportsDeprecatedTag),
documentation:
asStringOrMarkupContent(formats, cleanDartdoc(suggestion.docComplete)),
deprecated: supportsDeprecatedFlag && suggestion.isDeprecated ? true : null,
// Relevance is a number, highest being best. LSP does text sort so subtract
// from a large number so that a text sort will result in the correct order.
// 555 -> 999455
// 10 -> 999990
// 1 -> 999999
sortText: (1000000 - suggestion.relevance).toString(),
filterText: filterText != label
? filterText
: null, // filterText uses label if not set
insertText: insertText != label
? insertText
: null, // insertText uses label if not set
insertTextFormat: insertTextFormat != lsp.InsertTextFormat.PlainText
? insertTextFormat
: null, // Defaults to PlainText if not supplied
insertTextMode: supportsAsIsInsertMode && isMultilineCompletion
? InsertTextMode.asIs
: null,
textEdit: supportsInsertReplace && insertLength != replacementLength
? Either2<TextEdit, InsertReplaceEdit>.t2(
InsertReplaceEdit(
insert: toRange(lineInfo, replacementOffset, insertLength),
replace: toRange(lineInfo, replacementOffset, replacementLength),
newText: insertText,
),
)
: Either2<TextEdit, InsertReplaceEdit>.t1(
TextEdit(
range: toRange(lineInfo, replacementOffset, replacementLength),
newText: insertText,
),
),
);
}
lsp.Diagnostic toDiagnostic(
server.ResolvedUnitResult result,
server.AnalysisError error, {
@required HashSet<lsp.DiagnosticTag> supportedTags,
server.ErrorSeverity errorSeverity,
}) {
var errorCode = error.errorCode;
// Default to the error's severity if none is specified.
errorSeverity ??= errorCode.errorSeverity;
List<lsp.DiagnosticRelatedInformation> relatedInformation;
if (error.contextMessages.isNotEmpty) {
relatedInformation = error.contextMessages
.map((message) => toDiagnosticRelatedInformation(result, message))
.toList();
}
var message = error.message;
if (error.correctionMessage != null) {
message = '$message\n${error.correctionMessage}';
}
return lsp.Diagnostic(
range: toRange(result.lineInfo, error.offset, error.length),
severity: toDiagnosticSeverity(errorSeverity),
code: errorCode.name.toLowerCase(),
source: languageSourceName,
message: message,
tags: getDiagnosticTags(supportedTags, error),
relatedInformation: relatedInformation,
);
}
lsp.DiagnosticRelatedInformation toDiagnosticRelatedInformation(
server.ResolvedUnitResult result, analyzer.DiagnosticMessage message) {
var file = message.filePath;
var lineInfo = result.session.getFile(file).lineInfo;
return lsp.DiagnosticRelatedInformation(
location: lsp.Location(
uri: Uri.file(file).toString(),
range: toRange(
lineInfo,
message.offset,
message.length,
),
),
message: message.message);
}
lsp.DiagnosticSeverity toDiagnosticSeverity(server.ErrorSeverity severity) {
switch (severity) {
case server.ErrorSeverity.ERROR:
return lsp.DiagnosticSeverity.Error;
case server.ErrorSeverity.WARNING:
return lsp.DiagnosticSeverity.Warning;
case server.ErrorSeverity.INFO:
return lsp.DiagnosticSeverity.Information;
// Note: LSP also supports "Hint", but they won't render in things like the
// VS Code errors list as they're apparently intended to communicate
// non-visible diagnostics back (for example, if you wanted to grey out
// unreachable code without producing an item in the error list).
default:
throw 'Unknown AnalysisErrorSeverity: $severity';
}
}
lsp.Element toElement(server.LineInfo lineInfo, server.Element element) =>
lsp.Element(
range: element.location != null
? toRange(lineInfo, element.location.offset, element.location.length)
: null,
name: toElementName(element),
kind: element.kind.name,
parameters: element.parameters,
typeParameters: element.typeParameters,
returnType: element.returnType,
);
String toElementName(server.Element element) {
return element.name != null && element.name != ''
? element.name
: (element.kind == server.ElementKind.EXTENSION
? '<unnamed extension>'
: '<unnamed>');
}
lsp.FlutterOutline toFlutterOutline(
server.LineInfo lineInfo, server.FlutterOutline outline) =>
lsp.FlutterOutline(
kind: outline.kind.name,
label: outline.label,
className: outline.className,
variableName: outline.variableName,
attributes: outline.attributes != null
? outline.attributes
.map(
(attribute) => toFlutterOutlineAttribute(lineInfo, attribute))
.toList()
: null,
dartElement: outline.dartElement != null
? toElement(lineInfo, outline.dartElement)
: null,
range: toRange(lineInfo, outline.offset, outline.length),
codeRange: toRange(lineInfo, outline.codeOffset, outline.codeLength),
children: outline.children != null
? outline.children.map((c) => toFlutterOutline(lineInfo, c)).toList()
: null,
);
lsp.FlutterOutlineAttribute toFlutterOutlineAttribute(
server.LineInfo lineInfo, server.FlutterOutlineAttribute attribute) =>
lsp.FlutterOutlineAttribute(
name: attribute.name,
label: attribute.label,
valueRange: attribute.valueLocation != null
? toRange(lineInfo, attribute.valueLocation.offset,
attribute.valueLocation.length)
: null);
lsp.FoldingRange toFoldingRange(
server.LineInfo lineInfo, server.FoldingRegion region) {
final range = toRange(lineInfo, region.offset, region.length);
return lsp.FoldingRange(
startLine: range.start.line,
startCharacter: range.start.character,
endLine: range.end.line,
endCharacter: range.end.character,
kind: toFoldingRangeKind(region.kind));
}
lsp.FoldingRangeKind toFoldingRangeKind(server.FoldingKind kind) {
switch (kind) {
case server.FoldingKind.COMMENT:
case server.FoldingKind.DOCUMENTATION_COMMENT:
case server.FoldingKind.FILE_HEADER:
return lsp.FoldingRangeKind.Comment;
case server.FoldingKind.DIRECTIVES:
return lsp.FoldingRangeKind.Imports;
default:
// null (actually undefined in LSP, the toJson() takes care of that) is
// valid, and actually the value used for the majority of folds
// (class/functions/etc.).
return null;
}
}
List<lsp.DocumentHighlight> toHighlights(
server.LineInfo lineInfo, server.Occurrences occurrences) {
return occurrences.offsets
.map((offset) => lsp.DocumentHighlight(
range: toRange(lineInfo, offset, occurrences.length)))
.toList();
}
lsp.Location toLocation(server.Location location, server.LineInfo lineInfo) =>
lsp.Location(
uri: Uri.file(location.file).toString(),
range: toRange(
lineInfo,
location.offset,
location.length,
),
);
ErrorOr<int> toOffset(
server.LineInfo lineInfo,
lsp.Position pos, {
failureIsCritial = false,
}) {
// line is zero-based so cannot equal lineCount
if (pos.line >= lineInfo.lineCount) {
return ErrorOr<int>.error(lsp.ResponseError(
code: failureIsCritial
? lsp.ServerErrorCodes.ClientServerInconsistentState
: lsp.ServerErrorCodes.InvalidFileLineCol,
message: 'Invalid line number',
data: pos.line.toString()));
}
// TODO(dantup): Is there any way to validate the character? We could ensure
// it's less than the offset of the next line, but that would only work for
// all lines except the last one.
return ErrorOr<int>.success(
lineInfo.getOffsetOfLine(pos.line) + pos.character);
}
lsp.Outline toOutline(server.LineInfo lineInfo, server.Outline outline) =>
lsp.Outline(
element: toElement(lineInfo, outline.element),
range: toRange(lineInfo, outline.offset, outline.length),
codeRange: toRange(lineInfo, outline.codeOffset, outline.codeLength),
children: outline.children != null
? outline.children.map((c) => toOutline(lineInfo, c)).toList()
: null,
);
lsp.Position toPosition(server.CharacterLocation location) {
// LSP is zero-based, but analysis server is 1-based.
return lsp.Position(
line: location.lineNumber - 1, character: location.columnNumber - 1);
}
lsp.Range toRange(server.LineInfo lineInfo, int offset, int length) {
server.CharacterLocation start = lineInfo.getLocation(offset);
server.CharacterLocation end = lineInfo.getLocation(offset + length);
return lsp.Range(
start: toPosition(start),
end: toPosition(end),
);
}
lsp.SignatureHelp toSignatureHelp(List<lsp.MarkupKind> preferredFormats,
server.AnalysisGetSignatureResult signature) {
// For now, we only support returning one (though we may wish to use named
// args. etc. to provide one for each possible "next" option when the cursor
// is at the end ready to provide another argument).
/// Gets the label for an individual parameter in the form
/// String s = 'foo'
String getParamLabel(server.ParameterInfo p) {
final def = p.defaultValue != null ? ' = ${p.defaultValue}' : '';
final prefix =
p.kind == server.ParameterKind.REQUIRED_NAMED ? 'required ' : '';
return '$prefix${p.type} ${p.name}$def';
}
/// Gets the full signature label in the form
/// foo(String s, int i, bool a = true)
String getSignatureLabel(server.AnalysisGetSignatureResult resp) {
final positionalRequired = signature.parameters
.where((p) => p.kind == server.ParameterKind.REQUIRED_POSITIONAL)
.toList();
final positionalOptional = signature.parameters
.where((p) => p.kind == server.ParameterKind.OPTIONAL_POSITIONAL)
.toList();
final named = signature.parameters
.where((p) =>
p.kind == server.ParameterKind.OPTIONAL_NAMED ||
p.kind == server.ParameterKind.REQUIRED_NAMED)
.toList();
final params = [];
if (positionalRequired.isNotEmpty) {
params.add(positionalRequired.map(getParamLabel).join(', '));
}
if (positionalOptional.isNotEmpty) {
params.add('[' + positionalOptional.map(getParamLabel).join(', ') + ']');
}
if (named.isNotEmpty) {
params.add('{' + named.map(getParamLabel).join(', ') + '}');
}
return '${resp.name}(${params.join(", ")})';
}
lsp.ParameterInformation toParameterInfo(server.ParameterInfo param) {
// LSP 3.14.0 supports providing label offsets (to avoid clients having
// to guess based on substrings). We should check the
// signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport
// capability when deciding to send that.
return lsp.ParameterInformation(label: getParamLabel(param));
}
final cleanDoc = cleanDartdoc(signature.dartdoc);
return lsp.SignatureHelp(
signatures: [
lsp.SignatureInformation(
label: getSignatureLabel(signature),
documentation: asStringOrMarkupContent(preferredFormats, cleanDoc),
parameters: signature.parameters.map(toParameterInfo).toList(),
),
],
activeSignature: 0, // activeSignature
// TODO(dantup): The LSP spec says this value will default to 0 if it's
// not supplied or outside of the value range. However, setting -1 results
// in no parameters being selected in VS Code, whereas null/0 will select the first.
// We'd like for none to be selected (since we don't support this yet) so
// we send -1. I've made a request for LSP to support not selecting a parameter
// (because you could also be on param 5 of an invalid call to a function
// taking only 3 arguments) here:
// https://github.com/Microsoft/language-server-protocol/issues/456#issuecomment-452318297
activeParameter: -1, // activeParameter
);
}
ErrorOr<server.SourceRange> toSourceRange(
server.LineInfo lineInfo, Range range) {
if (range == null) {
return success(null);
}
// If there is a range, convert to offsets because that's what
// the tokens are computed using initially.
final start = toOffset(lineInfo, range.start);
final end = toOffset(lineInfo, range.end);
if (start?.isError ?? false) {
return failure(start);
}
if (end?.isError ?? false) {
return failure(end);
}
final startOffset = start?.result;
final endOffset = end?.result;
return success(server.SourceRange(startOffset, endOffset - startOffset));
}
lsp.TextDocumentEdit toTextDocumentEdit(FileEditInformation edit) {
return lsp.TextDocumentEdit(
textDocument: edit.doc,
edits: edit.edits
.map((e) => Either2<lsp.TextEdit, lsp.AnnotatedTextEdit>.t1(
toTextEdit(edit.lineInfo, e)))
.toList(),
);
}
lsp.TextEdit toTextEdit(server.LineInfo lineInfo, server.SourceEdit edit) {
return lsp.TextEdit(
range: toRange(lineInfo, edit.offset, edit.length),
newText: edit.replacement,
);
}
lsp.WorkspaceEdit toWorkspaceEdit(
lsp.ClientCapabilitiesWorkspace capabilities,
List<FileEditInformation> edits,
) {
final clientSupportsTextDocumentEdits =
capabilities?.workspaceEdit?.documentChanges == true;
if (clientSupportsTextDocumentEdits) {
final clientSupportsCreate = capabilities?.workspaceEdit?.resourceOperations
?.contains(ResourceOperationKind.Create) ??
false;
final changes = <
Either4<lsp.TextDocumentEdit, lsp.CreateFile, lsp.RenameFile,
lsp.DeleteFile>>[];
// Convert each SourceEdit to either a TextDocumentEdit or a
// CreateFile + a TextDocumentEdit depending on whether it's a new
// file.
for (final edit in edits) {
if (clientSupportsCreate && edit.newFile) {
final create = lsp.CreateFile(uri: edit.doc.uri);
final createUnion = Either4<lsp.TextDocumentEdit, lsp.CreateFile,
lsp.RenameFile, lsp.DeleteFile>.t2(create);
changes.add(createUnion);
}
final textDocEdit = toTextDocumentEdit(edit);
final textDocEditUnion = Either4<lsp.TextDocumentEdit, lsp.CreateFile,
lsp.RenameFile, lsp.DeleteFile>.t1(textDocEdit);
changes.add(textDocEditUnion);
}
return lsp.WorkspaceEdit(
documentChanges: Either2<
List<lsp.TextDocumentEdit>,
List<
Either4<lsp.TextDocumentEdit, lsp.CreateFile, lsp.RenameFile,
lsp.DeleteFile>>>.t2(changes));
} else {
return lsp.WorkspaceEdit(changes: toWorkspaceEditChanges(edits));
}
}
Map<String, List<lsp.TextEdit>> toWorkspaceEditChanges(
List<FileEditInformation> edits) {
MapEntry<String, List<lsp.TextEdit>> createEdit(FileEditInformation file) {
final edits =
file.edits.map((edit) => toTextEdit(file.lineInfo, edit)).toList();
return MapEntry(file.doc.uri, edits);
}
return Map<String, List<lsp.TextEdit>>.fromEntries(edits.map(createEdit));
}
lsp.MarkupContent _asMarkup(
List<lsp.MarkupKind> preferredFormats, String content) {
// It's not valid to call this function with a null format, as null formats
// do not support MarkupContent. [asStringOrMarkupContent] is probably the
// better choice.
assert(preferredFormats != null);
if (content == null) {
return null;
}
if (preferredFormats.isEmpty) {
preferredFormats.add(lsp.MarkupKind.Markdown);
}
final supportsMarkdown = preferredFormats.contains(lsp.MarkupKind.Markdown);
final supportsPlain = preferredFormats.contains(lsp.MarkupKind.PlainText);
// Since our PlainText version is actually just Markdown, only advertise it
// as PlainText if the client explicitly supports PlainText and not Markdown.
final format = supportsPlain && !supportsMarkdown
? lsp.MarkupKind.PlainText
: lsp.MarkupKind.Markdown;
return lsp.MarkupContent(kind: format, value: content);
}
Pair<String, lsp.InsertTextFormat> _buildInsertText({
@required bool supportsSnippets,
@required bool includeCommitCharacters,
@required bool completeFunctionCalls,
@required bool isCallable,
@required bool isInvocation,
@required String defaultArgumentListString,
@required List<int> defaultArgumentListTextRanges,
@required String completion,
@required int selectionOffset,
@required int selectionLength,
}) {
var insertText = completion;
var insertTextFormat = lsp.InsertTextFormat.PlainText;
// SuggestionBuilder already does the equiv of completeFunctionCalls for
// some methods (for example Flutter's setState). If the completion already
// includes any `(` then disable our own insertion as the special-cased code
// will likely provide better code.
if (completion.contains('(')) {
completeFunctionCalls = false;
}
// If the client supports snippets, we can support completeFunctionCalls or
// setting a selection.
if (supportsSnippets) {
// completeFunctionCalls should only work if commit characters are disabled
// otherwise the editor may insert parens that we're also inserting.
if (!includeCommitCharacters &&
completeFunctionCalls &&
isCallable &&
isInvocation) {
insertTextFormat = lsp.InsertTextFormat.Snippet;
final hasRequiredParameters =
(defaultArgumentListTextRanges?.length ?? 0) > 0;
final functionCallSuffix = hasRequiredParameters
? buildSnippetStringWithTabStops(
defaultArgumentListString,
defaultArgumentListTextRanges,
)
: '\${0:}'; // No required params still gets a tabstop in the parens.
insertText += '($functionCallSuffix)';
} else if (selectionOffset != 0 &&
// We don't need a tabstop if the selection is the end of the string.
selectionOffset != completion.length) {
insertTextFormat = lsp.InsertTextFormat.Snippet;
insertText = buildSnippetStringWithTabStops(
completion,
[selectionOffset, selectionLength],
);
}
}
return Pair(insertText, insertTextFormat);
}