Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/message/browser/messageController.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { renderMarkdown } from '../../../../base/browser/markdownRenderer.js';
7
import { alert } from '../../../../base/browser/ui/aria/aria.js';
8
import { Event } from '../../../../base/common/event.js';
9
import { IMarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';
10
import { KeyCode } from '../../../../base/common/keyCodes.js';
11
import { DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
12
import './messageController.css';
13
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../browser/editorBrowser.js';
14
import { EditorCommand, EditorContributionInstantiation, registerEditorCommand, registerEditorContribution } from '../../../browser/editorExtensions.js';
15
import { IPosition } from '../../../common/core/position.js';
16
import { Range } from '../../../common/core/range.js';
17
import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js';
18
import { PositionAffinity } from '../../../common/model.js';
19
import { openLinkFromMarkdown } from '../../../browser/widget/markdownRenderer/browser/markdownRenderer.js';
20
import * as nls from '../../../../nls.js';
21
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
22
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
23
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
24
import * as dom from '../../../../base/browser/dom.js';
25
26
export class MessageController implements IEditorContribution {
27
28
public static readonly ID = 'editor.contrib.messageController';
29
30
static readonly MESSAGE_VISIBLE = new RawContextKey<boolean>('messageVisible', false, nls.localize('messageVisible', 'Whether the editor is currently showing an inline message'));
31
32
static get(editor: ICodeEditor): MessageController | null {
33
return editor.getContribution<MessageController>(MessageController.ID);
34
}
35
36
private readonly _editor: ICodeEditor;
37
private readonly _visible: IContextKey<boolean>;
38
private readonly _messageWidget = new MutableDisposable<MessageWidget>();
39
private readonly _messageListeners = new DisposableStore();
40
private _mouseOverMessage: boolean = false;
41
42
constructor(
43
editor: ICodeEditor,
44
@IContextKeyService contextKeyService: IContextKeyService,
45
@IOpenerService private readonly _openerService: IOpenerService
46
) {
47
48
this._editor = editor;
49
this._visible = MessageController.MESSAGE_VISIBLE.bindTo(contextKeyService);
50
}
51
52
dispose(): void {
53
this._messageListeners.dispose();
54
this._messageWidget.dispose();
55
this._visible.reset();
56
}
57
58
isVisible() {
59
return this._visible.get();
60
}
61
62
showMessage(message: IMarkdownString | string, position: IPosition): void {
63
64
alert(isMarkdownString(message) ? message.value : message);
65
66
this._visible.set(true);
67
this._messageWidget.clear();
68
this._messageListeners.clear();
69
70
if (isMarkdownString(message)) {
71
const renderedMessage = this._messageListeners.add(renderMarkdown(message, {
72
actionHandler: (url, mdStr) => {
73
this.closeMessage();
74
openLinkFromMarkdown(this._openerService, url, mdStr.isTrusted);
75
},
76
}));
77
this._messageWidget.value = new MessageWidget(this._editor, position, renderedMessage.element);
78
} else {
79
this._messageWidget.value = new MessageWidget(this._editor, position, message);
80
}
81
82
// close on blur (debounced to allow to tab into the message), cursor, model change, dispose
83
this._messageListeners.add(Event.debounce(this._editor.onDidBlurEditorText, (last, event) => event, 0)(() => {
84
if (this._mouseOverMessage) {
85
return; // override when mouse over message
86
}
87
88
if (this._messageWidget.value && dom.isAncestor(dom.getActiveElement(), this._messageWidget.value.getDomNode())) {
89
return; // override when focus is inside the message
90
}
91
92
this.closeMessage();
93
}
94
));
95
this._messageListeners.add(this._editor.onDidChangeCursorPosition(() => this.closeMessage()));
96
this._messageListeners.add(this._editor.onDidDispose(() => this.closeMessage()));
97
this._messageListeners.add(this._editor.onDidChangeModel(() => this.closeMessage()));
98
this._messageListeners.add(dom.addDisposableListener(this._messageWidget.value.getDomNode(), dom.EventType.MOUSE_ENTER, () => this._mouseOverMessage = true, true));
99
this._messageListeners.add(dom.addDisposableListener(this._messageWidget.value.getDomNode(), dom.EventType.MOUSE_LEAVE, () => this._mouseOverMessage = false, true));
100
101
// close on mouse move
102
let bounds: Range;
103
this._messageListeners.add(this._editor.onMouseMove(e => {
104
// outside the text area
105
if (!e.target.position) {
106
return;
107
}
108
109
if (!bounds) {
110
// define bounding box around position and first mouse occurance
111
bounds = new Range(position.lineNumber - 3, 1, e.target.position.lineNumber + 3, 1);
112
} else if (!bounds.containsPosition(e.target.position)) {
113
// check if position is still in bounds
114
this.closeMessage();
115
}
116
}));
117
}
118
119
closeMessage(): void {
120
this._visible.reset();
121
this._messageListeners.clear();
122
if (this._messageWidget.value) {
123
this._messageListeners.add(MessageWidget.fadeOut(this._messageWidget.value));
124
}
125
}
126
}
127
128
const MessageCommand = EditorCommand.bindToContribution<MessageController>(MessageController.get);
129
130
131
registerEditorCommand(new MessageCommand({
132
id: 'leaveEditorMessage',
133
precondition: MessageController.MESSAGE_VISIBLE,
134
handler: c => c.closeMessage(),
135
kbOpts: {
136
weight: KeybindingWeight.EditorContrib + 30,
137
primary: KeyCode.Escape
138
}
139
}));
140
141
class MessageWidget implements IContentWidget {
142
143
// Editor.IContentWidget.allowEditorOverflow
144
readonly allowEditorOverflow = true;
145
readonly suppressMouseDown = false;
146
147
private readonly _editor: ICodeEditor;
148
private readonly _position: IPosition;
149
private readonly _domNode: HTMLDivElement;
150
151
static fadeOut(messageWidget: MessageWidget): IDisposable {
152
const dispose = () => {
153
messageWidget.dispose();
154
clearTimeout(handle);
155
messageWidget.getDomNode().removeEventListener('animationend', dispose);
156
};
157
const handle = setTimeout(dispose, 110);
158
messageWidget.getDomNode().addEventListener('animationend', dispose);
159
messageWidget.getDomNode().classList.add('fadeOut');
160
return { dispose };
161
}
162
163
constructor(editor: ICodeEditor, { lineNumber, column }: IPosition, text: HTMLElement | string) {
164
165
this._editor = editor;
166
this._editor.revealLinesInCenterIfOutsideViewport(lineNumber, lineNumber, ScrollType.Smooth);
167
this._position = { lineNumber, column };
168
169
this._domNode = document.createElement('div');
170
this._domNode.classList.add('monaco-editor-overlaymessage');
171
this._domNode.style.marginLeft = '-6px';
172
173
const anchorTop = document.createElement('div');
174
anchorTop.classList.add('anchor', 'top');
175
this._domNode.appendChild(anchorTop);
176
177
const message = document.createElement('div');
178
if (typeof text === 'string') {
179
message.classList.add('message');
180
message.textContent = text;
181
} else {
182
text.classList.add('message');
183
message.appendChild(text);
184
}
185
this._domNode.appendChild(message);
186
187
const anchorBottom = document.createElement('div');
188
anchorBottom.classList.add('anchor', 'below');
189
this._domNode.appendChild(anchorBottom);
190
191
this._editor.addContentWidget(this);
192
this._domNode.classList.add('fadeIn');
193
}
194
195
dispose() {
196
this._editor.removeContentWidget(this);
197
}
198
199
getId(): string {
200
return 'messageoverlay';
201
}
202
203
getDomNode(): HTMLElement {
204
return this._domNode;
205
}
206
207
getPosition(): IContentWidgetPosition {
208
return {
209
position: this._position,
210
preference: [
211
ContentWidgetPositionPreference.ABOVE,
212
ContentWidgetPositionPreference.BELOW,
213
],
214
positionAffinity: PositionAffinity.Right,
215
};
216
}
217
218
afterRender(position: ContentWidgetPositionPreference | null): void {
219
this._domNode.classList.toggle('below', position === ContentWidgetPositionPreference.BELOW);
220
}
221
222
}
223
224
registerEditorContribution(MessageController.ID, MessageController, EditorContributionInstantiation.Lazy);
225
226