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
19 changes: 0 additions & 19 deletions src/controllers/previewController.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* Unit tests for preview controller
*/
import { jest } from '@jest/globals';
import * as vscode from 'vscode';
import {
Expand All @@ -12,22 +9,6 @@ import {
openInPreview,
} from './previewController';

// Create the mock object before using it
jest.mock(
'vscode',
() => ({
workspace: {
getConfiguration: jest.fn(() => ({
get: jest.fn(),
})),
},
commands: {
executeCommand: jest.fn(),
},
}),
{ virtual: true },
);

describe('previewController', () => {
const mockVscode = require('vscode') as any;

Expand Down
113 changes: 61 additions & 52 deletions src/controllers/previewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,74 +4,85 @@
import * as vscode from 'vscode';

// Track last click times for double-click detection
/** Map of file paths to their last click timestamps */
const lastClickTimes: Map<string, number> = new Map();
const DOUBLE_CLICK_THRESHOLD = 500; // milliseconds
/** Time threshold (in milliseconds) to consider two clicks as a double-click */
const DOUBLE_CLICK_THRESHOLD = 500;

/** Default commands per extension when configuration is missing or invalid */
const DEFAULT_OPEN_WITH: Record<string, string> = {
md: 'markdown.showPreview',
markdown: 'markdown.showPreview',
txt: 'vscode.open',
};

/**
* Opens a file in preview mode
* Gets the file extension from a URI
* @param uri The URI to extract the file extension from
* @returns The file extension in lowercase, or undefined if none exists
*/
export function openInPreview(uri: vscode.Uri): void {
function getFileExtension(uri: vscode.Uri): string | undefined {
const parts = uri.fsPath.split('.');
if (parts.length < 2) {
return undefined;
}

return parts.pop()?.toLowerCase();
}

/**
* Retrieves the 'openWith' configuration from settings
* @returns A mapping of file extensions to commands
*/
function getOpenWithConfig(): Record<string, string> {
const config = vscode.workspace.getConfiguration('workspaceWiki');
const openWith = (config.get('openWith') as Record<string, string>) || {
md: 'markdown.showPreview',
markdown: 'markdown.showPreview',
txt: 'vscode.open',
};

const fileExt = uri.fsPath.split('.').pop()?.toLowerCase();
const command = fileExt && openWith[fileExt] ? openWith[fileExt] : 'vscode.open';
const userValue = config.get('openWith') as unknown;

if (userValue && typeof userValue === 'object' && !Array.isArray(userValue)) {
const entries = Object.entries(userValue);

if (entries.every(([k, v]) => typeof k === 'string' && typeof v === 'string')) {
return userValue as Record<string, string>;
}
}

return DEFAULT_OPEN_WITH;
}

/**
* Opens a file in preview mode
* @param uri The URI of the file to open
*/
export function openInPreview(uri: vscode.Uri): void {
const command = getOpenCommand(uri, 'preview');
vscode.commands.executeCommand(command, uri);
}

/**
* Opens a file in editor mode
* @param uri The URI of the file to open
*/
export function openInEditor(uri: vscode.Uri): void {
vscode.commands.executeCommand('vscode.open', uri);
}

/**
* Handles file clicks with double-click detection
* @param uri The URI of the file that was clicked
* @param defaultCommand The default command to execute on single click
*/
export function handleFileClick(uri: vscode.Uri, defaultCommand: string): void {
// Handle case when vscode is not available (e.g., in tests)
if (typeof vscode === 'undefined' || !vscode.commands) {
// In test environment, use the global mock
const globalVscode = (global as any).vscode;
if (globalVscode?.commands?.executeCommand) {
const now = Date.now();
const path = uri.fsPath;
const lastClick = lastClickTimes.get(path) || 0;

if (now - lastClick < DOUBLE_CLICK_THRESHOLD) {
// Double-click detected - open in editor
globalVscode.commands.executeCommand('vscode.open', uri);
lastClickTimes.delete(path); // Clear to prevent triple-click issues
} else {
// Single click - execute default command
globalVscode.commands.executeCommand(defaultCommand, uri);
lastClickTimes.set(path, now);

// Clear old entries to prevent memory leaks
setTimeout(() => {
if (lastClickTimes.get(path) === now) {
lastClickTimes.delete(path);
}
}, DOUBLE_CLICK_THRESHOLD + 100);
}
}
return;
}

const now = Date.now();
const path = uri.fsPath;
const lastClick = lastClickTimes.get(path) || 0;

if (now - lastClick < DOUBLE_CLICK_THRESHOLD) {
// Double-click detected - open in editor
openInEditor(uri);
lastClickTimes.delete(path); // Clear to prevent triple-click issues

// Clear to prevent triple-click issues
lastClickTimes.delete(path);
} else {
// Single click - execute default command (preview)
vscode.commands.executeCommand(defaultCommand, uri);
Expand All @@ -88,32 +99,30 @@ export function handleFileClick(uri: vscode.Uri, defaultCommand: string): void {

/**
* Gets the appropriate command for opening a file based on extension and settings
*
* @param uri The URI of the file
* @param mode The mode to open the file in ('preview' or 'editor')
* @returns The command string to execute
*/
export function getOpenCommand(uri: vscode.Uri, mode: 'preview' | 'editor' = 'preview'): string {
if (mode === 'editor') {
return 'vscode.open';
}

const config = vscode.workspace.getConfiguration('workspaceWiki');
const openWith = (config.get('openWith') as Record<string, string>) || {
md: 'markdown.showPreview',
markdown: 'markdown.showPreview',
txt: 'vscode.open',
};

const fileExt = uri.fsPath.split('.').pop()?.toLowerCase();
return fileExt && openWith[fileExt] ? openWith[fileExt] : 'vscode.open';
const openWith = getOpenWithConfig();
const fileExt = getFileExtension(uri);

return fileExt && fileExt in openWith ? openWith[fileExt] : 'vscode.open';
}

/**
* Clears all stored click times (useful for testing)
*/
/** Clears all stored click times (useful for testing) */
export function clearClickTimes(): void {
lastClickTimes.clear();
}

/**
* Gets the double click threshold value
* @returns The double click threshold in milliseconds
*/
export function getDoubleClickThreshold(): number {
return DOUBLE_CLICK_THRESHOLD;
Expand Down