diff --git a/core/llm/rules/getSystemMessageWithRules.vitest.ts b/core/llm/rules/getSystemMessageWithRules.vitest.ts index 104ceecf8ca..c99533b18cf 100644 --- a/core/llm/rules/getSystemMessageWithRules.vitest.ts +++ b/core/llm/rules/getSystemMessageWithRules.vitest.ts @@ -476,4 +476,36 @@ describe("Content pattern matching", () => { shouldApplyRule(nestedPatternRule, [utilFilePath], {}, utilContents), ).toBe(false); }); + + it("should apply rules to files whose names contain spaces", () => { + const docsRule: RuleWithSource = { + name: "Docs Rule", + rule: "Write docs in the active voice", + globs: "docs/**/*.md", + source: "rules-block", + sourceFile: "/path/to/repo/.continue/rules/docs.md", + }; + + // Code block headers are "``` ()", where the + // language and range are both optional + const messages: UserChatMessage[] = [ + { + role: "user", + content: "What do you think?\n```docs/foo bar.md\n# Title\n```", + }, + { + role: "user", + content: "What do you think?\n```md docs/foo bar.md\n# Title\n```", + }, + { + role: "user", + content: + "What do you think?\n```md docs/foo bar.md (1-1)\n# Title\n```", + }, + ]; + + for (const message of messages) { + expect(getApplicableRules(message, [docsRule], [])).toHaveLength(1); + } + }); }); diff --git a/core/llm/utils/extractPathsFromCodeBlocks.test.ts b/core/llm/utils/extractPathsFromCodeBlocks.test.ts index b318348dc9e..3c120559a14 100644 --- a/core/llm/utils/extractPathsFromCodeBlocks.test.ts +++ b/core/llm/utils/extractPathsFromCodeBlocks.test.ts @@ -28,6 +28,18 @@ describe("extractPathsFromCodeBlocks", () => { expect(result.length).toBe(3); }); + it("should extract paths containing spaces", () => { + expect( + extractPathsFromCodeBlocks("```docs/foo bar.md\n# Title\n```"), + ).toEqual(["docs/foo bar.md"]); + expect( + extractPathsFromCodeBlocks("```md docs/foo bar.md\n# Title\n```"), + ).toEqual(["docs/foo bar.md"]); + expect( + extractPathsFromCodeBlocks("```md docs/foo bar.md (1-3)\n# Title\n```"), + ).toEqual(["docs/foo bar.md"]); + }); + it("should not extract paths from code blocks without file paths", () => { const content = "```typescript\nconst x = 1;\n```"; expect(extractPathsFromCodeBlocks(content)).toEqual([]); diff --git a/core/llm/utils/extractPathsFromCodeBlocks.ts b/core/llm/utils/extractPathsFromCodeBlocks.ts index 99f2764b21d..8fb4bc08e56 100644 --- a/core/llm/utils/extractPathsFromCodeBlocks.ts +++ b/core/llm/utils/extractPathsFromCodeBlocks.ts @@ -1,3 +1,34 @@ +/** + * Extracts the file path from a single code block opening line, e.g. + * "```typescript src/main.ts (1-10)" -> "src/main.ts" + */ +function extractPathFromCodeBlockStart(blockStart: string): string | undefined { + let path = blockStart + .replace(/^`+/, "") + // Drop a trailing line range, e.g. " (1-10)" + .replace(/\s+\([\d-]+\)$/, "") + .trim(); + + // A leading language tag can only be told apart from the path itself when it + // has no path characters, e.g. "```md docs/my file.md" but not "```my file.md" + const firstSpaceIndex = path.search(/\s/); + if (firstSpaceIndex !== -1) { + const firstToken = path.slice(0, firstSpaceIndex); + if (!/[./\\]/.test(firstToken)) { + path = path.slice(firstSpaceIndex + 1).trim(); + } + } + + const isValidPath = + // Check if valid extension + /\.[a-zA-Z0-9]+$/.test(path) && + // Make sure it's not a URL + !path.includes("://") && + !path.includes("`"); + + return isValidPath ? path : undefined; +} + /** * Extracts file paths from markdown code blocks */ @@ -13,23 +44,11 @@ export function extractPathsFromCodeBlocks(content: string): string[] { const codeBlockStarts = content.match(/```[^\n]+/g) || []; for (const blockStart of codeBlockStarts) { - // Try to extract a valid filename with extension - const filenameMatches = blockStart.match(/([^\s()```]+\.[a-zA-Z0-9]+)/); - - if (filenameMatches && filenameMatches[1]) { - const filename = filenameMatches[1]; - - // Verify this is a legitimate filename (not part of something else) - if ( - // Check if valid extension - /\.[a-zA-Z0-9]+$/.test(filename) && - // Make sure it's not a URL - !filename.includes("://") && - // Avoid duplicates - !paths.includes(filename) - ) { - paths.push(filename); - } + const path = extractPathFromCodeBlockStart(blockStart); + + // Avoid duplicates + if (path && !paths.includes(path)) { + paths.push(path); } } return paths;