Skip to content

Commit afa9826

Browse files
committed
WIP: Keyword completions based on the syntax tree
rdar://98551200
1 parent 53b7888 commit afa9826

File tree

6 files changed

+1281
-2
lines changed

6 files changed

+1281
-2
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
extension SyntaxProtocol {
16+
static func completions(at keyPath: AnyKeyPath, visitedNodeKinds: inout [SyntaxProtocol.Type]) -> (completions: Set<TokenKind>, hasRequiredToken: Bool) {
17+
visitedNodeKinds.append(Self.self)
18+
if let keyPath = keyPath as? KeyPath<Self, TokenSyntax> {
19+
return (Set(keyPath.tokenChoices), true)
20+
} else if let keyPath = keyPath as? KeyPath<Self, TokenSyntax?> {
21+
return (Set(keyPath.tokenChoices), false)
22+
} else if let value = type(of: keyPath).valueType as? SyntaxOrOptionalProtocol.Type {
23+
switch value.syntaxOrOptionalProtocolType {
24+
case .optional(let syntaxType):
25+
return (syntaxType.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds).completions, false)
26+
case .nonOptional(let syntaxType):
27+
return (syntaxType.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds).completions, !syntaxType.structure.isCollection)
28+
}
29+
} else {
30+
assertionFailure("Unexpected keypath")
31+
return ([], false)
32+
}
33+
}
34+
35+
36+
static func completionsAtStartOfNode(visitedNodeKinds: inout [SyntaxProtocol.Type]) -> (completions: Set<TokenKind>, hasRequiredToken: Bool) {
37+
if visitedNodeKinds.contains(where: { $0 == Self.self }) {
38+
return ([], true)
39+
}
40+
if self == Syntax.self {
41+
return ([], true)
42+
}
43+
44+
var hasRequiredToken: Bool
45+
var completions: Set<TokenKind> = []
46+
47+
switch self.structure {
48+
case .layout(let keyPaths):
49+
hasRequiredToken = false // Only relevant if keyPaths is empty and the loop below isn't traversed
50+
for keyPath in keyPaths {
51+
let res = self.completions(at: keyPath, visitedNodeKinds: &visitedNodeKinds)
52+
completions.formUnion(res.completions)
53+
hasRequiredToken = hasRequiredToken || res.hasRequiredToken
54+
if hasRequiredToken {
55+
break
56+
}
57+
}
58+
case .collection(let collectionElementType):
59+
return collectionElementType.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds)
60+
case .choices(let choices):
61+
hasRequiredToken = true
62+
for choice in choices {
63+
switch choice {
64+
case .node(let nodeType):
65+
let res = nodeType.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds)
66+
completions.formUnion(res.completions)
67+
hasRequiredToken = hasRequiredToken && res.hasRequiredToken
68+
case .token(let tokenKind):
69+
completions.insert(tokenKind)
70+
}
71+
}
72+
}
73+
74+
return (completions, hasRequiredToken)
75+
}
76+
}
77+
78+
extension SyntaxProtocol {
79+
func completions(afterIndex index: Int) -> Set<TokenKind> {
80+
var hasRequiredToken: Bool = false
81+
82+
var completions: Set<TokenKind> = []
83+
var visitedNodeKinds: [SyntaxProtocol.Type] = []
84+
if case .layout(let childrenKeyPaths) = self.kind.syntaxNodeType.structure {
85+
if index < childrenKeyPaths.count {
86+
for keyPath in childrenKeyPaths[(index + 1)...] {
87+
let res = self.kind.syntaxNodeType.completions(at: keyPath, visitedNodeKinds: &visitedNodeKinds)
88+
completions.formUnion(res.completions)
89+
hasRequiredToken = res.hasRequiredToken
90+
if hasRequiredToken {
91+
break
92+
}
93+
}
94+
}
95+
}
96+
if !hasRequiredToken, let parent = parent {
97+
completions.formUnion(parent.completions(afterIndex: self.indexInParent))
98+
}
99+
return completions
100+
}
101+
102+
public func completions(at position: AbsolutePosition) -> Set<TokenKind> {
103+
if position <= self.positionAfterSkippingLeadingTrivia {
104+
var visitedNodeKinds: [SyntaxProtocol.Type] = []
105+
return Self.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds).completions
106+
}
107+
let finder = TokenFinder(targetPosition: position)
108+
finder.walk(self)
109+
guard let found = finder.found?.previousToken(viewMode: .sourceAccurate), let parent = found.parent else {
110+
return []
111+
}
112+
return parent.completions(afterIndex: found.indexInParent)
113+
}
114+
}
115+
116+
/// Finds the first token whose text (ignoring trivia) starts after targetPosition.
117+
class TokenFinder: SyntaxAnyVisitor {
118+
var targetPosition: AbsolutePosition
119+
var found: TokenSyntax? = nil
120+
121+
init(targetPosition: AbsolutePosition) {
122+
self.targetPosition = targetPosition
123+
super.init(viewMode: .sourceAccurate)
124+
}
125+
126+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
127+
if found != nil || node.endPosition < targetPosition {
128+
return .skipChildren
129+
} else {
130+
return .visitChildren
131+
}
132+
}
133+
134+
override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
135+
if targetPosition <= node.positionAfterSkippingLeadingTrivia, found == nil {
136+
found = node
137+
}
138+
return .skipChildren
139+
}
140+
}

Sources/SwiftSyntax/Syntax.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,25 @@ public extension SyntaxHashable {
178178
}
179179
}
180180

181+
public enum SyntaxOrOptionalProtocolType {
182+
case optional(SyntaxProtocol.Type)
183+
case nonOptional(SyntaxProtocol.Type)
184+
}
185+
186+
public protocol SyntaxOrOptionalProtocol {
187+
static var syntaxOrOptionalProtocolType: SyntaxOrOptionalProtocolType { get }
188+
}
189+
190+
extension Optional: SyntaxOrOptionalProtocol where Wrapped: SyntaxProtocol {
191+
public static var syntaxOrOptionalProtocolType: SyntaxOrOptionalProtocolType {
192+
return .optional(Wrapped.self)
193+
}
194+
}
195+
181196
/// Provide common functionality for specialized syntax nodes. Extend this
182197
/// protocol to provide common functionality for all syntax nodes.
183198
/// DO NOT CONFORM TO THIS PROTOCOL YOURSELF!
184-
public protocol SyntaxProtocol: CustomStringConvertible,
199+
public protocol SyntaxProtocol: SyntaxOrOptionalProtocol, CustomStringConvertible,
185200
CustomDebugStringConvertible, TextOutputStreamable {
186201

187202
/// Retrieve the generic syntax node that is represented by this node.
@@ -203,6 +218,10 @@ public protocol SyntaxProtocol: CustomStringConvertible,
203218
}
204219

205220
public extension SyntaxProtocol {
221+
static var syntaxOrOptionalProtocolType: SyntaxOrOptionalProtocolType {
222+
return .nonOptional(Self.self)
223+
}
224+
206225
/// If the parent has a dedicated "name for diagnostics" for this node, return it.
207226
/// Otherwise, return `nil`.
208227
var childNameInParent: String? {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
%{
2+
# -*- mode: Swift -*-
3+
from gyb_syntax_support import *
4+
# Ignore the following admonition it applies to the resulting .swift file only
5+
}%
6+
//// Automatically Generated From TokenChoices.swift.gyb.
7+
//// Do Not Edit Directly!
8+
//===----------------------------------------------------------------------===//
9+
//
10+
// This source file is part of the Swift.org open source project
11+
//
12+
// Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
13+
// Licensed under Apache License v2.0 with Runtime Library Exception
14+
//
15+
// See https://swift.org/LICENSE.txt for license information
16+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
17+
//
18+
//===----------------------------------------------------------------------===//
19+
20+
public extension KeyPath where Root: SyntaxProtocol {
21+
var tokenChoices: [TokenKind] {
22+
switch self {
23+
% for node in SYNTAX_NODES:
24+
% for child in [child for child in node.children if child.type_name == 'TokenSyntax']:
25+
case \${node.name}.${child.swift_name}:
26+
% assert len(child.text_choices) == 0 or len(child.token_choices) <= 1, f'cannot combine text_choices ({child.text_choices}) and token_choices ({child.token_choices}) in {node.name}.{child.name}'
27+
% choices = []
28+
% for choice in child.token_choices:
29+
% if choice.text:
30+
% choices.append(f'.{choice.swift_kind()}')
31+
% elif child.text_choices:
32+
% for text_choice in child.text_choices:
33+
% choices.append(f'.{choice.swift_kind()}("{text_choice}")')
34+
% end
35+
% else:
36+
% choices.append(f'.{choice.swift_kind()}("")')
37+
% end
38+
% end
39+
return [${', '.join(choices)}]
40+
% end
41+
% end
42+
default:
43+
return []
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)