diff --git a/.leetcode b/.leetcode index 180f5d7..5f97a73 100644 --- a/.leetcode +++ b/.leetcode @@ -7,6 +7,11 @@ { "slug": "30-days-of-javascript", "name": "30 Days of JavaScript" + }, + { + "slug": "neetcode-150", + "name": "NeetCode 150", + "path": "data/neetcode-150.json" } ], "problemLists": [ diff --git a/README.md b/README.md index 0b1e3b2..85165a2 100644 --- a/README.md +++ b/README.md @@ -258,7 +258,10 @@ Any setting above can be overridden per-workspace in the `.leetcode` file. Works ```json { - "studyPlans": [{ "slug": "top-interview-150", "name": "Top Interview 150" }], + "studyPlans": [ + { "slug": "top-interview-150", "name": "Top Interview 150" }, + { "slug": "my-custom-list", "name": "My Custom List", "path": "data/custom-list.json" } + ], "problemLists": [{ "slug": "graph", "name": "Graph" }], "activeStudyPlan": "top-interview-150", "activeProblemList": "graph", @@ -268,6 +271,21 @@ Any setting above can be overridden per-workspace in the `.leetcode` file. Works } ``` +### Custom Study Plans + +You can load your own custom-categorized study plans (like NeetCode 150) without relying on LeetCode's backend. + +Simply add the `path` property to a study plan entry in your `.leetcode` config and point it to a local JSON file in your workspace: + +```json +{ + "Arrays & Hashing": ["contains-duplicate", "valid-anagram"], + "Two Pointers": ["valid-palindrome"] +} +``` + +The extension will read this JSON file and instantly render it in the Study Plans sidebar as a fully categorized tree view. + ## Requirements - VS Code or Cursor **1.85+** diff --git a/src/modules/LeetcodeConfig.ts b/src/modules/LeetcodeConfig.ts index bb7251c..54bf9cd 100644 --- a/src/modules/LeetcodeConfig.ts +++ b/src/modules/LeetcodeConfig.ts @@ -9,7 +9,7 @@ export type ActiveListSource = "studyPlan" | "problemList"; /** Schema for .leetcode config file. Overrides VS Code settings for this workspace. */ export interface LeetcodeConfig { - studyPlans?: Array<{ slug: string; name: string }>; + studyPlans?: Array<{ slug: string; name: string; path?: string }>; /** LeetCode problem-list slugs (e.g. graph → /problem-list/graph/). */ problemLists?: Array<{ slug: string; name: string }>; /** Default study plan slug for the Study Plans sidebar (must match `studyPlans`). */ @@ -79,21 +79,24 @@ const DEFAULTS: Required< "Explain my solution code for this LeetCode problem. Respond with: (1) Intuition — core idea in plain language; (2) Step-by-step dry run — walk through the algorithm with a small example, including loop/state changes; (3) Time and space complexity with brief justification. Do not rewrite the full solution unless needed for clarity.", }; -function isValidStudyPlanEntry(obj: unknown): obj is { slug: string; name: string } { +function isValidStudyPlanEntry(obj: unknown): obj is { slug: string; name: string; path?: string } { return ( typeof obj === "object" && obj !== null && typeof (obj as { slug?: unknown }).slug === "string" && - typeof (obj as { name?: unknown }).name === "string" + typeof (obj as { name?: unknown }).name === "string" && + ((obj as { path?: unknown }).path === undefined || typeof (obj as { path?: unknown }).path === "string") ); } -function parseStudyPlans(raw: unknown): Array<{ slug: string; name: string }> { +function parseStudyPlans(raw: unknown): Array<{ slug: string; name: string; path?: string }> { if (!Array.isArray(raw)) return DEFAULTS.studyPlans; - const result: Array<{ slug: string; name: string }> = []; + const result: Array<{ slug: string; name: string; path?: string }> = []; for (const item of raw) { if (isValidStudyPlanEntry(item)) { - result.push({ slug: item.slug, name: item.name }); + const entry: { slug: string; name: string; path?: string } = { slug: item.slug, name: item.name }; + if (item.path !== undefined) entry.path = item.path; + result.push(entry); } } return result.length > 0 ? result : DEFAULTS.studyPlans; @@ -347,7 +350,7 @@ export function getEffectiveConfig( ): LeetcodeConfig & { internalApiUrl: string; problemViewMode: "ui" | "text" } { const vscodeConfig = vscode.workspace.getConfiguration("leetcodePractice"); const leetcode = parseLeetcodeConfig(workspaceFolders); - const studyPlans = leetcode.studyPlans ?? vscodeConfig.get>("studyPlans") ?? DEFAULTS.studyPlans; + const studyPlans = leetcode.studyPlans ?? vscodeConfig.get>("studyPlans") ?? DEFAULTS.studyPlans; const problemLists = leetcode.problemLists ?? vscodeConfig.get>("problemLists") ?? DEFAULTS.problemLists; const vsActiveSource = vscodeConfig.get("activeListSource"); diff --git a/src/modules/ProblemsProvider.ts b/src/modules/ProblemsProvider.ts index 0b84d78..60f3327 100644 --- a/src/modules/ProblemsProvider.ts +++ b/src/modules/ProblemsProvider.ts @@ -7,7 +7,7 @@ import { type ProblemListItem, type StudyPlanGroup, } from "./LeetCode"; -import { NO_PROBLEM_LIST_SENTINEL } from "./LeetcodeConfig"; +import { NO_PROBLEM_LIST_SENTINEL, getEffectiveConfig } from "./LeetcodeConfig"; const STATUS_KEY = "leetcode-practice.problemStatus"; export type ProblemStatus = "solved" | "attempting"; @@ -248,7 +248,41 @@ export class ProblemsTreeProvider implements vscode.TreeDataProvider 0 ? [{ category, problems }] : []; } } else { - this.groups = await this.leetcode.getStudyPlanProblemListGrouped(planSlug); + const lcexCfg = getEffectiveConfig(vscode.workspace.workspaceFolders ?? []); + const planConfig = lcexCfg.studyPlans?.find((p) => p.slug === planSlug); + + if (planConfig?.path) { + let localPath = ""; + for (const f of vscode.workspace.workspaceFolders ?? []) { + const candidate = path.resolve(f.uri.fsPath, planConfig.path); + if (fs.existsSync(candidate)) { + localPath = candidate; + break; + } + } + if (localPath) { + try { + const raw = fs.readFileSync(localPath, "utf-8"); + const customPlan = JSON.parse(raw) as Record; + + const fullProvider = new ProblemsTreeProvider("problemset", this.memento, this.storagePath); + const allProblems = await fullProvider.getProblemList(); + const bySlug = new Map(allProblems.map(p => [p.titleSlug, p])); + + this.groups = Object.entries(customPlan).map(([category, slugs]) => ({ + category, + problems: slugs.map(slug => bySlug.get(slug)).filter((p): p is ProblemListItem => !!p) + })).filter(g => g.problems.length > 0); + } catch (e) { + console.error(`Failed to load custom study plan from ${localPath}`, e); + this.groups = []; + } + } else { + this.groups = []; + } + } else { + this.groups = await this.leetcode.getStudyPlanProblemListGrouped(planSlug); + } } if (this.groups.length > 0) { try {