Skip to content

Commit 6ab4aac

Browse files
Enact code actions when change set file created (#15724)
Signed-off-by: Simon Graband <[email protected]> Co-authored-by: Simon Graband <[email protected]> Co-authored-by: Colin Grant <[email protected]>
1 parent 787071c commit 6ab4aac

File tree

9 files changed

+427
-156
lines changed

9 files changed

+427
-156
lines changed

packages/ai-chat/src/browser/change-set-file-element.ts

Lines changed: 168 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,29 @@
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
1616

17-
import { DisposableCollection, Emitter, URI } from '@theia/core';
18-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
19-
import { Replacement } from '@theia/core/lib/common/content-replacer';
2017
import { ConfigurableInMemoryResources, ConfigurableMutableReferenceResource } from '@theia/ai-core';
18+
import { CancellationToken, DisposableCollection, Emitter, URI } from '@theia/core';
19+
import { ConfirmDialog } from '@theia/core/lib/browser';
20+
import { Replacement } from '@theia/core/lib/common/content-replacer';
21+
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
22+
import { EditorPreferences } from '@theia/editor/lib/browser';
23+
import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
24+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
25+
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
26+
import { TrimTrailingWhitespaceCommand } from '@theia/monaco-editor-core/esm/vs/editor/common/commands/trimTrailingWhitespaceCommand';
27+
import { Selection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection';
28+
import { CommandExecutor } from '@theia/monaco-editor-core/esm/vs/editor/common/cursor/cursor';
29+
import { formatDocumentWithSelectedProvider, FormattingMode } from '@theia/monaco-editor-core/esm/vs/editor/contrib/format/browser/format';
30+
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
31+
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
32+
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
33+
import { insertFinalNewline } from '@theia/monaco/lib/browser/monaco-utilities';
34+
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
2135
import { ChangeSetElement } from '../common';
2236
import { createChangeSetFileUri } from './change-set-file-resource';
2337
import { ChangeSetFileService } from './change-set-file-service';
24-
import { FileService } from '@theia/filesystem/lib/browser/file-service';
25-
import { ConfirmDialog } from '@theia/core/lib/browser';
26-
import { ChangeSetDecoratorService } from './change-set-decorator-service';
38+
import { Deferred } from '@theia/core/lib/common/promise-util';
39+
import { MonacoCodeActionService } from '@theia/monaco/lib/browser';
2740

2841
export const ChangeSetFileElementFactory = Symbol('ChangeSetFileElementFactory');
2942
export type ChangeSetFileElementFactory = (elementProps: ChangeSetElementArgs) => ChangeSetFileElement;
@@ -62,28 +75,37 @@ export class ChangeSetFileElement implements ChangeSetElement {
6275
@inject(ChangeSetFileService)
6376
protected readonly changeSetFileService: ChangeSetFileService;
6477

65-
@inject(ChangeSetDecoratorService)
66-
protected readonly changeSetDecoratorService: ChangeSetDecoratorService;
67-
6878
@inject(FileService)
6979
protected readonly fileService: FileService;
7080

7181
@inject(ConfigurableInMemoryResources)
7282
protected readonly inMemoryResources: ConfigurableInMemoryResources;
7383

74-
@inject(ChangeSetFileElementFactory) protected readonly factory: ChangeSetFileElementFactory;
84+
@inject(MonacoTextModelService)
85+
protected readonly monacoTextModelService: MonacoTextModelService;
86+
87+
@inject(EditorPreferences)
88+
protected readonly editorPreferences: EditorPreferences;
89+
90+
@inject(FileSystemPreferences)
91+
protected readonly fileSystemPreferences: FileSystemPreferences;
92+
93+
@inject(MonacoCodeActionService)
94+
protected readonly codeActionService: MonacoCodeActionService;
7595

7696
protected readonly toDispose = new DisposableCollection();
7797
protected _state: ChangeSetElementState;
7898

7999
private _originalContent: string | undefined;
80100
protected _initialized = false;
81101
protected _initializationPromise: Promise<void> | undefined;
102+
protected _targetStateWithCodeActions: string | undefined;
103+
protected codeActionDeferred?: Deferred<string>;
82104

83105
protected readonly onDidChangeEmitter = new Emitter<void>();
84106
readonly onDidChange = this.onDidChangeEmitter.event;
85-
protected _readOnlyResource: ConfigurableMutableReferenceResource;
86-
protected _changeResource: ConfigurableMutableReferenceResource;
107+
protected _readOnlyResource?: ConfigurableMutableReferenceResource;
108+
protected _changeResource?: ConfigurableMutableReferenceResource;
87109

88110
@postConstruct()
89111
init(): void {
@@ -174,7 +196,8 @@ export class ChangeSetFileElement implements ChangeSetElement {
174196
protected get changeResource(): ConfigurableMutableReferenceResource {
175197
if (!this._changeResource) {
176198
this._changeResource = this.getInMemoryUri(createChangeSetFileUri(this.elementProps.chatSessionId, this.uri));
177-
this._changeResource.update({ autosaveable: false });
199+
this._changeResource.update({ autosaveable: false, contents: this.targetState });
200+
this.applyCodeActionsToTargetState();
178201
this.toDispose.push(this._changeResource);
179202
}
180203
return this._changeResource;
@@ -236,6 +259,10 @@ export class ChangeSetFileElement implements ChangeSetElement {
236259
}
237260

238261
get targetState(): string {
262+
return this._targetStateWithCodeActions ?? this.elementProps.targetState ?? '';
263+
}
264+
265+
get originalTargetState(): string {
239266
return this.elementProps.targetState ?? '';
240267
}
241268

@@ -255,14 +282,16 @@ export class ChangeSetFileElement implements ChangeSetElement {
255282
async apply(contents?: string): Promise<void> {
256283
await this.ensureInitialized();
257284
if (!await this.confirm('Apply')) { return; }
258-
if (!(await this.changeSetFileService.trySave(this.changedUri))) {
259-
if (this.type === 'delete') {
260-
await this.changeSetFileService.delete(this.uri);
261-
this.state = 'applied';
262-
} else {
263-
await this.writeChanges(contents);
264-
}
285+
286+
if (this.type === 'delete') {
287+
await this.changeSetFileService.delete(this.uri);
288+
this.state = 'applied';
289+
this.changeSetFileService.closeDiff(this.readOnlyUri);
290+
return;
265291
}
292+
293+
// Load Monaco model for the base file URI and apply changes
294+
await this.applyChangesWithMonaco(contents);
266295
this.changeSetFileService.closeDiff(this.readOnlyUri);
267296
}
268297

@@ -271,9 +300,124 @@ export class ChangeSetFileElement implements ChangeSetElement {
271300
this.state = 'applied';
272301
}
273302

303+
/**
304+
* Applies changes using Monaco utilities, including loading the model for the base file URI,
305+
* setting the value to the intended state, and running code actions on save.
306+
*/
307+
protected async applyChangesWithMonaco(contents?: string): Promise<void> {
308+
let modelReference: IReference<MonacoEditorModel> | undefined;
309+
310+
try {
311+
modelReference = await this.monacoTextModelService.createModelReference(this.uri);
312+
const model = modelReference.object;
313+
const targetContent = contents ?? this.targetState;
314+
model.textEditorModel.setValue(targetContent);
315+
316+
const languageId = model.languageId;
317+
const uriStr = this.uri.toString();
318+
319+
await this.codeActionService.applyOnSaveCodeActions(model.textEditorModel, languageId, uriStr, CancellationToken.None);
320+
await this.applyFormatting(model, languageId, uriStr);
321+
322+
await model.save();
323+
this.state = 'applied';
324+
325+
} catch (error) {
326+
console.error('Failed to apply changes with Monaco:', error);
327+
await this.writeChanges(contents);
328+
} finally {
329+
modelReference?.dispose();
330+
}
331+
}
332+
333+
protected applyCodeActionsToTargetState(): Promise<string> {
334+
if (!this.codeActionDeferred) {
335+
this.codeActionDeferred = new Deferred();
336+
this.codeActionDeferred.resolve(this.doApplyCodeActionsToTargetState());
337+
}
338+
return this.codeActionDeferred.promise;
339+
}
340+
341+
protected async doApplyCodeActionsToTargetState(): Promise<string> {
342+
const targetState = this.originalTargetState;
343+
if (!targetState) {
344+
this._targetStateWithCodeActions = '';
345+
return this._targetStateWithCodeActions;
346+
}
347+
348+
let tempResource: ConfigurableMutableReferenceResource | undefined;
349+
let tempModel: IReference<MonacoEditorModel> | undefined;
350+
try {
351+
// Create a temporary model to apply code actions
352+
const tempUri = new URI(`untitled://changeset/${Date.now()}${this.uri.path.ext}`);
353+
tempResource = this.inMemoryResources.add(tempUri, { contents: this.targetState });
354+
tempModel = await this.monacoTextModelService.createModelReference(tempUri);
355+
tempModel.object.suppressOpenEditorWhenDirty = true;
356+
tempModel.object.textEditorModel.setValue(this.targetState);
357+
358+
const languageId = tempModel.object.languageId;
359+
const uriStr = this.uri.toString();
360+
361+
await this.codeActionService.applyOnSaveCodeActions(tempModel.object.textEditorModel, languageId, uriStr, CancellationToken.None);
362+
363+
// Apply formatting and other editor preferences
364+
await this.applyFormatting(tempModel.object, languageId, uriStr);
365+
366+
this._targetStateWithCodeActions = tempModel.object.textEditorModel.getValue();
367+
if (this._changeResource?.contents === this.elementProps.targetState) {
368+
this._changeResource?.update({ contents: this.targetState });
369+
}
370+
} catch (error) {
371+
console.warn('Failed to apply code actions to target state:', error);
372+
this._targetStateWithCodeActions = targetState;
373+
} finally {
374+
tempModel?.dispose();
375+
tempResource?.dispose();
376+
}
377+
378+
return this.targetState;
379+
}
380+
381+
/**
382+
* Applies formatting preferences like format on save, trim trailing whitespace, and insert final newline.
383+
*/
384+
protected async applyFormatting(model: MonacoEditorModel, languageId: string, uriStr: string): Promise<void> {
385+
try {
386+
const formatOnSave = this.editorPreferences.get({ preferenceName: 'editor.formatOnSave', overrideIdentifier: languageId }, undefined, uriStr);
387+
if (formatOnSave) {
388+
const instantiation = StandaloneServices.get(IInstantiationService);
389+
await instantiation.invokeFunction(
390+
formatDocumentWithSelectedProvider,
391+
model.textEditorModel,
392+
FormattingMode.Explicit,
393+
{ report(): void { } },
394+
CancellationToken.None, true
395+
);
396+
}
397+
398+
const trimTrailingWhitespace = this.fileSystemPreferences.get({ preferenceName: 'files.trimTrailingWhitespace', overrideIdentifier: languageId }, undefined, uriStr);
399+
if (trimTrailingWhitespace) {
400+
const ttws = new TrimTrailingWhitespaceCommand(new Selection(1, 1, 1, 1), [], false);
401+
CommandExecutor.executeCommands(model.textEditorModel, [], [ttws]);
402+
}
403+
404+
const shouldInsertFinalNewline = this.fileSystemPreferences.get({ preferenceName: 'files.insertFinalNewline', overrideIdentifier: languageId }, undefined, uriStr);
405+
if (shouldInsertFinalNewline) {
406+
insertFinalNewline(model);
407+
}
408+
} catch (error) {
409+
console.warn('Failed to apply formatting:', error);
410+
}
411+
}
412+
274413
onShow(): void {
275-
// Ensure we have the latest state when showing
276-
this.changeResource.update({ contents: this.targetState, onSave: content => this.writeChanges(content) });
414+
this.changeResource.update({
415+
contents: this.targetState,
416+
onSave: async content => {
417+
// Use Monaco utilities when saving from the change resource
418+
await this.applyChangesWithMonaco(content);
419+
}
420+
});
277421
}
278422

279423
async revert(): Promise<void> {
@@ -290,11 +434,11 @@ export class ChangeSetFileElement implements ChangeSetElement {
290434
async confirm(verb: string): Promise<boolean> {
291435
if (this._state !== 'stale') { return true; }
292436
await this.openChange();
293-
const thing = await new ConfirmDialog({
437+
const answer = await new ConfirmDialog({
294438
title: `${verb} suggestion.`,
295439
msg: `The file ${this.uri.path.toString()} has changed since this suggestion was created. Are you certain you wish to ${verb.toLowerCase()} the change?`
296440
}).open(true);
297-
return !!thing;
441+
return !!answer;
298442
}
299443

300444
dispose(): void {

packages/ai-chat/src/common/chat-session-summary-agent-prompt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const CHAT_SESSION_SUMMARY_PROMPT = {
2323
'Ensure that the summary is sufficiently comprehensive to allow seamless continuation of the workflow. ' +
2424
'The summary will primarily be used by other AI agents, so tailor your response for use by AI agents. ' +
2525
'Also consider the system message. ' +
26-
'Make sure you include all necessary context information and use unique references(such as URIs, file paths, etc.). ' +
26+
'Make sure you include all necessary context information and use unique references (such as URIs, file paths, etc.). ' +
2727
'If the conversation was about a task, describe the state of the task, i.e.what has been completed and what is open. ' +
2828
'If a changeset is open in the session, describe the state of the suggested changes. ' +
2929
`\n\n{{${CHANGE_SET_SUMMARY_VARIABLE_ID}}}`,

packages/ai-core/src/common/configurable-in-memory-resources.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ export class ConfigurableMutableResource implements Resource {
8989
return !!this.options?.initiallyDirty;
9090
}
9191

92+
get contents(): string | Promise<string> {
93+
return this.options?.contents ?? '';
94+
}
95+
9296
readContents(): Promise<string> {
9397
return Promise.resolve(this.options?.contents ?? '');
9498
}
@@ -153,4 +157,8 @@ export class ConfigurableMutableReferenceResource implements Resource {
153157
get autosaveable(): boolean {
154158
return this.reference.object.autosaveable;
155159
}
160+
161+
get contents(): string | Promise<string> {
162+
return this.reference.object.contents;
163+
}
156164
}

packages/monaco/src/browser/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
// *****************************************************************************
1616

1717
export * from './monaco-frontend-module';
18+
export * from './monaco-code-action-service';

0 commit comments

Comments
 (0)