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
75 changes: 75 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A VS Code extension called "AtCoder Helper neo" for browsing AtCoder competitive programming contest problems, with LaTeX rendering, DeepL translation, code submission, and contest registration. Chinese-language UI and documentation.

## Commands

```bash
pnpm install # Install dependencies
pnpm build # Build all packages (turbo)
pnpm dev # Dev watch mode (extension + webview)
pnpm lint # ESLint all packages
pnpm format # Prettier format
pnpm test # Run all tests

# Run unit tests directly
cd apps/vscode-extension && pnpm test

# Package extension
cd apps/vscode-extension && pnpm package
# Output: release/extension.vsix
```

Debug: press F5 in VS Code to launch Extension Host (preLaunchTask runs `pnpm dev`).

## Architecture

Monorepo with pnpm workspaces + Turborepo. Webpack dual-config builds both the Node extension and the React webview into `apps/vscode-extension/dist/`.

**Dependency flow:** `apps/vscode-extension` → `packages/webview` → `packages/ui` + `packages/core`

Webpack resolves `@template/ui` and `@template/core` via path aliases directly to source — no need to pre-build packages during development.

### Extension host (Node, CommonJS)

Entry: `apps/vscode-extension/src/extension.ts` — registers commands, creates WebView panel, routes messages.

Communication protocol: WebView sends `{ command: "..." }`, Extension responds with `{ type: "..." }`. Message types defined in `packages/webview/src/types.ts` (`WebviewMessage` union). Adding a new message requires updating both `types.ts` and the `switch` in `extension.ts`.

Network layer (`tools/fetch.ts`): custom HTTP client using Node https/http/zlib, no external deps. Three error classes: `CfError` (Cloudflare), `ProxyError`, `LoginRequiredError` — handled uniformly by `handleErrorWithCfAndLogin()`.

### WebView frontend (React, browser target)

Entry: `packages/webview/src/index.tsx` → `WebviewApp.tsx`. Uses Tailwind CSS with `var(--vscode-*)` variables for theme adaptation. KaTeX math is pre-rendered server-side in the extension host.

### Shared packages

- `packages/core` — `MessageBus` (pub/sub) and `StateManager<T>`
- `packages/ui` — Button, Card, Input, Spinner components styled with VS Code CSS variables

## Code Conventions

- 4-space indent, no `export default`, prefer `interface` over `type`
- No `any` — use `unknown` instead
- Functions max 120 lines (CI enforces 130 hard limit)
- Import order: external deps → `@template/*` packages → relative paths
- Component files: PascalCase. Tool/utility files: camelCase
- Error handling: use `CfError`/`ProxyError`/`LoginRequiredError`, never bare `throw new Error()`
- Git branches: `feat/xxx`, `fix/xxx`, `refactor/xxx`, `docs/xxx`
- Main branch: `main`, development branch: `dev`

## CI

PR to `main` triggers: lint → build → test → function length check (>130 lines fails). Results posted as PR comment.

## Git Branch Rules (Claude MUST follow)

- **NEVER** commit, push, or merge to `main` branch directly
- **NEVER** run `git checkout main` or `git push origin main`
- **ALWAYS** work on `dev` branch or feature branches (`feat/*`, `fix/*`, `refactor/*`, `docs/*`)
- Before any Git operation, confirm current branch with `git branch --show-current`
- If user asks to modify `main`, remind them to use `dev` instead
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ node_modules/
.DS_Store
.swc
.vsix
DEEP.md
ToDo.md
memory.md
issue.md
3 changes: 3 additions & 0 deletions apps/vscode-extension/media/atcoder.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 22 additions & 3 deletions apps/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
"Other"
],
"activationEvents": [
"onCommand:extension.showWebview"
"onCommand:extension.showWebview",
"onView:atcoderHelper.webviewView"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "extension.showWebview",
"title": "AtCoder"
"title": "AtCoder Helper(编辑器)"
},
{
"command": "extension.setDeeplApiKey",
Expand All @@ -33,7 +34,25 @@
"command": "extension.setAtCoderCookie",
"title": "Set AtCoder Login Cookie"
}
]
],
"viewsContainers": {
"activitybar": [
{
"id": "atcoder-helper-container",
"title": "AtCoder Helper",
"icon": "media/atcoder.svg"
}
]
},
"views": {
"atcoder-helper-container": [
{
"type": "webview",
"id": "atcoderHelper.webviewView",
"name": "AtCoder Helper"
}
]
}
},
"scripts": {
"vscode:prepublish": "pnpm run build",
Expand Down
35 changes: 35 additions & 0 deletions apps/vscode-extension/src/atcoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface AtCoderProblem {
outputFormat: string;
samples: SampleCase[];
sampleUrl?: string;
timeLimit?: number;
memoryLimit?: number;
}

function decodeEntities(text: string): string {
Expand Down Expand Up @@ -148,6 +150,37 @@ function getLangContent(html: string, lang: "en" | "ja"): string {
return html.slice(start);
}

function extractHeaderValue(html: string, labels: string[]): string | undefined {
const rowRegex = /<tr[^>]*>([\s\S]*?)<\/tr>/gi;
let rowMatch: RegExpExecArray | null;
for (; (rowMatch = rowRegex.exec(html)) !== null;) {
const row = rowMatch[1];
if (!labels.some((label) => row.includes(label))) continue;
const tds = Array.from(row.matchAll(/<td[^>]*>([\s\S]*?)<\/td>/gi));
if (tds.length === 0) continue;
const value = cleanText(tds[tds.length - 1][1]);
return value || undefined;
}
return undefined;
}

function extractTimeLimit(html: string): number | undefined {
const value = extractHeaderValue(html, ["Time Limit", "時間制限"]);
const match = value && value.match(/(\d+(?:\.\d+)?)/);
if (!match) return undefined;
const ms = value!.toLowerCase().includes("sec")
? parseFloat(match[1]) * 1000
: parseFloat(match[1]);
return Math.round(ms);
}

function extractMemoryLimit(html: string): number | undefined {
const value = extractHeaderValue(html, ["Memory Limit", "メモリ制限"]);
const match = value && value.match(/(\d+(?:\.\d+)?)/);
if (!match) return undefined;
return Math.round(parseFloat(match[1]));
}

export function parseProblemPage(html: string, url: string): AtCoderProblem {
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
const title = titleMatch ? cleanText(titleMatch[1]) : "Untitled";
Expand Down Expand Up @@ -208,6 +241,8 @@ export function parseProblemPage(html: string, url: string): AtCoderProblem {
outputFormat,
samples,
sampleUrl,
timeLimit: extractTimeLimit(html),
memoryLimit: extractMemoryLimit(html),
};
}

Expand Down
Loading
Loading