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' ;
2017import { 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' ;
2135import { ChangeSetElement } from '../common' ;
2236import { createChangeSetFileUri } from './change-set-file-resource' ;
2337import { 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
2841export const ChangeSetFileElementFactory = Symbol ( 'ChangeSetFileElementFactory' ) ;
2942export 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 {
0 commit comments