@theia/monaco 1.62.0-next.3 → 1.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/lib/browser/monaco-code-action-save-participant.d.ts +14 -0
  2. package/lib/browser/monaco-code-action-save-participant.d.ts.map +1 -0
  3. package/lib/browser/monaco-code-action-save-participant.js +129 -0
  4. package/lib/browser/monaco-code-action-save-participant.js.map +1 -0
  5. package/lib/browser/monaco-editor-model.d.ts +8 -13
  6. package/lib/browser/monaco-editor-model.d.ts.map +1 -1
  7. package/lib/browser/monaco-editor-model.js +21 -57
  8. package/lib/browser/monaco-editor-model.js.map +1 -1
  9. package/lib/browser/monaco-editor-provider.d.ts +18 -7
  10. package/lib/browser/monaco-editor-provider.d.ts.map +1 -1
  11. package/lib/browser/monaco-editor-provider.js +98 -51
  12. package/lib/browser/monaco-editor-provider.js.map +1 -1
  13. package/lib/browser/monaco-frontend-module.d.ts.map +1 -1
  14. package/lib/browser/monaco-frontend-module.js +4 -0
  15. package/lib/browser/monaco-frontend-module.js.map +1 -1
  16. package/lib/browser/monaco-menu.d.ts.map +1 -1
  17. package/lib/browser/monaco-menu.js +5 -4
  18. package/lib/browser/monaco-menu.js.map +1 -1
  19. package/lib/browser/monaco-workspace.d.ts +1 -4
  20. package/lib/browser/monaco-workspace.d.ts.map +1 -1
  21. package/lib/browser/monaco-workspace.js +0 -6
  22. package/lib/browser/monaco-workspace.js.map +1 -1
  23. package/package.json +9 -9
  24. package/src/browser/monaco-code-action-save-participant.ts +143 -0
  25. package/src/browser/monaco-editor-model.ts +25 -74
  26. package/src/browser/monaco-editor-provider.ts +115 -55
  27. package/src/browser/monaco-frontend-module.ts +6 -1
  28. package/src/browser/monaco-menu.ts +5 -4
  29. package/src/browser/monaco-workspace.ts +1 -9
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@theia/monaco",
3
- "version": "1.62.0-next.3+907647f99",
3
+ "version": "1.62.0",
4
4
  "description": "Theia - Monaco Extension",
5
5
  "dependencies": {
6
- "@theia/core": "1.62.0-next.3+907647f99",
7
- "@theia/editor": "1.62.0-next.3+907647f99",
8
- "@theia/filesystem": "1.62.0-next.3+907647f99",
9
- "@theia/markers": "1.62.0-next.3+907647f99",
6
+ "@theia/core": "1.62.0",
7
+ "@theia/editor": "1.62.0",
8
+ "@theia/filesystem": "1.62.0",
9
+ "@theia/markers": "1.62.0",
10
10
  "@theia/monaco-editor-core": "1.96.302",
11
- "@theia/outline-view": "1.62.0-next.3+907647f99",
12
- "@theia/workspace": "1.62.0-next.3+907647f99",
11
+ "@theia/outline-view": "1.62.0",
12
+ "@theia/workspace": "1.62.0",
13
13
  "fast-plist": "^0.1.2",
14
14
  "idb": "^4.0.5",
15
15
  "jsonc-parser": "^2.2.0",
@@ -52,10 +52,10 @@
52
52
  "watch": "theiaext watch"
53
53
  },
54
54
  "devDependencies": {
55
- "@theia/ext-scripts": "1.61.0"
55
+ "@theia/ext-scripts": "1.62.0"
56
56
  },
57
57
  "nyc": {
58
58
  "extends": "../../configs/nyc.json"
59
59
  },
60
- "gitHead": "907647f99bd78541b18e8e6133acc9246d85a8a2"
60
+ "gitHead": "a4e035ccc26fde24c2769a466cb18e3b54c517c5"
61
61
  }
@@ -0,0 +1,143 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2025 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { CancellationToken } from '@theia/core';
18
+ import { SaveOptions, SaveReason } from '@theia/core/lib/browser';
19
+ import { MonacoEditor } from './monaco-editor';
20
+ import { SaveParticipant, SAVE_PARTICIPANT_DEFAULT_ORDER } from './monaco-editor-provider';
21
+ import { inject, injectable } from '@theia/core/shared/inversify';
22
+ import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
23
+ import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
24
+ import { CodeActionKind, CodeActionSet, CodeActionTriggerSource } from '@theia/monaco-editor-core/esm/vs/editor/contrib/codeAction/common/types';
25
+ import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from '@theia/monaco-editor-core/esm/vs/editor/contrib/codeAction/browser/codeAction';
26
+
27
+ import { HierarchicalKind } from '@theia/monaco-editor-core/esm/vs/base/common/hierarchicalKind';
28
+ import { EditorPreferences } from '@theia/editor/lib/browser';
29
+ import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
30
+ import { CodeActionProvider, CodeActionTriggerType } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
31
+ import { IProgress } from '@theia/monaco-editor-core/esm/vs/platform/progress/common/progress';
32
+ import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
33
+ /*---------------------------------------------------------------------------------------------
34
+ * Copyright (c) Microsoft Corporation. All rights reserved.
35
+ * Licensed under the MIT License. See License.txt in the project root for license information.
36
+ *--------------------------------------------------------------------------------------------*/
37
+ // Partially copied from https://github.com/microsoft/vscode/blob/f66e839a38dfe39ee66a86619a790f9c2336d698/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts#L272
38
+ @injectable()
39
+ export class MonacoCodeActionSaveParticipant implements SaveParticipant {
40
+ @inject(EditorPreferences)
41
+ protected readonly editorPreferences: EditorPreferences;
42
+
43
+ readonly order = SAVE_PARTICIPANT_DEFAULT_ORDER;
44
+
45
+ async applyChangesOnSave(editor: MonacoEditor, cancellationToken: CancellationToken, options?: SaveOptions): Promise<void> {
46
+ if (options?.saveReason !== SaveReason.Manual) {
47
+ return undefined;
48
+ }
49
+
50
+ const setting = this.editorPreferences.get({
51
+ preferenceName: 'editor.codeActionsOnSave',
52
+ overrideIdentifier: editor.document.textEditorModel.getLanguageId()
53
+ }, undefined, editor.document.textEditorModel.uri.toString());
54
+
55
+ if (!setting) {
56
+ return undefined;
57
+ }
58
+
59
+ const settingItems: string[] = Array.isArray(setting)
60
+ ? setting
61
+ : Object.keys(setting).filter(x => setting[x]);
62
+
63
+ const codeActionsOnSave = this.createCodeActionsOnSave(settingItems);
64
+
65
+ if (!codeActionsOnSave.length) {
66
+ return undefined;
67
+ }
68
+
69
+ if (!Array.isArray(setting)) {
70
+ codeActionsOnSave.sort((a, b) => {
71
+ if (CodeActionKind.SourceFixAll.contains(a)) {
72
+ if (CodeActionKind.SourceFixAll.contains(b)) {
73
+ return 0;
74
+ }
75
+ return -1;
76
+ }
77
+ if (CodeActionKind.SourceFixAll.contains(b)) {
78
+ return 1;
79
+ }
80
+ return 0;
81
+ });
82
+ }
83
+
84
+ const excludedActions = Array.isArray(setting)
85
+ ? []
86
+ : Object.keys(setting)
87
+ .filter(x => setting[x] === false)
88
+ .map(x => new HierarchicalKind(x));
89
+
90
+ await this.applyOnSaveActions(editor.document.textEditorModel, codeActionsOnSave, excludedActions, cancellationToken);
91
+ }
92
+
93
+ private createCodeActionsOnSave(settingItems: readonly string[]): HierarchicalKind[] {
94
+ const kinds = settingItems.map(x => new HierarchicalKind(x));
95
+
96
+ // Remove subsets
97
+ return kinds.filter(kind => kinds.every(otherKind => otherKind.equals(kind) || !otherKind.contains(kind)));
98
+ }
99
+
100
+ private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly HierarchicalKind[],
101
+ excludes: readonly HierarchicalKind[], token: CancellationToken): Promise<void> {
102
+
103
+ const instantiationService = StandaloneServices.get(IInstantiationService);
104
+
105
+ for (const codeActionKind of codeActionsOnSave) {
106
+ const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token);
107
+
108
+ if (token.isCancellationRequested) {
109
+ actionsToRun.dispose();
110
+ return;
111
+ }
112
+
113
+ try {
114
+ for (const action of actionsToRun.validActions) {
115
+ await instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token);
116
+ if (token.isCancellationRequested) {
117
+ return;
118
+ }
119
+ }
120
+ } catch {
121
+ // Failure to apply a code action should not block other on save actions
122
+ } finally {
123
+ actionsToRun.dispose();
124
+ }
125
+ }
126
+ }
127
+
128
+ private getActionsToRun(model: ITextModel, codeActionKind: HierarchicalKind, excludes: readonly HierarchicalKind[], token: CancellationToken): Promise<CodeActionSet> {
129
+ const { codeActionProvider } = StandaloneServices.get(ILanguageFeaturesService);
130
+
131
+ const progress: IProgress<CodeActionProvider> = {
132
+ report(item): void {
133
+ // empty
134
+ },
135
+ };
136
+
137
+ return getCodeActions(codeActionProvider, model, model.getFullModelRange(), {
138
+ type: CodeActionTriggerType.Auto,
139
+ triggerAction: CodeActionTriggerSource.OnSave,
140
+ filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true },
141
+ }, progress, token);
142
+ }
143
+ }
@@ -20,11 +20,11 @@ import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposa
20
20
  import { Emitter, Event } from '@theia/core/lib/common/event';
21
21
  import { CancellationTokenSource, CancellationToken } from '@theia/core/lib/common/cancellation';
22
22
  import { Resource, ResourceError, ResourceVersion } from '@theia/core/lib/common/resource';
23
- import { Saveable, SaveOptions } from '@theia/core/lib/browser/saveable';
23
+ import { Saveable, SaveOptions, SaveReason } from '@theia/core/lib/browser/saveable';
24
24
  import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
25
25
  import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
26
26
  import { ILogger, Loggable, Log } from '@theia/core/lib/common/logger';
27
- import { IIdentifiedSingleEditOperation, ITextBufferFactory, ITextModel, ITextSnapshot } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
27
+ import { ITextBufferFactory, ITextModel, ITextSnapshot } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
28
28
  import { IResolvedTextEditorModel } from '@theia/monaco-editor-core/esm/vs/editor/common/services/resolverService';
29
29
  import * as monaco from '@theia/monaco-editor-core';
30
30
  import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
@@ -39,12 +39,7 @@ export {
39
39
  TextDocumentSaveReason
40
40
  };
41
41
 
42
- export interface WillSaveMonacoModelEvent {
43
- readonly model: MonacoEditorModel;
44
- readonly reason: TextDocumentSaveReason;
45
- readonly options?: SaveOptions;
46
- waitUntil(thenable: Thenable<IIdentifiedSingleEditOperation[]>): void;
47
- }
42
+ export type WillSaveMonacoModelListener = (model: MonacoEditorModel, token: CancellationToken, options?: SaveOptions) => Promise<void>;
48
43
 
49
44
  export interface MonacoModelContentChangedEvent {
50
45
  readonly model: MonacoEditorModel;
@@ -83,9 +78,6 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
83
78
  protected readonly onDidSaveModelEmitter = new Emitter<ITextModel>();
84
79
  readonly onDidSaveModel = this.onDidSaveModelEmitter.event;
85
80
 
86
- protected readonly onWillSaveModelEmitter = new Emitter<WillSaveMonacoModelEvent>();
87
- readonly onWillSaveModel = this.onWillSaveModelEmitter.event;
88
-
89
81
  protected readonly onDidChangeValidEmitter = new Emitter<void>();
90
82
  readonly onDidChangeValid = this.onDidChangeValidEmitter.event;
91
83
 
@@ -99,6 +91,8 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
99
91
 
100
92
  protected resourceVersion: ResourceVersion | undefined;
101
93
 
94
+ protected readonly willSaveModelListeners: WillSaveMonacoModelListener[] = [];
95
+
102
96
  constructor(
103
97
  protected readonly resource: Resource,
104
98
  protected readonly m2p: MonacoToProtocolConverter,
@@ -110,7 +104,6 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
110
104
  this.toDispose.push(this.toDisposeOnAutoSave);
111
105
  this.toDispose.push(this.onDidChangeContentEmitter);
112
106
  this.toDispose.push(this.onDidSaveModelEmitter);
113
- this.toDispose.push(this.onWillSaveModelEmitter);
114
107
  this.toDispose.push(this.onDirtyChangedEmitter);
115
108
  this.toDispose.push(this.onDidChangeValidEmitter);
116
109
  this.toDispose.push(Disposable.create(() => this.cancelSave()));
@@ -154,7 +147,7 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
154
147
  if (mode === EncodingMode.Decode) {
155
148
  return this.sync();
156
149
  }
157
- return this.scheduleSave(TextDocumentSaveReason.Manual, this.cancelSave(), true);
150
+ return this.scheduleSave(this.cancelSave(), true, { saveReason: SaveReason.Manual });
158
151
  }
159
152
 
160
153
  getEncoding(): string | undefined {
@@ -386,7 +379,10 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
386
379
  }
387
380
 
388
381
  save(options?: SaveOptions): Promise<void> {
389
- return this.scheduleSave(options?.saveReason ?? TextDocumentSaveReason.Manual, undefined, undefined, options);
382
+ return this.scheduleSave(undefined, undefined, {
383
+ saveReason: TextDocumentSaveReason.Manual,
384
+ ...options
385
+ });
390
386
  }
391
387
 
392
388
  protected pendingOperation = Promise.resolve();
@@ -485,8 +481,8 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
485
481
  return this.saveCancellationTokenSource.token;
486
482
  }
487
483
 
488
- protected scheduleSave(reason: TextDocumentSaveReason, token: CancellationToken = this.cancelSave(), overwriteEncoding?: boolean, options?: SaveOptions): Promise<void> {
489
- return this.run(() => this.doSave(reason, token, overwriteEncoding, options));
484
+ protected scheduleSave(token: CancellationToken = this.cancelSave(), overwriteEncoding?: boolean, options?: SaveOptions): Promise<void> {
485
+ return this.run(() => this.doSave(token, overwriteEncoding, options));
490
486
  }
491
487
 
492
488
  protected ignoreContentChanges = false;
@@ -546,18 +542,18 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
546
542
  }
547
543
  }
548
544
 
549
- protected async doSave(reason: TextDocumentSaveReason, token: CancellationToken, overwriteEncoding?: boolean, options?: SaveOptions): Promise<void> {
545
+ protected async doSave(token: CancellationToken, overwriteEncoding?: boolean, options?: SaveOptions): Promise<void> {
550
546
  if (token.isCancellationRequested || !this.resource.saveContents) {
551
547
  return;
552
548
  }
553
549
 
554
- await this.fireWillSaveModel(reason, token, options);
550
+ await this.fireWillSaveModel(token, options);
555
551
  if (token.isCancellationRequested) {
556
552
  return;
557
553
  }
558
554
 
559
555
  const changes = [...this.contentChanges];
560
- if ((changes.length === 0 && !this.resource.initiallyDirty) && !overwriteEncoding && reason !== TextDocumentSaveReason.Manual) {
556
+ if ((changes.length === 0 && !this.resource.initiallyDirty) && !overwriteEncoding && options?.saveReason !== TextDocumentSaveReason.Manual) {
561
557
  return;
562
558
  }
563
559
 
@@ -586,64 +582,19 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
586
582
  }
587
583
  }
588
584
 
589
- protected async fireWillSaveModel(reason: TextDocumentSaveReason, token: CancellationToken, options?: SaveOptions): Promise<void> {
590
- type EditContributor = Thenable<monaco.editor.IIdentifiedSingleEditOperation[]>;
591
-
592
- const firing = this.onWillSaveModelEmitter.sequence(async listener => {
593
- if (token.isCancellationRequested) {
594
- return false;
595
- }
596
- const waitables: EditContributor[] = [];
597
- const { version } = this;
598
-
599
- const event = {
600
- model: this, reason, options,
601
- waitUntil: (thenable: EditContributor) => {
602
- if (Object.isFrozen(waitables)) {
603
- throw new Error('waitUntil cannot be called asynchronously.');
604
- }
605
- waitables.push(thenable);
606
- }
607
- };
608
-
609
- // Fire.
610
- try {
611
- listener(event);
612
- } catch (err) {
613
- console.error(err);
614
- return true;
615
- }
616
-
617
- // Asynchronous calls to `waitUntil` should fail.
618
- Object.freeze(waitables);
619
-
620
- // Wait for all promises.
621
- const edits = await Promise.all(waitables).then(allOperations =>
622
- ([] as monaco.editor.IIdentifiedSingleEditOperation[]).concat(...allOperations)
623
- );
624
- if (token.isCancellationRequested) {
625
- return false;
626
- }
627
-
628
- // In a perfect world, we should only apply edits if document is clean.
629
- if (version !== this.version) {
630
- console.error('onWillSave listeners should provide edits, not directly alter the document.');
631
- }
632
-
633
- // Finally apply edits provided by this listener before firing the next.
634
- if (edits && edits.length > 0) {
635
- this.applyEdits(edits, {
636
- ignoreDirty: true,
637
- });
585
+ registerWillSaveModelListener(listener: WillSaveMonacoModelListener): Disposable {
586
+ this.willSaveModelListeners.push(listener);
587
+ return Disposable.create(() => {
588
+ const index = this.willSaveModelListeners.indexOf(listener);
589
+ if (index >= 0) {
590
+ this.willSaveModelListeners.splice(index, 1);
638
591
  }
639
-
640
- return true;
641
592
  });
593
+ }
642
594
 
643
- try {
644
- await firing;
645
- } catch (e) {
646
- console.error(e);
595
+ protected async fireWillSaveModel(token: CancellationToken, options?: SaveOptions): Promise<void> {
596
+ for (const listener of this.willSaveModelListeners) {
597
+ await listener(this, token, options);
647
598
  }
648
599
  }
649
600
 
@@ -18,26 +18,23 @@
18
18
  import URI from '@theia/core/lib/common/uri';
19
19
  import { EditorPreferenceChange, EditorPreferences, TextEditor, DiffNavigator } from '@theia/editor/lib/browser';
20
20
  import { DiffUris } from '@theia/core/lib/browser/diff-uris';
21
- import { inject, injectable, named } from '@theia/core/shared/inversify';
22
- import { DisposableCollection, deepClone, Disposable } from '@theia/core/lib/common';
23
- import { TextDocumentSaveReason } from '@theia/core/shared/vscode-languageserver-protocol';
21
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
22
+ import { DisposableCollection, deepClone, Disposable, CancellationToken } from '@theia/core/lib/common';
24
23
  import { MonacoDiffEditor } from './monaco-diff-editor';
25
24
  import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory';
26
25
  import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor';
27
- import { MonacoEditorModel, WillSaveMonacoModelEvent } from './monaco-editor-model';
26
+ import { MonacoEditorModel, TextDocumentSaveReason } from './monaco-editor-model';
28
27
  import { MonacoWorkspace } from './monaco-workspace';
29
28
  import { ContributionProvider } from '@theia/core';
30
- import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions, FormatType } from '@theia/core/lib/browser';
29
+ import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions, SaveOptions, FormatType } from '@theia/core/lib/browser';
31
30
  import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding';
32
31
  import { HttpOpenHandlerOptions } from '@theia/core/lib/browser/http-open-handler';
33
32
  import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
34
33
  import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
35
- import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
36
34
  import * as monaco from '@theia/monaco-editor-core';
37
35
  import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
38
36
  import { IOpenerService, OpenExternalOptions, OpenInternalOptions } from '@theia/monaco-editor-core/esm/vs/platform/opener/common/opener';
39
37
  import { IKeybindingService } from '@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding';
40
- import { timeoutReject } from '@theia/core/lib/common/promise-util';
41
38
  import { IContextMenuService } from '@theia/monaco-editor-core/esm/vs/platform/contextview/browser/contextView';
42
39
  import { KeyCodeChord } from '@theia/monaco-editor-core/esm/vs/base/common/keybindings';
43
40
  import { IContextKeyService } from '@theia/monaco-editor-core/esm/vs/platform/contextkey/common/contextkey';
@@ -46,6 +43,8 @@ import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecyc
46
43
  import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
47
44
  import { SimpleMonacoEditor } from './simple-monaco-editor';
48
45
  import { ICodeEditorWidgetOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditor/codeEditorWidget';
46
+ import { timeoutReject } from '@theia/core/lib/common/promise-util';
47
+ import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
49
48
 
50
49
  export const MonacoEditorFactory = Symbol('MonacoEditorFactory');
51
50
  export interface MonacoEditorFactory {
@@ -53,6 +52,17 @@ export interface MonacoEditorFactory {
53
52
  create(model: MonacoEditorModel, defaultOptions: MonacoEditor.IOptions, defaultOverrides: EditorServiceOverrides): Promise<MonacoEditor>;
54
53
  }
55
54
 
55
+ export const SaveParticipant = Symbol('SaveParticipant');
56
+
57
+ export interface SaveParticipant {
58
+ readonly order: number;
59
+ applyChangesOnSave(
60
+ editor: MonacoEditor,
61
+ cancellationToken: CancellationToken,
62
+ options?: SaveOptions): Promise<void>;
63
+ }
64
+ export const SAVE_PARTICIPANT_DEFAULT_ORDER = 0;
65
+
56
66
  @injectable()
57
67
  export class MonacoEditorProvider {
58
68
 
@@ -68,9 +78,13 @@ export class MonacoEditorProvider {
68
78
 
69
79
  @inject(OpenerService)
70
80
  protected readonly openerService: OpenerService;
81
+ @inject(ContributionProvider)
82
+ @named(SaveParticipant)
83
+ protected readonly saveProviderContributions: ContributionProvider<SaveParticipant>;
71
84
 
72
85
  @inject(FileSystemPreferences)
73
86
  protected readonly filePreferences: FileSystemPreferences;
87
+ protected saveParticipants: SaveParticipant[];
74
88
 
75
89
  protected _current: MonacoEditor | undefined;
76
90
  /**
@@ -210,7 +224,7 @@ export class MonacoEditorProvider {
210
224
  }));
211
225
  toDispose.push(editor.onLanguageChanged(() => this.updateMonacoEditorOptions(editor)));
212
226
  toDispose.push(editor.onDidChangeReadOnly(() => this.updateReadOnlyMessage(options, model.readOnly)));
213
- editor.document.onWillSaveModel(event => event.waitUntil(this.formatOnSave(editor, event)));
227
+ toDispose.push(editor.document.registerWillSaveModelListener((_, token, o) => this.runSaveParticipants(editor, token, o)));
214
228
  return editor;
215
229
  }
216
230
 
@@ -239,46 +253,6 @@ export class MonacoEditorProvider {
239
253
  }
240
254
  }
241
255
 
242
- protected shouldFormat(editor: MonacoEditor, event: WillSaveMonacoModelEvent): boolean {
243
- if (event.reason !== TextDocumentSaveReason.Manual) {
244
- return false;
245
- }
246
- if (event.options?.formatType) {
247
- switch (event.options.formatType) {
248
- case FormatType.ON: return true;
249
- case FormatType.OFF: return false;
250
- case FormatType.DIRTY: return editor.document.dirty;
251
- }
252
- }
253
- return true;
254
- }
255
-
256
- protected async formatOnSave(editor: MonacoEditor, event: WillSaveMonacoModelEvent): Promise<monaco.editor.IIdentifiedSingleEditOperation[]> {
257
- if (!this.shouldFormat(editor, event)) {
258
- return [];
259
- }
260
- const edits: monaco.editor.IIdentifiedSingleEditOperation[] = [];
261
- const overrideIdentifier = editor.document.languageId;
262
- const uri = editor.uri.toString();
263
- const formatOnSave = this.editorPreferences.get({ preferenceName: 'editor.formatOnSave', overrideIdentifier }, undefined, uri);
264
- if (formatOnSave) {
265
- const formatOnSaveTimeout = this.editorPreferences.get({ preferenceName: 'editor.formatOnSaveTimeout', overrideIdentifier }, undefined, uri)!;
266
- await Promise.race([
267
- timeoutReject(formatOnSaveTimeout, `Aborted format on save after ${formatOnSaveTimeout}ms`),
268
- editor.runAction('editor.action.formatDocument')
269
- ]);
270
- }
271
- const shouldRemoveWhiteSpace = this.filePreferences.get({ preferenceName: 'files.trimTrailingWhitespace', overrideIdentifier }, undefined, uri);
272
- if (shouldRemoveWhiteSpace) {
273
- await editor.runAction('editor.action.trimTrailingWhitespace');
274
- }
275
- const insertFinalNewline = this.filePreferences.get({ preferenceName: 'files.insertFinalNewline', overrideIdentifier }, undefined, uri);
276
- if (insertFinalNewline) {
277
- edits.push(...this.insertFinalNewline(editor));
278
- }
279
- return edits;
280
- }
281
-
282
256
  protected get diffPreferencePrefixes(): string[] {
283
257
  return [...this.preferencePrefixes, 'diffEditor.'];
284
258
  }
@@ -497,20 +471,106 @@ export class MonacoEditorProvider {
497
471
  );
498
472
  }
499
473
 
500
- protected insertFinalNewline(editor: MonacoEditor): monaco.editor.IIdentifiedSingleEditOperation[] {
501
- const model = editor.document && editor.document.textEditorModel;
474
+ @postConstruct()
475
+ init(): void {
476
+ this.saveParticipants = this.saveProviderContributions.getContributions().slice().sort((left, right) => left.order - right.order);
477
+ this.registerSaveParticipant({
478
+ order: 1000,
479
+ applyChangesOnSave: (
480
+ editor: MonacoEditor,
481
+ cancellationToken: monaco.CancellationToken,
482
+ options: SaveOptions): Promise<void> => this.formatOnSave(editor, editor.document, cancellationToken, options)
483
+ });
484
+ }
485
+
486
+ registerSaveParticipant(saveParticipant: SaveParticipant): Disposable {
487
+ if (this.saveParticipants.find(value => value === saveParticipant)) {
488
+ throw new Error('Save participant already registered');
489
+ }
490
+ this.saveParticipants.push(saveParticipant);
491
+ this.saveParticipants.sort((left, right) => left.order - right.order);
492
+ return Disposable.create(() => {
493
+ const index = this.saveParticipants.indexOf(saveParticipant);
494
+ if (index >= 0) {
495
+ this.saveParticipants.splice(index, 1);
496
+ }
497
+ });
498
+ }
499
+
500
+ protected shouldFormat(model: MonacoEditorModel, options: SaveOptions): boolean {
501
+ if (options.saveReason !== TextDocumentSaveReason.Manual) {
502
+ return false;
503
+ }
504
+ switch (options.formatType) {
505
+ case FormatType.ON: return true;
506
+ case FormatType.OFF: return false;
507
+ case FormatType.DIRTY: return model.dirty;
508
+ }
509
+ return true;
510
+ }
511
+
512
+ async runSaveParticipants(editor: MonacoEditor, cancellationToken: CancellationToken, options?: SaveOptions): Promise<void> {
513
+ const initialState = editor.document.createSnapshot();
514
+ for (const participant of this.saveParticipants) {
515
+ if (cancellationToken.isCancellationRequested) {
516
+ break;
517
+ }
518
+ const snapshot = editor.document.createSnapshot();
519
+ try {
520
+ await participant.applyChangesOnSave(editor, cancellationToken, options);
521
+ } catch (e) {
522
+ console.error(e);
523
+ editor.document.applySnapshot(snapshot);
524
+ }
525
+ }
526
+ if (cancellationToken.isCancellationRequested) {
527
+ editor.document.applySnapshot(initialState);
528
+ }
529
+ }
530
+
531
+ protected async formatOnSave(
532
+ editor: MonacoEditor,
533
+ model: MonacoEditorModel,
534
+ cancellationToken: CancellationToken,
535
+ options: SaveOptions): Promise<void> {
536
+ if (!this.shouldFormat(model, options)) {
537
+ return;
538
+ }
539
+
540
+ const overrideIdentifier = model.languageId;
541
+ const uri = model.uri.toString();
542
+ const formatOnSave = this.editorPreferences.get({ preferenceName: 'editor.formatOnSave', overrideIdentifier }, undefined, uri);
543
+ if (formatOnSave) {
544
+ const formatOnSaveTimeout = this.editorPreferences.get({ preferenceName: 'editor.formatOnSaveTimeout', overrideIdentifier }, undefined, uri)!;
545
+ await Promise.race([
546
+ timeoutReject(formatOnSaveTimeout, `Aborted format on save after ${formatOnSaveTimeout}ms`),
547
+ await editor.runAction('editor.action.formatDocument')
548
+ ]);
549
+ }
550
+ const shouldRemoveWhiteSpace = this.filePreferences.get({ preferenceName: 'files.trimTrailingWhitespace', overrideIdentifier }, undefined, uri);
551
+ if (shouldRemoveWhiteSpace) {
552
+ await editor.runAction('editor.action.trimTrailingWhitespace');
553
+ }
554
+ const insertFinalNewline = this.filePreferences.get({ preferenceName: 'files.insertFinalNewline', overrideIdentifier }, undefined, uri);
555
+ if (insertFinalNewline) {
556
+ this.insertFinalNewline(model);
557
+ }
558
+ }
559
+
560
+ protected insertFinalNewline(editorModel: MonacoEditorModel): void {
561
+ const model = editorModel.textEditorModel;
502
562
  if (!model) {
503
- return [];
563
+ return;
504
564
  }
505
565
 
506
566
  const lines = model?.getLineCount();
507
567
  if (lines === 0) {
508
- return [];
568
+ return;
509
569
  }
510
570
 
511
571
  const lastLine = model?.getLineContent(lines);
512
572
  if (lastLine.trim() === '') {
513
- return [];
573
+ return;
514
574
  }
515
575
 
516
576
  const lastLineMaxColumn = model?.getLineMaxColumn(lines);
@@ -520,9 +580,9 @@ export class MonacoEditorProvider {
520
580
  endLineNumber: lines,
521
581
  endColumn: lastLineMaxColumn
522
582
  };
523
- return [{
583
+ model.applyEdits([{
524
584
  range,
525
585
  text: model?.getEOL()
526
- }];
586
+ }]);
527
587
  }
528
588
  }
@@ -25,7 +25,7 @@ import {
25
25
  WidgetStatusBarContribution
26
26
  } from '@theia/core/lib/browser';
27
27
  import { TextEditorProvider, DiffNavigatorProvider, TextEditor } from '@theia/editor/lib/browser';
28
- import { MonacoEditorProvider, MonacoEditorFactory } from './monaco-editor-provider';
28
+ import { MonacoEditorProvider, MonacoEditorFactory, SaveParticipant } from './monaco-editor-provider';
29
29
  import { MonacoEditorMenuContribution } from './monaco-menu';
30
30
  import { MonacoEditorCommandHandlers } from './monaco-command';
31
31
  import { MonacoKeybindingContribution } from './monaco-keybinding';
@@ -79,6 +79,7 @@ import { ActiveMonacoUndoRedoHandler, FocusedMonacoUndoRedoHandler } from './mon
79
79
  import { ILogService } from '@theia/monaco-editor-core/esm/vs/platform/log/common/log';
80
80
  import { DefaultContentHoverWidgetPatcher } from './default-content-hover-widget-patcher';
81
81
  import { MonacoWorkspaceContextService } from './monaco-workspace-context-service';
82
+ import { MonacoCodeActionSaveParticipant } from './monaco-code-action-save-participant';
82
83
 
83
84
  export default new ContainerModule((bind, unbind, isBound, rebind) => {
84
85
  bind(MonacoThemingService).toSelf().inSingletonScope();
@@ -91,6 +92,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
91
92
  bind(FrontendApplicationContribution).toService(MonacoFrontendApplicationContribution);
92
93
  bind(StylingParticipant).toService(MonacoFrontendApplicationContribution);
93
94
 
95
+ bind(MonacoCodeActionSaveParticipant).toSelf().inSingletonScope();
96
+ bind(SaveParticipant).toService(MonacoCodeActionSaveParticipant);
97
+
94
98
  bind(MonacoToProtocolConverter).toSelf().inSingletonScope();
95
99
  bind(ProtocolToMonacoConverter).toSelf().inSingletonScope();
96
100
 
@@ -122,6 +126,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
122
126
  bindContributionProvider(bind, MonacoEditorFactory);
123
127
  bindContributionProvider(bind, MonacoEditorModelFactory);
124
128
  bindContributionProvider(bind, MonacoEditorModelFilter);
129
+ bindContributionProvider(bind, SaveParticipant);
125
130
  bind(MonacoCommandService).toSelf().inTransientScope();
126
131
 
127
132
  bind(TextEditorProvider).toProvider(context =>