Skip to content

feat: prefix folder path for colocated rules #6394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 10, 2025
Merged
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
14 changes: 8 additions & 6 deletions packages/config-yaml/src/markdown/createMarkdownRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { markdownToRule } from "./markdownToRule.js";
// Mock package identifier for testing
const mockPackageId: PackageIdentifier = {
uriType: "file",
filePath: "/path/to/file",
filePath: "/path/to/folder",
};

describe("sanitizeRuleName", () => {
Expand Down Expand Up @@ -85,7 +85,9 @@ Just markdown content.`;

expect(parsed.name).toBe(originalFrontmatter.name);
expect(parsed.description).toBe(originalFrontmatter.description);
expect(parsed.globs).toEqual(originalFrontmatter.globs);
expect(parsed.globs).toEqual(
originalFrontmatter.globs.map((glob) => `/path/to/**/${glob}`),
);
expect(parsed.alwaysApply).toBe(originalFrontmatter.alwaysApply);
expect(parsed.rule).toBe(originalMarkdown);
});
Expand All @@ -102,7 +104,7 @@ describe("createRuleMarkdown", () => {
const parsed = markdownToRule(result, mockPackageId);

expect(parsed.description).toBe("Test description");
expect(parsed.globs).toEqual(["*.ts", "*.js"]);
expect(parsed.globs).toEqual(["/path/to/**/*.ts", "/path/to/**/*.js"]);
expect(parsed.alwaysApply).toBe(true);
expect(parsed.rule).toBe("This is the rule content");
});
Expand All @@ -113,7 +115,7 @@ describe("createRuleMarkdown", () => {
const parsed = markdownToRule(result, mockPackageId);

expect(parsed.description).toBeUndefined();
expect(parsed.globs).toBeUndefined();
expect(parsed.globs).toBe("/path/to/**/*");
expect(parsed.alwaysApply).toBeUndefined();
expect(parsed.rule).toBe("Simple content");
});
Expand All @@ -124,7 +126,7 @@ describe("createRuleMarkdown", () => {
});

const parsed = markdownToRule(result, mockPackageId);
expect(parsed.globs).toBe("*.py");
expect(parsed.globs).toBe("/path/to/**/*.py");
});

it("should trim description and globs", () => {
Expand All @@ -135,7 +137,7 @@ describe("createRuleMarkdown", () => {

const parsed = markdownToRule(result, mockPackageId);
expect(parsed.description).toBe("spaced description");
expect(parsed.globs).toBe("*.ts");
expect(parsed.globs).toBe("/path/to/**/*.ts");
});

it("should handle alwaysApply false explicitly", () => {
Expand Down
105 changes: 98 additions & 7 deletions packages/config-yaml/src/markdown/markdownToRule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.rule).toBe("# Test Rule\n\nThis is a test rule.");
expect(result.globs).toBe("**/test/**/*.kt");
expect(result.globs).toBe("/path/to/**/test/**/*.kt");
expect(result.name).toBe("Custom Name");
});

Expand All @@ -34,7 +34,7 @@ globs: "**/test/**/*.kt"
This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBe("**/test/**/*.kt");
expect(result.globs).toBe("/path/to/**/test/**/*.kt");
expect(result.rule).toBe("# Test Rule\n\nThis is a test rule.");
expect(result.name).toBe("to/file"); // Should use last two path segments
});
Expand All @@ -45,7 +45,7 @@ This is a test rule.`;
This is a test rule without frontmatter.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBeUndefined();
expect(result.globs).toBe("/path/to/**/*");
expect(result.rule).toBe(content);
expect(result.name).toBe("to/file"); // Should use last two path segments
});
Expand All @@ -59,13 +59,104 @@ This is a test rule without frontmatter.`;
This is a test rule with empty frontmatter.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBeUndefined();
expect(result.globs).toBe("/path/to/**/*");
expect(result.rule).toBe(
"# Test Rule\n\nThis is a test rule with empty frontmatter.",
);
expect(result.name).toBe("to/file"); // Should use last two path segments
});

describe("glob patterns", () => {
// we want to ensure the following glob changes happen
// *.ts -> path/to/**/*.ts
// **/subdir/** -> path/to/**/**/subdir/** OR path/to/**/subdir/**
// myfile -> path/to/**/myfile (any depth including 0)
// mydir/ -> path/to/**/mydir/**
// **abc** -> path/to/**abc**
// *xyz* -> path/to/**/*xyz*

it("should match glob pattern for file extensions", () => {
const content = `---
globs: "*.ts"
name: glob pattern testing
---

# Test Rule

This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBe("/path/to/**/*.ts");
});

it("should match glob pattern for dotfiles", () => {
const content = `---
globs: ".gitignore"
name: glob pattern testing
---

# Test Rule

This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBe("/path/to/**/.gitignore");
});

it("should also work in root as base directory", () => {
const content = `---
globs: "src/**/Dockerfile"
name: glob pattern testing
---

# Test Rule

This is a test rule.`;

const result = markdownToRule(content, {
uriType: "file",
filePath: "/",
});
expect(result.globs).toBe("/**/src/**/Dockerfile");
});

it("should match for multiple globs", () => {
const content = `---
globs: ["**/nested/**/deeper/**/*.rs", ".zshrc1", "**abc**", "*xyz*"]
name: glob pattern testing
---

# Test Rule

This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toEqual([
"/path/to/**/nested/**/deeper/**/*.rs",
"/path/to/**/.zshrc1",
"/path/to/**abc**",
"/path/to/**/*xyz*",
]);
});

it("should not prepend when inside .continue", () => {
const content = `---
globs: ".git"
name: glob pattern testing
---

# Test Rule

This is a test rule.`;

const result = markdownToRule(content, {
uriType: "file",
filePath: "/Documents/myproject/.continue/rules/rule1.md",
});
expect(result.globs).toBe(".git");
});
});

it("should handle frontmatter with whitespace", () => {
const content = `---
globs: "**/test/**/*.kt"
Expand All @@ -76,7 +167,7 @@ globs: "**/test/**/*.kt"
This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBe("**/test/**/*.kt");
expect(result.globs).toBe("/path/to/**/test/**/*.kt");
expect(result.rule).toBe("# Test Rule\n\nThis is a test rule.");
expect(result.name).toBe("to/file"); // Should use last two path segments
});
Expand All @@ -92,7 +183,7 @@ globs: "**/test/**/*.kt"\r
This is a test rule.`;

const result = markdownToRule(content, mockId);
expect(result.globs).toBe("**/test/**/*.kt");
expect(result.globs).toBe("/path/to/**/test/**/*.kt");
// The result should be normalized to \n
expect(result.rule).toBe("# Test Rule\n\nThis is a test rule.");
expect(result.name).toBe("to/file"); // Should use last two path segments
Expand All @@ -110,7 +201,7 @@ This is a test rule.`;

// Should treat as only markdown when frontmatter is malformed
const result = markdownToRule(content, mockId);
expect(result.globs).toBeUndefined();
expect(result.globs).toBe("/path/to/**/*");
expect(result.rule).toBe(content);
expect(result.name).toBe("to/file"); // Should use last two path segments
});
Expand Down
32 changes: 31 additions & 1 deletion packages/config-yaml/src/markdown/markdownToRule.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from "path";
import * as YAML from "yaml";
import {
PackageIdentifier,
Expand Down Expand Up @@ -69,6 +70,35 @@ export function getRuleName(
return displayName;
}

function getGlobPattern(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't prepend the directory if the rule is inside of .continue/rules

globs: RuleFrontmatter["globs"],
id: PackageIdentifier,
) {
if (id.uriType !== "file") {
return globs;
}
let dir = path.dirname(id.filePath);
if (dir.includes(".continue")) {
return globs;
}
if (!dir.endsWith("/")) {
dir = dir.concat("/");
}
const prependDirAndApplyGlobstar = (glob: string) => {
if (glob.startsWith("**")) {
return dir.concat(glob);
}
return dir.concat("**/", glob);
};
if (!globs) {
return dir.concat("**/*");
}
if (Array.isArray(globs)) {
return globs.map(prependDirAndApplyGlobstar);
}
return prependDirAndApplyGlobstar(globs);
}

export function markdownToRule(
rule: string,
id: PackageIdentifier,
Expand All @@ -78,7 +108,7 @@ export function markdownToRule(
return {
name: getRuleName(frontmatter, id),
rule: markdown,
globs: frontmatter.globs,
globs: getGlobPattern(frontmatter.globs, id),
regex: frontmatter.regex,
description: frontmatter.description,
alwaysApply: frontmatter.alwaysApply,
Expand Down
Loading