@@ -19,12 +19,23 @@ public struct DocumentSnapshot {
19
19
public var document : Document
20
20
public var version : Int
21
21
public var lineTable : LineTable
22
- public var text : String { return lineTable. content }
22
+ /// Syntax highlighting tokens for the document. Note that
23
+ /// `uri` + `latestVersion` only uniquely identifies a snapshot's content,
24
+ /// the tokens are updated independently and only used internally.
25
+ public var tokens : DocumentTokens
23
26
24
- public init ( document: Document , version: Int , lineTable: LineTable ) {
27
+ public var text : String { lineTable. content }
28
+
29
+ public init (
30
+ document: Document ,
31
+ version: Int ,
32
+ lineTable: LineTable ,
33
+ tokens: DocumentTokens
34
+ ) {
25
35
self . document = document
26
36
self . version = version
27
37
self . lineTable = lineTable
38
+ self . tokens = tokens
28
39
}
29
40
30
41
func index( of pos: Position ) -> String . Index ? {
@@ -37,17 +48,24 @@ public final class Document {
37
48
public let language : Language
38
49
var latestVersion : Int
39
50
var latestLineTable : LineTable
51
+ var latestTokens : DocumentTokens
40
52
41
53
init ( uri: DocumentURI , language: Language , version: Int , text: String ) {
42
54
self . uri = uri
43
55
self . language = language
44
56
self . latestVersion = version
45
57
self . latestLineTable = LineTable ( text)
58
+ self . latestTokens = DocumentTokens ( )
46
59
}
47
60
48
61
/// **Not thread safe!** Use `DocumentManager.latestSnapshot` instead.
49
62
fileprivate var latestSnapshot : DocumentSnapshot {
50
- return DocumentSnapshot ( document: self , version: latestVersion, lineTable: latestLineTable)
63
+ DocumentSnapshot (
64
+ document: self ,
65
+ version: latestVersion,
66
+ lineTable: latestLineTable,
67
+ tokens: latestTokens
68
+ )
51
69
}
52
70
}
53
71
@@ -98,43 +116,99 @@ public final class DocumentManager {
98
116
99
117
/// Applies the given edits to the document.
100
118
///
101
- /// - parameter editCallback: Optional closure to call for each edit.
119
+ /// - parameter willEditDocument: Optional closure to call before each edit.
120
+ /// - parameter updateDocumentTokens: Optional closure to call after each edit.
102
121
/// - parameter before: The document contents *before* the edit is applied.
122
+ /// - parameter after: The document contents *after* the edit is applied.
103
123
/// - returns: The contents of the file after all the edits are applied.
104
124
/// - throws: Error.missingDocument if the document is not open.
105
125
@discardableResult
106
- public func edit( _ uri: DocumentURI , newVersion: Int , edits: [ TextDocumentContentChangeEvent ] , editCallback: ( ( _ before: DocumentSnapshot , TextDocumentContentChangeEvent ) -> Void ) ? = nil ) throws -> DocumentSnapshot {
126
+ public func edit(
127
+ _ uri: DocumentURI ,
128
+ newVersion: Int ,
129
+ edits: [ TextDocumentContentChangeEvent ] ,
130
+ willEditDocument: ( ( _ before: DocumentSnapshot , TextDocumentContentChangeEvent ) -> Void ) ? = nil ,
131
+ updateDocumentTokens: ( ( _ after: DocumentSnapshot ) -> DocumentTokens ) ? = nil
132
+ ) throws -> DocumentSnapshot {
107
133
return try queue. sync {
108
134
guard let document = documents [ uri] else {
109
135
throw Error . missingDocument ( uri)
110
136
}
111
137
112
138
for edit in edits {
113
- if let f = editCallback {
139
+ if let f = willEditDocument {
114
140
f ( document. latestSnapshot, edit)
115
141
}
116
142
117
143
if let range = edit. range {
118
-
119
144
document. latestLineTable. replace (
120
145
fromLine: range. lowerBound. line,
121
146
utf16Offset: range. lowerBound. utf16index,
122
147
toLine: range. upperBound. line,
123
148
utf16Offset: range. upperBound. utf16index,
124
149
with: edit. text)
150
+
151
+ // Remove all tokens in the updated range and shift later ones.
125
152
153
+ let replacedLineCount = 1 + range. upperBound. line - range. lowerBound. line
154
+ let newLines = edit. text. split ( separator: " \n " , omittingEmptySubsequences: false )
155
+ let upperUtf16IndexAfterEdit = (
156
+ newLines. count == 1 ? range. lowerBound. utf16index : 0
157
+ ) + newLines. last!. utf16. count
158
+ let lastLineCharDelta = upperUtf16IndexAfterEdit - range. upperBound. utf16index
159
+ let lineDelta = newLines. count - replacedLineCount // may be negative
160
+
161
+ document. latestTokens. withMutableTokensOfEachKind { tokens in
162
+ tokens = Array ( tokens. lazy
163
+ . filter {
164
+ // Only keep tokens that don't overlap with the edit range
165
+ !$0. range. overlaps ( range)
166
+ }
167
+ . map {
168
+ // Shift tokens after the edit range
169
+ var token = $0
170
+ if token. start. line == range. upperBound. line
171
+ && token. start. utf16index >= range. upperBound. utf16index {
172
+ token. move ( lineDelta: lineDelta, utf16indexDelta: lastLineCharDelta)
173
+ } else if token. start. line > range. upperBound. line {
174
+ token. move ( lineDelta: lineDelta)
175
+ }
176
+ return token
177
+ } )
178
+ }
126
179
} else {
127
180
// Full text replacement.
128
181
document. latestLineTable = LineTable ( edit. text)
182
+ document. latestTokens = DocumentTokens ( )
129
183
}
130
184
185
+ if let f = updateDocumentTokens {
186
+ document. latestTokens = f ( document. latestSnapshot)
187
+ }
131
188
}
132
189
133
190
document. latestVersion = newVersion
134
191
return document. latestSnapshot
135
192
}
136
193
}
137
194
195
+ /// Updates the tokens in a document.
196
+ ///
197
+ /// - parameter uri: The URI of the document to be updated
198
+ /// - parameter tokens: The new tokens for the document
199
+ @discardableResult
200
+ public func updateTokens( _ uri: DocumentURI , tokens: DocumentTokens ) throws -> DocumentSnapshot {
201
+ return try queue. sync {
202
+ guard let document = documents [ uri] else {
203
+ throw Error . missingDocument ( uri)
204
+ }
205
+
206
+ document. latestTokens = tokens
207
+
208
+ return document. latestSnapshot
209
+ }
210
+ }
211
+
138
212
public func latestSnapshot( _ uri: DocumentURI ) -> DocumentSnapshot ? {
139
213
return queue. sync {
140
214
guard let document = documents [ uri] else {
@@ -165,11 +239,22 @@ extension DocumentManager {
165
239
}
166
240
}
167
241
168
- /// Convenience wrapper for `edit(_:newVersion:edits:editCallback:)` that logs on failure.
242
+ /// Convenience wrapper for `edit(_:newVersion:edits:willEditDocument:updateDocumentTokens:)`
243
+ /// that logs on failure.
169
244
@discardableResult
170
- func edit( _ note: DidChangeTextDocumentNotification , editCallback: ( ( _ before: DocumentSnapshot , TextDocumentContentChangeEvent ) -> Void ) ? = nil ) -> DocumentSnapshot ? {
245
+ func edit(
246
+ _ note: DidChangeTextDocumentNotification ,
247
+ willEditDocument: ( ( _ before: DocumentSnapshot , TextDocumentContentChangeEvent ) -> Void ) ? = nil ,
248
+ updateDocumentTokens: ( ( _ after: DocumentSnapshot ) -> DocumentTokens ) ? = nil
249
+ ) -> DocumentSnapshot ? {
171
250
return orLog ( " failed to edit document " , level: . error) {
172
- try edit ( note. textDocument. uri, newVersion: note. textDocument. version ?? - 1 , edits: note. contentChanges, editCallback: editCallback)
251
+ try edit (
252
+ note. textDocument. uri,
253
+ newVersion: note. textDocument. version ?? - 1 ,
254
+ edits: note. contentChanges,
255
+ willEditDocument: willEditDocument,
256
+ updateDocumentTokens: updateDocumentTokens
257
+ )
173
258
}
174
259
}
175
260
}
0 commit comments