Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(116)

Side by Side Diff: pkg/analysis_server/lib/src/services/completion/statement/statement_completion.dart

Issue 2803313002: Statement completion framework with a few examples (Closed)
Patch Set: Address review comments Created 3 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file.
4
5 library services.src.completion.statement;
6
7 import 'dart:async';
8
9 import 'package:analysis_server/plugin/protocol/protocol.dart';
10 import 'package:analysis_server/src/protocol_server.dart' hide Element;
11 import 'package:analysis_server/src/services/correction/source_buffer.dart';
12 import 'package:analysis_server/src/services/correction/source_range.dart';
13 import 'package:analysis_server/src/services/correction/util.dart';
14 import 'package:analyzer/dart/ast/ast.dart';
15 import 'package:analyzer/dart/ast/token.dart';
16 import 'package:analyzer/dart/element/element.dart';
17 import 'package:analyzer/error/error.dart';
18 import 'package:analyzer/error/error.dart' as engine;
19 import 'package:analyzer/src/dart/ast/utilities.dart';
20 import 'package:analyzer/src/dart/error/hint_codes.dart';
21 import 'package:analyzer/src/dart/error/syntactic_errors.dart';
22 import 'package:analyzer/src/generated/engine.dart';
23 import 'package:analyzer/src/generated/java_core.dart';
24 import 'package:analyzer/src/generated/source.dart';
25
26 /**
27 * An enumeration of possible statement completion kinds.
28 */
29 class DartStatementCompletion {
30 static const NO_COMPLETION =
31 const StatementCompletionKind('No_COMPLETION', 'No completion available');
32 static const PLAIN_OLE_ENTER = const StatementCompletionKind(
33 'PLAIN_OLE_ENTER', "Insert a newline at the end of the current line");
34 static const SIMPLE_SEMICOLON = const StatementCompletionKind(
35 'SIMPLE_SEMICOLON', "Add a semicolon and newline");
36 static const COMPLETE_IF_STMT = const StatementCompletionKind(
37 'COMPLETE_IF_STMT', "Complete if-statement");
38 static const COMPLETE_WHILE_STMT = const StatementCompletionKind(
39 'COMPLETE_WHILE_STMT', "Complete while-statement");
40 }
41
42 /**
43 * A description of a statement completion.
44 *
45 * Clients may not extend, implement or mix-in this class.
46 */
47 class StatementCompletion {
48 /**
49 * A description of the assist being proposed.
50 */
51 final StatementCompletionKind kind;
52
53 /**
54 * The change to be made in order to apply the assist.
55 */
56 final SourceChange change;
57
58 /**
59 * Initialize a newly created completion to have the given [kind] and [change] .
60 */
61 StatementCompletion(this.kind, this.change);
62 }
63
64 /**
65 * The context for computing a statement completion.
66 */
67 class StatementCompletionContext {
68 final String file;
69 final LineInfo lineInfo;
70 final int selectionOffset;
71 final CompilationUnit unit;
72 final CompilationUnitElement unitElement;
73 final List<engine.AnalysisError> errors;
74
75 StatementCompletionContext(this.file, this.lineInfo, this.selectionOffset,
76 this.unit, this.unitElement, this.errors) {
77 if (unitElement.context == null) {
78 throw new Error(); // not reached; see getStatementCompletion()
79 }
80 }
81 }
82
83 /**
84 * A description of a class of statement completions. Instances are intended to
85 * hold the information that is common across a number of completions and to be
86 * shared by those completions.
87 *
88 * Clients may not extend, implement or mix-in this class.
89 */
90 class StatementCompletionKind {
91 /**
92 * The name of this kind of statement completion, used for debugging.
93 */
94 final String name;
95
96 /**
97 * A human-readable description of the changes that will be applied by this
98 * kind of statement completion.
99 */
100 final String message;
101
102 /**
103 * Initialize a newly created kind of statement completion to have the given
104 * [name] and [message].
105 */
106 const StatementCompletionKind(this.name, this.message);
107
108 @override
109 String toString() => name;
110 }
111
112 /**
113 * The computer for Dart statement completions.
114 */
115 class StatementCompletionProcessor {
116 static final NO_COMPLETION = new StatementCompletion(
117 DartStatementCompletion.NO_COMPLETION, new SourceChange("", edits: []));
118
119 final StatementCompletionContext statementContext;
120 final AnalysisContext analysisContext;
121 final CorrectionUtils utils;
122 int fileStamp;
123 AstNode node;
124 StatementCompletion completion;
125 SourceChange change = new SourceChange('statement-completion');
126 List errors = <engine.AnalysisError>[];
127 final Map<String, LinkedEditGroup> linkedPositionGroups =
128 <String, LinkedEditGroup>{};
129 Position exitPosition = null;
130
131 StatementCompletionProcessor(this.statementContext)
132 : analysisContext = statementContext.unitElement.context,
133 utils = new CorrectionUtils(statementContext.unit) {
134 fileStamp = analysisContext.getModificationStamp(source);
135 }
136
137 String get eol => utils.endOfLine;
138
139 String get file => statementContext.file;
140
141 LineInfo get lineInfo => statementContext.lineInfo;
142
143 int get requestLine => lineInfo.getLocation(selectionOffset).lineNumber;
144
145 int get selectionOffset => statementContext.selectionOffset;
146
147 Source get source => statementContext.unitElement.source;
148
149 CompilationUnit get unit => statementContext.unit;
150
151 CompilationUnitElement get unitElement => statementContext.unitElement;
152
153 Future<StatementCompletion> compute() async {
154 // If the source was changed between the constructor and running
155 // this asynchronous method, it is not safe to use the unit.
156 if (analysisContext.getModificationStamp(source) != fileStamp) {
157 return NO_COMPLETION;
158 }
159 node = new NodeLocator(selectionOffset).searchWithin(unit);
160 if (node == null) {
161 return NO_COMPLETION;
162 }
163 // TODO(messick): This needs to work for declarations.
164 node = node.getAncestor((n) => n is Statement);
165 for (engine.AnalysisError error in statementContext.errors) {
166 if (error.offset >= node.offset &&
167 error.offset <= node.offset + node.length) {
168 if (error.errorCode is! HintCode) {
169 errors.add(error);
170 }
171 }
172 }
173
174 if (_complete_ifStatement() ||
175 _complete_whileStatement() ||
176 _complete_simpleSemicolon() ||
177 _complete_plainOleEnter()) {
178 return completion;
179 }
180 return NO_COMPLETION;
181 }
182
183 void _addIndentEdit(SourceRange range, String oldIndent, String newIndent) {
184 SourceEdit edit = utils.createIndentEdit(range, oldIndent, newIndent);
185 doSourceChange_addElementEdit(change, unitElement, edit);
186 }
187
188 void _addInsertEdit(int offset, String text) {
189 SourceEdit edit = new SourceEdit(offset, 0, text);
190 doSourceChange_addElementEdit(change, unitElement, edit);
191 }
192
193 void _addReplaceEdit(SourceRange range, String text) {
194 SourceEdit edit = new SourceEdit(range.offset, range.length, text);
195 doSourceChange_addElementEdit(change, unitElement, edit);
196 }
197
198 void _appendEmptyBraces(SourceBuilder sb, [bool needsExitMark = false]) {
199 sb.append(' {');
200 sb.append(eol);
201 String indent = utils.getLinePrefix(selectionOffset);
202 sb.append(indent);
203 sb.append(utils.getIndent(1));
204 if (needsExitMark) {
205 sb.setExitOffset();
206 }
207 sb.append(eol);
208 sb.append(indent);
209 sb.append('}');
210 }
211
212 int _appendNewlinePlusIndent() {
213 // Append a newline plus proper indent and another newline.
214 // Return the position before the second newline.
215 String indent = utils.getLinePrefix(selectionOffset);
216 int loc = utils.getLineNext(selectionOffset);
217 _addInsertEdit(loc, indent + eol);
218 return loc + indent.length;
219 }
220
221 bool _complete_ifOrWhileStatement(
222 _IfWhileStructure statement, StatementCompletionKind kind) {
223 String text = utils.getNodeText(node);
224 if (text.endsWith(eol)) {
225 text = text.substring(0, text.length - eol.length);
226 }
227 SourceBuilder sb;
228 bool needsExit = false;
229 if (statement.leftParenthesis.lexeme.isEmpty) {
230 if (!statement.rightParenthesis.lexeme.isEmpty) {
231 // Quite unlikely to see this so don't try to fix it.
232 return false;
233 }
234 int len = statement.keyword.length;
235 if (text.length == len ||
236 !text.substring(len, len + 1).contains(new RegExp(r'\s'))) {
237 sb = new SourceBuilder(file, statement.offset + len);
238 sb.append(' ');
239 } else {
240 sb = new SourceBuilder(file, statement.offset + len + 1);
241 }
242 sb.append('(');
243 sb.setExitOffset();
244 sb.append(')');
245 } else {
246 if (_isEmptyExpression(statement.condition)) {
247 exitPosition = _newPosition(statement.leftParenthesis.offset + 1);
248 sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
249 } else {
250 sb = new SourceBuilder(file, statement.rightParenthesis.offset + 1);
251 needsExit = true;
252 }
253 }
254 if (statement.block is EmptyStatement) {
255 _appendEmptyBraces(sb, needsExit);
256 }
257 _insertBuilder(sb);
258 _setCompletion(kind);
259 return true;
260 }
261
262 bool _complete_ifStatement() {
263 if (errors.isEmpty || node is! IfStatement) {
264 return false;
265 }
266 IfStatement ifNode = node;
267 if (ifNode != null) {
268 if (ifNode.elseKeyword != null) {
269 return false;
270 }
271 var stmt = new _IfWhileStructure(ifNode.ifKeyword, ifNode.leftParenthesis,
272 ifNode.condition, ifNode.rightParenthesis, ifNode.thenStatement);
273 return _complete_ifOrWhileStatement(
274 stmt, DartStatementCompletion.COMPLETE_IF_STMT);
275 }
276 return false;
277 }
278
279 bool _complete_plainOleEnter() {
280 int offset;
281 if (!errors.isEmpty) {
282 offset = selectionOffset;
283 } else {
284 String indent = utils.getLinePrefix(selectionOffset);
285 int loc = utils.getLineNext(selectionOffset);
286 _addInsertEdit(loc, indent + eol);
287 offset = loc + indent.length + eol.length;
288 }
289 _setCompletionAt(DartStatementCompletion.PLAIN_OLE_ENTER, offset);
290 return true;
291 }
292
293 bool _complete_simpleSemicolon() {
294 if (errors.length != 1) {
295 return false;
296 }
297 var error = _findError(ParserErrorCode.EXPECTED_TOKEN, partialMatch: "';'");
298 if (error != null) {
299 int insertOffset = error.offset + error.length;
300 _addInsertEdit(insertOffset, ';');
301 int offset = _appendNewlinePlusIndent() + 1 /* ';' */;
302 _setCompletionAt(DartStatementCompletion.SIMPLE_SEMICOLON, offset);
303 return true;
304 }
305 return false;
306 }
307
308 bool _complete_whileStatement() {
309 if (errors.isEmpty || node is! WhileStatement) {
310 return false;
311 }
312 WhileStatement whileNode = node;
313 if (whileNode != null) {
314 var stmt = new _IfWhileStructure(
315 whileNode.whileKeyword,
316 whileNode.leftParenthesis,
317 whileNode.condition,
318 whileNode.rightParenthesis,
319 whileNode.body);
320 return _complete_ifOrWhileStatement(
321 stmt, DartStatementCompletion.COMPLETE_WHILE_STMT);
322 }
323 return false;
324 }
325
326 engine.AnalysisError _findError(ErrorCode code, {partialMatch: null}) {
327 var error =
328 errors.firstWhere((err) => err.errorCode == code, orElse: () => null);
329 if (error != null) {
330 if (partialMatch != null) {
331 return error.message.contains(partialMatch) ? error : null;
332 }
333 return error;
334 }
335 return null;
336 }
337
338 LinkedEditGroup _getLinkedPosition(String groupId) {
339 LinkedEditGroup group = linkedPositionGroups[groupId];
340 if (group == null) {
341 group = new LinkedEditGroup.empty();
342 linkedPositionGroups[groupId] = group;
343 }
344 return group;
345 }
346
347 void _insertBuilder(SourceBuilder builder, [int length = 0]) {
348 {
349 SourceRange range = rangeStartLength(builder.offset, length);
350 String text = builder.toString();
351 _addReplaceEdit(range, text);
352 }
353 // add linked positions
354 builder.linkedPositionGroups.forEach((String id, LinkedEditGroup group) {
355 LinkedEditGroup fixGroup = _getLinkedPosition(id);
356 group.positions.forEach((Position position) {
357 fixGroup.addPosition(position, group.length);
358 });
359 group.suggestions.forEach((LinkedEditSuggestion suggestion) {
360 fixGroup.addSuggestion(suggestion);
361 });
362 });
363 // add exit position
364 {
365 int exitOffset = builder.exitOffset;
366 if (exitOffset != null) {
367 exitPosition = _newPosition(exitOffset);
368 }
369 }
370 }
371
372 bool _isEmptyExpression(Expression expr) {
373 if (expr is! SimpleIdentifier) {
374 return false;
375 }
376 SimpleIdentifier id = expr as SimpleIdentifier;
377 return id.length == 0;
378 }
379
380 Position _newPosition(int offset) {
381 return new Position(file, offset);
382 }
383
384 void _setCompletion(StatementCompletionKind kind, [List args]) {
385 assert(exitPosition != null);
386 change.selection = exitPosition;
387 change.message = formatList(kind.message, args);
388 linkedPositionGroups.values
389 .forEach((group) => change.addLinkedEditGroup(group));
390 completion = new StatementCompletion(kind, change);
391 }
392
393 void _setCompletionAt(StatementCompletionKind kind, int offset, [List args]) {
394 exitPosition = _newPosition(offset);
395 _setCompletion(kind, args);
396 }
397 }
398
399 // Encapsulate common structure of if-statement and while-statement.
400 class _IfWhileStructure {
401 final Token keyword;
402 final Token leftParenthesis, rightParenthesis;
403 final Expression condition;
404 final Statement block;
405
406 _IfWhileStructure(this.keyword, this.leftParenthesis, this.condition,
407 this.rightParenthesis, this.block);
408
409 int get offset => keyword.offset;
410 }
OLDNEW
« no previous file with comments | « pkg/analysis_server/lib/src/edit/edit_domain.dart ('k') | pkg/analysis_server/test/edit/statement_completion_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698