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
2 changes: 2 additions & 0 deletions packages/rangelink-vscode-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **R-F (paste current file path) now works for any active tab that maps to a file** — image previews, notebooks, and diff views — not just text editors. (#643)

## [2.0.0]

### Added
Expand Down
16 changes: 16 additions & 0 deletions packages/rangelink-vscode-extension/qa/qa-test-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,22 @@ test_cases:
expected_result: 'File path inserted at cursor. Status bar "File path sent to Text Editor".'
automated: true

- id: send-file-path-017
labels:
- clipboard
feature: 'send-file-path'
scenario: 'R-F sends file path when active tab is an image preview'
expected_result: 'Terminal receives workspace-relative path to the image file. No "No active file" error toast.'
automated: true

- id: send-file-path-018
labels:
- clipboard
feature: 'send-file-path'
scenario: 'R-F sends the modified side path when active tab is a text diff view'
expected_result: 'Terminal receives workspace-relative path to the right (modified) file in the diff, not the left (original).'
automated: true

# ---------------------------------------------------------------------------
# Section 7 — Context Menu Integrations
# ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,42 @@ import { getWorkspaceRoot, settle } from './testEnv';

let fileCounter = 0;

let fileCleanupRegistry: vscode.Uri[] = [];

const registerFileForCleanup = (uri: vscode.Uri): void => {
fileCleanupRegistry.push(uri);
};

export const cleanupTrackedFiles = (): void => {
cleanupFiles(fileCleanupRegistry);
fileCleanupRegistry = [];
};

const ensureParentDir = (filePath: string): void => {
const dir = path.dirname(filePath);
if (dir !== getWorkspaceRoot()) {
fs.mkdirSync(dir, { recursive: true });
}
};

export const createWorkspaceFile = (descriptor: string, content: string): vscode.Uri => {
fileCounter++;
const filePath = path.join(
getWorkspaceRoot(),
`__rl-test-${descriptor}-${Date.now()}-${fileCounter}.txt`,
);
fs.writeFileSync(filePath, content, 'utf8');
return vscode.Uri.file(filePath);
const uri = vscode.Uri.file(filePath);
registerFileForCleanup(uri);
return uri;
};

export const createAndOpenFile = async (
descriptor: string,
content: string,
viewColumn?: vscode.ViewColumn,
trackingArray?: vscode.Uri[],
): Promise<vscode.Uri> => {
const uri = createWorkspaceFile(descriptor, content);
trackingArray?.push(uri);
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc, {
viewColumn: viewColumn ?? vscode.ViewColumn.One,
Expand All @@ -47,8 +65,40 @@ export const findTestItemsByPrefix = (

export const createFileAt = (filename: string, content: string): vscode.Uri => {
const filePath = path.join(getWorkspaceRoot(), filename);
ensureParentDir(filePath);
fs.writeFileSync(filePath, content, 'utf8');
return vscode.Uri.file(filePath);
const uri = vscode.Uri.file(filePath);
registerFileForCleanup(uri);
return uri;
};

const PNG_MAGIC_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

export type PngFixtureMode = 'real-image' | 'magic-only';

export const createPngFixture = (
descriptor: string,
mode: PngFixtureMode = 'real-image',
): vscode.Uri => {
fileCounter++;
const pngPath = path.join(
getWorkspaceRoot(),
`__rl-test-${descriptor}-${Date.now()}-${fileCounter}.png`,
);
if (mode === 'real-image') {
const extension = vscode.extensions.getExtension('couimet.rangelink-vscode-extension');
if (!extension) {
throw new Error(
'createPngFixture(real-image) requires the RangeLink extension to be registered',
);
}
fs.copyFileSync(path.join(extension.extensionPath, 'icon.png'), pngPath);
} else {
fs.writeFileSync(pngPath, PNG_MAGIC_BYTES);
}
const uri = vscode.Uri.file(pngPath);
registerFileForCleanup(uri);
return uri;
};

export const openEditor = async (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ import type { LogCapture } from '../../LogCapture';

import type { CapturingTerminal } from './capturingPtyHelpers';
import { createAndBindCapturingTerminal, createCapturingTerminal } from './capturingPtyHelpers';
import { cleanupFiles, createAndOpenFile, createWorkspaceFile, openEditor } from './fileHelpers';
import {
cleanupTrackedFiles,
createAndOpenFile,
createFileAt,
createPngFixture,
createWorkspaceFile,
openEditor,
type PngFixtureMode,
} from './fileHelpers';
import { getLogCapture } from './getLogCapture';
import { SETTLE_MS, TERMINAL_READY_MS, waitForExtensionActive } from './testEnv';
import {
Expand All @@ -31,7 +39,25 @@ export interface SsContext {
lineCount: number,
lineFactory: (index: number) => string,
) => { uri: vscode.Uri; filename: string };
/**
* Create a tracked workspace file with a generated filename
* (`__rl-test-<descriptor>-<timestamp>-<counter>.txt`).
*/
createWorkspaceFile: (descriptor: string, content: string) => vscode.Uri;
/**
* Create a tracked workspace file with an exact filename.
* Prefer `createWorkspaceFile` when the descriptor-based naming is sufficient;
* use this when the test needs a specific filename (spaces, parentheses, etc.).
*/
createTrackedFile: (filename: string, content: string) => vscode.Uri;
/**
* Create a tracked `.png` fixture in the workspace root. Auto-tracked for
* teardown. `'real-image'` (default) copies the extension's `icon.png` so
* the file opens in VS Code's image preview as a real custom editor;
* `'magic-only'` writes just the 8-byte PNG signature for tests that only
* need a binary classification.
*/
createPngFixture: (descriptor: string, mode?: PngFixtureMode) => vscode.Uri;
createAndOpenFile: (
descriptor: string,
content: string,
Expand Down Expand Up @@ -72,12 +98,10 @@ export interface SsContext {
expectContextKeys: (keys: Record<string, unknown>) => void;
openEditor: (uri: vscode.Uri, viewColumn?: vscode.ViewColumn) => Promise<vscode.TextEditor>;
waitForExtensionActive: (extensionId: string, timeoutMs?: number) => Promise<void>;
trackFileUri: (uri: vscode.Uri) => void;
clearDummyAi: () => Promise<void>;
}

export class SsContextImpl implements SsContext {
private tmpFileUris: vscode.Uri[] = [];
private tmpTerminals: vscode.Terminal[] = [];
private suiteLog: (msg: string) => void;
private expectedStatusBarMessages: string[] = [];
Expand All @@ -92,11 +116,8 @@ export class SsContextImpl implements SsContext {
// calls disposeAllTerminals() + CMD_UNBIND_DESTINATION before beginTest().
// Disposing here would fire onDidCloseTerminal during the observation window
// and leak status bar messages into verify().
// File deletion is safe here: fs.unlinkSync does not trigger editor-close events.
// Only editor close (closeAllEditors, in setup) can fire onDidCloseTextDocument.
this.tmpTerminals.splice(0);
cleanupFiles(this.tmpFileUris);
this.tmpFileUris.splice(0);
cleanupTrackedFiles();
await this.settle();
});
}
Expand Down Expand Up @@ -135,23 +156,27 @@ export class SsContextImpl implements SsContext {
): { uri: vscode.Uri; filename: string } {
const lines = Array.from({ length: lineCount }, (_, i) => lineFactory(i));
const uri = createWorkspaceFile(descriptor, lines.join('\n') + '\n');
this.tmpFileUris.push(uri);
return { uri, filename: path.basename(uri.fsPath) };
}

createWorkspaceFile(descriptor: string, content: string): vscode.Uri {
const uri = createWorkspaceFile(descriptor, content);
this.tmpFileUris.push(uri);
return uri;
return createWorkspaceFile(descriptor, content);
}

createTrackedFile(filename: string, content: string): vscode.Uri {
return createFileAt(filename, content);
}

createPngFixture(descriptor: string, mode: PngFixtureMode = 'real-image'): vscode.Uri {
return createPngFixture(descriptor, mode);
}

async createAndOpenFile(
descriptor: string,
content: string,
viewColumn?: vscode.ViewColumn,
): Promise<vscode.Uri> {
const uri = await createAndOpenFile(descriptor, content, viewColumn, this.tmpFileUris);
return uri;
return createAndOpenFile(descriptor, content, viewColumn);
}

async settle(ms?: number): Promise<void> {
Expand Down Expand Up @@ -201,10 +226,6 @@ export class SsContextImpl implements SsContext {
await waitForExtensionActive(extensionId, this.suiteLog, timeoutMs);
}

trackFileUri(uri: vscode.Uri): void {
this.tmpFileUris.push(uri);
}

async clearDummyAi(): Promise<void> {
await vscode.commands.executeCommand('dummyAi.clearAll');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,6 @@ standardSuite('Context Menus — Editor Content', (ss) => {
const ANCHOR_END = 'ANCHOR_END';
const destUri = ss.createWorkspaceFile('rvec-014-dest', `${ANCHOR_START}\n${ANCHOR_END}\n`);
const sourceUri = ss.createWorkspaceFile('rvec-014-source', `${ANCHOR_START}\n`);
ss.trackFileUri(sourceUri);
const destBasename = path.basename(destUri.fsPath);

const destDoc = await vscode.workspace.openTextDocument(destUri);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import assert from 'node:assert';
import * as fs from 'node:fs';
import * as path from 'node:path';

import * as vscode from 'vscode';
Expand All @@ -9,7 +8,6 @@ import {
extractQuickPickItemsLogged,
findTestItemsByPrefix,
getLogCapture,
getWorkspaceRoot,
openAndDismiss,
standardSuite,
waitForHumanVerdict,
Expand Down Expand Up @@ -80,12 +78,7 @@ standardSuite('Editor Binding Validation', (ss) => {
});

test('editor-binding-validation-004: binary .png file is excluded from R-D destination picker', async () => {
// Minimal PNG magic bytes — enough for VSCode to detect as binary
const PNG_MAGIC = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const pngPath = path.join(getWorkspaceRoot(), `__rl-test-ebv-004-${Date.now()}.png`);
fs.writeFileSync(pngPath, PNG_MAGIC);
const pngUri = vscode.Uri.file(pngPath);
ss.trackFileUri(pngUri);
const pngUri = ss.createPngFixture('ebv-004', 'magic-only');

const txtUri = ss.createWorkspaceFile('ebv-004-txt', 'control file\n');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import assert from 'node:assert';
import * as fs from 'node:fs';
import * as path from 'node:path';

import * as vscode from 'vscode';
Expand All @@ -10,11 +9,11 @@ import {
CMD_OPEN_STATUS_BAR_MENU,
} from '../../constants/commandIds';
import {
createFileAt,
extractQuickPickItemsLogged,
findTestItemsByPrefix,
getLogCapture,
getQuickPickLines,
getWorkspaceRoot,
openAndDismiss,
parseQuickPickItemsFromLogLine,
standardSuite,
Expand Down Expand Up @@ -123,21 +122,8 @@ standardSuite('File Picker', (ss) => {
});

test('file-picker-003: same base name shows path disambiguation', async () => {
const wsRoot = getWorkspaceRoot();
const subDirA = path.join(wsRoot, 'src', 'dirA');
const subDirB = path.join(wsRoot, 'src', 'dirB');
fs.mkdirSync(subDirA, { recursive: true });
fs.mkdirSync(subDirB, { recursive: true });

const fileA = path.join(subDirA, '__rl-test-fp-003-shared.ts');
const fileB = path.join(subDirB, '__rl-test-fp-003-shared.ts');
fs.writeFileSync(fileA, 'file A\n', 'utf8');
fs.writeFileSync(fileB, 'file B\n', 'utf8');
const uriA = vscode.Uri.file(fileA);
const uriB = vscode.Uri.file(fileB);

ss.trackFileUri(uriA);
ss.trackFileUri(uriB);
const uriA = createFileAt('src/dirA/__rl-test-fp-003-shared.ts', 'file A\n');
const uriB = createFileAt('src/dirB/__rl-test-fp-003-shared.ts', 'file B\n');

await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(uriA), {
viewColumn: vscode.ViewColumn.One,
Expand Down Expand Up @@ -441,22 +427,8 @@ standardSuite('File Picker', (ss) => {
});

test('[assisted] file-picker-010: secondary picker shows path disambiguation for same-name files', async () => {
const wsRoot = getWorkspaceRoot();
const subDirA = path.join(wsRoot, 'src', 'fp010A');
const subDirB = path.join(wsRoot, 'src', 'fp010B');
fs.mkdirSync(subDirA, { recursive: true });
fs.mkdirSync(subDirB, { recursive: true });

const sharedName = '__rl-test-fp-010-shared.ts';
const fileA = path.join(subDirA, sharedName);
const fileB = path.join(subDirB, sharedName);
fs.writeFileSync(fileA, 'file A\n', 'utf8');
fs.writeFileSync(fileB, 'file B\n', 'utf8');
const uriA = vscode.Uri.file(fileA);
const uriB = vscode.Uri.file(fileB);

ss.trackFileUri(uriA);
ss.trackFileUri(uriB);
const uriA = createFileAt('src/fp010A/__rl-test-fp-010-shared.ts', 'file A\n');
const uriB = createFileAt('src/fp010B/__rl-test-fp-010-shared.ts', 'file B\n');

await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(uriA), {
viewColumn: vscode.ViewColumn.One,
Expand Down Expand Up @@ -508,26 +480,11 @@ standardSuite('File Picker', (ss) => {
});

test('file-picker-011: three files with same name show deeper disambiguation paths', async () => {
const wsRoot = getWorkspaceRoot();
const dirA = path.join(wsRoot, 'src', 'a', 'nested');
const dirB = path.join(wsRoot, 'src', 'b', 'nested');
const dirC = path.join(wsRoot, 'src', 'c');
fs.mkdirSync(dirA, { recursive: true });
fs.mkdirSync(dirB, { recursive: true });
fs.mkdirSync(dirC, { recursive: true });

const sharedName = '__rl-test-fp-011-shared.ts';
const files = [
path.join(dirA, sharedName),
path.join(dirB, sharedName),
path.join(dirC, sharedName),
const uris = [
createFileAt('src/a/nested/__rl-test-fp-011-shared.ts', 'content\n'),
createFileAt('src/b/nested/__rl-test-fp-011-shared.ts', 'content\n'),
createFileAt('src/c/__rl-test-fp-011-shared.ts', 'content\n'),
];
const uris = files.map((f) => {
fs.writeFileSync(f, 'content\n', 'utf8');
const uri = vscode.Uri.file(f);
ss.trackFileUri(uri);
return uri;
});

await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(uris[0]), {
viewColumn: vscode.ViewColumn.One,
Expand Down
Loading