diff --git a/.gitignore b/.gitignore index 7b8b548..02b285f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,90 +1,3 @@ -# Xcode -# -# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore - -## User settings -xcuserdata/ - -## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) -*.xcscmblueprint -*.xccheckout - -## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) -build/ -DerivedData/ -*.moved-aside -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 - -## Obj-C/Swift specific -*.hmap - -## App packaging -*.ipa -*.dSYM.zip -*.dSYM - -## Playgrounds -timeline.xctimeline -playground.xcworkspace - -# Swift Package Manager -# -# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. -# Packages/ -# Package.pins -Package.resolved -# *.xcodeproj -# -# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata -# hence it is not needed unless you have added a package configuration file to your project -# .swiftpm - +.swiftpm/ .build/ - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - -# fastlane -# -# It is recommended to not store the screenshots in the git repo. -# Instead, use fastlane to re-generate the screenshots whenever they are needed. -# For more information about the recommended setup visit: -# https://docs.fastlane.tools/best-practices/source-control/#source-control - -fastlane/report.xml -fastlane/Preview.html -fastlane/screenshots/**/*.png -fastlane/test_output - -# Code Injection -# -# After new code Injection tools there's a generated folder /iOSInjectionProject -# https://github.com/johnno1962/injectionforxcode - -iOSInjectionProject/ +Package.resolved \ No newline at end of file diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Package.swift b/Package.swift index 4a2ef89..f3a0b7d 100644 --- a/Package.swift +++ b/Package.swift @@ -11,13 +11,14 @@ let package = Package( targets: ["SwiftLSP"]), ], dependencies: [ +// .package(path: "../FoundationToolz"), .package( url: "https://github.com/flowtoolz/FoundationToolz.git", - exact: "0.1.1" + exact: "0.4.1" ), .package( url: "https://github.com/flowtoolz/SwiftyToolz.git", - exact: "0.1.1" + exact: "0.5.1" ) ], targets: [ diff --git a/README.md b/README.md index 51a8de3..cc4835e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SwiftLSP -[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftLSP%2Fbadge%3Ftype%3Dswift-versions&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftLSP) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftLSP%2Fbadge%3Ftype%3Dplatforms&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftLSP) [![](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat-square)](LICENSE) +[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftLSP%2Fbadge%3Ftype%3Dswift-versions&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftLSP)  [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fcodeface-io%2FSwiftLSP%2Fbadge%3Ftype%3Dplatforms&style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftLSP)  [![](https://img.shields.io/badge/Documentation-DocC-blue.svg?style=flat-square)](https://swiftpackageindex.com/codeface-io/SwiftLSP/documentation)  [![](https://img.shields.io/badge/License-MIT-lightgrey.svg?style=flat-square)](LICENSE) 👩🏻‍🚀 *This project [is still a tad experimental](#development-status). Contributors and pioneers welcome!* diff --git a/Sources/LSP.CodebaseLocation.swift b/Sources/LSP.CodebaseLocation.swift new file mode 100644 index 0000000..eaab953 --- /dev/null +++ b/Sources/LSP.CodebaseLocation.swift @@ -0,0 +1,22 @@ +import Foundation + +public extension LSP { + + /** + Demarcates a codebase in the file system: by location, language and source file types + */ + struct CodebaseLocation: Codable, Equatable, Sendable { + + public init(folder: URL, + languageName: String, + codeFileEndings: [String]) { + self.folder = folder + self.languageName = languageName + self.codeFileEndings = codeFileEndings + } + + public var folder: URL + public let languageName: String + public let codeFileEndings: [String] + } +} diff --git a/Sources/Message/LSP.Message.swift b/Sources/Message/LSP.Message.swift index 685781f..0579e89 100644 --- a/Sources/Message/LSP.Message.swift +++ b/Sources/Message/LSP.Message.swift @@ -13,7 +13,7 @@ extension LSP See */ - public enum Message: Equatable + public enum Message: Equatable, Sendable { case response(Response) case request(Request) @@ -26,7 +26,7 @@ extension LSP See */ - public struct Response: Equatable + public struct Response: Equatable, Sendable { public init(id: NullableID, result: Result) { @@ -43,6 +43,13 @@ extension LSP public struct ErrorResult: Error, Equatable { + public init(code: Int, message: String, data: JSON? = nil) + { + self.code = code + self.message = message + self.data = data + } + public let code: Int public let message: String public let data: JSON? @@ -50,7 +57,7 @@ extension LSP } /// An LSP message ID that can also be null - public enum NullableID: Equatable + public enum NullableID: Equatable, Sendable { case value(ID), null } @@ -60,9 +67,9 @@ extension LSP See */ - public struct Request: Equatable + public struct Request: Equatable, Sendable { - public init(id: ID = ID(), method: String, params: JSON.Container?) + public init(id: ID = ID(), method: String, params: JSON.Container? = nil) { self.id = id self.method = method @@ -75,7 +82,7 @@ extension LSP } /// A basic LSP message ID is either a string or an integer - public enum ID: Hashable + public enum ID: Hashable, Sendable { public init() { self = .string(UUID().uuidString) } @@ -87,9 +94,9 @@ extension LSP See */ - public struct Notification: Equatable + public struct Notification: Equatable, Sendable { - public init(method: String, params: JSON.Container?) + public init(method: String, params: JSON.Container? = nil) { self.method = method self.params = params diff --git a/Sources/Message/LSP.Notification+LogMessage.swift b/Sources/Message/LSP.Notification+LogMessage.swift new file mode 100644 index 0000000..0afb565 --- /dev/null +++ b/Sources/Message/LSP.Notification+LogMessage.swift @@ -0,0 +1,41 @@ +import SwiftyToolz + +public extension LSP.Notification { + + var logMessageParameters: LSP.LogMessageParams? { + method == "window/logMessage" ? try? params?.json().decode() : nil + } +} + +public extension LSP.LogMessageParams { + + var logLevel: Log.Level { + switch type { + case 1: return .error + case 2: return .warning + case 3: return .info + case 4: return .verbose + default: + log(error: "Unknown \(Self.self) message type code: \(type). See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageType") + return .info + } + } +} + +public extension LSP { + + /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#logMessageParams + struct LogMessageParams: Codable { + /** + The message type + + + */ + public let type: Int // MessageType; + + /** + * The actual message + */ + public let message: String + } +} diff --git a/Sources/Packet/LSP.Packet.swift b/Sources/Packet/LSP.Packet.swift index 7dbedc7..9bfd315 100644 --- a/Sources/Packet/LSP.Packet.swift +++ b/Sources/Packet/LSP.Packet.swift @@ -8,7 +8,7 @@ public extension LSP See how [the LSP specifies its "Base Protocol"](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol). */ - struct Packet: Equatable + struct Packet: Equatable, Sendable { /** Detects a ``LSP/Packet`` that starts at the beginning of a `Data` instance @@ -96,7 +96,7 @@ public extension LSP guard let separatorIndex = indexOfSeparator(in: data) else { - log(warning: "Data contains no header/content separator:\n\(data.utf8String!)") + log(verbose: "Data (\(data.count) Byte) contains no header/content separator:\n\(data.utf8String!) (yet)") return nil } diff --git a/Sources/Server Communication/LSP.LanguageIdentifier.swift b/Sources/Server Communication/LSP.LanguageIdentifier.swift index d03e852..8f7a73a 100644 --- a/Sources/Server Communication/LSP.LanguageIdentifier.swift +++ b/Sources/Server Communication/LSP.LanguageIdentifier.swift @@ -5,7 +5,7 @@ public extension LSP See [the corresponding specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) */ - struct LanguageIdentifier + struct LanguageIdentifier: Sendable { /** Create an LSP-conform language identifier from a language name diff --git a/Sources/Server Communication/LSP.ServerCommunicationHandler.swift b/Sources/Server Communication/LSP.ServerCommunicationHandler.swift index 390ec5f..8d13e82 100644 --- a/Sources/Server Communication/LSP.ServerCommunicationHandler.swift +++ b/Sources/Server Communication/LSP.ServerCommunicationHandler.swift @@ -17,12 +17,28 @@ extension LSP.ServerCommunicationHandler */ public func request(_ request: LSP.Request) async throws -> Value { - try await self.request(request).decode() + let resultJSON = try await self.request(request) + + guard resultJSON != .null else + { + throw "Cannot interpret JSON as \(Value.self). If \(Value.self) is an optional type, you must use function requestOptional(...) instead of request(...)" + } + + return try resultJSON.decode() + } + + // TODO: can we rather "overload" the plain `request(...)` func properly so it covers the optional and non-optional code without ambiguity? + public func requestOptional(_ request: LSP.Request) async throws -> Value? + { + let resultJSON = try await self.request(request) + guard resultJSON != .null else { return nil } + return try resultJSON.decode() } } extension LSP { + // TODO: should this be named client??? ... the client sends request via its server connection to the server ... public typealias Server = ServerCommunicationHandler /// An actor for easy communication with an LSP server via an ``LSPServerConnection`` @@ -110,27 +126,25 @@ extension LSP */ public func request(_ request: Request) async throws -> JSON { - async let json: JSON = withCheckedThrowingContinuation + try await withCheckedThrowingContinuation { continuation in Task { - [weak self] in await self?.save(continuation, for: request.id) + save(continuation, for: request.id) + + do + { + try await connection.sendToServer(.request(request)) + } + catch + { + removeContinuation(for: request.id) + continuation.resume(throwing: error) + } } } - - do - { - try await connection.sendToServer(.request(request)) - } - catch - { - removeContinuation(for: request.id) - throw error - } - - return try await json } private func serverDidSend(_ response: Response) async @@ -147,7 +161,13 @@ extension LSP switch response.result { case .success(let jsonResult): + if jsonResult == .null + { + log(verbose: "Received valid LSP response message with result property of ") + } + continuation.resume(returning: jsonResult) + case .failure(let errorResult): // TODO: ensure clients actually try to cast thrown errors to LSP.ErrorResult continuation.resume(throwing: errorResult) diff --git a/Sources/Server Communication/LSP.ServerExecutable.swift b/Sources/Server Communication/LSP.ServerExecutable.swift index 062deda..1403973 100644 --- a/Sources/Server Communication/LSP.ServerExecutable.swift +++ b/Sources/Server Communication/LSP.ServerExecutable.swift @@ -32,4 +32,14 @@ public extension LSP { } } +public extension Executable.Configuration +{ + static var sourceKitLSP: Executable.Configuration + { + .init(path: "/usr/bin/xcrun", + arguments: ["sourcekit-lsp"], + environment: ["SOURCEKIT_LOGGING": "0"]) + } +} + #endif diff --git a/Sources/Use Cases/Basic LSP Types/LSPLocation.swift b/Sources/Use Cases/Basic LSP Types/LSPLocation.swift index e88e602..ca54864 100644 --- a/Sources/Use Cases/Basic LSP Types/LSPLocation.swift +++ b/Sources/Use Cases/Basic LSP Types/LSPLocation.swift @@ -1,7 +1,7 @@ /** https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#location */ -public struct LSPLocation: Codable, Equatable +public struct LSPLocation: Codable, Equatable, Sendable { public let uri: LSPDocumentUri public let range: LSPRange @@ -33,8 +33,14 @@ public extension LSPRange } } -public struct LSPRange: Codable, Equatable +public struct LSPRange: Codable, Equatable, Sendable { + public init(start: LSPPosition, end: LSPPosition) + { + self.start = start + self.end = end + } + /** * The range's start position. */ @@ -46,8 +52,14 @@ public struct LSPRange: Codable, Equatable public let end: LSPPosition } -public struct LSPPosition: Codable, Equatable +public struct LSPPosition: Codable, Equatable, Sendable { + public init(line: Int, character: Int) + { + self.line = line + self.character = character + } + /** * Line position in a document (zero-based). */ diff --git a/Sources/Use Cases/Basic LSP Types/LSPTextDocumentPositionParams.swift b/Sources/Use Cases/Basic LSP Types/LSPTextDocumentPositionParams.swift index ebb4b39..7781344 100644 --- a/Sources/Use Cases/Basic LSP Types/LSPTextDocumentPositionParams.swift +++ b/Sources/Use Cases/Basic LSP Types/LSPTextDocumentPositionParams.swift @@ -1,7 +1,7 @@ /** https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams */ -public struct LSPTextDocumentPositionParams: Codable +public struct LSPTextDocumentPositionParams: Codable, Sendable { /** * The text document. @@ -14,7 +14,7 @@ public struct LSPTextDocumentPositionParams: Codable public let position: LSPPosition } -public struct LSPTextDocumentIdentifier: Codable +public struct LSPTextDocumentIdentifier: Codable, Sendable { /** * The text document's URI. diff --git a/Sources/Use Cases/Server Life Cycle/LSP.Message.Request+Initialize.swift b/Sources/Use Cases/Server Life Cycle/LSP.Message.Request+Initialize.swift index 69727c8..33cf81f 100644 --- a/Sources/Use Cases/Server Life Cycle/LSP.Message.Request+Initialize.swift +++ b/Sources/Use Cases/Server Life Cycle/LSP.Message.Request+Initialize.swift @@ -6,9 +6,11 @@ public extension LSP.Message.Request { /** https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize + + - Parameter clientProcessID: [We assume](https://github.com/ChimeHQ/ProcessService/pull/3) this is the process ID of the actual LSP **client**, which is not necessarily the same process as the server's parent process that technically launched the LSP server. This is important because the LSP client might interact with the LSP server via intermediate processes like [LSPService](https://github.com/codeface-io/LSPService) or XPC services. You may omit this parameter and SwiftLSP will use the current process's ID. This will virtually always be correct since the LSP client typically creates the initialize request. */ static func initialize(folder: URL, - clientProcessID: Int, + clientProcessID: Int = Int(ProcessInfo.processInfo.processIdentifier), capabilities: JSON = defaultClientCapabilities) -> Self { .init(method: "initialize", diff --git a/Sources/Use Cases/Symbols/LSP.ServerCommunicationHandler+Symbols.swift b/Sources/Use Cases/Symbols/LSP.ServerCommunicationHandler+Symbols.swift index 4e765b4..d10809d 100644 --- a/Sources/Use Cases/Symbols/LSP.ServerCommunicationHandler+Symbols.swift +++ b/Sources/Use Cases/Symbols/LSP.ServerCommunicationHandler+Symbols.swift @@ -1,8 +1,8 @@ extension LSP.ServerCommunicationHandler { /// This just adds the knowledge of what result type the server returns - public func requestSymbols(in document: LSPDocumentUri) async throws -> [LSPDocumentSymbol] + public func requestSymbols(in document: LSPDocumentUri) async throws -> [LSPDocumentSymbol]? { - try await request(.symbols(in: document)) + try await requestOptional(.symbols(in: document)) } } diff --git a/Sources/Use Cases/Symbols/LSPDocumentSymbol.swift b/Sources/Use Cases/Symbols/LSPDocumentSymbol.swift index 4f5c520..43aac41 100644 --- a/Sources/Use Cases/Symbols/LSPDocumentSymbol.swift +++ b/Sources/Use Cases/Symbols/LSPDocumentSymbol.swift @@ -39,13 +39,26 @@ public extension LSPDocumentSymbol.SymbolKind /** https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbol */ -public struct LSPDocumentSymbol: Codable, Equatable +public struct LSPDocumentSymbol: Codable, Equatable, Sendable { + public init(name: String, + kind: Int, + range: LSPRange, + selectionRange: LSPRange, + children: [LSPDocumentSymbol] = []) + { + self.name = name + self.kind = kind + self.range = range + self.selectionRange = selectionRange + self.children = children + } + public let name: String public var decodedKind: SymbolKind? { .init(rawValue: kind) } - public enum SymbolKind: Int, CaseIterable, Codable, Equatable + public enum SymbolKind: Int, CaseIterable, Codable, Equatable, Sendable { case File = 1 case Module = 2 @@ -81,5 +94,5 @@ public struct LSPDocumentSymbol: Codable, Equatable public let selectionRange: LSPRange - public let children: [Self] + public let children: [Self]? } diff --git a/Tests/PublicAPITests.swift b/Tests/PublicAPITests.swift new file mode 100644 index 0000000..9c357dd --- /dev/null +++ b/Tests/PublicAPITests.swift @@ -0,0 +1,13 @@ +import SwiftLSP // Do not use @testable❗️ we wanna test public API here like a real client +import XCTest + +final class PublicAPITests: XCTestCase { + + func testCreatingLSPTypes() { + _ = LSP.Message.Request(method: "just do it!") + _ = LSP.Message.Response(id: .value(.string(.randomID())), + result: .success(.bool(true))) + _ = LSP.Message.Notification(method: "just wanted to say hi") + _ = LSP.ErrorResult(code: 1000, message: "some LSP error occured") + } +} diff --git a/Tests/SwiftLSPTests.swift b/Tests/SwiftLSPTests.swift index 777250b..cf44ca0 100644 --- a/Tests/SwiftLSPTests.swift +++ b/Tests/SwiftLSPTests.swift @@ -1,7 +1,7 @@ -import XCTest @testable import SwiftLSP import FoundationToolz import SwiftyToolz +import XCTest final class SwiftLSPTests: XCTestCase {