From addd1bc6e757cdd7a713b90033307548c003af24 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 23 Sep 2025 14:53:09 -0500 Subject: [PATCH] feat: enhance openImage function to handle vscode webview CDN URLs --- .../misc/__tests__/image-handler.spec.ts | 76 +++++++++++++++++++ src/integrations/misc/image-handler.ts | 70 ++++++++++++----- 2 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 src/integrations/misc/__tests__/image-handler.spec.ts diff --git a/src/integrations/misc/__tests__/image-handler.spec.ts b/src/integrations/misc/__tests__/image-handler.spec.ts new file mode 100644 index 0000000000..d5871773f3 --- /dev/null +++ b/src/integrations/misc/__tests__/image-handler.spec.ts @@ -0,0 +1,76 @@ +vi.mock("vscode", () => { + const executeCommand = vi.fn() + const writeText = vi.fn() + const showInformationMessage = vi.fn() + const showErrorMessage = vi.fn() + const file = vi.fn((p: string) => ({ fsPath: p, path: p, scheme: "file" })) + const parse = (input: string) => { + if (input.startsWith("https://") && input.includes("vscode-cdn.net")) { + const url = new URL(input) + return { + scheme: "https", + authority: url.host, + path: url.pathname, + fsPath: url.pathname, + with: vi.fn(), + } + } + if (input.startsWith("file://")) { + return { + scheme: "file", + authority: "", + path: input.substring("file://".length), + fsPath: input.substring("file://".length), + with: vi.fn(), + } + } + return { + scheme: "file", + authority: "", + path: input, + fsPath: input, + with: vi.fn(), + } + } + return { + commands: { executeCommand }, + env: { clipboard: { writeText } }, + window: { showInformationMessage, showErrorMessage }, + Uri: { file, parse }, + } +}) + +import * as vscode from "vscode" +import { openImage } from "../image-handler" + +describe("openImage - vscode webview CDN url handling", () => { + const cdnUrlPosix = "https://file+.vscode-resource.vscode-cdn.net/file//Users/test/workspace/image.png" + + beforeEach(() => { + vi.clearAllMocks() + }) + + test("opens image from vscode-cdn webview URL by stripping /file/ and normalizing", async () => { + await openImage(cdnUrlPosix) + + // Should normalize to /Users/test/workspace/image.png and open it + expect((vscode.Uri.file as any).mock.calls.length).toBe(1) + const calledWithPath = (vscode.Uri.file as any).mock.calls[0][0] + expect(calledWithPath).toBe(require("path").normalize("/Users/test/workspace/image.png")) + + expect(vscode.commands.executeCommand).toHaveBeenCalledTimes(1) + expect(vscode.commands.executeCommand).toHaveBeenCalledWith( + "vscode.open", + expect.objectContaining({ fsPath: calledWithPath }), + ) + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + + test("copy action writes normalized fs path to clipboard (no open)", async () => { + await openImage(cdnUrlPosix, { values: { action: "copy" } }) + + const expectedPath = require("path").normalize("/Users/test/workspace/image.png") + expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith(expectedPath) + expect(vscode.commands.executeCommand).not.toHaveBeenCalled() + }) +}) diff --git a/src/integrations/misc/image-handler.ts b/src/integrations/misc/image-handler.ts index 24e18e5d57..e0af2b19e6 100644 --- a/src/integrations/misc/image-handler.ts +++ b/src/integrations/misc/image-handler.ts @@ -6,18 +6,51 @@ import { getWorkspacePath } from "../../utils/path" import { t } from "../../i18n" export async function openImage(dataUriOrPath: string, options?: { values?: { action?: string } }) { - // Check if it's a file path (absolute or relative) + // Minimal handling for VS Code webview CDN URLs: + // Example: https://file+.vscode-resource.vscode-cdn.net/file/ + try { + const u = vscode.Uri.parse(dataUriOrPath) + if (u.scheme === "https" && u.authority.includes("vscode-cdn.net")) { + let fsPath = decodeURIComponent(u.path || "") + // Strip the leading "/file/" prefix if present + if (fsPath.startsWith("/file/")) { + fsPath = fsPath.slice("/file/".length) + } + + fsPath = path.normalize(fsPath) + if (fsPath) { + const fileUri = vscode.Uri.file(fsPath) + await vscode.commands.executeCommand("vscode.open", fileUri) + return + } + } + } catch { + // fall through + } + + // Handle file:// URIs directly + if (dataUriOrPath.startsWith("file://")) { + try { + const fileUri = vscode.Uri.parse(dataUriOrPath) + + await vscode.commands.executeCommand("vscode.open", fileUri) + } catch (error) { + vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error })) + } + return + } + + // Fallback: treat plain strings (absolute or relative) as file paths const isFilePath = !dataUriOrPath.startsWith("data:") && !dataUriOrPath.startsWith("http:") && !dataUriOrPath.startsWith("https:") && !dataUriOrPath.startsWith("vscode-resource:") && - !dataUriOrPath.startsWith("file+.vscode-resource") + !dataUriOrPath.startsWith("file+.vscode-resource") && + !dataUriOrPath.startsWith("vscode-webview-resource:") if (isFilePath) { - // Handle file path - open directly in VSCode try { - // Resolve the path relative to workspace if needed let filePath = dataUriOrPath if (!path.isAbsolute(filePath)) { const workspacePath = getWorkspacePath() @@ -28,14 +61,12 @@ export async function openImage(dataUriOrPath: string, options?: { values?: { ac const fileUri = vscode.Uri.file(filePath) - // Check if this is a copy action if (options?.values?.action === "copy") { await vscode.env.clipboard.writeText(filePath) vscode.window.showInformationMessage(t("common:info.path_copied_to_clipboard")) return } - // Open the image file directly await vscode.commands.executeCommand("vscode.open", fileUri) } catch (error) { vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error })) @@ -43,47 +74,44 @@ export async function openImage(dataUriOrPath: string, options?: { values?: { ac return } + // Finally, handle base64 data URIs explicitly const matches = dataUriOrPath.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/) if (!matches) { - vscode.window.showErrorMessage(t("common:errors.invalid_data_uri")) + // Do not show an "invalid data URI" error for non-data URIs; try opening as a generic URI + try { + const generic = vscode.Uri.parse(dataUriOrPath) + await vscode.commands.executeCommand("vscode.open", generic) + } catch { + vscode.window.showErrorMessage(t("common:errors.invalid_data_uri")) + } return } + const [, format, base64Data] = matches const imageBuffer = Buffer.from(base64Data, "base64") - // Default behavior: open the image const tempFilePath = path.join(os.tmpdir(), `temp_image_${Date.now()}.${format}`) try { await vscode.workspace.fs.writeFile(vscode.Uri.file(tempFilePath), imageBuffer) - // Check if this is a copy action + if (options?.values?.action === "copy") { try { - // Read the image file const imageData = await vscode.workspace.fs.readFile(vscode.Uri.file(tempFilePath)) - - // Convert to base64 for clipboard const base64Image = Buffer.from(imageData).toString("base64") const dataUri = `data:image/${format};base64,${base64Image}` - - // Use vscode.env.clipboard to copy the data URI - // Note: VSCode doesn't support copying binary image data directly to clipboard - // So we copy the data URI which can be pasted in many applications await vscode.env.clipboard.writeText(dataUri) - vscode.window.showInformationMessage(t("common:info.image_copied_to_clipboard")) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) vscode.window.showErrorMessage(t("common:errors.error_copying_image", { errorMessage })) } finally { - // Clean up temp file try { await vscode.workspace.fs.delete(vscode.Uri.file(tempFilePath)) - } catch { - // Ignore cleanup errors - } + } catch {} } return } + await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(tempFilePath)) } catch (error) { vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error }))