Skip to content

Commit b18bbba

Browse files
committed
WIP: Keyword completions
1 parent 9876a95 commit b18bbba

File tree

8 files changed

+1445
-3
lines changed

8 files changed

+1445
-3
lines changed

CodeGeneration/Sources/generate-swiftsyntax/GenerateSwiftSyntax.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ struct GenerateSwiftSyntax: ParsableCommand {
8282
// SwiftBasicFormat
8383
GeneratedFileSpec(swiftBasicFormatGeneratedDir + ["BasicFormat+Extensions.swift"], basicFormatExtensionsFile),
8484

85-
// IDEUtils
85+
// SwiftIDEUtils
8686
GeneratedFileSpec(swiftideUtilsGeneratedDir + ["SyntaxClassification.swift"], syntaxClassificationFile),
87+
GeneratedFileSpec(swiftideUtilsGeneratedDir + ["TokenChoices.swift"], tokenChoicesFile),
8788

8889
// SwiftParser
8990
GeneratedFileSpec(swiftParserGeneratedDir + ["DeclarationModifier.swift"], declarationModifierFile),
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
import SwiftSyntaxBuilder
15+
import SyntaxSupport
16+
import Utils
17+
18+
let tokenChoicesFile = SourceFileSyntax(leadingTrivia: copyrightHeader) {
19+
StmtSyntax("import SwiftSyntax")
20+
21+
try! ExtensionDeclSyntax("public extension KeyPath where Root: SyntaxProtocol") {
22+
try! VariableDeclSyntax("var tokenChoices: [TokenKind]") {
23+
try! SwitchExprSyntax("switch self") {
24+
for node in SYNTAX_NODES {
25+
for child in node.children {
26+
if case .token(choices: let choices, requiresLeadingSpace: _, requiresTrailingSpace: _) = child.kind {
27+
SwitchCaseSyntax("case \\\(raw: node.name).\(raw: child.swiftName):") {
28+
let array = ArrayExprSyntax {
29+
for choice in choices {
30+
switch choice {
31+
case .keyword(text: "init"):
32+
ArrayElementSyntax(expression: ExprSyntax(".keyword(.`init`)"))
33+
case .keyword(text: var text):
34+
ArrayElementSyntax(expression: ExprSyntax(".keyword(.\(raw: text))"))
35+
case .token(tokenKind: let kind):
36+
let token = SYNTAX_TOKEN_MAP[kind]!
37+
if token.text == nil {
38+
ArrayElementSyntax(expression: ExprSyntax(#".\#(raw: token.swiftKind)("")"#))
39+
} else {
40+
ArrayElementSyntax(expression: ExprSyntax(".\(raw: token.swiftKind)"))
41+
}
42+
}
43+
}
44+
}
45+
StmtSyntax("return \(array)")
46+
}
47+
}
48+
}
49+
}
50+
SwitchCaseSyntax("default: return []")
51+
}
52+
}
53+
}
54+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
static func completionsAtStartOfNode(visitedNodeKinds: inout [SyntaxProtocol.Type]) -> (completions: Set<TokenKind>, hasRequiredToken: Bool) {
36+
if visitedNodeKinds.contains(where: { $0 == Self.self }) {
37+
return ([], true)
38+
}
39+
if self == Syntax.self {
40+
return ([], true)
41+
}
42+
43+
var hasRequiredToken: Bool
44+
var completions: Set<TokenKind> = []
45+
46+
switch self.structure {
47+
case .layout(let keyPaths):
48+
hasRequiredToken = false // Only relevant if keyPaths is empty and the loop below isn't traversed
49+
for keyPath in keyPaths {
50+
let res = self.completions(at: keyPath, visitedNodeKinds: &visitedNodeKinds)
51+
completions.formUnion(res.completions)
52+
hasRequiredToken = hasRequiredToken || res.hasRequiredToken
53+
if hasRequiredToken {
54+
break
55+
}
56+
}
57+
case .collection(let collectionElementType):
58+
return collectionElementType.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds)
59+
case .choices(let choices):
60+
hasRequiredToken = true
61+
for choice in choices {
62+
switch choice {
63+
case .node(let nodeType):
64+
let res = nodeType.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds)
65+
completions.formUnion(res.completions)
66+
hasRequiredToken = hasRequiredToken && res.hasRequiredToken
67+
case .token(let tokenKind):
68+
completions.insert(tokenKind)
69+
}
70+
}
71+
}
72+
73+
return (completions, hasRequiredToken)
74+
}
75+
}
76+
77+
extension SyntaxProtocol {
78+
func completions(after keyPath: AnyKeyPath) -> Set<TokenKind> {
79+
var hasRequiredToken: Bool = false
80+
81+
var completions: Set<TokenKind> = []
82+
var visitedNodeKinds: [SyntaxProtocol.Type] = []
83+
if case .layout(let childrenKeyPaths) = self.kind.syntaxNodeType.structure,
84+
let index = childrenKeyPaths.firstIndex(of: keyPath)
85+
{
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+
if !hasRequiredToken, let parent = parent, let keyPathInParent = self.keyPathInParent {
96+
completions.formUnion(parent.completions(after: keyPathInParent))
97+
}
98+
return completions
99+
}
100+
101+
public func completions(at position: AbsolutePosition) -> Set<TokenKind> {
102+
if position <= self.positionAfterSkippingLeadingTrivia {
103+
var visitedNodeKinds: [SyntaxProtocol.Type] = []
104+
return Self.completionsAtStartOfNode(visitedNodeKinds: &visitedNodeKinds).completions
105+
}
106+
let finder = TokenFinder(targetPosition: position)
107+
finder.walk(self)
108+
guard let found = finder.found?.previousToken(viewMode: .sourceAccurate), let parent = found.parent, let keyPathInParent = found.keyPathInParent else {
109+
return []
110+
}
111+
return parent.completions(after: keyPathInParent)
112+
}
113+
}
114+
115+
/// Finds the first token whose text (ignoring trivia) starts after targetPosition.
116+
class TokenFinder: SyntaxAnyVisitor {
117+
var targetPosition: AbsolutePosition
118+
var found: TokenSyntax? = nil
119+
120+
init(targetPosition: AbsolutePosition) {
121+
self.targetPosition = targetPosition
122+
super.init(viewMode: .sourceAccurate)
123+
}
124+
125+
override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
126+
if found != nil || node.endPosition < targetPosition {
127+
return .skipChildren
128+
} else {
129+
return .visitChildren
130+
}
131+
}
132+
133+
override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
134+
if targetPosition <= node.positionAfterSkippingLeadingTrivia, found == nil {
135+
found = node
136+
}
137+
return .skipChildren
138+
}
139+
}

0 commit comments

Comments
 (0)