Get the language server actually running

+2 -2
Package.swift
···
name: "PterodactylBuild",
targets: ["PterodactylBuild"]
),
-
.library(
+
.executable(
name: "PterodactylLanguageServer",
targets: ["PterodactylLanguageServer"]
)
···
.product(name: "llbuild2fx", package: "swift-llbuild2")
]
),
-
.target(
+
.executableTarget(
name: "PterodactylLanguageServer",
dependencies: [
"LanguageServer",
+1
Sources/PterodactylBuild/Keys/Blob/ParseDocument.swift
···
import Foundation
import PterodactylSyntax
import llbuild2fx
+
import Logging
extension Keys.Blob {
public struct ParseDocument: BuildKey {
+40
Sources/PterodactylLanguageServer/Archive/SourceTreeManager.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
//import LanguageServerProtocol
+
//import PterodactylBuild
+
//import TSCBasic
+
//import llbuild2fx
+
//
+
//actor SourceTreeManager {
+
// private let buildEngine: FXEngine
+
// private let casClient: LLBCASFSClient
+
// private let casContext: TSCUtility.Context
+
// private(set) var sourceTree: LLBCASFileTree
+
//
+
// init(buildEngine: FXEngine, casClient: LLBCASFSClient, casContext: TSCUtility.Context, sourceTree: LLBCASFileTree? = nil) async throws {
+
// self.buildEngine = buildEngine
+
// self.casClient = casClient
+
// self.casContext = casContext
+
// if let sourceTree {
+
// self.sourceTree = sourceTree
+
// } else {
+
// self.sourceTree = try await casClient.storeDir(.dir([:]), casContext).get()
+
// }
+
// }
+
//
+
// func setBufferText(uri: DocumentUri, text: String) async throws -> LLBCASFileTree {
+
// let path = try AbsolutePath.fromDocumentUri(uri)
+
// let singletonDeclTree = LLBDeclFileTree.file(absolutePath: path, contents: text)
+
// let singletonTree: LLBCASFileTree = try await casClient.storeDir(singletonDeclTree, casContext).get()
+
// self.sourceTree = try await sourceTree.merge(with: singletonTree, in: casClient.db, casContext).get()
+
// return sourceTree
+
// }
+
//
+
// func getBufferText(uri: DocumentUri) async throws -> String? {
+
// let path = try AbsolutePath.fromDocumentUri(uri)
+
// guard let (id, _) = try await sourceTree.lookup(path: path, in: casClient.db, casContext).get() else { return nil }
+
// return try await buildEngine.build(key: Keys.Blob.ReadContents(blobId: id), casContext).get()
+
// }
+
//}
+44 -27
Sources/PterodactylLanguageServer/EventHandler.swift
···
import LanguageServerProtocol
import Logging
import PterodactylBuild
+
import PterodactylSyntax
import TSCBasic
import llbuild2fx
···
private let buildEngine: FXEngine
private let casContext: TSCUtility.Context
private let casClient: LLBCASFSClient
-
private let sourceTreeManager: SourceTreeManager
-
init(connection: JSONRPCClientConnection) async throws {
+
var storedBlobs: [DocumentUri: (blobId: LLBDataID, version: Int?)] = [:]
+
+
init(connection: JSONRPCClientConnection) {
self.connection = connection
let group = LLBMakeDefaultDispatchGroup()
···
self.buildEngine = FXEngine(group: group, db: db, functionCache: functionCache, executor: executor)
self.casClient = LLBCASFSClient(db)
self.casContext = Context()
-
-
self.sourceTreeManager = try await SourceTreeManager(buildEngine: buildEngine, casClient: casClient, casContext: casContext)
}
-
func publishDiagnostics(uri: String, version: Int?) async throws {
+
func storeBlob(text: String, uri: DocumentUri, version: Int?) async throws -> LLBDataID {
+
if let stored = storedBlobs[uri], let version, let storedVersion = stored.version, storedVersion >= version {
+
return stored.blobId
+
}
+
let blobId: LLBDataID = try await casClient.store(LLBByteBuffer(string: text), casContext).get()
+
storedBlobs[uri] = (blobId: blobId, version: version)
+
return blobId
+
}
+
+
func publishLiveDiagnostics(blobId: LLBDataID, uri: DocumentUri, version: Int?) async throws {
+
let lineMap = try await buildEngine.build(key: Keys.Blob.GetLineMap(blobId: blobId), casContext).get()
+
let parseResult = try await buildEngine.build(key: Keys.Blob.ParseDocument(blobId: blobId), casContext).get()
+
let diagnostics = parseResult.diagnostics.map { $0.lspDiagnostic(lineMap: lineMap) }
+
let publishParams = PublishDiagnosticsParams(uri: uri, version: version, diagnostics: diagnostics)
+
try await connection.sendNotification(.textDocumentPublishDiagnostics(publishParams))
}
}
···
TextDocumentSyncOptions(
openClose: true,
change: .full,
+
save: .optionA(false)
)
}
···
func textDocumentDidChange(_ params: DidChangeTextDocumentParams) async {
guard let text = params.contentChanges.first?.text else { return }
do {
-
try await sourceTreeManager.setBufferText(uri: params.textDocument.uri, text: text)
-
// FIXME: this needs to be a transaction.
-
// try await publishDiagnostics(uri: params.textDocument.uri, version: params.textDocument.version)
+
let blobId: LLBDataID = try await storeBlob(text: text, uri: params.textDocument.uri, version: params.textDocument.version)
+
try await publishLiveDiagnostics(blobId: blobId, uri: params.textDocument.uri, version: params.textDocument.version)
} catch {}
}
func textDocumentDidOpen(_ params: DidOpenTextDocumentParams) async {
-
// TODO: restore
-
// await bufferManager.setBufferText(key: params.textDocument.uri, text: params.textDocument.text)
+
let text = params.textDocument.text
do {
-
try await publishDiagnostics(uri: params.textDocument.uri, version: params.textDocument.version)
+
let blobId: LLBDataID = try await storeBlob(text: text, uri: params.textDocument.uri, version: params.textDocument.version)
+
try await publishLiveDiagnostics(blobId: blobId, uri: params.textDocument.uri, version: params.textDocument.version)
} catch {}
}
func semanticTokensFull(id: JSONId, params: SemanticTokensParams) async -> Response<SemanticTokensResponse> {
-
// TODO: restore
-
// guard let document = await bufferManager.documentForBuffer(key: params.textDocument.uri) else { return .success(nil) }
+
guard let storedBlob = storedBlobs[params.textDocument.uri] else { return .success(nil) }
-
let tokenSink: [SemanticToken] = []
-
// TODO: restore
-
// document.tree.collectSemanticTokens(&tokenSink)
-
-
let response = SemanticTokensResponse(SemanticTokens(resultId: nil, tokens: tokenSink))
-
return .success(response)
+
do {
+
let lineMap = try await buildEngine.build(key: Keys.Blob.GetLineMap(blobId: storedBlob.blobId), casContext).get()
+
let parseResult = try await buildEngine.build(key: Keys.Blob.ParseDocument(blobId: storedBlob.blobId), casContext).get()
+
let cursor = SyntaxCursor(lineMap: lineMap, node: .tree(parseResult.tree), utf16Offset: 0)
+
return .success(SemanticTokensResponse(SemanticTokens(resultId: nil, tokens: cursor.semanticTokens)))
+
} catch {
+
return .success(nil)
+
}
}
func foldingRange(id: JSONId, params: FoldingRangeParams) async -> Response<FoldingRangeResponse> {
-
// TODO: restore
-
// guard let document = await bufferManager.documentForBuffer(key: params.textDocument.uri) else { return .success(nil) }
-
let rangeSink: [FoldingRange] = []
-
// TODO: restore
-
// document.tree.collectFoldingRanges(&rangeSink)
-
let response = FoldingRangeResponse(rangeSink)
-
Logger.shared.info("Ranges: \(rangeSink)")
-
return .success(response)
+
guard let storedBlob = storedBlobs[params.textDocument.uri] else { return .success(nil) }
+
+
do {
+
let lineMap = try await buildEngine.build(key: Keys.Blob.GetLineMap(blobId: storedBlob.blobId), casContext).get()
+
let parseResult = try await buildEngine.build(key: Keys.Blob.ParseDocument(blobId: storedBlob.blobId), casContext).get()
+
let cursor = SyntaxCursor(lineMap: lineMap, node: .tree(parseResult.tree), utf16Offset: 0)
+
return .success(FoldingRangeResponse(cursor.foldingRanges))
+
} catch {
+
return .success(nil)
+
}
}
Sources/PterodactylLanguageServer/LLBDeclFileTree+Singleton.swift Sources/PterodactylLanguageServer/Archive/LLBDeclFileTree+Singleton.swift
+54
Sources/PterodactylLanguageServer/PterodactylLanguageServer.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import JSONRPC
+
import LanguageServer
+
import LanguageServerProtocol
+
import Logging
+
+
final class Server {
+
private let connection: JSONRPCClientConnection
+
private let eventHandler: EventHandler
+
+
init() {
+
let channel = DataChannel.stdio()
+
connection = JSONRPCClientConnection(channel)
+
eventHandler = EventHandler(connection: connection)
+
}
+
+
func start() async throws {
+
do {
+
Logger.shared.debug("Starting")
+
try await startEventLoop()
+
} catch {
+
Logger.shared.error("Server error: \(error)")
+
throw error
+
}
+
}
+
+
func startEventLoop() async throws {
+
for await event in await connection.eventSequence {
+
try await handle(event: event)
+
}
+
}
+
+
func handle(event: ClientEvent) async throws {
+
switch event {
+
case .request(let id, let request):
+
await eventHandler.handleRequest(id: id, request: request)
+
case .notification(let notification):
+
await eventHandler.handleNotification(notification)
+
case .error(_): ()
+
}
+
}
+
}
+
+
@main
+
struct PterodactylLanguageServer {
+
static func main() async throws {
+
let server = Server()
+
try await server.start()
+
}
+
}
-39
Sources/PterodactylLanguageServer/SourceTreeManager.swift
···
-
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
-
//
-
// SPDX-License-Identifier: MPL-2.0
-
-
import LanguageServerProtocol
-
import PterodactylBuild
-
import TSCBasic
-
import llbuild2fx
-
-
actor SourceTreeManager {
-
private let buildEngine: FXEngine
-
private let casClient: LLBCASFSClient
-
private let casContext: TSCUtility.Context
-
private(set) var sourceTree: LLBCASFileTree
-
-
init(buildEngine: FXEngine, casClient: LLBCASFSClient, casContext: TSCUtility.Context, sourceTree: LLBCASFileTree? = nil) async throws {
-
self.buildEngine = buildEngine
-
self.casClient = casClient
-
self.casContext = casContext
-
if let sourceTree {
-
self.sourceTree = sourceTree
-
} else {
-
self.sourceTree = try await casClient.storeDir(.dir([:]), casContext).get()
-
}
-
}
-
-
func setBufferText(uri: DocumentUri, text: String) async throws {
-
let path = try AbsolutePath.fromDocumentUri(uri)
-
let singletonDeclTree = LLBDeclFileTree.file(absolutePath: path, contents: text)
-
let singletonTree: LLBCASFileTree = try await casClient.storeDir(singletonDeclTree, casContext).get()
-
self.sourceTree = try await sourceTree.merge(with: singletonTree, in: casClient.db, casContext).get()
-
}
-
-
func getBufferText(uri: DocumentUri) async throws -> String? {
-
let path = try AbsolutePath.fromDocumentUri(uri)
-
guard let (id, _) = try await sourceTree.lookup(path: path, in: casClient.db, casContext).get() else { return nil }
-
return try await buildEngine.build(key: Keys.Blob.ReadContents(blobId: id), casContext).get()
-
}
-
}
+1 -1
Sources/PterodactylSyntax/BlockLayoutProcessor.swift
···
result.append(Token(kind: .blockEnd, text: ""))
}
-
if !firstTokenInBlock && indentStack.count > 1 && locatedToken.location.startColumn == indentStack.last! {
+
if !firstTokenInBlock && indentStack.count > 1 && locatedToken.token.kind.isVisible && locatedToken.location.startColumn == indentStack.last! {
result.append(Token(kind: .blockSep, text: ""))
}
}
+22 -5
Sources/PterodactylSyntax/Diagnostic.swift
···
import LanguageServerProtocol
public struct Diagnostic: Equatable, Codable, Sendable {
-
typealias Severity = LanguageServerProtocol.DiagnosticSeverity
-
-
let message: String
-
let severity: Severity
-
let absoluteUtf16Range: Range<Int>
+
public typealias Severity = LanguageServerProtocol.DiagnosticSeverity
+
+
public let message: String
+
public let severity: Severity
+
public let absoluteUtf16Range: Range<Int>
init(message: String, severity: Severity, absoluteRange: Range<Int>) {
self.message = message
···
init(message: String, absoluteRange: Range<Int>) {
self.init(message: message, severity: Severity.error, absoluteRange: absoluteRange)
+
}
+
+
func lspRange(lineMap: LineMap) -> LanguageServerProtocol.LSPRange {
+
let start = lineMap.location(at: absoluteUtf16Range.lowerBound)
+
let end = lineMap.location(at: absoluteUtf16Range.upperBound)
+
return LSPRange(
+
start: Position(line: start.line, character: start.column),
+
end: Position(line: end.line, character: end.column)
+
)
+
}
+
+
public func lspDiagnostic(lineMap: LineMap) -> LanguageServerProtocol.Diagnostic {
+
LanguageServerProtocol.Diagnostic(
+
range: lspRange(lineMap: lineMap),
+
severity: severity,
+
message: message
+
)
}
}
+3 -7
Sources/PterodactylSyntax/Grammar/Document/Theory/TheoryBlock.swift
···
}
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
-
parser.expect(kind: .keyword(.where), metadata: TokenMetadata(semanticTokenType: .keyword), recovery: recovery.union([.blockBegin]))
+
parser.expect(kind: .keyword(.where), metadata: TokenMetadata(semanticTokenType: .keyword), recovery: recovery.union([.keyword(.theory), .blockComment(terminated: true), .lineComment]))
if parser.eat(kind: .blockBegin, metadata: nil) {
parser.eatTrivia()
-
while Declaration.tryParse(&parser, recovery: recovery.union([.blockSep, .blockEnd])) {
+
while parser.eat(kind: .blockSep, metadata: nil) {
+
Declaration.parse(&parser, recovery: recovery.union([.blockSep, .blockEnd]))
parser.eatTrivia()
-
if parser.eat(kind: .blockSep, metadata: nil) {
-
continue
-
} else {
-
break
-
}
}
_ = parser.eat(kind: .blockEnd, metadata: nil)
}
+1 -1
Sources/PterodactylSyntax/LineMap.swift
···
private let utf16LineOffsets: [Int]
public init(source: String) {
-
var offsets: [Int] = []
+
var offsets: [Int] = [0]
for idx in source.indices {
let c = source[idx]
if c == "\n" || c == "\r\n" || c == "\r" {
+1 -1
Sources/PterodactylSyntax/Parser.swift
···
extension Parser {
mutating func eatTrivium() -> Bool {
switch currentToken.kind {
-
case .whitespace:
+
case .whitespace, .newline:
advance(metadata: nil)
return true
case .blockComment(let terminated):
+22
Sources/PterodactylSyntax/SemanticToken.swift
···
return result
}
}
+
+
extension SyntaxCursor {
+
public func collectSemanticTokens(_ sink: inout [SemanticToken]) {
+
if let (_, metadata) = node.token, let metadata {
+
for lineRange in singleLineRanges {
+
if let semanticToken = metadata.semanticToken(range: lineRange) {
+
sink.append(semanticToken)
+
}
+
}
+
}
+
+
for child in children {
+
child.collectSemanticTokens(&sink)
+
}
+
}
+
+
public var semanticTokens: [SemanticToken] {
+
var tokens: [SemanticToken] = []
+
collectSemanticTokens(&tokens)
+
return tokens
+
}
+
}
+1 -1
Sources/PterodactylSyntax/SyntaxCursor.swift
···
utf16Offset..<utf16Offset + node.utf16Length
}
-
init(lineMap: LineMap, node: SyntaxTree.Child, utf16Offset: Int) {
+
public init(lineMap: LineMap, node: SyntaxTree.Child, utf16Offset: Int) {
self.lineMap = lineMap
self.node = node
self.utf16Offset = utf16Offset
+1
Sources/PterodactylSyntax/Types.swift
···
default: true
}
}
+
public var canDetermineLayoutColumn: Bool {
switch self {
case .whitespace, .eof: false
+37
Tests/PterodactylBuildTests/Test.swift
···
import Testing
@testable import PterodactylBuild
+
@testable import PterodactylSyntax
@testable import llbuild2fx
struct BuildTests {
+
@Test
+
func testBlockLayout() async throws {
+
let code = """
+
theory Foo where
+
foo : bar
+
baz : sdf
+
"""
+
+
var lexer = PterodactylSyntax.Lexer(input: code)
+
let flatTokens = lexer.tokenize()
+
let blockTokens = BlockLayoutProcessor(tokens: flatTokens).layout()
+
+
#expect(
+
blockTokens.map(\.kind) == [
+
.keyword(.theory), .whitespace, .identifier, .whitespace, .keyword(.where), .blockBegin, .newline, .whitespace, .blockSep, .identifier, .whitespace,
+
.punctuation(.colon),
+
.whitespace, .identifier, .newline, .whitespace, .blockSep, .identifier, .whitespace, .punctuation(.colon), .whitespace, .identifier, .blockEnd, .eof
+
])
+
}
+
+
@Test
+
func testParse() async throws {
+
let code = """
+
theory Foo where
+
asdf : asdf
+
"""
+
+
var lexer = PterodactylSyntax.Lexer(input: code)
+
let flatTokens = lexer.tokenize()
+
let blockTokens = BlockLayoutProcessor(tokens: flatTokens).layout()
+
var parser = Parser(source: code, tokens: blockTokens)
+
Document.parse(&parser, recovery: [])
+
+
#expect(parser.diagnostics.isEmpty)
+
}
+
@Test
func testImports() async throws {
let group = LLBMakeDefaultDispatchGroup()
+4 -17
Tests/PterodactylLanguageServerTests/Test.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
//
// File.swift
// Pterodactyl
···
])
#expect(foo.debugDescription == expected.debugDescription)
-
}
-
-
@Test
-
func testSourceTreeManager() async throws {
-
let group = LLBMakeDefaultDispatchGroup()
-
let db = LLBInMemoryCASDatabase(group: group)
-
let functionCache = FXInMemoryFunctionCache(group: group)
-
let executor = FXLocalExecutor()
-
let engine = FXEngine(group: group, db: db, functionCache: functionCache, executor: executor)
-
let client = LLBCASFSClient(db)
-
let context = Context()
-
-
let manager = try await SourceTreeManager(buildEngine: engine, casClient: client, casContext: context)
-
let uri = "file://foo/bar/wooo.txt"
-
let contents = "hello world"
-
try await manager.setBufferText(uri: uri, text: contents)
-
try await #expect(manager.getBufferText(uri: uri) == contents)
}
}
+6
test.ptero
···
+
theory asdf where
+
foo : asdfs
+
foo : sdf
+
+
/* asdfasdf */
+
// asdf;lkj asdf;klj asdfkjh asdfjlkha sdfljk