Files
openidea/crates/ide-core/src/jsonc.rs
l0ng-ai 1f9be73174 feat(remote): Reload the environment and apply devcontainer settings on the remote server (#183)
The remote server read the login shell's PATH once and cached it, had no project-level environment, and changed environment variables only took effect after reinstalling or a manual restart.

- Every openidea-server start re-reads the remote login shell's full environment (`$SHELL -ilc env -0`), with the cached PATH only as a fallback when the shell doesn't answer.
- A project's `.devcontainer/devcontainer.json` (or `.devcontainer.json`) applies on remote hosts: `remoteEnv` (with `${localEnv|containerEnv|env:X[:default]}`, workspace variables and `null` to unset) reaches every child process through one environment overlay (terminals, run configurations, language servers, debuggers, build tools, git), and `postStartCommand` (string, array or parallel object) runs once per server start in the project root as a cancellable background task; a failure raises a notification with the exit code and the last output lines without blocking the project.
- Reload Environment (`remote.reloadEnvironment`, File menu and Find Action) re-resolves the login shell and `remoteEnv` without restarting the server, starting from the process's original environment so prepended PATH entries don't pile up; it lists the changed variables, notes that running processes keep their environment, and offers to restart language servers.
- The JSONC reader moves to `ide_core::jsonc` and is shared with ide-run.

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
2026-10-29 09:28:18 +08:00

91 lines
2.8 KiB
Rust

//! JSON with comments (`devcontainer.json`, `deno.jsonc`, `tsconfig.json`): turned
//! into plain JSON for `serde_json`.
/// Strips `//` and `/* */` comments and trailing commas so JSONC parses as JSON.
pub fn strip(src: &str) -> String {
let mut out = String::with_capacity(src.len());
let mut chars = src.chars().peekable();
let mut in_str = false;
while let Some(c) = chars.next() {
if in_str {
out.push(c);
if c == '\\' {
if let Some(n) = chars.next() {
out.push(n);
}
} else if c == '"' {
in_str = false;
}
continue;
}
match c {
'"' => {
in_str = true;
out.push(c);
}
'/' if chars.peek() == Some(&'/') => {
for n in chars.by_ref() {
if n == '\n' {
out.push('\n');
break;
}
}
}
'/' if chars.peek() == Some(&'*') => {
chars.next();
let mut prev = '\0';
for n in chars.by_ref() {
if prev == '*' && n == '/' {
break;
}
prev = n;
}
}
_ => out.push(c),
}
}
// Remove trailing commas before `}` / `]` (outside strings).
let mut cleaned = String::with_capacity(out.len());
let bytes: Vec<char> = out.chars().collect();
let mut in_str = false;
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if in_str {
cleaned.push(c);
if c == '\\' && i + 1 < bytes.len() {
cleaned.push(bytes[i + 1]);
i += 1;
} else if c == '"' {
in_str = false;
}
} else if c == '"' {
in_str = true;
cleaned.push(c);
} else if c == ',' {
let next = bytes[i + 1..].iter().find(|ch| !ch.is_whitespace());
if !matches!(next, Some('}') | Some(']')) {
cleaned.push(c);
}
} else {
cleaned.push(c);
}
i += 1;
}
cleaned
}
#[cfg(test)]
mod tests {
use super::strip;
#[test]
fn drops_comments_and_trailing_commas_outside_strings() {
let src = "{\n // line\n \"a\": [1, 2,], /* block */\n \"url\": \"https://x.dev//y /* no */\",\n \"q\": \"say \\\"hi\\\",\",\n}\n";
let v: serde_json::Value = serde_json::from_str(&strip(src)).unwrap();
assert_eq!(v["a"], serde_json::json!([1, 2]));
assert_eq!(v["url"], "https://x.dev//y /* no */");
assert_eq!(v["q"], "say \"hi\",");
}
}