Skip to content

Commit d457bdc

Browse files
authored
Merge branch 'main' into fix/improve-marker-placements
2 parents 33064ec + 361561f commit d457bdc

File tree

42 files changed

+695
-243
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+695
-243
lines changed

extensions/git/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"scmSelectedProvider",
3232
"scmTextDocument",
3333
"scmValidation",
34+
"tabInputMultiDiff",
3435
"tabInputTextMerge",
3536
"timeline"
3637
],

extensions/git/src/repository.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as fs from 'fs';
77
import * as path from 'path';
88
import * as picomatch from 'picomatch';
9-
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern, CancellationTokenSource, LogOutputChannel, LogLevel, CancellationError, l10n } from 'vscode';
9+
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, TabInputTextDiff, TabInputNotebookDiff, TabInputTextMultiDiff, RelativePattern, CancellationTokenSource, LogOutputChannel, LogLevel, CancellationError, l10n } from 'vscode';
1010
import TelemetryReporter from '@vscode/extension-telemetry';
1111
import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git';
1212
import { AutoFetcher } from './autofetch';
@@ -1358,22 +1358,29 @@ export class Repository implements Disposable {
13581358
const config = workspace.getConfiguration('git', Uri.file(this.root));
13591359
if (!config.get<boolean>('closeDiffOnOperation', false) && !ignoreSetting) { return; }
13601360

1361-
const diffEditorTabsToClose: Tab[] = [];
1362-
1363-
for (const tab of window.tabGroups.all.map(g => g.tabs).flat()) {
1364-
const { input } = tab;
1365-
if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) {
1366-
if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) {
1367-
// Index
1368-
diffEditorTabsToClose.push(tab);
1369-
}
1370-
if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) {
1371-
// Working Tree
1372-
diffEditorTabsToClose.push(tab);
1373-
}
1361+
function checkTabShouldClose(input: TabInputTextDiff | TabInputNotebookDiff) {
1362+
if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) {
1363+
// Index
1364+
return true;
1365+
}
1366+
if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) {
1367+
// Working Tree
1368+
return true;
13741369
}
1370+
return false;
13751371
}
13761372

1373+
const diffEditorTabsToClose = window.tabGroups.all
1374+
.flatMap(g => g.tabs)
1375+
.filter(({ input }) => {
1376+
if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) {
1377+
return checkTabShouldClose(input);
1378+
} else if (input instanceof TabInputTextMultiDiff) {
1379+
return input.textDiffs.every(checkTabShouldClose);
1380+
}
1381+
return false;
1382+
});
1383+
13771384
// Close editors
13781385
window.tabGroups.close(diffEditorTabsToClose, true);
13791386
}

extensions/git/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"../../src/vscode-dts/vscode.proposed.scmValidation.d.ts",
1919
"../../src/vscode-dts/vscode.proposed.scmMultiDiffEditor.d.ts",
2020
"../../src/vscode-dts/vscode.proposed.scmTextDocument.d.ts",
21+
"../../src/vscode-dts/vscode.proposed.tabInputMultiDiff.d.ts",
2122
"../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts",
2223
"../../src/vscode-dts/vscode.proposed.timeline.d.ts",
2324
"../types/lib.textEncoder.d.ts"

extensions/vscode-api-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"taskPresentationGroup",
4545
"terminalDataWriteEvent",
4646
"terminalDimensions",
47+
"terminalShellIntegration",
4748
"tunnels",
4849
"testObserver",
4950
"textSearchProvider",
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { deepStrictEqual, notStrictEqual, ok, strictEqual } from 'assert';
7+
import { platform } from 'os';
8+
import { env, TerminalShellExecutionCommandLineConfidence, UIKind, window, workspace, type Disposable, type Terminal, type TerminalShellExecution, type TerminalShellExecutionCommandLine, type TerminalShellExecutionEndEvent, type TerminalShellIntegration } from 'vscode';
9+
import { assertNoRpc } from '../utils';
10+
11+
// Terminal integration tests are disabled on web https://github.com/microsoft/vscode/issues/92826
12+
// Windows images will often not have functional shell integration
13+
(env.uiKind === UIKind.Web || platform() === 'win32' ? suite.skip : suite)('vscode API - Terminal.shellIntegration', () => {
14+
const disposables: Disposable[] = [];
15+
16+
suiteSetup(async () => {
17+
const config = workspace.getConfiguration('terminal.integrated');
18+
await config.update('shellIntegration.enabled', true);
19+
});
20+
21+
suiteTeardown(async () => {
22+
const config = workspace.getConfiguration('terminal.integrated');
23+
await config.update('shellIntegration.enabled', undefined);
24+
});
25+
26+
teardown(async () => {
27+
assertNoRpc();
28+
disposables.forEach(d => d.dispose());
29+
disposables.length = 0;
30+
});
31+
32+
function createTerminalAndWaitForShellIntegration(): Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }> {
33+
return new Promise<{ terminal: Terminal; shellIntegration: TerminalShellIntegration }>(resolve => {
34+
disposables.push(window.onDidChangeTerminalShellIntegration(e => {
35+
if (e.terminal === terminal) {
36+
resolve({
37+
terminal,
38+
shellIntegration: e.shellIntegration
39+
});
40+
}
41+
}));
42+
const terminal = window.createTerminal();
43+
terminal.show();
44+
});
45+
}
46+
47+
function executeCommandAsync(shellIntegration: TerminalShellIntegration, command: string, args?: string[]): { execution: Promise<TerminalShellExecution>; endEvent: Promise<TerminalShellExecutionEndEvent> } {
48+
return {
49+
execution: new Promise<TerminalShellExecution>(resolve => {
50+
// Await a short period as pwsh's first SI prompt can fail when launched in quick succession
51+
setTimeout(() => {
52+
if (args) {
53+
resolve(shellIntegration.executeCommand(command, args));
54+
} else {
55+
resolve(shellIntegration.executeCommand(command));
56+
}
57+
}, 500);
58+
}),
59+
endEvent: new Promise<TerminalShellExecutionEndEvent>(resolve => {
60+
disposables.push(window.onDidEndTerminalShellExecution(e => {
61+
if (e.shellIntegration === shellIntegration) {
62+
resolve(e);
63+
}
64+
}));
65+
})
66+
};
67+
}
68+
69+
function closeTerminalAsync(terminal: Terminal): Promise<void> {
70+
return new Promise<void>(resolve => {
71+
disposables.push(window.onDidCloseTerminal(e => {
72+
if (e === terminal) {
73+
resolve();
74+
}
75+
}));
76+
terminal.dispose();
77+
});
78+
}
79+
80+
test('window.onDidChangeTerminalShellIntegration should activate for the default terminal', async () => {
81+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
82+
ok(terminal.shellIntegration);
83+
ok(shellIntegration);
84+
await closeTerminalAsync(terminal);
85+
});
86+
87+
test('execution events should fire in order when a command runs', async () => {
88+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
89+
const events: string[] = [];
90+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
91+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
92+
93+
await executeCommandAsync(shellIntegration, 'echo hello').endEvent;
94+
95+
deepStrictEqual(events, ['start', 'end']);
96+
97+
await closeTerminalAsync(terminal);
98+
});
99+
100+
test('end execution event should report zero exit code for successful commands', async () => {
101+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
102+
const events: string[] = [];
103+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
104+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
105+
106+
const endEvent = await executeCommandAsync(shellIntegration, 'echo hello').endEvent;
107+
strictEqual(endEvent.exitCode, 0);
108+
109+
await closeTerminalAsync(terminal);
110+
});
111+
112+
test('end execution event should report non-zero exit code for failed commands', async () => {
113+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
114+
const events: string[] = [];
115+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
116+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
117+
118+
const endEvent = await executeCommandAsync(shellIntegration, 'fakecommand').endEvent;
119+
notStrictEqual(endEvent.exitCode, 0);
120+
121+
await closeTerminalAsync(terminal);
122+
});
123+
124+
test('TerminalShellExecution.read iterables should be available between the start and end execution events', async () => {
125+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
126+
const events: string[] = [];
127+
disposables.push(window.onDidStartTerminalShellExecution(() => events.push('start')));
128+
disposables.push(window.onDidEndTerminalShellExecution(() => events.push('end')));
129+
130+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
131+
for await (const _ of (await execution).read()) {
132+
events.push('data');
133+
}
134+
await endEvent;
135+
136+
ok(events.length >= 3, `should have at least 3 events ${JSON.stringify(events)}`);
137+
strictEqual(events[0], 'start', `first event should be 'start' ${JSON.stringify(events)}`);
138+
strictEqual(events.at(-1), 'end', `last event should be 'end' ${JSON.stringify(events)}`);
139+
for (let i = 1; i < events.length - 1; i++) {
140+
strictEqual(events[i], 'data', `all middle events should be 'data' ${JSON.stringify(events)}`);
141+
}
142+
143+
await closeTerminalAsync(terminal);
144+
});
145+
146+
test('TerminalShellExecution.read events should fire with contents of command', async () => {
147+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
148+
const events: string[] = [];
149+
150+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
151+
for await (const data of (await execution).read()) {
152+
events.push(data);
153+
}
154+
await endEvent;
155+
156+
ok(events.join('').includes('hello'), `should include 'hello' in ${JSON.stringify(events)}`);
157+
158+
await closeTerminalAsync(terminal);
159+
});
160+
161+
test('TerminalShellExecution.read events should give separate iterables per call', async () => {
162+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
163+
164+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
165+
const executionSync = await execution;
166+
const firstRead = executionSync.read();
167+
const secondRead = executionSync.read();
168+
169+
const [firstReadEvents, secondReadEvents] = await Promise.all([
170+
new Promise<string[]>(resolve => {
171+
(async () => {
172+
const events: string[] = [];
173+
for await (const data of firstRead) {
174+
events.push(data);
175+
}
176+
resolve(events);
177+
})();
178+
}),
179+
new Promise<string[]>(resolve => {
180+
(async () => {
181+
const events: string[] = [];
182+
for await (const data of secondRead) {
183+
events.push(data);
184+
}
185+
resolve(events);
186+
})();
187+
}),
188+
]);
189+
await endEvent;
190+
191+
ok(firstReadEvents.join('').includes('hello'), `should include 'hello' in ${JSON.stringify(firstReadEvents)}`);
192+
deepStrictEqual(firstReadEvents, secondReadEvents);
193+
194+
await closeTerminalAsync(terminal);
195+
});
196+
197+
test('executeCommand(commandLine)', async () => {
198+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
199+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo hello');
200+
const executionSync = await execution;
201+
const expectedCommandLine: TerminalShellExecutionCommandLine = {
202+
value: 'echo hello',
203+
isTrusted: true,
204+
confidence: TerminalShellExecutionCommandLineConfidence.High
205+
};
206+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
207+
await endEvent;
208+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
209+
await closeTerminalAsync(terminal);
210+
});
211+
212+
test('executeCommand(executable, args)', async () => {
213+
const { terminal, shellIntegration } = await createTerminalAndWaitForShellIntegration();
214+
const { execution, endEvent } = executeCommandAsync(shellIntegration, 'echo', ['hello']);
215+
const executionSync = await execution;
216+
const expectedCommandLine: TerminalShellExecutionCommandLine = {
217+
value: 'echo "hello"',
218+
isTrusted: true,
219+
confidence: TerminalShellExecutionCommandLineConfidence.High
220+
};
221+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
222+
await endEvent;
223+
deepStrictEqual(executionSync.commandLine, expectedCommandLine);
224+
await closeTerminalAsync(terminal);
225+
});
226+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
"kerberos": "^2.0.1",
9797
"minimist": "^1.2.6",
9898
"native-is-elevated": "0.7.0",
99-
"native-keymap": "^3.3.4",
99+
"native-keymap": "^3.3.5",
100100
"native-watchdog": "^1.4.1",
101101
"node-pty": "1.1.0-beta11",
102102
"tas-client-umd": "0.1.8",

src/vs/base/browser/ui/grid/grid.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import 'vs/css!./gridview';
1111
import { Box, GridView, IGridViewOptions, IGridViewStyles, IView as IGridViewView, IViewSize, orthogonal, Sizing as GridViewSizing, GridLocation } from './gridview';
1212
import type { SplitView, AutoSizing as SplitViewAutoSizing } from 'vs/base/browser/ui/splitview/splitview';
1313

14-
export { IViewSize, LayoutPriority, Orientation, orthogonal } from './gridview';
14+
export type { IViewSize };
15+
export { LayoutPriority, Orientation, orthogonal } from './gridview';
1516

1617
export const enum Direction {
1718
Up,

src/vs/base/common/extpath.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ export function sanitizeFilePath(candidate: string, cwd: string): string {
282282
candidate = normalize(candidate);
283283

284284
// Ensure no trailing slash/backslash
285+
return removeTrailingPathSeparator(candidate);
286+
}
287+
288+
export function removeTrailingPathSeparator(candidate: string): string {
285289
if (isWindows) {
286290
candidate = rtrim(candidate, sep);
287291

0 commit comments

Comments
 (0)