Skip to content
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
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"binary/build/**": true,
"binary/out/**": true,
"binary/tmp/**": true,
"core/dist/**": true,
"core/edit/lazy/test-examples/**": true,
"core/llm/llamaTokenizer.js": true,
"core/llm/llamaTokenizer.mjs": true,
Expand Down
2 changes: 1 addition & 1 deletion core/commands/slash/customSlashCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export function convertCustomCommandToSlashCommand(
description: customCommand.description ?? "",
prompt: customCommand.prompt,
source: "json-custom-command",
promptFile: customCommand.sourceFile, // TODO refactor promptFile to align with sourceFile
sourceFile: customCommand.sourceFile,
};
}
2 changes: 1 addition & 1 deletion core/commands/slash/promptBlockSlashCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ export function convertPromptBlockToSlashCommand(
description: prompt.description ?? "",
prompt: prompt.prompt,
source: "yaml-prompt-block",
promptFile: prompt.sourceFile, // Align with the sourceFile property
sourceFile: prompt.sourceFile,
};
}
2 changes: 1 addition & 1 deletion core/commands/slash/promptFileSlashCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function slashCommandFromPromptFile(
description,
prompt,
source: version === 1 ? "prompt-file-v1" : "prompt-file-v2",
promptFile: path,
sourceFile: path,
overrideSystemMessage: systemMessage,
};
}
3 changes: 2 additions & 1 deletion core/commands/slash/ruleBlockSlashCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export function convertRuleBlockToSlashCommand(
description: rule.description ?? "",
prompt: rule.rule,
source: "invokable-rule",
promptFile: rule.ruleFile,
sourceFile: rule.sourceFile,
slug: rule.slug,
};
}
4 changes: 2 additions & 2 deletions core/commands/slash/ruleBlockSlashCommand.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe("convertRuleBlockToSlashCommand", () => {
rule: "Review this code for best practices",
description: "Performs a code review",
source: "rules-block",
ruleFile: "/path/to/rules.yaml",
sourceFile: "file:///path/to/rules.yaml",
invokable: true,
};

Expand All @@ -20,7 +20,7 @@ describe("convertRuleBlockToSlashCommand", () => {
description: "Performs a code review",
prompt: "Review this code for best practices",
source: "invokable-rule",
promptFile: "/path/to/rules.yaml",
sourceFile: "file:///path/to/rules.yaml",
});
});

Expand Down
2 changes: 1 addition & 1 deletion core/config/getWorkspaceContinueRuleDotFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export async function getWorkspaceContinueRuleDotFiles(ide: IDE) {
const content = await ide.readFile(dotFile);
rules.push({
rule: content,
ruleFile: dotFile,
sourceFile: dotFile,
source: ".continuerules",
});
}
Expand Down
5 changes: 2 additions & 3 deletions core/config/loadLocalAssistants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,12 @@ import { getGlobalFolderWithName } from "../util/paths";
import { localPathToUri } from "../util/pathToUri";
import { getUriPathBasename, joinPathsToUri } from "../util/uri";
import { SYSTEM_PROMPT_DOT_FILE } from "./getWorkspaceContinueRuleDotFiles";
import { SUPPORTED_AGENT_FILES } from "./markdown";
export function isContinueConfigRelatedUri(uri: string): boolean {
return (
uri.endsWith(".continuerc.json") ||
uri.endsWith(".prompt") ||
uri.endsWith("AGENTS.md") ||
uri.endsWith("AGENT.md") ||
uri.endsWith("CLAUDE.md") ||
!!SUPPORTED_AGENT_FILES.find((file) => uri.endsWith(`/${file}`)) ||
uri.endsWith(SYSTEM_PROMPT_DOT_FILE) ||
(uri.includes(".continue") &&
(uri.endsWith(".yaml") ||
Expand Down
60 changes: 41 additions & 19 deletions core/config/markdown/loadCodebaseRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,38 @@ export class CodebaseRulesCache {
}
async update(ide: IDE, uri: string) {
const content = await ide.readFile(uri);
const { relativePathOrBasename } = findUriInDirs(
const workspaceDirs = await ide.getWorkspaceDirs();
const { relativePathOrBasename, foundInDir } = findUriInDirs(
uri,
await ide.getWorkspaceDirs(),
workspaceDirs,
);
if (!foundInDir) {
console.warn(
`Failed to load codebase rule ${uri}: URI not found in workspace`,
);
}
const rule = markdownToRule(
content,
{
uriType: "file",
fileUri: uri,
},
relativePathOrBasename,
);
const rule = markdownToRule(content, {
uriType: "file",
fileUri: relativePathOrBasename,
});
const ruleWithSource: RuleWithSource = {
...rule,
source: "colocated-markdown",
ruleFile: uri,
sourceFile: uri,
};
const matchIdx = this.rules.findIndex((r) => r.ruleFile === uri);
const matchIdx = this.rules.findIndex((r) => r.sourceFile === uri);
if (matchIdx === -1) {
this.rules.push(ruleWithSource);
} else {
this.rules[matchIdx] = ruleWithSource;
}
}
remove(uri: string) {
this.rules = this.rules.filter((r) => r.ruleFile !== uri);
this.rules = this.rules.filter((r) => r.sourceFile !== uri);
}
}

Expand Down Expand Up @@ -75,20 +85,32 @@ export async function loadCodebaseRules(ide: IDE): Promise<{
for (const filePath of rulesMdFiles) {
try {
const content = await ide.readFile(filePath);
const { relativePathOrBasename } = findUriInDirs(
const { relativePathOrBasename, foundInDir, uri } = findUriInDirs(
filePath,
await ide.getWorkspaceDirs(),
);
const rule = markdownToRule(content, {
uriType: "file",
fileUri: relativePathOrBasename,
});
if (foundInDir) {
const lastSlashIndex = relativePathOrBasename.lastIndexOf("/");
const parentDir = relativePathOrBasename.substring(0, lastSlashIndex);
const rule = markdownToRule(
content,
{
uriType: "file",
fileUri: uri,
},
parentDir,
);

rules.push({
...rule,
source: "colocated-markdown",
ruleFile: filePath,
});
rules.push({
...rule,
source: "colocated-markdown",
sourceFile: filePath,
});
} else {
console.warn(
`Failed to load codebase rule ${uri}: URI not found in workspace dirs`,
);
}
} catch (e) {
errors.push({
fatal: false,
Expand Down
107 changes: 55 additions & 52 deletions core/config/markdown/loadCodebaseRules.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { markdownToRule } from "@continuedev/config-yaml";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IDE } from "../..";
import { walkDirs } from "../../indexing/walkDir";
import { findUriInDirs, getUriPathBasename } from "../../util/uri";
import { loadCodebaseRules } from "./loadCodebaseRules";

// Mock dependencies
Expand All @@ -14,14 +13,10 @@ vi.mock("@continuedev/config-yaml", () => ({
markdownToRule: vi.fn(),
}));

vi.mock("../../util/uri", () => ({
findUriInDirs: vi.fn(),
getUriPathBasename: vi.fn(),
}));

describe("loadCodebaseRules", () => {
// Mock IDE with properly typed mock functions
const mockIde = {
fileExists: vi.fn().mockImplementation(() => true),
readFile: vi.fn() as unknown as IDE["readFile"] & {
mockImplementation: Function;
},
Expand All @@ -30,52 +25,45 @@ describe("loadCodebaseRules", () => {
},
} as unknown as IDE;

// Setup test files
const mockFiles = [
"src/rules.md",
"src/redux/rules.md",
"src/components/rules.md",
"src/utils/helper.ts", // Non-rules file
".continue/rules.md", // This should also be loaded
];

// Mock rule content
const mockRuleContent: Record<string, string> = {
"src/rules.md": "# General Rules\nFollow coding standards",
"src/redux/rules.md":
"file:///workspace/src/rules.md":
"# General Rules\nFollow coding standards",
"file:///workspace/src/redux/rules.md":
'---\nglobs: "**/*.{ts,tsx}"\n---\n# Redux Rules\nUse Redux Toolkit',
"src/components/rules.md":
"file:///workspace/src/components/rules.md":
'---\nglobs: ["**/*.tsx", "**/*.jsx"]\n---\n# Component Rules\nUse functional components',
".continue/rules.md": "# Global Rules\nFollow project guidelines",
"file:///workspace/.continue/rules.md":
"# Global Rules\nFollow project guidelines",
};

// Mock converted rules
const mockConvertedRules: Record<string, any> = {
"src/rules.md": {
"file:///workspace/src/rules.md": {
name: "General Rules",
rule: "Follow coding standards",
source: "colocated-markdown",
ruleFile: "src/rules.md",
sourceFile: "file:///workspace/src/rules.md",
},
"src/redux/rules.md": {
"file:///workspace/src/redux/rules.md": {
name: "Redux Rules",
rule: "Use Redux Toolkit",
globs: "**/*.{ts,tsx}",
source: "colocated-markdown",
ruleFile: "src/redux/rules.md",
sourceFile: "file:///workspace/src/redux/rules.md",
},
"src/components/rules.md": {
"file:///workspace/src/components/rules.md": {
name: "Component Rules",
rule: "Use functional components",
globs: ["**/*.tsx", "**/*.jsx"],
source: "colocated-markdown",
ruleFile: "src/components/rules.md",
sourceFile: "file:///workspace/src/components/rules.md",
},
".continue/rules.md": {
"file:///workspace/.continue/rules.md": {
name: "Global Rules",
rule: "Follow project guidelines",
source: "colocated-markdown",
ruleFile: ".continue/rules.md",
sourceFile: "file:///workspace/.continue/rules.md",
},
};

Expand All @@ -84,20 +72,13 @@ describe("loadCodebaseRules", () => {
vi.resetAllMocks();

// Mock walkDirs to return our test files
(walkDirs as any).mockResolvedValue(mockFiles);
(walkDirs as any).mockResolvedValue([
...Object.keys(mockRuleContent),
"file:///workspace/src/utils/helper.ts", // Non-rules file
]);

// Mock getWorkspaceDirs to return a workspace directory
(mockIde.getWorkspaceDirs as any).mockResolvedValue(["/workspace"]);

// Mock getUriPathBasename to return just the filename
(getUriPathBasename as any).mockImplementation((path: string) => {
return path.split("/").pop() || "";
});

// Mock findUriInDirs to return relative path
(findUriInDirs as any).mockImplementation((uri: string) => {
return { relativePathOrBasename: uri };
});
(mockIde.getWorkspaceDirs as any).mockResolvedValue(["file:///workspace"]);

// Mock readFile to return content based on path
(mockIde.readFile as any).mockImplementation((path: string) => {
Expand All @@ -123,20 +104,36 @@ describe("loadCodebaseRules", () => {

// Should read all rules.md files
expect(mockIde.readFile).toHaveBeenCalledTimes(4);
expect(mockIde.readFile).toHaveBeenCalledWith("src/rules.md");
expect(mockIde.readFile).toHaveBeenCalledWith("src/redux/rules.md");
expect(mockIde.readFile).toHaveBeenCalledWith("src/components/rules.md");
expect(mockIde.readFile).toHaveBeenCalledWith(".continue/rules.md");
expect(mockIde.readFile).toHaveBeenCalledWith(
"file:///workspace/src/rules.md",
);
expect(mockIde.readFile).toHaveBeenCalledWith(
"file:///workspace/src/redux/rules.md",
);
expect(mockIde.readFile).toHaveBeenCalledWith(
"file:///workspace/src/components/rules.md",
);
expect(mockIde.readFile).toHaveBeenCalledWith(
"file:///workspace/.continue/rules.md",
);

// Should convert all rules
expect(markdownToRule).toHaveBeenCalledTimes(4);

// Should return all rules
expect(rules).toHaveLength(4);
expect(rules).toContainEqual(mockConvertedRules["src/rules.md"]);
expect(rules).toContainEqual(mockConvertedRules["src/redux/rules.md"]);
expect(rules).toContainEqual(mockConvertedRules["src/components/rules.md"]);
expect(rules).toContainEqual(mockConvertedRules[".continue/rules.md"]);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/src/rules.md"],
);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/src/redux/rules.md"],
);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/src/components/rules.md"],
);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/.continue/rules.md"],
);

// Should not have errors
expect(errors).toHaveLength(0);
Expand All @@ -145,7 +142,7 @@ describe("loadCodebaseRules", () => {
it("should handle errors when reading a rule file", async () => {
// Setup mock to throw for a specific file
(mockIde.readFile as any).mockImplementation((path: string) => {
if (path === "src/redux/rules.md") {
if (path === "file:///workspace/src/redux/rules.md") {
return Promise.reject(new Error("Failed to read file"));
}
return Promise.resolve(mockRuleContent[path] || "");
Expand All @@ -155,14 +152,20 @@ describe("loadCodebaseRules", () => {

// Should still return other rules
expect(rules).toHaveLength(3);
expect(rules).toContainEqual(mockConvertedRules["src/rules.md"]);
expect(rules).toContainEqual(mockConvertedRules["src/components/rules.md"]);
expect(rules).toContainEqual(mockConvertedRules[".continue/rules.md"]);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/src/rules.md"],
);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/src/components/rules.md"],
);
expect(rules).toContainEqual(
mockConvertedRules["file:///workspace/.continue/rules.md"],
);

// Should have one error
expect(errors).toHaveLength(1);
expect(errors[0].message).toContain(
"Failed to parse colocated rule file src/redux/rules.md",
"Failed to parse colocated rule file file:///workspace/src/redux/rules.md: Failed to read file",
);
});

Expand Down
Loading
Loading