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

Unified Diff: pkg/analysis_server/lib/src/services/completion/completion_target.dart

Issue 820773002: Introduce CompletionTarget to take the place of the Node being completed. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years 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 side-by-side diff with in-line comments
Download patch
Index: pkg/analysis_server/lib/src/services/completion/completion_target.dart
diff --git a/pkg/analysis_server/lib/src/services/completion/completion_target.dart b/pkg/analysis_server/lib/src/services/completion/completion_target.dart
new file mode 100644
index 0000000000000000000000000000000000000000..4e32cd5db45b26e4313f0eccc1e72571eeba26ad
--- /dev/null
+++ b/pkg/analysis_server/lib/src/services/completion/completion_target.dart
@@ -0,0 +1,206 @@
+import 'package:analyzer/src/generated/ast.dart';
+import 'package:analyzer/src/generated/scanner.dart';
+
+/**
+ * A CompletionTarget represents an edge in the parse tree which connects an
+ * AST node (representing the [context] of the completion) to one of its
+ * children (the [entity], which represents the place in the parse tree where
+ * the newly completed text will be inserted).
+ *
+ * To illustrate, consider the following snippet of code, and its associated
+ * parse tree. (T's represent tokens, N's represent AST nodes. Some trivial
+ * AST nodes are not shown).
+ *
+ * ___N (function declaration)
+ * / \
+ * / __N_______ (function body)
+ * / / |a \
+ * / / N______ \ (statement)
+ * / / / \ \
+ * / / N____ \ \ (assignment expression)
+ * | / /| \ \ \
+ * . | / | _N___ \ | ("as" expression)
+ * . | | | / | \c | |
+ * . | N | N |b N | | (simple identifiers)
+ * | | | | | | | |
+ * T T T T T T T T
+ * m() { foo = bar as Baz; }
+ *
+ * The Completion target is usually placed as high in the tree as possible so
+ * that we can produce the most meaningful completions with minimal effort.
+ * For instance, if the cursor is inside the identifier "foo", the completion
+ * target will be the edge marked "a", so that we will produce all completions
+ * that could possibly start a statement, even those which would conflict with
+ * the current parse (such as the keyword "for", which begins a "for"
+ * statement). As a consequence of this, the [entity] will usually not be the
+ * first child of the [context] node.
+ *
+ * Note that the [context] is always an AST node, but the [entity] may not be.
+ * For instance, if the cursor is inside the keyword "as", the completion
+ * target will be the edge marked "b", so the [context] is the token "as".
danrubel 2014/12/20 05:22:57 Should the end of this sentence be "... so the [en
Paul Berry 2014/12/20 15:43:24 Yes, thanks. Fixed.
+ *
+ * If the cursor is between tokens, the completion target is usually associated
+ * with the token that follows the cursor (since that's the token that will be
+ * displaced when the new text is inserted). For example, if the cursor is
+ * after the "{" character, then the completion target will be the edge marked
+ * "a", just as it is if the cursor is inside the identifier "foo". However
+ * there is one exception: if the cursor is at the rightmost edge of a keyword
+ * or identifier, then the completion target is associated with that token,
+ * since any further letters typed will change the meaning of the identifier or
+ * keyword, rather than creating a new token. So for instance, if the cursor
+ * is just after the "s" of "as", the completion target will be the edge marked
+ * "b", but if the cursor target is after the first space following "as", then
+ * the completion target will be the edge marked "c".
+ *
+ * If the file is empty, or the cursor is after all the text in the file, then
+ * there may be no edge in the parse tree which is appropriate to act as the
+ * completion target; in this case, [entity] is set to null and [context] is
+ * set to the CompilationUnit.
+ */
+class CompletionTarget {
+ /**
+ * The context in which the completion is occurring. This is the AST node
+ * which is a direct parent of [entity].
+ */
+ final AstNode context;
danrubel 2014/12/20 05:22:57 Since "context" already has a strong connotation a
Paul Berry 2014/12/20 15:43:24 Good idea. Renamed to "containingNode".
+
+ /**
+ * The entity which the completed text will replace (or which will be
+ * displaced once the completed text is inserted). This may be an AstNode or
+ * a Token, or it may be null if the cursor is after all tokens in the file.
+ *
+ * Usually, the entity won't be the first child of the [context] (this is a
+ * consequence of placing the completion target as high in the tree as
+ * possible). However, there is one exception: when the cursor is inside of
+ * a multi-character token which is not a keyword or identifier (e.g. a
+ * comment, or a token like "+=", the entity will be always be the token.
+ */
+ final Object entity;
+
+ /**
+ * Compute the appropriate [CompletionTarget] for the given [offset] within
+ * the [compilationUnit].
+ */
+ factory CompletionTarget.forOffset(CompilationUnit compilationUnit,
+ int offset) {
+ // The precise algorithm is as follows. We perform a depth-first search of
+ // all edges in the parse tree (both those that point to AST nodes and
+ // those that point to tokens), visiting parents before children. The
+ // first edge which points to an entity satisfying either _isCandidateToken
+ // or _isCandidateNode is the completion target. If no edge is found that
+ // satisfies these two predicates, then we set the completion target entity
+ // to null and the context to the compilationUnit.
+ //
+ // Note that if a token is not a candidate target, then none of the tokens
+ // that precede it are candidate targets either. Therefore any entity
+ // whose last token is not a candidate target can be skipped. This lets us
+ // prune the search to the point where no recursion is necessary; at each
+ // step in the process we know exactly which child node we need to proceed
+ // to.
+ AstNode containingNode = compilationUnit;
+ outerLoop: while (true) {
+ if (containingNode is Comment) {
+ // Comments are handled specially: we descend into any CommentReference
+ // child node that contains the cursor offset.
+ Comment comment = containingNode;
+ for (CommentReference commentReference in comment.references) {
+ if (commentReference.offset <= offset &&
+ offset <= commentReference.end) {
+ containingNode = commentReference;
+ continue outerLoop;
+ }
+ }
+ }
+ for (var entity in containingNode.childEntities) {
+ if (entity is Token) {
+ if (_isCandidateToken(entity, offset)) {
+ // Target found.
+ return new CompletionTarget._(containingNode, entity);
+ } else {
+ // Since entity is a token, we don't need to look inside it; just
+ // proceed to the next entity.
+ continue;
+ }
+ } else if (entity is AstNode) {
+ // If the last token in the node isn't a candidate target, then
+ // neither the node nor any of its descendants can possibly be the
+ // completion target, so we can skip the node entirely.
+ if (!_isCandidateToken(entity.endToken, offset)) {
+ continue;
+ }
+
+ // If the node is a candidate target, then we are done.
+ if (_isCandidateNode(entity, offset)) {
+ return new CompletionTarget._(containingNode, entity);
+ }
+
+ // Otherwise, the completion target is somewhere inside the entity,
+ // so we need to jump to the start of the outer loop to examine its
+ // contents.
+ containingNode = entity;
+ continue outerLoop;
+ } else {
+ // Unexpected entity found (all entities in a parse tree should be
+ // AST nodes or tokens).
+ assert(false);
+ }
+ }
+
+ // No completion target found. It should only be possible to reach here
+ // the first time through the outer loop (since we only jump to the start
+ // of the outer loop after determining that the completion target is
+ // inside an entity). We can check that assumption by verifying that
+ // containingNode is still the compilationUnit.
+ assert(identical(containingNode, compilationUnit));
+
+ // Since no completion target was found, we set the completion target
+ // entity to null and use the compilationUnit as the parent.
+ return new CompletionTarget._(compilationUnit, null);
+ }
+ }
+
+ /**
+ * Create a [CompletionTarget] holding the given [context] and [entity].
+ */
+ CompletionTarget._(this.context, this.entity);
+
+ /**
+ * Determine whether [node] could possibly be the [entity] for a
+ * [CompletionTarget] associated with the given [offset].
+ */
+ static bool _isCandidateNode(AstNode node, int offset) {
+ // If the node's first token is a keyword or identifier, then the node is a
+ // candidate entity if its first token is.
+ Token beginToken = node.beginToken;
+ if (beginToken.type == TokenType.KEYWORD ||
+ beginToken.type == TokenType.IDENTIFIER) {
+ return _isCandidateToken(beginToken, offset);
+ }
+
+ // Otherwise, the node is a candidate entity only if the offset is before
+ // the beginning of the node. This ensures that completions within a token
+ // (e.g. inside a literal string or inside a comment) are evaluated within
+ // the context of the token itself.
+ return offset <= node.offset;
+ }
+
+ /**
+ * Determine whether [token] could possibly be the [entity] for a
+ * [CompletionTarget] associated with the given [offset].
+ */
+ static bool _isCandidateToken(Token token, int offset) {
+ // A token is considered a candidate entity if the cursor offset is (a)
+ // before the start of the token, (b) within the token, (c) at the end of
+ // the token and the token is a keyword or identifier, or (d) at the
+ // location of the token and the token is zero length.
+ if (offset < token.end) {
+ return true;
+ } else if (offset == token.end) {
+ return token.type == TokenType.KEYWORD ||
+ token.type == TokenType.IDENTIFIER ||
+ token.length == 0;
+ } else {
+ return false;
+ }
+ }
+}

Powered by Google App Engine
This is Rietveld 408576698