Skip to content

Commit 19a843c

Browse files
authored
Add configurable editor command and open-file shortcut (#43)
1 parent 761f30e commit 19a843c

15 files changed

Lines changed: 406 additions & 140 deletions

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ is running so changes apply to open windows.
8888
"settings": {
8989
"copyCommentsOnClose": false,
9090
"diffStyle": "split",
91+
"editorCommand": "",
9192
"lastRepositoryPath": "",
9293
"openAIModel": "gpt-5.3-codex-spark",
9394
"showWhitespace": false,
@@ -99,6 +100,7 @@ is running so changes apply to open windows.
99100
"diffSearch": "Mod+f",
100101
"fileFilter": "Mod+p",
101102
"nextSearchMatch": "Enter",
103+
"openFile": "Mod+k",
102104
"prevSearchMatch": "Shift+Enter",
103105
"closeSearch": "Escape",
104106
"submitComment": "Mod+Enter",
@@ -108,6 +110,9 @@ is running so changes apply to open windows.
108110
}
109111
```
110112

113+
Set `settings.editorCommand` to customize file opening. Use `{file}` for the selected file and
114+
`{repo}` for the repository root, for example `"subl \"{repo}\" \"{file}\""`.
115+
111116
Choose `View > Split Diff` or `View > Unified Diff`, use Toggle Diff Layout in the command bar,
112117
or set `settings.diffStyle` to `split` for side-by-side diffs or `unified` for unified diffs.
113118
Choose `View > Word Wrap`, use Toggle Word Wrap in the command bar, or set `settings.wordWrap`

config/defaults.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"settings": {
3+
"copyCommentsOnClose": false,
4+
"diffStyle": "split",
5+
"editorCommand": "",
6+
"lastRepositoryPath": "",
7+
"openAIModel": "gpt-5.3-codex-spark",
8+
"showOutdated": false,
9+
"showWhitespace": false,
10+
"theme": "system",
11+
"wordWrap": false
12+
},
13+
"keymap": {
14+
"closeSearch": "Escape",
15+
"commandBar": "Mod+Shift+p",
16+
"diffSearch": "Mod+f",
17+
"discardComment": "Escape",
18+
"fileFilter": "Mod+p",
19+
"nextSearchMatch": "Enter",
20+
"openFile": "Mod+k",
21+
"prevSearchMatch": "Shift+Enter",
22+
"submitComment": "Mod+Enter",
23+
"toggleSidebar": "Mod+b"
24+
}
25+
}

electron/__tests__/editor.test.ts

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,115 @@ import { expect, test } from 'vite-plus/test';
44
const require = createRequire(import.meta.url);
55
const { createEditorOpener } = require('../main/editor.cjs') as {
66
createEditorOpener: (options: {
7+
getEditorCommand?: () => string;
78
platform?: NodeJS.Platform;
89
shell: {
910
openPath: (path: string) => Promise<string>;
1011
};
1112
}) => {
12-
getEditorCommands: (absolutePath: string) => Array<{
13+
getEditorCommands: (
14+
absolutePath: string,
15+
context?: {
16+
repoPath?: string;
17+
},
18+
) => Array<{
1319
args: Array<string>;
1420
command: string;
1521
}>;
1622
parseEditorCommand: (command: string) => Array<string>;
1723
};
1824
};
1925

20-
test('falls back to the macOS default text editor for text files without app associations', () => {
21-
const opener = createEditorOpener({
22-
platform: 'darwin',
26+
const createOpener = (
27+
options: { getEditorCommand?: () => string; platform?: NodeJS.Platform } = {},
28+
) =>
29+
createEditorOpener({
30+
...options,
2331
shell: {
2432
openPath: async () => '',
2533
},
2634
});
2735

36+
test('falls back to the macOS default text editor for text files without app associations', () => {
37+
const opener = createOpener({
38+
platform: 'darwin',
39+
});
40+
2841
expect(opener.getEditorCommands('/Users/test/.codiff/codiff.jsonc')).toContainEqual({
2942
args: ['-t', '/Users/test/.codiff/codiff.jsonc'],
3043
command: 'open',
3144
});
3245
});
3346

3447
test('parses custom editor commands with quoted arguments', () => {
35-
const opener = createEditorOpener({
36-
shell: {
37-
openPath: async () => '',
38-
},
39-
});
48+
const opener = createOpener();
4049

4150
expect(opener.parseEditorCommand('editor --goto "{file}"')).toEqual([
4251
'editor',
4352
'--goto',
4453
'{file}',
4554
]);
4655
});
56+
57+
test('uses the configured editor command before built-in commands', () => {
58+
const opener = createOpener({
59+
getEditorCommand: () => 'cursor --goto "{file}"',
60+
});
61+
62+
expect(opener.getEditorCommands('/Users/test/project/file.ts')[0]).toEqual({
63+
args: ['--goto', '/Users/test/project/file.ts'],
64+
command: 'cursor',
65+
});
66+
});
67+
68+
test('expands the repo placeholder in configured editor commands', () => {
69+
const opener = createOpener({
70+
getEditorCommand: () => 'subl "{repo}" "{file}"',
71+
});
72+
73+
expect(
74+
opener.getEditorCommands('/Users/test/project/src/file.ts', {
75+
repoPath: '/Users/test/project',
76+
})[0],
77+
).toEqual({
78+
args: ['/Users/test/project', '/Users/test/project/src/file.ts'],
79+
command: 'subl',
80+
});
81+
});
82+
83+
test('appends the file path when the configured editor command only uses the repo placeholder', () => {
84+
const opener = createOpener({
85+
getEditorCommand: () => 'subl "{repo}"',
86+
});
87+
88+
expect(
89+
opener.getEditorCommands('/Users/test/project/src/file.ts', {
90+
repoPath: '/Users/test/project',
91+
})[0],
92+
).toEqual({
93+
args: ['/Users/test/project', '/Users/test/project/src/file.ts'],
94+
command: 'subl',
95+
});
96+
});
97+
98+
test('lets CODIFF_EDITOR override the configured editor command', () => {
99+
const previous = process.env.CODIFF_EDITOR;
100+
process.env.CODIFF_EDITOR = 'zed --wait';
101+
102+
try {
103+
const opener = createOpener({
104+
getEditorCommand: () => 'cursor --goto "{file}"',
105+
});
106+
107+
expect(opener.getEditorCommands('/Users/test/project/file.ts')[0]).toEqual({
108+
args: ['--wait', '/Users/test/project/file.ts'],
109+
command: 'zed',
110+
});
111+
} finally {
112+
if (previous === undefined) {
113+
delete process.env.CODIFF_EDITOR;
114+
} else {
115+
process.env.CODIFF_EDITOR = previous;
116+
}
117+
}
118+
});

electron/config.cjs

Lines changed: 45 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,21 @@ const { join } = require('node:path');
1414
/**
1515
* @typedef {import('../src/config/types.ts').CodiffConfig} CodiffConfig
1616
* @typedef {import('../src/config/types.ts').CodiffDiffStyle} CodiffDiffStyle
17-
* @typedef {import('../src/config/types.ts').CodiffKeymap} CodiffKeymap
18-
* @typedef {import('../src/config/types.ts').CodiffSettings} CodiffSettings
1917
* @typedef {import('../src/config/types.ts').CodiffTheme} CodiffTheme
2018
* @typedef {import('../src/types.ts').CodiffPreferences} CodiffPreferences
2119
*/
2220

21+
/** @type {CodiffConfig} */
22+
const defaultConfigTemplate = require('../config/defaults.json');
23+
2324
const SCHEMA_URL =
2425
'https://raw.githubusercontent.com/nkzw-tech/codiff/main/src/config/codiff-config.schema.json';
2526

26-
/** @type {CodiffSettings} */
27-
const defaultSettings = {
28-
copyCommentsOnClose: false,
29-
diffStyle: 'split',
30-
lastRepositoryPath: '',
31-
openAIModel: 'gpt-5.3-codex-spark',
32-
showOutdated: false,
33-
showWhitespace: false,
34-
theme: 'system',
35-
wordWrap: false,
36-
};
37-
38-
/** @type {CodiffKeymap} */
39-
const defaultKeymap = {
40-
closeSearch: 'Escape',
41-
commandBar: 'Mod+Shift+p',
42-
diffSearch: 'Mod+f',
43-
discardComment: 'Escape',
44-
fileFilter: 'Mod+p',
45-
nextSearchMatch: 'Enter',
46-
prevSearchMatch: 'Shift+Enter',
47-
submitComment: 'Mod+Enter',
48-
toggleSidebar: 'Mod+b',
49-
};
50-
51-
/** @type {CodiffConfig} */
52-
const defaultConfig = {
53-
keymap: defaultKeymap,
54-
settings: defaultSettings,
55-
};
27+
/** @returns {CodiffConfig} */
28+
const createDefaultConfig = () => ({
29+
keymap: { ...defaultConfigTemplate.keymap },
30+
settings: { ...defaultConfigTemplate.settings },
31+
});
5632

5733
const getConfigDir = () => join(homedir(), '.codiff');
5834

@@ -147,8 +123,10 @@ const normalizeLastRepositoryPath = (path) =>
147123
* @returns {CodiffConfig}
148124
*/
149125
const mergeConfig = (raw) => {
126+
const defaults = createDefaultConfig();
127+
150128
if (typeof raw !== 'object' || raw === null) {
151-
return defaultConfig;
129+
return defaults;
152130
}
153131

154132
const obj = /** @type {Record<string, unknown>} */ (raw);
@@ -166,56 +144,70 @@ const mergeConfig = (raw) => {
166144
closeSearch:
167145
typeof rawKeymap.closeSearch === 'string'
168146
? rawKeymap.closeSearch
169-
: defaultKeymap.closeSearch,
147+
: defaults.keymap.closeSearch,
170148
commandBar:
171-
typeof rawKeymap.commandBar === 'string' ? rawKeymap.commandBar : defaultKeymap.commandBar,
149+
typeof rawKeymap.commandBar === 'string'
150+
? rawKeymap.commandBar
151+
: defaults.keymap.commandBar,
172152
diffSearch:
173-
typeof rawKeymap.diffSearch === 'string' ? rawKeymap.diffSearch : defaultKeymap.diffSearch,
153+
typeof rawKeymap.diffSearch === 'string'
154+
? rawKeymap.diffSearch
155+
: defaults.keymap.diffSearch,
174156
discardComment:
175157
typeof rawKeymap.discardComment === 'string'
176158
? rawKeymap.discardComment
177-
: defaultKeymap.discardComment,
159+
: defaults.keymap.discardComment,
178160
fileFilter:
179-
typeof rawKeymap.fileFilter === 'string' ? rawKeymap.fileFilter : defaultKeymap.fileFilter,
161+
typeof rawKeymap.fileFilter === 'string'
162+
? rawKeymap.fileFilter
163+
: defaults.keymap.fileFilter,
180164
nextSearchMatch:
181165
typeof rawKeymap.nextSearchMatch === 'string'
182166
? rawKeymap.nextSearchMatch
183-
: defaultKeymap.nextSearchMatch,
167+
: defaults.keymap.nextSearchMatch,
168+
openFile:
169+
typeof rawKeymap.openFile === 'string' ? rawKeymap.openFile : defaults.keymap.openFile,
184170
prevSearchMatch:
185171
typeof rawKeymap.prevSearchMatch === 'string'
186172
? rawKeymap.prevSearchMatch
187-
: defaultKeymap.prevSearchMatch,
173+
: defaults.keymap.prevSearchMatch,
188174
submitComment:
189175
typeof rawKeymap.submitComment === 'string'
190176
? rawKeymap.submitComment
191-
: defaultKeymap.submitComment,
177+
: defaults.keymap.submitComment,
192178
toggleSidebar:
193179
typeof rawKeymap.toggleSidebar === 'string'
194180
? rawKeymap.toggleSidebar
195-
: defaultKeymap.toggleSidebar,
181+
: defaults.keymap.toggleSidebar,
196182
},
197183
settings: {
198184
copyCommentsOnClose:
199185
typeof rawSettings.copyCommentsOnClose === 'boolean'
200186
? rawSettings.copyCommentsOnClose
201-
: defaultSettings.copyCommentsOnClose,
187+
: defaults.settings.copyCommentsOnClose,
202188
diffStyle: normalizeDiffStyle(rawSettings.diffStyle),
189+
editorCommand:
190+
typeof rawSettings.editorCommand === 'string'
191+
? rawSettings.editorCommand
192+
: defaults.settings.editorCommand,
203193
lastRepositoryPath: normalizeLastRepositoryPath(rawSettings.lastRepositoryPath),
204194
openAIModel:
205195
typeof rawSettings.openAIModel === 'string'
206196
? rawSettings.openAIModel
207-
: defaultSettings.openAIModel,
197+
: defaults.settings.openAIModel,
208198
showOutdated:
209199
typeof rawSettings.showOutdated === 'boolean'
210200
? rawSettings.showOutdated
211-
: defaultSettings.showOutdated,
201+
: defaults.settings.showOutdated,
212202
showWhitespace:
213203
typeof rawSettings.showWhitespace === 'boolean'
214204
? rawSettings.showWhitespace
215-
: defaultSettings.showWhitespace,
205+
: defaults.settings.showWhitespace,
216206
theme: normalizeTheme(rawSettings.theme),
217207
wordWrap:
218-
typeof rawSettings.wordWrap === 'boolean' ? rawSettings.wordWrap : defaultSettings.wordWrap,
208+
typeof rawSettings.wordWrap === 'boolean'
209+
? rawSettings.wordWrap
210+
: defaults.settings.wordWrap,
219211
},
220212
};
221213
};
@@ -228,15 +220,15 @@ const readConfig = () => {
228220
const configPath = getConfigPath();
229221

230222
if (!existsSync(configPath)) {
231-
return defaultConfig;
223+
return createDefaultConfig();
232224
}
233225

234226
try {
235227
const text = readFileSync(configPath, 'utf8');
236228
const raw = parseJsonc(text);
237229
return mergeConfig(raw);
238230
} catch {
239-
return defaultConfig;
231+
return createDefaultConfig();
240232
}
241233
};
242234

@@ -268,7 +260,7 @@ const initConfig = () => {
268260
return false;
269261
}
270262

271-
writeConfig(defaultConfig);
263+
writeConfig(createDefaultConfig());
272264
return true;
273265
};
274266

@@ -297,11 +289,12 @@ const migrateFromPreferences = (userDataPath, normalizeOpenAIModel) => {
297289

298290
try {
299291
const oldPrefs = JSON.parse(readFileSync(oldPath, 'utf8'));
292+
const defaults = createDefaultConfig();
300293
const config = mergeConfig({
301294
settings: {
302295
...oldPrefs,
303296
lastRepositoryPath: normalizeLastRepositoryPath(oldPrefs?.lastRepositoryPath),
304-
openAIModel: normalizeOpenAIModel(oldPrefs?.openAIModel ?? defaultSettings.openAIModel),
297+
openAIModel: normalizeOpenAIModel(oldPrefs?.openAIModel ?? defaults.settings.openAIModel),
305298
theme: normalizeTheme(oldPrefs?.theme),
306299
},
307300
});
@@ -364,7 +357,7 @@ const configToPreferences = (config) => ({
364357

365358
module.exports = {
366359
configToPreferences,
367-
defaultConfig,
360+
createDefaultConfig,
368361
getConfigPath,
369362
initConfig,
370363
mergeConfig,

0 commit comments

Comments
 (0)