feat(quick-switcher): derive workspace dir from active tab

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
This commit is contained in:
Amit Haridas
2026-09-30 20:28:22 +05:30
parent b36d0919a3
commit 0d3df32140
3 changed files with 115 additions and 4 deletions
@@ -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 };
+22 -4
View File
@@ -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,
+54
View File
@@ -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');
});
});