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
5 changes: 5 additions & 0 deletions .leetcode
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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+**
Expand Down
17 changes: 10 additions & 7 deletions src/modules/LeetcodeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`). */
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<Array<{ slug: string; name: string }>>("studyPlans") ?? DEFAULTS.studyPlans;
const studyPlans = leetcode.studyPlans ?? vscodeConfig.get<Array<{ slug: string; name: string; path?: string }>>("studyPlans") ?? DEFAULTS.studyPlans;
const problemLists =
leetcode.problemLists ?? vscodeConfig.get<Array<{ slug: string; name: string }>>("problemLists") ?? DEFAULTS.problemLists;
const vsActiveSource = vscodeConfig.get<string>("activeListSource");
Expand Down
38 changes: 36 additions & 2 deletions src/modules/ProblemsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -248,7 +248,41 @@ export class ProblemsTreeProvider implements vscode.TreeDataProvider<ProblemTree
this.groups = problems.length > 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<string, string[]>;

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 {
Expand Down