Compare changes

Choose any two refs to compare.

+1
.gitignore
···
#
# SPDX-License-Identifier: MPL-2.0
+
.jj/*
.DS_Store
/.build
/Packages
+26 -2
Package.swift
···
.library(
name: "PterodactylBuild",
targets: ["PterodactylBuild"]
+
),
+
.executable(
+
name: "PterodactylLanguageServer",
+
targets: ["PterodactylLanguageServer"]
)
],
dependencies: [
-
// .package(url: "https://github.com/ChimeHQ/LanguageServer", branch: "main"),
+
.package(url: "https://github.com/ChimeHQ/LanguageServer", branch: "main"),
.package(url: "https://github.com/ChimeHQ/LanguageServerProtocol", branch: "main"),
.package(url: "https://github.com/apple/swift-llbuild2.git", branch: "main"),
-
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0")
+
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
+
.package(url: "https://github.com/apple/swift-log", from: "1.6.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
···
.product(name: "llbuild2fx", package: "swift-llbuild2")
]
),
+
.executableTarget(
+
name: "PterodactylLanguageServer",
+
dependencies: [
+
"LanguageServer",
+
.product(name: "llbuild2fx", package: "swift-llbuild2"),
+
.product(name: "Logging", package: "swift-log"),
+
"PterodactylBuild",
+
"PterodactylSyntax"
+
]
+
),
.testTarget(
name: "PterodactylBuildTests",
dependencies: [
"PterodactylBuild",
"PterodactylSyntax",
+
.product(name: "llbuild2fx", package: "swift-llbuild2")
+
]
+
),
+
.testTarget(
+
name: "PterodactylLanguageServerTests",
+
dependencies: [
+
"PterodactylBuild",
+
"PterodactylSyntax",
+
"PterodactylLanguageServer",
.product(name: "llbuild2fx", package: "swift-llbuild2")
]
),
+9 -9
Sources/PterodactylBuild/BuildContext.swift
···
}
extension BuildKey {
-
func computeValue(_ fi: FXFunctionInterface<Self>, _ ctx: Context) async throws -> ValueType {
+
public func computeValue(_ fi: FXFunctionInterface<Self>, _ ctx: Context) async throws -> ValueType {
try await computeValue(BuildContext(functionInterface: fi, context: ctx))
}
}
extension LLBCASFileTree {
-
static func load<X: FXKey>(id: LLBDataID, in ctx: BuildContext<X>) async throws -> LLBCASFileTree {
-
try await load(id: id, from: ctx.db, ctx.context).get()
+
public static func load<X: FXKey>(id: LLBDataID, in ctx: BuildContext<X>) async throws -> LLBCASFileTree {
+
try await load(id: id, from: ctx.db, ctx.context).get()
}
-
func remove<X: FXKey>(path: AbsolutePath, in ctx: BuildContext<X>) async throws -> LLBCASFileTree {
-
try await remove(path: path, in: ctx.db, ctx.context).get()
-
}
+
public func remove<X: FXKey>(path: AbsolutePath, in ctx: BuildContext<X>) async throws -> LLBCASFileTree {
+
try await remove(path: path, in: ctx.db, ctx.context).get()
+
}
-
func traverse<X: FXKey>(root: AbsolutePath, in ctx: BuildContext<X>, _ callback: (AbsolutePath, LLBDataID, LLBDirectoryEntry) async throws -> Void) async throws {
-
try await traverse(root: root, in: ctx.db, ctx.context, callback)
-
}
+
public func traverse<X: FXKey>(root: AbsolutePath, in ctx: BuildContext<X>, _ callback: (AbsolutePath, LLBDataID, LLBDirectoryEntry) async throws -> Void) async throws {
+
try await traverse(root: root, in: ctx.db, ctx.context, callback)
+
}
}
+8 -5
Sources/PterodactylBuild/Keys/Blob/GetLineMap.swift
···
import llbuild2fx
extension Keys.Blob {
-
struct GetLineMap: BuildKey {
-
let blobId: LLBDataID
+
public struct GetLineMap: BuildKey {
+
public let blobId: LLBDataID
+
public init(blobId: LLBDataID) {
+
self.blobId = blobId
+
}
-
typealias ValueType = PterodactylSyntax.LineMap
+
public typealias ValueType = PterodactylSyntax.LineMap
-
static let versionDependencies: [any FXVersioning.Type] = [ReadContents.self]
+
public static let versionDependencies: [any FXVersioning.Type] = [ReadContents.self]
-
func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
+
public func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
let code = try await ctx.request(ReadContents(blobId: blobId))
return PterodactylSyntax.LineMap(source: code)
}
+15 -8
Sources/PterodactylBuild/Keys/Blob/ParseDocument.swift
···
import Foundation
import PterodactylSyntax
import llbuild2fx
+
import Logging
extension Keys.Blob {
-
struct ParseDocument: BuildKey {
-
let blobId: LLBDataID
-
-
typealias ValueType = PterodactylSyntax.SyntaxTree
+
public struct ParseDocument: BuildKey {
+
public let blobId: LLBDataID
+
public init(blobId: LLBDataID) {
+
self.blobId = blobId
+
}
+
+
public struct ValueType: Codable, FXValue {
+
public let tree: PterodactylSyntax.SyntaxTree
+
public let diagnostics: [Diagnostic]
+
}
-
static let versionDependencies: [any FXVersioning.Type] = [ReadContents.self, Tokenise.self]
+
public static let versionDependencies: [any FXVersioning.Type] = [ReadContents.self, Tokenise.self]
-
func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
+
public func computeValue(_ ctx: BuildContext<Self>) async throws -> ValueType {
let code = try await ctx.request(ReadContents(blobId: blobId))
let tokens = try await ctx.request(Tokenise(blobId: blobId))
var parser = Parser(source: code, tokens: tokens)
-
PterodactylSyntax.Document.parse(&parser)
-
return parser.tree
+
PterodactylSyntax.Document.parse(&parser, recovery: [])
+
return ValueType(tree: parser.builder.tree, diagnostics: parser.diagnostics)
}
}
}
-35
Sources/PterodactylBuild/Keys/Blob/ParseImports.swift
···
}
-
private struct ImportParser {
-
private var lexer: PterodactylSyntax.Lexer
-
public private(set) var imports: [String] = []
-
public init(input: String) {
-
self.lexer = PterodactylSyntax.Lexer(input: input)
-
}
-
-
public mutating func parseHeader() {
-
while true {
-
guard let token = nextSignificantToken() else { return }
-
switch token.kind {
-
case .keyword(.import): parseImportStatement()
-
default: return
-
}
-
}
-
}
-
-
/// Returns the next non-whitespace token.
-
private mutating func nextSignificantToken() -> Token? {
-
var token = lexer.nextToken()
-
while token?.kind.isTrivia == true {
-
token = lexer.nextToken()
-
}
-
-
guard let token else { return nil}
-
return Token(kind: token.kind, text: token.text)
-
}
-
-
/// Parses a single `import xyz` line.
-
private mutating func parseImportStatement() {
-
guard let next = nextSignificantToken() else { return }
-
guard next.kind == .identifier else { return }
-
imports.append(next.text)
-
}
-
}
+6
Sources/PterodactylBuild/Keys/Blob/README.md
···
+
<!--
+
SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
+
SPDX-License-Identifier: MPL-2.0
+
-->
+
Certain operations do not require knowledge of the entire source tree, only a specific blob inside the source tree. These include import analysis, tokenisation, line maps, parsing, etc.
+8 -4
Sources/PterodactylBuild/Keys/Blob/ReadContents.swift
···
import llbuild2fx
extension Keys.Blob {
-
struct ReadContents: BuildKey {
+
public struct ReadContents: BuildKey {
let blobId: LLBDataID
-
typealias ValueType = String
+
public init(blobId: LLBDataID) {
+
self.blobId = blobId
+
}
-
static let versionDependencies: [any FXVersioning.Type] = []
+
public typealias ValueType = String
-
func computeValue(_ ctx: BuildContext<Self>) async throws -> String {
+
public static let versionDependencies: [any FXVersioning.Type] = []
+
+
public func computeValue(_ ctx: BuildContext<Self>) async throws -> String {
let contents = try await ctx.load(blobId)
return try await String(decoding: Data(ctx.read(blob: contents.blob!)), as: UTF8.self)
}
+2 -2
Sources/PterodactylBuild/Keys/SourceTree/GetUnitMap.swift
···
let sourceTreeId: LLBDataID
typealias ValueType = UnitMap
-
+
func computeValue(_ ctx: BuildContext<Self>) async throws -> UnitMap {
let sourceTree = try await LLBCASFileTree.load(id: sourceTreeId, in: ctx)
var units: [UnitName: UnitInfo] = [:]
try await sourceTree.traverse(root: .root, in: ctx) { path, blobID, directoryEntry in
-
let unitName = UnitName.fromPath(path)
+
let unitName = UnitName.fromPath(path)
units[unitName] = UnitInfo(path: path, blobId: blobID)
}
+3 -3
Sources/PterodactylBuild/Keys.swift
···
import Foundation
-
enum Keys {
-
enum Blob {}
-
enum SourceTree {}
+
public enum Keys {
+
public enum Blob {}
+
public enum SourceTree {}
}
+19
Sources/PterodactylLanguageServer/AbsolutePath+URI.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import LanguageServerProtocol
+
+
extension AbsolutePath {
+
public static func fromDocumentUri(_ uri: DocumentUri) throws -> Self {
+
guard let url = URL(string: uri) else {
+
throw URLError(.badURL)
+
}
+
guard url.isFileURL else {
+
throw URLError(.unsupportedURL)
+
}
+
return try Self(validating: url.path)
+
}
+
}
+17
Sources/PterodactylLanguageServer/Archive/LLBDeclFileTree+Singleton.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import TSCBasic
+
import llbuild2fx
+
+
extension LLBDeclFileTree {
+
public static func file(absolutePath: AbsolutePath, contents: String) -> Self {
+
let components = absolutePath.components
+
let leaf = LLBDeclFileTree.file(contents)
+
return components.dropFirst().reversed().reduce(leaf) { tail, name in
+
.directory(files: [name: tail])
+
}
+
}
+
}
+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()
+
// }
+
//}
+269
Sources/PterodactylLanguageServer/EventHandler.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import JSONRPC
+
import LanguageServer
+
import LanguageServerProtocol
+
import Logging
+
import PterodactylBuild
+
import PterodactylSyntax
+
import TSCBasic
+
import llbuild2fx
+
+
final class EventHandler {
+
private let connection: JSONRPCClientConnection
+
private let buildEngine: FXEngine
+
private let casContext: TSCUtility.Context
+
private let casClient: LLBCASFSClient
+
+
var storedBlobs: [DocumentUri: (blobId: LLBDataID, version: Int?)] = [:]
+
+
init(connection: JSONRPCClientConnection) {
+
self.connection = connection
+
+
let group = LLBMakeDefaultDispatchGroup()
+
let db = LLBInMemoryCASDatabase(group: group)
+
let functionCache = FXInMemoryFunctionCache(group: group)
+
let executor = FXLocalExecutor()
+
self.buildEngine = FXEngine(group: group, db: db, functionCache: functionCache, executor: executor)
+
self.casClient = LLBCASFSClient(db)
+
self.casContext = Context()
+
}
+
+
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))
+
}
+
}
+
+
+
extension EventHandler: LanguageServer.EventHandler {
+
var textDocumentSinkOptions: TextDocumentSyncOptions {
+
TextDocumentSyncOptions(
+
openClose: true,
+
change: .full,
+
+
save: .optionA(false)
+
)
+
}
+
+
var completionOptions: CompletionOptions {
+
CompletionOptions(
+
workDoneProgress: false,
+
triggerCharacters: [], // TODO
+
allCommitCharacters: nil,
+
resolveProvider: false,
+
completionItem: nil
+
)
+
}
+
+
var semanticTokensLegend: SemanticTokensLegend {
+
SemanticTokensLegend(tokenTypes: SemanticTokenTypes.allStrings, tokenModifiers: SemanticTokenModifiers.allStrings)
+
}
+
+
var semanticTokensOptions: SemanticTokensOptions {
+
SemanticTokensOptions(legend: semanticTokensLegend, full: .optionB(SemanticTokensClientCapabilities.Requests.Full(delta: false)))
+
}
+
+
func initialize(id: JSONId, params: InitializeParams) async -> Response<InitializationResponse> {
+
Logger.shared.debug("Received initialize request")
+
var serverCapabilities = ServerCapabilities()
+
serverCapabilities.textDocumentSync = .optionA(textDocumentSinkOptions)
+
serverCapabilities.completionProvider = completionOptions
+
serverCapabilities.hoverProvider = .optionA(false)
+
serverCapabilities.semanticTokensProvider = .optionA(semanticTokensOptions)
+
serverCapabilities.foldingRangeProvider = .optionA(true)
+
let response = InitializationResponse(
+
capabilities: serverCapabilities,
+
serverInfo: nil
+
)
+
return .success(response)
+
}
+
+
func textDocumentDidChange(_ params: DidChangeTextDocumentParams) async {
+
guard let text = params.contentChanges.first?.text else { return }
+
do {
+
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 {
+
let text = params.textDocument.text
+
do {
+
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> {
+
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(SemanticTokensResponse(SemanticTokens(resultId: nil, tokens: cursor.semanticTokens)))
+
} catch {
+
return .success(nil)
+
}
+
}
+
+
func foldingRange(id: JSONId, params: FoldingRangeParams) async -> Response<FoldingRangeResponse> {
+
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)
+
}
+
}
+
+
+
func typeHierarchySupertypes(
+
id: JSONRPC.JSONId,
+
params: TypeHierarchySupertypesParams
+
) async -> Response<TypeHierarchySupertypesResponse> {
+
.success(nil)
+
}
+
+
func typeHierarchySubtypes(
+
id: JSONId,
+
params: TypeHierarchySubtypesParams
+
) async -> Response<TypeHierarchySubtypesResponse> {
+
.success(nil)
+
}
+
+
func internalError(_ error: any Error) async {
+
Logger.shared.error("Received error: \(error)")
+
}
+
+
func initialized(_ params: InitializedParams) async {
+
+
}
+
+
func exit() async {
+
Logger.shared.info("Received exit notification")
+
Foundation.exit(0)
+
}
+
+
func diagnostics(id: JSONId, params: DocumentDiagnosticParams) async -> Response<DocumentDiagnosticReport> {
+
return .success(.init(kind: .unchanged))
+
}
+
+
+
+
func textDocumentDidClose(_ params: DidCloseTextDocumentParams) async {
+
+
}
+
+
func textDocumentWillSave(_ params: WillSaveTextDocumentParams) async {
+
+
}
+
+
func textDocumentDidSave(_ params: DidSaveTextDocumentParams) async {
+
+
}
+
+
func protocolCancelRequest(_ params: CancelParams) async {
+
+
}
+
+
func protocolSetTrace(_ params: SetTraceParams) async {
+
+
}
+
+
func workspaceDidChangeWatchedFiles(_ params: DidChangeWatchedFilesParams) async {
+
+
}
+
+
func windowWorkDoneProgressCancel(_ params: WorkDoneProgressCancelParams) async {
+
+
}
+
+
func workspaceDidChangeWorkspaceFolders(_ params: DidChangeWorkspaceFoldersParams) async {
+
+
}
+
+
func workspaceDidChangeConfiguration(_ params: DidChangeConfigurationParams) async {
+
+
}
+
+
func workspaceDidCreateFiles(_ params: CreateFilesParams) async {
+
+
}
+
+
func workspaceDidRenameFiles(_ params: RenameFilesParams) async {
+
+
}
+
+
func workspaceDidDeleteFiles(_ params: DeleteFilesParams) async {
+
+
}
+
+
func shutdown(id: JSONId) async {}
+
func workspaceInlayHintRefresh(id: JSONId) async {}
+
func workspaceExecuteCommand(id: JSONId, params: ExecuteCommandParams) async -> Response<LSPAny?> { .success(nil) }
+
func workspaceWillCreateFiles(id: JSONId, params: CreateFilesParams) async -> Response<WorkspaceEdit?> { .success(nil) }
+
func workspaceWillRenameFiles(id: JSONId, params: RenameFilesParams) async -> Response<WorkspaceEdit?> { .success(nil) }
+
func workspaceWillDeleteFiles(id: JSONId, params: DeleteFilesParams) async -> Response<WorkspaceEdit?> { .success(nil) }
+
func workspaceSymbol(id: JSONId, params: WorkspaceSymbolParams) async -> Response<WorkspaceSymbolResponse> { .success(nil) }
+
// func workspaceSymbolResolve(id: JSONId, params: WorkspaceSymbol) async -> Response<WorkspaceSymbol> { .success(nil) }
+
func textDocumentWillSaveWaitUntil(id: JSONId, params: WillSaveTextDocumentParams) async -> Response<[TextEdit]?> { .success(nil) }
+
func completion(id: JSONId, params: CompletionParams) async -> Response<CompletionResponse> {
+
.success(CompletionResponse(.optionB(.init(isIncomplete: false, items: []))))
+
}
+
// func completionItemResolve(id: JSONId, params: CompletionItem) async -> Response<CompletionItem> { .success(nil) }
+
func hover(id: JSONId, params: TextDocumentPositionParams) async -> Response<HoverResponse> { .success(nil) }
+
func signatureHelp(id: JSONId, params: TextDocumentPositionParams) async -> Response<SignatureHelpResponse> { .success(nil) }
+
func declaration(id: JSONId, params: TextDocumentPositionParams) async -> Response<DeclarationResponse> { .success(nil) }
+
func definition(id: JSONId, params: TextDocumentPositionParams) async -> Response<DefinitionResponse> { .success(nil) }
+
func typeDefinition(id: JSONId, params: TextDocumentPositionParams) async -> Response<TypeDefinitionResponse> { .success(nil) }
+
func implementation(id: JSONId, params: TextDocumentPositionParams) async -> Response<ImplementationResponse> { .success(nil) }
+
func documentHighlight(id: JSONId, params: DocumentHighlightParams) async -> Response<DocumentHighlightResponse> { .success(nil) }
+
func documentSymbol(id: JSONId, params: DocumentSymbolParams) async -> Response<DocumentSymbolResponse> { .success(nil) }
+
func codeAction(id: JSONId, params: CodeActionParams) async -> Response<CodeActionResponse> { .success(nil) }
+
// func codeActionResolve(id: JSONId, params: CodeAction) async -> Response<CodeAction> { .success(nil) }
+
func codeLens(id: JSONId, params: CodeLensParams) async -> Response<CodeLensResponse> { .success(nil) }
+
// func codeLensResolve(id: JSONId, params: CodeLens) async -> Response<CodeLens> { .success(nil) }
+
func selectionRange(id: JSONId, params: SelectionRangeParams) async -> Response<SelectionRangeResponse> { .success(nil) }
+
func linkedEditingRange(id: JSONId, params: LinkedEditingRangeParams) async -> Response<LinkedEditingRangeResponse> { .success(nil) }
+
func prepareCallHierarchy(id: JSONId, params: CallHierarchyPrepareParams) async -> Response<CallHierarchyPrepareResponse> { .success(nil) }
+
func prepareRename(id: JSONId, params: PrepareRenameParams) async -> Response<PrepareRenameResponse> { .success(nil) }
+
func prepareTypeHeirarchy(id: JSONId, params: TypeHierarchyPrepareParams) async -> Response<PrepareTypeHeirarchyResponse> { .success(nil) }
+
func rename(id: JSONId, params: RenameParams) async -> Response<RenameResponse> { .success(nil) }
+
func inlayHint(id: JSONId, params: InlayHintParams) async -> Response<InlayHintResponse> { .success(nil) }
+
func inlayHintResolve(id: JSONId, params: InlayHint) async -> Response<InlayHintResponse> { .success(nil) }
+
func documentLink(id: JSONId, params: DocumentLinkParams) async -> Response<DocumentLinkResponse> { .success(nil) }
+
// func documentLinkResolve(id: JSONId, params: DocumentLink) async -> Response<DocumentLink> { .success(nil) }
+
// func documentColor(id: JSONId, params: DocumentColorParams) async -> Response<DocumentColorResponse> { .success(nil) }
+
// func colorPresentation(id: JSONId, params: ColorPresentationParams) async -> Response<ColorPresentationResponse> { .success(nil) }
+
func formatting(id: JSONId, params: DocumentFormattingParams) async -> Response<FormattingResult> { .success(nil) }
+
func rangeFormatting(id: JSONId, params: DocumentRangeFormattingParams) async -> Response<FormattingResult> { .success(nil) }
+
func onTypeFormatting(id: JSONId, params: DocumentOnTypeFormattingParams) async -> Response<FormattingResult> { .success(nil) }
+
func references(id: JSONId, params: ReferenceParams) async -> Response<ReferenceResponse> { .success(nil) }
+
func moniker(id: JSONId, params: MonikerParams) async -> Response<MonikerResponse> { .success(nil) }
+
func semanticTokensFullDelta(id: JSONId, params: SemanticTokensDeltaParams) async -> Response<SemanticTokensDeltaResponse> { .success(nil) }
+
func semanticTokensRange(id: JSONId, params: SemanticTokensRangeParams) async -> Response<SemanticTokensResponse> { .success(nil) }
+
func callHierarchyIncomingCalls(id: JSONId, params: CallHierarchyIncomingCallsParams) async -> Response<CallHierarchyIncomingCallsResponse> { .success(nil) }
+
func callHierarchyOutgoingCalls(id: JSONId, params: CallHierarchyOutgoingCallsParams) async -> Response<CallHierarchyOutgoingCallsResponse> { .success(nil) }
+
func custom(id: JSONId, method: String, params: LSPAny) async -> Response<LSPAny> { .success(nil) }
+
}
+63
Sources/PterodactylLanguageServer/Logger.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import Logging
+
+
struct FileLogHandler: LogHandler {
+
private let label: String
+
private let fileHandle: FileHandle
+
var logLevel: Logger.Level = .info
+
var metadata: Logger.Metadata = [:]
+
+
init(label: String, fileURL: URL) {
+
self.label = label
+
+
// Ensure file exists
+
if !FileManager.default.fileExists(atPath: fileURL.path) {
+
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
+
}
+
+
// Open file for updating (read/write)
+
self.fileHandle = try! FileHandle(forUpdating: fileURL)
+
self.fileHandle.seekToEndOfFile()
+
}
+
+
subscript(metadataKey key: String) -> Logger.Metadata.Value? {
+
get { metadata[key] }
+
set { metadata[key] = newValue }
+
}
+
+
func log(
+
level: Logger.Level,
+
message: Logger.Message,
+
metadata: Logger.Metadata?,
+
source: String,
+
file: String,
+
function: String,
+
line: UInt
+
) {
+
var fullMetadata = self.metadata
+
metadata?.forEach { fullMetadata[$0] = $1 }
+
+
let line = "[\(level)] \(message)\n"
+
if let data = line.data(using: .utf8) {
+
fileHandle.write(data)
+
}
+
}
+
}
+
+
extension Logger {
+
static let shared: Self = {
+
let logFile = URL(fileURLWithPath: "/tmp/pterodactyl-language-server.log")
+
+
LoggingSystem.bootstrap { label in
+
FileLogHandler(label: label, fileURL: logFile)
+
}
+
+
var logger = Logger(label: "org.pterodactyl.language-server")
+
logger.logLevel = .debug
+
return logger
+
}()
+
}
+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()
+
}
+
}
+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: ""))
}
}
-53
Sources/PterodactylSyntax/Cursor.swift
···
-
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
-
//
-
// SPDX-License-Identifier: MPL-2.0
-
-
import Foundation
-
import LanguageServerProtocol
-
-
extension SyntaxTree {
-
public final class Cursor {
-
public let lineMap: LineMap
-
public let node: SyntaxTree.Child
-
public let utf16Offset: Int
-
-
public private(set) lazy var children: [Cursor] = {
-
var children: [Cursor] = []
-
var utf16Offset = utf16Offset
-
for childNode in node.children {
-
children.append(Self(lineMap: lineMap, node: childNode, utf16Offset: utf16Offset))
-
utf16Offset += childNode.utf16Length
-
}
-
-
return children
-
}()
-
-
public var utf16Range: Range<Int> {
-
utf16Offset..<utf16Offset + node.utf16Length
-
}
-
-
init(lineMap: LineMap, node: SyntaxTree.Child, utf16Offset: Int) {
-
self.lineMap = lineMap
-
self.node = node
-
self.utf16Offset = utf16Offset
-
}
-
}
-
}
-
-
extension SyntaxTree.Cursor {
-
public func firstChild<T>(mapping: (SyntaxTree.Cursor) -> T?) -> T? {
-
for child in children {
-
if let result = mapping(child) {
-
return result
-
} else {
-
continue
-
}
-
}
-
return nil
-
}
-
-
public func children<T>(mapping: (SyntaxTree.Cursor) -> T?) -> [T] {
-
children.compactMap(mapping)
-
}
-
}
-
+26 -11
Sources/PterodactylSyntax/Diagnostic.swift
···
//
// SPDX-License-Identifier: MPL-2.0
-
public struct Diagnostic: Equatable {
-
enum Severity: Equatable {
-
case error
-
case warning
-
case note
-
}
-
let message: String
-
let severity: Severity
-
/// Absolute UTF-16 code unit offsets from start of source
-
let absoluteRange: Range<Int>
+
import LanguageServerProtocol
+
+
public struct Diagnostic: Equatable, Codable, Sendable {
+
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
self.severity = severity
-
self.absoluteRange = absoluteRange
+
self.absoluteUtf16Range = absoluteRange
}
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
+
)
}
}
+4 -5
Sources/PterodactylSyntax/FoldingRanges.swift
···
case reverse
}
-
extension Array {
-
fileprivate func apply(symmetry: ArraySymmetry) -> any Collection<Element> {
+
fileprivate extension Array {
+
func apply(symmetry: ArraySymmetry) -> any Collection<Element> {
switch symmetry {
case .identity: self
case .reverse: reversed()
···
}
}
-
extension SyntaxTree.Cursor {
-
private func firstVisibleNode(under symmetry: ArraySymmetry) -> SyntaxTree.Cursor? {
+
extension SyntaxCursor {
+
private func firstVisibleNode(under symmetry: ArraySymmetry) -> SyntaxCursor? {
switch node {
case .token(let token, _):
return token.kind.isVisible ? self : nil
case .tree:
for child in children.apply(symmetry: symmetry) {
if let visibleChild = child.firstVisibleNode(under: symmetry) { return visibleChild }
-
continue
}
return nil
+7 -7
Sources/PterodactylSyntax/Grammar/Document/Import.swift
···
static let kind = SyntaxTreeKind(name: "import.name")
static let kinds = [kind]
-
static func before(_ parser: inout Parser) -> Bool {
+
static func precondition(_ parser: inout Parser) -> Bool {
parser.isAt(kind: .identifier)
}
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
parser.expect(kind: .identifier, metadata: TokenMetadata(semanticTokenType: .namespace))
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
parser.expect(kind: .identifier, metadata: TokenMetadata(semanticTokenType: .namespace), recovery: recovery)
return ParseResult(kind: Self.kind)
}
}
···
static let kind = SyntaxTreeKind(name: "import")
static let kinds = [kind]
-
static func before(_ parser: inout Parser) -> Bool {
+
static func precondition(_ parser: inout Parser) -> Bool {
parser.isAt(kind: .keyword(.import))
}
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
parser.expect(kind: .keyword(.import), metadata: TokenMetadata(semanticTokenType: .keyword))
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
parser.expect(kind: .keyword(.import), metadata: TokenMetadata(semanticTokenType: .keyword), recovery: recovery)
parser.eatTrivia()
-
ImportName.parse(&parser)
+
ImportName.parse(&parser, recovery: recovery)
return ParseResult(kind: Self.kind)
}
}
+13 -11
Sources/PterodactylSyntax/Grammar/Document/Theory/Declaration/Lhs.swift
···
import Foundation
-
enum Lhs: Grammar {
-
static let kind = SyntaxTreeKind(name: "declaration.lhs")
-
static let kinds = [kind]
-
-
static func before(_ parser: inout Parser) -> Bool {
-
parser.isAt(kind: .identifier)
-
}
-
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
parser.expect(kind: .identifier, metadata: TokenMetadata(semanticTokenType: .method))
-
return ParseResult(kind: Self.kind)
+
extension Declaration {
+
enum Lhs: Grammar {
+
static let kind = SyntaxTreeKind(name: "declaration.lhs")
+
static let kinds = [kind]
+
+
static func precondition(_ parser: inout Parser) -> Bool {
+
parser.isAt(kind: .identifier)
+
}
+
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
parser.expect(kind: .identifier, metadata: TokenMetadata(semanticTokenType: .method), recovery: recovery)
+
return ParseResult(kind: Self.kind)
+
}
}
}
+13 -11
Sources/PterodactylSyntax/Grammar/Document/Theory/Declaration/Rhs.swift
···
import Foundation
-
enum Rhs: Grammar {
-
static let kind = SyntaxTreeKind(name: "declaration.lhs")
-
static let kinds = [kind]
-
-
static func before(_ parser: inout Parser) -> Bool {
-
parser.isAt(kind: .identifier)
-
}
-
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
parser.expect(kind: .identifier, metadata: TokenMetadata(semanticTokenType: .method))
-
return ParseResult(kind: Self.kind)
+
extension Declaration {
+
enum Rhs: Grammar {
+
static let kind = SyntaxTreeKind(name: "declaration.lhs")
+
static let kinds = [kind]
+
+
static func precondition(_ parser: inout Parser) -> Bool {
+
parser.isAt(kind: .identifier)
+
}
+
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
parser.expect(kind: .identifier, metadata: TokenMetadata(semanticTokenType: .method), recovery: recovery)
+
return ParseResult(kind: Self.kind)
+
}
}
}
+8 -7
Sources/PterodactylSyntax/Grammar/Document/Theory/Declaration.swift
···
static let kinds = [Kinds.claim, Kinds.refine, Kinds.define]
-
static func before(_ parser: inout Parser) -> Bool {
-
Lhs.before(&parser)
+
static func precondition(_ parser: inout Parser) -> Bool {
+
Lhs.precondition(&parser)
}
static let punctuationMap: [Punctuation: SyntaxTreeKind] = [
···
.doubleRightArrow: Kinds.define
]
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
Lhs.parse(&parser)
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
let punctuations = punctuationMap.keys.map { TokenKind.punctuation($0) }
+
Lhs.parse(&parser, recovery: recovery.union(punctuations))
parser.eatTrivia()
var kind: SyntaxTreeKind = .error
···
}
parser.eatTrivia()
-
Rhs.parse(&parser)
+
Rhs.parse(&parser, recovery: recovery)
return ParseResult(kind: kind)
}
}
extension SyntaxView<Declaration> {
-
var lhs: SyntaxView<Lhs>? { matchingSubview() }
-
var rhs: SyntaxView<Rhs>? { matchingSubview() }
+
var lhs: SyntaxView<Declaration.Lhs>? { matchingSubview() }
+
var rhs: SyntaxView<Declaration.Rhs>? { matchingSubview() }
}
+5 -13
Sources/PterodactylSyntax/Grammar/Document/Theory/TheoryBlock.swift
···
static let kind = SyntaxTreeKind(name: "theory.block")
static let kinds = [kind]
-
static func before(_ parser: inout Parser) -> Bool {
+
static func precondition(_ parser: inout Parser) -> Bool {
parser.isAt(kind: .keyword(.where))
}
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
parser.expect(kind: .keyword(.where), metadata: TokenMetadata(semanticTokenType: .keyword))
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
parser.expect(kind: .keyword(.where), metadata: TokenMetadata(semanticTokenType: .keyword), recovery: recovery.union([.keyword(.theory), .blockComment(terminated: true), .lineComment]))
-
parser.eatTrivia()
if parser.eat(kind: .blockBegin, metadata: nil) {
parser.eatTrivia()
-
-
while Declaration.tryParse(&parser) {
+
while parser.eat(kind: .blockSep, metadata: nil) {
+
Declaration.parse(&parser, recovery: recovery.union([.blockSep, .blockEnd]))
parser.eatTrivia()
-
if parser.eat(kind: .blockSep, metadata: nil) {
-
parser.eatTrivia()
-
continue
-
} else {
-
break
-
}
}
-
_ = parser.eat(kind: .blockEnd, metadata: nil)
}
+2 -2
Sources/PterodactylSyntax/Grammar/Document/Theory/TheoryName.swift
···
static let kind = SyntaxTreeKind(name: "theory.name")
static let kinds = [kind]
-
static func before(_ parser: inout Parser) -> Bool {
+
static func precondition(_ parser: inout Parser) -> Bool {
parser.isAt(kind: .identifier)
}
-
static func inside(_ parser: inout Parser) -> ParseResult {
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
parser.advance(metadata: TokenMetadata(semanticTokenType: .interface))
return ParseResult(kind: Self.kind)
}
+6 -6
Sources/PterodactylSyntax/Grammar/Document/Theory.swift
···
enum Theory: Grammar {
static let kind = SyntaxTreeKind(name: "theory")
static let kinds = [kind]
-
-
static func before(_ parser: inout Parser) -> Bool {
+
+
static func precondition(_ parser: inout Parser) -> Bool {
parser.isAt(kind: .keyword(.theory))
}
-
static func inside(_ parser: inout Parser) -> ParseResult {
-
parser.expect(kind: .keyword(.theory), metadata: TokenMetadata(semanticTokenType: .keyword))
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
+
parser.expect(kind: .keyword(.theory), metadata: TokenMetadata(semanticTokenType: .keyword), recovery: recovery)
parser.eatTrivia()
-
if !TheoryName.tryParse(&parser) {
+
if !TheoryName.tryParse(&parser, recovery: recovery) {
parser.advance(error: "Expected theory name")
}
parser.eatTrivia()
-
TheoryBlock.parse(&parser)
+
TheoryBlock.parse(&parser, recovery: recovery)
return ParseResult(kind: Self.kind)
}
+5 -5
Sources/PterodactylSyntax/Grammar/Document.swift
···
public static let kind = SyntaxTreeKind(name: "document")
public static let kinds = [kind]
-
public static func before(_ parser: inout Parser) -> Bool {
+
public static func precondition(_ parser: inout Parser) -> Bool {
true
}
-
public static func inside(_ parser: inout Parser) -> ParseResult {
+
public static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult {
parser.eatTrivia()
// Parse imports
while !parser.isAt(kind: .eof) {
parser.eatTrivia()
-
if Theory.before(&parser) { break }
+
if Theory.precondition(&parser) { break }
-
if !Import.tryParse(&parser) {
+
if !Import.tryParse(&parser, recovery: recovery) {
parser.advance(error: "Expected to see either an import or a theory declaration, but instead got \(parser.currentToken.kind): \(parser.currentToken.text)")
}
}
// Theories section
while !parser.isAt(kind: .eof) {
-
if !Theory.tryParse(&parser) {
+
if !Theory.tryParse(&parser, recovery: recovery) {
if parser.isAt(kindSatisfying: \.isVisible) {
let token = parser.currentToken
parser.advance(error: "Unexpected token: \(token.kind)")
+23 -17
Sources/PterodactylSyntax/Grammar.swift
···
import Foundation
+
/// This is an abstraction of grammatical productions.
public protocol Grammar: Sendable {
+
/// The kinds of tree that the production produces.
static var kinds: [SyntaxTreeKind] { get }
-
static func before(_ parser: inout Parser) -> Bool
-
static func inside(_ parser: inout Parser) -> ParseResult
+
+
/// Indicates whether the current parser state is consistent with the grammatical production starting here. When a given grammatical element is optional, this can be used to avoid backtracking. This is a *precondition* for parsing.
+
static func precondition(_ parser: inout Parser) -> Bool
+
+
/// Parse the grammatical production, assuming the precondition indicated by ``precondition(_:)``. This function should not be called outside this module (instead, use ``parse(_:recovery:)`` and ``tryParse(_:recovery:)``.
+
static func inside(_ parser: inout Parser, recovery: Set<TokenKind>) -> ParseResult
+
}
+
+
extension Grammar {
+
public static func tryParse(_ parser: inout Parser, recovery: Set<TokenKind>) -> Bool {
+
guard !parser.isEndOfFile && precondition(&parser) else { return false }
+
parse(&parser, recovery: recovery)
+
return true
+
}
+
+
public static func parse(_ parser: inout Parser, recovery: Set<TokenKind>) {
+
let mark = parser.builder.open()
+
let result = inside(&parser, recovery: recovery)
+
parser.builder.close(mark: mark, kind: result.kind, metadata: result.metadata)
+
}
}
public struct ParseResult {
public var kind: SyntaxTreeKind
public var metadata: SyntaxTreeMetadata? = nil
-
+
public init(kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata? = nil) {
self.kind = kind
self.metadata = metadata
}
}
-
-
public extension Grammar {
-
static func tryParse(_ parser: inout Parser) -> Bool {
-
guard !parser.isEndOfFile && before(&parser) else { return false }
-
parse(&parser)
-
return true
-
}
-
-
static func parse(_ parser: inout Parser) {
-
let mark = parser.open()
-
let result = inside(&parser)
-
parser.close(mark: mark, kind: result.kind, metadata: result.metadata)
-
}
-
}
+42
Sources/PterodactylSyntax/ImportParser.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
public struct ImportParser {
+
private var lexer: PterodactylSyntax.Lexer
+
public private(set) var imports: [String] = []
+
+
public init(input: String) {
+
self.lexer = PterodactylSyntax.Lexer(input: input)
+
}
+
+
public mutating func parseHeader() {
+
while true {
+
guard let token = nextSignificantToken() else { return }
+
switch token.kind {
+
case .keyword(.import): parseImportStatement()
+
default: return
+
}
+
}
+
}
+
+
/// Returns the next non-whitespace token.
+
private mutating func nextSignificantToken() -> Token? {
+
var token = lexer.nextToken()
+
while token?.kind.isTrivia == true {
+
token = lexer.nextToken()
+
}
+
+
guard let token else { return nil }
+
return Token(kind: token.kind, text: token.text)
+
}
+
+
/// Parses a single `import xyz` line.
+
private mutating func parseImportStatement() {
+
guard let next = nextSignificantToken() else { return }
+
guard next.kind == .identifier else { return }
+
imports.append(next.text)
+
}
+
}
+1
Sources/PterodactylSyntax/Lexer.swift
···
import Foundation
+
/// This tokenises a string, without handling block layout at all. The tokens produced here should be fed into the ``BlockLayoutProcessor``.
public struct Lexer {
private let input: String
private var index: String.Index
+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" {
+61 -83
Sources/PterodactylSyntax/Parser.swift
···
// SPDX-License-Identifier: MPL-2.0
public struct Parser {
-
enum Event: Equatable {
-
case open(kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata?)
-
case close
-
case advance(metadata: TokenMetadata?)
-
}
-
public struct MarkOpened {
internal let index: Int
}
···
}
public private(set) var diagnostics: [Diagnostic] = []
+
public var builder: SyntaxTreeBuilder = SyntaxTreeBuilder()
-
private var fuel: Int = 0
+
private var inError: Bool = false
private var position: Int = 0
-
private var events: [Event] = []
private var absoluteUtf16Offset: Int = 0
public var absoluteRangeAtCursor: Range<Int> {
return absoluteUtf16Offset..<absoluteUtf16Offset
···
}
-
public mutating func open() -> MarkOpened {
-
let mark = MarkOpened(index: events.count)
-
events.append(.open(kind: .error, metadata: nil))
-
return mark
-
}
-
-
public mutating func close(mark: MarkOpened, kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata?) {
-
events[mark.index] = .open(kind: kind, metadata: metadata)
-
events.append(.close)
-
}
-
public mutating func advance(metadata: TokenMetadata?) {
precondition(!isEndOfFile)
-
events.append(.advance(metadata: metadata))
+
builder.advance(token: currentToken, metadata: metadata)
absoluteUtf16Offset += currentToken.utf16Length
position += 1
-
fuel = 256
}
-
public mutating func advance(error: String?, metadata: TokenMetadata? = nil) {
-
let mark = open()
-
if let error {
-
let diagnostic = Diagnostic(
-
message: error,
-
absoluteRange: absoluteRangeOfCurrentToken
-
)
+
public mutating func advance(error: String, metadata: TokenMetadata? = nil) {
+
let mark = builder.open()
+
let diagnostic = Diagnostic(
+
message: error,
+
absoluteRange: absoluteRangeOfCurrentToken
+
)
-
diagnostics.append(diagnostic)
-
}
+
diagnostics.append(diagnostic)
advance(metadata: metadata)
-
close(mark: mark, kind: .error, metadata: nil)
-
}
-
-
public mutating func lookahead(_ k: Int) -> TokenKind? {
-
precondition(fuel > 0, "Parser is stuck!")
-
fuel -= 1
-
let index = position + k
-
guard tokens.indices.contains(index) else { return nil }
-
return tokens[index].kind
+
builder.close(mark: mark, kind: .error, metadata: nil)
}
-
public mutating func eat(kindSatisfying predicate: (TokenKind) -> Bool, metadata: TokenMetadata?) -> Bool {
-
guard !isEndOfFile && isAt(kindSatisfying: predicate) else { return false }
+
public mutating func eat(kind: TokenKind, metadata: TokenMetadata?) -> Bool {
+
guard !isEndOfFile && isAt(kindSatisfying: { $0 == kind }) else { return false }
advance(metadata: metadata)
return true
}
-
public mutating func eat(kind: TokenKind, metadata: TokenMetadata?) -> Bool {
-
eat(kindSatisfying: { $0 == kind }, metadata: metadata)
+
enum ControlFlow {
+
case `continue`
+
case `break`
}
-
public mutating func expect(kind: TokenKind, metadata: TokenMetadata?, error: String? = nil) {
-
if eat(kind: kind, metadata: metadata) { return }
-
let diagnostic = Diagnostic(
-
message: error ?? "Expected \(kind) but got \(currentToken.kind): `\(currentToken.text)`",
-
absoluteRange: absoluteRangeAtCursor
-
)
-
diagnostics.append(diagnostic)
+
mutating func ate(kind: TokenKind, metadata: TokenMetadata?) -> ControlFlow {
+
guard eat(kind: kind, metadata: metadata) else { return .continue }
+
inError = false
+
eatTrivia()
+
return .break
}
-
public var tree: SyntaxTree {
-
var events = events
-
var stack: [SyntaxTree.Builder] = []
-
var cursor: Int = 0
+
mutating func recoverUntil(_ anchors: Set<TokenKind>, expected: TokenKind, error: String? = nil) {
+
var discardTokens: [Token] = []
+
let startOffset = absoluteUtf16Offset
-
precondition(events.popLast() == .close)
+
while !self.isAt(kindSatisfying: { anchors.contains($0) }) {
+
if isEndOfFile { break }
+
let token = currentToken
+
advance(metadata: nil)
+
discardTokens.append(token)
+
}
-
for event in events {
-
switch event {
-
case .open(let kind, let metadata):
-
stack.append(SyntaxTree.Builder(kind: kind, metadata: metadata, children: []))
-
case .close:
-
let tree = stack.popLast()!
-
stack.modifyLast { last in
-
last.children.append(.tree(tree.tree))
-
}
-
case .advance(let metadata):
-
let token = tokens[cursor]
-
cursor += 1
-
stack.modifyLast { last in
-
last.children.append(.token(token, metadata: metadata))
-
}
+
var endOffset = startOffset
+
+
let error = error ?? "Expected \(expected) but got \(discardTokens)"
+
+
if discardTokens.isEmpty {
+
if !inError {
+
inError = true
+
diagnostics.append(Diagnostic(message: error, absoluteRange: absoluteRangeAtCursor))
+
}
+
return
+
} else {
+
let mark = builder.open()
+
for discardToken in discardTokens {
+
endOffset += discardToken.utf16Length
+
}
+
+
builder.close(mark: mark, kind: .error, metadata: nil)
+
+
if !inError {
+
inError = true
+
diagnostics.append(Diagnostic(message: error, absoluteRange: startOffset..<endOffset))
}
}
+
}
-
assert(stack.count == 1)
-
return stack.popLast()!.tree
+
public mutating func expect(kind: TokenKind, metadata: TokenMetadata?, recovery: Set<TokenKind>, error: String? = nil) {
+
var anchors = recovery
+
if ate(kind: kind, metadata: metadata) == .break { return }
+
anchors.insert(kind)
+
recoverUntil(anchors, expected: kind, error: error)
+
let _ = ate(kind: kind, metadata: metadata)
}
+
}
+
extension Parser {
mutating func eatTrivium() -> Bool {
switch currentToken.kind {
-
case .whitespace:
+
case .whitespace, .newline:
advance(metadata: nil)
return true
case .blockComment(let terminated):
···
mutating func eatTrivia() {
while !isEndOfFile && eatTrivium() {}
-
}
-
-
}
-
-
extension Array {
-
fileprivate mutating func modifyLast(_ modifier: (inout Element) -> Void) {
-
if var last = popLast() {
-
modifier(&last)
-
append(last)
-
}
}
}
+23 -1
Sources/PterodactylSyntax/SemanticToken.swift
···
}
}
-
extension SyntaxTree.Cursor {
+
extension SyntaxCursor {
var singleLineRanges: [SingleLineRange] {
var result: [SingleLineRange] = []
var location = lineMap.location(at: utf16Offset)
···
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
+
}
+
}
+51
Sources/PterodactylSyntax/SyntaxCursor.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
import LanguageServerProtocol
+
+
/// This is a โ€œred treeโ€ in the sense of Roslyn. In essence it instruments syntax trees with non-relative location information.
+
public final class SyntaxCursor {
+
public let lineMap: LineMap
+
public let node: SyntaxTree.Child
+
public let utf16Offset: Int
+
+
public private(set) lazy var children: [SyntaxCursor] = {
+
var children: [SyntaxCursor] = []
+
var utf16Offset = utf16Offset
+
for childNode in node.children {
+
children.append(Self(lineMap: lineMap, node: childNode, utf16Offset: utf16Offset))
+
utf16Offset += childNode.utf16Length
+
}
+
+
return children
+
}()
+
+
public var utf16Range: Range<Int> {
+
utf16Offset..<utf16Offset + node.utf16Length
+
}
+
+
public init(lineMap: LineMap, node: SyntaxTree.Child, utf16Offset: Int) {
+
self.lineMap = lineMap
+
self.node = node
+
self.utf16Offset = utf16Offset
+
}
+
}
+
+
extension SyntaxCursor {
+
public func firstChild<T>(mapping: (SyntaxCursor) -> T?) -> T? {
+
for child in children {
+
if let result = mapping(child) {
+
return result
+
} else {
+
continue
+
}
+
}
+
return nil
+
}
+
+
public func children<T>(mapping: (SyntaxCursor) -> T?) -> [T] {
+
children.compactMap(mapping)
+
}
+
}
+2 -1
Sources/PterodactylSyntax/SyntaxTree.swift
···
import Foundation
+
/// This is a โ€œgreen treeโ€ in the sense of Roslyn.
public struct SyntaxTree: Codable, Sendable {
public let kind: SyntaxTreeKind
public let metadata: SyntaxTreeMetadata?
···
extension SyntaxTree {
/// A mutable version of ``SyntaxTree`` that does not keep track of textual length, for use when constructing trees.
-
public struct Builder {
+
public struct MutableTree {
public var kind: SyntaxTreeKind
public var metadata: SyntaxTreeMetadata?
public var children: [Child]
+70
Sources/PterodactylSyntax/SyntaxTreeBuilder.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
import Foundation
+
+
public struct SyntaxTreeBuilder {
+
private enum Event: Equatable {
+
case open(kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata?)
+
case close
+
case advance(token: Token, metadata: TokenMetadata?)
+
}
+
+
public struct MarkOpened {
+
internal let index: Int
+
}
+
+
private var events: [Event] = []
+
+
public mutating func advance(token: Token, metadata: TokenMetadata?) {
+
events.append(.advance(token: token, metadata: metadata))
+
}
+
+
public mutating func open() -> MarkOpened {
+
let mark = MarkOpened(index: events.count)
+
events.append(.open(kind: .error, metadata: nil))
+
return mark
+
}
+
+
public mutating func close(mark: MarkOpened, kind: SyntaxTreeKind, metadata: SyntaxTreeMetadata?) {
+
events[mark.index] = .open(kind: kind, metadata: metadata)
+
events.append(.close)
+
}
+
+
public var tree: SyntaxTree {
+
var events = events
+
var stack: [SyntaxTree.MutableTree] = []
+
+
precondition(events.popLast() == .close)
+
+
for event in events {
+
switch event {
+
case .open(let kind, let metadata):
+
stack.append(SyntaxTree.MutableTree(kind: kind, metadata: metadata, children: []))
+
case .close:
+
let tree = stack.popLast()!
+
stack.modifyLast { last in
+
last.children.append(.tree(tree.tree))
+
}
+
case .advance(let token, let metadata):
+
stack.modifyLast { last in
+
last.children.append(.token(token, metadata: metadata))
+
}
+
}
+
}
+
+
assert(stack.count == 1)
+
return stack.popLast()!.tree
+
}
+
}
+
+
+
extension Array {
+
fileprivate mutating func modifyLast(_ modifier: (inout Element) -> Void) {
+
if var last = popLast() {
+
modifier(&last)
+
append(last)
+
}
+
}
+
}
+3 -2
Sources/PterodactylSyntax/SyntaxView.swift
···
import Foundation
+
/// An abstract syntax view around a ``SyntaxCursor``. This is to be populated by extensions targetting specific `G`.
struct SyntaxView<G: Grammar> {
-
let cursor: SyntaxTree.Cursor
-
init?(_ cursor: SyntaxTree.Cursor) {
+
let cursor: SyntaxCursor
+
init?(_ cursor: SyntaxCursor) {
guard let kind = cursor.node.tree?.kind, G.kinds.contains(kind) else { return nil }
self.cursor = cursor
}
+1 -1
Sources/PterodactylSyntax/Token.swift
···
import Foundation
-
public struct Token: Codable {
+
public struct Token: Codable, Equatable {
public let kind: TokenKind
public let text: String
public let utf16Length: Int
+2 -1
Sources/PterodactylSyntax/Types.swift
···
case equal = "="
}
-
public enum TokenKind: Codable, Equatable, Sendable {
+
public enum TokenKind: Codable, Equatable, Sendable, Hashable {
case eof
case keyword(Keyword)
case punctuation(Punctuation)
···
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()
+36
Tests/PterodactylLanguageServerTests/Test.swift
···
+
// SPDX-FileCopyrightText: 2025 The Project Pterodactyl Developers
+
//
+
// SPDX-License-Identifier: MPL-2.0
+
+
//
+
// File.swift
+
// Pterodactyl
+
//
+
// Created by Jon Sterling on 30/11/2025.
+
//
+
+
import Foundation
+
import Testing
+
+
@testable import PterodactylBuild
+
@testable import PterodactylLanguageServer
+
@testable import TSCBasic
+
@testable import llbuild2fx
+
+
struct LanguageServerTests {
+
@Test
+
func testSingletonFileTree() throws {
+
let path = try AbsolutePath(validating: "/foo/bar/file.txt")
+
let foo = LLBDeclFileTree.file(absolutePath: path, contents: "foobar")
+
let expected: LLBDeclFileTree =
+
.dir([
+
"foo": .dir([
+
"bar": .dir([
+
"file.txt": .file("foobar")
+
])
+
])
+
])
+
+
#expect(foo.debugDescription == expected.debugDescription)
+
}
+
}
+6
test.ptero
···
+
theory asdf where
+
foo : asdfs
+
foo : sdf
+
+
/* asdfasdf */
+
// asdf;lkj asdf;klj asdfkjh asdfjlkha sdfljk