Skip to content

Commit 07b4da7

Browse files
fix(Mountain): Fix extension location URI marshalling for VS Code renderer
VS Code's URI revival system ignores objects lacking the `$mid === 1` marshalling marker. Without it, extension locations sent to the renderer become plain `UriComponents` bags where `.fsPath` and `.with` are undefined — the sidebar silently filters the entire batch out. This change adds three functions in Extensions.rs: - `Normalize_Location_To_UriComponents`: converts the scanner's raw `file://` URL string into VS Code's `UriComponents` object shape - `Stamp_Mid_Uri`: attaches `$mid: 1` to any UriComponents that lack it - `Parse_File_Url_To_UriComponents`: parses `file:///absolute/path` URLs into the required structure Also extends `BuildInitialUrl` to recognize Mountain's own `{ uri: "file://…" }` format (written by the recently opened persistence) alongside VS Code's historical `folderUri`/`workspace.configPath` shapes for backward compatibility with imported profiles. Extension locations in the sidebar now resolve correctly.
1 parent aa5eafb commit 07b4da7

2 files changed

Lines changed: 110 additions & 3 deletions

File tree

Source/Binary/Build/WindowBuild.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ fn BuildInitialUrl(LocalhostUrl:&str) -> String {
119119
// broadcast. Probe them in the same priority order the workbench
120120
// itself uses in `getRecentlyOpenedWorkspaces`.
121121
let Probe = |Entry:&serde_json::Value| -> Option<String> {
122+
// Mountain's own writer emits `{ uri: "file://…", label }` (see
123+
// `RecentlyOpened.json` on a freshly closed window). VS Code's
124+
// historical `folderUri` / `workspace.configPath` shapes are kept
125+
// as fallbacks so imported profiles and third-party writers keep
126+
// working.
127+
if let Some(Uri) = Entry.get("uri").and_then(|V| V.as_str()) {
128+
return Some(Uri.to_string());
129+
}
122130
if let Some(Uri) = Entry.get("folderUri").and_then(|V| V.as_str()) {
123131
return Some(Uri.to_string());
124132
}

Source/IPC/WindServiceHandlers/Extensions.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,96 @@ use serde_json::{Value, json};
99

1010
use crate::{dev_log, RunTime::ApplicationRunTime::ApplicationRunTime};
1111

12+
/// `MarshalledId.Uri` from VS Code's `src/vs/base/common/marshallingIds.ts`.
13+
/// VS Code's `transformAndReviveIncomingURIs` walks every property of every
14+
/// object returned from the IPC channel and only calls `URI.revive()` on
15+
/// nested objects that carry `$mid === 1`. Without this marker the extension
16+
/// location surfaces as a plain `UriComponents` bag; downstream
17+
/// `resources.joinPath(local.location, …)` then trips on
18+
/// `uri.with is not a function` the moment the sidebar tries to build an
19+
/// icon URL. Every UriComponents we produce has to carry `$mid: 1`.
20+
const MID_URI:u64 = 1;
21+
22+
/// Reshape an `extensionLocation` field into VS Code's `UriComponents` object
23+
/// form. The extension scanner stores it as a raw `file://` URL string; the
24+
/// renderer's URI reviver is keyed off the `$mid` marshalling marker and only
25+
/// revives objects that carry it, so the emitted shape must always include
26+
/// `$mid: 1` alongside the usual `scheme / authority / path / query /
27+
/// fragment` fields - otherwise the sidebar silently filters the whole batch
28+
/// out (no `$mid` ⇒ no revive ⇒ `.fsPath` / `.with` undefined).
29+
fn Normalize_Location_To_UriComponents(Raw:Option<&Value>) -> Value {
30+
let Base = match Raw {
31+
Some(Value::Object(Map)) if Map.contains_key("scheme") => Value::Object(Map.clone()),
32+
Some(Value::String(Url)) => Parse_File_Url_To_UriComponents(Url),
33+
_ => json!({
34+
"scheme": "file",
35+
"authority": "",
36+
"path": "/extensions/unknown",
37+
"query": "",
38+
"fragment": "",
39+
}),
40+
};
41+
Stamp_Mid_Uri(Base)
42+
}
43+
44+
/// Attach `$mid: 1` to a `UriComponents` object if it isn't already present.
45+
/// Guards against both missing markers (the common case; Mountain built the
46+
/// object ourselves) and pre-marked payloads (passthrough from an upstream
47+
/// service that already tagged it). Returns the input unchanged for any
48+
/// non-object value - callers are expected to have already produced a shape.
49+
fn Stamp_Mid_Uri(Input:Value) -> Value {
50+
match Input {
51+
Value::Object(mut Map) => {
52+
Map.entry("$mid".to_string()).or_insert(json!(MID_URI));
53+
Value::Object(Map)
54+
}
55+
Other => Other,
56+
}
57+
}
58+
59+
/// Minimal `file://` URL → `UriComponents` parser. Accepts
60+
/// `file:///absolute/path` (authority empty) and preserves the path verbatim.
61+
/// Non-`file:` schemes are parsed generically (scheme: path).
62+
fn Parse_File_Url_To_UriComponents(Url:&str) -> Value {
63+
if let Some(Rest) = Url.strip_prefix("file://") {
64+
// `file:///Volumes/...` → authority="", path="/Volumes/..."
65+
let (Authority, Path) = match Rest.find('/') {
66+
Some(0) => ("", Rest),
67+
Some(Index) => (&Rest[..Index], &Rest[Index..]),
68+
None => ("", ""),
69+
};
70+
return json!({
71+
"scheme": "file",
72+
"authority": Authority,
73+
"path": Path,
74+
"query": "",
75+
"fragment": "",
76+
});
77+
}
78+
if let Some((Scheme, PathPart)) = Url.split_once(':') {
79+
let Trimmed = PathPart.trim_start_matches("//");
80+
let (Authority, Path) = match Trimmed.find('/') {
81+
Some(0) => ("", Trimmed),
82+
Some(Index) => (&Trimmed[..Index], &Trimmed[Index..]),
83+
None => ("", Trimmed),
84+
};
85+
return json!({
86+
"scheme": Scheme,
87+
"authority": Authority,
88+
"path": Path,
89+
"query": "",
90+
"fragment": "",
91+
});
92+
}
93+
json!({
94+
"scheme": "file",
95+
"authority": "",
96+
"path": Url,
97+
"query": "",
98+
"fragment": "",
99+
})
100+
}
101+
12102
/// Return scanned extensions reshaped as VS Code's `ILocalExtension[]`
13103
/// so `ExtensionManagementChannelClient.getInstalled` can destructure
14104
/// `extension.identifier.id`, `extension.manifest.*`, and
@@ -31,9 +121,18 @@ pub async fn handle_extensions_get_installed(runtime:Arc<ApplicationRunTime>) ->
31121
let Name = Manifest.get("name").and_then(Value::as_str).unwrap_or("unknown").to_string();
32122
let Id = format!("{}.{}", Publisher, Name);
33123

34-
let Location = Manifest.get("extensionLocation").cloned().unwrap_or_else(|| {
35-
json!({ "scheme": "file", "path": "/extensions/unknown", "authority": "" })
36-
});
124+
// VS Code's `URI.revive()` is a no-op on strings, so the scanner's
125+
// `file://…` raw URL has to be reshaped into `UriComponents` here -
126+
// otherwise every `location.fsPath` / `location.scheme` access in
127+
// the sidebar silently returns `undefined` and the whole batch is
128+
// filtered out. Normalize once, reuse for both the top-level
129+
// `location` and the mirror inside `manifest.extensionLocation` so
130+
// callers that read either field get the same shape.
131+
let Location = Normalize_Location_To_UriComponents(Manifest.get("extensionLocation"));
132+
let mut Manifest = Manifest;
133+
if let Value::Object(ref mut Map) = Manifest {
134+
Map.insert("extensionLocation".to_string(), Location.clone());
135+
}
37136

38137
json!({
39138
// IExtension (base)

0 commit comments

Comments
 (0)