use crate::prelude::*;
use deno_core::serde_v8;
use deno_core::{v8, JsRuntime, RuntimeOptions};
use serde_json::{json, Value};
use std::future::Future;
use super::{TextDocumentContext, WorkbenchExecutionContext};
const ACTIVATION_EVENT_LOOP_PUMPS: usize = 128;
fn exec_script(
runtime: &mut JsRuntime,
name: &'static str,
source: String,
) -> Result<v8::Global<v8::Value>> {
runtime
.execute_script(name, source)
.map_err(|e| anyhow::anyhow!("{}", e))
}
fn exec_script_named(
runtime: &mut JsRuntime,
name: String,
source: String,
) -> Result<v8::Global<v8::Value>> {
runtime
.execute_script(name, source)
.map_err(|e| anyhow::anyhow!("{}", e))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct BundleInspection {
pub export_names: Vec<String>,
pub registered_commands: Vec<String>,
pub output_channels: Vec<String>,
pub workspace_listeners: Vec<String>,
pub subscription_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExtensionMessageEntry {
pub level: String,
pub message: String,
pub items: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShowDocumentEntry {
pub uri: String,
pub view_column: i32,
pub preserve_focus: bool,
pub selection_start_line: Option<u32>,
pub selection_start_char: Option<u32>,
pub selection_end_line: Option<u32>,
pub selection_end_char: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct CommandExecution {
pub command: String,
pub panels: Vec<WebviewPanel>,
pub diagnostics: Vec<DiagnosticEntry>,
pub status_bar_items: Vec<StatusBarItemEntry>,
pub tree_views: Vec<TreeViewData>,
pub show_documents: Vec<ShowDocumentEntry>,
pub messages: Vec<ExtensionMessageEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticEntry {
pub uri: String,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub range: DiagnosticRange,
pub message: String,
pub severity: u8,
pub source: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiagnosticRange {
pub start_line: u32,
pub start_character: u32,
pub end_line: u32,
pub end_character: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatusBarItemEntry {
pub id: String,
pub text: String,
pub tooltip: String,
pub alignment: u8,
pub priority: i32,
pub command: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TreeViewData {
pub view_id: String,
pub items: Vec<TreeItemEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TreeItemEntry {
pub id: String,
pub label: String,
pub description: Option<String>,
pub tooltip: Option<String>,
pub collapsible_state: u8,
pub command: Option<String>,
pub context_value: Option<String>,
pub children: Vec<TreeItemEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebviewPanel {
pub panel_id: String,
pub view_type: String,
pub title: String,
pub html: String,
pub messages: Vec<Value>,
}
pub fn inspect_commonjs_bundle(source: &str, filename: &Path) -> Result<BundleInspection> {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
exec_script(
&mut runtime,
"<wsb:bootstrap>",
extension_bundle_bootstrap(
filename,
None,
None,
&HashMap::new(),
&WorkbenchExecutionContext {
groups: Vec::new(),
active_group_id: None,
document: None,
},
),
)
.context("failed to initialize extension bundle bootstrap")?;
exec_script_named(
&mut runtime,
filename.display().to_string(),
wrap_commonjs_bundle(source),
)
.with_context(|| format!("failed to evaluate {}", filename.display()))?;
let export_names = read_global_json::<Vec<String>>(&mut runtime, "__wsb_export_names__")
.context("failed to read CommonJS export names from embedded runtime")?;
exec_script(&mut runtime, "<wsb:activate>", activation_script())
.with_context(|| format!("failed to activate {}", filename.display()))?;
for _ in 0..ACTIVATION_EVENT_LOOP_PUMPS {
drive_runtime(&mut runtime)?;
}
let registered_commands =
read_global_json::<Vec<String>>(&mut runtime, "__wsb_registered_commands__")
.context("failed to read registered commands from embedded runtime")?;
let output_channels = read_global_json::<Vec<String>>(&mut runtime, "__wsb_output_channels__")
.context("failed to read output channels from embedded runtime")?;
let workspace_listeners =
read_global_json::<Vec<String>>(&mut runtime, "__wsb_workspace_listeners__")
.context("failed to read workspace listeners from embedded runtime")?;
let subscription_count = read_global_json::<usize>(&mut runtime, "__wsb_subscription_count__")
.context("failed to read extension subscription count from embedded runtime")?;
Ok(BundleInspection {
export_names,
registered_commands,
output_channels,
workspace_listeners,
subscription_count,
})
}
pub fn execute_commonjs_command(
source: &str,
filename: &Path,
extension_id: &str,
command_id: &str,
server_base_url: &str,
asset_files: &HashMap<String, Vec<u8>>,
context: &WorkbenchExecutionContext,
) -> Result<CommandExecution> {
let mut runtime = JsRuntime::new(RuntimeOptions::default());
exec_script(
&mut runtime,
"<wsb:bootstrap>",
extension_bundle_bootstrap(
filename,
Some(extension_id),
Some(server_base_url),
asset_files,
context,
),
)
.context("failed to initialize extension command bootstrap")?;
exec_script_named(
&mut runtime,
filename.display().to_string(),
wrap_commonjs_bundle(source),
)
.with_context(|| format!("failed to evaluate {}", filename.display()))?;
exec_script(&mut runtime, "<wsb:activate>", activation_script())
.with_context(|| format!("failed to activate {}", filename.display()))?;
for _ in 0..ACTIVATION_EVENT_LOOP_PUMPS {
drive_runtime(&mut runtime)?;
}
exec_script(
&mut runtime,
"<wsb:execute-command>",
command_execution_script(command_id),
)
.with_context(|| {
format!(
"failed to execute command '{}' from {}",
command_id,
filename.display()
)
})?;
drive_runtime(&mut runtime)?;
let panels = read_global_json::<Vec<WebviewPanel>>(&mut runtime, "__wsb_webview_panels__")
.context("failed to read created webview panels from embedded runtime")?;
exec_script(
&mut runtime,
"<wsb:collect-diagnostics>",
r#"globalThis.__wsb_diagnostics_snapshot__ = (function() {
const result = [];
for (const collection of globalThis.__wsb_diagnostic_collections__) {
collection.__entries__.forEach((diagnostics, uri) => {
result.push({
uri: String(uri),
diagnostics: (diagnostics || []).map(d => ({
range: {
start_line: d.range?.start?.line ?? 0,
start_character: d.range?.start?.character ?? 0,
end_line: d.range?.end?.line ?? 0,
end_character: d.range?.end?.character ?? 0,
},
message: String(d.message ?? ""),
severity: typeof d.severity === "number" ? d.severity : 0,
source: d.source != null ? String(d.source) : null,
code: d.code != null ? String(d.code) : null,
})),
});
});
}
return result;
})();"#
.to_string(),
)
.context("failed to collect diagnostics from embedded runtime")?;
let diagnostics =
read_global_json::<Vec<DiagnosticEntry>>(&mut runtime, "__wsb_diagnostics_snapshot__")
.context("failed to read diagnostics from embedded runtime")?;
exec_script(
&mut runtime,
"<wsb:collect-status-bar>",
r#"globalThis.__wsb_status_bar_snapshot__ = globalThis.__wsb_status_bar_items__
.filter(item => item._visible)
.map(item => ({
id: String(item.id),
text: String(item.text ?? ""),
tooltip: String(item.tooltip ?? ""),
alignment: typeof item.alignment === "number" ? item.alignment : 1,
priority: typeof item.priority === "number" ? item.priority : 0,
command: item.command != null ? String(item.command) : null,
}));"#
.to_string(),
)
.context("failed to collect status bar items from embedded runtime")?;
let status_bar_items =
read_global_json::<Vec<StatusBarItemEntry>>(&mut runtime, "__wsb_status_bar_snapshot__")
.context("failed to read status bar items from embedded runtime")?;
exec_script(
&mut runtime,
"<wsb:collect-tree-views>",
r#"globalThis.__wsb_tree_views_snapshot__ = (function() {
const views = [];
const providers = globalThis.__wsb_tree_data_providers__;
for (const viewId of Object.keys(providers)) {
const provider = providers[viewId];
if (!provider || typeof provider.getChildren !== "function") continue;
let raw;
try {
raw = provider.getChildren();
} catch (_) { continue; }
// getChildren may return a Promise or an array; resolve synchronously if possible
if (raw && typeof raw.then === "function") {
// Can't resolve async here, skip
continue;
}
const items = (raw || []).map(function serializeItem(element) {
let treeItem;
try {
treeItem = typeof provider.getTreeItem === "function" ? provider.getTreeItem(element) : element;
} catch (_) { treeItem = element; }
// getTreeItem may also return a Promise
if (treeItem && typeof treeItem.then === "function") {
return null;
}
const label = typeof treeItem.label === "string" ? treeItem.label
: typeof treeItem.label?.label === "string" ? treeItem.label.label
: String(treeItem.label ?? "");
return {
id: String(treeItem.id ?? label),
label,
description: treeItem.description != null ? String(treeItem.description) : null,
tooltip: treeItem.tooltip != null ? String(treeItem.tooltip) : null,
collapsible_state: typeof treeItem.collapsibleState === "number" ? treeItem.collapsibleState : 0,
command: treeItem.command?.command != null ? String(treeItem.command.command) : null,
context_value: treeItem.contextValue != null ? String(treeItem.contextValue) : null,
children: [],
};
}).filter(Boolean);
views.push({ view_id: viewId, items });
}
return views;
})();"#
.to_string(),
)
.context("failed to collect tree view data from embedded runtime")?;
let tree_views =
read_global_json::<Vec<TreeViewData>>(&mut runtime, "__wsb_tree_views_snapshot__")
.context("failed to read tree view data from embedded runtime")?;
let show_documents =
read_global_json::<Vec<ShowDocumentEntry>>(&mut runtime, "__wsb_show_documents__")
.context("failed to read show_documents from embedded runtime")?;
let messages = read_global_json::<Vec<ExtensionMessageEntry>>(&mut runtime, "__wsb_messages__")
.context("failed to read messages from embedded runtime")?;
Ok(CommandExecution {
command: command_id.to_string(),
panels,
diagnostics,
status_bar_items,
tree_views,
show_documents,
messages,
})
}
const VSCODE_COMPAT_VERSION: &str = "1.96.0";
fn extension_bundle_bootstrap(
filename: &Path,
extension_id: Option<&str>,
server_base_url: Option<&str>,
asset_files: &HashMap<String, Vec<u8>>,
context: &WorkbenchExecutionContext,
) -> String {
let file = filename.to_string_lossy();
let dir = filename
.parent()
.map(|value| value.to_string_lossy().into_owned())
.unwrap_or_else(|| ".".to_string());
let extension_root = "/".to_string();
let extension_id_json = serde_json::to_string(extension_id.unwrap_or("")).unwrap();
let server_base_url_json = serde_json::to_string(server_base_url.unwrap_or("")).unwrap();
let extension_root_json = serde_json::to_string(&extension_root).unwrap();
let asset_files_json = serde_json::to_string(asset_files).unwrap();
let workspace_folder_paths =
serde_json::to_string(&initial_workspace_folder_paths(context.document.as_ref())).unwrap();
let text_document_specs =
serde_json::to_string(&initial_text_document_specs(context.document.as_ref())).unwrap();
let workbench_context_json = serde_json::to_string(context).unwrap();
let template = r#"
__COMMON_RUNTIME_PRIMITIVES__
globalThis.__wsb_extension_id__ = __EXTENSION_ID_JSON__;
globalThis.__wsb_server_base_url__ = __SERVER_BASE_URL_JSON__;
globalThis.__wsb_extension_root__ = __EXTENSION_ROOT_JSON__;
globalThis.__wsb_asset_files__ = __ASSET_FILES_JSON__;
globalThis.__wsb_registered_commands__ = [];
globalThis.__wsb_output_channels__ = [];
globalThis.__wsb_workspace_listeners__ = [];
globalThis.__wsb_subscription_count__ = 0;
globalThis.__wsb_webview_panels__ = [];
globalThis.__wsb_diagnostic_collections__ = [];
globalThis.__wsb_status_bar_items__ = [];
globalThis.__wsb_status_bar_counter__ = 0;
globalThis.__wsb_tree_data_providers__ = Object.create(null);
globalThis.__wsb_command_callbacks__ = Object.create(null);
globalThis.__wsb_language_client_specs__ = [];
globalThis.__wsb_language_client_handlers__ = Object.create(null);
globalThis.__wsb_panel_counter__ = 0;
globalThis.__wsb_show_documents__ = [];
globalThis.__wsb_messages__ = [];
globalThis.__wsb_workbench_context__ = __WORKBENCH_CONTEXT_JSON__;
globalThis.__wsb_module_cache__ = Object.create(null);
globalThis.__wsb_filename__ = "__FILE__";
globalThis.__wsb_dirname__ = "__DIR__";
globalThis.process = {
env: Object.create(null),
argv: ["node", "__FILE__"],
pid: 1,
platform: "linux",
versions: { node: "20.0.0" },
cwd() { return "__DIR__"; },
nextTick(callback, ...args) { queueMicrotask(() => callback(...args)); },
kill() { return true; },
exit(_code) {},
on(_event, _callback) {},
};
globalThis.console = {
log() {},
warn() {},
error() {},
debug() {},
trace() {},
assert(condition, ...args) {
if (!condition) {
throw new Error(args.join(" ") || "console.assert failed in embedded extension runtime");
}
},
};
globalThis.__wsb_require__ = function(name) {{
if (globalThis.__wsb_module_cache__[name]) {{
return globalThis.__wsb_module_cache__[name];
}}
const __wsb_resolved__ = globalThis.__wsb_resolve_module__(name);
globalThis.__wsb_module_cache__[name] = __wsb_resolved__;
return __wsb_resolved__;
}};
globalThis.__wsb_resolve_module__ = function(name) {{
switch (name) {{
case "vscode":
const workspaceFolders = __WORKSPACE_FOLDER_PATHS__.map((fsPath) => ({{
name: fsPath.split("/").filter(Boolean).pop() ?? "workspace",
uri: globalThis.__wsb_uri_from_path__(fsPath),
}}));
const textDocuments = __TEXT_DOCUMENT_SPECS__.map((spec) => {{
const uri = globalThis.__wsb_uri_from_string__(spec.uri);
const text = spec.content || "";
const lines = text.split("\n");
return {{
uri,
languageId: spec.languageId,
version: spec.version || 1,
fileName: uri.fsPath || "",
isUntitled: false,
isDirty: false,
isClosed: false,
eol: 1,
lineCount: lines.length,
getText(range) {{
if (!range) return text;
const startOff = lines.slice(0, range.start.line).reduce((s, l) => s + l.length + 1, 0) + range.start.character;
const endOff = lines.slice(0, range.end.line).reduce((s, l) => s + l.length + 1, 0) + range.end.character;
return text.substring(startOff, endOff);
}},
lineAt(lineOrPos) {{
const idx = typeof lineOrPos === "number" ? lineOrPos : lineOrPos.line;
const lineText = lines[idx] || "";
return {{
lineNumber: idx,
text: lineText,
range: new WsbRange(idx, 0, idx, lineText.length),
rangeIncludingLineBreak: new WsbRange(idx, 0, idx + 1, 0),
firstNonWhitespaceCharacterIndex: lineText.search(/\S/),
isEmptyOrWhitespace: lineText.trim().length === 0,
}};
}},
offsetAt(position) {{
return lines.slice(0, position.line).reduce((s, l) => s + l.length + 1, 0) + position.character;
}},
positionAt(offset) {{
let remaining = offset;
for (let i = 0; i < lines.length; i++) {{
if (remaining <= lines[i].length) return new WsbPosition(i, remaining);
remaining -= lines[i].length + 1;
}}
return new WsbPosition(lines.length - 1, (lines[lines.length - 1] || "").length);
}},
getWordRangeAtPosition(_position, _regex) {{ return undefined; }},
validateRange(range) {{ return range; }},
validatePosition(position) {{ return position; }},
save() {{ return Promise.resolve(true); }},
}};
}});
// --- Disposable ---
class WsbDisposable {{
constructor(callOnDispose) {{
this._callOnDispose = callOnDispose;
this._isDisposed = false;
}}
dispose() {{
if (!this._isDisposed) {{
this._isDisposed = true;
if (typeof this._callOnDispose === "function") {{
this._callOnDispose();
}}
}}
}}
static from(...disposables) {{
const flat = disposables.flat();
return new WsbDisposable(() => {{
for (const d of flat) {{
if (d && typeof d.dispose === "function") {{
d.dispose();
}}
}}
}});
}}
}}
// --- EventEmitter ---
class WsbEventEmitter {{
constructor() {{
this._listeners = [];
this._event = null;
}}
get event() {{
if (!this._event) {{
const self = this;
this._event = function wsbEvent(listener, thisArgs, disposables) {{
const bound = thisArgs ? listener.bind(thisArgs) : listener;
self._listeners.push(bound);
const disposable = new WsbDisposable(() => {{
const idx = self._listeners.indexOf(bound);
if (idx !== -1) {{
self._listeners.splice(idx, 1);
}}
}});
if (Array.isArray(disposables)) {{
disposables.push(disposable);
}}
return disposable;
}};
}}
return this._event;
}}
fire(data) {{
for (const listener of this._listeners.slice()) {{
listener(data);
}}
}}
dispose() {{
this._listeners.length = 0;
this._event = null;
}}
}}
// --- CancellationError ---
class WsbCancellationError extends Error {{
constructor(message) {{
super(message || "Cancelled");
this.name = "CancellationError";
}}
}}
// --- CancellationToken / CancellationTokenSource ---
class WsbCancellationTokenSource {{
constructor() {{
this._isCancelled = false;
this._emitter = new WsbEventEmitter();
this.token = {{
isCancellationRequested: false,
onCancellationRequested: this._emitter.event,
}};
}}
cancel() {{
if (!this._isCancelled) {{
this._isCancelled = true;
this.token.isCancellationRequested = true;
this._emitter.fire(undefined);
}}
}}
dispose() {{
this._emitter.dispose();
}}
}}
// Helper: create a no-op Event<T> function (callable, returns Disposable)
function __wsb_create_event__() {{
const emitter = new WsbEventEmitter();
return emitter.event;
}}
// --- Position / Range / Location ---
class WsbPosition {{
constructor(line, character) {{
this.line = line;
this.character = character;
}}
isEqual(other) {{
return this.line === other.line && this.character === other.character;
}}
isBefore(other) {{
return this.line < other.line || (this.line === other.line && this.character < other.character);
}}
isAfter(other) {{
return this.line > other.line || (this.line === other.line && this.character > other.character);
}}
compareTo(other) {{
if (this.isBefore(other)) return -1;
if (this.isAfter(other)) return 1;
return 0;
}}
translate(lineDelta = 0, characterDelta = 0) {{
return new WsbPosition(this.line + lineDelta, this.character + characterDelta);
}}
with(line, character) {{
return new WsbPosition(line ?? this.line, character ?? this.character);
}}
}}
class WsbRange {{
constructor(startLine, startCharacter, endLine, endCharacter) {{
if (startLine instanceof WsbPosition) {{
this.start = startLine;
this.end = startCharacter instanceof WsbPosition ? startCharacter : startLine;
}} else {{
this.start = new WsbPosition(startLine, startCharacter);
this.end = new WsbPosition(endLine ?? startLine, endCharacter ?? startCharacter);
}}
}}
get isEmpty() {{
return this.start.isEqual(this.end);
}}
get isSingleLine() {{
return this.start.line === this.end.line;
}}
contains(positionOrRange) {{
if (positionOrRange instanceof WsbRange) {{
return this.contains(positionOrRange.start) && this.contains(positionOrRange.end);
}}
return !positionOrRange.isBefore(this.start) && !positionOrRange.isAfter(this.end);
}}
with(start, end) {{
return new WsbRange(start ?? this.start, end ?? this.end);
}}
}}
class WsbLocation {{
constructor(uri, rangeOrPosition) {{
this.uri = uri;
if (rangeOrPosition instanceof WsbRange) {{
this.range = rangeOrPosition;
}} else {{
this.range = new WsbRange(rangeOrPosition, rangeOrPosition);
}}
}}
}}
// --- Diagnostic types ---
const WsbDiagnosticSeverity = Object.freeze({{
Error: 0,
Warning: 1,
Information: 2,
Hint: 3,
}});
const WsbCompletionItemKind = Object.freeze({{
Text: 0, Method: 1, Function: 2, Constructor: 3, Field: 4,
Variable: 5, Class: 6, Interface: 7, Module: 8, Property: 9,
Unit: 10, Value: 11, Enum: 12, Keyword: 13, Snippet: 14,
Color: 15, File: 16, Reference: 17, Folder: 18, EnumMember: 19,
Constant: 20, Struct: 21, Event: 22, Operator: 23, TypeParameter: 24,
}});
const WsbSymbolKind = Object.freeze({{
File: 0, Module: 1, Namespace: 2, Package: 3, Class: 4,
Method: 5, Property: 6, Field: 7, Constructor: 8, Enum: 9,
Interface: 10, Function: 11, Variable: 12, Constant: 13, String: 14,
Number: 15, Boolean: 16, Array: 17, Object: 18, Key: 19,
Null: 20, EnumMember: 21, Struct: 22, Event: 23, Operator: 24,
TypeParameter: 25,
}});
const WsbCodeActionKind = Object.freeze({{
Empty: "",
QuickFix: "quickfix",
Refactor: "refactor",
RefactorExtract: "refactor.extract",
RefactorInline: "refactor.inline",
RefactorRewrite: "refactor.rewrite",
Source: "source",
SourceOrganizeImports: "source.organizeImports",
SourceFixAll: "source.fixAll",
}});
const WsbTextEditorRevealType = Object.freeze({{
Default: 0,
InCenter: 1,
InCenterIfOutsideViewport: 2,
AtTop: 3,
}});
const WsbConfigurationTarget = Object.freeze({{
Global: 1,
Workspace: 2,
WorkspaceFolder: 3,
}});
class WsbMarkdownString {{
constructor(value = "", supportThemeIcons = false) {{
this.value = String(value);
this.isTrusted = false;
this.supportThemeIcons = supportThemeIcons;
this.supportHtml = false;
}}
appendText(value) {{
this.value += String(value);
return this;
}}
appendMarkdown(value) {{
this.value += String(value);
return this;
}}
appendCodeblock(value, language = "") {{
this.value += "\n```" + language + "\n" + String(value) + "\n```\n";
return this;
}}
}}
class WsbThemeColor {{
constructor(id) {{
this.id = String(id);
}}
}}
class WsbThemeIcon {{
constructor(id, color) {{
this.id = String(id);
this.color = color;
}}
static File = new WsbThemeIcon("file");
static Folder = new WsbThemeIcon("folder");
}}
// File system watcher event types
const WsbFileChangeType = Object.freeze({{
Changed: 1,
Created: 2,
Deleted: 3,
}});
// --- Extendable VS Code API classes ---
// These are classes that vscode-languageclient and other bundled
// libraries extend via `class Foo extends vscode.CompletionItem` etc.
class WsbCompletionItem {{
constructor(label, kind) {{
this.label = label;
this.kind = kind;
this.detail = undefined;
this.documentation = undefined;
this.sortText = undefined;
this.filterText = undefined;
this.insertText = undefined;
this.range = undefined;
this.commitCharacters = undefined;
this.additionalTextEdits = undefined;
this.command = undefined;
this.tags = undefined;
this.preselect = false;
}}
}}
class WsbCodeAction {{
constructor(title, kind) {{
this.title = title;
this.kind = kind;
this.diagnostics = [];
this.edit = undefined;
this.command = undefined;
this.isPreferred = false;
this.disabled = undefined;
}}
}}
class WsbCodeLens {{
constructor(range, command) {{
this.range = range;
this.command = command;
this.isResolved = command !== undefined;
}}
}}
class WsbDiagnostic {{
constructor(range, message, severity) {{
this.range = range;
this.message = message;
this.severity = severity ?? 0;
this.source = undefined;
this.code = undefined;
this.relatedInformation = undefined;
this.tags = undefined;
}}
}}
class WsbDocumentLink {{
constructor(range, target) {{
this.range = range;
this.target = target;
this.tooltip = undefined;
}}
}}
class WsbCallHierarchyItem {{
constructor(kind, name, detail, uri, range, selectionRange) {{
this.kind = kind;
this.name = name;
this.detail = detail;
this.uri = uri;
this.range = range;
this.selectionRange = selectionRange;
this.tags = undefined;
}}
}}
class WsbTypeHierarchyItem {{
constructor(kind, name, detail, uri, range, selectionRange) {{
this.kind = kind;
this.name = name;
this.detail = detail;
this.uri = uri;
this.range = range;
this.selectionRange = selectionRange;
this.tags = undefined;
}}
}}
class WsbSymbolInformation {{
constructor(name, kind, containerName, location) {{
this.name = name;
this.kind = kind;
this.containerName = containerName || "";
this.location = location;
this.tags = undefined;
}}
}}
class WsbDocumentSymbol {{
constructor(name, detail, kind, range, selectionRange) {{
this.name = name;
this.detail = detail;
this.kind = kind;
this.range = range;
this.selectionRange = selectionRange;
this.children = [];
this.tags = undefined;
}}
}}
class WsbInlayHint {{
constructor(position, label, kind) {{
this.position = position;
this.label = label;
this.kind = kind;
this.tooltip = undefined;
this.textEdits = undefined;
this.paddingLeft = false;
this.paddingRight = false;
}}
}}
const WsbInlayHintKind = Object.freeze({{
Type: 1,
Parameter: 2,
}});
class WsbHover {{
constructor(contents, range) {{
this.contents = contents;
this.range = range;
}}
}}
class WsbSignatureHelp {{
constructor() {{
this.signatures = [];
this.activeSignature = 0;
this.activeParameter = 0;
}}
}}
class WsbSignatureInformation {{
constructor(label, documentation) {{
this.label = label;
this.documentation = documentation;
this.parameters = [];
this.activeParameter = undefined;
}}
}}
class WsbParameterInformation {{
constructor(label, documentation) {{
this.label = label;
this.documentation = documentation;
}}
}}
class WsbTextEdit {{
constructor(range, newText) {{
this.range = range;
this.newText = newText;
}}
static replace(range, newText) {{
return new WsbTextEdit(range, newText);
}}
static insert(position, newText) {{
return new WsbTextEdit(new WsbRange(position, position), newText);
}}
static delete(range) {{
return new WsbTextEdit(range, "");
}}
}}
class WsbWorkspaceEdit {{
constructor() {{
this._edits = new Map();
}}
replace(uri, range, newText) {{
const key = uri?.toString?.() ?? String(uri);
if (!this._edits.has(key)) this._edits.set(key, []);
this._edits.get(key).push(WsbTextEdit.replace(range, newText));
}}
insert(uri, position, newText) {{
const key = uri?.toString?.() ?? String(uri);
if (!this._edits.has(key)) this._edits.set(key, []);
this._edits.get(key).push(WsbTextEdit.insert(position, newText));
}}
delete(uri, range) {{
const key = uri?.toString?.() ?? String(uri);
if (!this._edits.has(key)) this._edits.set(key, []);
this._edits.get(key).push(WsbTextEdit.delete(range));
}}
has(uri) {{
return this._edits.has(uri?.toString?.() ?? String(uri));
}}
set(uri, edits) {{
this._edits.set(uri?.toString?.() ?? String(uri), edits);
}}
get(uri) {{
return this._edits.get(uri?.toString?.() ?? String(uri)) ?? [];
}}
entries() {{
return Array.from(this._edits.entries());
}}
get size() {{
return this._edits.size;
}}
}}
class WsbFoldingRange {{
constructor(start, end, kind) {{
this.start = start;
this.end = end;
this.kind = kind;
}}
}}
const WsbFoldingRangeKind = Object.freeze({{
Comment: 1,
Imports: 2,
Region: 3,
}});
class WsbSelectionRange {{
constructor(range, parent) {{
this.range = range;
this.parent = parent;
}}
}}
class WsbSemanticTokensLegend {{
constructor(tokenTypes, tokenModifiers = []) {{
this.tokenTypes = tokenTypes;
this.tokenModifiers = tokenModifiers;
}}
}}
class WsbSemanticTokensBuilder {{
constructor(legend) {{
this._legend = legend;
this._data = [];
this._prevLine = 0;
this._prevChar = 0;
}}
push(line, char, length, tokenType, tokenModifiers = 0) {{
this._data.push(
line - this._prevLine,
line === this._prevLine ? char - this._prevChar : char,
length,
tokenType,
tokenModifiers
);
this._prevLine = line;
this._prevChar = char;
}}
build() {{
return {{ data: new Uint32Array(this._data) }};
}}
}}
class WsbSnippetString {{
constructor(value = "") {{
this.value = String(value);
}}
appendText(string) {{
this.value += String(string);
return this;
}}
appendPlaceholder(value, number) {{
this.value += String(value);
return this;
}}
appendVariable(name, defaultValue) {{
this.value += String(defaultValue ?? "");
return this;
}}
appendTabstop(_number) {{
return this;
}}
}}
class WsbTreeItem {{
constructor(labelOrResourceUri, collapsibleState) {{
if (typeof labelOrResourceUri === "string") {{
this.label = labelOrResourceUri;
}} else {{
this.resourceUri = labelOrResourceUri;
}}
this.collapsibleState = collapsibleState ?? 0;
this.id = undefined;
this.iconPath = undefined;
this.description = undefined;
this.tooltip = undefined;
this.command = undefined;
this.contextValue = undefined;
}}
}}
const WsbDiagnosticTag = Object.freeze({{
Unnecessary: 1,
Deprecated: 2,
}});
const WsbCompletionTriggerKind = Object.freeze({{
Invoke: 0,
TriggerCharacter: 1,
TriggerForIncompleteCompletions: 2,
}});
class WsbCompletionList {{
constructor(items = [], isIncomplete = false) {{
this.items = items;
this.isIncomplete = isIncomplete;
}}
}}
const vscodeHost = {{
commands: {{
registerCommand(command, callback) {{
const commandId = String(command);
if (typeof callback !== "function") {{
throw new Error(`command '${{commandId}}' must register a callable callback`);
}}
globalThis.__wsb_registered_commands__.push(commandId);
globalThis.__wsb_command_callbacks__[commandId] = callback;
return new WsbDisposable(() => {{
delete globalThis.__wsb_command_callbacks__[commandId];
}});
}},
executeCommand(command, ...args) {{
const callback = globalThis.__wsb_command_callbacks__[String(command)];
if (typeof callback !== "function") {{
throw new Error(`command '${{command}}' is not registered in embedded runtime`);
}}
return callback(...args);
}},
}},
workspace: {{
workspaceFolders,
textDocuments,
onDidChangeWorkspaceFolders: __wsb_create_event__(),
onDidOpenTextDocument: __wsb_create_event__(),
onDidCloseTextDocument: __wsb_create_event__(),
onDidChangeTextDocument: __wsb_create_event__(),
onDidSaveTextDocument: __wsb_create_event__(),
onWillSaveTextDocument: __wsb_create_event__(),
onDidChangeConfiguration: __wsb_create_event__(),
getWorkspaceFolder(uri) {{
const target = typeof uri === "string" ? uri : uri?.toString?.() ?? "";
return workspaceFolders.find((folder) => target.startsWith(folder.uri.toString())) ?? null;
}},
getConfiguration(section) {{
return {{
get(_key, defaultValue) {{ return defaultValue; }},
has(_key) {{ return false; }},
inspect(_key) {{ return undefined; }},
update(_key, _value, _configurationTarget) {{ return Promise.resolve(); }},
}};
}},
createFileSystemWatcher(_globPattern, _ignoreCreateEvents, _ignoreChangeEvents, _ignoreDeleteEvents) {{
return {{
onDidCreate: __wsb_create_event__(),
onDidChange: __wsb_create_event__(),
onDidDelete: __wsb_create_event__(),
dispose() {{}},
}};
}},
fs: {{
readFile(uri) {{
const fsPath = typeof uri === "string" ? uri : uri?.fsPath ?? uri?.toString?.();
const normalized = globalThis.__wsb_normalize_path__(fsPath);
const bytes = globalThis.__wsb_asset_files__[normalized];
if (!bytes) {{
return Promise.reject(new Error(`workspace.fs.readFile: '${{normalized}}' not found`));
}}
return Promise.resolve(new Uint8Array(bytes));
}},
writeFile(_uri, _content) {{
return Promise.resolve();
}},
stat(_uri) {{
return Promise.resolve({{ type: 1, ctime: 0, mtime: 0, size: 0 }});
}},
}},
}},
window: {{
createOutputChannel(name) {{
globalThis.__wsb_output_channels__.push(String(name));
return {{
name: String(name),
append() {{}},
appendLine() {{}},
clear() {{}},
show() {{}},
hide() {{}},
dispose() {{}},
replace() {{}},
}};
}},
createWebviewPanel(viewType, title, _column, options = {{}}) {{
const panelRecord = {{
panel_id: `panel:${{++globalThis.__wsb_panel_counter__}}`,
view_type: String(viewType),
title: String(title),
html: "",
messages: [],
}};
const onDidDisposeEmitter = new WsbEventEmitter();
const onDidChangeViewStateEmitter = new WsbEventEmitter();
const webviewOnDidReceiveMessageEmitter = new WsbEventEmitter();
const panel = {{
viewType: String(viewType),
title: String(title),
visible: true,
active: true,
webview: {{
get html() {{
return panelRecord.html;
}},
set html(value) {{
panelRecord.html = String(value);
}},
postMessage(message) {{
panelRecord.messages.push(message);
return Promise.resolve(true);
}},
onDidReceiveMessage: webviewOnDidReceiveMessageEmitter.event,
asWebviewUri(uri) {{
const fsPath = typeof uri === "string"
? uri
: uri?.fsPath ?? uri?.toString?.();
if (!fsPath) {{
throw new Error("webview.asWebviewUri received an invalid URI");
}}
const normalized = globalThis.__wsb_normalize_path__(fsPath);
const assetUrl = globalThis.__wsb_asset_url__(normalized);
return {{
scheme: "https",
authority: "",
path: normalized,
fsPath: normalized,
query: "",
fragment: "",
toString() {{
return assetUrl;
}},
}};
}},
options: options ?? {{}},
cspSource: "",
}},
reveal(_column, _preserveFocus) {{}},
dispose() {{
onDidDisposeEmitter.fire(undefined);
onDidDisposeEmitter.dispose();
onDidChangeViewStateEmitter.dispose();
webviewOnDidReceiveMessageEmitter.dispose();
}},
onDidDispose: onDidDisposeEmitter.event,
onDidChangeViewState: onDidChangeViewStateEmitter.event,
}};
globalThis.__wsb_webview_panels__.push(panelRecord);
return panel;
}},
showTextDocument(documentOrUri, options) {{
const uri = documentOrUri?.uri?.toString?.() ?? documentOrUri?.fsPath ?? String(documentOrUri);
globalThis.__wsb_show_documents__.push({{
uri: String(uri),
view_column: options?.viewColumn ?? 1,
preserve_focus: options?.preserveFocus ?? false,
selection_start_line: options?.selection?.start?.line ?? null,
selection_start_char: options?.selection?.start?.character ?? null,
selection_end_line: options?.selection?.end?.line ?? null,
selection_end_char: options?.selection?.end?.character ?? null,
}});
return Promise.resolve({{ document: documentOrUri, selection: new WsbRange(0,0,0,0), selections: [], visibleRanges: [], options: {{}}, viewColumn: options?.viewColumn ?? 1 }});
}},
showInformationMessage(message, ...items) {{
globalThis.__wsb_messages__.push({{ level: "info", message: String(message), items: items.filter(i => typeof i === "string") }});
return Promise.resolve(undefined);
}},
showWarningMessage(message, ...items) {{
globalThis.__wsb_messages__.push({{ level: "warn", message: String(message), items: items.filter(i => typeof i === "string") }});
return Promise.resolve(undefined);
}},
showErrorMessage(message, ...items) {{
globalThis.__wsb_messages__.push({{ level: "error", message: String(message), items: items.filter(i => typeof i === "string") }});
return Promise.resolve(undefined);
}},
showQuickPick(_items, _options) {{
return Promise.resolve(undefined);
}},
showInputBox(_options) {{
return Promise.resolve(undefined);
}},
withProgress(_options, task) {{
const fakeProgress = {{
report(_value) {{}},
}};
const fakeToken = {{
isCancellationRequested: false,
onCancellationRequested: __wsb_create_event__(),
}};
return task(fakeProgress, fakeToken);
}},
createStatusBarItem(_alignmentOrId, _priority) {{
const id = `statusbar-${{globalThis.__wsb_status_bar_counter__++}}`;
// VS Code overloads: (alignment, priority) or (id, alignment)
let alignment = 1;
let priority = 0;
if (typeof _alignmentOrId === "number") {{
alignment = _alignmentOrId;
priority = typeof _priority === "number" ? _priority : 0;
}} else if (typeof _alignmentOrId === "string") {{
alignment = typeof _priority === "number" ? _priority : 1;
}}
const item = {{
id,
alignment,
priority,
text: "",
tooltip: "",
color: undefined,
backgroundColor: undefined,
command: undefined,
_visible: false,
show() {{ item._visible = true; }},
hide() {{ item._visible = false; }},
dispose() {{
item._visible = false;
const idx = globalThis.__wsb_status_bar_items__.indexOf(item);
if (idx !== -1) globalThis.__wsb_status_bar_items__.splice(idx, 1);
}},
}};
globalThis.__wsb_status_bar_items__.push(item);
return item;
}},
activeTextEditor: (function() {{
const ctx = globalThis.__wsb_workbench_context__;
if (!ctx) return textDocuments.length > 0 ? {{
document: textDocuments[0],
selection: new WsbRange(0, 0, 0, 0),
selections: [new WsbRange(0, 0, 0, 0)],
visibleRanges: [new WsbRange(0, 0, 0, 0)],
options: {{}},
viewColumn: 1,
}} : undefined;
const activeGroup = ctx.groups.find(g => g.id === ctx.active_group_id);
if (!activeGroup) return undefined;
const activeTab = activeGroup.tabs.find(t => t.id === activeGroup.active_tab_id);
if (!activeTab || activeTab.input_kind !== "file") return undefined;
const doc = textDocuments.find(d => d.uri?.toString?.()?.endsWith?.(activeTab.file_path) || d.fileName?.endsWith?.(activeTab.file_path));
if (!doc) return undefined;
return {{
document: doc,
selection: new WsbRange(0, 0, 0, 0),
selections: [new WsbRange(0, 0, 0, 0)],
visibleRanges: [new WsbRange(0, 0, 0, 0)],
options: {{}},
viewColumn: ctx.groups.indexOf(activeGroup) + 1,
}};
}})(),
visibleTextEditors: (function() {{
const ctx = globalThis.__wsb_workbench_context__;
if (!ctx || !ctx.groups) return [];
const editors = [];
for (let gi = 0; gi < ctx.groups.length; gi++) {{
const group = ctx.groups[gi];
for (const tab of group.tabs) {{
if (tab.input_kind !== "file") continue;
const doc = textDocuments.find(d => d.uri?.toString?.()?.endsWith?.(tab.file_path) || d.fileName?.endsWith?.(tab.file_path));
if (!doc) continue;
editors.push({{
document: doc,
selection: new WsbRange(0, 0, 0, 0),
selections: [new WsbRange(0, 0, 0, 0)],
visibleRanges: [new WsbRange(0, 0, 0, 0)],
options: {{}},
viewColumn: gi + 1,
}});
}}
}}
return editors;
}})(),
onDidChangeActiveTextEditor: __wsb_create_event__(),
onDidChangeVisibleTextEditors: __wsb_create_event__(),
onDidChangeTextEditorSelection: __wsb_create_event__(),
tabGroups: (function() {{
const ctx = globalThis.__wsb_workbench_context__;
if (!ctx || !ctx.groups || ctx.groups.length === 0) {{
return {{
all: [],
activeTabGroup: {{ tabs: [], isActive: true, viewColumn: 1 }},
onDidChangeTabs: __wsb_create_event__(),
onDidChangeTabGroups: __wsb_create_event__(),
}};
}}
const all = ctx.groups.map((group, gi) => {{
const tabs = group.tabs.map(tab => {{
const inputUri = tab.file_path
? globalThis.__wsb_uri_from_path__(tab.file_path)
: {{ scheme: "wsb", fsPath: "", path: tab.id, toString() {{ return "wsb:" + tab.id; }} }};
return {{
label: tab.label,
input: {{ uri: inputUri, viewColumn: gi + 1 }},
isActive: tab.is_active,
isPinned: false,
isDirty: false,
}};
}});
return {{
tabs,
isActive: group.is_active,
viewColumn: gi + 1,
}};
}});
const active = all.find(g => g.isActive) || all[0];
return {{
all,
activeTabGroup: active,
onDidChangeTabs: __wsb_create_event__(),
onDidChangeTabGroups: __wsb_create_event__(),
}};
}})(),
createTextEditorDecorationType(_options) {{
return {{ key: "wsb-decoration", dispose() {{}} }};
}},
createTreeView(_viewId, _options) {{
if (_options?.treeDataProvider) {{
globalThis.__wsb_tree_data_providers__[_viewId] = _options.treeDataProvider;
}}
const onDidExpandElementEmitter = new WsbEventEmitter();
const onDidCollapseElementEmitter = new WsbEventEmitter();
const onDidChangeSelectionEmitter = new WsbEventEmitter();
const onDidChangeVisibilityEmitter = new WsbEventEmitter();
return {{
onDidExpandElement: onDidExpandElementEmitter.event,
onDidCollapseElement: onDidCollapseElementEmitter.event,
onDidChangeSelection: onDidChangeSelectionEmitter.event,
onDidChangeVisibility: onDidChangeVisibilityEmitter.event,
visible: true,
selection: [],
reveal(_element, _options) {{ return Promise.resolve(); }},
dispose() {{
onDidExpandElementEmitter.dispose();
onDidCollapseElementEmitter.dispose();
onDidChangeSelectionEmitter.dispose();
onDidChangeVisibilityEmitter.dispose();
delete globalThis.__wsb_tree_data_providers__[_viewId];
}},
}};
}},
registerTreeDataProvider(_viewId, _treeDataProvider) {{
globalThis.__wsb_tree_data_providers__[_viewId] = _treeDataProvider;
return new WsbDisposable(() => {{
delete globalThis.__wsb_tree_data_providers__[_viewId];
}});
}},
}},
languages: {{
registerCompletionItemProvider(_selector, _provider, ..._triggerCharacters) {{
return new WsbDisposable(() => {{}});
}},
registerHoverProvider(_selector, _provider) {{
return new WsbDisposable(() => {{}});
}},
registerDefinitionProvider(_selector, _provider) {{
return new WsbDisposable(() => {{}});
}},
registerCodeActionsProvider(_selector, _provider, _metadata) {{
return new WsbDisposable(() => {{}});
}},
registerDocumentFormattingEditProvider(_selector, _provider) {{
return new WsbDisposable(() => {{}});
}},
createDiagnosticCollection(_name) {{
const entries = new Map();
const collection = {{
name: _name ?? "wsb",
set(uri, diagnostics) {{ entries.set(uri?.toString?.() ?? String(uri), diagnostics); }},
delete(uri) {{ entries.delete(uri?.toString?.() ?? String(uri)); }},
clear() {{ entries.clear(); }},
forEach(callback) {{ entries.forEach(callback); }},
get(uri) {{ return entries.get(uri?.toString?.() ?? String(uri)); }},
has(uri) {{ return entries.has(uri?.toString?.() ?? String(uri)); }},
dispose() {{ entries.clear(); }},
__entries__: entries,
}};
globalThis.__wsb_diagnostic_collections__.push(collection);
return collection;
}},
match(_selector, _document) {{
return 10;
}},
getDiagnostics(_resource) {{
const all = [];
for (const collection of globalThis.__wsb_diagnostic_collections__) {{
if (_resource) {{
const key = _resource?.toString?.() ?? String(_resource);
const diags = collection.__entries__.get(key);
if (diags) all.push(...diags);
}} else {{
collection.__entries__.forEach((diags) => all.push(...diags));
}}
}}
return all;
}},
onDidChangeDiagnostics: __wsb_create_event__(),
}},
Uri: {{
parse(value) {{
return globalThis.__wsb_uri_from_string__(String(value));
}},
file(path) {{
return globalThis.__wsb_uri_from_path__(String(path));
}},
joinPath(base, ...parts) {{
const rawBase = typeof base === "string" ? base : base.fsPath ?? base.toString();
const fsPath = globalThis.__wsb_normalize_path__([rawBase, ...parts].join("/"));
return globalThis.__wsb_uri_from_path__(fsPath);
}},
from(components) {{
const scheme = components.scheme || "file";
const path = components.path || "";
const str = `${{scheme}}://${{components.authority || ""}}${{path}}`;
return globalThis.__wsb_uri_from_string__(str);
}},
}},
ViewColumn: {{ Beside: 2, One: 1, Two: 2, Three: 3, Active: -1 }},
EventEmitter: WsbEventEmitter,
Disposable: WsbDisposable,
CancellationError: WsbCancellationError,
CancellationTokenSource: WsbCancellationTokenSource,
Position: WsbPosition,
Range: WsbRange,
Location: WsbLocation,
DiagnosticSeverity: WsbDiagnosticSeverity,
CompletionItemKind: WsbCompletionItemKind,
SymbolKind: WsbSymbolKind,
CodeActionKind: WsbCodeActionKind,
TextEditorRevealType: WsbTextEditorRevealType,
ConfigurationTarget: WsbConfigurationTarget,
MarkdownString: WsbMarkdownString,
ThemeColor: WsbThemeColor,
ThemeIcon: WsbThemeIcon,
FileChangeType: WsbFileChangeType,
CompletionItem: WsbCompletionItem,
CompletionList: WsbCompletionList,
CompletionTriggerKind: WsbCompletionTriggerKind,
CodeAction: WsbCodeAction,
CodeLens: WsbCodeLens,
Diagnostic: WsbDiagnostic,
DiagnosticTag: WsbDiagnosticTag,
DocumentLink: WsbDocumentLink,
CallHierarchyItem: WsbCallHierarchyItem,
TypeHierarchyItem: WsbTypeHierarchyItem,
SymbolInformation: WsbSymbolInformation,
DocumentSymbol: WsbDocumentSymbol,
InlayHint: WsbInlayHint,
InlayHintKind: WsbInlayHintKind,
Hover: WsbHover,
SignatureHelp: WsbSignatureHelp,
SignatureInformation: WsbSignatureInformation,
ParameterInformation: WsbParameterInformation,
TextEdit: WsbTextEdit,
WorkspaceEdit: WsbWorkspaceEdit,
FoldingRange: WsbFoldingRange,
FoldingRangeKind: WsbFoldingRangeKind,
SelectionRange: WsbSelectionRange,
SemanticTokensLegend: WsbSemanticTokensLegend,
SemanticTokensBuilder: WsbSemanticTokensBuilder,
SnippetString: WsbSnippetString,
TreeItem: WsbTreeItem,
StatusBarAlignment: Object.freeze({{ Left: 1, Right: 2 }}),
ProgressLocation: Object.freeze({{ SourceControl: 1, Window: 10, Notification: 15 }}),
TreeItemCollapsibleState: Object.freeze({{ None: 0, Collapsed: 1, Expanded: 2 }}),
OverviewRulerLane: Object.freeze({{ Left: 1, Center: 2, Right: 4, Full: 7 }}),
DecorationRangeBehavior: Object.freeze({{ OpenOpen: 0, ClosedClosed: 1, OpenClosed: 2, ClosedOpen: 3 }}),
EndOfLine: Object.freeze({{ LF: 1, CRLF: 2 }}),
IndentAction: Object.freeze({{ None: 0, Indent: 1, IndentOutdent: 2, Outdent: 3 }}),
version: __VSCODE_VERSION__,
env: {{
appName: "wsb",
appRoot: globalThis.__wsb_dirname__,
language: "en",
machineId: "wsb-embedded",
sessionId: "wsb-session",
uriScheme: "file",
clipboard: {{
readText() {{ return Promise.resolve(""); }},
writeText(_value) {{ return Promise.resolve(); }},
}},
}},
extensions: {{
all: [],
getExtension(_extensionId) {{ return undefined; }},
onDidChange: __wsb_create_event__(),
}},
}};
return vscodeHost;
case "vscode-languageclient/node":
return {{
TransportKind: {{ ipc: 1 }},
LanguageClient: class LanguageClient {{
constructor(id, _name, serverOptions, clientOptions) {{
this.id = String(id);
this.serverOptions = serverOptions;
this.clientOptions = clientOptions;
globalThis.__wsb_language_client_handlers__[this.id] = Object.create(null);
}}
start() {{
const modulePath =
this.serverOptions?.run?.module ??
this.serverOptions?.debug?.module;
if (!modulePath) {{
throw new Error(`embedded LanguageClient '${{this.id}}' requires a server module`);
}}
globalThis.__wsb_language_client_specs__.push({{
client_id: this.id,
server_module: String(modulePath),
initialization_options: this.clientOptions?.initializationOptions ?? {{}},
}});
return Promise.resolve(this);
}}
onNotification(method, callback) {{
if (typeof callback !== "function") {{
throw new Error(`embedded LanguageClient notification '${{method}}' requires a callable listener`);
}}
globalThis.__wsb_language_client_handlers__[this.id][String(method)] = callback;
return {{ dispose() {{ delete globalThis.__wsb_language_client_handlers__[this.id][String(method)]; }} }};
}}
stop() {{
return Promise.resolve();
}}
}},
}};
__BUILTIN_MODULES__
default:
throw new Error(`unsupported builtin module: ${{name}}`);
}}
}};
"#;
template
.replace("{{", "{")
.replace("}}", "}")
.replace("__COMMON_RUNTIME_PRIMITIVES__", common_runtime_primitives())
.replace("__EXTENSION_ID_JSON__", &extension_id_json)
.replace("__SERVER_BASE_URL_JSON__", &server_base_url_json)
.replace("__EXTENSION_ROOT_JSON__", &extension_root_json)
.replace("__ASSET_FILES_JSON__", &asset_files_json)
.replace("__FILE__", &file)
.replace("__DIR__", &dir)
.replace("__WORKSPACE_FOLDER_PATHS__", &workspace_folder_paths)
.replace("__TEXT_DOCUMENT_SPECS__", &text_document_specs)
.replace("__WORKBENCH_CONTEXT_JSON__", &workbench_context_json)
.replace(
"__VSCODE_VERSION__",
&format!("\"{VSCODE_COMPAT_VERSION}\""),
)
.replace("__BUILTIN_MODULES__", &builtin_module_switch_arms())
}
fn common_runtime_primitives() -> &'static str {
r#"
globalThis.queueMicrotask = globalThis.queueMicrotask ?? function(callback) {
Promise.resolve().then(callback);
};
globalThis.setTimeout = function(callback, _ms, ...args) {
queueMicrotask(() => callback(...args));
return 1;
};
globalThis.clearTimeout = function(_id) {};
globalThis.setImmediate = function(callback, ...args) {
queueMicrotask(() => callback(...args));
return 1;
};
globalThis.clearImmediate = function(_id) {};
globalThis.setInterval = function(_callback, _ms, ..._args) {
return 1;
};
globalThis.clearInterval = function(_id) {};
function __wsb_utf8_encode__(value) {
const encoded = encodeURIComponent(String(value));
const bytes = [];
for (let index = 0; index < encoded.length; index += 1) {
const char = encoded[index];
if (char === "%") {
bytes.push(parseInt(encoded.slice(index + 1, index + 3), 16));
index += 2;
continue;
}
bytes.push(char.charCodeAt(0));
}
return new Uint8Array(bytes);
}
function __wsb_utf8_decode__(value) {
let encoded = "";
for (const byte of value) {
encoded += `%${byte.toString(16).padStart(2, "0")}`;
}
return decodeURIComponent(encoded);
}
class WsbTextEncoder {
encode(value = "") {
return __wsb_utf8_encode__(value);
}
}
class WsbTextDecoder {
constructor(label = "utf-8") {
const normalized = String(label).toLowerCase();
if (normalized !== "utf-8" && normalized !== "utf8") {
throw new Error(`unsupported TextDecoder encoding '${label}'`);
}
}
decode(value = new Uint8Array()) {
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
return __wsb_utf8_decode__(bytes);
}
}
globalThis.TextEncoder = WsbTextEncoder;
globalThis.TextDecoder = WsbTextDecoder;
class WsbBuffer extends Uint8Array {
static alloc(size, fill = 0) {
const buffer = new WsbBuffer(size);
buffer.fill(fill);
return buffer;
}
static allocUnsafe(size) {
return new WsbBuffer(size);
}
static from(value) {
if (typeof value === "string") {
return new WsbBuffer(new TextEncoder().encode(value));
}
if (ArrayBuffer.isView(value)) {
return new WsbBuffer(value);
}
if (value instanceof ArrayBuffer) {
return new WsbBuffer(new Uint8Array(value));
}
if (Array.isArray(value)) {
return new WsbBuffer(value);
}
throw new Error("unsupported Buffer.from input during embedded runtime execution");
}
static concat(values) {
const total = values.reduce((sum, value) => sum + value.length, 0);
const result = new WsbBuffer(total);
let offset = 0;
for (const value of values) {
result.set(value, offset);
offset += value.length;
}
return result;
}
static byteLength(value) {
return WsbBuffer.from(value).length;
}
toString() {
return new TextDecoder().decode(this);
}
}
globalThis.Buffer = WsbBuffer;
globalThis.Buffer.isBuffer = function(value) {
return value instanceof WsbBuffer;
};
globalThis.__wsb_normalize_path__ = function(value) {
const parts = String(value).split("/");
const normalized = [];
for (const part of parts) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
throw new Error(`path traversal is not allowed in embedded runtime: ${value}`);
}
normalized.push(part);
}
return "/" + normalized.join("/");
};
globalThis.__wsb_uri_from_path__ = function(fsPath) {
const normalized = globalThis.__wsb_normalize_path__(fsPath);
const uriValue = `file://${normalized}`;
return {
scheme: "file",
fsPath: normalized,
path: normalized,
toString() {
return uriValue;
},
};
};
globalThis.__wsb_uri_from_string__ = function(value) {
const stringValue = String(value);
if (stringValue.startsWith("file://")) {
const fsPath = stringValue.slice("file://".length) || "/";
return {
scheme: "file",
fsPath,
path: fsPath,
toString() {
return stringValue;
},
};
}
const scheme = stringValue.includes(":") ? stringValue.split(":")[0] : "file";
return {
scheme,
fsPath: stringValue,
path: stringValue,
toString() {
return stringValue;
},
};
};
globalThis.__wsb_asset_url__ = function(path) {
if (!globalThis.__wsb_server_base_url__) {
throw new Error("extension asset URL requested without configured server base URL");
}
const normalized = globalThis.__wsb_normalize_path__(path);
return `${globalThis.__wsb_server_base_url__}/api/v1/workspace/extensions/${encodeURIComponent(globalThis.__wsb_extension_id__)}/assets?path=${encodeURIComponent(normalized)}`;
};
"#
}
fn builtin_module_switch_arms() -> String {
let arms = vec![
r#"case "path":
return {
join: (...parts) => globalThis.__wsb_normalize_path__(parts.join("/")),
normalize: (value) => globalThis.__wsb_normalize_path__(value),
isAbsolute: (value) => String(value).startsWith("/"),
delimiter: ":",
};"#,
r#"case "fs":
return {
readFileSync(path, encoding) {
const normalized = globalThis.__wsb_normalize_path__(path);
const bytes = globalThis.__wsb_asset_files__[normalized];
if (!bytes) {
throw new Error(`fs.readFileSync cannot access '${normalized}' in embedded runtime`);
}
const buffer = WsbBuffer.from(bytes);
if (encoding === "utf8" || encoding === "utf-8") {
return new TextDecoder().decode(buffer);
}
if (encoding === undefined || encoding === null) {
return buffer;
}
throw new Error(`unsupported fs.readFileSync encoding '${encoding}'`);
},
existsSync(path) {
const normalized = globalThis.__wsb_normalize_path__(path);
return Object.prototype.hasOwnProperty.call(globalThis.__wsb_asset_files__, normalized);
},
lstat(path, callback) {
const normalized = globalThis.__wsb_normalize_path__(path);
const isFile = Object.prototype.hasOwnProperty.call(globalThis.__wsb_asset_files__, normalized);
// In the embedded runtime, report all paths as existing.
// LanguageClient uses lstat to verify server working directories
// before spawning (which we intercept), so this must succeed.
callback(null, { isFile() { return isFile; }, isDirectory() { return !isFile; }, isSymbolicLink() { return false; } });
},
statSync(path) {
const normalized = globalThis.__wsb_normalize_path__(path);
const exists = Object.prototype.hasOwnProperty.call(globalThis.__wsb_asset_files__, normalized);
if (!exists) {
throw new Error(`ENOENT: no such file or directory, stat '${normalized}'`);
}
return { isFile() { return true; }, isDirectory() { return false; }, isSymbolicLink() { return false; } };
},
};"#,
r#"case "os":
return {
platform: () => "linux",
};"#,
r#"case "child_process":
return {
fork(modulePath, _args, _options) {
const normalized = globalThis.__wsb_normalize_path__(modulePath);
const bytes = globalThis.__wsb_asset_files__[normalized];
if (!bytes) {
throw new Error(`child_process.fork: server module '${normalized}' not found in embedded assets`);
}
const serverSource = new TextDecoder().decode(new Uint8Array(bytes));
// Message listeners registered by the LanguageClient on the child process
const messageListeners = [];
const exitListeners = [];
const errorListeners = [];
// Message listeners registered by the server via process.on('message', ...)
const serverMessageListeners = [];
// Fake readable streams (stderr/stdout) that the LanguageClient reads from
function makeStream() {
const dataListeners = [];
return {
on(event, listener) {
if (event === 'data') dataListeners.push(listener);
},
removeListener() {},
_push(data) {
for (const l of dataListeners) l(data);
},
};
}
const fakeStdout = makeStream();
const fakeStderr = makeStream();
// The fake child process object
const fakeProcess = {
pid: 2,
connected: true,
killed: false,
exitCode: null,
signalCode: null,
stdout: fakeStdout,
stderr: fakeStderr,
stdin: { write() {}, end() {}, on() {} },
send(message, _sendHandle, _options, callback) {
// Route message from LanguageClient → server
queueMicrotask(() => {
for (const listener of serverMessageListeners) {
listener(message);
}
});
if (typeof callback === 'function') callback(null);
return true;
},
kill() {
fakeProcess.killed = true;
fakeProcess.connected = false;
for (const l of exitListeners) l(0, null);
return true;
},
on(event, listener) {
if (event === 'message') messageListeners.push(listener);
else if (event === 'exit') exitListeners.push(listener);
else if (event === 'error') errorListeners.push(listener);
return fakeProcess;
},
off(event, listener) {
let arr;
if (event === 'message') arr = messageListeners;
else if (event === 'exit') arr = exitListeners;
else if (event === 'error') arr = errorListeners;
if (arr) {
const idx = arr.indexOf(listener);
if (idx !== -1) arr.splice(idx, 1);
}
return fakeProcess;
},
removeListener(event, listener) { return fakeProcess.off(event, listener); },
once(event, listener) {
const wrapper = (...args) => {
fakeProcess.off(event, wrapper);
listener(...args);
};
return fakeProcess.on(event, wrapper);
},
addListener(event, listener) { return fakeProcess.on(event, listener); },
removeAllListeners(event) {
if (event === 'message') messageListeners.length = 0;
else if (event === 'exit') exitListeners.length = 0;
else if (event === 'error') errorListeners.length = 0;
return fakeProcess;
},
unref() { return fakeProcess; },
ref() { return fakeProcess; },
disconnect() { fakeProcess.connected = false; },
emit() { return false; },
eventNames() { return []; },
listenerCount() { return 0; },
listeners() { return []; },
rawListeners() { return []; },
prependListener(event, listener) { return fakeProcess.on(event, listener); },
prependOnceListener(event, listener) { return fakeProcess.once(event, listener); },
getMaxListeners() { return 100; },
setMaxListeners() { return fakeProcess; },
};
// Boot the server module in-process with a fake process.send/process.on
const serverProcess = {
env: Object.create(null),
argv: ["node", normalized, "--node-ipc"],
pid: 2,
platform: "linux",
versions: { node: "20.0.0" },
cwd() { return globalThis.__wsb_dirname__; },
nextTick(callback, ...args) { queueMicrotask(() => callback(...args)); },
kill() { return true; },
exit(_code) {},
on(event, callback) {
if (event === "message") {
serverMessageListeners.push(callback);
}
},
send(message) {
// Route message from server → LanguageClient
queueMicrotask(() => {
for (const listener of messageListeners) {
listener(message);
}
});
},
};
// Swap in the server's fake process and require, then eval the server source.
// We save/restore globals to avoid permanently clobbering the extension's environment.
const savedProcess = globalThis.process;
const savedRequire = globalThis.__wsb_require__;
const savedFilename = globalThis.__wsb_filename__;
const savedDirname = globalThis.__wsb_dirname__;
const serverDir = normalized.split("/").slice(0, -1).join("/") || "/";
const serverRequire = function(name) {
const known = ["path", "fs", "os", "crypto", "util", "node:util", "net"];
if (known.includes(name)) return savedRequire(name);
if (name === "url") return { parse(v) { const p = new URL(String(v)); return { protocol: p.protocol, path: `${p.pathname}${p.search}`, href: p.href }; } };
// child_process is required by the server bundle's externals but
// the server itself only uses process.on/process.send for IPC
if (name === "child_process") return { fork() { throw new Error("nested child_process.fork is not supported"); } };
throw new Error(`server require: unsupported module '${name}'`);
};
try {
globalThis.process = serverProcess;
globalThis.require = serverRequire;
globalThis.module = { exports: {} };
globalThis.exports = globalThis.module.exports;
globalThis.__filename = normalized;
globalThis.__dirname = serverDir;
globalThis.__wsb_filename__ = normalized;
globalThis.__wsb_dirname__ = serverDir;
// The server source is a webpack IIFE (() => { ... })() that
// references process, require, module, __filename, __dirname from
// the enclosing scope (which is globalThis when eval'd).
(0, eval)(serverSource);
} catch (e) {
if (errorListeners.length > 0) {
for (const l of errorListeners) l(e);
} else {
throw new Error(`child_process.fork: server module failed to load: ${e?.message ?? e}`);
}
} finally {
globalThis.process = savedProcess;
globalThis.__wsb_require__ = savedRequire;
globalThis.__wsb_filename__ = savedFilename;
globalThis.__wsb_dirname__ = savedDirname;
delete globalThis.require;
delete globalThis.module;
delete globalThis.exports;
delete globalThis.__filename;
delete globalThis.__dirname;
}
return fakeProcess;
},
spawnSync() {
throw new Error("child_process.spawnSync is unavailable in the embedded runtime");
},
};"#,
r#"case "crypto":
return {
randomUUID() {
return "00000000-0000-4000-8000-000000000000";
},
};"#,
r#"case "net":
return Object.create(null);"#,
r#"case "events":
case "node:events":
class EventEmitter {
constructor() {
this.__wsb_listeners__ = Object.create(null);
this.__wsb_max_listeners__ = 10;
}
on(event, listener) {
return this.addListener(event, listener);
}
addListener(event, listener) {
if (typeof listener !== "function") {
throw new TypeError("EventEmitter listener must be a function");
}
const key = String(event);
const listeners = this.__wsb_listeners__[key] ?? (this.__wsb_listeners__[key] = []);
listeners.push(listener);
return this;
}
off(event, listener) {
return this.removeListener(event, listener);
}
removeListener(event, listener) {
const key = String(event);
const listeners = this.__wsb_listeners__[key];
if (!listeners) {
return this;
}
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
if (listeners.length === 0) {
delete this.__wsb_listeners__[key];
}
return this;
}
once(event, listener) {
if (typeof listener !== "function") {
throw new TypeError("EventEmitter listener must be a function");
}
const wrapper = (...args) => {
this.removeListener(event, wrapper);
listener.apply(this, args);
};
return this.addListener(event, wrapper);
}
prependListener(event, listener) {
if (typeof listener !== "function") {
throw new TypeError("EventEmitter listener must be a function");
}
const key = String(event);
const listeners = this.__wsb_listeners__[key] ?? (this.__wsb_listeners__[key] = []);
listeners.unshift(listener);
return this;
}
prependOnceListener(event, listener) {
if (typeof listener !== "function") {
throw new TypeError("EventEmitter listener must be a function");
}
const wrapper = (...args) => {
this.removeListener(event, wrapper);
listener.apply(this, args);
};
return this.prependListener(event, wrapper);
}
emit(event, ...args) {
const key = String(event);
const listeners = this.__wsb_listeners__[key];
if (!listeners || listeners.length === 0) {
return false;
}
for (const listener of [...listeners]) {
listener.apply(this, args);
}
return true;
}
removeAllListeners(event) {
if (event === undefined) {
this.__wsb_listeners__ = Object.create(null);
return this;
}
delete this.__wsb_listeners__[String(event)];
return this;
}
listeners(event) {
return [...(this.__wsb_listeners__[String(event)] ?? [])];
}
rawListeners(event) {
return this.listeners(event);
}
listenerCount(event) {
return this.listeners(event).length;
}
eventNames() {
return Object.keys(this.__wsb_listeners__);
}
setMaxListeners(value) {
this.__wsb_max_listeners__ = Number(value);
return this;
}
getMaxListeners() {
return this.__wsb_max_listeners__;
}
}
return {
EventEmitter,
default: EventEmitter,
once(emitter, event) {
return new Promise((resolve) => {
emitter.once(event, (...args) => resolve(args));
});
},
};"#,
r#"case "util":
case "node:util":
return {
inspect(value) {
return String(value);
},
};"#,
];
arms.join("\n ")
}
fn wrap_commonjs_bundle(source: &str) -> String {
format!(
r#"
const __wsb_module = {{ exports: {{}} }};
const __wsb_exports = __wsb_module.exports;
(function(exports, require, module, __filename, __dirname) {{
{source}
}})(__wsb_exports, globalThis.__wsb_require__, __wsb_module, globalThis.__wsb_filename__, globalThis.__wsb_dirname__);
globalThis.__wsb_export_names__ = Object.keys(__wsb_module.exports);
globalThis.__wsb_module__ = __wsb_module;
"#
)
}
#[cfg(test)]
mod tests {
use super::inspect_commonjs_bundle;
use std::path::Path;
#[test]
fn inspect_bundle_supports_node_events_builtin() {
let source = r#"
const { EventEmitter } = require("events");
const emitter = new EventEmitter();
let fired = false;
emitter.once("ready", () => { fired = true; });
emitter.emit("ready");
if (!fired) {
throw new Error("EventEmitter did not deliver the event");
}
exports.activate = () => {};
"#;
let inspection =
inspect_commonjs_bundle(source, Path::new("./dist/extension/desktop/extension.cjs"))
.expect(
"embedded runtime must support the Node events builtin during bundle inspection",
);
assert!(
inspection
.export_names
.iter()
.any(|name| name == "activate"),
"bundle inspection must complete after requiring the Node events builtin"
);
}
}
fn activation_script() -> String {
r#"
if (typeof globalThis.__wsb_module__?.exports?.activate === "function") {
const __wsb_root__ = globalThis.__wsb_extension_root__;
const context = {
subscriptions: [],
extensionUri: globalThis.__wsb_uri_from_path__(__wsb_root__),
extensionPath: __wsb_root__,
asAbsolutePath(path) {
return globalThis.__wsb_normalize_path__([__wsb_root__, path].join("/"));
},
};
globalThis.__wsb_module__.exports.activate(context);
globalThis.__wsb_subscription_count__ = context.subscriptions.length;
}
"#
.to_string()
}
fn command_execution_script(command_id: &str) -> String {
let command_id = serde_json::to_string(command_id).unwrap();
format!(
r#"
const __wsb_command_id__ = {command_id};
const __wsb_command_callback__ = globalThis.__wsb_command_callbacks__[__wsb_command_id__];
if (typeof __wsb_command_callback__ !== "function") {{
throw new Error(`command '${{__wsb_command_id__}}' is not registered by the activated extension`);
}}
const __wsb_command_result__ = __wsb_command_callback__();
if (__wsb_command_result__ && typeof __wsb_command_result__.then === "function") {{
globalThis.__wsb_command_result__ = __wsb_command_result__;
}} else {{
globalThis.__wsb_command_result__ = Promise.resolve(__wsb_command_result__);
}}
"#
)
}
fn initial_workspace_folder_paths(document: Option<&TextDocumentContext>) -> Vec<String> {
match document {
Some(doc) => {
let path = std::path::Path::new(&doc.path);
match path.parent() {
Some(parent) => vec![parent.to_string_lossy().into_owned()],
None => Vec::new(),
}
}
None => Vec::new(),
}
}
fn initial_text_document_specs(document: Option<&TextDocumentContext>) -> Vec<Value> {
match document {
Some(doc) => vec![
json!({ "uri": doc.uri, "languageId": doc.language_id, "content": doc.content, "version": 1 }),
],
None => Vec::new(),
}
}
fn drive_runtime(runtime: &mut JsRuntime) -> Result<()> {
let result = block_on_maybe_async(runtime.run_event_loop(Default::default()))
.context("embedded runtime event loop failed")?;
result
.map_err(|e| anyhow::anyhow!("{}", e))
.context("embedded runtime event loop failed")
}
fn block_on_maybe_async<F>(future: F) -> Result<F::Output>
where
F: Future,
{
if let Ok(handle) = tokio::runtime::Handle::try_current() {
Ok(tokio::task::block_in_place(|| handle.block_on(future)))
} else {
let runtime = tokio::runtime::Runtime::new()
.context("failed to create Tokio runtime for embedded extension execution")?;
Ok(runtime.block_on(future))
}
}
fn read_global_json<T>(runtime: &mut JsRuntime, key: &str) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let value = exec_script(runtime, "<wsb:read-global>", format!("globalThis[{key:?}]"))
.with_context(|| format!("failed to evaluate global lookup for {key}"))?;
deno_core::scope!(scope, runtime);
let local = v8::Local::new(scope, value);
let parsed =
serde_v8::from_v8::<T>(scope, local).with_context(|| format!("failed to decode {key}"))?;
Ok(parsed)
}