@theia/monaco
Version:
Theia - Monaco Extension
235 lines (207 loc) • 9.92 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2023 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor';
import { CodeEditorWidget, ICodeEditorWidgetOptions } from '@theia/monaco-editor-core/esm/vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { IInstantiationService } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/instantiation';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection';
import { Disposable, DisposableCollection, Emitter, Event, TextDocumentContentChangeDelta, URI } from '@theia/core';
import { MonacoEditorModel } from './monaco-editor-model';
import { Dimension, EditorMouseEvent, MouseTarget, Position, TextDocumentChangeEvent } from '@theia/editor/lib/browser';
import * as monaco from '@theia/monaco-editor-core';
import { ElementExt } from '@theia/core/shared/@lumino/domutils';
import { Selection } from '@theia/editor/lib/browser/editor';
import { SelectionDirection } from '@theia/monaco-editor-core/esm/vs/editor/common/core/selection';
import { ShowLightbulbIconMode } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
export class SimpleMonacoEditor extends MonacoEditorServices implements Disposable {
protected editor: CodeEditorWidget;
protected readonly toDispose = new DisposableCollection();
protected readonly onCursorPositionChangedEmitter = new Emitter<Position>();
protected readonly onFocusChangedEmitter = new Emitter<boolean>();
protected readonly onDocumentContentChangedEmitter = new Emitter<TextDocumentChangeEvent>();
readonly onDocumentContentChanged = this.onDocumentContentChangedEmitter.event;
protected readonly onMouseDownEmitter = new Emitter<EditorMouseEvent>();
readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly;
protected readonly onLanguageChangedEmitter = new Emitter<string>();
readonly onLanguageChanged = this.onLanguageChangedEmitter.event;
protected readonly onScrollChangedEmitter = new Emitter<void>();
readonly onEncodingChanged = this.document.onDidChangeEncoding;
protected readonly onResizeEmitter = new Emitter<Dimension | null>();
readonly onDidResize = this.onResizeEmitter.event;
get onDispose(): Event<void> {
return this.editor.onDidDispose;
}
constructor(
readonly uri: URI,
readonly document: MonacoEditorModel,
readonly node: HTMLElement,
services: MonacoEditorServices,
options?: MonacoEditor.IOptions,
override?: EditorServiceOverrides,
widgetOptions?: ICodeEditorWidgetOptions
) {
super(services);
this.toDispose.pushAll([
this.onCursorPositionChangedEmitter,
this.onFocusChangedEmitter,
this.onDocumentContentChangedEmitter,
this.onMouseDownEmitter,
this.onLanguageChangedEmitter,
this.onScrollChangedEmitter
]);
this.toDispose.push(this.create({
...MonacoEditor.createReadOnlyOptions(document.readOnly),
...options,
model: undefined,
}, override, widgetOptions));
this.addHandlers(this.editor);
this.editor.setModel(document.textEditorModel);
}
getControl(): CodeEditorWidget {
return this.editor;
}
onSelectionChanged(listener: (range: Selection) => void): Disposable {
return this.editor.onDidChangeCursorSelection(event =>
listener({
...this.m2p.asRange(event.selection),
direction: event.selection.getDirection() === SelectionDirection.LTR ? 'ltr' : 'rtl'
}));
}
protected create(options?: MonacoEditor.IOptions, override?: EditorServiceOverrides, widgetOptions?: ICodeEditorWidgetOptions): Disposable {
const combinedOptions = {
...options,
lightbulb: { enabled: ShowLightbulbIconMode.On },
fixedOverflowWidgets: true,
automaticLayout: true,
scrollbar: {
useShadows: false,
verticalHasArrows: false,
horizontalHasArrows: false,
verticalScrollbarSize: 10,
horizontalScrollbarSize: 10,
...options?.scrollbar,
}
};
const instantiator = this.getInstantiatorWithOverrides(override);
return this.editor = instantiator.createInstance(CodeEditorWidget, this.node, {
...combinedOptions,
dimension: {
width: 0,
height: 0
},
}, widgetOptions ?? {});
}
protected addHandlers(codeEditor: CodeEditorWidget): void {
this.toDispose.push(codeEditor.onDidChangeModelLanguage(e =>
this.fireLanguageChanged(e.newLanguage)
));
this.toDispose.push(codeEditor.onDidChangeConfiguration(() => this.refresh()));
this.toDispose.push(codeEditor.onDidChangeModel(() => this.refresh()));
this.toDispose.push(codeEditor.onDidChangeModelContent(e => {
this.refresh();
this.onDocumentContentChangedEmitter.fire({ document: this.document, contentChanges: e.changes.map(this.mapModelContentChange.bind(this)) });
}));
this.toDispose.push(codeEditor.onMouseDown(e => {
const { element, position, range } = e.target;
this.onMouseDownEmitter.fire({
target: {
...(e.target as unknown as MouseTarget),
element: element || undefined,
mouseColumn: this.m2p.asPosition(undefined, e.target.mouseColumn).character,
range: range && this.m2p.asRange(range) || undefined,
position: position && this.m2p.asPosition(position.lineNumber, position.column) || undefined,
detail: undefined
},
event: e.event.browserEvent
});
}));
this.toDispose.push(codeEditor.onDidScrollChange(e => {
this.onScrollChangedEmitter.fire(undefined);
}));
this.toDispose.push(this.onDidChangeReadOnly(readOnly => {
codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly));
}));
}
setLanguage(languageId: string): void {
monaco.editor.setModelLanguage(this.document.textEditorModel, languageId);
}
protected fireLanguageChanged(languageId: string): void {
this.onLanguageChangedEmitter.fire(languageId);
}
protected getInstantiatorWithOverrides(override?: EditorServiceOverrides): IInstantiationService {
const instantiator = StandaloneServices.get(IInstantiationService);
if (override) {
const overrideServices = new ServiceCollection(...override);
const childService = instantiator.createChild(overrideServices);
this.toDispose.push(childService);
return childService;
}
return instantiator;
}
protected mapModelContentChange(change: monaco.editor.IModelContentChange): TextDocumentContentChangeDelta {
return {
range: this.m2p.asRange(change.range),
rangeLength: change.rangeLength,
text: change.text
};
}
focus(): void {
this.editor.focus();
}
refresh(): void {
this.autoresize();
}
resizeToFit(): void {
this.autoresize();
// eslint-disable-next-line no-null/no-null
this.onResizeEmitter.fire(null);
}
setSize(dimension: Dimension): void {
this.resize(dimension);
this.onResizeEmitter.fire(dimension);
}
protected autoresize(): void {
this.resize();
}
protected resize(dimension?: Dimension): void {
if (this.node) {
const layoutSize = this.computeLayoutSize(this.node, dimension);
this.editor.layout(layoutSize);
}
}
protected computeLayoutSize(hostNode: HTMLElement, dimension: monaco.editor.IDimension | undefined): monaco.editor.IDimension {
if (dimension && dimension.width >= 0 && dimension.height >= 0) {
return dimension;
}
const boxSizing = ElementExt.boxSizing(hostNode);
const width = (!dimension || dimension.width < 0) ?
this.getWidth(hostNode, boxSizing) :
dimension.width;
const height = (!dimension || dimension.height < 0) ?
this.getHeight(hostNode, boxSizing) :
dimension.height;
return { width, height };
}
protected getWidth(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number {
return hostNode.offsetWidth - boxSizing.horizontalSum;
}
protected getHeight(hostNode: HTMLElement, boxSizing: ElementExt.IBoxSizing): number {
return this.editor.getContentHeight();
}
dispose(): void {
this.toDispose.dispose();
}
}