Skip to content
Open
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
13 changes: 9 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@

## Unreleased

### Added

- Support for paths that begin with a tilde (`~`).

### Fixed

- Fixed race condition when multiple VS Code windows download the Coder CLI binary simultaneously.
Other windows now wait and display real-time progress instead of attempting concurrent downloads,
preventing corruption and failures.
- Remove duplicate "Cancel" buttons on the workspace update dialog.

### Changed

Expand All @@ -15,9 +20,9 @@

## [v1.11.4](https://github.com/coder/vscode-coder/releases/tag/v1.11.4) 2025-11-20

### Fixed
### Added

- Add support for `google.antigravity-remote-openssh` Remote SSH extension.
- Support for the `google.antigravity-remote-openssh` Remote SSH extension.

### Changed

Expand Down Expand Up @@ -55,7 +60,7 @@

### Changed

- Always enable verbose (`-v`) flag when a log directory is configured (`coder.proxyLogDir`).
- Always enable verbose (`-v`) flag when a log directory is configured (`coder.proxyLogDirectory`).
- Automatically start a workspace without prompting if it is explicitly opened but not running.

### Added
Expand Down Expand Up @@ -134,7 +139,7 @@

### Added

- Coder extension sidebar now displays available app statuses, and let's
- Coder extension sidebar now displays available app statuses, and lets
the user click them to drop into a session with a running AI Agent.

## [v1.7.1](https://github.com/coder/vscode-coder/releases/tag/v1.7.1) (2025-04-14)
Expand Down
12 changes: 7 additions & 5 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,14 @@ export function toSafeHost(rawUrl: string): string {
}

/**
* Expand a path with ${userHome} in the input string
* @param input string
* @returns string
* Expand a path if it starts with tilde (~) or contains ${userHome}.
*/
export function expandPath(input: string): string {
const userHome = os.homedir();
return input.replace(/\${userHome}/g, userHome);
if (input.startsWith("~")) {
input = userHome + input.substring("~".length);
}
return input.replaceAll("${userHome}", userHome);
}

/**
Expand All @@ -145,5 +146,6 @@ export function countSubstring(needle: string, haystack: string): number {
}

export function escapeCommandArg(arg: string): string {
return `"${arg.replace(/"/g, '\\"')}"`;
const escapedString = arg.replaceAll('"', String.raw`\"`);
return `"${escapedString}"`;
}
68 changes: 67 additions & 1 deletion test/unit/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import os from "node:os";
import { describe, it, expect } from "vitest";

import { countSubstring, parseRemoteAuthority, toSafeHost } from "@/util";
import {
countSubstring,
escapeCommandArg,
expandPath,
parseRemoteAuthority,
toSafeHost,
} from "@/util";

it("ignore unrelated authorities", () => {
const tests = [
Expand Down Expand Up @@ -124,3 +131,62 @@ describe("countSubstring", () => {
expect(countSubstring("aa", "aaaaaa")).toBe(3);
});
});

describe("escapeCommandArg", () => {
it("wraps simple string in quotes", () => {
expect(escapeCommandArg("hello")).toBe('"hello"');
});

it("handles empty string", () => {
expect(escapeCommandArg("")).toBe('""');
});

it("escapes double quotes", () => {
expect(escapeCommandArg('say "hello"')).toBe(String.raw`"say \"hello\""`);
});

it("preserves backslashes", () => {
expect(escapeCommandArg(String.raw`path\to\file`)).toBe(
String.raw`"path\to\file"`,
);
});

it("handles string with spaces", () => {
expect(escapeCommandArg("hello world")).toBe('"hello world"');
});
});

describe("expandPath", () => {
const home = os.homedir();

it("expands tilde at start of path", () => {
expect(expandPath("~/foo/bar")).toBe(`${home}/foo/bar`);
});

it("expands standalone tilde", () => {
expect(expandPath("~")).toBe(home);
});

it("does not expand tilde in middle of path", () => {
expect(expandPath("/foo/~/bar")).toBe("/foo/~/bar");
});

it("expands ${userHome} variable", () => {
expect(expandPath("${userHome}/foo")).toBe(`${home}/foo`);
});

it("expands multiple ${userHome} variables", () => {
expect(expandPath("${userHome}/foo/${userHome}/bar")).toBe(
`${home}/foo/${home}/bar`,
);
});

it("leaves paths without tilde or variable unchanged", () => {
expect(expandPath("/absolute/path")).toBe("/absolute/path");
expect(expandPath("relative/path")).toBe("relative/path");
});

it("expands both tilde and ${userHome}", () => {
expect(expandPath("~/${userHome}/foo")).toBe(`${home}/${home}/foo`);
});
});