@theia/output 1.45.1 → 1.46.0-next.72
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.
- package/README.md +33 -33
- package/lib/browser/output-channel.d.ts +125 -125
- package/lib/browser/output-channel.js +334 -334
- package/lib/browser/output-commands.d.ts +17 -17
- package/lib/browser/output-commands.js +84 -84
- package/lib/browser/output-context-menu.d.ts +11 -11
- package/lib/browser/output-context-menu.js +42 -42
- package/lib/browser/output-contribution.d.ts +29 -29
- package/lib/browser/output-contribution.js +284 -284
- package/lib/browser/output-editor-factory.d.ts +12 -12
- package/lib/browser/output-editor-factory.d.ts.map +1 -1
- package/lib/browser/output-editor-factory.js +79 -84
- package/lib/browser/output-editor-factory.js.map +1 -1
- package/lib/browser/output-editor-model-factory.d.ts +15 -15
- package/lib/browser/output-editor-model-factory.js +61 -61
- package/lib/browser/output-frontend-module.d.ts +3 -3
- package/lib/browser/output-frontend-module.js +50 -50
- package/lib/browser/output-preferences.d.ts +11 -11
- package/lib/browser/output-preferences.js +46 -46
- package/lib/browser/output-resource.d.ts +18 -18
- package/lib/browser/output-resource.js +54 -54
- package/lib/browser/output-toolbar-contribution.d.ts +21 -21
- package/lib/browser/output-toolbar-contribution.js +125 -125
- package/lib/browser/output-widget.d.ts +48 -48
- package/lib/browser/output-widget.js +248 -248
- package/lib/common/output-uri.d.ts +7 -7
- package/lib/common/output-uri.js +47 -47
- package/lib/common/output-uri.spec.d.ts +1 -1
- package/lib/common/output-uri.spec.js +50 -50
- package/package.json +7 -7
- package/src/browser/output-channel.ts +366 -366
- package/src/browser/output-commands.ts +100 -100
- package/src/browser/output-context-menu.ts +34 -34
- package/src/browser/output-contribution.ts +274 -274
- package/src/browser/output-editor-factory.ts +68 -74
- package/src/browser/output-editor-model-factory.ts +54 -54
- package/src/browser/output-frontend-module.ts +53 -53
- package/src/browser/output-preferences.ts +58 -58
- package/src/browser/output-resource.ts +65 -65
- package/src/browser/output-toolbar-contribution.tsx +116 -116
- package/src/browser/output-widget.ts +256 -256
- package/src/browser/style/output.css +31 -31
- package/src/common/output-uri.spec.ts +53 -53
- package/src/common/output-uri.ts +47 -47
|
@@ -1,366 +1,366 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2018 TypeFox 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 * as PQueue from 'p-queue';
|
|
18
|
-
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
19
|
-
import URI from '@theia/core/lib/common/uri';
|
|
20
|
-
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
21
|
-
import { Resource, ResourceResolver } from '@theia/core/lib/common/resource';
|
|
22
|
-
import { Emitter, Event, Disposable, DisposableCollection } from '@theia/core';
|
|
23
|
-
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
|
24
|
-
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
|
25
|
-
import { OutputUri } from '../common/output-uri';
|
|
26
|
-
import { OutputResource } from '../browser/output-resource';
|
|
27
|
-
import { OutputPreferences } from './output-preferences';
|
|
28
|
-
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
|
29
|
-
import * as monaco from '@theia/monaco-editor-core';
|
|
30
|
-
|
|
31
|
-
@injectable()
|
|
32
|
-
export class OutputChannelManager implements Disposable, ResourceResolver {
|
|
33
|
-
|
|
34
|
-
@inject(MonacoTextModelService)
|
|
35
|
-
protected readonly textModelService: MonacoTextModelService;
|
|
36
|
-
|
|
37
|
-
@inject(OutputPreferences)
|
|
38
|
-
protected readonly preferences: OutputPreferences;
|
|
39
|
-
|
|
40
|
-
protected readonly channels = new Map<string, OutputChannel>();
|
|
41
|
-
protected readonly resources = new Map<string, OutputResource>();
|
|
42
|
-
protected _selectedChannel: OutputChannel | undefined;
|
|
43
|
-
|
|
44
|
-
protected readonly channelAddedEmitter = new Emitter<{ name: string }>();
|
|
45
|
-
protected readonly channelDeletedEmitter = new Emitter<{ name: string }>();
|
|
46
|
-
protected readonly channelWasShownEmitter = new Emitter<{ name: string, preserveFocus?: boolean }>();
|
|
47
|
-
protected readonly channelWasHiddenEmitter = new Emitter<{ name: string }>();
|
|
48
|
-
protected readonly selectedChannelChangedEmitter = new Emitter<{ name: string } | undefined>();
|
|
49
|
-
|
|
50
|
-
readonly onChannelAdded = this.channelAddedEmitter.event;
|
|
51
|
-
readonly onChannelDeleted = this.channelDeletedEmitter.event;
|
|
52
|
-
readonly onChannelWasShown = this.channelWasShownEmitter.event;
|
|
53
|
-
readonly onChannelWasHidden = this.channelWasHiddenEmitter.event;
|
|
54
|
-
readonly onSelectedChannelChanged = this.selectedChannelChangedEmitter.event;
|
|
55
|
-
|
|
56
|
-
protected readonly toDispose = new DisposableCollection();
|
|
57
|
-
protected readonly toDisposeOnChannelDeletion = new Map<string, Disposable>();
|
|
58
|
-
|
|
59
|
-
getChannel(name: string): OutputChannel {
|
|
60
|
-
const existing = this.channels.get(name);
|
|
61
|
-
if (existing) {
|
|
62
|
-
return existing;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// We have to register the resource first, because `textModelService#createModelReference` will require it
|
|
66
|
-
// right after creating the monaco.editor.ITextModel.
|
|
67
|
-
// All `append` and `appendLine` will be deferred until the underlying text-model instantiation.
|
|
68
|
-
let resource = this.resources.get(name);
|
|
69
|
-
if (!resource) {
|
|
70
|
-
const uri = OutputUri.create(name);
|
|
71
|
-
const editorModelRef = new Deferred<IReference<MonacoEditorModel>>();
|
|
72
|
-
resource = this.createResource({ uri, editorModelRef });
|
|
73
|
-
this.resources.set(name, resource);
|
|
74
|
-
this.textModelService.createModelReference(uri).then(ref => editorModelRef.resolve(ref));
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const channel = this.createChannel(resource);
|
|
78
|
-
this.channels.set(name, channel);
|
|
79
|
-
this.toDisposeOnChannelDeletion.set(name, this.registerListeners(channel));
|
|
80
|
-
this.channelAddedEmitter.fire(channel);
|
|
81
|
-
if (!this.selectedChannel) {
|
|
82
|
-
this.selectedChannel = channel;
|
|
83
|
-
}
|
|
84
|
-
return channel;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
protected registerListeners(channel: OutputChannel): Disposable {
|
|
88
|
-
const { name } = channel;
|
|
89
|
-
return new DisposableCollection(
|
|
90
|
-
channel,
|
|
91
|
-
channel.onVisibilityChange(({ isVisible, preserveFocus }) => {
|
|
92
|
-
if (isVisible) {
|
|
93
|
-
this.selectedChannel = channel;
|
|
94
|
-
this.channelWasShownEmitter.fire({ name, preserveFocus });
|
|
95
|
-
} else {
|
|
96
|
-
if (channel === this.selectedChannel) {
|
|
97
|
-
this.selectedChannel = this.getVisibleChannels()[0];
|
|
98
|
-
}
|
|
99
|
-
this.channelWasHiddenEmitter.fire({ name });
|
|
100
|
-
}
|
|
101
|
-
}),
|
|
102
|
-
channel.onDisposed(() => this.deleteChannel(name)),
|
|
103
|
-
Disposable.create(() => {
|
|
104
|
-
const resource = this.resources.get(name);
|
|
105
|
-
if (resource) {
|
|
106
|
-
resource.dispose();
|
|
107
|
-
this.resources.delete(name);
|
|
108
|
-
} else {
|
|
109
|
-
console.warn(`Could not dispose. No resource was for output channel: '${name}'.`);
|
|
110
|
-
}
|
|
111
|
-
}),
|
|
112
|
-
Disposable.create(() => {
|
|
113
|
-
const toDispose = this.channels.get(name);
|
|
114
|
-
if (!toDispose) {
|
|
115
|
-
console.warn(`Could not dispose. No channel exist with name: '${name}'.`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
this.channels.delete(name);
|
|
119
|
-
toDispose.dispose();
|
|
120
|
-
this.channelDeletedEmitter.fire({ name });
|
|
121
|
-
if (this.selectedChannel && this.selectedChannel.name === name) {
|
|
122
|
-
this.selectedChannel = this.getVisibleChannels()[0];
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
deleteChannel(name: string): void {
|
|
129
|
-
const toDispose = this.toDisposeOnChannelDeletion.get(name);
|
|
130
|
-
if (toDispose) {
|
|
131
|
-
toDispose.dispose();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
getChannels(): OutputChannel[] {
|
|
136
|
-
return Array.from(this.channels.values()).sort(this.channelComparator);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
getVisibleChannels(): OutputChannel[] {
|
|
140
|
-
return this.getChannels().filter(channel => channel.isVisible);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
protected get channelComparator(): (left: OutputChannel, right: OutputChannel) => number {
|
|
144
|
-
return (left, right) => {
|
|
145
|
-
if (left.isVisible !== right.isVisible) {
|
|
146
|
-
return left.isVisible ? -1 : 1;
|
|
147
|
-
}
|
|
148
|
-
return left.name.toLocaleLowerCase().localeCompare(right.name.toLocaleLowerCase());
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
dispose(): void {
|
|
153
|
-
this.toDispose.dispose();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
get selectedChannel(): OutputChannel | undefined {
|
|
157
|
-
return this._selectedChannel;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
set selectedChannel(channel: OutputChannel | undefined) {
|
|
161
|
-
this._selectedChannel = channel;
|
|
162
|
-
if (this._selectedChannel) {
|
|
163
|
-
this.selectedChannelChangedEmitter.fire({ name: this._selectedChannel.name });
|
|
164
|
-
} else {
|
|
165
|
-
this.selectedChannelChangedEmitter.fire(undefined);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Non-API: do not call directly.
|
|
171
|
-
*/
|
|
172
|
-
async resolve(uri: URI): Promise<Resource> {
|
|
173
|
-
if (!OutputUri.is(uri)) {
|
|
174
|
-
throw new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`);
|
|
175
|
-
}
|
|
176
|
-
const resource = this.resources.get(OutputUri.channelName(uri));
|
|
177
|
-
if (!resource) {
|
|
178
|
-
throw new Error(`No output resource was registered with URI: ${uri.toString()}`);
|
|
179
|
-
}
|
|
180
|
-
return resource;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
protected createResource({ uri, editorModelRef }: { uri: URI, editorModelRef: Deferred<IReference<MonacoEditorModel>> }): OutputResource {
|
|
184
|
-
return new OutputResource(uri, editorModelRef);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
protected createChannel(resource: OutputResource): OutputChannel {
|
|
188
|
-
return new OutputChannel(resource, this.preferences);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export enum OutputChannelSeverity {
|
|
194
|
-
Error = 1,
|
|
195
|
-
Warning = 2,
|
|
196
|
-
Info = 3
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export class OutputChannel implements Disposable {
|
|
200
|
-
|
|
201
|
-
protected readonly contentChangeEmitter = new Emitter<void>();
|
|
202
|
-
protected readonly visibilityChangeEmitter = new Emitter<{ isVisible: boolean, preserveFocus?: boolean }>();
|
|
203
|
-
protected readonly disposedEmitter = new Emitter<void>();
|
|
204
|
-
protected readonly textModifyQueue = new PQueue({ autoStart: true, concurrency: 1 });
|
|
205
|
-
protected readonly toDispose = new DisposableCollection(
|
|
206
|
-
Disposable.create(() => this.textModifyQueue.clear()),
|
|
207
|
-
this.contentChangeEmitter,
|
|
208
|
-
this.visibilityChangeEmitter,
|
|
209
|
-
this.disposedEmitter
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
protected disposed = false;
|
|
213
|
-
protected visible = true;
|
|
214
|
-
protected _maxLineNumber: number;
|
|
215
|
-
protected decorationIds = new Set<string>();
|
|
216
|
-
|
|
217
|
-
readonly onVisibilityChange: Event<{ isVisible: boolean, preserveFocus?: boolean }> = this.visibilityChangeEmitter.event;
|
|
218
|
-
readonly onContentChange: Event<void> = this.contentChangeEmitter.event;
|
|
219
|
-
readonly onDisposed: Event<void> = this.disposedEmitter.event;
|
|
220
|
-
|
|
221
|
-
constructor(protected readonly resource: OutputResource, protected readonly preferences: OutputPreferences) {
|
|
222
|
-
this._maxLineNumber = this.preferences['output.maxChannelHistory'];
|
|
223
|
-
this.toDispose.push(resource);
|
|
224
|
-
this.toDispose.push(Disposable.create(() => this.decorationIds.clear()));
|
|
225
|
-
this.toDispose.push(this.preferences.onPreferenceChanged(event => {
|
|
226
|
-
if (event.preferenceName === 'output.maxChannelHistory') {
|
|
227
|
-
const maxLineNumber = event.newValue;
|
|
228
|
-
if (this.maxLineNumber !== maxLineNumber) {
|
|
229
|
-
this.maxLineNumber = maxLineNumber;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
get name(): string {
|
|
236
|
-
return OutputUri.channelName(this.uri);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
get uri(): URI {
|
|
240
|
-
return this.resource.uri;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
hide(): void {
|
|
244
|
-
this.visible = false;
|
|
245
|
-
this.visibilityChangeEmitter.fire({ isVisible: this.isVisible });
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* If `preserveFocus` is `true`, the channel will not take focus. It is `false` by default.
|
|
250
|
-
* - Calling `show` without args or with `preserveFocus: false` will reveal **and** activate the `Output` widget.
|
|
251
|
-
* - Calling `show` with `preserveFocus: true` will reveal the `Output` widget but **won't** activate it.
|
|
252
|
-
*/
|
|
253
|
-
show({ preserveFocus }: { preserveFocus: boolean } = { preserveFocus: false }): void {
|
|
254
|
-
this.visible = true;
|
|
255
|
-
this.visibilityChangeEmitter.fire({ isVisible: this.isVisible, preserveFocus });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Note: if `false` it does not meant it is disposed or not available, it is only hidden from the UI.
|
|
260
|
-
*/
|
|
261
|
-
get isVisible(): boolean {
|
|
262
|
-
return this.visible;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
clear(): void {
|
|
266
|
-
this.textModifyQueue.add(async () => {
|
|
267
|
-
const textModel = (await this.resource.editorModelRef.promise).object.textEditorModel;
|
|
268
|
-
textModel.deltaDecorations(Array.from(this.decorationIds), []);
|
|
269
|
-
this.decorationIds.clear();
|
|
270
|
-
textModel.setValue('');
|
|
271
|
-
this.contentChangeEmitter.fire();
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
dispose(): void {
|
|
276
|
-
if (this.disposed) {
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
this.disposed = true;
|
|
280
|
-
this.toDispose.dispose();
|
|
281
|
-
this.disposedEmitter.fire();
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
append(content: string, severity: OutputChannelSeverity = OutputChannelSeverity.Info): void {
|
|
285
|
-
this.textModifyQueue.add(() => this.doAppend({ content, severity }));
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
appendLine(content: string, severity: OutputChannelSeverity = OutputChannelSeverity.Info): void {
|
|
289
|
-
this.textModifyQueue.add(() => this.doAppend({ content, severity, appendEol: true }));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
protected async doAppend({ content, severity, appendEol }: { content: string, severity: OutputChannelSeverity, appendEol?: boolean }): Promise<void> {
|
|
293
|
-
const textModel = (await this.resource.editorModelRef.promise).object.textEditorModel;
|
|
294
|
-
const lastLine = textModel.getLineCount();
|
|
295
|
-
const lastLineMaxColumn = textModel.getLineMaxColumn(lastLine);
|
|
296
|
-
const position = new monaco.Position(lastLine, lastLineMaxColumn);
|
|
297
|
-
const range = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column);
|
|
298
|
-
const edits = [{
|
|
299
|
-
range,
|
|
300
|
-
text: !!appendEol ? `${content}${textModel.getEOL()}` : content,
|
|
301
|
-
forceMoveMarkers: true
|
|
302
|
-
}];
|
|
303
|
-
// We do not use `pushEditOperations` as we do not need undo/redo support. VS Code uses `applyEdits` too.
|
|
304
|
-
// https://github.com/microsoft/vscode/blob/dc348340fd1a6c583cb63a1e7e6b4fd657e01e01/src/vs/workbench/services/output/common/outputChannelModel.ts#L108-L115
|
|
305
|
-
textModel.applyEdits(edits);
|
|
306
|
-
if (severity !== OutputChannelSeverity.Info) {
|
|
307
|
-
const inlineClassName = severity === OutputChannelSeverity.Error ? 'theia-output-error' : 'theia-output-warning';
|
|
308
|
-
let endLineNumber = textModel.getLineCount();
|
|
309
|
-
// If last line is empty (the first non-whitespace is 0), apply decorator to previous line's last non-whitespace instead
|
|
310
|
-
// Note: if the user appends `inlineWarning `, the new decorator's range includes the trailing whitespace.
|
|
311
|
-
if (!textModel.getLineFirstNonWhitespaceColumn(endLineNumber)) {
|
|
312
|
-
endLineNumber--;
|
|
313
|
-
}
|
|
314
|
-
const endColumn = textModel.getLineLastNonWhitespaceColumn(endLineNumber);
|
|
315
|
-
const newDecorations = [{
|
|
316
|
-
range: new monaco.Range(range.startLineNumber, range.startColumn, endLineNumber, endColumn), options: {
|
|
317
|
-
inlineClassName
|
|
318
|
-
}
|
|
319
|
-
}];
|
|
320
|
-
for (const decorationId of textModel.deltaDecorations([], newDecorations)) {
|
|
321
|
-
this.decorationIds.add(decorationId);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
this.ensureMaxChannelHistory(textModel);
|
|
325
|
-
this.contentChangeEmitter.fire();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
protected ensureMaxChannelHistory(textModel: monaco.editor.ITextModel): void {
|
|
329
|
-
this.contentChangeEmitter.fire();
|
|
330
|
-
const linesToRemove = textModel.getLineCount() - this.maxLineNumber - 1; // -1 as the last line is usually empty -> `appendLine`.
|
|
331
|
-
if (linesToRemove > 0) {
|
|
332
|
-
const endColumn = textModel.getLineMaxColumn(linesToRemove);
|
|
333
|
-
// `endLineNumber` is `linesToRemove` + 1 as monaco is one based.
|
|
334
|
-
const range = new monaco.Range(1, 1, linesToRemove, endColumn + 1);
|
|
335
|
-
// eslint-disable-next-line no-null/no-null
|
|
336
|
-
const text = null;
|
|
337
|
-
const decorationsToRemove = textModel.getLinesDecorations(range.startLineNumber, range.endLineNumber)
|
|
338
|
-
.filter(({ id }) => this.decorationIds.has(id)).map(({ id }) => id); // Do we need to filter here? Who else can put decorations to the output model?
|
|
339
|
-
if (decorationsToRemove.length) {
|
|
340
|
-
for (const newId of textModel.deltaDecorations(decorationsToRemove, [])) {
|
|
341
|
-
this.decorationIds.add(newId);
|
|
342
|
-
}
|
|
343
|
-
for (const toRemoveId of decorationsToRemove) {
|
|
344
|
-
this.decorationIds.delete(toRemoveId);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
textModel.applyEdits([
|
|
348
|
-
{
|
|
349
|
-
range: new monaco.Range(1, 1, linesToRemove + 1, textModel.getLineFirstNonWhitespaceColumn(linesToRemove + 1)),
|
|
350
|
-
text,
|
|
351
|
-
forceMoveMarkers: true
|
|
352
|
-
}
|
|
353
|
-
]);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
protected get maxLineNumber(): number {
|
|
358
|
-
return this._maxLineNumber;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
protected set maxLineNumber(maxLineNumber: number) {
|
|
362
|
-
this._maxLineNumber = maxLineNumber;
|
|
363
|
-
this.append(''); // will trigger an `ensureMaxChannelHistory` call and will refresh the content.
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2018 TypeFox 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 * as PQueue from 'p-queue';
|
|
18
|
+
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
19
|
+
import URI from '@theia/core/lib/common/uri';
|
|
20
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
21
|
+
import { Resource, ResourceResolver } from '@theia/core/lib/common/resource';
|
|
22
|
+
import { Emitter, Event, Disposable, DisposableCollection } from '@theia/core';
|
|
23
|
+
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
|
24
|
+
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
|
|
25
|
+
import { OutputUri } from '../common/output-uri';
|
|
26
|
+
import { OutputResource } from '../browser/output-resource';
|
|
27
|
+
import { OutputPreferences } from './output-preferences';
|
|
28
|
+
import { IReference } from '@theia/monaco-editor-core/esm/vs/base/common/lifecycle';
|
|
29
|
+
import * as monaco from '@theia/monaco-editor-core';
|
|
30
|
+
|
|
31
|
+
@injectable()
|
|
32
|
+
export class OutputChannelManager implements Disposable, ResourceResolver {
|
|
33
|
+
|
|
34
|
+
@inject(MonacoTextModelService)
|
|
35
|
+
protected readonly textModelService: MonacoTextModelService;
|
|
36
|
+
|
|
37
|
+
@inject(OutputPreferences)
|
|
38
|
+
protected readonly preferences: OutputPreferences;
|
|
39
|
+
|
|
40
|
+
protected readonly channels = new Map<string, OutputChannel>();
|
|
41
|
+
protected readonly resources = new Map<string, OutputResource>();
|
|
42
|
+
protected _selectedChannel: OutputChannel | undefined;
|
|
43
|
+
|
|
44
|
+
protected readonly channelAddedEmitter = new Emitter<{ name: string }>();
|
|
45
|
+
protected readonly channelDeletedEmitter = new Emitter<{ name: string }>();
|
|
46
|
+
protected readonly channelWasShownEmitter = new Emitter<{ name: string, preserveFocus?: boolean }>();
|
|
47
|
+
protected readonly channelWasHiddenEmitter = new Emitter<{ name: string }>();
|
|
48
|
+
protected readonly selectedChannelChangedEmitter = new Emitter<{ name: string } | undefined>();
|
|
49
|
+
|
|
50
|
+
readonly onChannelAdded = this.channelAddedEmitter.event;
|
|
51
|
+
readonly onChannelDeleted = this.channelDeletedEmitter.event;
|
|
52
|
+
readonly onChannelWasShown = this.channelWasShownEmitter.event;
|
|
53
|
+
readonly onChannelWasHidden = this.channelWasHiddenEmitter.event;
|
|
54
|
+
readonly onSelectedChannelChanged = this.selectedChannelChangedEmitter.event;
|
|
55
|
+
|
|
56
|
+
protected readonly toDispose = new DisposableCollection();
|
|
57
|
+
protected readonly toDisposeOnChannelDeletion = new Map<string, Disposable>();
|
|
58
|
+
|
|
59
|
+
getChannel(name: string): OutputChannel {
|
|
60
|
+
const existing = this.channels.get(name);
|
|
61
|
+
if (existing) {
|
|
62
|
+
return existing;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// We have to register the resource first, because `textModelService#createModelReference` will require it
|
|
66
|
+
// right after creating the monaco.editor.ITextModel.
|
|
67
|
+
// All `append` and `appendLine` will be deferred until the underlying text-model instantiation.
|
|
68
|
+
let resource = this.resources.get(name);
|
|
69
|
+
if (!resource) {
|
|
70
|
+
const uri = OutputUri.create(name);
|
|
71
|
+
const editorModelRef = new Deferred<IReference<MonacoEditorModel>>();
|
|
72
|
+
resource = this.createResource({ uri, editorModelRef });
|
|
73
|
+
this.resources.set(name, resource);
|
|
74
|
+
this.textModelService.createModelReference(uri).then(ref => editorModelRef.resolve(ref));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const channel = this.createChannel(resource);
|
|
78
|
+
this.channels.set(name, channel);
|
|
79
|
+
this.toDisposeOnChannelDeletion.set(name, this.registerListeners(channel));
|
|
80
|
+
this.channelAddedEmitter.fire(channel);
|
|
81
|
+
if (!this.selectedChannel) {
|
|
82
|
+
this.selectedChannel = channel;
|
|
83
|
+
}
|
|
84
|
+
return channel;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected registerListeners(channel: OutputChannel): Disposable {
|
|
88
|
+
const { name } = channel;
|
|
89
|
+
return new DisposableCollection(
|
|
90
|
+
channel,
|
|
91
|
+
channel.onVisibilityChange(({ isVisible, preserveFocus }) => {
|
|
92
|
+
if (isVisible) {
|
|
93
|
+
this.selectedChannel = channel;
|
|
94
|
+
this.channelWasShownEmitter.fire({ name, preserveFocus });
|
|
95
|
+
} else {
|
|
96
|
+
if (channel === this.selectedChannel) {
|
|
97
|
+
this.selectedChannel = this.getVisibleChannels()[0];
|
|
98
|
+
}
|
|
99
|
+
this.channelWasHiddenEmitter.fire({ name });
|
|
100
|
+
}
|
|
101
|
+
}),
|
|
102
|
+
channel.onDisposed(() => this.deleteChannel(name)),
|
|
103
|
+
Disposable.create(() => {
|
|
104
|
+
const resource = this.resources.get(name);
|
|
105
|
+
if (resource) {
|
|
106
|
+
resource.dispose();
|
|
107
|
+
this.resources.delete(name);
|
|
108
|
+
} else {
|
|
109
|
+
console.warn(`Could not dispose. No resource was for output channel: '${name}'.`);
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
Disposable.create(() => {
|
|
113
|
+
const toDispose = this.channels.get(name);
|
|
114
|
+
if (!toDispose) {
|
|
115
|
+
console.warn(`Could not dispose. No channel exist with name: '${name}'.`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.channels.delete(name);
|
|
119
|
+
toDispose.dispose();
|
|
120
|
+
this.channelDeletedEmitter.fire({ name });
|
|
121
|
+
if (this.selectedChannel && this.selectedChannel.name === name) {
|
|
122
|
+
this.selectedChannel = this.getVisibleChannels()[0];
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deleteChannel(name: string): void {
|
|
129
|
+
const toDispose = this.toDisposeOnChannelDeletion.get(name);
|
|
130
|
+
if (toDispose) {
|
|
131
|
+
toDispose.dispose();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
getChannels(): OutputChannel[] {
|
|
136
|
+
return Array.from(this.channels.values()).sort(this.channelComparator);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
getVisibleChannels(): OutputChannel[] {
|
|
140
|
+
return this.getChannels().filter(channel => channel.isVisible);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
protected get channelComparator(): (left: OutputChannel, right: OutputChannel) => number {
|
|
144
|
+
return (left, right) => {
|
|
145
|
+
if (left.isVisible !== right.isVisible) {
|
|
146
|
+
return left.isVisible ? -1 : 1;
|
|
147
|
+
}
|
|
148
|
+
return left.name.toLocaleLowerCase().localeCompare(right.name.toLocaleLowerCase());
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
dispose(): void {
|
|
153
|
+
this.toDispose.dispose();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get selectedChannel(): OutputChannel | undefined {
|
|
157
|
+
return this._selectedChannel;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
set selectedChannel(channel: OutputChannel | undefined) {
|
|
161
|
+
this._selectedChannel = channel;
|
|
162
|
+
if (this._selectedChannel) {
|
|
163
|
+
this.selectedChannelChangedEmitter.fire({ name: this._selectedChannel.name });
|
|
164
|
+
} else {
|
|
165
|
+
this.selectedChannelChangedEmitter.fire(undefined);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Non-API: do not call directly.
|
|
171
|
+
*/
|
|
172
|
+
async resolve(uri: URI): Promise<Resource> {
|
|
173
|
+
if (!OutputUri.is(uri)) {
|
|
174
|
+
throw new Error(`Expected '${OutputUri.SCHEME}' URI scheme. Got: ${uri} instead.`);
|
|
175
|
+
}
|
|
176
|
+
const resource = this.resources.get(OutputUri.channelName(uri));
|
|
177
|
+
if (!resource) {
|
|
178
|
+
throw new Error(`No output resource was registered with URI: ${uri.toString()}`);
|
|
179
|
+
}
|
|
180
|
+
return resource;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
protected createResource({ uri, editorModelRef }: { uri: URI, editorModelRef: Deferred<IReference<MonacoEditorModel>> }): OutputResource {
|
|
184
|
+
return new OutputResource(uri, editorModelRef);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
protected createChannel(resource: OutputResource): OutputChannel {
|
|
188
|
+
return new OutputChannel(resource, this.preferences);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export enum OutputChannelSeverity {
|
|
194
|
+
Error = 1,
|
|
195
|
+
Warning = 2,
|
|
196
|
+
Info = 3
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export class OutputChannel implements Disposable {
|
|
200
|
+
|
|
201
|
+
protected readonly contentChangeEmitter = new Emitter<void>();
|
|
202
|
+
protected readonly visibilityChangeEmitter = new Emitter<{ isVisible: boolean, preserveFocus?: boolean }>();
|
|
203
|
+
protected readonly disposedEmitter = new Emitter<void>();
|
|
204
|
+
protected readonly textModifyQueue = new PQueue({ autoStart: true, concurrency: 1 });
|
|
205
|
+
protected readonly toDispose = new DisposableCollection(
|
|
206
|
+
Disposable.create(() => this.textModifyQueue.clear()),
|
|
207
|
+
this.contentChangeEmitter,
|
|
208
|
+
this.visibilityChangeEmitter,
|
|
209
|
+
this.disposedEmitter
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
protected disposed = false;
|
|
213
|
+
protected visible = true;
|
|
214
|
+
protected _maxLineNumber: number;
|
|
215
|
+
protected decorationIds = new Set<string>();
|
|
216
|
+
|
|
217
|
+
readonly onVisibilityChange: Event<{ isVisible: boolean, preserveFocus?: boolean }> = this.visibilityChangeEmitter.event;
|
|
218
|
+
readonly onContentChange: Event<void> = this.contentChangeEmitter.event;
|
|
219
|
+
readonly onDisposed: Event<void> = this.disposedEmitter.event;
|
|
220
|
+
|
|
221
|
+
constructor(protected readonly resource: OutputResource, protected readonly preferences: OutputPreferences) {
|
|
222
|
+
this._maxLineNumber = this.preferences['output.maxChannelHistory'];
|
|
223
|
+
this.toDispose.push(resource);
|
|
224
|
+
this.toDispose.push(Disposable.create(() => this.decorationIds.clear()));
|
|
225
|
+
this.toDispose.push(this.preferences.onPreferenceChanged(event => {
|
|
226
|
+
if (event.preferenceName === 'output.maxChannelHistory') {
|
|
227
|
+
const maxLineNumber = event.newValue;
|
|
228
|
+
if (this.maxLineNumber !== maxLineNumber) {
|
|
229
|
+
this.maxLineNumber = maxLineNumber;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
get name(): string {
|
|
236
|
+
return OutputUri.channelName(this.uri);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
get uri(): URI {
|
|
240
|
+
return this.resource.uri;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
hide(): void {
|
|
244
|
+
this.visible = false;
|
|
245
|
+
this.visibilityChangeEmitter.fire({ isVisible: this.isVisible });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* If `preserveFocus` is `true`, the channel will not take focus. It is `false` by default.
|
|
250
|
+
* - Calling `show` without args or with `preserveFocus: false` will reveal **and** activate the `Output` widget.
|
|
251
|
+
* - Calling `show` with `preserveFocus: true` will reveal the `Output` widget but **won't** activate it.
|
|
252
|
+
*/
|
|
253
|
+
show({ preserveFocus }: { preserveFocus: boolean } = { preserveFocus: false }): void {
|
|
254
|
+
this.visible = true;
|
|
255
|
+
this.visibilityChangeEmitter.fire({ isVisible: this.isVisible, preserveFocus });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Note: if `false` it does not meant it is disposed or not available, it is only hidden from the UI.
|
|
260
|
+
*/
|
|
261
|
+
get isVisible(): boolean {
|
|
262
|
+
return this.visible;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
clear(): void {
|
|
266
|
+
this.textModifyQueue.add(async () => {
|
|
267
|
+
const textModel = (await this.resource.editorModelRef.promise).object.textEditorModel;
|
|
268
|
+
textModel.deltaDecorations(Array.from(this.decorationIds), []);
|
|
269
|
+
this.decorationIds.clear();
|
|
270
|
+
textModel.setValue('');
|
|
271
|
+
this.contentChangeEmitter.fire();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
dispose(): void {
|
|
276
|
+
if (this.disposed) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.disposed = true;
|
|
280
|
+
this.toDispose.dispose();
|
|
281
|
+
this.disposedEmitter.fire();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
append(content: string, severity: OutputChannelSeverity = OutputChannelSeverity.Info): void {
|
|
285
|
+
this.textModifyQueue.add(() => this.doAppend({ content, severity }));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
appendLine(content: string, severity: OutputChannelSeverity = OutputChannelSeverity.Info): void {
|
|
289
|
+
this.textModifyQueue.add(() => this.doAppend({ content, severity, appendEol: true }));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
protected async doAppend({ content, severity, appendEol }: { content: string, severity: OutputChannelSeverity, appendEol?: boolean }): Promise<void> {
|
|
293
|
+
const textModel = (await this.resource.editorModelRef.promise).object.textEditorModel;
|
|
294
|
+
const lastLine = textModel.getLineCount();
|
|
295
|
+
const lastLineMaxColumn = textModel.getLineMaxColumn(lastLine);
|
|
296
|
+
const position = new monaco.Position(lastLine, lastLineMaxColumn);
|
|
297
|
+
const range = new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column);
|
|
298
|
+
const edits = [{
|
|
299
|
+
range,
|
|
300
|
+
text: !!appendEol ? `${content}${textModel.getEOL()}` : content,
|
|
301
|
+
forceMoveMarkers: true
|
|
302
|
+
}];
|
|
303
|
+
// We do not use `pushEditOperations` as we do not need undo/redo support. VS Code uses `applyEdits` too.
|
|
304
|
+
// https://github.com/microsoft/vscode/blob/dc348340fd1a6c583cb63a1e7e6b4fd657e01e01/src/vs/workbench/services/output/common/outputChannelModel.ts#L108-L115
|
|
305
|
+
textModel.applyEdits(edits);
|
|
306
|
+
if (severity !== OutputChannelSeverity.Info) {
|
|
307
|
+
const inlineClassName = severity === OutputChannelSeverity.Error ? 'theia-output-error' : 'theia-output-warning';
|
|
308
|
+
let endLineNumber = textModel.getLineCount();
|
|
309
|
+
// If last line is empty (the first non-whitespace is 0), apply decorator to previous line's last non-whitespace instead
|
|
310
|
+
// Note: if the user appends `inlineWarning `, the new decorator's range includes the trailing whitespace.
|
|
311
|
+
if (!textModel.getLineFirstNonWhitespaceColumn(endLineNumber)) {
|
|
312
|
+
endLineNumber--;
|
|
313
|
+
}
|
|
314
|
+
const endColumn = textModel.getLineLastNonWhitespaceColumn(endLineNumber);
|
|
315
|
+
const newDecorations = [{
|
|
316
|
+
range: new monaco.Range(range.startLineNumber, range.startColumn, endLineNumber, endColumn), options: {
|
|
317
|
+
inlineClassName
|
|
318
|
+
}
|
|
319
|
+
}];
|
|
320
|
+
for (const decorationId of textModel.deltaDecorations([], newDecorations)) {
|
|
321
|
+
this.decorationIds.add(decorationId);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.ensureMaxChannelHistory(textModel);
|
|
325
|
+
this.contentChangeEmitter.fire();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
protected ensureMaxChannelHistory(textModel: monaco.editor.ITextModel): void {
|
|
329
|
+
this.contentChangeEmitter.fire();
|
|
330
|
+
const linesToRemove = textModel.getLineCount() - this.maxLineNumber - 1; // -1 as the last line is usually empty -> `appendLine`.
|
|
331
|
+
if (linesToRemove > 0) {
|
|
332
|
+
const endColumn = textModel.getLineMaxColumn(linesToRemove);
|
|
333
|
+
// `endLineNumber` is `linesToRemove` + 1 as monaco is one based.
|
|
334
|
+
const range = new monaco.Range(1, 1, linesToRemove, endColumn + 1);
|
|
335
|
+
// eslint-disable-next-line no-null/no-null
|
|
336
|
+
const text = null;
|
|
337
|
+
const decorationsToRemove = textModel.getLinesDecorations(range.startLineNumber, range.endLineNumber)
|
|
338
|
+
.filter(({ id }) => this.decorationIds.has(id)).map(({ id }) => id); // Do we need to filter here? Who else can put decorations to the output model?
|
|
339
|
+
if (decorationsToRemove.length) {
|
|
340
|
+
for (const newId of textModel.deltaDecorations(decorationsToRemove, [])) {
|
|
341
|
+
this.decorationIds.add(newId);
|
|
342
|
+
}
|
|
343
|
+
for (const toRemoveId of decorationsToRemove) {
|
|
344
|
+
this.decorationIds.delete(toRemoveId);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
textModel.applyEdits([
|
|
348
|
+
{
|
|
349
|
+
range: new monaco.Range(1, 1, linesToRemove + 1, textModel.getLineFirstNonWhitespaceColumn(linesToRemove + 1)),
|
|
350
|
+
text,
|
|
351
|
+
forceMoveMarkers: true
|
|
352
|
+
}
|
|
353
|
+
]);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
protected get maxLineNumber(): number {
|
|
358
|
+
return this._maxLineNumber;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
protected set maxLineNumber(maxLineNumber: number) {
|
|
362
|
+
this._maxLineNumber = maxLineNumber;
|
|
363
|
+
this.append(''); // will trigger an `ensureMaxChannelHistory` call and will refresh the content.
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
}
|