├── .gitignore ├── .spi.yml ├── Documentation ├── SwiftLSP.png ├── architecture.graffle ├── architecture_dark.png └── architecture_light.png ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── LSP.CodebaseLocation.swift ├── LSP.swift ├── Message │ ├── LSP.Message+Data.swift │ ├── LSP.Message+JSON.swift │ ├── LSP.Message+ReadableDescription.swift │ ├── LSP.Message.swift │ └── LSP.Notification+LogMessage.swift ├── Packet │ ├── LSP.Packet+Message.swift │ ├── LSP.Packet.swift │ └── LSP.PacketDetector.swift ├── Server Communication │ ├── LSP.LanguageIdentifier.swift │ ├── LSP.ServerCommunicationHandler.swift │ ├── LSP.ServerExecutable.swift │ ├── LSP.WebSocketConnection.swift │ └── LSPServerConnection.swift └── Use Cases │ ├── Basic LSP Types │ ├── LSPLocation.swift │ └── LSPTextDocumentPositionParams.swift │ ├── Document Sync │ ├── LSP.Message.Notification+DocumentSync.swift │ └── LSP.ServerCommunicationHandler+DocumentSync.swift │ ├── References │ ├── LSP.Message.Request+References.swift │ └── LSP.ServerCommunicationHandler+References.swift │ ├── Server Life Cycle │ ├── LSP.Message.Notification+Initialized.swift │ └── LSP.Message.Request+Initialize.swift │ └── Symbols │ ├── LSP.Message.Request+Symbols.swift │ ├── LSP.ServerCommunicationHandler+Symbols.swift │ └── LSPDocumentSymbol.swift └── Tests ├── PublicAPITests.swift └── SwiftLSPTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .swiftpm/ 2 | .build/ 3 | Package.resolved -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftLSP] 5 | -------------------------------------------------------------------------------- /Documentation/SwiftLSP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftLSP/e156fc27cfb214c9cff79a5770542bd636a45102/Documentation/SwiftLSP.png -------------------------------------------------------------------------------- /Documentation/architecture.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftLSP/e156fc27cfb214c9cff79a5770542bd636a45102/Documentation/architecture.graffle -------------------------------------------------------------------------------- /Documentation/architecture_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftLSP/e156fc27cfb214c9cff79a5770542bd636a45102/Documentation/architecture_dark.png -------------------------------------------------------------------------------- /Documentation/architecture_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codeface-io/SwiftLSP/e156fc27cfb214c9cff79a5770542bd636a45102/Documentation/architecture_light.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sebastian Fichtner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.6 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "SwiftLSP", 7 | platforms: [.iOS(.v13), .tvOS(.v13), .macOS(.v10_15), .watchOS(.v6)], 8 | products: [ 9 | .library( 10 | name: "SwiftLSP", 11 | targets: ["SwiftLSP"]), 12 | ], 13 | dependencies: [ 14 | // .package(path: "../FoundationToolz"), 15 | .package( 16 | url: "https://github.com/flowtoolz/FoundationToolz.git", 17 | exact: "0.4.1" 18 | ), 19 | .package( 20 | url: "https://github.com/flowtoolz/SwiftyToolz.git", 21 | exact: "0.5.1" 22 | ) 23 | ], 24 | targets: [ 25 | .target( 26 | name: "SwiftLSP", 27 | dependencies: ["FoundationToolz", "SwiftyToolz"], 28 | path: "Sources" 29 | ), 30 | .testTarget( 31 | name: "SwiftLSPTests", 32 | dependencies: ["SwiftLSP", "FoundationToolz", "SwiftyToolz"], 33 | path: "Tests" 34 | ), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftLSP 2 | 3 | [![](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) 4 | 5 | 👩🏻‍🚀 *This project [is still a tad experimental](#development-status). Contributors and pioneers welcome!* 6 | 7 | ## What? 8 | 9 | SwiftLSP offers a quite dynamic Swift representation of the [LSP (Language Server Protocol)](https://microsoft.github.io/language-server-protocol) and helps with many related use cases. It is foundational for [LSPService](https://github.com/codeface-io/LSPService) and [LSPServiceKit](https://github.com/codeface-io/LSPServiceKit). 10 | 11 | Since the LSP standard defines a complex amorphous multitude of valid JSON objects, it doesn't exactly lend itself to being represented as a strict type system that would mirror the standard down to every permutation and property. So SwiftLSP is strictly typed at the higher level of LSP messages but falls back onto a more dynamic and flexible JSON representation for the details. The strict typing can easily be expanded on client demand. 12 | 13 | ## How? 14 | 15 | Some of these examples build upon preceding ones, so it's best to read them from the beginning. 16 | 17 | ### Create Messages 18 | 19 | ```swift 20 | let myRequest = LSP.Request(method: "myMethod", params: nil) 21 | let myRequestMessage = LSP.Message.request(myRequest) 22 | 23 | let myNotification = LSP.Notification(method: "myMethod", params: nil) 24 | let myNotificationMessage = LSP.Message.notification(myNotification) 25 | ``` 26 | 27 | ### Encode and Decode Messages 28 | 29 | SwiftLSP encodes LSP messages with the [LSP-conform JSON-RPC encoding](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#abstractMessage). 30 | 31 | ```swift 32 | let myRequestMessageEncoded = try myRequestMessage.encode() // Data 33 | let myRequestMessageDecoded = try LSP.Message(myRequestMessageEncoded) 34 | ``` 35 | 36 | ### Wrap Messages in Packets 37 | 38 | To send LSP messages via data channels, the standard defines how to [wrap each message](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol) in what we call an `LSP.Packet`, which holds the `Data` of its `header`- and `content` part. 39 | 40 | ```swift 41 | let myRequestMessagePacket = try LSP.Packet(myRequestMessage) 42 | let packetHeader = myRequestMessagePacket.header // Data 43 | let packetContent = myRequestMessagePacket.content // Data 44 | let packetTotalData = myRequestMessagePacket.data // Data 45 | ``` 46 | 47 | ### Extract Messages From Packets 48 | 49 | ```swift 50 | let myRequestMessageUnpacked = try myRequestMessagePacket.message() // LSP.Message 51 | ``` 52 | 53 | ### Extract Packets From Data 54 | 55 | A client talking to an LSP server might need to extract `LSP.Packet`s from the server's output `Data` stream. 56 | 57 | SwiftLSP can parse an `LSP.Packet` from the beginning of a `Data` instance: 58 | 59 | ```swift 60 | let dataStartingWithPacket = packetTotalData + "Some other data".data(using: .utf8)! 61 | let detectedPacket = try LSP.Packet(parsingPrefixOf: dataStartingWithPacket) 62 | 63 | // now detectedPacket == myRequestMessagePacket 64 | ``` 65 | 66 | SwiftLSP also offers the `LSP.PacketDetector` for parsing a stream of `Data` incrementally: 67 | 68 | ```swift 69 | var streamedPacket: LSP.Packet? = nil 70 | 71 | let detector = LSP.PacketDetector { packet in 72 | streamedPacket = packet 73 | } 74 | 75 | for byte in dataStartingWithPacket { 76 | detector.read(byte) 77 | } 78 | 79 | // now streamedPacket == myRequestMessagePacket 80 | ``` 81 | 82 | ## More Use Cases 83 | 84 | Beyond what the examples above have touched, SwiftLSP also helps with: 85 | 86 | * Creating messages for specific use cases (initialize server, request symbols, request references ...) 87 | * Launching an LSP server executable 88 | * Matching response messages to request messages 89 | * Making requests to an LSP Server through `async` functions 90 | * Using an LSP Server via WebSocket 91 | 92 | ## Architecture 93 | 94 | Some context and essential types: 95 | 96 | ![architecture](Documentation/architecture_dark.png#gh-dark-mode-only) 97 | ![architecture](Documentation/architecture_light.png#gh-light-mode-only) 98 | 99 | Internal architecture (composition and essential dependencies) of the top-level source folder: 100 | 101 | ![](Documentation/SwiftLSP.png) 102 | 103 | The above image was generated with [Codeface](https://codeface.io). 104 | 105 | ## Development Status 106 | 107 | From version/tag 0.1.0 on, SwiftLSP adheres to [semantic versioning](https://semver.org). So until it has reached 1.0.0, its API may still break frequently, but this will be expressed in version bumps. 108 | 109 | SwiftLSP is already being used in production, but [Codeface](https://codeface.io) is still its primary client. SwiftLSP will move to version 1.0.0 as soon as its basic practicality and conceptual soundness have been validated by serving multiple real-world clients. -------------------------------------------------------------------------------- /Sources/LSP.CodebaseLocation.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension LSP { 4 | 5 | /** 6 | Demarcates a codebase in the file system: by location, language and source file types 7 | */ 8 | struct CodebaseLocation: Codable, Equatable, Sendable { 9 | 10 | public init(folder: URL, 11 | languageName: String, 12 | codeFileEndings: [String]) { 13 | self.folder = folder 14 | self.languageName = languageName 15 | self.codeFileEndings = codeFileEndings 16 | } 17 | 18 | public var folder: URL 19 | public let languageName: String 20 | public let codeFileEndings: [String] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/LSP.swift: -------------------------------------------------------------------------------- 1 | /// The basic namespace containing all of SwiftLSP 2 | public enum LSP {} 3 | -------------------------------------------------------------------------------- /Sources/Message/LSP.Message+Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftyToolz 3 | 4 | extension LSP.Message 5 | { 6 | /// Create a ``LSP/Message`` from a valid LSP `JSON` encoding 7 | /// - Parameter data: JSON encoded LSP message. See 8 | public init(_ data: Data) throws 9 | { 10 | self = try Self(JSON(data)) 11 | } 12 | 13 | /// Create a valid LSP `JSON` encoding of the message 14 | /// - Returns: Valid LSP `JSON` encoding. See 15 | public func encode() throws -> Data 16 | { 17 | try json().encode() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Message/LSP.Message+JSON.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | extension LSP.Message 4 | { 5 | /** 6 | Create a ``LSP/Message`` from `JSON` 7 | 8 | Throws an error if the JSON does not form a valid LSP message according to the LSP specification. 9 | 10 | See 11 | */ 12 | public init(_ messageJSON: JSON) throws 13 | { 14 | // TODO: this func is super critical and should be covered by multiple unit tests ensuring it throws errors exactly when the JSON does not comply to the LSP specification 15 | 16 | guard let nullableID = Self.getNullableID(fromMessage: messageJSON) else 17 | { 18 | // if it has no id, it must be a notification 19 | self = try .notification(.init(method: messageJSON.string("method"), 20 | params: Self.params(fromMessageJSON: messageJSON))) 21 | return 22 | } 23 | 24 | // it's not a notification. if it has result or error, it's a response 25 | 26 | if let result = messageJSON.result // success response 27 | { 28 | guard messageJSON.method == nil else 29 | { 30 | throw "LSP message JSON has an id, a result and a method, so the message type is unclear." 31 | } 32 | 33 | self = .response(.init(id: nullableID, result: .success(result))) 34 | } 35 | else if let error = messageJSON.error // error response 36 | { 37 | guard messageJSON.method == nil else 38 | { 39 | throw "LSP message JSON has an id, an error and a method, so the message type is unclear." 40 | } 41 | 42 | self = .response(.init(id: nullableID, 43 | result: .failure(try .init(error)))) 44 | } 45 | else // request 46 | { 47 | guard case .value(let id) = nullableID else 48 | { 49 | throw "Invalid LSP message JSON: It contains neither an error nor a result (so it's not a response) and has a id (so it's neither a notification nor a request)." 50 | } 51 | 52 | self = try .request(.init(id: id, 53 | method: messageJSON.string("method"), 54 | params: try Self.params(fromMessageJSON: messageJSON))) 55 | } 56 | } 57 | 58 | private static func params(fromMessageJSON messageJSON: JSON) throws -> JSON.Container? 59 | { 60 | guard let paramsJSON = messageJSON.params else { return nil } 61 | return try .init(paramsJSON) 62 | } 63 | 64 | private static func getNullableID(fromMessage messageJSON: JSON) -> NullableID? 65 | { 66 | guard let idJSON = messageJSON.id else { return nil } 67 | 68 | switch idJSON 69 | { 70 | case .null: return .null 71 | case .int(let int): return .value(.int(int)) 72 | case .string(let string): return .value(.string(string)) 73 | default: return nil 74 | } 75 | } 76 | 77 | /** 78 | Create a valid LSP JSON of the message 79 | 80 | See 81 | */ 82 | public func json() -> JSON 83 | { 84 | .object(["jsonrpc": JSON.string("2.0")] + caseJSONDictionary()) 85 | } 86 | 87 | internal func caseJSONDictionary() -> [String: JSON] 88 | { 89 | switch self 90 | { 91 | case .request(let request): return request.jsonDictionary() 92 | case .response(let response): return response.jsonDictionary() 93 | case .notification(let notification): return notification.jsonDictionary() 94 | } 95 | } 96 | } 97 | 98 | extension LSP.Message.Request 99 | { 100 | func jsonDictionary() -> [String : JSON] 101 | { 102 | var dictionary = [ "id": id.json, "method": .string(method) ] 103 | dictionary["params"] = params?.json() 104 | return dictionary 105 | } 106 | } 107 | 108 | extension LSP.Message.Response 109 | { 110 | func jsonDictionary() -> [String : JSON] 111 | { 112 | var dictionary = ["id": id.json] 113 | 114 | switch result 115 | { 116 | case .success(let jsonResult): dictionary["result"] = jsonResult 117 | case .failure(let errorResult): dictionary["error"] = errorResult.json() 118 | } 119 | 120 | return dictionary 121 | } 122 | } 123 | 124 | extension LSP.Message.Notification 125 | { 126 | func jsonDictionary() -> [String : JSON] 127 | { 128 | var dictionary = ["method": JSON.string(method)] 129 | dictionary["params"] = params?.json() 130 | return dictionary 131 | } 132 | } 133 | 134 | extension LSP.ErrorResult 135 | { 136 | init(_ json: JSON) throws 137 | { 138 | self.code = try json.int("code") 139 | self.message = try json.string("message") 140 | data = json.data 141 | } 142 | 143 | func json() -> JSON 144 | { 145 | var dictionary: [String: JSON] = 146 | [ 147 | "code": .int(code), 148 | "message": .string(message) 149 | ] 150 | 151 | dictionary["data"] = data 152 | 153 | return .object(dictionary) 154 | } 155 | } 156 | 157 | extension LSP.Message.NullableID 158 | { 159 | var json: JSON 160 | { 161 | switch self 162 | { 163 | case .value(let id): return id.json 164 | case .null: return .null 165 | } 166 | } 167 | } 168 | 169 | extension LSP.Message.ID 170 | { 171 | var json: JSON 172 | { 173 | switch self 174 | { 175 | case .string(let string): return .string(string) 176 | case .int(let int): return .int(int) 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /Sources/Message/LSP.Message+ReadableDescription.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftyToolz 3 | 4 | // MARK: - Readable Error 5 | 6 | extension LSP.ErrorResult: ReadableErrorConvertible 7 | { 8 | public var readableErrorMessage: String { description } 9 | } 10 | 11 | // MARK: - CustomStringConvertible 12 | 13 | extension LSP.Message: CustomStringConvertible 14 | { 15 | public var description: String 16 | { 17 | json().description 18 | } 19 | } 20 | 21 | extension LSP.ErrorResult: CustomStringConvertible 22 | { 23 | public var description: String 24 | { 25 | var errorString = "LSP Error: \(message) (code \(code))" 26 | data.forSome { errorString += " data:\n\($0)" } 27 | return errorString 28 | } 29 | } 30 | 31 | extension LSP.Message.NullableID: CustomStringConvertible 32 | { 33 | public var description: String 34 | { 35 | switch self 36 | { 37 | case .value(let id): return id.description 38 | case .null: return NSNull().description 39 | } 40 | } 41 | } 42 | 43 | extension LSP.Message.ID: CustomStringConvertible 44 | { 45 | public var description: String 46 | { 47 | switch self 48 | { 49 | case .string(let string): return string.description 50 | case .int(let int): return int.description 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Message/LSP.Message.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftyToolz 3 | 4 | extension LSP 5 | { 6 | public typealias Request = Message.Request 7 | public typealias Response = Message.Response 8 | public typealias ErrorResult = Message.Response.ErrorResult 9 | public typealias Notification = Message.Notification 10 | 11 | /** 12 | An LSP Message is either a request, a response or a notification. 13 | 14 | See 15 | */ 16 | public enum Message: Equatable, Sendable 17 | { 18 | case response(Response) 19 | case request(Request) 20 | case notification(Notification) 21 | 22 | /** 23 | An LSP response message sent from an LSP server t a client in response to an LSP request message. 24 | 25 | Its `id` should match the `id` of the corresponding ``Request`` that triggered this response. 26 | 27 | See 28 | */ 29 | public struct Response: Equatable, Sendable 30 | { 31 | public init(id: NullableID, result: Result) 32 | { 33 | self.id = id 34 | self.result = result 35 | } 36 | 37 | public let id: NullableID 38 | 39 | /** 40 | Here are 2 minor deviations from the LSP specification: According to LSP 1) a result value can NOT be an array and 2) when an error is returned, there COULD still also be a result 41 | */ 42 | public let result: Result 43 | 44 | public struct ErrorResult: Error, Equatable 45 | { 46 | public init(code: Int, message: String, data: JSON? = nil) 47 | { 48 | self.code = code 49 | self.message = message 50 | self.data = data 51 | } 52 | 53 | public let code: Int 54 | public let message: String 55 | public let data: JSON? 56 | } 57 | } 58 | 59 | /// An LSP message ID that can also be null 60 | public enum NullableID: Equatable, Sendable 61 | { 62 | case value(ID), null 63 | } 64 | 65 | /** 66 | An LSP request message sent from a client to an LSPServer 67 | 68 | See 69 | */ 70 | public struct Request: Equatable, Sendable 71 | { 72 | public init(id: ID = ID(), method: String, params: JSON.Container? = nil) 73 | { 74 | self.id = id 75 | self.method = method 76 | self.params = params 77 | } 78 | 79 | public let id: ID 80 | public let method: String 81 | public let params: JSON.Container? 82 | } 83 | 84 | /// A basic LSP message ID is either a string or an integer 85 | public enum ID: Hashable, Sendable 86 | { 87 | public init() { self = .string(UUID().uuidString) } 88 | 89 | case string(String), int(Int) 90 | } 91 | 92 | /** 93 | An LSP notification message is sent between an LSPServer and its client 94 | 95 | See 96 | */ 97 | public struct Notification: Equatable, Sendable 98 | { 99 | public init(method: String, params: JSON.Container? = nil) 100 | { 101 | self.method = method 102 | self.params = params 103 | } 104 | 105 | public let method: String 106 | public let params: JSON.Container? 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Message/LSP.Notification+LogMessage.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension LSP.Notification { 4 | 5 | var logMessageParameters: LSP.LogMessageParams? { 6 | method == "window/logMessage" ? try? params?.json().decode() : nil 7 | } 8 | } 9 | 10 | public extension LSP.LogMessageParams { 11 | 12 | var logLevel: Log.Level { 13 | switch type { 14 | case 1: return .error 15 | case 2: return .warning 16 | case 3: return .info 17 | case 4: return .verbose 18 | default: 19 | log(error: "Unknown \(Self.self) message type code: \(type). See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#messageType") 20 | return .info 21 | } 22 | } 23 | } 24 | 25 | public extension LSP { 26 | 27 | /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#logMessageParams 28 | struct LogMessageParams: Codable { 29 | /** 30 | The message type 31 | 32 | 33 | */ 34 | public let type: Int // MessageType; 35 | 36 | /** 37 | * The actual message 38 | */ 39 | public let message: String 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Packet/LSP.Packet+Message.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension LSP.Packet 4 | { 5 | /** 6 | Creates a ``LSP/Packet`` that wraps an encoded ``LSP/Message`` for data transport 7 | 8 | - Parameter message: The LSP message to encode for data transport 9 | */ 10 | init(_ message: LSP.Message) throws 11 | { 12 | try self.init(withContent: message.encode()) 13 | } 14 | 15 | /** 16 | Extracts the ``LSP/Message`` encoded in the packet's `content` part 17 | 18 | See the [LSP content part specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#contentPart). 19 | 20 | - Returns: The ``LSP/Message`` encoded in the packet's `content` part 21 | */ 22 | func message() throws -> LSP.Message 23 | { 24 | try LSP.Message(content) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Packet/LSP.Packet.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftyToolz 3 | 4 | public extension LSP 5 | { 6 | /** 7 | Wraps a ``LSP/Message`` on the data level and corresponds to the LSP "Base Protocol" 8 | 9 | See how [the LSP specifies its "Base Protocol"](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol). 10 | */ 11 | struct Packet: Equatable, Sendable 12 | { 13 | /** 14 | Detects a ``LSP/Packet`` that starts at the beginning of a `Data` instance 15 | 16 | `LSP.Packet` wraps an LSP message on the level of data / data streams and corresponds to the LSP "Base Protocol". See how [the LSP specifies its "Base Protocol"](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol). 17 | - Parameter data: Data that presumably starts with an LSP-conform endoded `LSP.Packet` 18 | */ 19 | public init(parsingPrefixOf data: Data) throws 20 | { 21 | (header, content) = try Parser.parseHeaderAndContent(fromPrefixOf: data) 22 | } 23 | 24 | /** 25 | Make a ``LSP/Packet`` from the given packet content data 26 | 27 | Throws an error if the given content data is not an LSP-conform encoding of a packet's content part. See the [LSP content part specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#contentPart). 28 | 29 | - Parameter content: LSP-conform JSON encoding of an LSP packet's content part 30 | */ 31 | public init(withContent content: Data) throws 32 | { 33 | try Parser.verify(content: content) 34 | self.header = "Content-Length: \(content.count)".data! 35 | self.content = content 36 | } 37 | 38 | /// The LSP-conform encoding of the whole packet 39 | /// 40 | /// See how [the LSP specifies its "Base Protocol"](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol). 41 | public var data: Data { header + separator + content } 42 | 43 | /// The length of the whole packet data in Bytes 44 | public var length: Int { header.count + separator.count + content.count } 45 | 46 | /// The LSP-conform encoding of the packet's header part 47 | /// 48 | /// See the [LSP content part specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#headerPart). 49 | public let header: Data 50 | 51 | /// The LSP-conform encoding of the packet's header/content separator 52 | /// 53 | /// See how [the LSP specifies its "Base Protocol"](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseProtocol). 54 | public var separator: Data { Parser.separator } 55 | 56 | /// The LSP-conform encoding of the packet's content part 57 | /// 58 | /// See the [LSP content part specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#contentPart). 59 | public let content: Data 60 | 61 | private enum Parser 62 | { 63 | static func parseHeaderAndContent(fromPrefixOf data: Data) throws -> (Data, Data) 64 | { 65 | guard !data.isEmpty else { throw "Data is empty" } 66 | 67 | guard let header = Parser.header(fromBeginningOf: data) else 68 | { 69 | throw "Data doesn't start with header:\n\(data.utf8String!)" 70 | } 71 | 72 | guard let contentLength = Parser.contentLength(fromHeader: header) else 73 | { 74 | throw "Header declares no content length" 75 | } 76 | 77 | let headerPlusSeparatorLength = header.count + separator.count 78 | let packetLength = headerPlusSeparatorLength + contentLength 79 | 80 | guard packetLength <= data.count else { throw "Incomplete packet data" } 81 | 82 | let content = data[headerPlusSeparatorLength ..< packetLength] 83 | try verify(content: content) 84 | 85 | return (header, content) 86 | } 87 | 88 | static func verify(content: Data) throws 89 | { 90 | _ = try Message(content) 91 | } 92 | 93 | private static func header(fromBeginningOf data: Data) -> Data? 94 | { 95 | guard !data.isEmpty else { return nil } 96 | 97 | guard let separatorIndex = indexOfSeparator(in: data) else 98 | { 99 | log(verbose: "Data (\(data.count) Byte) contains no header/content separator:\n\(data.utf8String!) (yet)") 100 | return nil 101 | } 102 | 103 | guard separatorIndex > 0 else 104 | { 105 | log(error: "Empty header") 106 | return nil 107 | } 108 | 109 | return data[0 ..< separatorIndex] 110 | } 111 | 112 | private static func indexOfSeparator(in data: Data) -> Int? 113 | { 114 | guard !data.isEmpty else { return nil } 115 | let lastDataIndex = data.count - 1 116 | let lastPossibleSeparatorIndex = lastDataIndex - (separator.count - 1) 117 | guard lastPossibleSeparatorIndex >= 0 else { return nil } 118 | 119 | for index in 0 ... lastPossibleSeparatorIndex 120 | { 121 | let potentialSeparator = data[index ..< index + separator.count] 122 | if potentialSeparator == separator { return index } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | private static func contentLength(fromHeader header: Data) -> Int? 129 | { 130 | let headerString = header.utf8String! 131 | let headerLines = headerString.components(separatedBy: "\r\n") 132 | 133 | for headerLine in headerLines 134 | { 135 | if headerLine.hasPrefix("Content-Length") 136 | { 137 | guard let lengthString = headerLine.components(separatedBy: ": ").last 138 | else { return nil } 139 | 140 | return Int(lengthString) 141 | } 142 | } 143 | 144 | return nil 145 | } 146 | 147 | fileprivate static let separator = Data([13, 10, 13, 10]) // ascii: "\r\n\r\n" 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/Packet/LSP.PacketDetector.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import Foundation 3 | import SwiftyToolz 4 | 5 | extension LSP 6 | { 7 | /** 8 | Parses ``LSP/Packet``s from `Data` 9 | */ 10 | public class PacketDetector 11 | { 12 | // MARK: - Public API 13 | 14 | /** 15 | Creates a ``LSP/PacketDetector`` with a closure for handling detected ``LSP/Packet``s 16 | */ 17 | public init(_ handleDetectedPacket: @escaping (Packet) -> Void) 18 | { 19 | didDetect = handleDetectedPacket 20 | } 21 | 22 | /** 23 | Reads another Byte from a `Data` stream. Calls the given handler for new ``LSP/Packet``s 24 | 25 | Calls the handler provided via the initializer if the stream contains a new `LSP.Packet` since the last call of the handler 26 | */ 27 | public func read(_ byte: Byte) 28 | { 29 | read(Data([byte])) 30 | } 31 | 32 | /** 33 | Reads another chunk of a `Data` stream. Calls the given handler for new ``LSP/Packet``s 34 | 35 | Calls the handler provided via the initializer once for each `LSP.Packet` in the stream since the last call of the handler 36 | */ 37 | public func read(_ data: Data) 38 | { 39 | buffer += data 40 | 41 | while !buffer.isEmpty, let lspPacket = removeLSPPacketFromBuffer() 42 | { 43 | didDetect(lspPacket) 44 | } 45 | } 46 | 47 | private let didDetect: (Packet) -> Void 48 | 49 | // MARK: - Data Buffer 50 | 51 | private func removeLSPPacketFromBuffer() -> Packet? 52 | { 53 | guard !buffer.isEmpty, 54 | let packet = try? Packet(parsingPrefixOf: buffer) 55 | else { return nil } 56 | 57 | buffer.removeFirst(packet.length) 58 | buffer.resetIndices() 59 | 60 | return packet 61 | } 62 | 63 | private var buffer = Data() 64 | } 65 | } 66 | 67 | extension Data 68 | { 69 | mutating func resetIndices() 70 | { 71 | self = Data(self) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Server Communication/LSP.LanguageIdentifier.swift: -------------------------------------------------------------------------------- 1 | public extension LSP 2 | { 3 | /** 4 | An LSP-conform language identifier created from a language name 5 | 6 | See [the corresponding specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) 7 | */ 8 | struct LanguageIdentifier: Sendable 9 | { 10 | /** 11 | Create an LSP-conform language identifier from a language name 12 | 13 | See [the corresponding specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) 14 | 15 | - Parameter languageName: The (more casual) name of the language 16 | */ 17 | public init(languageName: String) 18 | { 19 | string = Self.string(forLanguageName: languageName) 20 | } 21 | 22 | /** 23 | `String` representation of an LSP language identifier 24 | 25 | See [the corresponding specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) 26 | */ 27 | public let string: String 28 | 29 | private static func string(forLanguageName languageName: String) -> String 30 | { 31 | let lowercasedLanguageName = languageName.lowercased() 32 | return stringByLowercasedLanguageName[lowercasedLanguageName] ?? lowercasedLanguageName 33 | } 34 | 35 | private static let stringByLowercasedLanguageName: [String : String] = [ 36 | "objective-c++": "objective-cpp", 37 | "c++": "cpp", 38 | "c#": "csharp", 39 | "visual basic": "vb" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Server Communication/LSP.ServerCommunicationHandler.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import Foundation 3 | import SwiftyToolz 4 | 5 | extension LSP.ServerCommunicationHandler 6 | { 7 | /** 8 | Requests a value of a generic type from the connected LSP server 9 | 10 | When the LSP server produces an LSP response error (see [its specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError)), the thrown error is a ``LSP/Message/Response/ErrorResult`` a.k.a. `LSP.ErrorResult`. So the caller should check whether thrown errors are indeed `LSP.ErrorResult`s. 11 | 12 | - Parameter request: The LSP request message to send to the LSP server 13 | 14 | - Returns: The value returned from the LSP server when no error occured. The value's type must be generically determined by the caller. 15 | 16 | - Throws: If the LSP server sends a [response error message](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError) in return, the error is a ``LSP/Message/Response/ErrorResult``, otherwise it's any `Error`. 17 | */ 18 | public func request(_ request: LSP.Request) async throws -> Value 19 | { 20 | let resultJSON = try await self.request(request) 21 | 22 | guard resultJSON != .null else 23 | { 24 | throw "Cannot interpret JSON as \(Value.self). If \(Value.self) is an optional type, you must use function requestOptional(...) instead of request(...)" 25 | } 26 | 27 | return try resultJSON.decode() 28 | } 29 | 30 | // TODO: can we rather "overload" the plain `request(...)` func properly so it covers the optional and non-optional code without ambiguity? 31 | public func requestOptional(_ request: LSP.Request) async throws -> Value? 32 | { 33 | let resultJSON = try await self.request(request) 34 | guard resultJSON != .null else { return nil } 35 | return try resultJSON.decode() 36 | } 37 | } 38 | 39 | extension LSP 40 | { 41 | // TODO: should this be named client??? ... the client sends request via its server connection to the server ... 42 | public typealias Server = ServerCommunicationHandler 43 | 44 | /// An actor for easy communication with an LSP server via an ``LSPServerConnection`` 45 | public actor ServerCommunicationHandler 46 | { 47 | // MARK: - Initialize 48 | 49 | /** 50 | Create a ``LSP/ServerCommunicationHandler`` 51 | 52 | - Parameters: 53 | - connection: An ``LSPServerConnection`` for talking to an LSP server 54 | - languageName: The name of the language whose identifier shall be set in requests 55 | */ 56 | public init(connection: LSPServerConnection, languageName: String) 57 | { 58 | self.connection = connection 59 | self.languageIdentifier = .init(languageName: languageName) 60 | 61 | connection.serverDidSendResponse = 62 | { 63 | response in 64 | 65 | Task 66 | { 67 | [weak self] in await self?.serverDidSend(response) 68 | } 69 | } 70 | 71 | connection.serverDidSendNotification = 72 | { 73 | notification in 74 | 75 | Task 76 | { 77 | [weak self] in await self?.notifyClientAboutNotificationFromServer(notification) 78 | } 79 | } 80 | 81 | connection.serverDidSendErrorOutput = 82 | { 83 | errorOutput in 84 | 85 | Task 86 | { 87 | [weak self] in await self?.notifyClientAboutErrorOutputFromServer(errorOutput) 88 | } 89 | } 90 | 91 | connection.didCloseWithError = 92 | { 93 | error in 94 | 95 | Task 96 | { 97 | [weak self] in await self?.connectionDidClose(with: error) 98 | } 99 | } 100 | } 101 | 102 | /// The identifier of the language whose name was provided via the initializer 103 | public let languageIdentifier: LanguageIdentifier 104 | 105 | // MARK: - Observe the Connection 106 | 107 | private func connectionDidClose(with error: Error) 108 | { 109 | cancelAllPendingRequests(with: error) 110 | notifyClientThatConnectionDidShutDown(error) 111 | } 112 | 113 | // MARK: - Make Async Requests to LSP Server 114 | 115 | /** 116 | Makes a request to the connected LSP server 117 | 118 | When the LSP server produces an LSP response error (see [its specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError)), the thrown error is a ``LSP/Message/Response/ErrorResult`` a.k.a. `LSP.ErrorResult`. So the caller should check whether thrown errors are indeed `LSP.ErrorResult`s. 119 | 120 | - Parameter request: The LSP request message to send to the LSP server 121 | 122 | - Returns: The `JSON` value returned by the LSP server when no error occured. It corresponds to the `result` property in [the specification of the LSP response message](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage) 123 | 124 | 125 | - Throws: If the LSP server sends a [response error message](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseError) in return, the error is a ``LSP/Message/Response/ErrorResult``, otherwise it's any `Error`. 126 | */ 127 | public func request(_ request: Request) async throws -> JSON 128 | { 129 | try await withCheckedThrowingContinuation 130 | { 131 | continuation in 132 | 133 | Task 134 | { 135 | save(continuation, for: request.id) 136 | 137 | do 138 | { 139 | try await connection.sendToServer(.request(request)) 140 | } 141 | catch 142 | { 143 | removeContinuation(for: request.id) 144 | continuation.resume(throwing: error) 145 | } 146 | } 147 | } 148 | } 149 | 150 | private func serverDidSend(_ response: Response) async 151 | { 152 | switch response.id 153 | { 154 | case .value(let id): 155 | guard let continuation = removeContinuation(for: id) else 156 | { 157 | log(error: "No matching continuation found") 158 | break 159 | } 160 | 161 | switch response.result 162 | { 163 | case .success(let jsonResult): 164 | if jsonResult == .null 165 | { 166 | log(verbose: "Received valid LSP response message with result property of ") 167 | } 168 | 169 | continuation.resume(returning: jsonResult) 170 | 171 | case .failure(let errorResult): 172 | // TODO: ensure clients actually try to cast thrown errors to LSP.ErrorResult 173 | continuation.resume(throwing: errorResult) 174 | } 175 | case .null: 176 | switch response.result 177 | { 178 | case .success(let jsonResult): 179 | log(error: "Server did respond with value but no request ID: \(jsonResult)") 180 | case .failure(let errorResult): 181 | log(error: "Server did respond with error but no request ID: \(errorResult)") 182 | } 183 | } 184 | } 185 | 186 | private func cancelAllPendingRequests(with error: Error) 187 | { 188 | for continuation in continuationsByMessageID.values 189 | { 190 | continuation.resume(throwing: error) 191 | } 192 | 193 | continuationsByMessageID.removeAll() 194 | } 195 | 196 | private func save(_ continuation: Continuation, for id: Message.ID) 197 | { 198 | continuationsByMessageID[id] = continuation 199 | } 200 | 201 | @discardableResult 202 | private func removeContinuation(for id: Message.ID) -> Continuation? 203 | { 204 | continuationsByMessageID.removeValue(forKey: id) 205 | } 206 | 207 | private var continuationsByMessageID = [Message.ID: Continuation]() 208 | private typealias Continuation = CheckedContinuation 209 | 210 | // MARK: - Send Notification to LSP Server 211 | 212 | /** 213 | Sends a ``LSP/Message/Notification`` to the connected LSP server 214 | 215 | - Parameter notification: The ``LSP/Message/Notification`` to send to the LSP server 216 | */ 217 | public func notify(_ notification: Message.Notification) async throws 218 | { 219 | try await connection.sendToServer(.notification(notification)) 220 | } 221 | 222 | // MARK: - Receive Feedback from LSP Server and from Connection 223 | 224 | /// Sets the handler for LSP notifications sent by the LSP server 225 | public func handleNotificationFromServer(_ handleNotification: @escaping (Message.Notification) -> Void) 226 | { 227 | notifyClientAboutNotificationFromServer = handleNotification 228 | } 229 | 230 | private var notifyClientAboutNotificationFromServer: (Message.Notification) -> Void = 231 | { 232 | _ in log(warning: "notification handler not set") 233 | } 234 | 235 | /// Sets the handler for strings sent by the LSP server via `stdErr` 236 | public func handleErrorOutputFromServer(_ handleErrorOutput: @escaping (String) -> Void) 237 | { 238 | notifyClientAboutErrorOutputFromServer = handleErrorOutput 239 | } 240 | 241 | private var notifyClientAboutErrorOutputFromServer: (String) -> Void = 242 | { 243 | _ in log(warning: "stdErr handler not set") 244 | } 245 | 246 | /// Sets the handler for errors sent by the LSP server connection itself when it shuts down 247 | /// 248 | /// This indicates that the connection had to shut down. We never assume a connection still works when it has produced an error. 249 | public func handleConnectionShutdown(_ handleError: @escaping (Error) -> Void) 250 | { 251 | notifyClientThatConnectionDidShutDown = handleError 252 | } 253 | 254 | private var notifyClientThatConnectionDidShutDown: (Error) -> Void = 255 | { 256 | _ in log(warning: "connection close handler not set") 257 | } 258 | 259 | // MARK: - Server Connection 260 | 261 | private let connection: LSPServerConnection 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /Sources/Server Communication/LSP.ServerExecutable.swift: -------------------------------------------------------------------------------- 1 | #if os(macOS) 2 | 3 | import FoundationToolz 4 | import SwiftyToolz 5 | 6 | public extension LSP { 7 | 8 | /** 9 | Represents an LSP server's executable file and allows to receive ``LSP/Message``s from it 10 | 11 | This does not work in a sandboxed app! 12 | */ 13 | class ServerExecutable: Executable { 14 | 15 | // MARK: - Life Cycle 16 | 17 | /** 18 | Initializes with an `Executable.Configuration` and a message packet handler 19 | */ 20 | public init(config: Configuration, 21 | handleLSPPacket: @escaping (LSP.Packet) -> Void) throws { 22 | packetDetector = PacketDetector(handleLSPPacket) 23 | try super.init(config: config) 24 | 25 | // TODO: output-, error- and termination handler should be passed to the Executable initializer directly! also to force they're being set or at least to force the client to make a conscious decision on wehther to set them 26 | didSendOutput = { [weak self] in self?.packetDetector.read($0) } 27 | } 28 | 29 | // MARK: - LSP Packet Output 30 | 31 | private let packetDetector: LSP.PacketDetector 32 | } 33 | } 34 | 35 | public extension Executable.Configuration 36 | { 37 | static var sourceKitLSP: Executable.Configuration 38 | { 39 | .init(path: "/usr/bin/xcrun", 40 | arguments: ["sourcekit-lsp"], 41 | environment: ["SOURCEKIT_LOGGING": "0"]) 42 | } 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /Sources/Server Communication/LSP.WebSocketConnection.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import Foundation 3 | import SwiftyToolz 4 | 5 | public extension LSP 6 | { 7 | /// An ``LSPServerConnection`` that uses a `WebSocket` to talk to an LSP server 8 | /// 9 | /// The client should set the four handlers defined by the protocol ``LSPServerConnection`` 10 | class WebSocketConnection: LSPServerConnection 11 | { 12 | // MARK: - Initialize 13 | 14 | /// Initialize with a WebSocket 15 | /// - Parameter webSocket: A WebSocket connected to an LSP server 16 | public init(webSocket: WebSocket) 17 | { 18 | self.webSocket = webSocket 19 | 20 | webSocket.didReceiveData = 21 | { 22 | [weak self] data in self?.process(data: data) 23 | } 24 | 25 | webSocket.didReceiveText = 26 | { 27 | [weak self] text in self?.serverDidSendErrorOutput(text) 28 | } 29 | 30 | webSocket.didCloseWithError = 31 | { 32 | [weak self] _, error in self?.didCloseWithError(error) 33 | } 34 | } 35 | 36 | // MARK: - Talk to LSP Server 37 | 38 | private func process(data: Data) 39 | { 40 | do 41 | { 42 | let message = try LSP.Message(LSP.Packet(parsingPrefixOf: data).content) 43 | 44 | switch message 45 | { 46 | case .request: 47 | throw "Received request from LSP server" 48 | case .response(let response): 49 | serverDidSendResponse(response) 50 | case .notification(let notification): 51 | serverDidSendNotification(notification) 52 | } 53 | } 54 | catch 55 | { 56 | log(error.readable) 57 | log("Received data:\n" + (data.utf8String ?? "")) 58 | } 59 | } 60 | 61 | public var serverDidSendResponse: (LSP.Message.Response) -> Void = { _ in } 62 | public var serverDidSendNotification: (LSP.Message.Notification) -> Void = { _ in } 63 | public var serverDidSendErrorOutput: (String) -> Void = { _ in } 64 | 65 | /// Send a ``LSP/Message`` via the data channel of the `WebSocket` 66 | /// - Parameter message: The `LSP.Message` to send 67 | public func sendToServer(_ message: LSP.Message) async throws 68 | { 69 | try await webSocket.send(try LSP.Packet(message).data) 70 | } 71 | 72 | // MARK: - Manage Connection 73 | 74 | public var didCloseWithError: (Error) -> Void = 75 | { 76 | _ in log(warning: "LSP WebSocket connection error handler not set") 77 | } 78 | 79 | // MARK: - WebSocket 80 | 81 | private let webSocket: WebSocket 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Server Communication/LSPServerConnection.swift: -------------------------------------------------------------------------------- 1 | /// Interface defining the raw I/O capabilities of an LSP server 2 | public protocol LSPServerConnection: AnyObject 3 | { 4 | // MARK: - Communicate with the LSP Server 5 | 6 | func sendToServer(_ message: LSP.Message) async throws 7 | var serverDidSendResponse: (LSP.Message.Response) -> Void { get set } 8 | var serverDidSendNotification: (LSP.Message.Notification) -> Void { get set } 9 | var serverDidSendErrorOutput: (String) -> Void { get set } 10 | 11 | // MARK: - Manage the LSP Server Connection itself 12 | 13 | var didCloseWithError: (Error) -> Void { get set } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/Use Cases/Basic LSP Types/LSPLocation.swift: -------------------------------------------------------------------------------- 1 | /** 2 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#location 3 | */ 4 | public struct LSPLocation: Codable, Equatable, Sendable 5 | { 6 | public let uri: LSPDocumentUri 7 | public let range: LSPRange 8 | } 9 | 10 | public typealias LSPDocumentUri = String 11 | 12 | public extension LSPRange 13 | { 14 | func contains(_ otherRange: LSPRange) -> Bool 15 | { 16 | if otherRange.start.line < start.line { return false } 17 | if otherRange.start.line == start.line, 18 | otherRange.start.character < start.character { return false } 19 | 20 | if otherRange.start.line > end.line { return false } 21 | if otherRange.start.line == end.line, 22 | otherRange.start.character > end.character { return false } 23 | 24 | if otherRange.end.line < start.line { return false } 25 | if otherRange.end.line == start.line, 26 | otherRange.end.character < start.character { return false } 27 | 28 | if otherRange.end.line > end.line { return false } 29 | if otherRange.end.line == end.line, 30 | otherRange.end.character > end.character { return false } 31 | 32 | return true 33 | } 34 | } 35 | 36 | public struct LSPRange: Codable, Equatable, Sendable 37 | { 38 | public init(start: LSPPosition, end: LSPPosition) 39 | { 40 | self.start = start 41 | self.end = end 42 | } 43 | 44 | /** 45 | * The range's start position. 46 | */ 47 | public let start: LSPPosition 48 | 49 | /** 50 | * The range's end position. 51 | */ 52 | public let end: LSPPosition 53 | } 54 | 55 | public struct LSPPosition: Codable, Equatable, Sendable 56 | { 57 | public init(line: Int, character: Int) 58 | { 59 | self.line = line 60 | self.character = character 61 | } 62 | 63 | /** 64 | * Line position in a document (zero-based). 65 | */ 66 | public let line: Int // uinteger; 67 | 68 | /** 69 | * Character offset on a line in a document (zero-based). The meaning of this 70 | * offset is determined by the negotiated `PositionEncodingKind`. 71 | * 72 | * If the character value is greater than the line length it defaults back 73 | * to the line length. 74 | */ 75 | public let character: Int // uinteger; 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Use Cases/Basic LSP Types/LSPTextDocumentPositionParams.swift: -------------------------------------------------------------------------------- 1 | /** 2 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentPositionParams 3 | */ 4 | public struct LSPTextDocumentPositionParams: Codable, Sendable 5 | { 6 | /** 7 | * The text document. 8 | */ 9 | public let textDocument: LSPTextDocumentIdentifier 10 | 11 | /** 12 | * The position inside the text document. 13 | */ 14 | public let position: LSPPosition 15 | } 16 | 17 | public struct LSPTextDocumentIdentifier: Codable, Sendable 18 | { 19 | /** 20 | * The text document's URI. 21 | */ 22 | public let uri: LSPDocumentUri 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Use Cases/Document Sync/LSP.Message.Notification+DocumentSync.swift: -------------------------------------------------------------------------------- 1 | import SwiftyToolz 2 | 3 | public extension LSP.Message.Notification 4 | { 5 | /** 6 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen 7 | */ 8 | static func didOpen(doc: JSON) -> Self 9 | { 10 | .init(method: "textDocument/didOpen", 11 | params: .object(["textDocument": doc])) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Use Cases/Document Sync/LSP.ServerCommunicationHandler+DocumentSync.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import SwiftyToolz 3 | 4 | public extension LSP.ServerCommunicationHandler 5 | { 6 | func notifyDidOpen(_ document: LSPDocumentUri, 7 | containingText text: String) async throws 8 | { 9 | let docJSONObject: [String: JSONObject] = 10 | [ 11 | "uri": document, 12 | "languageId": languageIdentifier.string, 13 | "version": 1, 14 | "text": text 15 | ] 16 | 17 | try await notify(.didOpen(doc: JSON(jsonObject: docJSONObject))) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Use Cases/References/LSP.Message.Request+References.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import SwiftyToolz 3 | 4 | public extension LSP.Message.Request 5 | { 6 | /** 7 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references 8 | */ 9 | static func references(forSymbolSelectionRange selectionRange: LSPRange, 10 | in document: LSPDocumentUri) throws -> Self 11 | { 12 | let docIdentifierJSON = try JSON(LSPTextDocumentIdentifier(uri: document).encode()) 13 | 14 | let params = JSON.Container.object([ 15 | /** 16 | * The text document. 17 | */ 18 | "textDocument": docIdentifierJSON, 19 | 20 | /** 21 | * The position inside the text document. 22 | */ 23 | "position": try JSON(selectionRange.start.encode()), 24 | 25 | "context": JSON.object([ 26 | /** 27 | * Include the declaration of the current symbol. 28 | */ 29 | "includeDeclaration": .bool(false) 30 | ]) 31 | ]) 32 | 33 | return .init(method: "textDocument/references", params: params) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/Use Cases/References/LSP.ServerCommunicationHandler+References.swift: -------------------------------------------------------------------------------- 1 | extension LSP.ServerCommunicationHandler 2 | { 3 | /// This just adds the knowledge of what result type the server returns 4 | public func requestReferences(forSymbolSelectionRange selectionRange: LSPRange, 5 | in document: LSPDocumentUri) async throws -> [LSPLocation] 6 | { 7 | try await request(.references(forSymbolSelectionRange: selectionRange, in: document)) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Use Cases/Server Life Cycle/LSP.Message.Notification+Initialized.swift: -------------------------------------------------------------------------------- 1 | public extension LSP.Message.Notification 2 | { 3 | static var initialized: Self 4 | { 5 | .init(method: "initialized", params: .emptyObject) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Use Cases/Server Life Cycle/LSP.Message.Request+Initialize.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import Foundation 3 | import SwiftyToolz 4 | 5 | public extension LSP.Message.Request 6 | { 7 | /** 8 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize 9 | 10 | - 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. 11 | */ 12 | static func initialize(folder: URL, 13 | clientProcessID: Int = Int(ProcessInfo.processInfo.processIdentifier), 14 | capabilities: JSON = defaultClientCapabilities) -> Self 15 | { 16 | .init(method: "initialize", 17 | params: .object(["rootUri": .string(folder.absoluteString), 18 | "processId": .int(clientProcessID), 19 | "capabilities": capabilities])) 20 | } 21 | 22 | /** 23 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities 24 | */ 25 | static var defaultClientCapabilities: JSON 26 | { 27 | .object( 28 | [ 29 | "textDocument": .object( // TextDocumentClientCapabilities; 30 | [ 31 | /** 32 | * Capabilities specific to the `textDocument/documentSymbol` request. 33 | */ 34 | "documentSymbol": .object( //DocumentSymbolClientCapabilities; 35 | [ 36 | // https://github.com/microsoft/language-server-protocol/issues/884 37 | "hierarchicalDocumentSymbolSupport": .bool(true) 38 | ]) 39 | ]) 40 | ]) 41 | } 42 | } 43 | 44 | public func log(initializeResult: JSON) throws 45 | { 46 | // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#serverCapabilities 47 | guard let serverCapabilities = initializeResult.capabilities else 48 | { 49 | throw "LSP initialize result has no \"capabilities\" field" 50 | } 51 | 52 | log("LSP Server Capabilities:\n\(serverCapabilities.description)") 53 | } 54 | -------------------------------------------------------------------------------- /Sources/Use Cases/Symbols/LSP.Message.Request+Symbols.swift: -------------------------------------------------------------------------------- 1 | import FoundationToolz 2 | import SwiftyToolz 3 | 4 | public extension LSP.Message.Request 5 | { 6 | /** 7 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_symbol 8 | */ 9 | static func workspaceSymbols(forQuery query: String = "") -> Self 10 | { 11 | .init(method: "workspace/symbol", 12 | params: .object(["query": .string(query)])) 13 | } 14 | 15 | /** 16 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentSymbol 17 | */ 18 | static func symbols(in document: LSPDocumentUri) throws -> Self 19 | { 20 | let docIdentifier = LSPTextDocumentIdentifier(uri: document) 21 | 22 | let params = JSON.Container.object( 23 | [ 24 | "textDocument": try JSON(docIdentifier.encode()) 25 | ]) 26 | 27 | return .init(method: "textDocument/documentSymbol", params: params) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/Use Cases/Symbols/LSP.ServerCommunicationHandler+Symbols.swift: -------------------------------------------------------------------------------- 1 | extension LSP.ServerCommunicationHandler 2 | { 3 | /// This just adds the knowledge of what result type the server returns 4 | public func requestSymbols(in document: LSPDocumentUri) async throws -> [LSPDocumentSymbol]? 5 | { 6 | try await requestOptional(.symbols(in: document)) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Use Cases/Symbols/LSPDocumentSymbol.swift: -------------------------------------------------------------------------------- 1 | public extension LSPDocumentSymbol.SymbolKind 2 | { 3 | static let names = allCases.map { $0.name } 4 | 5 | var name: String 6 | { 7 | switch self 8 | { 9 | case .File: return "File" 10 | case .Module: return "Module" 11 | case .Namespace: return "Namespace" 12 | case .Package: return "Package" 13 | case .Class: return "Class" 14 | case .Method: return "Method" 15 | case .Property: return "Property" 16 | case .Field: return "Field" 17 | case .Constructor: return "Constructor" 18 | case .Enum: return "Enum" 19 | case .Interface: return "Interface" 20 | case .Function: return "Function" 21 | case .Variable: return "Variable" 22 | case .Constant: return "Constant" 23 | case .String: return "String" 24 | case .Number: return "Number" 25 | case .Boolean: return "Boolean" 26 | case .Array: return "Array" 27 | case .Object: return "Object" 28 | case .Key: return "Key" 29 | case .Null: return "Null" 30 | case .EnumMember: return "EnumMember" 31 | case .Struct: return "Struct" 32 | case .Event: return "Event" 33 | case .Operator: return "Operator" 34 | case .TypeParameter: return "TypeParameter" 35 | } 36 | } 37 | } 38 | 39 | /** 40 | https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentSymbol 41 | */ 42 | public struct LSPDocumentSymbol: Codable, Equatable, Sendable 43 | { 44 | public init(name: String, 45 | kind: Int, 46 | range: LSPRange, 47 | selectionRange: LSPRange, 48 | children: [LSPDocumentSymbol] = []) 49 | { 50 | self.name = name 51 | self.kind = kind 52 | self.range = range 53 | self.selectionRange = selectionRange 54 | self.children = children 55 | } 56 | 57 | public let name: String 58 | 59 | public var decodedKind: SymbolKind? { .init(rawValue: kind) } 60 | 61 | public enum SymbolKind: Int, CaseIterable, Codable, Equatable, Sendable 62 | { 63 | case File = 1 64 | case Module = 2 65 | case Namespace = 3 66 | case Package = 4 67 | case Class = 5 68 | case Method = 6 69 | case Property = 7 70 | case Field = 8 71 | case Constructor = 9 72 | case Enum = 10 73 | case Interface = 11 74 | case Function = 12 75 | case Variable = 13 76 | case Constant = 14 77 | case String = 15 78 | case Number = 16 79 | case Boolean = 17 80 | case Array = 18 81 | case Object = 19 82 | case Key = 20 83 | case Null = 21 84 | case EnumMember = 22 85 | case Struct = 23 86 | case Event = 24 87 | case Operator = 25 88 | case TypeParameter = 26 89 | } 90 | 91 | public let kind: Int 92 | 93 | public let range: LSPRange 94 | 95 | public let selectionRange: LSPRange 96 | 97 | public let children: [Self]? 98 | } 99 | -------------------------------------------------------------------------------- /Tests/PublicAPITests.swift: -------------------------------------------------------------------------------- 1 | import SwiftLSP // Do not use @testable❗️ we wanna test public API here like a real client 2 | import XCTest 3 | 4 | final class PublicAPITests: XCTestCase { 5 | 6 | func testCreatingLSPTypes() { 7 | _ = LSP.Message.Request(method: "just do it!") 8 | _ = LSP.Message.Response(id: .value(.string(.randomID())), 9 | result: .success(.bool(true))) 10 | _ = LSP.Message.Notification(method: "just wanted to say hi") 11 | _ = LSP.ErrorResult(code: 1000, message: "some LSP error occured") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/SwiftLSPTests.swift: -------------------------------------------------------------------------------- 1 | @testable import SwiftLSP 2 | import FoundationToolz 3 | import SwiftyToolz 4 | import XCTest 5 | 6 | final class SwiftLSPTests: XCTestCase { 7 | 8 | // MARK: - Message 9 | 10 | func testCodeExamplesFromREADME() throws { 11 | let myRequest = LSP.Request(method: "myMethod", params: nil) 12 | let myRequestMessage = LSP.Message.request(myRequest) 13 | 14 | let myNotification = LSP.Notification(method: "myMethod", params: nil) 15 | _ = LSP.Message.notification(myNotification) 16 | 17 | let myRequestMessageEncoded = try myRequestMessage.encode() // Data 18 | let myRequestMessageDecoded = try LSP.Message(myRequestMessageEncoded) 19 | XCTAssertEqual(myRequestMessageDecoded, myRequestMessage) 20 | 21 | let myRequestMessagePacket = try LSP.Packet(myRequestMessage) 22 | _ = myRequestMessagePacket.header // Data 23 | _ = myRequestMessagePacket.content // Data 24 | let packetTotalData = myRequestMessagePacket.data // Data 25 | 26 | let myRequestMessageUnpacked = try myRequestMessagePacket.message() // LSP.Message 27 | XCTAssertEqual(myRequestMessageUnpacked, myRequestMessage) 28 | 29 | let dataStartingWithPacket = packetTotalData + "Some other data".data(using: .utf8)! 30 | let detectedPacket = try LSP.Packet(parsingPrefixOf: dataStartingWithPacket) 31 | XCTAssertEqual(detectedPacket, myRequestMessagePacket) 32 | 33 | var streamedPacket: LSP.Packet? = nil 34 | 35 | let detector = LSP.PacketDetector { packet in 36 | streamedPacket = packet 37 | } 38 | 39 | for byte in dataStartingWithPacket { 40 | detector.read(byte) 41 | } 42 | 43 | XCTAssertEqual(streamedPacket, myRequestMessagePacket) 44 | } 45 | 46 | func testNewRequestMessageHasUUIDasID() throws { 47 | 48 | let message = LSP.Message.Request(method: "method", params: nil) 49 | 50 | guard case .string(let idString) = message.id else { 51 | throw "Message ID is not a String" 52 | } 53 | 54 | XCTAssertNotNil(UUID(uuidString: idString)) 55 | } 56 | 57 | // MARK: - Message JSON 58 | 59 | func testGettingRequestMessageJSON() throws 60 | { 61 | let request = LSP.Message.Request(method: "someMethod", 62 | params: .object(["testBool": .bool(true)])) 63 | let requestMessage = LSP.Message.request(request) 64 | let requestMessageJSON = requestMessage.json() 65 | 66 | XCTAssertEqual(requestMessageJSON.jsonrpc, .string("2.0")) 67 | try testMessageJSONHasUUIDBasedID(requestMessageJSON) 68 | XCTAssertEqual(requestMessageJSON.method, .string("someMethod")) 69 | XCTAssertEqual(requestMessageJSON.params, .object(["testBool": .bool(true)])) 70 | XCTAssertNil(requestMessageJSON.bullshit) 71 | } 72 | 73 | func testGettingResponseMessageJSON() throws 74 | { 75 | let response = LSP.Message.Response(id: .value(.string(UUID().uuidString)), 76 | result: .success(.string("42"))) 77 | let responseMessage = LSP.Message.response(response) 78 | let responseMessageJSON = responseMessage.json() 79 | 80 | XCTAssertEqual(responseMessageJSON.jsonrpc, .string("2.0")) 81 | try testMessageJSONHasUUIDBasedID(responseMessageJSON) 82 | XCTAssertEqual(responseMessageJSON.result, .string("42")) 83 | XCTAssertNil(responseMessageJSON.method) 84 | } 85 | 86 | func testMessageJSONHasUUIDBasedID(_ messageJSON: JSON) throws 87 | { 88 | guard case .string(let idString) = messageJSON.id else 89 | { 90 | throw "Message JSON id is not a String" 91 | } 92 | 93 | XCTAssertNotNil(UUID(uuidString: idString)) 94 | } 95 | 96 | func testGettingNotificationMessageJSON() throws 97 | { 98 | let notification = LSP.Message.Notification(method: "someMethod", 99 | params: nil) 100 | let notificationMessage = LSP.Message.notification(notification) 101 | let notificationMessageJSON = notificationMessage.json() 102 | 103 | XCTAssertEqual(notificationMessageJSON.jsonrpc, .string("2.0")) 104 | XCTAssertNil(notificationMessageJSON.id) 105 | XCTAssertEqual(notificationMessageJSON.method, .string("someMethod")) 106 | XCTAssertNil(notificationMessageJSON.params) 107 | } 108 | 109 | func testMakingRequestMessageFromJSON() throws 110 | { 111 | let requestMessageJSON = JSON.object([ 112 | "id": .string("someID"), 113 | "method": .string("someMethod") 114 | ]) 115 | 116 | let requestMessage = try LSP.Message(requestMessageJSON) 117 | 118 | guard case .request(let request) = requestMessage else 119 | { 120 | throw "Message from request message JSON is not a request message" 121 | } 122 | 123 | XCTAssertEqual(request.id, .string("someID")) 124 | XCTAssertEqual(request.method, "someMethod") 125 | XCTAssertNil(request.params) 126 | } 127 | 128 | func testMakingResponseMessageFromJSON() throws 129 | { 130 | let responseMessageJSON = JSON.object([ 131 | "id": .string("someID"), 132 | "result": .string("Some Result") 133 | ]) 134 | 135 | let responseMessage = try LSP.Message(responseMessageJSON) 136 | 137 | guard case .response(let response) = responseMessage else 138 | { 139 | throw "Message from response message JSON is not a response message" 140 | } 141 | 142 | XCTAssertEqual(response.id, .value(.string("someID"))) 143 | XCTAssertEqual(response.result, .success(.string("Some Result"))) 144 | } 145 | 146 | func testMakingErrorResponseMessageFromJSON() throws 147 | { 148 | let errorResult = LSP.ErrorResult(code: 1, 149 | message: "Error Message", 150 | data: .string("Error Data")) 151 | 152 | let responseMessageJSON = JSON.object([ 153 | "id": .string("someID"), 154 | "error": errorResult.json() 155 | ]) 156 | 157 | let responseMessage = try LSP.Message(responseMessageJSON) 158 | 159 | guard case .response(let response) = responseMessage else 160 | { 161 | throw "Message from response message JSON is not a response message" 162 | } 163 | 164 | XCTAssertEqual(response.id, .value(.string("someID"))) 165 | XCTAssertEqual(response.result, .failure(errorResult)) 166 | } 167 | 168 | func testMakingNotificationMessageFromJSON() throws 169 | { 170 | let notificationMessageJSON = JSON.object([ 171 | "method": .string("someMethod"), 172 | "params": .object(["testNumber": .int(123)]) 173 | ]) 174 | 175 | let notificationMessage = try LSP.Message(notificationMessageJSON) 176 | 177 | guard case .notification(let notification) = notificationMessage else 178 | { 179 | throw "Message from notification message JSON is not a notification message" 180 | } 181 | 182 | XCTAssertEqual(notification.method, "someMethod") 183 | XCTAssertEqual(notification.params, .object(["testNumber": .int(123)])) 184 | } 185 | 186 | func testMakingMessageFromInvalidJSONFails() 187 | { 188 | XCTAssertThrowsError(try LSP.Message(JSON.object([:]))) 189 | XCTAssertThrowsError(try LSP.Message(JSON.object(["id": .int(123)]))) 190 | XCTAssertThrowsError(try LSP.Message(JSON.object(["id": .null]))) 191 | XCTAssertThrowsError(try LSP.Message(JSON.object(["id": .null, 192 | "method": .string("someMethod")]))) 193 | 194 | // if it has id, method AND result, it's not clear whether it's a request or a response 195 | XCTAssertThrowsError(try LSP.Message(JSON.object(["id": .int(123), 196 | "method": .string("someMethod"), 197 | "result": .object(["resultInt": .int(42)])]))) 198 | } 199 | 200 | // MARK: - Message Data 201 | 202 | func testConvertingBetweenMessageAndData() throws { 203 | let messageJSONString = #"{"jsonrpc":"2.0","id":"C0DC9B39-5DCF-474A-BF78-7C18F37CFDEF","result":{"capabilities":{"hoverProvider":true,"implementationProvider":true,"colorProvider":true,"codeActionProvider":true,"foldingRangeProvider":true,"documentHighlightProvider":true,"definitionProvider":true,"documentSymbolProvider":true,"executeCommandProvider":{"commands":["semantic.refactor.command"]},"completionProvider":{"resolveProvider":false,"triggerCharacters":["."]},"referencesProvider":true,"textDocumentSync":{"willSave":true,"save":{"includeText":false},"openClose":true,"change":2,"willSaveWaitUntil":false},"workspaceSymbolProvider":true}}}"# 204 | 205 | let message = try LSP.Message(messageJSONString.data!) 206 | let encodedMessage = try message.encode() 207 | let messageDecodedAgain = try LSP.Message(encodedMessage) 208 | XCTAssertEqual(message, messageDecodedAgain) 209 | } 210 | 211 | // MARK: - Packet 212 | 213 | func testPacket() throws { 214 | let messageJSONString = #"{"jsonrpc":"2.0", "method":"someMethod"}"# 215 | let packet1 = try LSP.Packet(withContent: messageJSONString.data!) 216 | _ = try packet1.message() 217 | 218 | let packetBufferString = "Content-Length: 40\r\n\r\n" + messageJSONString + "Next packet or other data" 219 | let packet2 = try LSP.Packet(parsingPrefixOf: packetBufferString.data!) 220 | _ = try packet2.message() 221 | 222 | XCTAssertThrowsError(try LSP.Packet(withContent: Data())) 223 | XCTAssertThrowsError(try LSP.Packet(withContent: (messageJSONString + "{}").data!)) 224 | XCTAssertThrowsError(try LSP.Packet(withContent: messageJSONString.removing("}").data!)) 225 | XCTAssertThrowsError(try LSP.Packet(parsingPrefixOf: messageJSONString.data!)) 226 | } 227 | 228 | // MARK: - Packet Detector 229 | 230 | func testPacketDetector() { 231 | var detectedPackets = [LSP.Packet]() 232 | let detector = LSP.PacketDetector { detectedPackets += $0 } 233 | 234 | let header = "Content-Length: 40".data! 235 | let separator = "\r\n\r\n".data! 236 | let messageJSON = #"{"jsonrpc":"2.0", "method":"someMethod"}"#.data! 237 | 238 | detector.read(header) 239 | XCTAssertEqual(detectedPackets.count, 0) 240 | 241 | detector.read(separator) 242 | XCTAssertEqual(detectedPackets.count, 0) 243 | 244 | detector.read(messageJSON) 245 | XCTAssertEqual(detectedPackets.count, 1) 246 | 247 | detector.read(header) 248 | XCTAssertEqual(detectedPackets.count, 1) 249 | 250 | detector.read(separator) 251 | XCTAssertEqual(detectedPackets.count, 1) 252 | 253 | detector.read(messageJSON + Data(count: 10)) 254 | XCTAssertEqual(detectedPackets.count, 2) 255 | } 256 | 257 | // MARK: - LSP Language Identifier 258 | 259 | func testLanguageIdentifier() { 260 | XCTAssertEqual(LSP.LanguageIdentifier(languageName: "Swift").string, "swift") 261 | XCTAssertEqual(LSP.LanguageIdentifier(languageName: "python").string, "python") 262 | XCTAssertEqual(LSP.LanguageIdentifier(languageName: "C++").string, "cpp") 263 | XCTAssertEqual(LSP.LanguageIdentifier(languageName: "C#").string, "csharp") 264 | } 265 | } 266 | --------------------------------------------------------------------------------