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
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,4 @@ node_modules/
.DS_Store
.swc
.vsix
ToDo.md
memory.md
issue.md
DEEP.md
3 changes: 0 additions & 3 deletions apps/vscode-extension/media/atcoder.svg

This file was deleted.

25 changes: 3 additions & 22 deletions apps/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@
"Other"
],
"activationEvents": [
"onCommand:extension.showWebview",
"onView:atcoderHelper.webviewView"
"onCommand:extension.showWebview"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "extension.showWebview",
"title": "AtCoder Helper(编辑器)"
"title": "AtCoder"
},
{
"command": "extension.setDeeplApiKey",
Expand All @@ -34,25 +33,7 @@
"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
165 changes: 22 additions & 143 deletions apps/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import * as vscode from "vscode";
import * as path from "path";
import { AtCoderProblem, fetchAtCoderProblem, fetchAtCoderTasks } from "./atcoder";
import { CfError, ProxyError, LoginRequiredError, setSessionCookie, setStaleCookieHandler, fetchSubStatus, fetchSubmitHistory } from "./tools/fetch";
import { fetchContest, signedUpContest, fetchContestAnnouncement } from "./tools/SignUpContest";
import { CfError, ProxyError, LoginRequiredError, setSessionCookie, fetchSubStatus, fetchSubmitHistory } from "./tools/fetch";
import { fetchContest, signedUpContest } from "./tools/SignUpContest";
import { translateTextRaw, translateTextFree } from "./tools/deepl";
import { runCommand } from "./tools/command";
import { fetchSubmitPage, submitCodeWithRedirect } from "./tools/submit";
import { buildCphProblem, sendToCph } from "./tools/cph";
import { fetchStandings } from "./tools/standings";
import { fetchHomepageContests } from "./tools/homepage";
import { fetchSubmissionDetail } from "./tools/submission";
import { buildCphProblem, sendToCph } from "./tools/cph"
import { IncomingMessage } from "./tools/types";
import { getWebviewContent } from "./tools/webview";
import { AtCoderViewProvider } from "./viewProvider";
import { send } from "process";

const log = {
info: (...args: unknown[]) => {
Expand All @@ -26,6 +22,22 @@ const log = {

const sleep = (ms: number): Promise<void> => new Promise<void>(resolve => setTimeout(resolve, ms));

function getWebviewContent(webviewJsSrc: vscode.Uri): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'unsafe-eval' 'unsafe-inline' vscode-resource:; style-src vscode-resource: 'unsafe-inline';">
<title>VSCode Boilerplate</title>
</head>
<body>
<div id="root"></div>
<script src="${webviewJsSrc}"></script>
</body>
</html>`;
}

function handleErrorWithCfAndLogin(error: unknown, send: (payload: Record<string, unknown>) => void): boolean {
if (error instanceof CfError) {
vscode.window.showErrorMessage(error.message, "在浏览器中打开").then((choice) => {
Expand Down Expand Up @@ -77,8 +89,7 @@ export async function handleContestLoad(contest: string, send: (payload: Record<

try {
const contestInfo = await fetchContest(contest);
const announcement = await fetchContestAnnouncement(contest);
send({ type: "contestInfo", Rated: contestInfo.Rated, announcement, title: contestInfo.title });
send({ type: "contestInfo", Rated: contestInfo.Rated });
} catch (e) {
//不处理
}
Expand Down Expand Up @@ -200,14 +211,6 @@ export async function handleFetchSubmitPage(contest: string, send: (payload: Rec
send({ type: "submitPage", submitTasks: pageData.tasks, languages: pageData.languages, csrfToken: pageData.csrfToken });
send({ type: "update", text: "已获取提交页面信息" });
} catch (error) {
if (error instanceof CfError) {
send({ type: "submitPageError", message: "该比赛提交需要 Cloudflare 验证,插件无法自动完成。请在浏览器中打开提交页完成验证后提交。", url: error.url });
return;
}
if (error instanceof LoginRequiredError) {
send({ type: "submitPageError", message: "提交需要登录,请先设置 AtCoder Cookie 后再试。", url: `https://atcoder.jp/contests/${contest}/submit` });
return;
}
if (!handleErrorWithCfAndLogin(error, send)) {
send({ type: "error", text: error instanceof Error ? error.message : "获取提交页面失败" });
}
Expand Down Expand Up @@ -243,14 +246,6 @@ export async function handleSubmitCode(
}
} else send({ type: "error", text: result.message });
} catch (error) {
if (error instanceof CfError) {
send({ type: "submitResult", submitResult: { success: false, message: "该比赛提交需要 Cloudflare 验证,插件无法自动完成。请在浏览器中打开提交页完成验证后提交。" } });
return;
}
if (error instanceof LoginRequiredError) {
send({ type: "submitResult", submitResult: { success: false, message: "提交需要登录,请先设置 AtCoder Cookie 后再试。" } });
return;
}
if (!handleErrorWithCfAndLogin(error, send)) {
send({ type: "submitResult", submitResult: { success: false, message: error instanceof Error ? error.message : "提交失败" } });
}
Expand All @@ -269,50 +264,14 @@ export async function handleFetchSubHistory(contest: string, send: (payload: Rec
}
}

export async function handleFetchSubmissionDetail(contest: string, id: string, send: (payload: Record<string, unknown>) => void) {
send({ type: "loading", text: `正在获取提交 ${id} 的详细信息...` });
try {
const detail = await fetchSubmissionDetail(contest, id);
send({ type: "submissionDetail", submissionDetail: detail });
} catch (error) {
if (!handleErrorWithCfAndLogin(error, send)) {
send({ type: "error", text: error instanceof Error ? error.message : "获取提交详情失败" });
}
}
}

export async function handleFetchStandings(contest: string, send: (payload: Record<string, unknown>) => void) {
send({ type: "loading", text: `正在获取 ${contest} 排行榜...` });
try {
const standings = await fetchStandings(contest);
send({ type: "standings", contest, standings });
} catch (error) {
if (!handleErrorWithCfAndLogin(error, send)) {
send({ type: "error", text: error instanceof Error ? error.message : "获取排行榜失败" });
}
}
}

export async function handleGetContests(send: (payload: Record<string, unknown>) => void) {
send({ type: "loading", text: "正在抓取 AtCoder 首页比赛列表..." });
try {
const contests = await fetchHomepageContests();
send({ type: "contestList", contests });
} catch (error) {
if (!handleErrorWithCfAndLogin(error, send)) {
send({ type: "error", text: error instanceof Error ? error.message : "获取比赛列表失败" });
}
}
}

export async function handleExportToCph(problem: AtCoderProblem, send: (payload: Record<string, unknown>) => void) {
try {
const payload = buildCphProblem(problem);
await sendToCph(payload);
send({ type: "cphExportResult", success: true, message: "success send to cph" });
} catch (error) {
const message = error instanceof Error ? error.message : "fail to send cph";
send({ type: "cphExportResult", success: false, message: message });
send({ type: "error", success: false, message: message });
}
}

Expand Down Expand Up @@ -416,83 +375,9 @@ function createShowWebview(context: vscode.ExtensionContext) {
};
}

export function openContestPanel(context: vscode.ExtensionContext, contest: string) {
const panel = vscode.window.createWebviewPanel(
"atcoderContest",
`AtCoder - ${contest}`,
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(path.join(context.extensionPath, "dist")),
],
}
);

const webviewJsPath = vscode.Uri.file(
path.join(context.extensionPath, "dist", "webview.js")
);
const webviewJsSrc = panel.webview.asWebviewUri(webviewJsPath);

panel.webview.html = getWebviewContent(webviewJsSrc, "contest", contest);

const sendToWebview = (payload: Record<string, unknown>) => {
panel.webview.postMessage(payload);
};

panel.webview.onDidReceiveMessage(
(message: IncomingMessage) => { runCommand(message, context, sendToWebview); },
undefined,
context.subscriptions
);

context.subscriptions.push(panel);
}

export function openSubmissionPanel(context: vscode.ExtensionContext, contest: string, id: string) {
const panel = vscode.window.createWebviewPanel(
"atcoderSubmission",
`提交 ${id} - ${contest}`,
vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(path.join(context.extensionPath, "dist")),
],
}
);

const webviewJsPath = vscode.Uri.file(
path.join(context.extensionPath, "dist", "webview.js")
);
const webviewJsSrc = panel.webview.asWebviewUri(webviewJsPath);

panel.webview.html = getWebviewContent(webviewJsSrc, "submission", contest, id);

const sendToWebview = (payload: Record<string, unknown>) => {
panel.webview.postMessage(payload);
};

panel.webview.onDidReceiveMessage(
(message: IncomingMessage) => { runCommand(message, context, sendToWebview); },
undefined,
context.subscriptions
);

context.subscriptions.push(panel);
}

export async function activate(context: vscode.ExtensionContext) {
log.info("Extension is now active!");

setStaleCookieHandler(() => {
// vscode.window.showWarningMessage(
// "检测到 AtCoder Cookie 可能已过期:已临时使用无 Cookie 访问公开页面。如需提交/报名等登录功能,请重新登录并更新 Cookie。"
// );
});

try {
const cookie = await context.secrets.get("atcoderCookie");
if (cookie) {
Expand All @@ -505,12 +390,6 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand("extension.showWebview", createShowWebview(context))
);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
AtCoderViewProvider.viewType,
new AtCoderViewProvider(context)
)
);
} catch (error) {
log.error("Failed to activate extension:", error);
}
Expand Down
24 changes: 0 additions & 24 deletions apps/vscode-extension/src/tools/SignUpContest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,6 @@ export async function fetchContest(contest: string): Promise<ContestPage> {
return { contest, title, url, signed, csrfToken, Rated };
}

function decodeAnnouncementEntities(text: string): string {
return text
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)))
.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)))
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&amp;/g, "&");
}

export async function fetchContestAnnouncement(contest: string): Promise<string> {
const url = `https://atcoder.jp/posts/${contest}_en`;
try {
let html = await fetchText(url);
html = html.replace(/<img[^>]*>/g, '');
const match = html.match(/class="panel-body blog-post"[^>]*>([\s\S]*?)<\/div>/i);
if (!match) return "";
return decodeAnnouncementEntities(match[1]).trim();
} catch {
return "";
}
}

export async function signedUpContest(contest: string, csrfToken: string, rated?: boolean): Promise<{ success: boolean; message: string }> {
const step1Url = `https://atcoder.jp/contests/${contest}/register`;
const step2Url = `https://atcoder.jp/contests/${contest}/rated_register`;
Expand Down
33 changes: 0 additions & 33 deletions apps/vscode-extension/src/tools/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,23 @@ import {
handleSubmitCode,
handleFetchSubHistory,
handleExportToCph,
handleFetchStandings,
handleGetContests,
handleFetchSubmissionDetail,
openContestPanel,
openSubmissionPanel,
} from "../extension";

const loadCommands = new Set(["loadContest", "loadProblem", "openBrowser"]);
const deeplCommands = new Set(["translate", "setApiKey"]);
const cookieCommands = new Set(["getCookie", "setCookie"]);
const problemCommands = new Set(["registerContest", "copyMarkdown", "alert", "sendCph"]);
const submitCommands = new Set(["fetchSubmitPage", "submitCode", "fetchSubmissionHistory", "fetchSubmissionDetail"]);
const contestCommands = new Set(["fetchStandings", "getContests", "openContest", "openSubmission"]);

export async function runCommand(message: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,) {
if (loadCommands.has(message.command!)) await runLoadCommand(message, sendToWebview);
else if (deeplCommands.has(message.command!)) await runDeepL(message, context, sendToWebview);
else if (cookieCommands.has(message.command!)) await runCookie(message, context, sendToWebview);
else if (problemCommands.has(message.command!)) await runProblem(message, context, sendToWebview);
else if (submitCommands.has(message.command!)) await runSubmit(message, context, sendToWebview);
else if (contestCommands.has(message.command!)) await runContest(message, context, sendToWebview);
else throw new Error("unknown command");
}

async function runContest(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
switch (command.command) {
case "fetchStandings":
if (!command.contest) return false;
await handleFetchStandings(command.contest, sendToWebview);
return true;
case "getContests":
await handleGetContests(sendToWebview);
return true;
case "openContest":
if (!command.contest) return false;
openContestPanel(context, command.contest);
return true;
case "openSubmission":
if (!command.contest || !command.id) return false;
openSubmissionPanel(context, command.contest, command.id);
return true;
default:
return false;
}
}

async function runSubmit(command: IncomingMessage, context: vscode.ExtensionContext, sendToWebview: (payload: Record<string, unknown>) => void,): Promise<boolean> {
switch (command.command) {
case "fetchSubmitPage":
Expand All @@ -72,10 +43,6 @@ async function runSubmit(command: IncomingMessage, context: vscode.ExtensionCont
if (!command.contest) return false;
await handleFetchSubHistory(command.contest, sendToWebview);
return true;
case "fetchSubmissionDetail":
if (!command.contest || !command.id) return false;
await handleFetchSubmissionDetail(command.contest, command.id, sendToWebview);
return true;
default:
return false;
}
Expand Down
Loading
Loading