Skip to content

Commit addd1bc

Browse files
committed
feat: enhance openImage function to handle vscode webview CDN URLs
1 parent e71ec43 commit addd1bc

File tree

2 files changed

+125
-21
lines changed

2 files changed

+125
-21
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
vi.mock("vscode", () => {
2+
const executeCommand = vi.fn()
3+
const writeText = vi.fn()
4+
const showInformationMessage = vi.fn()
5+
const showErrorMessage = vi.fn()
6+
const file = vi.fn((p: string) => ({ fsPath: p, path: p, scheme: "file" }))
7+
const parse = (input: string) => {
8+
if (input.startsWith("https://") && input.includes("vscode-cdn.net")) {
9+
const url = new URL(input)
10+
return {
11+
scheme: "https",
12+
authority: url.host,
13+
path: url.pathname,
14+
fsPath: url.pathname,
15+
with: vi.fn(),
16+
}
17+
}
18+
if (input.startsWith("file://")) {
19+
return {
20+
scheme: "file",
21+
authority: "",
22+
path: input.substring("file://".length),
23+
fsPath: input.substring("file://".length),
24+
with: vi.fn(),
25+
}
26+
}
27+
return {
28+
scheme: "file",
29+
authority: "",
30+
path: input,
31+
fsPath: input,
32+
with: vi.fn(),
33+
}
34+
}
35+
return {
36+
commands: { executeCommand },
37+
env: { clipboard: { writeText } },
38+
window: { showInformationMessage, showErrorMessage },
39+
Uri: { file, parse },
40+
}
41+
})
42+
43+
import * as vscode from "vscode"
44+
import { openImage } from "../image-handler"
45+
46+
describe("openImage - vscode webview CDN url handling", () => {
47+
const cdnUrlPosix = "https://file+.vscode-resource.vscode-cdn.net/file//Users/test/workspace/image.png"
48+
49+
beforeEach(() => {
50+
vi.clearAllMocks()
51+
})
52+
53+
test("opens image from vscode-cdn webview URL by stripping /file/ and normalizing", async () => {
54+
await openImage(cdnUrlPosix)
55+
56+
// Should normalize to /Users/test/workspace/image.png and open it
57+
expect((vscode.Uri.file as any).mock.calls.length).toBe(1)
58+
const calledWithPath = (vscode.Uri.file as any).mock.calls[0][0]
59+
expect(calledWithPath).toBe(require("path").normalize("/Users/test/workspace/image.png"))
60+
61+
expect(vscode.commands.executeCommand).toHaveBeenCalledTimes(1)
62+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
63+
"vscode.open",
64+
expect.objectContaining({ fsPath: calledWithPath }),
65+
)
66+
expect(vscode.window.showErrorMessage).not.toHaveBeenCalled()
67+
})
68+
69+
test("copy action writes normalized fs path to clipboard (no open)", async () => {
70+
await openImage(cdnUrlPosix, { values: { action: "copy" } })
71+
72+
const expectedPath = require("path").normalize("/Users/test/workspace/image.png")
73+
expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith(expectedPath)
74+
expect(vscode.commands.executeCommand).not.toHaveBeenCalled()
75+
})
76+
})

src/integrations/misc/image-handler.ts

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,51 @@ import { getWorkspacePath } from "../../utils/path"
66
import { t } from "../../i18n"
77

88
export async function openImage(dataUriOrPath: string, options?: { values?: { action?: string } }) {
9-
// Check if it's a file path (absolute or relative)
9+
// Minimal handling for VS Code webview CDN URLs:
10+
// Example: https://file+.vscode-resource.vscode-cdn.net/file/<absolute_path_to_image>
11+
try {
12+
const u = vscode.Uri.parse(dataUriOrPath)
13+
if (u.scheme === "https" && u.authority.includes("vscode-cdn.net")) {
14+
let fsPath = decodeURIComponent(u.path || "")
15+
// Strip the leading "/file/" prefix if present
16+
if (fsPath.startsWith("/file/")) {
17+
fsPath = fsPath.slice("/file/".length)
18+
}
19+
20+
fsPath = path.normalize(fsPath)
21+
if (fsPath) {
22+
const fileUri = vscode.Uri.file(fsPath)
23+
await vscode.commands.executeCommand("vscode.open", fileUri)
24+
return
25+
}
26+
}
27+
} catch {
28+
// fall through
29+
}
30+
31+
// Handle file:// URIs directly
32+
if (dataUriOrPath.startsWith("file://")) {
33+
try {
34+
const fileUri = vscode.Uri.parse(dataUriOrPath)
35+
36+
await vscode.commands.executeCommand("vscode.open", fileUri)
37+
} catch (error) {
38+
vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error }))
39+
}
40+
return
41+
}
42+
43+
// Fallback: treat plain strings (absolute or relative) as file paths
1044
const isFilePath =
1145
!dataUriOrPath.startsWith("data:") &&
1246
!dataUriOrPath.startsWith("http:") &&
1347
!dataUriOrPath.startsWith("https:") &&
1448
!dataUriOrPath.startsWith("vscode-resource:") &&
15-
!dataUriOrPath.startsWith("file+.vscode-resource")
49+
!dataUriOrPath.startsWith("file+.vscode-resource") &&
50+
!dataUriOrPath.startsWith("vscode-webview-resource:")
1651

1752
if (isFilePath) {
18-
// Handle file path - open directly in VSCode
1953
try {
20-
// Resolve the path relative to workspace if needed
2154
let filePath = dataUriOrPath
2255
if (!path.isAbsolute(filePath)) {
2356
const workspacePath = getWorkspacePath()
@@ -28,62 +61,57 @@ export async function openImage(dataUriOrPath: string, options?: { values?: { ac
2861

2962
const fileUri = vscode.Uri.file(filePath)
3063

31-
// Check if this is a copy action
3264
if (options?.values?.action === "copy") {
3365
await vscode.env.clipboard.writeText(filePath)
3466
vscode.window.showInformationMessage(t("common:info.path_copied_to_clipboard"))
3567
return
3668
}
3769

38-
// Open the image file directly
3970
await vscode.commands.executeCommand("vscode.open", fileUri)
4071
} catch (error) {
4172
vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error }))
4273
}
4374
return
4475
}
4576

77+
// Finally, handle base64 data URIs explicitly
4678
const matches = dataUriOrPath.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
4779
if (!matches) {
48-
vscode.window.showErrorMessage(t("common:errors.invalid_data_uri"))
80+
// Do not show an "invalid data URI" error for non-data URIs; try opening as a generic URI
81+
try {
82+
const generic = vscode.Uri.parse(dataUriOrPath)
83+
await vscode.commands.executeCommand("vscode.open", generic)
84+
} catch {
85+
vscode.window.showErrorMessage(t("common:errors.invalid_data_uri"))
86+
}
4987
return
5088
}
89+
5190
const [, format, base64Data] = matches
5291
const imageBuffer = Buffer.from(base64Data, "base64")
5392

54-
// Default behavior: open the image
5593
const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`)
5694
try {
5795
await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer)
58-
// Check if this is a copy action
96+
5997
if (options?.values?.action === "copy") {
6098
try {
61-
// Read the image file
6299
const imageData = await vscode.workspace.fs.readFile(vscode.Uri.file(tempFilePath))
63-
64-
// Convert to base64 for clipboard
65100
const base64Image = Buffer.from(imageData).toString("base64")
66101
const dataUri = `data:image/${format};base64,${base64Image}`
67-
68-
// Use vscode.env.clipboard to copy the data URI
69-
// Note: VSCode doesn't support copying binary image data directly to clipboard
70-
// So we copy the data URI which can be pasted in many applications
71102
await vscode.env.clipboard.writeText(dataUri)
72-
73103
vscode.window.showInformationMessage(t("common:info.image_copied_to_clipboard"))
74104
} catch (error) {
75105
const errorMessage = error instanceof Error ? error.message : String(error)
76106
vscode.window.showErrorMessage(t("common:errors.error_copying_image", { errorMessage }))
77107
} finally {
78-
// Clean up temp file
79108
try {
80109
await vscode.workspace.fs.delete(vscode.Uri.file(tempFilePath))
81-
} catch {
82-
// Ignore cleanup errors
83-
}
110+
} catch {}
84111
}
85112
return
86113
}
114+
87115
await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath))
88116
} catch (error) {
89117
vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error }))

0 commit comments

Comments
 (0)