From 0d3df32140771ea495e4b3c92c0422f126da2f7f Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Wed, 30 Sep 2026 20:28:22 +0530 Subject: [PATCH] feat(quick-switcher): derive workspace dir from active tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The explorer panel's explorerCurrentDir is hardcoded to null, so the Quick Switcher's workspace toggle was a no-op. Rather than fix the broken explorer flow, derive the workspace from the active tab's filePath's parent directory — the most reliable source we have without restructuring that panel. deriveWorkspaceDir() handles POSIX and Windows paths (including mixed separators and trailing separators), returns null for untitled / empty inputs so the workspace toggle silently stays off in those cases. 10 new tests cover POSIX, Windows, mixed-separator, trailing separator, UNC, and invalid input paths. Amit Haridas --- src/quick-switcher/workspace-dir-resolver.js | 39 ++++++++++++++ src/renderer.js | 26 ++++++++-- tests/workspace-dir-resolver.test.js | 54 ++++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/quick-switcher/workspace-dir-resolver.js create mode 100644 tests/workspace-dir-resolver.test.js diff --git a/src/quick-switcher/workspace-dir-resolver.js b/src/quick-switcher/workspace-dir-resolver.js new file mode 100644 index 0000000..83a63d3 --- /dev/null +++ b/src/quick-switcher/workspace-dir-resolver.js @@ -0,0 +1,39 @@ +/** + * Workspace dir resolver for the Quick Switcher overlay. + * + * Derives a workspace directory from a single file path by returning its + * parent directory. Auto-detects path separator (Windows backslash vs + * POSIX forward slash). + * + * Returns null when no usable file path is supplied (untitled tab, no + * active tab) — the renderer then surfaces only recent + open tabs in + * the Quick Switcher, and the workspace toggle is a no-op. + * + * @module workspace-dir-resolver + */ + +/** + * @param {string|null|undefined} filePath + * @returns {string|null} parent directory, or null if not derivable + */ +function deriveWorkspaceDir(filePath) { + if (typeof filePath !== 'string' || filePath.length === 0) return null; + // Trim trailing separators (so a path like '/foo/bar/' resolves to '/foo', + // not '/foo/bar'). + let end = filePath.length; + while (end > 0 && (filePath[end - 1] === '/' || filePath[end - 1] === '\\')) { + end--; + } + if (end === 0) return null; + const trimmed = filePath.slice(0, end); + // Find the last separator of either kind — mixed paths are ambiguous + // but taking the last one of either kind matches what users mean when + // they paste a path with mixed separators. + const lastSlash = trimmed.lastIndexOf('/'); + const lastBack = trimmed.lastIndexOf('\\'); + const lastSep = Math.max(lastSlash, lastBack); + if (lastSep <= 0) return null; + return trimmed.slice(0, lastSep); +} + +module.exports = { deriveWorkspaceDir }; diff --git a/src/renderer.js b/src/renderer.js index 5e313a8..aa3b847 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -223,10 +223,15 @@ function getCreateQuickSwitcherOverlay() { let _createInlineAiController; function getCreateInlineAiController() { if (!_createInlineAiController) - _createInlineAiController = require('./renderer/inline-ai-controller') - .createInlineAiController; + _createInlineAiController = require('./renderer/inline-ai-controller').createInlineAiController; return _createInlineAiController; } +let _deriveWorkspaceDir; +function getDeriveWorkspaceDir() { + if (!_deriveWorkspaceDir) + _deriveWorkspaceDir = require('./quick-switcher/workspace-dir-resolver').deriveWorkspaceDir; + return _deriveWorkspaceDir; +} function getPrintPreview() { if (!_PrintPreview) _PrintPreview = require('./print-preview').PrintPreview; return _PrintPreview; @@ -2578,7 +2583,18 @@ document.addEventListener('DOMContentLoaded', async () => { // v4.13.0 — explorer-driven workspace dir is the next iteration; // for now the workspace toggle is a no-op and we surface only // recent files + open tabs. - getWorkspaceDir: () => null, + getWorkspaceDir: () => { + // Derive workspace dir from the active tab's file (parent dir). + // The explorer panel's currentDir is hardcoded to null, so this is + // the most reliable source we have without restructuring that + // panel. Returns null for untitled tabs, which makes the workspace + // search toggle a no-op (recent + open tabs still surface). + const derive = getDeriveWorkspaceDir(); + const active = + tabManager.activeTab || + (tabManager.activeTabId && tabManager.tabs.get(tabManager.activeTabId)); + return active ? derive(active.filePath) : null; + }, }); return quickSwitcherOverlayInstance; } @@ -2593,7 +2609,9 @@ document.addEventListener('DOMContentLoaded', async () => { const create = getCreateInlineAiController(); inlineAiControllerInstance = create({ getEditorView: () => { - const active = tabManager.activeTab || (tabManager.activeTabId && tabManager.tabs.get(tabManager.activeTabId)); + const active = + tabManager.activeTab || + (tabManager.activeTabId && tabManager.tabs.get(tabManager.activeTabId)); return active ? active.editorView : null; }, electronAPI: window.electronAPI, diff --git a/tests/workspace-dir-resolver.test.js b/tests/workspace-dir-resolver.test.js new file mode 100644 index 0000000..6070993 --- /dev/null +++ b/tests/workspace-dir-resolver.test.js @@ -0,0 +1,54 @@ +/** + * @jest-environment node + * + * Workspace dir resolver — derive parent dir from a file path. + */ + +const { deriveWorkspaceDir } = require('../src/quick-switcher/workspace-dir-resolver'); + +describe('deriveWorkspaceDir', () => { + test('returns parent dir for a POSIX path', () => { + expect(deriveWorkspaceDir('/home/user/notes/readme.md')).toBe('/home/user/notes'); + }); + + test('returns parent dir for a Windows path', () => { + expect(deriveWorkspaceDir('C:\\Users\\me\\notes\\readme.md')).toBe('C:\\Users\\me\\notes'); + }); + + test('returns parent dir for a nested POSIX path', () => { + expect(deriveWorkspaceDir('/a/b/c/d/e.md')).toBe('/a/b/c/d'); + }); + + test('auto-detects separator when path contains both', () => { + // A path with both — backslash takes precedence (Windows convention) + expect(deriveWorkspaceDir('C:\\foo/bar.txt')).toBe('C:\\foo'); + }); + + test('returns null for a root-level POSIX file', () => { + expect(deriveWorkspaceDir('readme.md')).toBeNull(); + }); + + test('returns null for an empty string', () => { + expect(deriveWorkspaceDir('')).toBeNull(); + }); + + test('returns null for null / undefined / non-string', () => { + expect(deriveWorkspaceDir(null)).toBeNull(); + expect(deriveWorkspaceDir(undefined)).toBeNull(); + expect(deriveWorkspaceDir(42)).toBeNull(); + expect(deriveWorkspaceDir({})).toBeNull(); + }); + + test('returns null for a single-character filename', () => { + expect(deriveWorkspaceDir('/x')).toBeNull(); + expect(deriveWorkspaceDir('\\x')).toBeNull(); + }); + + test('preserves trailing separators', () => { + expect(deriveWorkspaceDir('/home/user/notes/')).toBe('/home/user'); + }); + + test('handles UNC-style Windows path', () => { + expect(deriveWorkspaceDir('\\\\server\\share\\file.md')).toBe('\\\\server\\share'); + }); +});