Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions core/llm/rules/getSystemMessageWithRules.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 "```<language> <path> (<range>)", 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);
}
});
});
12 changes: 12 additions & 0 deletions core/llm/utils/extractPathsFromCodeBlocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
Expand Down
53 changes: 36 additions & 17 deletions core/llm/utils/extractPathsFromCodeBlocks.ts
Original file line number Diff line number Diff line change
@@ -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
*/
Expand All @@ -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;
Expand Down
Loading