[clinerules] search clinerule in parent folders which make it easier to share within a github repo (#1832)

* [clinerules] search clinerule in parent folders which make it easier to share common clinerules for a git repo

* fix indent

* Fix test

---------

Co-authored-by: Ying Liu <lying@microsoft.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
ying 2025-03-25 02:18:30 +08:00 committed by GitHub
parent fdcc679f9f
commit 2953baef97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 83 additions and 9 deletions

View file

@ -106,7 +106,15 @@ describe("addCustomInstructions", () => {
})
it("should combine all instruction types when provided", async () => {
mockedFs.readFile.mockResolvedValue("mode specific rules")
// Mock implementation to return different values based on the file path
mockedFs.readFile.mockImplementation(((filePath: any) => {
// For .clinerules-test-mode, return mode-specific rules
if (filePath.toString().includes(".clinerules-test-mode")) {
return Promise.resolve("mode specific rules")
}
// For any other read operation, return empty
return Promise.reject({ code: "ENOENT" })
}) as any)
const result = await addCustomInstructions(
"mode instructions",

View file

@ -16,12 +16,30 @@ async function safeReadFile(filePath: string): Promise<string> {
}
}
async function findRuleInDirectory(dir: string, ruleFile: string): Promise<string> {
const filePath = path.join(dir, ruleFile)
const content = await safeReadFile(filePath)
if (content) {
return content
}
// Check if we've reached the root directory
const parentDir = path.dirname(dir)
if (parentDir === dir) {
return ""
}
// Recursively check parent directory
return findRuleInDirectory(parentDir, ruleFile)
}
export async function loadRuleFiles(cwd: string): Promise<string> {
const ruleFiles = [".clinerules", ".cursorrules", ".windsurfrules"]
let combinedRules = ""
for (const file of ruleFiles) {
const content = await safeReadFile(path.join(cwd, file))
const content = await findRuleInDirectory(cwd, file)
if (content) {
combinedRules += `\n# Rules from ${file}:\n${content}\n`
}
@ -30,6 +48,17 @@ export async function loadRuleFiles(cwd: string): Promise<string> {
return combinedRules
}
async function findCustomInstructionsFile(dir: string, filePattern: string): Promise<string> {
// First try to find as a direct file
const content = await findRuleInDirectory(dir, filePattern)
if (content) {
return content
}
// If not found as a file, check if it's raw content
return filePattern.trim()
}
export async function addCustomInstructions(
modeCustomInstructions: string,
globalCustomInstructions: string,
@ -54,14 +83,16 @@ export async function addCustomInstructions(
)
}
// Add global instructions first
if (typeof globalCustomInstructions === "string" && globalCustomInstructions.trim()) {
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
// Add global instructions first - try to find as file or use raw content
const globalContent = await findCustomInstructionsFile(cwd, globalCustomInstructions)
if (globalContent) {
sections.push(`Global Instructions:\n${globalContent}`)
}
// Add mode-specific instructions after
if (typeof modeCustomInstructions === "string" && modeCustomInstructions.trim()) {
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`)
// Add mode-specific instructions - try to find as file or use raw content
const modeContent = await findCustomInstructionsFile(cwd, modeCustomInstructions)
if (modeContent) {
sections.push(`Mode-specific Instructions:\n${modeContent}`)
}
// Add rules - include both mode-specific and generic rules if they exist

View file

@ -23,6 +23,23 @@ export async function openImage(dataUri: string) {
interface OpenFileOptions {
create?: boolean
content?: string
searchParents?: boolean
startFromWorkspace?: boolean
}
async function findFileInParentDirs(searchPath: string, fileName: string): Promise<string | null> {
try {
const fullPath = path.join(searchPath, fileName)
await vscode.workspace.fs.stat(vscode.Uri.file(fullPath))
return fullPath
} catch {
const parentDir = path.dirname(searchPath)
if (parentDir === searchPath) {
// Hit root
return null
}
return findFileInParentDirs(parentDir, fileName)
}
}
export async function openFile(filePath: string, options: OpenFileOptions = {}) {
@ -34,7 +51,17 @@ export async function openFile(filePath: string, options: OpenFileOptions = {})
}
// If path starts with ./, resolve it relative to workspace root
const fullPath = filePath.startsWith("./") ? path.join(workspaceRoot, filePath.slice(2)) : filePath
let fullPath = filePath.startsWith("./") ? path.join(workspaceRoot, filePath.slice(2)) : filePath
// Handle recursive search
if (options.searchParents) {
const startDir = options.startFromWorkspace ? workspaceRoot : path.dirname(fullPath)
const fileName = path.basename(filePath)
const foundPath = await findFileInParentDirs(startDir, fileName)
if (foundPath) {
fullPath = foundPath
}
}
const uri = vscode.Uri.file(fullPath)

View file

@ -462,6 +462,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
values: {
create: true,
content: JSON.stringify({ customModes: [] }, null, 2),
searchParents: true,
startFromWorkspace: true,
},
})
setShowConfigMenu(false)
@ -808,6 +810,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
values: {
create: true,
content: "",
searchParents: true,
startFromWorkspace: true,
},
})
}}
@ -915,6 +919,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
values: {
create: true,
content: "",
searchParents: true,
startFromWorkspace: true,
},
})
}}
@ -970,6 +976,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
values: {
create: true,
content: "",
searchParents: true,
startFromWorkspace: true,
},
})
}