Statement completion framework with a few examples
R=brianwilkerson@google.com, scheglov@google.com Review-Url: https://codereview.chromium.org/2803313002 .
This commit is contained in:
@@ -82,6 +82,7 @@ const String DISPLAY_NAME = 'displayName';
|
||||
const String EDIT_FORMAT = 'edit.format';
|
||||
const String EDIT_GET_ASSISTS = 'edit.getAssists';
|
||||
const String EDIT_GET_AVAILABLE_REFACTORINGS = 'edit.getAvailableRefactorings';
|
||||
const String EDIT_GET_STATEMENT_COMPLETION = "edit.getStatementCompletion";
|
||||
|
||||
//
|
||||
// Execution methods
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:analysis_server/src/analysis_server.dart';
|
||||
import 'package:analysis_server/src/collections.dart';
|
||||
import 'package:analysis_server/src/constants.dart';
|
||||
import 'package:analysis_server/src/protocol_server.dart' hide Element;
|
||||
import 'package:analysis_server/src/services/completion/statement/statement_completion.dart';
|
||||
import 'package:analysis_server/src/services/correction/assist.dart';
|
||||
import 'package:analysis_server/src/services/correction/assist_internal.dart';
|
||||
import 'package:analysis_server/src/services/correction/fix.dart';
|
||||
@@ -249,6 +250,38 @@ class EditDomainHandler implements RequestHandler {
|
||||
new EditGetFixesResult(errorFixesList).toResponse(request.id));
|
||||
}
|
||||
|
||||
Future getStatementCompletion(Request request) async {
|
||||
var params = new EditGetStatementCompletionParams.fromRequest(request);
|
||||
SourceChange change;
|
||||
|
||||
AnalysisResult result = await server.getAnalysisResult(params.file);
|
||||
if (result != null) {
|
||||
CompilationUnit unit = result.unit;
|
||||
CompilationUnitElement unitElement =
|
||||
resolutionMap.elementDeclaredByCompilationUnit(unit);
|
||||
if (unitElement.context != null) {
|
||||
StatementCompletionContext context = new StatementCompletionContext(
|
||||
params.file,
|
||||
result.lineInfo,
|
||||
params.offset,
|
||||
unit,
|
||||
unitElement,
|
||||
result.errors);
|
||||
StatementCompletionProcessor processor =
|
||||
new StatementCompletionProcessor(context);
|
||||
StatementCompletion completion = await processor.compute();
|
||||
change = completion.change;
|
||||
}
|
||||
}
|
||||
if (change == null) {
|
||||
change = new SourceChange("", edits: []);
|
||||
}
|
||||
|
||||
Response response = new EditGetStatementCompletionResult(change, false)
|
||||
.toResponse(request.id);
|
||||
server.sendResponse(response);
|
||||
}
|
||||
|
||||
@override
|
||||
Response handleRequest(Request request) {
|
||||
try {
|
||||
@@ -271,6 +304,9 @@ class EditDomainHandler implements RequestHandler {
|
||||
} else if (requestName == EDIT_SORT_MEMBERS) {
|
||||
sortMembers(request);
|
||||
return Response.DELAYED_RESPONSE;
|
||||
} else if (requestName == EDIT_GET_STATEMENT_COMPLETION) {
|
||||
getStatementCompletion(request);
|
||||
return Response.DELAYED_RESPONSE;
|
||||
}
|
||||
} on RequestFailure catch (exception) {
|
||||
return exception.response;
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
// Copyright (c) 2017, 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.
|
||||
|
||||
library services.src.completion.statement;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:analysis_server/plugin/protocol/protocol.dart';
|
||||
import 'package:analysis_server/src/protocol_server.dart' hide Element;
|
||||
import 'package:analysis_server/src/services/correction/source_buffer.dart';
|
||||
import 'package:analysis_server/src/services/correction/source_range.dart';
|
||||
import 'package:analysis_server/src/services/correction/util.dart';
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/ast/token.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/error/error.dart';
|
||||
import 'package:analyzer/error/error.dart' as engine;
|
||||
import 'package:analyzer/src/dart/ast/utilities.dart';
|
||||
import 'package:analyzer/src/dart/error/hint_codes.dart';
|
||||
import 'package:analyzer/src/dart/error/syntactic_errors.dart';
|
||||
import 'package:analyzer/src/generated/engine.dart';
|
||||
import 'package:analyzer/src/generated/java_core.dart';
|
||||
import 'package:analyzer/src/generated/source.dart';
|
||||
|
||||
/**
|
||||
* An enumeration of possible statement completion kinds.
|
||||
*/
|
||||
class DartStatementCompletion {
|
||||
static const NO_COMPLETION =
|
||||
const StatementCompletionKind('No_COMPLETION', 'No completion available');
|
||||
static const PLAIN_OLE_ENTER = const StatementCompletionKind(
|
||||
'PLAIN_OLE_ENTER', "Insert a newline at the end of the current line");
|
||||
static const SIMPLE_SEMICOLON = const StatementCompletionKind(
|
||||
'SIMPLE_SEMICOLON', "Add a semicolon and newline");
|
||||
static const COMPLETE_IF_STMT = const StatementCompletionKind(
|
||||
'COMPLETE_IF_STMT', "Complete if-statement");
|
||||
static const COMPLETE_WHILE_STMT = const StatementCompletionKind(
|
||||
'COMPLETE_WHILE_STMT', "Complete while-statement");
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of a statement completion.
|
||||
*
|
||||
* Clients may not extend, implement or mix-in this class.
|
||||
*/
|
||||
class StatementCompletion {
|
||||
/**
|
||||
* A description of the assist being proposed.
|
||||
*/
|
||||
final StatementCompletionKind kind;
|
||||
|
||||
/**
|
||||
* The change to be made in order to apply the assist.
|
||||
*/
|
||||
final SourceChange change;
|
||||
|
||||
/**
|
||||
* Initialize a newly created completion to have the given [kind] and [change].
|
||||
*/
|
||||
StatementCompletion(this.kind, this.change);
|
||||
}
|
||||
|
||||
/**
|
||||
* The context for computing a statement completion.
|
||||
*/
|
||||
class StatementCompletionContext {
|
||||
final String file;
|
||||
final LineInfo lineInfo;
|
||||
final int selectionOffset;
|
||||
final CompilationUnit unit;
|
||||
final CompilationUnitElement unitElement;
|
||||
final List<engine.AnalysisError> errors;
|
||||
|
||||
StatementCompletionContext(this.file, this.lineInfo, this.selectionOffset,
|
||||
this.unit, this.unitElement, this.errors) {
|
||||
if (unitElement.context == null) {
|
||||
throw new Error(); // not reached; see getStatementCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A description of a class of statement completions. Instances are intended to
|
||||
* hold the information that is common across a number of completions and to be
|
||||
* shared by those completions.
|
||||
*
|
||||
* Clients may not extend, implement or mix-in this class.
|
||||
*/
|
||||
class StatementCompletionKind {
|
||||
/**
|
||||
* The name of this kind of statement completion, used for debugging.
|
||||
*/
|
||||
final String name;
|
||||
|
||||
/**
|
||||
* A human-readable description of the changes that will be applied by this
|
||||
* kind of statement completion.
|
||||
*/
|
||||
final String message;
|
||||
|
||||
/**
|
||||
* Initialize a newly created kind of statement completion to have the given
|
||||
* [name] and [message].
|
||||
*/
|
||||
const StatementCompletionKind(this.name, this.message);
|
||||
|
||||
@override
|
||||
String toString() => name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The computer for Dart statement completions.
|
||||
*/
|
||||
class StatementCompletionProcessor {
|
||||
static final NO_COMPLETION = new StatementCompletion(
|
||||
DartStatementCompletion.NO_COMPLETION, new SourceChange("", edits: []));
|
||||
|
||||
final StatementCompletionContext statementContext;
|
||||
final AnalysisContext analysisContext;
|
||||
final CorrectionUtils utils;
|
||||
int fileStamp;
|
||||
AstNode node;
|
||||
StatementCompletion completion;
|
||||
SourceChange change = new SourceChange('statement-completion');
|
||||
List errors = <engine.AnalysisError>[];
|
||||
final Map<String, LinkedEditGroup> linkedPositionGroups =
|
||||
<String, LinkedEditGroup>{};
|
||||
Position exitPosition = null;
|
||||
|
||||
StatementCompletionProcessor(this.statementContext)
|
||||
: analysisContext = statementContext.unitElement.context,
|
||||
utils = new CorrectionUtils(statementContext.unit) {
|
||||
fileStamp = analysisContext.getModificationStamp(source);
|
||||
}
|
||||
|
||||
String get eol => utils.endOfLine;
|
||||
|
||||
String get file => statementContext.file;
|
||||
|
||||
LineInfo get lineInfo => statementContext.lineInfo;
|
||||
|
||||
int get requestLine => lineInfo.getLocation(selectionOffset).lineNumber;
|
||||
|
||||
int get selectionOffset => statementContext.selectionOffset;
|
||||
|
||||
Source get source => statementContext.unitElement.source;
|
||||
|
||||
CompilationUnit get unit => statementContext.unit;
|
||||
|
||||
CompilationUnitElement get unitElement => statementContext.unitElement;
|
||||
|
||||
Future<StatementCompletion> compute() async {
|
||||
// If the source was changed between the constructor and running
|
||||
// this asynchronous method, it is not safe to use the unit.
|
||||
if (analysisContext.getModificationStamp(source) != fileStamp) {
|
||||
return NO_COMPLETION;
|
||||
}
|
||||
node = new NodeLocator(selectionOffset).searchWithin(unit);
|
||||
if (node == null) {
|
||||
return NO_COMPLETION;
|
||||
}
|
||||
// TODO(messick): This needs to work for declarations.
|
||||
node = node.getAncestor((n) => n is Statement);
|
||||
for (engine.AnalysisError error in statementContext.errors) {
|
||||
if (error.offset >= node.offset &&
|
||||
error.offset <= node.offset + node.length) {
|
||||
if (error.errorCode is! HintCode) {
|
||||
errors.add(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_complete_ifStatement() ||
|
||||
_complete_whileStatement() ||
|
||||
_complete_simpleSemicolon() ||
|
||||
_complete_plainOleEnter()) {
|
||||
return completion;
|
||||
}
|
||||
return NO_COMPLETION;
|
||||
}
|
||||
|
||||
void _addIndentEdit(SourceRange range, String oldIndent, String newIndent) {
|
||||
SourceEdit edit = utils.createIndentEdit(range, oldIndent, newIndent);
|
||||
doSourceChange_addElementEdit(change, unitElement, edit);
|
||||
}
|
||||
|
||||
void _addInsertEdit(int offset, String text) {
|
||||
SourceEdit edit = new SourceEdit(offset, 0, text);
|
||||
doSourceChange_addElementEdit(change, unitElement, edit);
|
||||
}
|
||||
|
||||
void _addReplaceEdit(SourceRange range, String text) {
|
||||
SourceEdit edit = new SourceEdit(range.offset, range.length, text);
|
||||
doSourceChange_addElementEdit(change, unitElement, edit);
|
||||
}
|
||||
|
||||
void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) {
|
||||
sb.append(' {');
|
||||
sb.append(eol);
|
||||
String indent = utils.getLinePrefix(selectionOffset);
|
||||
sb.append(indent);
|
||||
sb.append(utils.getIndent(1));
|
||||
if (needsExitMark) {
|
||||
sb.setExitOffset();
|
||||
}
|
||||
sb.append(eol);
|
||||
sb.append(indent);
|
||||
sb.append('}');
|
||||
}
|
||||
|
||||
int _appendNewlinePlusIndent() {
|
||||
// Append a newline plus proper indent and another newline.
|
||||
// Return the position before the second newline.
|
||||
String indent = utils.getLinePrefix(selectionOffset);
|
||||
int loc = utils.getLineNext(selectionOffset);
|
||||
_addInsertEdit(loc, indent + eol);
|
||||
return loc + indent.length;
|
||||
}
|
||||
|
||||
bool _complete_ifOrWhileStatement(
|
||||
_IfWhileStructure statement, StatementCompletionKind kind) {
|
||||
String text = utils.getNodeText(node);
|
||||
if (text.endsWith(eol)) {
|
||||
text = text.substring(0, text.length - eol.length);
|
||||
}
|
||||
SourceBuilder sb;
|
||||
bool needsExit = false;
|
||||
if (statement.leftParenthesis.lexeme.isEmpty) {
|
||||
if (!statement.rightParenthesis.lexeme.isEmpty) {
|
||||
// Quite unlikely to see this so don't try to fix it.
|
||||
return false;
|
||||
}
|
||||
int len = statement.keyword.length;
|
||||
if (text.length == len ||
|
||||
!text.substring(len, len + 1).contains(new RegExp(r'\s'))) {
|
||||
sb = new SourceBuilder(file, statement.offset + len);
|
||||
sb.append(' ');
|
||||
} else {
|
||||
sb = new SourceBuilder(file, statement.offset + len + 1);
|
||||
}
|
||||
sb.append('(');
|
||||
sb.setExitOffset();
|
||||
sb.append(')');
|
||||
} else {
|
||||
if (_isEmptyExpression(statement.condition)) {
|
||||
exitPosition = _newPosition(statement.leftParenthesis.offset + 1);
|
||||
sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
|
||||
} else {
|
||||
sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
|
||||
needsExit = true;
|
||||
}
|
||||
}
|
||||
if (statement.block is EmptyStatement) {
|
||||
_appendEmptyBraces(sb, needsExit);
|
||||
}
|
||||
_insertBuilder(sb);
|
||||
_setCompletion(kind);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _complete_ifStatement() {
|
||||
if (errors.isEmpty || node is! IfStatement) {
|
||||
return false;
|
||||
}
|
||||
IfStatement ifNode = node;
|
||||
if (ifNode != null) {
|
||||
if (ifNode.elseKeyword != null) {
|
||||
return false;
|
||||
}
|
||||
var stmt = new _IfWhileStructure(ifNode.ifKeyword, ifNode.leftParenthesis,
|
||||
ifNode.condition, ifNode.rightParenthesis, ifNode.thenStatement);
|
||||
return _complete_ifOrWhileStatement(
|
||||
stmt, DartStatementCompletion.COMPLETE_IF_STMT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _complete_plainOleEnter() {
|
||||
int offset;
|
||||
if (!errors.isEmpty) {
|
||||
offset = selectionOffset;
|
||||
} else {
|
||||
String indent = utils.getLinePrefix(selectionOffset);
|
||||
int loc = utils.getLineNext(selectionOffset);
|
||||
_addInsertEdit(loc, indent + eol);
|
||||
offset = loc + indent.length + eol.length;
|
||||
}
|
||||
_setCompletionAt(DartStatementCompletion.PLAIN_OLE_ENTER, offset);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool _complete_simpleSemicolon() {
|
||||
if (errors.length != 1) {
|
||||
return false;
|
||||
}
|
||||
var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
|
||||
if (error != null) {
|
||||
int insertOffset = error.offset + error.length;
|
||||
_addInsertEdit(insertOffset, ';');
|
||||
int offset = _appendNewlinePlusIndent() + 1 /* ';' */;
|
||||
_setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _complete_whileStatement() {
|
||||
if (errors.isEmpty || node is! WhileStatement) {
|
||||
return false;
|
||||
}
|
||||
WhileStatement whileNode = node;
|
||||
if (whileNode != null) {
|
||||
var stmt = new _IfWhileStructure(
|
||||
whileNode.whileKeyword,
|
||||
whileNode.leftParenthesis,
|
||||
whileNode.condition,
|
||||
whileNode.rightParenthesis,
|
||||
whileNode.body);
|
||||
return _complete_ifOrWhileStatement(
|
||||
stmt, DartStatementCompletion.COMPLETE_WHILE_STMT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
engine.AnalysisError _findError(ErrorCode code, {partialMatch: null}) {
|
||||
var error =
|
||||
errors.firstWhere((err) => err.errorCode == code, orElse: () => null);
|
||||
if (error != null) {
|
||||
if (partialMatch != null) {
|
||||
return error.message.contains(partialMatch) ? error : null;
|
||||
}
|
||||
return error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LinkedEditGroup _getLinkedPosition(String groupId) {
|
||||
LinkedEditGroup group = linkedPositionGroups[groupId];
|
||||
if (group == null) {
|
||||
group = new LinkedEditGroup.empty();
|
||||
linkedPositionGroups[groupId] = group;
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
void _insertBuilder(SourceBuilder builder, [int length = 0]) {
|
||||
{
|
||||
SourceRange range = rangeStartLength(builder.offset, length);
|
||||
String text = builder.toString();
|
||||
_addReplaceEdit(range, text);
|
||||
}
|
||||
// add linked positions
|
||||
builder.linkedPositionGroups.forEach((String id, LinkedEditGroup group) {
|
||||
LinkedEditGroup fixGroup = _getLinkedPosition(id);
|
||||
group.positions.forEach((Position position) {
|
||||
fixGroup.addPosition(position, group.length);
|
||||
});
|
||||
group.suggestions.forEach((LinkedEditSuggestion suggestion) {
|
||||
fixGroup.addSuggestion(suggestion);
|
||||
});
|
||||
});
|
||||
// add exit position
|
||||
{
|
||||
int exitOffset = builder.exitOffset;
|
||||
if (exitOffset != null) {
|
||||
exitPosition = _newPosition(exitOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isEmptyExpression(Expression expr) {
|
||||
if (expr is! SimpleIdentifier) {
|
||||
return false;
|
||||
}
|
||||
SimpleIdentifier id = expr as SimpleIdentifier;
|
||||
return id.length == 0;
|
||||
}
|
||||
|
||||
Position _newPosition(int offset) {
|
||||
return new Position(file, offset);
|
||||
}
|
||||
|
||||
void _setCompletion(StatementCompletionKind kind, [List args]) {
|
||||
assert(exitPosition != null);
|
||||
change.selection = exitPosition;
|
||||
change.message = formatList(kind.message, args);
|
||||
linkedPositionGroups.values
|
||||
.forEach((group) => change.addLinkedEditGroup(group));
|
||||
completion = new StatementCompletion(kind, change);
|
||||
}
|
||||
|
||||
void _setCompletionAt(StatementCompletionKind kind, int offset, [List args]) {
|
||||
exitPosition = _newPosition(offset);
|
||||
_setCompletion(kind, args);
|
||||
}
|
||||
}
|
||||
|
||||
// Encapsulate common structure of if-statement and while-statement.
|
||||
class _IfWhileStructure {
|
||||
final Token keyword;
|
||||
final Token leftParenthesis, rightParenthesis;
|
||||
final Expression condition;
|
||||
final Statement block;
|
||||
|
||||
_IfWhileStructure(this.keyword, this.leftParenthesis, this.condition,
|
||||
this.rightParenthesis, this.block);
|
||||
|
||||
int get offset => keyword.offset;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2017, 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.
|
||||
|
||||
library test.edit.statement_completion;
|
||||
|
||||
import 'package:analysis_server/plugin/protocol/protocol.dart';
|
||||
import 'package:analysis_server/src/edit/edit_domain.dart';
|
||||
import 'package:plugin/manager.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import '../analysis_abstract.dart';
|
||||
|
||||
main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(StatementCompletionTest);
|
||||
});
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class StatementCompletionTest extends AbstractAnalysisTest {
|
||||
SourceChange change;
|
||||
|
||||
@override
|
||||
void setUp() {
|
||||
enableNewAnalysisDriver = true;
|
||||
super.setUp();
|
||||
createProject();
|
||||
ExtensionManager manager = new ExtensionManager();
|
||||
manager.processPlugins([server.serverPlugin]);
|
||||
handler = new EditDomainHandler(server);
|
||||
}
|
||||
|
||||
test_plainEnterFromStart() async {
|
||||
addTestFile('''
|
||||
main() {
|
||||
int v = 1;
|
||||
}
|
||||
''');
|
||||
await waitForTasksFinished();
|
||||
await _prepareCompletion('v = 1;', atStart: true);
|
||||
_assertHasChange(
|
||||
'Insert a newline at the end of the current line',
|
||||
'''
|
||||
main() {
|
||||
int v = 1;
|
||||
/*caret*/
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
test_plainOleEnter() async {
|
||||
addTestFile('''
|
||||
main() {
|
||||
int v = 1;
|
||||
}
|
||||
''');
|
||||
await waitForTasksFinished();
|
||||
await _prepareCompletion('v = 1;', atEnd: true);
|
||||
_assertHasChange(
|
||||
'Insert a newline at the end of the current line',
|
||||
'''
|
||||
main() {
|
||||
int v = 1;
|
||||
/*caret*/
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
test_plainOleEnterWithError() async {
|
||||
addTestFile('''
|
||||
main() {
|
||||
int v =
|
||||
}
|
||||
''');
|
||||
await waitForTasksFinished();
|
||||
String match = 'v =';
|
||||
await _prepareCompletion(match, atEnd: true);
|
||||
_assertHasChange(
|
||||
'Insert a newline at the end of the current line',
|
||||
'''
|
||||
main() {
|
||||
int v =
|
||||
x
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf(match) + match.length); // Ensure cursor after '='.
|
||||
}
|
||||
|
||||
void _assertHasChange(String message, String expectedCode, [Function cmp]) {
|
||||
if (change.message == message) {
|
||||
if (!change.edits.isEmpty) {
|
||||
String resultCode =
|
||||
SourceEdit.applySequence(testCode, change.edits[0].edits);
|
||||
expect(resultCode, expectedCode.replaceAll('/*caret*/', ''));
|
||||
if (cmp != null) {
|
||||
int offset = cmp(resultCode);
|
||||
expect(change.selection.offset, offset);
|
||||
}
|
||||
} else {
|
||||
if (cmp != null) {
|
||||
int offset = cmp(testCode);
|
||||
expect(change.selection.offset, offset);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
fail("Expected to find |$message| but got: " + change.message);
|
||||
}
|
||||
|
||||
_prepareCompletion(String search,
|
||||
{bool atStart: false, bool atEnd: false, int delta: 0}) async {
|
||||
int offset = findOffset(search);
|
||||
if (atStart) {
|
||||
delta = 0;
|
||||
} else if (atEnd) {
|
||||
delta = search.length;
|
||||
}
|
||||
await _prepareCompletionAt(offset + delta);
|
||||
}
|
||||
|
||||
_prepareCompletionAt(int offset) async {
|
||||
Request request =
|
||||
new EditGetStatementCompletionParams(testFile, offset).toRequest('0');
|
||||
Response response = await waitResponse(request);
|
||||
var result = new EditGetStatementCompletionResult.fromResponse(response);
|
||||
change = result.change;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import 'format_test.dart' as format_test;
|
||||
import 'organize_directives_test.dart' as organize_directives_test;
|
||||
import 'refactoring_test.dart' as refactoring_test;
|
||||
import 'sort_members_test.dart' as sort_members_test;
|
||||
import 'statement_completion_test.dart' as statement_completion_test;
|
||||
|
||||
/**
|
||||
* Utility for manually running all tests.
|
||||
@@ -24,5 +25,6 @@ main() {
|
||||
organize_directives_test.main();
|
||||
refactoring_test.main();
|
||||
sort_members_test.main();
|
||||
statement_completion_test.main();
|
||||
}, name: 'edit');
|
||||
}
|
||||
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
// Copyright (c) 2017, 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.
|
||||
|
||||
library test.services.completion.statement;
|
||||
|
||||
import 'package:analysis_server/src/protocol_server.dart';
|
||||
import 'package:analysis_server/src/services/completion/statement/statement_completion.dart';
|
||||
import 'package:analyzer/src/dart/analysis/driver.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import '../../../abstract_single_unit.dart';
|
||||
|
||||
main() {
|
||||
defineReflectiveSuite(() {
|
||||
defineReflectiveTests(StatementCompletionTest);
|
||||
});
|
||||
}
|
||||
|
||||
@reflectiveTest
|
||||
class StatementCompletionTest extends AbstractSingleUnitTest {
|
||||
SourceChange change;
|
||||
|
||||
bool get enableNewAnalysisDriver => true;
|
||||
|
||||
test_completeIfEmptyCondition() async {
|
||||
await _prepareCompletion(
|
||||
'if ()',
|
||||
'''
|
||||
main() {
|
||||
if ()
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Complete if-statement',
|
||||
'''
|
||||
main() {
|
||||
if () {
|
||||
////
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf('if (') + 'if ('.length);
|
||||
}
|
||||
|
||||
test_completeIfKeywordOnly() async {
|
||||
await _prepareCompletion(
|
||||
'if',
|
||||
'''
|
||||
main() {
|
||||
if ////
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Complete if-statement',
|
||||
'''
|
||||
main() {
|
||||
if () {
|
||||
////
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf('if (') + 'if ('.length);
|
||||
}
|
||||
|
||||
test_completeIfWithCondition() async {
|
||||
await _prepareCompletion(
|
||||
'if (tr', // Trigger completion from within expression.
|
||||
'''
|
||||
main() {
|
||||
if (true)
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Complete if-statement',
|
||||
'''
|
||||
main() {
|
||||
if (true) {
|
||||
////
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf(' ') + ' '.length);
|
||||
}
|
||||
|
||||
test_completeIfAfterCondition_BAD() async {
|
||||
// TODO(messick): Fix the code to make this like test_completeIfWithCondition.
|
||||
// Recap: Finding the node at the selectionOffset returns the block, not the
|
||||
// if-statement. Need to understand if that only happens when the if-statement
|
||||
// is the only statement in the block, or perhaps first or last? And what
|
||||
// happens when it is in the middle of other statements?
|
||||
await _prepareCompletion(
|
||||
'if (true) ', // Trigger completion after space.
|
||||
'''
|
||||
main() {
|
||||
if (true) ////
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
// Note: This is not what we want.
|
||||
'Insert a newline at the end of the current line',
|
||||
'''
|
||||
main() {
|
||||
if (true) ////
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf('if (true) ') + 'if (true) '.length);
|
||||
}
|
||||
|
||||
test_completeIfWithElse_BAD() async {
|
||||
await _prepareCompletion(
|
||||
'if ()',
|
||||
'''
|
||||
main() {
|
||||
if ()
|
||||
else
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
// Note: if-statement completion should not trigger.
|
||||
'Insert a newline at the end of the current line',
|
||||
'''
|
||||
main() {
|
||||
if ()
|
||||
else
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf('if ()') + 'if ()'.length);
|
||||
}
|
||||
|
||||
test_completeIfWithinEmptyCondition() async {
|
||||
await _prepareCompletion(
|
||||
'if (',
|
||||
'''
|
||||
main() {
|
||||
if ()
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Complete if-statement',
|
||||
'''
|
||||
main() {
|
||||
if () {
|
||||
////
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf('if (') + 'if ('.length);
|
||||
}
|
||||
|
||||
test_completeWhileKeywordOnly() async {
|
||||
await _prepareCompletion(
|
||||
'while',
|
||||
'''
|
||||
main() {
|
||||
while ////
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Complete while-statement',
|
||||
'''
|
||||
main() {
|
||||
while () {
|
||||
////
|
||||
}
|
||||
}
|
||||
''',
|
||||
(s) => s.indexOf('while (') + 'while ('.length);
|
||||
}
|
||||
|
||||
test_simpleEnter() async {
|
||||
await _prepareCompletion(
|
||||
'v = 1;',
|
||||
'''
|
||||
main() {
|
||||
int v = 1;
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Insert a newline at the end of the current line',
|
||||
'''
|
||||
main() {
|
||||
int v = 1;
|
||||
////
|
||||
}
|
||||
''');
|
||||
}
|
||||
|
||||
test_simpleSemicolon() async {
|
||||
await _prepareCompletion(
|
||||
'v = 1',
|
||||
'''
|
||||
main() {
|
||||
int v = 1
|
||||
}
|
||||
''',
|
||||
atEnd: true);
|
||||
_assertHasChange(
|
||||
'Add a semicolon and newline',
|
||||
'''
|
||||
main() {
|
||||
int v = 1;
|
||||
////
|
||||
}
|
||||
''',
|
||||
(s) => s.lastIndexOf(' ') + ' '.length);
|
||||
}
|
||||
|
||||
void _assertHasChange(String message, String expectedCode, [Function cmp]) {
|
||||
if (change.message == message) {
|
||||
if (!change.edits.isEmpty) {
|
||||
String resultCode =
|
||||
SourceEdit.applySequence(testCode, change.edits[0].edits);
|
||||
expect(resultCode, expectedCode.replaceAll('////', ''));
|
||||
if (cmp != null) {
|
||||
int offset = cmp(resultCode);
|
||||
expect(change.selection.offset, offset);
|
||||
}
|
||||
} else {
|
||||
if (cmp != null) {
|
||||
int offset = cmp(testCode);
|
||||
expect(change.selection.offset, offset);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
fail("Expected to find |$message| but got: " + change.message);
|
||||
}
|
||||
|
||||
_computeCompletion(int offset) async {
|
||||
driver.changeFile(testFile);
|
||||
AnalysisResult result = await driver.getResult(testFile);
|
||||
StatementCompletionContext context = new StatementCompletionContext(
|
||||
testFile,
|
||||
result.lineInfo,
|
||||
offset,
|
||||
testUnit,
|
||||
testUnitElement,
|
||||
result.errors);
|
||||
StatementCompletionProcessor processor =
|
||||
new StatementCompletionProcessor(context);
|
||||
StatementCompletion completion = await processor.compute();
|
||||
change = completion.change;
|
||||
}
|
||||
|
||||
_prepareCompletion(String search, String sourceCode,
|
||||
{bool atStart: false, bool atEnd: false, int delta: 0}) async {
|
||||
testCode = sourceCode.replaceAll('////', '');
|
||||
int offset = findOffset(search);
|
||||
if (atStart) {
|
||||
delta = 0;
|
||||
} else if (atEnd) {
|
||||
delta = search.length;
|
||||
}
|
||||
await _prepareCompletionAt(offset + delta, testCode);
|
||||
}
|
||||
|
||||
_prepareCompletionAt(int offset, String sourceCode) async {
|
||||
verifyNoTestUnitErrors = false;
|
||||
await resolveTestUnit(sourceCode);
|
||||
await _computeCompletion(offset);
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ import 'package:test_reflective_loader/test_reflective_loader.dart';
|
||||
|
||||
import 'completion_target_test.dart' as completion_target_test;
|
||||
import 'dart/test_all.dart' as dart_contributor_tests;
|
||||
import 'statement/statement_completion_test.dart' as statement_completion_test;
|
||||
|
||||
/// Utility for manually running all tests.
|
||||
main() {
|
||||
defineReflectiveSuite(() {
|
||||
completion_target_test.main();
|
||||
dart_contributor_tests.main();
|
||||
statement_completion_test.main();
|
||||
}, name: 'completion');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user