Skip to content

Commit 04da78f

Browse files
committed
Clean Tests Slightly, Add lspURI to URL type.
1 parent e7f4ff6 commit 04da78f

File tree

5 files changed

+103
-69
lines changed

5 files changed

+103
-69
lines changed

CodeEdit/Features/Documents/CodeFileDocument/CodeFileDocument.swift

+1-5
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,6 @@ extension CodeFileDocument: LanguageServerDocument {
231231
/// A stable string to use when identifying documents with language servers.
232232
/// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI.
233233
var languageServerURI: String? {
234-
if let path = fileURL?.absolutePath {
235-
return "file://" + path
236-
} else {
237-
return nil
238-
}
234+
fileURL?.lspURI
239235
}
240236
}

CodeEdit/Features/LSP/Service/LSPService.swift

+18-19
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import CodeEditLanguages
4242
/// do {
4343
/// guard var languageClient = self.languageClient(for: .python) else {
4444
/// print("Failed to get client")
45-
/// throw ServerManagerError.languageClientNotFound
45+
/// throw LSPServiceError.languageClientNotFound
4646
/// }
4747
///
4848
/// let testFilePathStr = ""
@@ -54,7 +54,7 @@ import CodeEditLanguages
5454
/// // Completion example
5555
/// let textPosition = Position(line: 32, character: 18) // Lines and characters start at 0
5656
/// let completions = try await languageClient.requestCompletion(
57-
/// document: testFileURL.absoluteString,
57+
/// document: testFileURL.lspURI,
5858
/// position: textPosition
5959
/// )
6060
/// switch completions {
@@ -168,6 +168,12 @@ final class LSPService: ObservableObject {
168168
return languageClients[ClientKey(languageId, workspacePath)]
169169
}
170170

171+
func languageClient(forDocument url: URL) -> LanguageServerType? {
172+
languageClients.values.first(where: { $0.openFiles.document(for: url.lspURI) != nil })
173+
}
174+
175+
// MARK: - Start Server
176+
171177
/// Given a language and workspace path, will attempt to start the language server
172178
/// - Parameters:
173179
/// - languageId: The ID of the language server to start.
@@ -195,6 +201,8 @@ final class LSPService: ObservableObject {
195201
return server
196202
}
197203

204+
// MARK: - Document Management
205+
198206
/// Notify all relevant language clients that a document was opened.
199207
/// - Note: Must be invoked after the contents of the file are available.
200208
/// - Parameter document: The code document that was opened.
@@ -230,21 +238,19 @@ final class LSPService: ObservableObject {
230238
/// Notify all relevant language clients that a document was closed.
231239
/// - Parameter url: The url of the document that was closed
232240
func closeDocument(_ url: URL) {
233-
guard let languageClient = languageClients.first(where: {
234-
$0.value.openFiles.document(for: url.absolutePath) != nil
235-
})?.value else {
236-
return
237-
}
241+
guard let languageClient = languageClient(forDocument: url) else { return }
238242
Task {
239243
do {
240-
try await languageClient.closeDocument(url.absolutePath)
244+
try await languageClient.closeDocument(url.lspURI)
241245
} catch {
242246
// swiftlint:disable:next line_length
243-
logger.error("Failed to close document: \(url.absolutePath, privacy: .private), language: \(languageClient.languageId.rawValue). Error \(error)")
247+
logger.error("Failed to close document: \(url.lspURI, privacy: .private), language: \(languageClient.languageId.rawValue). Error \(error)")
244248
}
245249
}
246250
}
247251

252+
// MARK: - Close Workspace
253+
248254
/// Close all language clients for a workspace.
249255
///
250256
/// This is intentionally synchronous so we can exit from the workspace document's ``WorkspaceDocument/close()``
@@ -268,6 +274,8 @@ final class LSPService: ObservableObject {
268274
}
269275
}
270276

277+
// MARK: - Stop Servers
278+
271279
/// Attempts to stop a running language server. Throws an error if the server is not found
272280
/// or if the language server throws an error while trying to shutdown.
273281
/// - Parameters:
@@ -276,7 +284,7 @@ final class LSPService: ObservableObject {
276284
func stopServer(forLanguage languageId: LanguageIdentifier, workspacePath: String) async throws {
277285
guard let server = server(for: languageId, workspacePath: workspacePath) else {
278286
logger.error("Server not found for language \(languageId.rawValue) during stop operation")
279-
throw ServerManagerError.serverNotFound
287+
throw LSPServiceError.serverNotFound
280288
}
281289
do {
282290
try await server.shutdownAndExit()
@@ -311,12 +319,3 @@ final class LSPService: ObservableObject {
311319
eventListeningTasks.removeAll()
312320
}
313321
}
314-
315-
// MARK: - Errors
316-
317-
enum ServerManagerError: Error {
318-
case serverNotFound
319-
case serverStartFailed
320-
case serverStopFailed
321-
case languageClientNotFound
322-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// LSPServiceError.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 3/24/25.
6+
//
7+
8+
enum LSPServiceError: Error {
9+
case serverNotFound
10+
case serverStartFailed
11+
case serverStopFailed
12+
case languageClientNotFound
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// URL+LSPURI.swift
3+
// CodeEdit
4+
//
5+
// Created by Khan Winter on 3/24/25.
6+
//
7+
8+
import Foundation
9+
10+
extension URL {
11+
/// A stable string to use when identifying documents with language servers.
12+
/// Needs to be a valid URI, so always returns with the `file://` prefix to indicate it's a file URI.
13+
///
14+
/// Use this whenever possible when using USLs in LSP processing if not using the ``LanguageServerDocument`` type.
15+
var lspURI: String {
16+
return "file://" + absolutePath
17+
}
18+
}

CodeEditTests/Features/LSP/LanguageServer+CodeFileDocument.swift

+53-45
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import LanguageServerProtocol
1313

1414
@testable import CodeEdit
1515

16+
/// This is an integration test for notifications relating to the ``CodeFileDocument`` class.
17+
///
18+
/// For *unit* tests with the language server class, add tests to the `LanguageServer+DocumentObjects` test class as
19+
/// it's cleaner and makes correct use of the mock document type.
1620
final class LanguageServerCodeFileDocumentTests: XCTestCase {
1721
// Test opening documents in CodeEdit triggers creating a language server,
1822
// further opened documents don't create new servers
@@ -97,8 +101,11 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase {
97101
// This is usually sent from the LSPService
98102
try await server.openDocument(codeFile)
99103

100-
await waitForClientEventCount(
101-
3,
104+
await waitForClientState(
105+
(
106+
[.initialize],
107+
[.initialized, .textDocumentDidOpen]
108+
),
102109
connection: connection,
103110
description: "Initialized (2) and opened (1) notification count"
104111
)
@@ -110,22 +117,27 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase {
110117
return codeFile
111118
}
112119

113-
func waitForClientEventCount(_ count: Int, connection: BufferingServerConnection, description: String) async {
120+
func waitForClientState(
121+
_ expectedValue: ([ClientRequest.Method], [ClientNotification.Method]),
122+
connection: BufferingServerConnection,
123+
description: String
124+
) async {
114125
let expectation = expectation(description: description)
115126

116127
await withTaskGroup(of: Void.self) { group in
128+
group.addTask { await self.fulfillment(of: [expectation], timeout: 2) }
117129
group.addTask {
118-
await self.fulfillment(of: [expectation], timeout: 2)
119-
}
120-
group.addTask {
121-
for await events in connection.clientEventSequence where events.0.count + events.1.count == count {
130+
for await events in connection.clientEventSequence
131+
where events.0.map(\.method) == expectedValue.0 && events.1.map(\.method) == expectedValue.1 {
122132
expectation.fulfill()
123133
return
124134
}
125135
}
126136
}
127137
}
128138

139+
// MARK: - Open Close
140+
129141
@MainActor
130142
func testOpenCloseFileNotifications() async throws {
131143
// Set up test server
@@ -155,40 +167,38 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase {
155167
file.fileDocument = codeFile
156168
CodeEditDocumentController.shared.addDocument(codeFile)
157169

158-
await waitForClientEventCount(3, connection: connection, description: "Pre-close event count")
170+
await waitForClientState(
171+
(
172+
[.initialize],
173+
[.initialized, .textDocumentDidOpen]
174+
),
175+
connection: connection,
176+
description: "Pre-close event count"
177+
)
159178

160179
// This should then trigger a documentDidClose event
161180
codeFile.close()
162181

163-
await waitForClientEventCount(4, connection: connection, description: "Post-close event count")
164-
165-
XCTAssertEqual(
166-
connection.clientRequests.map { $0.method },
167-
[
168-
ClientRequest.Method.initialize,
169-
]
170-
)
171-
172-
XCTAssertEqual(
173-
connection.clientNotifications.map { $0.method },
174-
[
175-
ClientNotification.Method.initialized,
176-
ClientNotification.Method.textDocumentDidOpen,
177-
ClientNotification.Method.textDocumentDidClose
178-
]
182+
await waitForClientState(
183+
(
184+
[.initialize],
185+
[.initialized, .textDocumentDidOpen, .textDocumentDidClose]
186+
),
187+
connection: connection,
188+
description: "Post-close event count"
179189
)
180190
}
181191

192+
// MARK: - Test Document Edit
193+
182194
/// Assert the changed contents received by the buffered connection
183195
func assertExpectedContentChanges(connection: BufferingServerConnection, changes: [String]) {
184196
var foundChangeContents: [String] = []
185197

186198
for notification in connection.clientNotifications {
187199
switch notification {
188200
case let .textDocumentDidChange(params):
189-
foundChangeContents.append(contentsOf: params.contentChanges.map { event in
190-
event.text
191-
})
201+
foundChangeContents.append(contentsOf: params.contentChanges.map(\.text))
192202
default:
193203
continue
194204
}
@@ -233,18 +243,17 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase {
233243
textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World")
234244

235245
// Added one notification
236-
await waitForClientEventCount(4, connection: connection, description: "Edited notification count")
246+
await waitForClientState(
247+
(
248+
[.initialize],
249+
[.initialized, .textDocumentDidOpen, .textDocumentDidChange]
250+
),
251+
connection: connection,
252+
description: "Edited notification count"
253+
)
237254

238255
// Make sure our text view is intact
239256
XCTAssertEqual(textView.string, #"func testFunction() -> String { "Hello World" }"#)
240-
XCTAssertEqual(
241-
[
242-
ClientNotification.Method.initialized,
243-
ClientNotification.Method.textDocumentDidOpen,
244-
ClientNotification.Method.textDocumentDidChange
245-
],
246-
connection.clientNotifications.map { $0.method }
247-
)
248257

249258
// Expect only one change due to throttling.
250259
assertExpectedContentChanges(
@@ -291,18 +300,17 @@ final class LanguageServerCodeFileDocumentTests: XCTestCase {
291300
textView.replaceCharacters(in: NSRange(location: 39, length: 0), with: "World")
292301

293302
// Throttling means we should receive one edited notification + init notification + didOpen + init request
294-
await waitForClientEventCount(4, connection: connection, description: "Edited notification count")
303+
await waitForClientState(
304+
(
305+
[.initialize],
306+
[.initialized, .textDocumentDidOpen, .textDocumentDidChange]
307+
),
308+
connection: connection,
309+
description: "Edited notification count"
310+
)
295311

296312
// Make sure our text view is intact
297313
XCTAssertEqual(textView.string, #"func testFunction() -> String { "Hello World" }"#)
298-
XCTAssertEqual(
299-
[
300-
ClientNotification.Method.initialized,
301-
ClientNotification.Method.textDocumentDidOpen,
302-
ClientNotification.Method.textDocumentDidChange
303-
],
304-
connection.clientNotifications.map { $0.method }
305-
)
306314

307315
// Expect three content changes.
308316
assertExpectedContentChanges(

0 commit comments

Comments
 (0)