mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
feat: Multi-Directory Workspace Support (part 3: configuration in settings.json) (#5354)
Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
@@ -188,6 +188,7 @@ export interface ConfigParameters {
|
||||
ideModeFeature?: boolean;
|
||||
ideMode?: boolean;
|
||||
ideClient: IdeClient;
|
||||
loadMemoryFromIncludeDirectories?: boolean;
|
||||
}
|
||||
|
||||
export class Config {
|
||||
@@ -247,6 +248,7 @@ export class Config {
|
||||
| Record<string, SummarizeToolOutputSettings>
|
||||
| undefined;
|
||||
private readonly experimentalAcp: boolean = false;
|
||||
private readonly loadMemoryFromIncludeDirectories: boolean = false;
|
||||
|
||||
constructor(params: ConfigParameters) {
|
||||
this.sessionId = params.sessionId;
|
||||
@@ -304,6 +306,8 @@ export class Config {
|
||||
this.ideModeFeature = params.ideModeFeature ?? false;
|
||||
this.ideMode = params.ideMode ?? false;
|
||||
this.ideClient = params.ideClient;
|
||||
this.loadMemoryFromIncludeDirectories =
|
||||
params.loadMemoryFromIncludeDirectories ?? false;
|
||||
|
||||
if (params.contextFileName) {
|
||||
setGeminiMdFilename(params.contextFileName);
|
||||
@@ -366,6 +370,10 @@ export class Config {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
shouldLoadMemoryFromIncludeDirectories(): boolean {
|
||||
return this.loadMemoryFromIncludeDirectories;
|
||||
}
|
||||
|
||||
getContentGeneratorConfig(): ContentGeneratorConfig {
|
||||
return this.contentGeneratorConfig;
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('loadServerHierarchicalMemory', () => {
|
||||
it('should return empty memory and count if no context files are found', async () => {
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
@@ -85,14 +86,13 @@ describe('loadServerHierarchicalMemory', () => {
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
default context content
|
||||
--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---\ndefault context content\n--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---`,
|
||||
fileCount: 1,
|
||||
});
|
||||
});
|
||||
@@ -108,14 +108,13 @@ default context content
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, customContextFile)} ---
|
||||
custom context content
|
||||
--- End of Context from: ${path.relative(cwd, customContextFile)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, customContextFile)} ---\ncustom context content\n--- End of Context from: ${path.relative(cwd, customContextFile)} ---`,
|
||||
fileCount: 1,
|
||||
});
|
||||
});
|
||||
@@ -135,18 +134,13 @@ custom context content
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, projectContextFile)} ---
|
||||
project context content
|
||||
--- End of Context from: ${path.relative(cwd, projectContextFile)} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, cwdContextFile)} ---
|
||||
cwd context content
|
||||
--- End of Context from: ${path.relative(cwd, cwdContextFile)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, projectContextFile)} ---\nproject context content\n--- End of Context from: ${path.relative(cwd, projectContextFile)} ---\n\n--- Context from: ${path.relative(cwd, cwdContextFile)} ---\ncwd context content\n--- End of Context from: ${path.relative(cwd, cwdContextFile)} ---`,
|
||||
fileCount: 2,
|
||||
});
|
||||
});
|
||||
@@ -163,18 +157,13 @@ cwd context content
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${customFilename} ---
|
||||
CWD custom memory
|
||||
--- End of Context from: ${customFilename} ---
|
||||
|
||||
--- Context from: ${path.join('subdir', customFilename)} ---
|
||||
Subdir custom memory
|
||||
--- End of Context from: ${path.join('subdir', customFilename)} ---`,
|
||||
memoryContent: `--- Context from: ${customFilename} ---\nCWD custom memory\n--- End of Context from: ${customFilename} ---\n\n--- Context from: ${path.join('subdir', customFilename)} ---\nSubdir custom memory\n--- End of Context from: ${path.join('subdir', customFilename)} ---`,
|
||||
fileCount: 2,
|
||||
});
|
||||
});
|
||||
@@ -191,18 +180,13 @@ Subdir custom memory
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
Project root memory
|
||||
--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, srcGeminiFile)} ---
|
||||
Src directory memory
|
||||
--- End of Context from: ${path.relative(cwd, srcGeminiFile)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\nProject root memory\n--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, srcGeminiFile)} ---\nSrc directory memory\n--- End of Context from: ${path.relative(cwd, srcGeminiFile)} ---`,
|
||||
fileCount: 2,
|
||||
});
|
||||
});
|
||||
@@ -219,18 +203,13 @@ Src directory memory
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---
|
||||
CWD memory
|
||||
--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---
|
||||
|
||||
--- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---
|
||||
Subdir memory
|
||||
--- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`,
|
||||
memoryContent: `--- Context from: ${DEFAULT_CONTEXT_FILENAME} ---\nCWD memory\n--- End of Context from: ${DEFAULT_CONTEXT_FILENAME} ---\n\n--- Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---\nSubdir memory\n--- End of Context from: ${path.join('subdir', DEFAULT_CONTEXT_FILENAME)} ---`,
|
||||
fileCount: 2,
|
||||
});
|
||||
});
|
||||
@@ -259,30 +238,13 @@ Subdir memory
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
default context content
|
||||
--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, rootGeminiFile)} ---
|
||||
Project parent memory
|
||||
--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
Project root memory
|
||||
--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---
|
||||
CWD memory
|
||||
--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---
|
||||
|
||||
--- Context from: ${path.relative(cwd, subDirGeminiFile)} ---
|
||||
Subdir memory
|
||||
--- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, defaultContextFile)} ---\ndefault context content\n--- End of Context from: ${path.relative(cwd, defaultContextFile)} ---\n\n--- Context from: ${path.relative(cwd, rootGeminiFile)} ---\nProject parent memory\n--- End of Context from: ${path.relative(cwd, rootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\nProject root memory\n--- End of Context from: ${path.relative(cwd, projectRootGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, cwdGeminiFile)} ---\nCWD memory\n--- End of Context from: ${path.relative(cwd, cwdGeminiFile)} ---\n\n--- Context from: ${path.relative(cwd, subDirGeminiFile)} ---\nSubdir memory\n--- End of Context from: ${path.relative(cwd, subDirGeminiFile)} ---`,
|
||||
fileCount: 5,
|
||||
});
|
||||
});
|
||||
@@ -302,6 +264,7 @@ Subdir memory
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
@@ -314,9 +277,7 @@ Subdir memory
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---
|
||||
My code memory
|
||||
--- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---\nMy code memory\n--- End of Context from: ${path.relative(cwd, regularSubDirGeminiFile)} ---`,
|
||||
fileCount: 1,
|
||||
});
|
||||
});
|
||||
@@ -333,6 +294,7 @@ My code memory
|
||||
// Pass the custom limit directly to the function
|
||||
await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
true,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[],
|
||||
@@ -353,6 +315,7 @@ My code memory
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
@@ -371,15 +334,36 @@ My code memory
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
[extensionFilePath],
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, extensionFilePath)} ---
|
||||
Extension memory content
|
||||
--- End of Context from: ${path.relative(cwd, extensionFilePath)} ---`,
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, extensionFilePath)} ---\nExtension memory content\n--- End of Context from: ${path.relative(cwd, extensionFilePath)} ---`,
|
||||
fileCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should load memory from included directories', async () => {
|
||||
const includedDir = await createEmptyDir(
|
||||
path.join(testRootDir, 'included'),
|
||||
);
|
||||
const includedFile = await createTestFile(
|
||||
path.join(includedDir, DEFAULT_CONTEXT_FILENAME),
|
||||
'included directory memory',
|
||||
);
|
||||
|
||||
const result = await loadServerHierarchicalMemory(
|
||||
cwd,
|
||||
[includedDir],
|
||||
false,
|
||||
new FileDiscoveryService(projectRoot),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
memoryContent: `--- Context from: ${path.relative(cwd, includedFile)} ---\nincluded directory memory\n--- End of Context from: ${path.relative(cwd, includedFile)} ---`,
|
||||
fileCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -83,6 +83,36 @@ async function findProjectRoot(startDir: string): Promise<string | null> {
|
||||
|
||||
async function getGeminiMdFilePathsInternal(
|
||||
currentWorkingDirectory: string,
|
||||
includeDirectoriesToReadGemini: readonly string[],
|
||||
userHomePath: string,
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
fileFilteringOptions: FileFilteringOptions,
|
||||
maxDirs: number,
|
||||
): Promise<string[]> {
|
||||
const dirs = new Set<string>([
|
||||
...includeDirectoriesToReadGemini,
|
||||
currentWorkingDirectory,
|
||||
]);
|
||||
const paths = [];
|
||||
for (const dir of dirs) {
|
||||
const pathsByDir = await getGeminiMdFilePathsInternalForEachDir(
|
||||
dir,
|
||||
userHomePath,
|
||||
debugMode,
|
||||
fileService,
|
||||
extensionContextFilePaths,
|
||||
fileFilteringOptions,
|
||||
maxDirs,
|
||||
);
|
||||
paths.push(...pathsByDir);
|
||||
}
|
||||
return Array.from(new Set<string>(paths));
|
||||
}
|
||||
|
||||
async function getGeminiMdFilePathsInternalForEachDir(
|
||||
dir: string,
|
||||
userHomePath: string,
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
@@ -115,8 +145,8 @@ async function getGeminiMdFilePathsInternal(
|
||||
|
||||
// FIX: Only perform the workspace search (upward and downward scans)
|
||||
// if a valid currentWorkingDirectory is provided.
|
||||
if (currentWorkingDirectory) {
|
||||
const resolvedCwd = path.resolve(currentWorkingDirectory);
|
||||
if (dir) {
|
||||
const resolvedCwd = path.resolve(dir);
|
||||
if (debugMode)
|
||||
logger.debug(
|
||||
`Searching for ${geminiMdFilename} starting from CWD: ${resolvedCwd}`,
|
||||
@@ -257,6 +287,7 @@ function concatenateInstructions(
|
||||
*/
|
||||
export async function loadServerHierarchicalMemory(
|
||||
currentWorkingDirectory: string,
|
||||
includeDirectoriesToReadGemini: readonly string[],
|
||||
debugMode: boolean,
|
||||
fileService: FileDiscoveryService,
|
||||
extensionContextFilePaths: string[] = [],
|
||||
@@ -274,6 +305,7 @@ export async function loadServerHierarchicalMemory(
|
||||
const userHomePath = homedir();
|
||||
const filePaths = await getGeminiMdFilePathsInternal(
|
||||
currentWorkingDirectory,
|
||||
includeDirectoriesToReadGemini,
|
||||
userHomePath,
|
||||
debugMode,
|
||||
fileService,
|
||||
@@ -282,7 +314,8 @@ export async function loadServerHierarchicalMemory(
|
||||
maxDirs,
|
||||
);
|
||||
if (filePaths.length === 0) {
|
||||
if (debugMode) logger.debug('No GEMINI.md files found in hierarchy.');
|
||||
if (debugMode)
|
||||
logger.debug('No GEMINI.md files found in hierarchy of the workspace.');
|
||||
return { memoryContent: '', fileCount: 0 };
|
||||
}
|
||||
const contentsWithPaths = await readGeminiMdFiles(
|
||||
|
||||
@@ -15,6 +15,8 @@ import * as path from 'path';
|
||||
export class WorkspaceContext {
|
||||
private directories: Set<string>;
|
||||
|
||||
private initialDirectories: Set<string>;
|
||||
|
||||
/**
|
||||
* Creates a new WorkspaceContext with the given initial directory and optional additional directories.
|
||||
* @param initialDirectory The initial working directory (usually cwd)
|
||||
@@ -22,11 +24,14 @@ export class WorkspaceContext {
|
||||
*/
|
||||
constructor(initialDirectory: string, additionalDirectories: string[] = []) {
|
||||
this.directories = new Set<string>();
|
||||
this.initialDirectories = new Set<string>();
|
||||
|
||||
this.addDirectoryInternal(initialDirectory);
|
||||
this.addInitialDirectoryInternal(initialDirectory);
|
||||
|
||||
for (const dir of additionalDirectories) {
|
||||
this.addDirectoryInternal(dir);
|
||||
this.addInitialDirectoryInternal(dir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +74,33 @@ export class WorkspaceContext {
|
||||
this.directories.add(realPath);
|
||||
}
|
||||
|
||||
private addInitialDirectoryInternal(
|
||||
directory: string,
|
||||
basePath: string = process.cwd(),
|
||||
): void {
|
||||
const absolutePath = path.isAbsolute(directory)
|
||||
? directory
|
||||
: path.resolve(basePath, directory);
|
||||
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
throw new Error(`Directory does not exist: ${absolutePath}`);
|
||||
}
|
||||
|
||||
const stats = fs.statSync(absolutePath);
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`Path is not a directory: ${absolutePath}`);
|
||||
}
|
||||
|
||||
let realPath: string;
|
||||
try {
|
||||
realPath = fs.realpathSync(absolutePath);
|
||||
} catch (_error) {
|
||||
throw new Error(`Failed to resolve path: ${absolutePath}`);
|
||||
}
|
||||
|
||||
this.initialDirectories.add(realPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a copy of all workspace directories.
|
||||
* @returns Array of absolute directory paths
|
||||
@@ -77,6 +109,17 @@ export class WorkspaceContext {
|
||||
return Array.from(this.directories);
|
||||
}
|
||||
|
||||
getInitialDirectories(): readonly string[] {
|
||||
return Array.from(this.initialDirectories);
|
||||
}
|
||||
|
||||
setDirectories(directories: readonly string[]): void {
|
||||
this.directories.clear();
|
||||
for (const dir of directories) {
|
||||
this.addDirectoryInternal(dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given path is within any of the workspace directories.
|
||||
* @param pathToCheck The path to validate
|
||||
|
||||
Reference in New Issue
Block a user