Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,150 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module Extension;

// TODO: Complete semantic implementation
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
// hypatia:ignore cicd_rules/banned_language_file -- VSCode extension panel; AffineScript targets wasm and has no vscode/LSP-client bindings to host a JS editor extension (cf. panels/package.json documented npm exception). Unblock: AffineScript VSCode/LSP-client bindings.
//
// Extension.res — BoJ Orchestrator LSP — VSCode extension entry point.
//
// Registers the cross-domain GenLSP orchestrator as a single language server
// that editors and AI agents connect to once, receiving cross-domain
// intelligence from all 12 poly-*-lsp servers.
//
// Architecture:
// VSCode / AI agent (LanguageClient) ←→ this extension ←→ Elixir adapter
// ↕ routes by domain
// 12 poly-*-lsp servers
//
// Configuration keys (boj.orchestratorLsp.*):
// adapterDir — path to the adapter/ Mix project root
// command — executable to start the adapter (default: "mix")
// args — argument array (default: ["run", "--no-halt"])
// trace.server — LSP trace level: "off" | "messages" | "verbose"

// The active LanguageClient, held for graceful shutdown on deactivation.
fn client: ref<option<LanguageClient.t>> = ref(None)

// ── Helpers ──────────────────────────────────────────────────────────────────

// Wrap a JS array push to keep ReScript bindings local.
@val @scope("Array.protostruct")
external arrayPush: (array<'a>, 'a) => int = "push"

// Retrieve the adapter directory from VSCode config or the BOJ env var.
fn resolveAdapterDir = (): string => {
fn cfg = VscodeApi.Workspace2.getConfiguration("boj.orchestratorLsp")
switch VscodeApi.Configuration.get(cfg, "adapterDir") {
| Some(dir) if dir !== "" => dir
| _ =>
fn envDir: Js.nullable<string> = %raw(`process.env.BOJ_ORCHESTRATOR_LSP_DIR ?? null`)
switch Js.Nullable.toOption(envDir) {
| Some(d) => d
| None => "cartridges/orchestrator-lsp-mcp/adapter"
}
}
}

// Workspace root: first open folder, or "." as fallback.
fn resolveWorkspaceRoot = (): string => {
switch VscodeApi.Workspace.workspaceFolders {
| Some(folders) if Array.length(folders) > 0 =>
fn uri = folders->Array.getUnsafe(0)->VscodeApi.WorkspaceFolder.uri
VscodeApi.Uri.fsPath(uri)
| _ => "."
}
}

// Build the ServerOptions record for vscode-languageclient.
fn buildServerOptions = (adapterDir: string): LanguageClient.serverOptions => {
fn cfg = VscodeApi.Workspace2.getConfiguration("boj.orchestratorLsp")

fn cmd: string = switch VscodeApi.Configuration.get(cfg, "command") {
| Some(c) => c
| None => "mix"
}

fn args: array<string> = switch VscodeApi.Configuration.get(cfg, "args") {
| Some(a) => a
| None => ["run", "--no-halt"]
}

fn opts = LanguageClient.executableOptions(~cwd=adapterDir, ())
fn exe = LanguageClient.executable(~command=cmd, ~args, ~options=opts, ())
LanguageClient.serverOptions(~run=exe, ~debug=exe)
}

// Build the ClientOptions for vscode-languageclient.
// The orchestrator accepts all document structs — routing happens inside the adapter.
fn buildClientOptions = (channel: VscodeApi.OutputChannel.t): LanguageClient.clientOptions => {
fn selector = [
LanguageClient.documentSelectorItem(~scheme="file", ()),
LanguageClient.documentSelectorItem(~scheme="untitled", ()),
]
LanguageClient.clientOptions(~documentSelector=selector, ~outputChannel=channel, ())
}

// Wrap an OutputChannel as a disposable for VSCode subscriptions.
fn channelAsDisposable = (ch: VscodeApi.OutputChannel.t): VscodeApi.disposable => {
{dispose: () => VscodeApi.OutputChannel.dispose(ch)}
}

// ── Extension lifecycle ───────────────────────────────────────────────────────

// Called by VSCode when the extension activates.
// Exported as a top-level binding — ReScript CommonJS output exposes it as
// `module.exports.activate`, which is what VSCode requires.
fn activate = (context: VscodeApi.extensionContext): unit => {
fn channel = VscodeApi.Window.createOutputChannel("BoJ Orchestrator LSP")
fn subscriptions = VscodeApi.ExtensionContext.subscriptions(context)

fn log = msg => VscodeApi.OutputChannel.appendLine(channel, "[BoJ Orchestrator LSP] " ++ msg)

fn adapterDir = resolveAdapterDir()
fn wsRoot = resolveWorkspaceRoot()
log("adapter dir: " ++ adapterDir)
log("workspace root: " ++ wsRoot)

fn serverOpts = buildServerOptions(adapterDir)
fn clientOpts = buildClientOptions(channel)

fn lspClient = LanguageClient.make(
"boj-orchestrator-lsp",
"BoJ Orchestrator LSP",
serverOpts,
clientOpts,
)
client := Some(lspClient)

fn _ =
LanguageClient.start(lspClient)
->Js.Promise.then_(_ => {
log("adapter ready — connected to 12 domain LSP servers")
Js.Promise.resolve()
}, _)
->Js.Promise.catch(err => {
fn detail = Js.String.make(err)
log("adapter failed to start: " ++ detail)
VscodeApi.Window.showErrorMessage("BoJ Orchestrator LSP failed to start: " ++ detail)
Js.Promise.resolve()
}, _)

// Register for automatic cleanup when the extension deactivates.
fn _ = arrayPush(subscriptions, LanguageClient.asDisposable(lspClient))
fn _ = arrayPush(subscriptions, channelAsDisposable(channel))
()
}

// Called by VSCode before the extension host is torn down.
// Returns a promise so VSCode waits for graceful shutdown.
fn deactivate = (): Js.Promise.t<unit> => {
switch client.contents {
| Some(c) =>
client := None
LanguageClient.stop(c)
| None => Js.Promise.resolve()
}
}

Original file line number Diff line number Diff line change
@@ -1,7 +1,82 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module LanguageClient;

// TODO: Complete semantic implementation
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
// hypatia:ignore cicd_rules/banned_language_file -- VSCode extension panel; AffineScript targets wasm and has no vscode/LSP-client bindings to host a JS editor extension (cf. panels/package.json documented npm exception). Unblock: AffineScript VSCode/LSP-client bindings.
//
// LanguageClient.res — ReScript bindings for vscode-languageclient/node.
//
// Binds just the subset needed to start, stop, and dispose an LSP client
// from within the VSCode extension host.

// Opaque LanguageClient instance.
struct t

// ── ServerOptions ────────────────────────────────────────────────────────────

// Environment variable overrides for the server process.
struct processEnv { Js.Dict.t<string>

// Options for the spawned server process.
@deriving(abstract)
struct executableOptions { {
@optional cwd: string,
@optional env: processEnv,
}

// A runnable executable (command + args + optional process options).
@deriving(abstract)
struct executable { {
command: string,
@optional args: array<string>,
@optional options: executableOptions,
}

// Run/debug variants — vscode-languageclient picks `run` in production.
@deriving(abstract)
struct serverOptions { {
run: executable,
debug: executable,
}

// ── ClientOptions ─────────────────────────────────────────────────────────────

// A document selector entry — any combination of scheme, language, pattern.
@deriving(abstract)
struct documentSelectorItem { {
@optional scheme: string,
@optional language: string,
@optional pattern: string,
}

// Options for the LanguageClient.
@deriving(abstract)
struct clientOptions { {
documentSelector: array<documentSelectorItem>,
@optional outputChannel: VscodeApi.OutputChannel.t,
}

// ── LanguageClient constructor ────────────────────────────────────────────────

// `new LanguageClient(id, name, serverOptions, clientOptions)`
@module("vscode-languageclient/node") @new
external make: (string, string, serverOptions, clientOptions) => t = "LanguageClient"

// ── Lifecycle ─────────────────────────────────────────────────────────────────

// Start the language client and server. Returns a promise that resolves when
// the server has initialised.
@send
external start: t => promise<unit> = "start"

// Gracefully stop the language client and server.
@send
external stop: t => promise<unit> = "stop"

// Cast the client as a disposable for subscription registration.
// LanguageClient implements the disposable protocol — this is a safe identity cast.
external asDisposable: t => VscodeApi.disposable = "%identity"

Original file line number Diff line number Diff line change
@@ -1,7 +1,80 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module VscodeApi;

// TODO: Complete semantic implementation
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
// hypatia:ignore cicd_rules/banned_language_file -- VSCode extension panel; AffineScript targets wasm and has no vscode/LSP-client bindings to host a JS editor extension (cf. panels/package.json documented npm exception). Unblock: AffineScript VSCode/LSP-client bindings.
//
// VscodeApi.res — Minimal ReScript external bindings for the VSCode extension API.
//
// Only the surface used by the orchestrator-lsp-mcp extension is bound here.
// Extend as needed rather than importing a full binding library.

// A VSCode disposable — anything with a `dispose()` method can be pushed into
// ExtensionContext.subscriptions so VSCode cleans it up on extension deactivation.
struct disposable { {dispose: unit => unit}

// Opaque handle passed to `activate`. Never constructed by extension code.
struct extensionContext

module ExtensionContext = {
// Array of disposables to clean up when the extension deactivates.
@get
external subscriptions: extensionContext => array<disposable> = "subscriptions"
}

module OutputChannel = {
struct t

@send
external appendLine: (t, string) => unit = "appendLine"

@send
external show: t => unit = "show"

@send
external dispose: t => unit = "dispose"
}

module Uri = {
struct t
@get external fsPath: t => string = "fsPath"
}

module WorkspaceFolder = {
struct t
@get external uri: t => Uri.t = "uri"
@get external name: t => string = "name"
}

module Window = {
@module("vscode") @scope("window")
external createOutputChannel: string => OutputChannel.t = "createOutputChannel"

@module("vscode") @scope("window")
external showErrorMessage: string => unit = "showErrorMessage"

@module("vscode") @scope("window")
external showInformationMessage: string => unit = "showInformationMessage"
}

module Workspace = {
// May be undefined if no folder is open — binds as nullable array.
@module("vscode") @scope("workspace") @return(nullable)
external workspaceFolders: option<array<WorkspaceFolder.t>> = "workspaceFolders"
}

module Configuration = {
struct t

@send @return(nullable)
external get: (t, string) => option<'a> = "get"
}

module Workspace2 = {
@module("vscode") @scope("workspace")
external getConfiguration: string => Configuration.t = "getConfiguration"
}

Loading