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
51 changes: 51 additions & 0 deletions core/llm/rules/getSystemMessageWithRules.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,55 @@ describe("Content pattern matching", () => {
shouldApplyRule(nestedPatternRule, [utilFilePath], {}, utilContents),
).toBe(false);
});

// Regression for https://github.com/continuedev/continue/issues/13135
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: "docs/rules.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);
}
});

// Regression for https://github.com/continuedev/continue/issues/13135
it("should match exact globs whose filenames contain spaces", () => {
const exactRule: RuleWithSource = {
name: "Exact Docs Rule",
rule: "Only docs/foo bar.md",
globs: "docs/foo bar.md",
source: "rules-block",
sourceFile: "docs/rules.md",
};

const message: UserChatMessage = {
role: "user",
content: "What do you think?\n```docs/foo bar.md\n# Title\n```",
};

expect(getApplicableRules(message, [exactRule], [])).toHaveLength(1);
});
});
13 changes: 13 additions & 0 deletions core/llm/utils/extractPathsFromCodeBlocks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ describe("extractPathsFromCodeBlocks", () => {
expect(result.length).toBe(3);
});

// Regression for https://github.com/continuedev/continue/issues/13135
it("should extract paths whose filenames contain 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
65 changes: 48 additions & 17 deletions core/llm/utils/extractPathsFromCodeBlocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,55 @@ 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);

// Verify this is a legitimate path (not part of something else)
if (
path &&
// Check if valid extension
/\.[a-zA-Z0-9]+$/.test(path) &&
// Make sure it's not a URL
!path.includes("://") &&
// Avoid duplicates
!paths.includes(path)
) {
paths.push(path);
}
}
return paths;
}

/**
* Extracts the file path from a single code block opening line, e.g.
* "```typescript src/main.ts (1-10)" -> "src/main.ts".
*
* The opening line can take any of these shapes:
* ```<lang> <path>
* ```<path>
* ```<lang> <path> (<start>-<end>)
*
* The path itself may contain spaces, so we only use spaces to split the
* optional language tag from the path.
*/
function extractPathFromCodeBlockStart(blockStart: string): string | undefined {
let path = blockStart
.replace(/^`+/, "")
// Drop a trailing line range, e.g. " (1-10)"
.replace(/\s+\([\d-]+\)$/, "")
.trim();

if (!path) return undefined;

// A leading language tag (e.g. "typescript", "md") is a single token with
// no path separators. Anything containing "/" or "\" must be part of the
// path so that paths with spaces (e.g. "docs/foo bar.md") survive intact.
const firstSpaceIndex = path.search(/\s/);
if (firstSpaceIndex !== -1) {
const firstToken = path.slice(0, firstSpaceIndex);
if (!firstToken.includes("/") && !firstToken.includes("\\")) {
path = path.slice(firstSpaceIndex + 1).trim();
}
}

return path || undefined;
}
61 changes: 61 additions & 0 deletions core/llm/utils/extractPathsFromCodeBlocks.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import { extractPathsFromCodeBlocks } from "./extractPathsFromCodeBlocks";

describe("extractPathsFromCodeBlocks", () => {
it("should extract paths from code blocks with language and filepath", () => {
const content = "```typescript src/main.ts\nconst x = 1;\n```";
expect(extractPathsFromCodeBlocks(content)).toEqual(["src/main.ts"]);
});

it("should extract paths from code blocks with filepath only (no language)", () => {
const content = "```test.js\nclass Calculator { }\n```";
expect(extractPathsFromCodeBlocks(content)).toEqual(["test.js"]);
});

it("should extract paths from code blocks with line ranges", () => {
const content = "```js test.js (19-25)\nfunction test() {}\n```";
expect(extractPathsFromCodeBlocks(content)).toEqual(["test.js"]);
});

it("should handle multiple code blocks with different formats", () => {
const content =
"```typescript src/main.ts\nconst x = 1;\n```\n" +
"```test.js\nclass Calculator { }\n```\n" +
"```js utils.js (19-25)\nfunction test() {}\n```";
const result = extractPathsFromCodeBlocks(content);
expect(result).toContain("src/main.ts");
expect(result).toContain("test.js");
expect(result).toContain("utils.js");
expect(result.length).toBe(3);
});

it("should not extract paths from code blocks without file paths", () => {
const content = "```typescript\nconst x = 1;\n```";
expect(extractPathsFromCodeBlocks(content)).toEqual([]);
});

// Regression for https://github.com/continuedev/continue/issues/13135
it("should extract paths whose filenames contain spaces (no language)", () => {
expect(
extractPathsFromCodeBlocks("```docs/foo bar.md\n# Title\n```"),
).toEqual(["docs/foo bar.md"]);
});

it("should extract paths whose filenames contain spaces (with language)", () => {
expect(
extractPathsFromCodeBlocks("```md docs/foo bar.md\n# Title\n```"),
).toEqual(["docs/foo bar.md"]);
});

it("should extract paths whose filenames contain spaces (with language and range)", () => {
expect(
extractPathsFromCodeBlocks("```md docs/foo bar.md (1-3)\n# Title\n```"),
).toEqual(["docs/foo bar.md"]);
});

it("should extract paths with spaces in nested directory segments", () => {
expect(
extractPathsFromCodeBlocks("```ts src/my folder/file.ts\ncode\n```"),
).toEqual(["src/my folder/file.ts"]);
});
});
Loading