LeetCode Practice
English · 繁體中文
A VS Code extension for practising LeetCode, built around spaced repetition:
problem folders lead with the problem number, so they pair with an
archive and review workflow that decides
what to redo and when.
Written from scratch — not a fork of any existing extension. Inspired by
Ayanrocks/better-leetcode, but
sharing none of its code.
What works today
Signing in
Click the status bar or the sidebar and LeetCode opens in your browser; once
you authorise there, VS Code picks the session up on its own. Nothing to copy.
Credentials live in VS Code's secret storage, scoped to this extension, so they
never reach a settings file. They are checked against LeetCode before being
stored, so a bad or expired session fails immediately rather than at the first
submission.
Because the handoff returns the session in a URL, a callback is accepted only
while an authorisation started here is still open (five minutes), only when
addressed to this extension, and only once. Otherwise any link could hand the
extension somebody else's session.
Signing in with a cookie instead
Run LeetCode Practice: Sign In with Cookie from the command palette. It is
the way in for anywhere the browser handoff cannot reach: a vscode:// scheme
the OS has not registered, Remote SSH or a dev container where the callback has
to cross a machine boundary, VS Code on the web, or LeetCode changing its
authorisation page.
- Sign in to https://leetcode.com in your browser
- Open DevTools (F12) and go to the Network tab
- Reload the page, click any request to
leetcode.com, and find
Request Headers → Cookie
- Copy that whole line and paste it in
The line is long and holds far more than is needed; only these two are read, and
they can be pasted on their own in either order:
LEETCODE_SESSION=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; csrftoken=8kZQ2nR7...
Sessions expire after a few weeks, at which point Test and Submit start failing
with "the session may have expired" and you sign in again.
- Daily Challenge — today's problem, one row
- Study Lists — your own lists, then LeetCode's official plans (Top
Interview 150, LeetCode 75 and the rest) as plan, section, problem, with how
many of each you have solved. Your lists have a listing query; the plans do
not, so which ones to show is a setting.
- Contests — upcoming ones with a countdown, then the last dozen that ran.
A finished contest expands into its problems, each opening like any other,
which is the practisable part; an upcoming one has no problems yet, so its row
opens the website where the timer and scoreboard are.
- Problems — the whole set, over 4,000 problems, searchable. The list is
cached on disk and only refetched in the background once it is a week old.
Unfiltered it renders the first 500 rows, because nobody scrolls four thousand
and search is the way in. A numeric query matches the number by prefix, so
typing
17 surfaces 17, 170 and 171 rather than being buried by the 1700s.
Icons: colour is difficulty, shape is progress
One glyph answers both "how hard" and "have I done this":
| State |
Icon |
Colour |
| Untouched |
hollow circle |
green Easy / yellow Medium / red Hard |
| Attempted, not solved |
filled circle |
as above |
| Solved |
tick in a circle |
as above |
| Premium |
padlock |
as above |
Colours come from VS Code's charts.green / yellow / red, so they follow
the reader's theme instead of hardcoding hex values.
Opening a problem
A folder appears under storagePath, named <number>-<titleSlug>:
3028-ant-on-the-boundary/
├─ main.py # LeetCode's template
├─ testcases.txt # every example case
└─ .metadata.json # both ids, language, lines per test case
The code opens in the editor and the statement opens beside it without taking
focus, so the cursor lands where the typing happens.
- Reopening a problem never overwrites work. An existing
main.* or
testcases.txt is left exactly as it was.
- Python templates get the typing imports they actually reference. LeetCode
serves
def twoSum(self, nums: List[int]) with no import, and since Python
evaluates annotations at definition time, that file raises NameError when run
locally. Only mentioned names are imported: two-sum gets List,
add-two-numbers gets Optional, reverse-integer gets nothing.
- The statement panel loads no scripts, behind a content security policy that
allows inline styles and remote images only.
Switching language
The button in the editor title bar lists the languages that problem offers, 19
of them for most. Picking one writes that language's template beside the
existing file rather than replacing it, so a problem can hold main.py and
main.rs at once — redoing a solved problem in another language is a practice
technique, not an accident. Test and Submit use whichever file is open.
Testing and submitting
With a solution file open, Ctrl+; runs it against the cases in
testcases.txt and Ctrl+Enter submits it; both also sit as buttons in the
editor title bar. Results appear in the LeetCode Results panel at the
bottom, alongside Terminal and Output: verdict, timing, each case next to its
expected output, and compile or runtime errors when there are any. They sit
there rather than in an editor tab because a result is something to glance at
while the code stays on screen. A submission asks for confirmation first, since it is recorded against
the account.
The language comes from the file's own extension, not the folder's metadata: a
problem can hold main.py and main.rs at once, and the one being submitted
is the one in front of you.
Cloudflare. The judge endpoints are behind bot mitigation that fingerprints
the TLS handshake before reading a single header — measured on
interpret_solution, submit and check, all three answer 403 with
cf-mitigated: challenge from Node's own HTTP stack, browser headers or not.
This extension therefore sends those three requests through
impit, which performs the handshake as Chrome
would; the same endpoints then answer from LeetCode itself. impit is a native
module, so a VSIX is platform specific.
Settings
| Setting |
What it does |
leetcodePractice.storagePath |
Where problem folders are created. Empty means solutions/ under the first workspace folder. |
leetcodePractice.defaultLanguage |
Which language template a problem opens with (python3, cpp, rust, …). |
leetcodePractice.studyPlans |
Which study plans the sidebar shows, by the slug in their URL. |
Three commands have no button, and live in the palette: Change Default
Language, Sign In with Cookie, and Clear Cache, which throws away the
cached problem list and any solutions read without touching your own files.
Reading other people's solutions
The discussion button in the editor title bar lists the most upvoted community
solutions for the problem, with votes, views and comments, and opens the one you
pick as a Markdown preview beside your code. They are Markdown already, so
VS Code's own preview renders them — no Markdown renderer or HTML sanitiser to
ship, and the code blocks come out highlighted and copyable. Articles are cached
under global storage rather than in the problem folder, since they are somebody
else's work and the archive workflow sweeps up whatever sits next to a solution.
Developing
bun install
bun run compile
F5 starts the Extension Development Host. It passes --disable-extensions by
default so a session exercises this extension alone, without noise or crashes
from unrelated ones.
bun run typecheck
bun run lint
bun test
The tests cover the parts where being wrong is silent: reading judge payloads,
undoing the escaping on community posts, deciding which language a file
submits as, and normalising the vocabularies LeetCode uses in different places.
They run against payloads recorded from real runs, so they need no credentials
and no network. The scripts under scripts/ do talk to the API and are run by
hand.
Conventions and the LeetCode API behaviour found by probing are in
CLAUDE.md.
Licence
MIT — see LICENSE.