summaryrefslogtreecommitdiff
path: root/packages/core/src/utils/editor.ts
blob: 4d09e3a93ba273fe0d37774139785cf783404814 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { execSync, spawn } from 'child_process';

export type EditorType = 'vscode' | 'windsurf' | 'cursor' | 'vim';

interface DiffCommand {
  command: string;
  args: string[];
}

function commandExists(cmd: string): boolean {
  try {
    execSync(
      process.platform === 'win32' ? `where.exe ${cmd}` : `command -v ${cmd}`,
      { stdio: 'ignore' },
    );
    return true;
  } catch {
    return false;
  }
}

const editorCommands: Record<EditorType, { win32: string; default: string }> = {
  vscode: { win32: 'code.cmd', default: 'code' },
  windsurf: { win32: 'windsurf', default: 'windsurf' },
  cursor: { win32: 'cursor', default: 'cursor' },
  vim: { win32: 'vim', default: 'vim' },
};

export function checkHasEditor(editor: EditorType): boolean {
  const commandConfig = editorCommands[editor];
  const command =
    process.platform === 'win32' ? commandConfig.win32 : commandConfig.default;
  return commandExists(command);
}

/**
 * Get the diff command for a specific editor.
 */
export function getDiffCommand(
  oldPath: string,
  newPath: string,
  editor: EditorType,
): DiffCommand | null {
  switch (editor) {
    case 'vscode':
      return {
        command: 'code',
        args: ['--wait', '--diff', oldPath, newPath],
      };
    case 'windsurf':
      return {
        command: 'windsurf',
        args: ['--wait', '--diff', oldPath, newPath],
      };
    case 'cursor':
      return {
        command: 'cursor',
        args: ['--wait', '--diff', oldPath, newPath],
      };
    case 'vim':
      return {
        command: 'vim',
        args: [
          '-d',
          // skip viminfo file to avoid E138 errors
          '-i',
          'NONE',
          // make the left window read-only and the right window editable
          '-c',
          'wincmd h | set readonly | wincmd l',
          // set up colors for diffs
          '-c',
          'highlight DiffAdd cterm=bold ctermbg=22 guibg=#005f00 | highlight DiffChange cterm=bold ctermbg=24 guibg=#005f87 | highlight DiffText ctermbg=21 guibg=#0000af | highlight DiffDelete ctermbg=52 guibg=#5f0000',
          // Show helpful messages
          '-c',
          'set showtabline=2 | set tabline=[Instructions]\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)',
          '-c',
          'wincmd h | setlocal statusline=OLD\\ FILE',
          '-c',
          'wincmd l | setlocal statusline=%#StatusBold#NEW\\ FILE\\ :wqa(save\\ &\\ quit)\\ \\|\\ i/esc(toggle\\ edit\\ mode)',
          // Auto close all windows when one is closed
          '-c',
          'autocmd WinClosed * wqa',
          oldPath,
          newPath,
        ],
      };
    default:
      return null;
  }
}

/**
 * Opens a diff tool to compare two files.
 * Terminal-based editors by default blocks parent process until the editor exits.
 * GUI-based editors requires args such as "--wait" to block parent process.
 */
export async function openDiff(
  oldPath: string,
  newPath: string,
  editor: EditorType,
): Promise<void> {
  const diffCommand = getDiffCommand(oldPath, newPath, editor);
  if (!diffCommand) {
    console.error('No diff tool available. Install vim or vscode.');
    return;
  }

  try {
    if (editor === 'vscode') {
      // Use spawn to avoid blocking the entire process, resolve this function when editor is closed.
      return new Promise((resolve, reject) => {
        const process = spawn(diffCommand.command, diffCommand.args, {
          stdio: 'inherit',
        });

        process.on('close', (code) => {
          if (code === 0) {
            resolve();
          } else {
            reject(new Error(`VS Code exited with code ${code}`));
          }
        });

        process.on('error', (error) => {
          reject(error);
        });
      });
    } else {
      // Use execSync for terminal-based editors like vim
      const command =
        process.platform === 'win32'
          ? `${diffCommand.command} ${diffCommand.args.join(' ')}`
          : `${diffCommand.command} ${diffCommand.args.map((arg) => `"${arg}"`).join(' ')}`;
      execSync(command, {
        stdio: 'inherit',
        encoding: 'utf8',
      });
    }
  } catch (error) {
    console.error(error);
  }
}