Mastering VS Code Extension Development: From Electron Basics to Custom Plugins
This article explains how VS Code extensions work by detailing Electron’s architecture, the multi‑process model, and the plugin loading mechanism, then walks through building a custom extension that adds a right‑click command to generate a Lynx component scaffold, complete with code snippets and configuration steps.
Big Company Tech: Weekly Updates, Selected Good Articles
Background
As a front‑end developer you are familiar with VS Code and its myriad of personal configurations, which are made possible by its rich plugin ecosystem. This article explains the basic principles of plugins and demonstrates how to create a simple VS Code extension.
What Makes VS Code
Electron
VS Code is built on Electron, whose core components are Chromium, Node.js, and native‑API.
Chromium (UI view) : Implements the UI using web technologies; it is an open‑source version of Chrome that runs without installation.
Node.js (desktop file system operations): Compiled via node‑gyp, used for file system access and network calls.
Native‑API (OS‑level API): Uses Node.js C++ Addons to call operating‑system APIs, extending Node.js capabilities.
Electron multi‑process :
Main process: Each Electron app launches a single main process.
Render process: The main process creates multiple web pages, each running in its own render process.
Example:
// main process
const { ipcMain } = require('electron');
ipcMain.on('main_msg', (event, arg) => {
console.log(arg); // ping
event.reply('renderer-msg-reply', 'pong');
}); // render process (child process)
const { ipcRenderer } = require('electron');
ipcRenderer.on('renderer-msg-reply', (event, arg) => {
console.log(arg); // pong
});
ipcRenderer.send('main_msg', 'ping');VS Code also runs other processes such as Extension (plugin) processes, Debug process, Search process, etc.
In simple terms, Electron provides a Chrome‑like shell with added desktop file access.
VS Code Plugin Loading Basics
https://github.com/microsoft/vscode/tree/main
Plugin Structure
├── extensions----------------------------------vscode built‑in plugins
├── src
│ ├── main.js--------------------------------entry file
│ ├── bootstrap-fork.js----------------------derived render process
│ ├── vs
│ │ └── workbench-------------------------workbench
│ │ ├── base
│ │ │ ├── browser----------------------browser API
│ │ │ ├── common-----------------------common JS
│ │ │ ├── node-------------------------node API
│ │ │ ├── code
│ │ │ │ ├── electron-browser---------electron render process
│ │ │ │ ├── electron-main------------electron main processPlugin Loading Process
Initialize Plugin Service
In the plugin’s constructor, the _initialize method starts the plugin service.
// src/vs/workbench/services/extensions/electron-browser/extensionService.ts
export class ExtensionService extends AbstractExtensionService implements IExtensionService {
constructor() {
this._lifecycleService.when(LifecyclePhase.Ready).then(() => {
runWhenIdle(() => {
this._initialize(); // initialize plugin service
}, 50);
});
}
}
// src/vs/workbench/services/extensions/common/abstractExtensionService.ts
protected async _initialize(): Promise<void> {
perf.mark('code/willLoadExtensions');
this._startExtensionHosts(true, []);
// ...
}
private _startExtensionHosts(isInitialStart: boolean, initialActivationEvents: string[]): void {
const extensionHosts = this._createExtensionHosts(isInitialStart);
extensionHosts.forEach((extensionHost) => {
const processManager: IExtensionHostManager = createExtensionHostManager(this._instantiationService, extensionHost, isInitialStart, initialActivationEvents, this._acquireInternalAPI());
processManager.onDidExit(([code, signal]) => this._onExtensionHostCrashOrExit(processManager, code, signal));
processManager.onDidChangeResponsiveState((responsiveState) => { this._onDidChangeResponsiveChange.fire({ isResponsive: responsiveState === ResponsiveState.Responsive }); });
this._extensionHostManagers.push(processManager);
});
}Fork Render Process
VS Code forks a render process for each extension to isolate it, ensuring stability and startup performance.
// src/vs/workbench/services/extensions/common/extensionHostManager.ts
class ExtensionHostManager extends Disposable {
constructor() {
this._proxy = this._extensionHost.start()!.then();
}
} // src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts
class LocalProcessExtensionHost implements IExtensionHost {
public start(): Promise<IMessagePassingProtocol> | null {
const opts = {
env: objects.mixin(objects.deepClone(process.env), {
AMD_ENTRYPOINT: 'vs/workbench/services/extensions/node/extensionHostProcess'
})
};
this._extensionHostProcess = fork(getPathFromAmdModule(require, 'bootstrap-fork'), ['--type=extensionHost'], opts);
}
}Initialize Plugin Activation Logic
// src/vs/workbench/services/extensions/node/extensionHostProcess.ts
import { startExtensionHostProcess } from "vs/workbench/services/extensions/node/extensionHostProcessSetup";
startExtensionHostProcess().catch(err => console.log(err));
// src/vs/workbench/services/extensions/node/extensionHostProcessSetup.ts
export async function startExtensionHostProcess(): Promise<void> {
const extensionHostMain = new ExtensionHostMain(renderer.protocol, initData, hostUtils, uriTransformer);
}
// src/vs/workbench/services/extensions/common/extensionHostMain.ts
export class ExtensionHostMain {
constructor() {
this._extensionService = instaService.invokeFunction(accessor => accessor.get(IExtHostExtensionService));
this._extensionService.initialize();
}
}Plugin Activation
// src/vs/workbench/api/node/extHost.services.ts
import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionService';
registerSingleton(IExtHostExtensionService, ExtHostExtensionService); // src/vs/workbench/api/node/extHostExtensionService.ts
export class ExtHostExtensionService extends AbstractExtHostExtensionService {
// ...
} // src/vs/workbench/api/common/extHostExtensionService.ts
abstract class AbstractExtHostExtensionService extends Disposable {
constructor() {
this._activator = this._register(new ExtensionsActivator());
}
private _activateByEvent(activationEvent: string, startup: boolean): Promise<void> {
return this._activator.activateByEvent(activationEvent, startup);
}
}Event List
// onLanguage
// onCommand
// onDebug
// workspaceContains
// onFileSystem
// onView
// onUri
// onWebviewPanel
// onCustomEditor
// onAuthenticationRequest
// onStartupFinished
// *Using onCommand Example
Register the command and activation event in package.json, then implement the logic in extension.ts.
Simple Practice
Goal: Create a VS Code extension that adds a right‑click menu to a selected folder and generates a basic Lynx component directory structure.
Environment Preparation
Node.js
VS Code
Install Yeoman and the VS Code Extension Generator: npm install -g yo generator-code Initialize the project:
yo codeImplementation
// package.json
{
"name": "lynxlowcode",
"displayName": "LynxLowcode",
"description": "",
"version": "0.0.2",
"engines": { "vscode": "^1.62.0" },
"categories": ["Other"],
"activationEvents": ["onCommand:lynxlowcode.newLynxComponent"],
"main": "./out/extension.js",
"contributes": {
"commands": [{ "command": "lynxlowcode.newLynxComponent", "title": "新建Lynx组件" }],
"menus": { "explorer/context": [{ "command": "lynxlowcode.newLynxComponent", "group": "z_commands", "when": "explorerResourceIsFolder" }] }
},
"scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", "pretest": "npm run compile && npm run lint", "lint": "eslint src --ext ts", "test": "node ./out/test/runTest.js" },
"devDependencies": { /* omitted for brevity */ },
"dependencies": { "import": "^0.0.6", "path": "^0.12.7" }
}The activationEvents field uses the special * type to trigger the extension after initialization.
// extension.ts
import * as vscode from 'vscode';
import { openInputBox } from './openInputBox';
export function activate(context: vscode.ExtensionContext) {
let newLynxComponent = vscode.commands.registerCommand('lynxlowcode.newLynxComponent', (file: vscode.Uri) => {
openInputBox(file);
});
context.subscriptions.push(newLynxComponent);
}
export function deactivate() {} // openInputBox.ts
import { window, InputBoxOptions, InputBox, Uri } from 'vscode';
import { pathExists } from 'fs-extra';
import { join } from 'path';
import { createTemplate } from './createTemplate';
export const openInputBox = (file: Uri): void => {
const inputBox = window.createInputBox();
inputBox.placeholder = '请输入你的组件名称,按Enter确认';
inputBox.onDidChangeValue(async (value: string) => {
if (value.length < 1) {
return '组件名称不能为空!!!';
}
const location = join(file.fsPath, value);
if (await pathExists(location)) {
return `该 ${location} 路径已经存在,请换一个名称或路径!!!`;
}
});
inputBox.onDidHide(() => {
inputBox.value = '';
inputBox.enabled = true;
inputBox.busy = false;
});
inputBox.onDidAccept(async () => {
inputBox.enabled = false;
inputBox.busy = true;
const result = createTemplate();
if (result) {
inputBox.hide();
window.showInformationMessage('创建成功成功,请查看!!!');
} else {
window.showInformationMessage('创建失败,请重试!!!');
}
inputBox.enabled = true;
inputBox.busy = false;
});
inputBox.show();
}; // createTemplate.ts
import fs from 'fs';
export const createTemplate = (location: string, name: string) => {
const mkdirResult = fs.mkdirSync(location, { recursive: true });
if (!mkdirResult) { return false; }
try {
fs.writeFileSync(`${location}/index.tsx`, `
import { Component } from '@byted-lynx/react-runtime';
import './index.scss';
interface ${name}PropsType {}
interface ${name}StateType {}
export default class ${name} extends Component<${name}PropsType, ${name}StateType> {
constructor(props: ${name}PropsType) {
super(props);
this.state = {};
}
render(): JSX.IntrinsicElements {
return (
<view>
<text>${name}组件</text>
</view>
);
}
}
`);
fs.writeFileSync(`${location}/index.scss`, '');
return true;
} catch (e) {
console.log(e);
return false;
}
};Optimization Points
Add more template types.
Download templates instead of writing string literals.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
