diff --git a/pkg/analysis_server/lib/src/constants.dart b/pkg/analysis_server/lib/src/constants.dart index 519bbfe2291..094c59f3fda 100644 --- a/pkg/analysis_server/lib/src/constants.dart +++ b/pkg/analysis_server/lib/src/constants.dart @@ -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 diff --git a/pkg/analysis_server/lib/src/edit/edit_domain.dart b/pkg/analysis_server/lib/src/edit/edit_domain.dart index 9c66d24b8a0..2d94108ee0c 100644 --- a/pkg/analysis_server/lib/src/edit/edit_domain.dart +++ b/pkg/analysis_server/lib/src/edit/edit_domain.dart @@ -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; diff --git a/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart b/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart new file mode 100644 index 00000000000..73fe15ed155 --- /dev/null +++ b/pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart @@ -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 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 = []; + final Map linkedPositionGroups = + {}; + 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 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; +} diff --git a/pkg/analysis_server/test/edit/statement_completion_test.dart b/pkg/analysis_server/test/edit/statement_completion_test.dart new file mode 100644 index 00000000000..943c7e2bc09 --- /dev/null +++ b/pkg/analysis_server/test/edit/statement_completion_test.dart @@ -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; + } +} diff --git a/pkg/analysis_server/test/edit/test_all.dart b/pkg/analysis_server/test/edit/test_all.dart index 16a89edaa38..8dd3e74091a 100644 --- a/pkg/analysis_server/test/edit/test_all.dart +++ b/pkg/analysis_server/test/edit/test_all.dart @@ -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'); } diff --git a/pkg/analysis_server/test/services/completion/statement/statement_completion_test.dart b/pkg/analysis_server/test/services/completion/statement/statement_completion_test.dart new file mode 100644 index 00000000000..18b8794e4a1 --- /dev/null +++ b/pkg/analysis_server/test/services/completion/statement/statement_completion_test.dart @@ -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); + } +} diff --git a/pkg/analysis_server/test/services/completion/test_all.dart b/pkg/analysis_server/test/services/completion/test_all.dart index fed3fef23be..ab8cb57ef76 100644 --- a/pkg/analysis_server/test/services/completion/test_all.dart +++ b/pkg/analysis_server/test/services/completion/test_all.dart @@ -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'); }