@@ -14,34 +14,166 @@ import Foundation
14
14
import SKLogging
15
15
@_spi ( RawSyntax) import SwiftSyntax
16
16
17
- func rewriteSourceKitPlaceholders( in string: String , clientSupportsSnippets: Bool ) -> String {
18
- var result = string
19
- var index = 1
20
- while let start = result. range ( of: " <# " ) {
21
- guard let end = result [ start. upperBound... ] . range ( of: " #> " ) else {
22
- logger. fault ( " Invalid placeholder in \( string) " )
23
- return string
24
- }
25
- let rawPlaceholder = String ( result [ start. lowerBound..< end. upperBound] )
26
- guard let displayName = nameForSnippet ( rawPlaceholder) else {
27
- logger. fault ( " Failed to decode placeholder \( rawPlaceholder) in \( string) " )
28
- return string
17
+ /// Translate SourceKit placeholder syntax — `<#foo#>` — in `input` to LSP
18
+ /// placeholder syntax: `${n:foo}`.
19
+ ///
20
+ /// If `clientSupportsSnippets` is `false`, the placeholder is rendered as an
21
+ /// empty string, to prevent the client from inserting special placeholder
22
+ /// characters as if they were literal text.
23
+ @_spi ( Testing)
24
+ public func rewriteSourceKitPlaceholders( in input: String , clientSupportsSnippets: Bool ) -> String {
25
+ var result = " "
26
+ var nextPlaceholderNumber = 1
27
+ // Current stack of nested placeholders, most nested last. Each element needs
28
+ // to be rendered inside the element before it.
29
+ var placeholders : [ ( number: Int , contents: String ) ] = [ ]
30
+ let tokens = tokenize ( input)
31
+ for token in tokens {
32
+ switch token {
33
+ case let . text( text) :
34
+ if placeholders. isEmpty {
35
+ result += text
36
+ } else {
37
+ placeholders. latest. contents += text
38
+ }
39
+
40
+ case let . curlyBrace( brace) :
41
+ if placeholders. isEmpty {
42
+ result. append ( brace)
43
+ } else {
44
+ // Braces are only escaped _inside_ a placeholder; otherwise the client
45
+ // would include the backslashes literally.
46
+ placeholders. latest. contents. append ( contentsOf: [ " \\ " , brace] )
47
+ }
48
+
49
+ case . placeholderOpen:
50
+ placeholders. append ( ( number: nextPlaceholderNumber, contents: " " ) )
51
+ nextPlaceholderNumber += 1
52
+
53
+ case . placeholderClose:
54
+ guard let ( number, placeholderBody) = placeholders. popLast ( ) else {
55
+ logger. fault ( " Invalid placeholder in \( input) " )
56
+ return input
57
+ }
58
+ guard let displayName = nameForSnippet ( placeholderBody) else {
59
+ logger. fault ( " Failed to decode placeholder \( placeholderBody) in \( input) " )
60
+ return input
61
+ }
62
+ let placeholder =
63
+ clientSupportsSnippets
64
+ ? formatLSPPlaceholder ( displayName, number: number)
65
+ : " "
66
+ if placeholders. isEmpty {
67
+ result += placeholder
68
+ } else {
69
+ placeholders. latest. contents += placeholder
70
+ }
29
71
}
30
- let snippet = clientSupportsSnippets ? " ${ \( index) : \( displayName) } " : " "
31
- result. replaceSubrange ( start. lowerBound..< end. upperBound, with: snippet)
32
- index += 1
33
72
}
73
+
34
74
return result
35
75
}
36
76
37
- /// Parse a SourceKit placeholder and extract the display name suitable for a
38
- /// LSP snippet.
39
- fileprivate func nameForSnippet( _ text: String ) -> String ? {
40
- var text = text
77
+ /// Scan `input` to identify special elements within: curly braces, which may
78
+ /// need to be escaped; and SourceKit placeholder open/close delimiters.
79
+ private func tokenize( _ input: String ) -> [ SnippetToken ] {
80
+ var index = input. startIndex
81
+ var isAtEnd : Bool { index == input. endIndex }
82
+ func match( _ char: Character ) -> Bool {
83
+ if isAtEnd || input [ index] != char {
84
+ return false
85
+ } else {
86
+ input. formIndex ( after: & index)
87
+ return true
88
+ }
89
+ }
90
+ func next( ) -> Character ? {
91
+ guard !isAtEnd else { return nil }
92
+ defer { input. formIndex ( after: & index) }
93
+ return input [ index]
94
+ }
95
+
96
+ var tokens : [ SnippetToken ] = [ ]
97
+ var text = " "
98
+ while let char = next ( ) {
99
+ switch char {
100
+ case " < " :
101
+ if match ( " # " ) {
102
+ tokens. append ( . text( text) )
103
+ text. removeAll ( )
104
+ tokens. append ( . placeholderOpen)
105
+ } else {
106
+ text. append ( char)
107
+ }
108
+
109
+ case " # " :
110
+ if match ( " > " ) {
111
+ tokens. append ( . text( text) )
112
+ text. removeAll ( )
113
+ tokens. append ( . placeholderClose)
114
+ } else {
115
+ text. append ( char)
116
+ }
117
+
118
+ case " { " , " } " :
119
+ tokens. append ( . text( text) )
120
+ text. removeAll ( )
121
+ tokens. append ( . curlyBrace( char) )
122
+
123
+ case let c:
124
+ text. append ( c)
125
+ }
126
+ }
127
+
128
+ tokens. append ( . text( text) )
129
+
130
+ return tokens
131
+ }
132
+
133
+ /// A syntactical element inside a SourceKit snippet.
134
+ private enum SnippetToken {
135
+ /// A placeholder delimiter.
136
+ case placeholderOpen, placeholderClose
137
+ /// One of '{' or '}', which may need to be escaped in the output.
138
+ case curlyBrace( Character )
139
+ /// Any other consecutive run of characters from the input, which needs no
140
+ /// special treatment.
141
+ case text( String )
142
+ }
143
+
144
+ /// Given the interior text of a SourceKit placeholder, extract a display name
145
+ /// suitable for a LSP snippet.
146
+ private func nameForSnippet( _ body: String ) -> String ? {
147
+ var text = rewrappedAsPlaceholder ( body)
41
148
return text. withSyntaxText {
42
149
guard let data = RawEditorPlaceholderData ( syntaxText: $0) else {
43
150
return nil
44
151
}
45
152
return String ( syntaxText: data. typeForExpansionText ?? data. displayText)
46
153
}
47
154
}
155
+
156
+ private let placeholderStart = " <# "
157
+ private let placeholderEnd = " #> "
158
+ private func rewrappedAsPlaceholder( _ body: String ) -> String {
159
+ return placeholderStart + body + placeholderEnd
160
+ }
161
+
162
+ /// Wrap `body` in LSP snippet placeholder syntax, using `number` as the
163
+ /// placeholder's index in the snippet.
164
+ private func formatLSPPlaceholder( _ body: String , number: Int ) -> String {
165
+ " ${ \( number) : \( body) } "
166
+ }
167
+
168
+ private extension Array {
169
+ /// Mutable access to the final element of an array.
170
+ ///
171
+ /// - precondition: The array must not be empty.
172
+ var latest : Element {
173
+ get { self . last! }
174
+ _modify {
175
+ let index = self . index ( before: self . endIndex)
176
+ yield & self [ index]
177
+ }
178
+ }
179
+ }
0 commit comments