Skip to content

VSCode 核心 API

Workspace、TreeView、Webview 与 LSP

目录


Workspace API

文档操作

typescript
// 获取当前文档
const document = vscode.window.activeTextEditor?.document;

// 打开文档
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);

// 创建新文档
const newDoc = await vscode.workspace.openTextDocument({
  content: 'Hello World',
  language: 'javascript'
});

// 读取文档内容
const text = document.getText();
const line = document.lineAt(0).text;
const range = document.getText(new vscode.Range(0, 0, 10, 0));

编辑操作

typescript
const editor = vscode.window.activeTextEditor;

if (editor) {
  await editor.edit((editBuilder) => {
    // 插入
    editBuilder.insert(new vscode.Position(0, 0), 'Header\n');
    
    // 替换
    editBuilder.replace(
      new vscode.Range(1, 0, 1, 10),
      'New content'
    );
    
    // 删除
    editBuilder.delete(new vscode.Range(5, 0, 6, 0));
  });
}

文件系统

typescript
// 查找文件
const files = await vscode.workspace.findFiles(
  '**/*.ts',           // 包含
  '**/node_modules/**' // 排除
);

// 读写文件
const uri = vscode.Uri.file('/path/to/file.txt');
const content = await vscode.workspace.fs.readFile(uri);
await vscode.workspace.fs.writeFile(uri, Buffer.from('content'));

// 监听文件变化
const watcher = vscode.workspace.createFileSystemWatcher('**/*.json');

watcher.onDidCreate(uri => console.log('Created:', uri.path));
watcher.onDidChange(uri => console.log('Changed:', uri.path));
watcher.onDidDelete(uri => console.log('Deleted:', uri.path));

context.subscriptions.push(watcher);

工作区事件

typescript
// 文档变化
vscode.workspace.onDidChangeTextDocument((e) => {
  console.log('Document changed:', e.document.fileName);
  e.contentChanges.forEach(change => {
    console.log('Change:', change.text, change.range);
  });
});

// 文档保存
vscode.workspace.onDidSaveTextDocument((doc) => {
  console.log('Saved:', doc.fileName);
});

// 编辑器切换
vscode.window.onDidChangeActiveTextEditor((editor) => {
  console.log('Active editor:', editor?.document.fileName);
});

TreeView

定义视图

json
// package.json
{
  "contributes": {
    "viewsContainers": {
      "activitybar": [
        {
          "id": "myExtensionContainer",
          "title": "My Extension",
          "icon": "resources/icon.svg"
        }
      ]
    },
    "views": {
      "myExtensionContainer": [
        {
          "id": "myTreeView",
          "name": "My Tree View",
          "icon": "resources/tree.svg"
        }
      ]
    }
  }
}

实现 TreeDataProvider

typescript
interface TreeItem {
  id: string;
  label: string;
  children?: TreeItem[];
}

class MyTreeDataProvider implements vscode.TreeDataProvider<TreeItem> {
  private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
  readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

  private data: TreeItem[] = [
    {
      id: '1',
      label: 'Item 1',
      children: [
        { id: '1-1', label: 'Child 1' },
        { id: '1-2', label: 'Child 2' }
      ]
    },
    { id: '2', label: 'Item 2' }
  ];

  getTreeItem(element: TreeItem): vscode.TreeItem {
    const treeItem = new vscode.TreeItem(
      element.label,
      element.children
        ? vscode.TreeItemCollapsibleState.Collapsed
        : vscode.TreeItemCollapsibleState.None
    );
    
    treeItem.id = element.id;
    treeItem.contextValue = 'myItem';
    treeItem.iconPath = new vscode.ThemeIcon('file');
    treeItem.command = {
      command: 'myExtension.selectItem',
      title: 'Select Item',
      arguments: [element]
    };
    
    return treeItem;
  }

  getChildren(element?: TreeItem): TreeItem[] {
    if (!element) {
      return this.data;
    }
    return element.children || [];
  }

  refresh(): void {
    this._onDidChangeTreeData.fire(undefined);
  }
}

// 注册
const treeDataProvider = new MyTreeDataProvider();
vscode.window.registerTreeDataProvider('myTreeView', treeDataProvider);

// 或使用 createTreeView(更多控制)
const treeView = vscode.window.createTreeView('myTreeView', {
  treeDataProvider,
  showCollapseAll: true,
  canSelectMany: true
});

Webview

创建 Webview Panel

typescript
function createWebviewPanel(context: vscode.ExtensionContext) {
  const panel = vscode.window.createWebviewPanel(
    'myWebview',         // 类型标识
    'My Webview',        // 标题
    vscode.ViewColumn.One,
    {
      enableScripts: true,
      retainContextWhenHidden: true,
      localResourceRoots: [
        vscode.Uri.joinPath(context.extensionUri, 'media')
      ]
    }
  );

  panel.webview.html = getWebviewContent(panel.webview, context.extensionUri);

  return panel;
}

function getWebviewContent(webview: vscode.Webview, extensionUri: vscode.Uri): string {
  const scriptUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'media', 'main.js')
  );
  const styleUri = webview.asWebviewUri(
    vscode.Uri.joinPath(extensionUri, 'media', 'style.css')
  );

  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src ${webview.cspSource};">
  <link href="${styleUri}" rel="stylesheet">
</head>
<body>
  <div id="app"></div>
  <script src="${scriptUri}"></script>
</body>
</html>`;
}

Webview 通信

typescript
// 插件 → Webview
panel.webview.postMessage({ type: 'update', data: { count: 10 } });

// Webview → 插件
panel.webview.onDidReceiveMessage(
  (message) => {
    switch (message.type) {
      case 'alert':
        vscode.window.showInformationMessage(message.text);
        break;
      case 'save':
        saveData(message.data);
        break;
    }
  },
  undefined,
  context.subscriptions
);

// Webview 端 (main.js)
const vscode = acquireVsCodeApi();

// 发送消息
vscode.postMessage({ type: 'alert', text: 'Hello!' });

// 接收消息
window.addEventListener('message', (event) => {
  const message = event.data;
  if (message.type === 'update') {
    updateUI(message.data);
  }
});

// 状态持久化
vscode.setState({ count: 10 });
const state = vscode.getState();

Language Server Protocol

概述

┌─────────────────┐         LSP          ┌─────────────────┐
│   VSCode        │ ◄──────────────────► │  Language       │
│   (Client)      │    JSON-RPC          │  Server         │
│                 │                      │                 │
│  • UI 展示      │                      │ • 语言分析      │
│  • 用户交互     │                      │ • 代码补全      │
│                 │                      │ • 诊断信息      │
└─────────────────┘                      └─────────────────┘

客户端配置

typescript
import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';

export function activate(context: vscode.ExtensionContext) {
  const serverModule = context.asAbsolutePath(
    path.join('server', 'out', 'server.js')
  );

  const serverOptions: ServerOptions = {
    run: { module: serverModule, transport: TransportKind.ipc },
    debug: { module: serverModule, transport: TransportKind.ipc }
  };

  const clientOptions: LanguageClientOptions = {
    documentSelector: [{ scheme: 'file', language: 'myLanguage' }],
    synchronize: {
      fileEvents: vscode.workspace.createFileSystemWatcher('**/.myrc')
    }
  };

  const client = new LanguageClient(
    'myLanguageServer',
    'My Language Server',
    serverOptions,
    clientOptions
  );

  client.start();
  context.subscriptions.push(client);
}

服务端示例

typescript
import {
  createConnection,
  TextDocuments,
  ProposedFeatures,
  InitializeResult,
  TextDocumentSyncKind,
  CompletionItem,
  CompletionItemKind
} from 'vscode-languageserver/node';

import { TextDocument } from 'vscode-languageserver-textdocument';

const connection = createConnection(ProposedFeatures.all);
const documents = new TextDocuments(TextDocument);

connection.onInitialize((): InitializeResult => {
  return {
    capabilities: {
      textDocumentSync: TextDocumentSyncKind.Incremental,
      completionProvider: { resolveProvider: true }
    }
  };
});

connection.onCompletion((): CompletionItem[] => {
  return [
    { label: 'TypeScript', kind: CompletionItemKind.Text },
    { label: 'JavaScript', kind: CompletionItemKind.Text }
  ];
});

documents.listen(connection);
connection.listen();

高频面试题

Q1: TreeView 如何实现数据刷新?

typescript
// 使用 EventEmitter 通知变化
private _onDidChangeTreeData = new vscode.EventEmitter<T | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;

refresh(item?: T): void {
  this._onDidChangeTreeData.fire(item);  // 传 undefined 刷新整棵树
}

Q2: Webview 安全注意事项?

  1. CSP:设置 Content-Security-Policy
  2. localResourceRoots:限制可访问的本地资源
  3. 禁用远程脚本:不加载外部 JS
  4. postMessage 验证:验证消息来源

Q3: LSP vs 直接实现的优势?

特性LSP直接实现
复用性可用于多个编辑器仅限 VSCode
性能独立进程,不阻塞 UI在主进程运行
复杂度需要额外服务端较简单

Q4: 如何在 Webview 中使用 React?

typescript
// 1. 构建 React 应用到 media 目录
// 2. 加载构建产物

function getWebviewContent(webview, extensionUri) {
  const scriptUri = webview.asWebviewUri(
    Uri.joinPath(extensionUri, 'media', 'app.js')
  );
  
  return `<!DOCTYPE html>
  <html>
  <head>
    <meta http-equiv="Content-Security-Policy" 
          content="script-src ${webview.cspSource};">
  </head>
  <body>
    <div id="root"></div>
    <script src="${scriptUri}"></script>
  </body>
  </html>`;
}

前端面试知识库