Skip to content
Merged
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
50 changes: 43 additions & 7 deletions src/providers/MessageRouter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,45 @@ describe("MessageRouter", () => {
);
});

it("opens workspace file at requested line and column from openFile message", async () => {
await router.handleMessage({
type: "openFile",
path: "src/providers/MessageRouter.ts",
line: 120,
column: 5,
});

expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
expect.objectContaining({
fsPath: "/workspace/src/providers/MessageRouter.ts",
}),
{
selection: {
start: { line: 119, character: 4 },
end: { line: 119, character: 4 },
},
preview: true,
},
);
});

it("rejects invalid openFile payloads without host side effects", async () => {
await router.handleMessage({ type: "openFile", path: 123 });
await router.handleMessage({
type: "openFile",
path: "src/providers/MessageRouter.ts",
line: 0,
});
await router.handleMessage({
type: "openFile",
path: "src/providers/MessageRouter.ts",
column: -1,
});

expect(vscode.window.showTextDocument).not.toHaveBeenCalled();
expect(vscode.workspace.findFiles).not.toHaveBeenCalled();
});

it("reports open file failures when fuzzy matching cannot recover", async () => {
vi.mocked(vscode.workspace.findFiles).mockResolvedValue([]);
vi.mocked(vscode.window.showTextDocument).mockRejectedValue(
Expand Down Expand Up @@ -961,19 +1000,16 @@ describe("MessageRouter", () => {
});

it("opens file URI and absolute paths and reports outer path failures", async () => {
vi.mocked(vscode.Uri.parse).mockImplementationOnce(() => {
throw new Error("parse failed");
});
await router.handleOpenFile("safe-file.ts");
await router.handleOpenFile("https://example.com/safe-file.ts");
expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
"Failed to open file: safe-file.ts",
"Invalid file path: Only file URIs can be opened",
);

await router.handleOpenFile("file:///workspace/absolute.ts", 1, undefined, 1);
await router.handleOpenFile("C:\\workspace\\absolute.ts");

expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
expect.objectContaining({ fsPath: "file:///workspace/absolute.ts" }),
expect.objectContaining({ fsPath: "/workspace/absolute.ts" }),
expect.objectContaining({ preview: true }),
);
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
Expand Down Expand Up @@ -1100,7 +1136,7 @@ describe("MessageRouter", () => {
vscode.workspace.workspaceFolders = originalFolders;

expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
expect.objectContaining({ fsPath: "file:///workspace/from-uri.ts" }),
expect.objectContaining({ fsPath: "/workspace/from-uri.ts" }),
expect.objectContaining({ preview: true }),
);
expect(vscode.window.showTextDocument).toHaveBeenCalledWith(
Expand Down
130 changes: 12 additions & 118 deletions src/providers/MessageRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import type {
TmuxRawSubcommand,
TmuxWebviewCommandId,
} from "../types";
import { isWindowsAbsolutePath } from "../utils/pathUtils";
import {
createSelection,
fuzzyMatchFile,
openFileInEditor,
} from "./openFile";

export interface MessageRouterProviderBridge {
startOpenCode(): Promise<void>;
Expand Down Expand Up @@ -363,63 +367,9 @@ export class MessageRouter {
endLine?: number,
column?: number,
): Promise<void> {
if (
filePath.includes("..") ||
filePath.includes("\0") ||
filePath.includes("~")
) {
void vscode.window.showErrorMessage(
"Invalid file path: Path traversal detected",
);
return;
}

try {
const normalizedPath = filePath.replace(/\\/g, "/");

let uri: vscode.Uri;

if (vscode.Uri.parse(filePath).scheme === "file") {
uri = vscode.Uri.file(filePath);
} else if (
normalizedPath.startsWith("/") ||
isWindowsAbsolutePath(filePath)
) {
uri = vscode.Uri.file(filePath);
} else {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length > 0) {
uri = vscode.Uri.joinPath(workspaceFolders[0].uri, normalizedPath);
} else {
uri = vscode.Uri.file(normalizedPath);
}
}

try {
const selection = this.createSelection(line, endLine, column);

await vscode.window.showTextDocument(uri, {
selection,
preview: true,
});
} catch {
const matchedUri = await this.fuzzyMatchFile(normalizedPath);
if (matchedUri) {
const selection = this.createSelection(line, endLine, column);

await vscode.window.showTextDocument(matchedUri, {
selection,
preview: true,
});
} else {
void vscode.window.showErrorMessage(
`Failed to open file: ${filePath}`,
);
}
}
} catch {
void vscode.window.showErrorMessage(`Failed to open file: ${filePath}`);
}
await openFileInEditor(filePath, line, endLine, column, (message) => {
this.logger.error(message);
});
}

public async handleFilesDropped(
Expand Down Expand Up @@ -751,68 +701,12 @@ export class MessageRouter {
endLine?: number,
column?: number,
): vscode.Range | undefined {
if (!line) {
return undefined;
}

const maxColumn = 9999;
return new vscode.Range(
Math.max(0, line - 1),
Math.max(0, (column || 1) - 1),
Math.max(0, (endLine || line) - 1),
endLine ? maxColumn : Math.max(0, (column || 1) - 1),
);
return createSelection(line, endLine, column);
}

public async fuzzyMatchFile(filePath: string): Promise<vscode.Uri | null> {
try {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
return null;
}

const pathParts = filePath
.split(/[\\/]/)
.filter((part) => part.length > 0);
const filename = pathParts[pathParts.length - 1];

const pattern = `**/${filename}*`;
const files = await vscode.workspace.findFiles(pattern, null, 100);

const normalizedInput = filePath.replace(/\\/g, "/").toLowerCase();
files.sort((a, b) => {
const aPath = a.fsPath.replace(/\\/g, "/").toLowerCase();
const bPath = b.fsPath.replace(/\\/g, "/").toLowerCase();

if (aPath.endsWith(normalizedInput)) {
return -1;
}
if (bPath.endsWith(normalizedInput)) {
return 1;
}

const aDirParts = a.fsPath.split(/[\\/]/);
const bDirParts = b.fsPath.split(/[\\/]/);

for (let i = 0; i < pathParts.length - 1; i++) {
const expectedPart = pathParts[i].toLowerCase();
if (aDirParts[i] && aDirParts[i].toLowerCase() === expectedPart) {
return -1;
}
if (bDirParts[i] && bDirParts[i].toLowerCase() === expectedPart) {
return 1;
}
}

return 0;
});

return files[0] || null;
} catch (error) {
this.logger.error(
`Fuzzy match failed: ${error instanceof Error ? error.message : String(error)}`,
);
return null;
}
return fuzzyMatchFile(filePath, (message) => {
this.logger.error(message);
});
}
}
Loading
Loading