TheUnknownBlog

Back

Constantly switching between the editor (VS Code) and browser was incredibly tedious. Looking at problem descriptions, examples, and outputs in the browser, then comparing results, copying code to VS Code, writing and debugging, copying back to browser for submission, and finally switching back to browser to check results… Although I could use split screen, the Stage Manager experience on macOS wasn’t great. This process not only interrupted my thought flow but was also inefficient.

Could I complete all these operations within VSCode? Seeing classmates in my class developing plugins, it didn’t seem that difficult. Having recently learned Golang, TypeScript didn’t seem too hard to learn either 😋 With this idea in mind, I created my first VSCode extension development journey, aiming to create a convenient assistant for ACMOJ. This article documents the process from conception to implementation, through pitfalls to the final working product.

Getting Started#

VS Code extensions are primarily written in TypeScript (or JavaScript) and run in a Node.js environment. Before starting, the essential tools are:

Node.js & npm/yarn serve as the basic runtime environment and package manager. Yeoman & generator-code are the official scaffolding tools recommended by VS Code for quickly generating project structure. Simply run npm install -g yo generator-code followed by yo code and select TypeScript Extension. VS Code itself is needed for developing and debugging the plugin.

The generated project structure is clear and straightforward. The src/extension.ts file serves as the plugin’s entry point, containing activate (called when activated) and deactivate (called when deactivated) functions. The package.json file is the core manifest file, defining the plugin’s metadata, contributions (such as commands, views, configurations), and activation events (determining when to load the plugin). The tsconfig.json file contains TypeScript configuration.

My initial blueprint was to implement these core features:

Authentication: Connect to the ACMOJ API. Problem/Assignment Browsing: View problem lists in VS Code’s sidebar. Problem Details: Display problem descriptions, examples, etc. in Webview. Code Submission: Quickly submit code from the current editor. Result Viewing: View submission status and results in sidebar or Webview.

API Interaction and Authentication#

ACMOJ provides an OpenAPI-compliant API, which forms the foundation for implementing functionality.

API Client Setup

I chose axios as the HTTP request library and encapsulated an ApiClient class to uniformly handle request sending, Base URL configuration, and error handling. The key was setting up request interceptors to automatically attach Bearer <token> in the Authorization Header.

Authentication “Episode” - OAuth vs PAT

The API documentation mentioned both OAuth2 (Authorization Code Flow) and Personal Access Token (PAT) authentication methods.

Initially, I tried implementing the OAuth2 flow. This involved directing users to browser authorization, then starting a temporary HTTP server locally to listen for callback URIs to obtain the code, then using the code and client_secret to exchange for an access_token. While this flow is standard for applications requiring multi-user authorization, it’s quite complex to implement, especially handling client_secret and local callbacks securely in a VS Code extension environment. (Actually, what stopped me initially was needing a client secret from the admin team. At that time, I didn’t know anyone on the admin team, though they seem to know me now after developing this plugin XD)

Considering that target users (mainly myself and classmates) could easily generate PATs on the ACMOJ website, I decided to switch to the simpler PAT authentication. This greatly simplified the flow: create an AuthService (or TokenManager), provide an acmoj.setToken command using vscode.window.showInputBox({ password: true }) to prompt users for PAT input, use VS Code’s SecretStorage API (context.secrets.store / context.secrets.get) to securely store and read PATs, provide an acmoj.clearToken command to clear stored PATs, directly get stored PATs from AuthService in ApiClient’s request interceptor to add to request headers, and in response interceptor, if encountering 401 Unauthorized errors, call AuthService methods to clear invalid tokens and prompt users to reset.

Building User Interface with TreeView and Webview#

To display information and provide interaction in VS Code, I mainly used TreeView and Webview.

TreeView (Sidebar)

I used the vscode.TreeDataProvider interface to create two views for the Activity Bar:

Problemsets (Contests/Assignments): Initially, I simply listed all problems but quickly found the information overwhelming. I improved it to display Problemsets that users joined. Further improvement involved categorizing Problemsets into “Ongoing”, “Upcoming”, and “Passed” top-level nodes based on their start/end times. This required fetching all Problemsets, then filtering and sorting them in the getChildren method based on current time and category nodes. I used two custom TreeItem types: CategoryTreeItem and ProblemsetTreeItem. Each Problemset node was set as expandable (vscode.TreeItemCollapsibleState.Collapsed), loading its contained problem list (ProblemBriefTreeItem) when clicked.

Submissions (Submission Records): This displays the user’s submission list, including ID, problem, status, language, time, etc. I set different icons (ThemeIcon) for different submission statuses (AC, WA, TLE, RE…) to make them more intuitive.

The key to implementing TreeView lies in the getChildren (get child nodes) and getTreeItem (define node appearance and behavior) methods. Through EventEmitter and onDidChangeTreeData events, you can notify VS Code to refresh the view.

Webview (Detail Display)

When users click on problems or submission records in TreeView, I use vscode.window.createWebviewPanel to create a Webview for displaying detailed information. Why use webview? Because I needed to render TeX formulas, and JSON requests returned Markdown results.

Content Rendering: Webview is essentially an embedded browser environment with HTML content. I used the markdown-it library to convert Markdown-formatted problem descriptions, input/output formats, etc. obtained from the API into HTML.

Challenge: Mathematical Formula Rendering: OJ problem descriptions often contain LaTeX formulas.

Attempt One (Failed): Initially, I tried including KaTeX JS library and auto-render script in the Webview HTML for client-side rendering. However, this caused the strange issue of formulas being rendered twice (once as original text, once as KaTeX rendered result).

Attempt Two (Success): I realized the problem was in the duplicate rendering flow. The final solution was using markdown-it’s KaTeX plugin (@vscode/markdown-it-katex - this package had another developer’s version when installing via npm, which was outdated and had security risks, but the good news is that VS Code officially noticed this project and made subsequent fixes, so I used this one). When using md.render() on the extension side (Node.js environment), this plugin directly converts LaTeX in Markdown ($...$, $$...$$) to final KaTeX HTML structure. This way, the HTML sent to Webview is already pre-rendered, and the Webview side only needs to include KaTeX CSS (katex.min.css) to display styles correctly, no longer needing KaTeX JS and auto-render scripts.

Commands and Status Bar

I used vscode.commands.registerCommand to register various user operations (set Token, refresh views, submit code, view problems by ID, etc.). I used vscode.window.createStatusBarItem to display current login status and username on the left side of the status bar, which can trigger corresponding commands (like showing user info or setting Token) when clicked.

Packaging and Publishing#

Everything worked smoothly during development and debugging (F5), but when I used vsce package to package into a VSIX file and installed it on another computer, I encountered the classic problem: Command 'acmoj.setToken' not found or Cannot find module 'axios'.

Debugging Process

I checked the developer tools by opening VS Code developer tools (Developer: Toggle Developer Tools) Console on the test computer. I found that activating the extension directly reported error Cannot find module 'axios'. I checked VSIX contents using vsce ls command (or renaming .vsix to .zip and extracting) to view package contents. I discovered that the node_modules folder wasn’t packaged at all!

Root Cause

I mistakenly placed runtime-required libraries (like axios, markdown-it, katex, @vscode/markdown-it-katex) under devDependencies instead of dependencies in package.json.

Dependencies are libraries required for extension runtime and will be packaged by vsce package. DevDependencies are libraries used during development (compilers, type definitions, linters, packaging tools, etc.) and will not be packaged.

Solution

I carefully checked package.json and moved all runtime dependencies (axios, etc.) to the dependencies section, while keeping development tools (typescript, @types/*, eslint, @vscode/vsce, etc.) in devDependencies.

{
    "dependencies": {
        "@vscode/markdown-it-katex": "...",
        "axios": "...",
        "katex": "...",
        "markdown-it": "..."
    },
    "devDependencies": {
        "@types/vscode": "...",
        "@types/node": "...",
        "@types/markdown-it": "...",
        "@vscode/vsce": "...", // The packaging tool itself is a dev dependency
        "typescript": "...",
        "eslint": "..."
    }
}
json

Key Step: After modifying package.json, it’s essential to perform “clean & reinstall” - I continued getting errors initially because I didn’t clear node_modules and package-lock.json.

This time, the generated VSIX file finally contained the correct node_modules, and after installation, commands could be found normally and the extension activated successfully.

TypeScript Interlude#

As a TypeScript project, I also encountered some typical type issues:

Module/Type Not Found: Cannot find module 'vscode' or other @types packages, usually resolved by npm install --save-dev @types/vscode @types/node ...

Implicit any: After enabling strict mode, I needed to explicitly add types for callback function parameters (like progress in withProgress, text in validateInput).

API Signature Mismatch: When calling vscode.window.showQuickPick, if providing option objects, you need to pass QuickPickItem[] instead of string[], requiring mapping.

Is This the End?#

While acmoj-helper can already run and has helped me considerably in daily use, during the development process, I gradually felt some “growing pains.” As features iterated (even with minor adjustments), I found the code becoming somewhat messy:

Unclear Responsibilities: The commands.ts file not only handled command registration but also contained substantial complex business logic implementations like submitCurrentFile. This made the file abnormally bloated, making modifications affect the entire system.

High Coupling: Modifying one module (like cache.ts handling API caching) might unexpectedly affect views (submissionProvider.ts) or command handling. When I mentioned rewriting submissionProvider earlier, that was a typical example - the view layer was too tightly coupled with data fetching and business logic.

Registration Chaos: Command registration was scattered across extension.ts and commands.ts, lacking centralization and clarity.

Extension Difficulties: If I wanted to add new features like “Contest” view or more complex problem filtering logic, it would be extremely painful under the existing structure, requiring careful navigation through various files to ensure existing functionality wasn’t broken.

Testing Obstacles: Code mixing UI logic, API calls, and business processing was very difficult to unit test.

These issues made me realize that while the current architecture works, it’s not “elegant” and lacks long-term viability. To ensure this project can develop healthily and to improve my own code design skills, I decided to conduct a thorough refactoring.

Refactoring Goals: Decoupling, Layering, Single Responsibility

The new architecture I’m currently working on is roughly divided into these layers:

VS Code Integration Layer (extension.ts, src/commands/index.ts)

Service Layer (src/services/) - Responsible for encapsulating core business logic and interactions with external resources (like APIs, caching). Each service corresponds to a clear domain.

Command Handling Layer (src/commands/) - Command handlers receive calls from VS Code and then use the service layer to complete specific tasks. They serve as bridges between VS Code commands and business logic. Complex logic (like submitCurrentFile) is now clearly encapsulated in corresponding command handlers.

UI Layer (src/views/, src/webviews/) - Responsible for data display and UI interaction. The views/ directory contains TreeDataProviders (like ProblemsetProvider, SubmissionProvider) that get data from the service layer and format it into structures needed by VS Code TreeView. The webviews/ directory contains Webview Panel logic. After refactoring, I created dedicated classes for problem details and submission details (ProblemDetailPanel, SubmissionDetailPanel), encapsulating their respective HTML generation, message handling, and lifecycle management. They also get data through the service layer, and Webview operations (like “copy code”) now typically send messages to VS Code via postMessage, responded to by corresponding command handlers.

Core/Data Layer (src/core/, src/types.ts) - Provides the most basic components and definitions. A typical example during refactoring was core/apiClient.ts: a purer HTTP client only responsible for sending requests, handling authentication headers, retry logic, and basic error interpretation. It no longer contains specific business endpoint logic. Previously, getUserProfile, getSubmission, etc. were all in there.

While the refactoring process was quite challenging and temporarily introduced new bugs, it laid a solid foundation for ACMOJ Helper’s long-term development. Now I can more confidently implement those more comprehensive features I envisioned at the end of version 1.0.

If you’re also interested in VSCode extension development or want to build integrations for tools or platforms you frequently use, don’t hesitate - just start doing it! Begin with yo code, encounter problems, solve problems - this process itself is the best learning experience.

Project Repository: TheUnknownThing/vscode-acmoj

Thanks for reading! I hope my experience can be helpful to you.

My First VSCode Extension - ACMOJ Helper from Scratch
https://theunknown.site/blog/vscode-extension
Author TheUnknownThing
Published at April 6, 2025
Comment seems to stuck. Try to refresh?✨
浙ICP备2025146421号-2 浙公网安备33010502012191号