Skip to content

Commit e083d5f

Browse files
benibenjCopilot
andcommitted
Merge main into agents/workspace-preselection-telemetry
Keep upstream removal of the temporary archive-nudge debug command while retaining workspace-preselection navigation tracking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 4859dc2 + 0b2d79f commit e083d5f

106 files changed

Lines changed: 7167 additions & 1006 deletions

File tree

Some content is hidden

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

‎extensions/copilot/src/extension/tools/node/findTextInFilesTool.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ Then if you want to include those files you can call the tool again by setting "
189189
if (!groupedMatches) {
190190
return this.errorResult(noMatchInstructions ? `No matches found. ${noMatchInstructions}` : 'No matches found.');
191191
}
192-
if (options.chatRequestId !== undefined) {
193-
this.grepResultService.addGrepResult(options.chatRequestId, groupedMatches);
192+
if (options.chatSessionResource !== undefined && options.chatRequestId !== undefined) {
193+
this.grepResultService.addGrepResult(options.chatSessionResource, options.chatRequestId, groupedMatches);
194194
}
195195
const prompt = await renderPromptElementJSON(this.instantiationService,
196196
FindTextInFilesGrepResult,

‎extensions/copilot/src/extension/tools/node/grepResultService.ts‎

Lines changed: 92 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import type * as vscode from 'vscode';
77

88
import { createServiceIdentifier } from '../../../util/common/services';
9+
import { binarySearch2 } from '../../../util/vs/base/common/arrays';
10+
import { Emitter, Event } from '../../../util/vs/base/common/event';
11+
import { Disposable } from '../../../util/vs/base/common/lifecycle';
912
import { LRUCache } from '../../../util/vs/base/common/map';
1013

1114
export const IGrepResultService = createServiceIdentifier<IGrepResultService>('IGrepResultService');
@@ -21,95 +24,126 @@ interface MatchResult {
2124

2225
export interface IGrepResultService {
2326
readonly _serviceBrand: undefined;
27+
readonly onDidRemoveGrepResult: Event<{ sessionUri: vscode.Uri; requestId: string }>;
2428

25-
addGrepResult(requestId: string, result: MatchResult): void;
26-
getGrepResult(requestId: string, uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] | undefined;
29+
addGrepResult(sessionUri: vscode.Uri, requestId: string, result: MatchResult): void;
30+
getGrepResult(sessionUri: vscode.Uri, uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] | undefined;
2731
}
2832

2933
export class NullGrepResultService implements IGrepResultService {
3034
declare readonly _serviceBrand: undefined;
35+
readonly onDidRemoveGrepResult = Event.None;
3136

32-
addGrepResult(requestId: string, result: MatchResult): void {
37+
addGrepResult(sessionUri: vscode.Uri, requestId: string, result: MatchResult): void {
3338
// No-op
3439
}
3540

36-
getGrepResult(requestId: string, uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] | undefined {
41+
getGrepResult(sessionUri: vscode.Uri, uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] | undefined {
3742
return undefined;
3843
}
3944
}
4045

41-
interface Matches {
42-
files: Map<string, vscode.Range[]>;
46+
interface FileMatches {
47+
ranges: vscode.Range[];
48+
prefixMaxEndLines: number[];
4349
}
4450

45-
export class GrepResultService implements IGrepResultService {
46-
readonly _serviceBrand: undefined;
51+
interface GrepResult {
52+
requestId: string;
53+
matches: Map<string, FileMatches>;
54+
}
55+
56+
class SessionMatches {
57+
private static readonly maxMatches = 16;
4758

48-
private readonly cache: LRUCache<string, Matches>;
59+
private readonly matches: GrepResult[];
4960

5061
constructor() {
51-
this.cache = new LRUCache<string, Matches>(10);
62+
this.matches = [];
5263
}
5364

54-
addGrepResult(requestId: string, result: MatchResult): void {
55-
let matches: Matches | undefined = this.cache.get(requestId);
56-
if (matches === undefined) {
57-
matches = { files: new Map() };
58-
for (const file of result.files) {
59-
matches.files.set(file.uri.toString(), file.matches.map(m => m.ranges[0].sourceRange));
65+
add(result: GrepResult): string | undefined {
66+
this.matches.push(result);
67+
if (this.matches.length > SessionMatches.maxMatches) {
68+
return this.matches.shift()?.requestId;
69+
}
70+
return undefined;
71+
}
72+
73+
get(uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] {
74+
const result: vscode.Range[] = [];
75+
const seen = new Set<string>();
76+
const uriKey = uri.toString();
77+
78+
for (let i = this.matches.length - 1; i >= 0; i--) {
79+
const fileMatches = this.matches[i].matches.get(uriKey);
80+
if (!fileMatches) {
81+
continue;
6082
}
61-
this.cache.set(requestId, matches);
62-
} else {
63-
for (const file of result.files) {
64-
const existingRanges = matches.files.get(file.uri.toString());
65-
if (existingRanges === undefined) {
66-
matches.files.set(file.uri.toString(), file.matches.map(m => m.ranges[0].sourceRange));
67-
} else {
68-
const existingRangesSet = new Set<number>(existingRanges.map(r => r.start.line));
69-
for (const match of file.matches) {
70-
const line = match.ranges[0].sourceRange.start.line;
71-
if (!existingRangesSet.has(line)) {
72-
existingRanges.push(match.ranges[0].sourceRange);
73-
existingRangesSet.add(line);
74-
}
75-
}
76-
existingRanges.sort((a, b) => a.start.line - b.start.line);
77-
matches.files.set(file.uri.toString(), existingRanges);
83+
84+
const startIndex = ~binarySearch2(fileMatches.ranges.length, index => fileMatches.prefixMaxEndLines[index] < startLine ? -1 : 1);
85+
const endIndex = ~binarySearch2(fileMatches.ranges.length, index => fileMatches.ranges[index].start.line <= endLine ? -1 : 1);
86+
for (let matchIndex = startIndex; matchIndex < endIndex; matchIndex++) {
87+
const match = fileMatches.ranges[matchIndex];
88+
if (match.end.line < startLine || match.start.line > endLine) {
89+
continue;
90+
}
91+
92+
const key = `${match.start.line}:${match.start.character}-${match.end.line}:${match.end.character}`;
93+
if (!seen.has(key)) {
94+
seen.add(key);
95+
result.push(match);
7896
}
7997
}
8098
}
99+
100+
return result;
81101
}
102+
}
82103

83-
getGrepResult(requestId: string, uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] | undefined {
84-
const matches = this.cache.get(requestId);
85-
if (!matches) {
86-
return undefined;
87-
}
88-
const fileMatches = matches.files.get(uri.toString());
89-
if (!fileMatches) {
90-
return undefined;
91-
}
104+
export class GrepResultService extends Disposable implements IGrepResultService {
105+
declare readonly _serviceBrand: undefined;
92106

93-
let low = 0;
94-
let high = fileMatches.length;
95-
while (low < high) {
96-
const mid = low + Math.floor((high - low) / 2);
97-
if (fileMatches[mid].start.line < startLine) {
98-
low = mid + 1;
99-
} else {
100-
high = mid;
101-
}
107+
private readonly _onDidRemoveGrepResult = this._register(new Emitter<{ sessionUri: vscode.Uri; requestId: string }>());
108+
readonly onDidRemoveGrepResult = this._onDidRemoveGrepResult.event;
109+
110+
private readonly cache: LRUCache<string, SessionMatches>;
111+
112+
constructor() {
113+
super();
114+
this.cache = new LRUCache<string, SessionMatches>(10);
115+
}
116+
117+
addGrepResult(sessionUri: vscode.Uri, requestId: string, result: MatchResult): void {
118+
const key = sessionUri.toString();
119+
let sessionMatches = this.cache.get(key);
120+
if (sessionMatches === undefined) {
121+
sessionMatches = new SessionMatches();
122+
this.cache.set(key, sessionMatches);
102123
}
103124

104-
const result: vscode.Range[] = [];
105-
for (let i = low; i < fileMatches.length; i++) {
106-
const match = fileMatches[i];
107-
if (match.start.line > endLine) {
108-
break;
125+
const matches = new Map<string, FileMatches>();
126+
for (const file of result.files) {
127+
const ranges = file.matches.map(match => match.ranges[0].sourceRange);
128+
const prefixMaxEndLines: number[] = [];
129+
let maxEndLine = -1;
130+
for (const range of ranges) {
131+
maxEndLine = Math.max(maxEndLine, range.end.line);
132+
prefixMaxEndLines.push(maxEndLine);
109133
}
110-
result.push(match);
134+
matches.set(file.uri.toString(), { ranges, prefixMaxEndLines });
111135
}
136+
const removedRequestId = sessionMatches.add({ requestId, matches });
137+
if (removedRequestId !== undefined) {
138+
this._onDidRemoveGrepResult.fire({ sessionUri, requestId: removedRequestId });
139+
}
140+
}
112141

113-
return result;
142+
getGrepResult(sessionUri: vscode.Uri, uri: vscode.Uri, startLine: number, endLine: number): vscode.Range[] | undefined {
143+
const matches = this.cache.get(sessionUri.toString());
144+
if (!matches) {
145+
return undefined;
146+
}
147+
return matches.get(uri, startLine, endLine);
114148
}
115149
}

‎extensions/copilot/src/extension/tools/node/readFileTool.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ export class ReadFileTool implements ICopilotTool<ReadFileParams> {
185185
const documentSnapshot = await this.getSnapshot(uri);
186186
ranges = getParamRanges(options.input, documentSnapshot);
187187
const languageId = documentSnapshot.languageId;
188-
if (options.chatRequestId !== undefined && uri.scheme === 'file' && (languageId === 'typescript' || languageId === 'javascript')) {
188+
if (options.chatSessionResource !== undefined && options.chatRequestId !== undefined && uri.scheme === 'file' && (languageId === 'typescript' || languageId === 'javascript')) {
189189
const startLine = ranges.start - 1;
190190
const endLine = ranges.end - 1;
191191
try {
192-
const grepResultMatches = this.grepResultService.getGrepResult(options.chatRequestId, uri, startLine, endLine);
192+
const grepResultMatches = this.grepResultService.getGrepResult(options.chatSessionResource, uri, startLine, endLine);
193193
if (grepResultMatches !== undefined && grepResultMatches.length > 0 && documentSnapshot.version === documentSnapshot.document.version) {
194194
const regionResult: RegionResult | undefined = await this.regionContextProvider.getRegions(documentSnapshot.uri, documentSnapshot.languageId, grepResultMatches, { start: startLine, end: endLine});
195195
if (regionResult !== undefined && regionResult.regions.length > 0 && documentSnapshot.version === documentSnapshot.document.version) {

‎extensions/copilot/src/extension/tools/node/test/grepResultService.spec.ts‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { GrepResultService, NullGrepResultService } from '../grepResultService';
1111

1212
suite('GrepResultService', () => {
1313
const uri = URI.file('/file.ts');
14+
const sessionUri = URI.file('/session');
1415

1516
function createMatch(range: vscode.Range): vscode.TextSearchMatch2 {
1617
return {
@@ -23,23 +24,52 @@ suite('GrepResultService', () => {
2324
};
2425
}
2526

26-
test('returns all ranges within the inclusive line bounds', () => {
27+
test('returns all ranges overlapping the inclusive line bounds', () => {
28+
const overlappingStart = new Range(2, 2, 4, 1);
2729
const before = new Range(3, 0, 3, 1);
2830
const first = new Range(4, 2, 4, 5);
2931
const second = new Range(8, 1, 8, 7);
3032
const after = new Range(9, 0, 9, 1);
3133
const service = new GrepResultService();
32-
service.addGrepResult('request', {
33-
files: [{ uri, matches: [before, first, second, after].map(createMatch) }]
34+
service.addGrepResult(sessionUri, 'request', {
35+
files: [{ uri, matches: [overlappingStart, before, first, second, after].map(createMatch) }]
3436
});
3537

36-
expect(service.getGrepResult('request', uri, 4, 8)).toEqual([first, second]);
38+
expect(service.getGrepResult(sessionUri, uri, 4, 8)).toEqual([overlappingStart, first, second]);
39+
});
40+
41+
test('returns unique ranges starting with the latest grep result', () => {
42+
const older = new Range(4, 2, 4, 5);
43+
const duplicate = new Range(6, 1, 6, 7);
44+
const latest = new Range(8, 0, 8, 3);
45+
const service = new GrepResultService();
46+
service.addGrepResult(sessionUri, 'first-request', {
47+
files: [{ uri, matches: [older, duplicate].map(createMatch) }]
48+
});
49+
service.addGrepResult(sessionUri, 'second-request', {
50+
files: [{ uri, matches: [duplicate, latest].map(createMatch) }]
51+
});
52+
53+
expect(service.getGrepResult(sessionUri, uri, 0, 10)).toEqual([duplicate, latest, older]);
54+
});
55+
56+
test('fires the session URI and request ID when the oldest grep result is removed', () => {
57+
const service = new GrepResultService();
58+
const removedResults: { sessionUri: vscode.Uri; requestId: string }[] = [];
59+
service.onDidRemoveGrepResult(result => removedResults.push(result));
60+
61+
for (let i = 0; i < 17; i++) {
62+
service.addGrepResult(sessionUri, `request-${i}`, { files: [] });
63+
}
64+
65+
expect(removedResults).toEqual([{ sessionUri, requestId: 'request-0' }]);
66+
service.dispose();
3767
});
3868

3969
test('returns undefined when no results are available', () => {
4070
const service = new GrepResultService();
4171

42-
expect(service.getGrepResult('unknown', uri, 0, 10)).toBeUndefined();
43-
expect(new NullGrepResultService().getGrepResult('request', uri, 0, 10)).toBeUndefined();
72+
expect(service.getGrepResult(sessionUri, uri, 0, 10)).toBeUndefined();
73+
expect(new NullGrepResultService().getGrepResult(sessionUri, uri, 0, 10)).toBeUndefined();
4474
});
4575
});

‎src/vs/editor/common/cursor/cursorTypeEditOperations.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ export class AutoClosingOpenCharTypeOperation {
199199
}
200200
let autoCloseConfig: EditorAutoClosingStrategy;
201201
let shouldAutoCloseBefore: (ch: string) => boolean;
202+
let shouldCheckBracketBalance = false;
202203

203204
const chIsQuote = isQuote(ch);
204205
if (chIsQuote) {
@@ -212,6 +213,7 @@ export class AutoClosingOpenCharTypeOperation {
212213
} else {
213214
autoCloseConfig = config.autoClosingBrackets;
214215
shouldAutoCloseBefore = config.shouldAutoCloseBefore.bracket;
216+
shouldCheckBracketBalance = true;
215217
}
216218
}
217219
if (autoCloseConfig === 'never') {
@@ -242,6 +244,16 @@ export class AutoClosingOpenCharTypeOperation {
242244
return null;
243245
}
244246
}
247+
if (
248+
shouldCheckBracketBalance
249+
// When 'always', always insert the closing bracket
250+
&& autoCloseConfig !== 'always'
251+
// Need to check character is not already typed so brackets are still imbalanced
252+
&& !chIsAlreadyTyped
253+
&& model.bracketPairs.hasUnmatchedClosingBracketAfter(new Position(lineNumber, beforeColumn), pair.open)
254+
) {
255+
return null;
256+
}
245257
// Do not auto-close ' or " after a word character
246258
if (pair.open.length === 1 && (ch === '\'' || ch === '"') && autoCloseConfig !== 'always') {
247259
const wordSeparators = getMapForWordSeparators(config.wordSeparators, []);

‎src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsImpl.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ export class BracketPairsTextModelPart extends Disposable implements IBracketPai
118118
return this.bracketPairsTree.value?.object.getBracketsInRange(range, onlyColorizedBrackets) || CallbackIterable.empty;
119119
}
120120

121+
public hasUnmatchedClosingBracketAfter(_position: IPosition, openingBracket: string): boolean {
122+
const position = this.textModel.validatePosition(_position);
123+
const languageId = this.textModel.getLanguageIdAtPosition(position.lineNumber, position.column);
124+
const openingBracketInfo = this.languageConfigurationService
125+
.getLanguageConfiguration(languageId)
126+
.bracketsNew.getOpeningBracketInfo(openingBracket);
127+
if (!openingBracketInfo) {
128+
return false;
129+
}
130+
this.bracketsRequested = true;
131+
this.updateBracketPairsTree();
132+
return this.bracketPairsTree.value?.object.hasUnmatchedClosingBracketAfter(position, openingBracketInfo) ?? false;
133+
}
134+
121135
public findMatchingBracketUp(_bracket: string, _position: IPosition, maxDuration?: number): Range | null {
122136
const position = this.textModel.validatePosition(_position);
123137
const languageId = this.textModel.getLanguageIdAtPosition(position.lineNumber, position.column);

0 commit comments

Comments
 (0)