blob: cb8c9231afb9142a4ae412c298145cb1ce5f97be [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 'package:analysis_server/src/protocol_server.dart';
import 'package:analysis_server/src/services/correction/util.dart';
import 'package:analysis_server/src/provisional/completion/dart/completion_dart.dart';
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:front_end/src/fasta/scanner.dart';
/// Constructs a [CompletionSuggestion] object.
CompletionSuggestion createCompletionSuggestion(
String completion, FeatureSet featureSet, int relevance) {
final tokens = TokenUtils.getTokens(completion, featureSet);
final token = tokens.isNotEmpty ? tokens[0] : null;
final completionKind = token != null && token.isKeyword
? CompletionSuggestionKind.KEYWORD
: CompletionSuggestionKind.IDENTIFIER;
if (isLiteral(completion) &&
(completion.startsWith('"package:') ||
completion.startsWith('\'package:'))) {
completion = completion.replaceAll('\'', '').replaceAll('\"', '');
}
return CompletionSuggestion(completionKind, relevance, completion,
completion.length, 0, false, false);
}
/// Finds the token at which completion was requested.
Token getCursorToken(DartCompletionRequest request) {
final entity = request.target.entity;
if (entity is AstNode) {
return entity.endToken;
}
return entity is Token ? entity : null;
}
/// Fuzzy matching between static analysis and model-predicted lexemes
/// that considers pairs like "main" and "main()" to be equal.
bool areCompletionsEquivalent(String dasCompletion, String modelCompletion) {
if (dasCompletion == null || modelCompletion == null) {
return false;
}
if (dasCompletion == modelCompletion) {
return true;
}
final index = dasCompletion.indexOf(new RegExp(r'[^0-9A-Za-z_]'));
// Disallow '.' since this gives all methods under a model-predicted token
// the same probability.
if (index == -1 || dasCompletion[index] == '.') {
return false;
}
// Checks that the strings are equal until the first non-alphanumeric token.
return dasCompletion.substring(0, index) == modelCompletion;
}
/// Step to previous token until we are at the first token before where the
/// cursor is located.
bool isStopToken(Token token, int cursorOffset) {
if (token == null) {
return true;
}
if (token.isSynthetic && !token.isEof) {
return false;
}
final position = token.offset + token.length;
if (position == cursorOffset) {
// If we are at the end of some token, step to previous only if the
// token is NOT an identifier, keyword, or literal. The rationale is that
// we want to keep moving if we have a situation like
// FooBar foo^ since foo is not a previous token to pass to the model.
return token.lexeme[token.lexeme.length - 1]
.contains(new RegExp(r'[0-9A-Za-z_]'));
}
// Stop if the token's location is strictly before the cursor, continue
// if the token's location is strictly after.
return position < cursorOffset;
}
bool isStringLiteral(String lexeme) {
if (lexeme == null || lexeme.isEmpty) {
return false;
}
return (lexeme.length > 1 && lexeme[0] == "'") ||
(lexeme.length > 2 && lexeme[0] == 'r' && lexeme[1] == "'");
}
bool isLiteral(String lexeme) {
if (lexeme == null || lexeme.isEmpty) {
return false;
}
if (new RegExp(r'^[0-9]+$').hasMatch(lexeme)) {
// Check for number lexeme.
return true;
}
return isStringLiteral(lexeme);
}
bool isNotWhitespace(String lexeme) {
return lexeme
.replaceAll("'", '')
.split('')
.any((String chr) => !new RegExp(r'\s').hasMatch(chr));
}
Token getCurrentToken(DartCompletionRequest request) {
var token = getCursorToken(request);
while (!isStopToken(token.previous, request.offset)) {
token = token.previous;
}
return token;
}
bool testInsideQuotes(DartCompletionRequest request) {
final token = getCurrentToken(request);
if (token.isSynthetic) {
return false;
}
final cursorOffset = request.offset;
if (cursorOffset == token.offset ||
cursorOffset == token.offset + token.length) {
// We are not inside the current token, quoted or otherwise.
return false;
}
final lexeme = token.lexeme;
if (lexeme.length > 2 && lexeme[0] == 'r') {
return lexeme[1] == "'" || lexeme[1] == '"';
}
return lexeme.length > 1 && (lexeme[0] == "'" || lexeme[0] == '"');
}
bool isTokenDot(Token token) {
return !token.isSynthetic && token.lexeme.endsWith('.');
}
bool testFollowingDot(DartCompletionRequest request) {
final token = getCurrentToken(request);
return isTokenDot(token) || isTokenDot(token.previous);
}
/// Finds the previous n tokens occurring before the cursor.
List<String> constructQuery(DartCompletionRequest request, int n) {
var token = getCursorToken(request);
if (request.offset == null) {
return null;
}
while (!isStopToken(token, request.offset)) {
token = token.previous;
}
if (token?.offset == null || token?.type == null) {
return null;
}
final result = List<String>();
for (var size = 0; size < n && !token.isEof; token = token.previous) {
if (!token.isSynthetic && token is! ErrorToken) {
result.add(token.lexeme);
size += 1;
}
}
return result.reversed.toList(growable: false);
}
/// Maps included relevance tags formatted as
/// '${element.librarySource.uri}::${element.name}' to element.name.
String elementNameFromRelevanceTag(String tag) {
final index = tag.lastIndexOf('::');
if (index == -1) {
return null;
}
return tag.substring(index + 2);
}