Skip to content

源代码管理 API

Trae IDE 的源代码管理 (SCM) API 允许扩展集成版本控制系统,如 Git、SVN 等。

概述

SCM 功能包括:

  • 源代码管理提供者注册
  • 文件状态跟踪
  • 变更管理
  • 提交和推送操作
  • 分支管理
  • 冲突解决

主要接口

SourceControl

typescript
interface SourceControl {
  // 基本信息
  readonly id: string
  readonly label: string
  readonly rootUri?: Uri
  
  // 输入框
  readonly inputBox: SourceControlInputBox
  
  // 资源组
  readonly count?: number
  
  // 快速差异提供者
  quickDiffProvider?: QuickDiffProvider
  
  // 接受输入命令
  acceptInputCommand?: Command
  
  // 状态栏命令
  statusBarCommands?: Command[]
  
  // 操作方法
  createResourceGroup(id: string, label: string): SourceControlResourceGroup
  dispose(): void
}

SourceControlResourceGroup

typescript
interface SourceControlResourceGroup {
  readonly id: string
  label: string
  hideWhenEmpty?: boolean
  
  // 资源状态
  resourceStates: SourceControlResourceState[]
  
  // 销毁
  dispose(): void
}

SourceControlResourceState

typescript
interface SourceControlResourceState {
  readonly resourceUri: Uri
  readonly command?: Command
  readonly decorations?: SourceControlResourceDecorations
}

创建 SCM 提供者

基本创建

typescript
import { scm, SourceControl, SourceControlResourceGroup } from 'trae-api'

class GitSCMProvider {
  private sourceControl: SourceControl
  private changesGroup: SourceControlResourceGroup
  private stagedGroup: SourceControlResourceGroup
  
  constructor(workspaceRoot: Uri) {
    // 创建源代码管理实例
    this.sourceControl = scm.createSourceControl(
      'git',           // 唯一标识符
      'Git',           // 显示名称
      workspaceRoot    // 工作区根目录
    )
    
    // 创建资源组
    this.changesGroup = this.sourceControl.createResourceGroup(
      'workingTree',
      '更改'
    )
    
    this.stagedGroup = this.sourceControl.createResourceGroup(
      'index',
      '暂存的更改'
    )
    
    this.setupInputBox()
    this.setupCommands()
  }
  
  private setupInputBox() {
    // 设置提交消息输入框
    this.sourceControl.inputBox.placeholder = '提交消息'
    this.sourceControl.acceptInputCommand = {
      command: 'git.commit',
      title: '提交',
      arguments: [this.sourceControl]
    }
  }
  
  private setupCommands() {
    // 设置状态栏命令
    this.sourceControl.statusBarCommands = [
      {
        command: 'git.sync',
        title: '$(sync) 同步',
        tooltip: '拉取和推送更改'
      },
      {
        command: 'git.publish',
        title: '$(cloud-upload) 发布',
        tooltip: '发布分支'
      }
    ]
  }
}

高级配置

typescript
class AdvancedSCMProvider {
  private sourceControl: SourceControl
  
  constructor(workspaceRoot: Uri) {
    this.sourceControl = scm.createSourceControl('advanced-git', 'Advanced Git', workspaceRoot)
    
    // 设置快速差异提供者
    this.sourceControl.quickDiffProvider = {
      provideOriginalResource: (uri: Uri) => {
        // 返回原始文件内容的 URI
        return Uri.parse(`git:${uri.path}?HEAD`)
      }
    }
    
    // 设置计数
    this.sourceControl.count = 5 // 显示未提交更改数量
    
    this.setupResourceGroups()
  }
  
  private setupResourceGroups() {
    // 工作区更改
    const workingTreeGroup = this.sourceControl.createResourceGroup(
      'workingTree',
      '工作区更改'
    )
    workingTreeGroup.hideWhenEmpty = true
    
    // 暂存区更改
    const indexGroup = this.sourceControl.createResourceGroup(
      'index',
      '暂存区更改'
    )
    indexGroup.hideWhenEmpty = true
    
    // 合并冲突
    const mergeGroup = this.sourceControl.createResourceGroup(
      'merge',
      '合并冲突'
    )
    mergeGroup.hideWhenEmpty = true
  }
}

资源状态管理

文件状态跟踪

typescript
import { SourceControlResourceDecorations, ThemeColor } from 'trae-api'

class FileStatusTracker {
  private workingTreeGroup: SourceControlResourceGroup
  private indexGroup: SourceControlResourceGroup
  
  constructor(sourceControl: SourceControl) {
    this.workingTreeGroup = sourceControl.createResourceGroup('workingTree', '更改')
    this.indexGroup = sourceControl.createResourceGroup('index', '暂存的更改')
  }
  
  updateFileStatus(uri: Uri, status: GitFileStatus) {
    const decorations = this.getDecorations(status)
    const command = this.getCommand(uri, status)
    
    const resourceState: SourceControlResourceState = {
      resourceUri: uri,
      command: command,
      decorations: decorations
    }
    
    // 根据状态添加到相应的组
    if (status.staged) {
      this.addToGroup(this.indexGroup, resourceState)
    } else {
      this.addToGroup(this.workingTreeGroup, resourceState)
    }
  }
  
  private getDecorations(status: GitFileStatus): SourceControlResourceDecorations {
    const decorations: SourceControlResourceDecorations = {}
    
    switch (status.type) {
      case 'modified':
        decorations.iconPath = new ThemeIcon('diff-modified')
        decorations.tooltip = '已修改'
        decorations.faded = false
        break
        
      case 'added':
        decorations.iconPath = new ThemeIcon('diff-added')
        decorations.tooltip = '新增文件'
        decorations.faded = false
        break
        
      case 'deleted':
        decorations.iconPath = new ThemeIcon('diff-removed')
        decorations.tooltip = '已删除'
        decorations.faded = true
        break
        
      case 'renamed':
        decorations.iconPath = new ThemeIcon('diff-renamed')
        decorations.tooltip = `重命名: ${status.originalPath} → ${status.path}`
        break
        
      case 'untracked':
        decorations.iconPath = new ThemeIcon('diff-added')
        decorations.tooltip = '未跟踪的文件'
        decorations.faded = false
        break
        
      case 'conflicted':
        decorations.iconPath = new ThemeIcon('warning')
        decorations.tooltip = '合并冲突'
        decorations.faded = false
        break
    }
    
    return decorations
  }
  
  private getCommand(uri: Uri, status: GitFileStatus): Command {
    return {
      command: 'vscode.diff',
      title: '打开更改',
      arguments: [
        Uri.parse(`git:${uri.path}?HEAD`), // 原始版本
        uri,                               // 当前版本
        `${path.basename(uri.path)} (工作区)`
      ]
    }
  }
  
  private addToGroup(group: SourceControlResourceGroup, resource: SourceControlResourceState) {
    // 检查是否已存在
    const existing = group.resourceStates.find(r => r.resourceUri.toString() === resource.resourceUri.toString())
    
    if (existing) {
      // 更新现有资源
      const index = group.resourceStates.indexOf(existing)
      group.resourceStates[index] = resource
    } else {
      // 添加新资源
      group.resourceStates = [...group.resourceStates, resource]
    }
  }
}

interface GitFileStatus {
  type: 'modified' | 'added' | 'deleted' | 'renamed' | 'untracked' | 'conflicted'
  path: string
  originalPath?: string
  staged: boolean
}

批量状态更新

typescript
class BatchStatusUpdater {
  private sourceControl: SourceControl
  private updateTimer: NodeJS.Timeout | undefined
  private pendingUpdates = new Map<string, GitFileStatus>()
  
  constructor(sourceControl: SourceControl) {
    this.sourceControl = sourceControl
  }
  
  scheduleUpdate(uri: Uri, status: GitFileStatus) {
    // 收集待更新的状态
    this.pendingUpdates.set(uri.toString(), status)
    
    // 防抖处理
    if (this.updateTimer) {
      clearTimeout(this.updateTimer)
    }
    
    this.updateTimer = setTimeout(() => {
      this.performBatchUpdate()
    }, 100)
  }
  
  private performBatchUpdate() {
    const workingTreeResources: SourceControlResourceState[] = []
    const indexResources: SourceControlResourceState[] = []
    
    this.pendingUpdates.forEach((status, uriString) => {
      const uri = Uri.parse(uriString)
      const resource = this.createResourceState(uri, status)
      
      if (status.staged) {
        indexResources.push(resource)
      } else {
        workingTreeResources.push(resource)
      }
    })
    
    // 批量更新资源组
    const workingTreeGroup = this.sourceControl.createResourceGroup('workingTree', '更改')
    const indexGroup = this.sourceControl.createResourceGroup('index', '暂存的更改')
    
    workingTreeGroup.resourceStates = workingTreeResources
    indexGroup.resourceStates = indexResources
    
    // 更新计数
    this.sourceControl.count = workingTreeResources.length + indexResources.length
    
    // 清理
    this.pendingUpdates.clear()
    this.updateTimer = undefined
  }
  
  private createResourceState(uri: Uri, status: GitFileStatus): SourceControlResourceState {
    return {
      resourceUri: uri,
      command: {
        command: 'vscode.diff',
        title: '查看更改',
        arguments: [
          Uri.parse(`git:${uri.path}?HEAD`),
          uri,
          `${path.basename(uri.path)} (工作区)`
        ]
      },
      decorations: this.getDecorations(status)
    }
  }
  
  private getDecorations(status: GitFileStatus): SourceControlResourceDecorations {
    // 装饰逻辑(同上面的示例)
    return {}
  }
}

实用示例

Git 集成

typescript
class GitIntegration {
  private sourceControl: SourceControl
  private workingTreeGroup: SourceControlResourceGroup
  private indexGroup: SourceControlResourceGroup
  private disposables: Disposable[] = []
  
  constructor(workspaceRoot: Uri) {
    this.sourceControl = scm.createSourceControl('git', 'Git', workspaceRoot)
    this.setupGroups()
    this.setupCommands()
    this.setupEventListeners()
    this.refreshStatus()
  }
  
  private setupGroups() {
    this.workingTreeGroup = this.sourceControl.createResourceGroup(
      'workingTree',
      '更改'
    )
    
    this.indexGroup = this.sourceControl.createResourceGroup(
      'index',
      '暂存的更改'
    )
    
    // 设置输入框
    this.sourceControl.inputBox.placeholder = '消息 (按 Ctrl+Enter 提交)'
    this.sourceControl.acceptInputCommand = {
      command: 'git.commit',
      title: '提交',
      arguments: [this.sourceControl]
    }
  }
  
  private setupCommands() {
    // 注册命令
    this.disposables.push(
      commands.registerCommand('git.commit', async (sourceControl: SourceControl) => {
        await this.commit(sourceControl.inputBox.value)
      })
    )
    
    this.disposables.push(
      commands.registerCommand('git.stage', async (resource: SourceControlResourceState) => {
        await this.stageFile(resource.resourceUri)
      })
    )
    
    this.disposables.push(
      commands.registerCommand('git.unstage', async (resource: SourceControlResourceState) => {
        await this.unstageFile(resource.resourceUri)
      })
    )
    
    // 设置状态栏命令
    this.sourceControl.statusBarCommands = [
      {
        command: 'git.sync',
        title: '$(sync) 同步',
        tooltip: '拉取和推送更改'
      }
    ]
  }
  
  private setupEventListeners() {
    // 监听文件系统变化
    this.disposables.push(
      workspace.onDidSaveTextDocument(() => {
        this.refreshStatus()
      })
    )
    
    this.disposables.push(
      workspace.onDidCreateFiles(() => {
        this.refreshStatus()
      })
    )
    
    this.disposables.push(
      workspace.onDidDeleteFiles(() => {
        this.refreshStatus()
      })
    )
  }
  
  private async refreshStatus() {
    try {
      const status = await this.getGitStatus()
      this.updateResourceGroups(status)
    } catch (error) {
      console.error('刷新 Git 状态失败:', error)
    }
  }
  
  private async getGitStatus(): Promise<GitFileStatus[]> {
    // 执行 git status 命令
    const result = await this.executeGitCommand(['status', '--porcelain'])
    return this.parseGitStatus(result)
  }
  
  private parseGitStatus(output: string): GitFileStatus[] {
    const statuses: GitFileStatus[] = []
    const lines = output.split('\n').filter(line => line.trim())
    
    for (const line of lines) {
      const indexStatus = line[0]
      const workingTreeStatus = line[1]
      const filePath = line.substring(3)
      
      if (indexStatus !== ' ') {
        statuses.push({
          type: this.getStatusType(indexStatus),
          path: filePath,
          staged: true
        })
      }
      
      if (workingTreeStatus !== ' ') {
        statuses.push({
          type: this.getStatusType(workingTreeStatus),
          path: filePath,
          staged: false
        })
      }
    }
    
    return statuses
  }
  
  private getStatusType(status: string): GitFileStatus['type'] {
    switch (status) {
      case 'M': return 'modified'
      case 'A': return 'added'
      case 'D': return 'deleted'
      case 'R': return 'renamed'
      case '?': return 'untracked'
      case 'U': return 'conflicted'
      default: return 'modified'
    }
  }
  
  private updateResourceGroups(statuses: GitFileStatus[]) {
    const workingTreeResources: SourceControlResourceState[] = []
    const indexResources: SourceControlResourceState[] = []
    
    for (const status of statuses) {
      const uri = Uri.file(path.join(this.sourceControl.rootUri!.fsPath, status.path))
      const resource = this.createResourceState(uri, status)
      
      if (status.staged) {
        indexResources.push(resource)
      } else {
        workingTreeResources.push(resource)
      }
    }
    
    this.workingTreeGroup.resourceStates = workingTreeResources
    this.indexGroup.resourceStates = indexResources
    
    // 更新计数
    this.sourceControl.count = workingTreeResources.length + indexResources.length
  }
  
  private createResourceState(uri: Uri, status: GitFileStatus): SourceControlResourceState {
    const command = status.staged ? 'git.unstage' : 'git.stage'
    
    return {
      resourceUri: uri,
      command: {
        command: command,
        title: status.staged ? '取消暂存' : '暂存更改',
        arguments: [{ resourceUri: uri }]
      },
      decorations: this.getDecorations(status)
    }
  }
  
  private getDecorations(status: GitFileStatus): SourceControlResourceDecorations {
    const decorations: SourceControlResourceDecorations = {}
    
    switch (status.type) {
      case 'modified':
        decorations.iconPath = new ThemeIcon('diff-modified')
        decorations.tooltip = '已修改'
        break
      case 'added':
        decorations.iconPath = new ThemeIcon('diff-added')
        decorations.tooltip = '新增文件'
        break
      case 'deleted':
        decorations.iconPath = new ThemeIcon('diff-removed')
        decorations.tooltip = '已删除'
        decorations.faded = true
        break
      case 'untracked':
        decorations.iconPath = new ThemeIcon('diff-added')
        decorations.tooltip = '未跟踪的文件'
        break
    }
    
    return decorations
  }
  
  private async commit(message: string) {
    if (!message.trim()) {
      window.showErrorMessage('请输入提交消息')
      return
    }
    
    try {
      await this.executeGitCommand(['commit', '-m', message])
      this.sourceControl.inputBox.value = ''
      this.refreshStatus()
      window.showInformationMessage('提交成功')
    } catch (error) {
      window.showErrorMessage(`提交失败: ${error}`)
    }
  }
  
  private async stageFile(uri: Uri) {
    try {
      const relativePath = path.relative(this.sourceControl.rootUri!.fsPath, uri.fsPath)
      await this.executeGitCommand(['add', relativePath])
      this.refreshStatus()
    } catch (error) {
      window.showErrorMessage(`暂存文件失败: ${error}`)
    }
  }
  
  private async unstageFile(uri: Uri) {
    try {
      const relativePath = path.relative(this.sourceControl.rootUri!.fsPath, uri.fsPath)
      await this.executeGitCommand(['reset', 'HEAD', relativePath])
      this.refreshStatus()
    } catch (error) {
      window.showErrorMessage(`取消暂存失败: ${error}`)
    }
  }
  
  private async executeGitCommand(args: string[]): Promise<string> {
    // 执行 Git 命令的实现
    return new Promise((resolve, reject) => {
      const { spawn } = require('child_process')
      const git = spawn('git', args, {
        cwd: this.sourceControl.rootUri!.fsPath
      })
      
      let output = ''
      let error = ''
      
      git.stdout.on('data', (data: Buffer) => {
        output += data.toString()
      })
      
      git.stderr.on('data', (data: Buffer) => {
        error += data.toString()
      })
      
      git.on('close', (code: number) => {
        if (code === 0) {
          resolve(output)
        } else {
          reject(new Error(error))
        }
      })
    })
  }
  
  dispose() {
    this.disposables.forEach(d => d.dispose())
    this.sourceControl.dispose()
  }
}

快速差异提供者

实现快速差异

typescript
class GitQuickDiffProvider implements QuickDiffProvider {
  private sourceControl: SourceControl
  
  constructor(sourceControl: SourceControl) {
    this.sourceControl = sourceControl
    sourceControl.quickDiffProvider = this
  }
  
  provideOriginalResource(uri: Uri, token: CancellationToken): ProviderResult<Uri> {
    // 返回 HEAD 版本的文件内容
    return Uri.parse(`git:${uri.path}?HEAD`)
  }
}

// 注册内容提供者来处理 git: scheme
class GitContentProvider implements TextDocumentContentProvider {
  async provideTextDocumentContent(uri: Uri): Promise<string> {
    const query = new URLSearchParams(uri.query)
    const ref = query.get('ref') || 'HEAD'
    const filePath = uri.path
    
    try {
      // 获取指定版本的文件内容
      const content = await this.getFileContent(filePath, ref)
      return content
    } catch (error) {
      return ''
    }
  }
  
  private async getFileContent(filePath: string, ref: string): Promise<string> {
    // 执行 git show 命令获取文件内容
    const { spawn } = require('child_process')
    
    return new Promise((resolve, reject) => {
      const git = spawn('git', ['show', `${ref}:${filePath}`])
      
      let content = ''
      git.stdout.on('data', (data: Buffer) => {
        content += data.toString()
      })
      
      git.on('close', (code: number) => {
        if (code === 0) {
          resolve(content)
        } else {
          reject(new Error('文件不存在或无法访问'))
        }
      })
    })
  }
}

最佳实践

  1. 性能优化: 使用防抖机制避免频繁的状态更新
  2. 用户体验: 提供清晰的文件状态图标和工具提示
  3. 错误处理: 妥善处理 Git 命令执行失败的情况
  4. 资源管理: 及时清理事件监听器和定时器
  5. 扩展性: 设计可扩展的架构支持多种版本控制系统

相关 API

您的终极 AI 驱动 IDE 学习指南