Skip to content

Commit 9622d83

Browse files
authored
Merge branch 'main' into copilot/fix-259201
2 parents 77955c9 + d1ad717 commit 9622d83

29 files changed

+343
-137
lines changed

extensions/terminal-suggest/src/env/pathExecutableCache.ts

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -105,55 +105,57 @@ export class PathExecutableCache implements vscode.Disposable {
105105
const result = new Set<ICompletionResource>();
106106
const fileResource = vscode.Uri.file(path);
107107
const files = await vscode.workspace.fs.readDirectory(fileResource);
108-
for (const [file, fileType] of files) {
109-
let kind: vscode.TerminalCompletionItemKind | undefined;
110-
let formattedPath: string | undefined;
111-
const resource = vscode.Uri.joinPath(fileResource, file);
112-
113-
// Skip unknown or directory file types early
114-
if (fileType === vscode.FileType.Unknown || fileType === vscode.FileType.Directory) {
115-
continue;
116-
}
108+
await Promise.all(
109+
files.map(([file, fileType]) => (async () => {
110+
let kind: vscode.TerminalCompletionItemKind | undefined;
111+
let formattedPath: string | undefined;
112+
const resource = vscode.Uri.joinPath(fileResource, file);
113+
114+
// Skip unknown or directory file types early
115+
if (fileType === vscode.FileType.Unknown || fileType === vscode.FileType.Directory) {
116+
return;
117+
}
117118

118-
try {
119-
const lstat = await fs.lstat(resource.fsPath);
120-
if (lstat.isSymbolicLink()) {
121-
try {
122-
const symlinkRealPath = await fs.realpath(resource.fsPath);
123-
const isExec = await isExecutable(symlinkRealPath, this._cachedWindowsExeExtensions);
124-
if (!isExec) {
125-
continue;
119+
try {
120+
const lstat = await fs.lstat(resource.fsPath);
121+
if (lstat.isSymbolicLink()) {
122+
try {
123+
const symlinkRealPath = await fs.realpath(resource.fsPath);
124+
const isExec = await isExecutable(symlinkRealPath, this._cachedWindowsExeExtensions);
125+
if (!isExec) {
126+
return;
127+
}
128+
kind = vscode.TerminalCompletionItemKind.Method;
129+
formattedPath = `${resource.fsPath} -> ${symlinkRealPath}`;
130+
} catch {
131+
return;
126132
}
127-
kind = vscode.TerminalCompletionItemKind.Method;
128-
formattedPath = `${resource.fsPath} -> ${symlinkRealPath}`;
129-
} catch {
130-
continue;
131133
}
134+
} catch {
135+
// Ignore errors for unreadable files
136+
return;
132137
}
133-
} catch {
134-
// Ignore errors for unreadable files
135-
continue;
136-
}
137138

138-
formattedPath = formattedPath ?? getFriendlyResourcePath(resource, pathSeparator);
139+
formattedPath = formattedPath ?? getFriendlyResourcePath(resource, pathSeparator);
139140

140-
// Check if already added or not executable
141-
if (labels.has(file)) {
142-
continue;
143-
}
141+
// Check if already added or not executable
142+
if (labels.has(file)) {
143+
return;
144+
}
144145

145-
const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(formattedPath, this._cachedWindowsExeExtensions);
146-
if (!isExec) {
147-
continue;
148-
}
146+
const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(formattedPath, this._cachedWindowsExeExtensions);
147+
if (!isExec) {
148+
return;
149+
}
149150

150-
result.add({
151-
label: file,
152-
documentation: formattedPath,
153-
kind: kind ?? vscode.TerminalCompletionItemKind.Method
154-
});
155-
labels.add(file);
156-
}
151+
result.add({
152+
label: file,
153+
documentation: formattedPath,
154+
kind: kind ?? vscode.TerminalCompletionItemKind.Method
155+
});
156+
labels.add(file);
157+
})())
158+
);
157159
return result;
158160
} catch (e) {
159161
// Ignore errors for directories that can't be read

extensions/terminal-suggest/src/fig/figInterface.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,26 @@ export async function getFigSuggestions(
4646
items: [],
4747
};
4848
const currentCommand = currentCommandAndArgString.split(' ')[0];
49+
50+
// Assemble a map to allow O(1) access to the available command from a spec
51+
// label. The label does not include an extension on Windows.
52+
const specLabelToAvailableCommandMap = new Map<string, ICompletionResource>();
53+
for (const command of availableCommands) {
54+
let label = typeof command.label === 'string' ? command.label : command.label.label;
55+
if (osIsWindows()) {
56+
label = removeAnyFileExtension(label);
57+
}
58+
specLabelToAvailableCommandMap.set(label, command);
59+
}
60+
4961
for (const spec of specs) {
5062
const specLabels = getFigSuggestionLabel(spec);
5163

5264
if (!specLabels) {
5365
continue;
5466
}
5567
for (const specLabel of specLabels) {
56-
const availableCommand = (osIsWindows()
57-
? availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label).match(new RegExp(`${specLabel}(\\.[^ ]+)?$`)))
58-
: availableCommands.find(command => (typeof command.label === 'string' ? command.label : command.label.label) === (specLabel)));
68+
const availableCommand = specLabelToAvailableCommandMap.get(specLabel);
5969
if (!availableCommand || (token && token.isCancellationRequested)) {
6070
continue;
6171
}

extensions/terminal-suggest/src/terminalSuggestMain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ export async function activate(context: vscode.ExtensionContext) {
295295
}
296296
}
297297

298+
298299
if (terminal.shellIntegration?.cwd && (result.filesRequested || result.foldersRequested)) {
299300
return new vscode.TerminalCompletionList(result.items, {
300301
filesRequested: result.filesRequested,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "code-oss-dev",
33
"version": "1.104.0",
4-
"distro": "5545a7bcc7ca289eee3a1bd8fff5d381f3811934",
4+
"distro": "b904906218b11808bcccad9ca29ac4f372a2ad96",
55
"author": {
66
"name": "Microsoft Corporation"
77
},

src/vs/workbench/contrib/chat/browser/chatContentParts/chatChangesSummaryPart.ts

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { ChatTreeItem } from '../chat.js';
1212
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1313
import { IChatChangesSummary as IChatFileChangesSummary, IChatService } from '../../common/chatService.js';
1414
import { IEditorService } from '../../../../services/editor/common/editorService.js';
15-
import { IEditSessionEntryDiff } from '../../common/chatEditingService.js';
15+
import { IChatEditingSession, IEditSessionEntryDiff } from '../../common/chatEditingService.js';
1616
import { WorkbenchList } from '../../../../../platform/list/browser/listService.js';
1717
import { ButtonWithIcon } from '../../../../../base/browser/ui/button/button.js';
1818
import { Codicon } from '../../../../../base/common/codicons.js';
@@ -24,7 +24,7 @@ import { IListRenderer, IListVirtualDelegate } from '../../../../../base/browser
2424
import { FileKind } from '../../../../../platform/files/common/files.js';
2525
import { createFileIconThemableTreeContainerScope } from '../../../files/browser/views/explorerView.js';
2626
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
27-
import { autorun, derived, IObservableWithChange } from '../../../../../base/common/observable.js';
27+
import { autorun, derived, IObservable, IObservableWithChange } from '../../../../../base/common/observable.js';
2828
import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';
2929
import { MultiDiffEditorItem } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js';
3030
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
@@ -40,6 +40,8 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
4040
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
4141
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
4242

43+
private readonly diffsBetweenRequests = new Map<string, IObservable<IEditSessionEntryDiff | undefined>>();
44+
4345
private fileChanges: readonly IChatFileChangesSummary[];
4446
private fileChangesDiffsObservable: IObservableWithChange<Map<string, IEditSessionEntryDiff>, void>;
4547

@@ -74,6 +76,8 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
7476
private computeFileChangesDiffs(context: IChatContentPartRenderContext, changes: readonly IChatFileChangesSummary[]): IObservableWithChange<Map<string, IEditSessionEntryDiff>, void> {
7577
return derived((r) => {
7678
const fileChangesDiffs = new Map<string, IEditSessionEntryDiff>();
79+
const firstRequestId = changes[0].requestId;
80+
const lastRequestId = changes[changes.length - 1].requestId;
7781
for (const change of changes) {
7882
const sessionId = change.sessionId;
7983
const session = this.chatService.getSession(sessionId);
@@ -84,28 +88,26 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
8488
if (!editSession) {
8589
continue;
8690
}
87-
const uri = change.reference;
88-
const modifiedEntry = editSession.getEntry(uri);
89-
if (!modifiedEntry) {
91+
const diff = this.getCachedEntryDiffBetweenRequests(editSession, change.reference, firstRequestId, lastRequestId)?.read(r);
92+
if (!diff) {
9093
continue;
9194
}
92-
const requestId = change.requestId;
93-
const undoStops = context.content.filter(e => e.kind === 'undoStop');
94-
95-
for (let i = undoStops.length - 1; i >= 0; i--) {
96-
const modifiedUri = modifiedEntry.modifiedURI;
97-
const undoStopID = undoStops[i].id;
98-
const diff = editSession.getEntryDiffBetweenStops(modifiedUri, requestId, undoStopID)?.read(r);
99-
if (!diff) {
100-
continue;
101-
}
102-
fileChangesDiffs.set(this.changeID(change), diff);
103-
}
95+
fileChangesDiffs.set(this.changeID(change), diff);
10496
}
10597
return fileChangesDiffs;
10698
});
10799
}
108100

101+
public getCachedEntryDiffBetweenRequests(editSession: IChatEditingSession, uri: URI, startRequestId: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined> | undefined {
102+
const key = `${uri}\0${startRequestId}\0${stopRequestId}`;
103+
let observable = this.diffsBetweenRequests.get(key);
104+
if (!observable) {
105+
observable = editSession.getEntryDiffBetweenRequests(uri, startRequestId, stopRequestId);
106+
this.diffsBetweenRequests.set(key, observable);
107+
}
108+
return observable;
109+
}
110+
109111
private renderHeader(container: HTMLElement): IDisposable {
110112
const viewListButtonContainer = container.appendChild($('.chat-file-changes-label'));
111113
const viewListButton = new ButtonWithIcon(viewListButtonContainer, {});
@@ -169,11 +171,13 @@ export class ChatCheckpointFileChangesSummaryContentPart extends Disposable impl
169171
private renderFilesList(container: HTMLElement): IDisposable {
170172
const store = new DisposableStore();
171173
this.list = store.add(this.instantiationService.createInstance(CollapsibleChangesSummaryListPool)).get();
174+
const listNode = this.list.getHTMLElement();
172175
const itemsShown = Math.min(this.fileChanges.length, this.MAX_ITEMS_SHOWN);
173176
const height = itemsShown * this.ELEMENT_HEIGHT;
174177
this.list.layout(height);
178+
listNode.style.height = height + 'px';
175179
this.updateList(this.fileChanges, this.fileChangesDiffsObservable.get());
176-
container.appendChild(this.list.getHTMLElement().parentElement!);
180+
container.appendChild(listNode.parentElement!);
177181

178182
store.add(this.list.onDidOpen((item) => {
179183
const element = item.element;

src/vs/workbench/contrib/chat/browser/chatContentParts/chatTodoListWidget.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ export class ChatTodoListWidget extends Disposable {
106106
todoList.forEach((todo, index) => {
107107
const todoElement = dom.$('.todo-item');
108108

109+
// Add tooltip if description exists
110+
if (todo.description && todo.description.trim()) {
111+
todoElement.title = todo.description;
112+
}
113+
109114
const statusIcon = dom.$('.todo-status-icon.codicon');
110115
statusIcon.classList.add(this.getStatusIconClass(todo.status));
111116
statusIcon.style.color = this.getStatusIconColor(todo.status);

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio
214214
return this._timeline.getEntryDiffBetweenStops(uri, requestId, stopId);
215215
}
216216

217+
public getEntryDiffBetweenRequests(uri: URI, startRequestId: string, stopRequestId: string) {
218+
return this._timeline.getEntryDiffBetweenRequests(uri, startRequestId, stopRequestId);
219+
}
220+
217221
public createSnapshot(requestId: string, undoStop: string | undefined, makeEmpty = undoStop !== undefined): void {
218222
this._timeline.pushSnapshot(
219223
requestId,

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingTimeline.ts

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77

88
import { equals as arraysEqual, binarySearch2 } from '../../../../../base/common/arrays.js';
9-
import { equals as objectsEqual } from '../../../../../base/common/objects.js';
109
import { findLast } from '../../../../../base/common/arraysFind.js';
1110
import { Iterable } from '../../../../../base/common/iterator.js';
1211
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
1312
import { ResourceMap } from '../../../../../base/common/map.js';
13+
import { equals as objectsEqual } from '../../../../../base/common/objects.js';
1414
import { derived, derivedOpts, IObservable, ITransaction, ObservablePromise, observableValue, transaction } from '../../../../../base/common/observable.js';
1515
import { isEqual } from '../../../../../base/common/resources.js';
1616
import { URI } from '../../../../../base/common/uri.js';
@@ -338,29 +338,7 @@ export class ChatEditingTimeline {
338338

339339
}
340340
const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader);
341-
const promise = this._editorWorkerService.computeDiff(
342-
refs[0].object.textEditorModel.uri,
343-
refs[1].object.textEditorModel.uri,
344-
{ ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 },
345-
'advanced'
346-
).then((diff): IEditSessionEntryDiff => {
347-
const entryDiff: IEditSessionEntryDiff = {
348-
originalURI: refs[0].object.textEditorModel.uri,
349-
modifiedURI: refs[1].object.textEditorModel.uri,
350-
identical: !!diff?.identical,
351-
quitEarly: !diff || diff.quitEarly,
352-
added: 0,
353-
removed: 0,
354-
};
355-
if (diff) {
356-
for (const change of diff.changes) {
357-
entryDiff.removed += change.original.endLineNumberExclusive - change.original.startLineNumber;
358-
entryDiff.added += change.modified.endLineNumberExclusive - change.modified.startLineNumber;
359-
}
360-
}
361-
362-
return entryDiff;
363-
});
341+
const promise = this._computeDiff(refs[0].object.textEditorModel.uri, refs[1].object.textEditorModel.uri, ignoreTrimWhitespace);
364342

365343
return new ObservablePromise(promise);
366344
});
@@ -418,6 +396,91 @@ export class ChatEditingTimeline {
418396
return observable;
419397
}
420398
}
399+
400+
public getEntryDiffBetweenRequests(uri: URI, startRequestId: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined> {
401+
const snapshotUris = derivedOpts<[URI | undefined, URI | undefined]>(
402+
{ equalsFn: (a, b) => arraysEqual(a, b, isEqual) },
403+
reader => {
404+
const history = this._linearHistory.read(reader);
405+
const firstSnapshotUri = this._getFirstSnapshotForUriAfterRequest(history, uri, startRequestId, true);
406+
const lastSnapshotUri = this._getFirstSnapshotForUriAfterRequest(history, uri, stopRequestId, false);
407+
return [firstSnapshotUri, lastSnapshotUri];
408+
},
409+
);
410+
const modelRefs = derived((reader) => {
411+
const snapshots = snapshotUris.read(reader);
412+
const firstSnapshotUri = snapshots[0];
413+
const lastSnapshotUri = snapshots[1];
414+
if (!firstSnapshotUri || !lastSnapshotUri) {
415+
return;
416+
}
417+
const store = new DisposableStore();
418+
reader.store.add(store);
419+
const referencesPromise = Promise.all([firstSnapshotUri, lastSnapshotUri].map(u => this._textModelService.createModelReference(u))).then(refs => {
420+
if (store.isDisposed) {
421+
refs.forEach(ref => ref.dispose());
422+
} else {
423+
refs.forEach(ref => store.add(ref));
424+
}
425+
return refs;
426+
});
427+
return new ObservablePromise(referencesPromise);
428+
});
429+
const diff = derived((reader): ObservablePromise<IEditSessionEntryDiff> | undefined => {
430+
const references = modelRefs.read(reader)?.promiseResult.read(reader);
431+
const refs = references?.data;
432+
if (!refs) {
433+
return;
434+
}
435+
const ignoreTrimWhitespace = this._ignoreTrimWhitespaceObservable.read(reader);
436+
const promise = this._computeDiff(refs[0].object.textEditorModel.uri, refs[1].object.textEditorModel.uri, ignoreTrimWhitespace);
437+
return new ObservablePromise(promise);
438+
});
439+
return derived(reader => {
440+
return diff.read(reader)?.promiseResult.read(reader)?.data || undefined;
441+
});
442+
}
443+
444+
private _computeDiff(originalUri: URI, modifiedUri: URI, ignoreTrimWhitespace: boolean): Promise<IEditSessionEntryDiff> {
445+
return this._editorWorkerService.computeDiff(
446+
originalUri,
447+
modifiedUri,
448+
{ ignoreTrimWhitespace, computeMoves: false, maxComputationTimeMs: 3000 },
449+
'advanced'
450+
).then((diff): IEditSessionEntryDiff => {
451+
const entryDiff: IEditSessionEntryDiff = {
452+
originalURI: originalUri,
453+
modifiedURI: modifiedUri,
454+
identical: !!diff?.identical,
455+
quitEarly: !diff || diff.quitEarly,
456+
added: 0,
457+
removed: 0,
458+
};
459+
if (diff) {
460+
for (const change of diff.changes) {
461+
entryDiff.removed += change.original.endLineNumberExclusive - change.original.startLineNumber;
462+
entryDiff.added += change.modified.endLineNumberExclusive - change.modified.startLineNumber;
463+
}
464+
}
465+
return entryDiff;
466+
});
467+
}
468+
469+
private _getFirstSnapshotForUriAfterRequest(history: readonly IChatEditingSessionSnapshot[], uri: URI, requestId: string, inclusive: boolean): URI | undefined {
470+
const requestIndex = history.findIndex(s => s.requestId === requestId);
471+
if (requestIndex === -1) { return undefined; }
472+
const processedIndex = requestIndex + (inclusive ? 0 : 1);
473+
for (let i = processedIndex; i < history.length; i++) {
474+
const snapshot = history[i];
475+
for (const stop of snapshot.stops) {
476+
const entry = stop.entries.get(uri);
477+
if (entry) {
478+
return entry.snapshotUri;
479+
}
480+
}
481+
}
482+
return uri;
483+
}
421484
}
422485

423486
function stopProvidesNewData(origin: IChatEditingSessionStop, target: IChatEditingSessionStop) {

0 commit comments

Comments
 (0)