[ide-mode] Support multi-folder workspaces (#6177)

This commit is contained in:
christine betts
2025-08-14 20:12:57 +00:00
committed by GitHub
parent e06d774996
commit 5c5fc89eb1
7 changed files with 427 additions and 39 deletions

View File

@@ -6,6 +6,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { isSubpath } from './paths.js';
import { marked } from 'marked';
// Simple console logger for import processing
@@ -411,10 +412,7 @@ export function validateImportPath(
const resolvedPath = path.resolve(basePath, importPath);
return allowedDirectories.some((allowedDir) => {
const normalizedAllowedDir = path.resolve(allowedDir);
const isSamePath = resolvedPath === normalizedAllowedDir;
const isSubPath = resolvedPath.startsWith(normalizedAllowedDir + path.sep);
return isSamePath || isSubPath;
});
return allowedDirectories.some((allowedDir) =>
isSubpath(allowedDir, resolvedPath),
);
}

View File

@@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import { escapePath, unescapePath } from './paths.js';
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { escapePath, unescapePath, isSubpath } from './paths.js';
describe('escapePath', () => {
it('should escape spaces', () => {
@@ -212,3 +212,105 @@ describe('unescapePath', () => {
expect(unescapePath('file\\\\\\(test\\).txt')).toBe('file\\\\(test).txt');
});
});
describe('isSubpath', () => {
it('should return true for a direct subpath', () => {
expect(isSubpath('/a/b', '/a/b/c')).toBe(true);
});
it('should return true for the same path', () => {
expect(isSubpath('/a/b', '/a/b')).toBe(true);
});
it('should return false for a parent path', () => {
expect(isSubpath('/a/b/c', '/a/b')).toBe(false);
});
it('should return false for a completely different path', () => {
expect(isSubpath('/a/b', '/x/y')).toBe(false);
});
it('should handle relative paths', () => {
expect(isSubpath('a/b', 'a/b/c')).toBe(true);
expect(isSubpath('a/b', 'a/c')).toBe(false);
});
it('should handle paths with ..', () => {
expect(isSubpath('/a/b', '/a/b/../b/c')).toBe(true);
expect(isSubpath('/a/b', '/a/c/../b')).toBe(true);
});
it('should handle root paths', () => {
expect(isSubpath('/', '/a')).toBe(true);
expect(isSubpath('/a', '/')).toBe(false);
});
it('should handle trailing slashes', () => {
expect(isSubpath('/a/b/', '/a/b/c')).toBe(true);
expect(isSubpath('/a/b', '/a/b/c/')).toBe(true);
expect(isSubpath('/a/b/', '/a/b/c/')).toBe(true);
});
});
describe('isSubpath on Windows', () => {
const originalPlatform = process.platform;
beforeAll(() => {
Object.defineProperty(process, 'platform', {
value: 'win32',
});
});
afterAll(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
});
it('should return true for a direct subpath on Windows', () => {
expect(isSubpath('C:\\Users\\Test', 'C:\\Users\\Test\\file.txt')).toBe(
true,
);
});
it('should return true for the same path on Windows', () => {
expect(isSubpath('C:\\Users\\Test', 'C:\\Users\\Test')).toBe(true);
});
it('should return false for a parent path on Windows', () => {
expect(isSubpath('C:\\Users\\Test\\file.txt', 'C:\\Users\\Test')).toBe(
false,
);
});
it('should return false for a different drive on Windows', () => {
expect(isSubpath('C:\\Users\\Test', 'D:\\Users\\Test')).toBe(false);
});
it('should be case-insensitive for drive letters on Windows', () => {
expect(isSubpath('c:\\Users\\Test', 'C:\\Users\\Test\\file.txt')).toBe(
true,
);
});
it('should be case-insensitive for path components on Windows', () => {
expect(isSubpath('C:\\Users\\Test', 'c:\\users\\test\\file.txt')).toBe(
true,
);
});
it('should handle mixed slashes on Windows', () => {
expect(isSubpath('C:/Users/Test', 'C:\\Users\\Test\\file.txt')).toBe(true);
});
it('should handle trailing slashes on Windows', () => {
expect(isSubpath('C:\\Users\\Test\\', 'C:\\Users\\Test\\file.txt')).toBe(
true,
);
});
it('should handle relative paths correctly on Windows', () => {
expect(isSubpath('Users\\Test', 'Users\\Test\\file.txt')).toBe(true);
expect(isSubpath('Users\\Test\\file.txt', 'Users\\Test')).toBe(false);
});
});

View File

@@ -200,3 +200,23 @@ export function getUserCommandsDir(): string {
export function getProjectCommandsDir(projectRoot: string): string {
return path.join(projectRoot, GEMINI_DIR, COMMANDS_DIR_NAME);
}
/**
* Checks if a path is a subpath of another path.
* @param parentPath The parent path.
* @param childPath The child path.
* @returns True if childPath is a subpath of parentPath, false otherwise.
*/
export function isSubpath(parentPath: string, childPath: string): boolean {
const isWindows = os.platform() === 'win32';
const pathModule = isWindows ? path.win32 : path;
// On Windows, path.relative is case-insensitive. On POSIX, it's case-sensitive.
const relative = pathModule.relative(parentPath, childPath);
return (
!relative.startsWith(`..${pathModule.sep}`) &&
relative !== '..' &&
!pathModule.isAbsolute(relative)
);
}