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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ The problems cover what's actually inside Transformers, vLLM, TRL, diffusion mod

### Features

- **Browser editor** — Monaco with Python syntax highlighting, no IDE setup
- **Browser editor** — Monaco with Python syntax highlighting and autocomplete for
`torch`, `nn`, `F`, `np`, `math` and tensor methods, no IDE setup
- **Instant feedback** — submit and see pass/fail per test case in seconds
- **Reference solutions** — compare after your own attempt
- **Progress tracking** — solved count and attempt history, persisted across sessions
Expand Down
3 changes: 2 additions & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@

### 功能亮点

- **浏览器直接写** — 内置 Monaco 编辑器,开箱即用,不用折腾本地 IDE
- **浏览器直接写** — 内置 Monaco 编辑器,支持 `torch`、`nn`、`F`、`np`、`math`
以及张量方法的自动补全,开箱即用,不用折腾本地 IDE
- **秒级反馈** — 提交即判,逐条显示测试结果
- **参考实现** — 先自己写,再看答案
- **进度记录** — 做了多少、试了几次,关掉浏览器也不丢
Expand Down
191 changes: 191 additions & 0 deletions scripts/gen_completions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""Generate Monaco completion data for the Python editor.

Monaco ships a syntax highlighter for Python but no language service, so the
editor can only suggest words that already appear in the buffer -- typing
``np.`` or ``torch.`` offers nothing. This script introspects the exact modules
the grading sandbox injects into every submission's namespace
(see ``grading_service.main._execute_tests``) and writes them to a JSON file the
editor loads lazily.

Run it after upgrading torch/numpy:

python scripts/gen_completions.py
"""

from __future__ import annotations

import inspect
import json
import math
import re
import sys
from pathlib import Path
from typing import Any

ROOT = Path(__file__).parent.parent
OUTPUT = ROOT / "web" / "src" / "lib" / "pythonCompletions.json"

MAX_DOC = 160

# Mirrors the sandbox namespace in grading_service/main.py. Keep in sync.
import numpy # noqa: E402
import torch # noqa: E402

NAMESPACES: dict[str, Any] = {
"torch": torch,
"nn": torch.nn,
"F": torch.nn.functional,
"np": numpy,
"math": math,
"Tensor": torch.Tensor,
}

# Offered at the top level, plus after an unrecognised ``name.`` we fall back to
# Tensor members -- in these problems almost every local variable is a tensor.
KEYWORDS = [
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif",
"else", "except", "False", "finally", "for", "from", "global", "if",
"import", "in", "is", "lambda", "None", "nonlocal", "not", "or", "pass",
"raise", "return", "True", "try", "while", "with", "yield",
]
BUILTINS = [
"abs", "all", "any", "bool", "dict", "enumerate", "filter", "float",
"format", "int", "isinstance", "len", "list", "map", "max", "min",
"print", "range", "repr", "reversed", "round", "set", "slice", "sorted",
"str", "sum", "tuple", "type", "zip",
]

_SIG_LINE = re.compile(r"^\s*\w+\((.*?)\)(\s*->\s*[^\n]+)?\s*$")


def _skip_signature_block(lines: list[str], name: str) -> list[str]:
"""Drop the leading ``name(...)`` signature, which torch and numpy docstrings
both carry and numpy wraps across several lines."""
out = [ln for ln in lines if ln]
while out:
first = out[0]
if _SIG_LINE.match(first) and first.startswith(f"{name}("):
out = out[1:]
continue
if first.startswith(f"{name}("):
# a multi-line signature: consume until the parentheses balance
depth = 0
for i, ln in enumerate(out):
depth += ln.count("(") - ln.count(")")
if depth <= 0:
out = out[i + 1:]
break
else:
return []
continue
break
return out


def _signature(obj: Any, name: str) -> str:
"""Best-effort signature. C extensions defeat inspect, so fall back to the
first docstring line, which torch and numpy both format as a signature."""
try:
return str(inspect.signature(obj))
except (TypeError, ValueError):
pass
doc = inspect.getdoc(obj) or ""
for line in doc.splitlines()[:3]:
line = line.strip()
if line.startswith(f"{name}(") and _SIG_LINE.match(line):
return line[len(name):]
return "(...)"


def _summary(obj: Any, name: str) -> str:
doc = inspect.getdoc(obj) or ""
lines = [ln.strip() for ln in doc.splitlines()]
body = _skip_signature_block(lines, name)
if not body:
return ""
text = body[0]
if len(text) > MAX_DOC:
text = text[: MAX_DOC - 1].rstrip() + "…"
return text


def _value(obj: Any) -> str:
"""Short repr for a constant. Never call getdoc on one: inspect walks up to
the *type*, so math.pi would document itself as float.__doc__."""
try:
text = repr(obj)
except Exception:
return ""
text = " ".join(text.split())
if len(text) > 60:
return ""
return text


def _kind(obj: Any) -> str:
if inspect.ismodule(obj):
return "module"
if inspect.isclass(obj):
return "class"
if callable(obj):
return "function"
if inspect.isdatadescriptor(obj):
# Tensor.shape and friends: a descriptor, not a value. Its own __doc__ is
# meaningful, and repr() would print "<attribute 'shape' of ...>".
return "property"
return "constant"


def collect(module: Any) -> list[dict[str, str]]:
out: list[dict[str, str]] = []
for name in sorted(dir(module)):
if name.startswith("_"):
continue
try:
obj = getattr(module, name)
except Exception:
continue
kind = _kind(obj)
entry: dict[str, str] = {"l": name, "k": kind}
if kind in ("function", "class", "property"):
if kind != "property":
sig = _signature(obj, name)
if sig and sig != "(...)":
entry["d"] = sig
summary = _summary(obj, name)
if summary:
entry["s"] = summary
elif kind == "constant":
value = _value(obj)
if value:
entry["d"] = value
out.append(entry)
return out


def main() -> None:
data = {
"_comment": "Generated by scripts/gen_completions.py -- do not edit by hand.",
"versions": {
"torch": torch.__version__,
"numpy": numpy.__version__,
"python": f"{sys.version_info.major}.{sys.version_info.minor}",
},
"keywords": KEYWORDS,
"builtins": BUILTINS,
"namespaces": {alias: collect(mod) for alias, mod in NAMESPACES.items()},
}
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")) + "\n",
encoding="utf-8")
total = sum(len(v) for v in data["namespaces"].values())
size_kb = OUTPUT.stat().st_size / 1024
print(f"Wrote {total} completions across {len(NAMESPACES)} namespaces "
f"to {OUTPUT.relative_to(ROOT)} ({size_kb:.0f} KB)")
for alias, entries in data["namespaces"].items():
print(f" {alias:8s} {len(entries):5d}")


if __name__ == "__main__":
main()
6 changes: 6 additions & 0 deletions web/src/components/workspace/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useRef, useEffect } from 'react';
import dynamic from 'next/dynamic';
import type { OnMount } from '@monaco-editor/react';
import { appleLight, appleDark } from '@/lib/monacoTheme';
import { registerPythonCompletions } from '@/lib/pythonCompletions';
import { useTheme } from '@/context/ThemeContext';

const Editor = dynamic(() => import('@monaco-editor/react').then((m) => m.default), {
Expand Down Expand Up @@ -47,6 +48,7 @@ export function CodeEditor({
monaco.editor.defineTheme('apple-light', appleLight);
monaco.editor.defineTheme('apple-dark', appleDark);
monaco.editor.setTheme(theme === 'dark' ? 'apple-dark' : 'apple-light');
void registerPythonCompletions(monaco);
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => runRef.current?.());
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => submitRef.current?.());
};
Expand Down Expand Up @@ -77,6 +79,10 @@ export function CodeEditor({
cursorBlinking: 'smooth',
cursorSmoothCaretAnimation: 'on',
bracketPairColorization: { enabled: true },
quickSuggestions: { other: true, comments: false, strings: false },
suggestOnTriggerCharacters: true,
wordBasedSuggestions: 'currentDocument',
suggest: { showWords: true, localityBonus: true },
overviewRulerBorder: false,
hideCursorInOverviewRuler: true,
scrollbar: {
Expand Down
1 change: 1 addition & 0 deletions web/src/lib/pythonCompletions.json

Large diffs are not rendered by default.

Loading