Skip to content

[pull] main from microsoft:main #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 23, 2025
11 changes: 11 additions & 0 deletions src/vs/editor/common/services/findSectionHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { IRange } from '../core/range.js';
import { FoldingRules } from '../languages/languageConfiguration.js';
import { isMultilineRegexSource } from '../model/textModelSearch.js';
import { regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';

export interface ISectionHeaderFinderTarget {
getLineCount(): number;
Expand Down Expand Up @@ -90,13 +91,23 @@ export function collectMarkHeaders(model: ISectionHeaderFinderTarget, options: F
const markHeaders: SectionHeader[] = [];
const endLineNumber = model.getLineCount();

// Validate regex to prevent infinite loops
if (!options.markSectionHeaderRegex || options.markSectionHeaderRegex.trim() === '') {
return markHeaders;
}

// Create regex with flags for:
// - 'd' for indices to get proper match positions
// - 'm' for multi-line mode so ^ and $ match line starts/ends
// - 's' for dot-all mode so . matches newlines
const multiline = isMultilineRegexSource(options.markSectionHeaderRegex);
const regex = new RegExp(options.markSectionHeaderRegex, `gdm${multiline ? 's' : ''}`);

// Check if the regex would lead to an endless loop
if (regExpLeadsToEndlessLoop(regex)) {
return markHeaders;
}

// Process text in overlapping chunks for better performance
for (let startLine = 1; startLine <= endLineNumber; startLine += CHUNK_SIZE - MAX_SECTION_LINES) {
const endLine = Math.min(startLine + CHUNK_SIZE - 1, endLineNumber);
Expand Down
34 changes: 34 additions & 0 deletions src/vs/editor/test/common/services/findSectionHeaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,40 @@ suite('FindSectionHeaders', () => {
assert.strictEqual(headers[1].range.endLineNumber, 103);
});

test('handles empty regex gracefully without infinite loop', () => {
const model = new TestSectionHeaderFinderTarget([
'line 1',
'line 2',
'line 3'
]);

const options: FindSectionHeaderOptions = {
findRegionSectionHeaders: false,
findMarkSectionHeaders: true,
markSectionHeaderRegex: '' // Empty string that would cause infinite loop
};

const headers = findSectionHeaders(model, options);
assert.strictEqual(headers.length, 0, 'Should return no headers for empty regex');
});

test('handles whitespace-only regex gracefully without infinite loop', () => {
const model = new TestSectionHeaderFinderTarget([
'line 1',
'line 2',
'line 3'
]);

const options: FindSectionHeaderOptions = {
findRegionSectionHeaders: false,
findMarkSectionHeaders: true,
markSectionHeaderRegex: ' ' // Whitespace that would cause infinite loop
};

const headers = findSectionHeaders(model, options);
assert.strictEqual(headers.length, 0, 'Should return no headers for whitespace-only regex');
});

test('correctly advances past matches without infinite loop', () => {
const model = new TestSectionHeaderFinderTarget([
'// ==========',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
&& !(verificationStatus === ExtensionSignatureVerificationCode.NotSigned && !shouldRequireSignature)
&& verifySignature
&& this.environmentService.isBuilt
&& (await this.getTargetPlatform()) !== TargetPlatform.LINUX_ARMHF
) {
try {
await this.extensionsDownloader.delete(location);
Expand Down
1 change: 0 additions & 1 deletion src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,6 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread
: reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected ? 'rejected'
: 'ignored'
};
console.log('Inline completion end of life:', endOfLifeSummary);
this._telemetryService.publicLog2<InlineCompletionEndOfLifeEvent, InlineCompletionsEndOfLifeClassification>('inlineCompletion.endOfLife', endOfLifeSummary);
},
disposeInlineCompletions: (completions: IdentifiableInlineCompletions, reason: languages.InlineCompletionsDisposeReason): void => {
Expand Down
36 changes: 23 additions & 13 deletions src/vs/workbench/contrib/chat/browser/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ export interface IChatWidgetContrib extends IDisposable {
setInputState?(s: any): void;
}

interface IChatRequestInputOptions {
input: string;
attachedContext: ChatRequestVariableSet;
}

export interface IChatWidgetLocationOptions {
location: ChatAgentLocation;
resolveData?(): IChatLocationData | undefined;
Expand Down Expand Up @@ -1216,7 +1221,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
return undefined;
}

private async _applyPromptFileIfSet(requestInput: { input: string; attachedContext: ChatRequestVariableSet }): Promise<IPromptParserResult | undefined> {
private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise<IPromptParserResult | undefined> {

let parseResult: IPromptParserResult | undefined;

Expand All @@ -1242,16 +1247,17 @@ export class ChatWidget extends Disposable implements IChatWidget {
if (!parseResult) {
return undefined;
}
const meta = parseResult.metadata;
if (meta?.promptType !== PromptsType.prompt) {
return undefined;
}

if (!requestInput.input.trim()) {
// NOTE this is a prompt and therefore not localized
requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()} )`;
}

const meta = parseResult.metadata;
if (meta?.promptType === PromptsType.prompt) {
await this._applyPromptMetadata(meta);
}
await this._applyPromptMetadata(meta, requestInput);

return parseResult;
}
Expand All @@ -1277,9 +1283,9 @@ export class ChatWidget extends Disposable implements IChatWidget {

const editorValue = this.getInput();
const requestId = this.chatAccessibilityService.acceptRequest();
const requestInputs = {
const requestInputs: IChatRequestInputOptions = {
input: !query ? editorValue : query.query,
attachedContext: this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId),
attachedContext: this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId)
};

const isUserQuery = !query;
Expand All @@ -1288,7 +1294,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
if (instructionsEnabled) {
// process the prompt command
await this._applyPromptFileIfSet(requestInputs);
await this._autoAttachInstructions(requestInputs.attachedContext);
await this._autoAttachInstructions(requestInputs);
}

if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentMode === ChatMode.Edit && !this.chatService.edits2Enabled) {
Expand Down Expand Up @@ -1573,11 +1579,10 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.agentInInput.set(!!currentAgent);
}

private async _applyPromptMetadata(metadata: TPromptMetadata): Promise<void> {
private async _applyPromptMetadata(metadata: TPromptMetadata, requestInput: IChatRequestInputOptions): Promise<void> {

const { mode, tools } = metadata;

// switch to appropriate chat mode if needed
if (mode && mode !== this.inputPart.currentMode) {
const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, this.inputPart.currentMode, mode, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model.editingSession);
if (!chatModeCheck) {
Expand Down Expand Up @@ -1623,9 +1628,14 @@ export class ChatWidget extends Disposable implements IChatWidget {
* - instructions referenced in the copilot settings 'copilot-instructions*
* - instructions referenced in an already included instruction file
*/
private async _autoAttachInstructions(attachedContext: ChatRequestVariableSet): Promise<void> {
const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions);
await computer.collect(attachedContext, true, CancellationToken.None);
private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise<void> {
let readFileTool = this.toolsService.getToolByName('readFile');
if (readFileTool && this.getUserSelectedTools()?.[readFileTool.id] === false) {
readFileTool = undefined;
}

const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, readFileTool);
await computer.collect(attachedContext, CancellationToken.None);

// add to attached list to make the instructions sticky
//this.inputPart.attachmentModel.addContext(...computer.autoAddedInstructions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { ILabelService } from '../../../../../platform/label/common/label.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { ChatRequestVariableSet, IChatRequestVariableEntry, IPromptFileVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry } from '../chatVariableEntries.js';
import { IToolData } from '../languageModelToolsService.js';
import { PromptsConfig } from './config/config.js';
import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME } from './config/promptFileLocations.js';
import { PromptsType } from './promptTypes.js';
Expand All @@ -29,6 +30,7 @@ export class ComputeAutomaticInstructions {
private _autoAddedInstructions: IPromptFileVariableEntry[] = [];

constructor(
private readonly _readFileTool: IToolData | undefined,
@IPromptsService private readonly _promptsService: IPromptsService,
@ILogService public readonly _logService: ILogService,
@ILabelService private readonly _labelService: ILabelService,
Expand All @@ -51,7 +53,7 @@ export class ComputeAutomaticInstructions {
return result;
}

public async collect(variables: ChatRequestVariableSet, addInstructionsSummary: boolean, token: CancellationToken): Promise<void> {
public async collect(variables: ChatRequestVariableSet, token: CancellationToken): Promise<void> {
const instructionFiles = await this._promptsService.listPromptFiles(PromptsType.instructions, token);

this._logService.trace(`[InstructionsContextComputer] ${instructionFiles.length} instruction files available.`);
Expand All @@ -72,7 +74,7 @@ export class ComputeAutomaticInstructions {
this._logService.trace(`[InstructionsContextComputer] ${copilotInstructions.files.size} Copilot instructions files added.`);

const copilotInstructionsFromSettings = this._getCopilotTextInstructions(copilotInstructions.instructionMessages);
const instructionsWithPatternsList = addInstructionsSummary ? await this._getInstructionsWithPatternsList(instructionFiles, variables, token) : [];
const instructionsWithPatternsList = await this._getInstructionsWithPatternsList(instructionFiles, variables, token);

if (copilotInstructionsFromSettings.length + instructionsWithPatternsList.length > 0) {
const text = `${copilotInstructionsFromSettings.join('\n')}\n\n${instructionsWithPatternsList.join('\n')}`;
Expand Down Expand Up @@ -220,6 +222,11 @@ export class ComputeAutomaticInstructions {
}

private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise<string[]> {
if (!this._readFileTool) {
this._logService.trace('[InstructionsContextComputer] No readFile tool available, skipping instructions with patterns list.');
return [];
}

const entries: string[] = [];
for (const instructionFile of instructionFiles) {
const { metadata, uri } = await this._parsePromptFile(instructionFile.uri, token);
Expand All @@ -235,11 +242,13 @@ export class ComputeAutomaticInstructions {
if (entries.length === 0) {
return entries;
}

const toolName = 'read_file'; // workaround https://github.com/microsoft/vscode/issues/252167
return [
'Here is a list of instruction files that contain rules for modifying or creating new code.',
'These files are important for ensuring that the code is modified or created correctly.',
'Please make sure to follow the rules specified in these files when working with the codebase.',
'If the file is not already available as attachment, use the `read_file` tool to acquire it.',
`If the file is not already available as attachment, use the \`${toolName}\` tool to acquire it.`,
'Make sure to acquire the instructions before making any changes to the code.',
'| Pattern | File Path | Description |',
'| ------- | --------- | ----------- |',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -875,7 +875,7 @@ suite('PromptsService', () => {
])).mock();

const instructionFiles = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
const contextComputer = instaService.createInstance(ComputeAutomaticInstructions);
const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined);
const context = {
files: new ResourceSet([
URI.joinPath(rootFolderUri, 'folder1/main.tsx'),
Expand Down Expand Up @@ -1060,7 +1060,7 @@ suite('PromptsService', () => {
])).mock();

const instructionFiles = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
const contextComputer = instaService.createInstance(ComputeAutomaticInstructions);
const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined);
const context = {
files: new ResourceSet([
URI.joinPath(rootFolderUri, 'folder1/main.tsx'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export class UnresolvedCommentsBadge extends Disposable implements IWorkbenchCon
super();
this._register(this._commentService.onDidSetAllCommentThreads(this.onAllCommentsChanged, this));
this._register(this._commentService.onDidUpdateCommentThreads(this.onCommentsUpdated, this));

this._register(this._commentService.onDidDeleteDataProvider(this.onCommentsUpdated, this));
}

private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void {
Expand Down
77 changes: 77 additions & 0 deletions src/vs/workbench/contrib/testing/browser/testExplorerActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1583,6 +1583,81 @@ export class CoverageLastRun extends RunOrDebugLastRun {
}
}

abstract class RunOrDebugFailedFromLastRun extends Action2 {
constructor(options: IAction2Options) {
super({
...options,
menu: {
id: MenuId.CommandPalette,
when: ContextKeyExpr.and(
hasAnyTestProvider,
TestingContextKeys.hasAnyResults.isEqualTo(true),
),
},
});
}

protected abstract getGroup(): TestRunProfileBitset;

/** @inheritdoc */
public override async run(accessor: ServicesAccessor, runId?: string) {
const resultService = accessor.get(ITestResultService);
const testService = accessor.get(ITestService);
const progressService = accessor.get(IProgressService);

const lastResult = runId ? resultService.results.find(r => r.id === runId) : resultService.results[0];
if (!lastResult) {
return;
}

const failedTestIds = new Set<string>();
for (const test of lastResult.tests) {
if (isFailedState(test.ownComputedState)) {
failedTestIds.add(test.item.extId);
}
}

if (failedTestIds.size === 0) {
return;
}

await discoverAndRunTests(
testService.collection,
progressService,
Array.from(failedTestIds),
tests => testService.runTests({ tests, group: this.getGroup() }),
);
}
}

export class ReRunFailedFromLastRun extends RunOrDebugFailedFromLastRun {
constructor() {
super({
id: TestCommandId.ReRunFailedFromLastRun,
title: localize2('testing.reRunFailedFromLastRun', 'Rerun Failed Tests from Last Run'),
category,
});
}

protected override getGroup(): TestRunProfileBitset {
return TestRunProfileBitset.Run;
}
}

export class DebugFailedFromLastRun extends RunOrDebugFailedFromLastRun {
constructor() {
super({
id: TestCommandId.DebugFailedFromLastRun,
title: localize2('testing.debugFailedFromLastRun', 'Debug Failed Tests from Last Run'),
category,
});
}

protected override getGroup(): TestRunProfileBitset {
return TestRunProfileBitset.Debug;
}
}

export class SearchForTestExtension extends Action2 {
constructor() {
super({
Expand Down Expand Up @@ -1962,4 +2037,6 @@ export const allTestActions = [
ToggleInlineTestOutput,
UnhideAllTestsAction,
UnhideTestAction,
ReRunFailedFromLastRun,
DebugFailedFromLastRun,
];
Loading
Loading