Skip to content

Commit 65ba2ee

Browse files
McCleese, Jamesmccheesy
authored andcommitted
Fix: Preserve view and pin preview tabs on :w
- Use Tab API when available to detect preview tabs and only call showTextDocument when needed. - Ensure saving in compare/diff editors does not switch to a normal editor (fixes #9800, #9796). - Keep previous behavior of pinning preview tabs on :w so :w matches Cmd+S (fixes #9697). - Add test coverage for write, including new doc handling logic references PR #9698 fixes #9800, fixes #9796, fixes #9697
1 parent fd134e0 commit 65ba2ee

File tree

2 files changed

+242
-1
lines changed

2 files changed

+242
-1
lines changed

src/cmd_line/commands/write.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ export class WriteCommand extends ExCommand {
135135
}
136136

137137
private async save(vimState: VimState): Promise<void> {
138-
await vscode.window.showTextDocument(vimState.document, { preview: false });
138+
if (this.shouldShowDocument(vimState.document.uri)) {
139+
await vscode.window.showTextDocument(vimState.document, { preview: false });
140+
}
141+
139142
await this.background(
140143
vimState.document.save().then((success) => {
141144
if (success) {
@@ -153,6 +156,40 @@ export class WriteCommand extends ExCommand {
153156
);
154157
}
155158

159+
/**
160+
* Determines whether to call showTextDocument when saving.
161+
* Avoids disrupting diff views and handles preview tab pinning.
162+
*/
163+
private shouldShowDocument(documentUri: vscode.Uri): boolean {
164+
const uriString = documentUri.toString();
165+
const matchingTab = vscode.window.tabGroups.activeTabGroup.tabs.find((tab: vscode.Tab) =>
166+
this.tabContainsDocument(tab, uriString),
167+
);
168+
169+
if (matchingTab) {
170+
return matchingTab.isPreview;
171+
}
172+
173+
// No matching tab found, show it
174+
return true;
175+
}
176+
177+
/**
178+
* Check if a tab contains the specified document URI.
179+
* Handles regular tabs and diff tabs.
180+
*/
181+
private tabContainsDocument(tab: vscode.Tab, uriString: string): boolean {
182+
const input = tab.input as
183+
| { uri?: vscode.Uri; original?: vscode.Uri; modified?: vscode.Uri }
184+
| undefined;
185+
186+
return (
187+
input?.uri?.toString() === uriString ||
188+
input?.original?.toString() === uriString ||
189+
input?.modified?.toString() === uriString
190+
);
191+
}
192+
156193
private async background<T>(fn: Thenable<T>): Promise<void> {
157194
if (!this.arguments.bgWrite) {
158195
await fn;

test/cmd_line/write.test.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import * as vscode from 'vscode';
4+
5+
import { getAndUpdateModeHandler } from '../../extension';
6+
import { ExCommandLine } from '../../src/cmd_line/commandLine';
7+
import { ModeHandler } from '../../src/mode/modeHandler';
8+
import { assertEqualLines, cleanUpWorkspace, setupWorkspace } from '../testUtils';
9+
10+
suite('Write command (:w)', () => {
11+
let modeHandler: ModeHandler;
12+
13+
setup(async () => {
14+
await setupWorkspace();
15+
modeHandler = (await getAndUpdateModeHandler())!;
16+
});
17+
18+
teardown(cleanUpWorkspace);
19+
20+
/**
21+
* Helper to type "hello world!" in insert mode
22+
*/
23+
const typeHelloWorld = async () => {
24+
await modeHandler.handleMultipleKeyEvents([
25+
'i',
26+
'h',
27+
'e',
28+
'l',
29+
'l',
30+
'o',
31+
' ',
32+
'w',
33+
'o',
34+
'r',
35+
'l',
36+
'd',
37+
'!',
38+
'<Esc>',
39+
]);
40+
};
41+
42+
/**
43+
* Helper to run the :w command
44+
*/
45+
const runWriteCommand = async () => {
46+
await new ExCommandLine('w', modeHandler.vimState.currentMode).run(modeHandler.vimState);
47+
};
48+
49+
suite('Basic functionality', () => {
50+
test('write (:w) - basic functionality', async () => {
51+
await typeHelloWorld();
52+
await runWriteCommand();
53+
54+
assertEqualLines(['hello world!']);
55+
});
56+
57+
test('write (:w) - should save document', async () => {
58+
await modeHandler.handleMultipleKeyEvents([
59+
'i',
60+
't',
61+
'e',
62+
's',
63+
't',
64+
' ',
65+
'c',
66+
'o',
67+
'n',
68+
't',
69+
'e',
70+
'n',
71+
't',
72+
'<Esc>',
73+
]);
74+
75+
await new ExCommandLine('w', modeHandler.vimState.currentMode).run(modeHandler.vimState);
76+
77+
assertEqualLines(['test content']);
78+
});
79+
});
80+
suite('Tab Groups API - diff view preservation', () => {
81+
let showTextDocumentStub: sinon.SinonStub;
82+
let tabGroupsStub: sinon.SinonStub;
83+
84+
setup(() => {
85+
showTextDocumentStub = sinon.stub(vscode.window, 'showTextDocument').resolves();
86+
tabGroupsStub = sinon.stub(vscode.window, 'tabGroups');
87+
});
88+
89+
teardown(() => {
90+
showTextDocumentStub.restore();
91+
tabGroupsStub.restore();
92+
});
93+
94+
test('should pin preview tabs when saving', async () => {
95+
const docUri = modeHandler.vimState.document.uri.toString();
96+
const mockTab = {
97+
isPreview: true,
98+
input: { uri: { toString: () => docUri } },
99+
};
100+
tabGroupsStub.get(() => ({ activeTabGroup: { tabs: [mockTab] } }));
101+
102+
await typeHelloWorld();
103+
await runWriteCommand();
104+
105+
assert.strictEqual(
106+
showTextDocumentStub.calledOnce,
107+
true,
108+
'showTextDocument should be called to pin preview tabs',
109+
);
110+
});
111+
112+
test('should preserve diff view for non-preview tabs', async () => {
113+
const docUri = modeHandler.vimState.document.uri.toString();
114+
const mockTab = {
115+
isPreview: false,
116+
input: { uri: { toString: () => docUri } },
117+
};
118+
tabGroupsStub.get(() => ({ activeTabGroup: { tabs: [mockTab] } }));
119+
120+
await typeHelloWorld();
121+
await runWriteCommand();
122+
123+
assert.strictEqual(
124+
showTextDocumentStub.called,
125+
false,
126+
'showTextDocument should not be called for non-preview tabs',
127+
);
128+
});
129+
130+
test('should handle diff tabs correctly (original side)', async () => {
131+
const docUri = modeHandler.vimState.document.uri.toString();
132+
const mockDiffTab = {
133+
isPreview: false,
134+
input: {
135+
original: { toString: () => docUri },
136+
modified: { toString: () => 'other-file-uri' },
137+
},
138+
};
139+
tabGroupsStub.get(() => ({ activeTabGroup: { tabs: [mockDiffTab] } }));
140+
141+
await typeHelloWorld();
142+
await runWriteCommand();
143+
144+
assert.strictEqual(
145+
showTextDocumentStub.called,
146+
false,
147+
'should not disrupt diff view when document is original side',
148+
);
149+
});
150+
151+
test('should handle diff tabs correctly (modified side)', async () => {
152+
const docUri = modeHandler.vimState.document.uri.toString();
153+
const mockDiffTab = {
154+
isPreview: false,
155+
input: {
156+
original: { toString: () => 'other-file-uri' },
157+
modified: { toString: () => docUri },
158+
},
159+
};
160+
tabGroupsStub.get(() => ({ activeTabGroup: { tabs: [mockDiffTab] } }));
161+
162+
await typeHelloWorld();
163+
await runWriteCommand();
164+
165+
assert.strictEqual(
166+
showTextDocumentStub.called,
167+
false,
168+
'should not disrupt diff view when document is modified side',
169+
);
170+
});
171+
172+
test('should call showTextDocument when document not in active tab group', async () => {
173+
tabGroupsStub.get(() => ({ activeTabGroup: { tabs: [] } }));
174+
175+
await typeHelloWorld();
176+
await runWriteCommand();
177+
178+
assert.strictEqual(
179+
showTextDocumentStub.calledOnce,
180+
true,
181+
'showTextDocument should be called when document not in active tab group',
182+
);
183+
});
184+
185+
test('should only check active tab group, not other tab groups', async () => {
186+
const docUri = modeHandler.vimState.document.uri.toString();
187+
// Active tab group has a different file
188+
tabGroupsStub.get(() => ({
189+
activeTabGroup: {
190+
tabs: [{ isPreview: false, input: { uri: { toString: () => 'other-file' } } }],
191+
},
192+
}));
193+
194+
await typeHelloWorld();
195+
await runWriteCommand();
196+
197+
assert.strictEqual(
198+
showTextDocumentStub.calledOnce,
199+
true,
200+
'should call showTextDocument when document not in active tab group',
201+
);
202+
});
203+
});
204+
});

0 commit comments

Comments
 (0)