blob: 99cc97d14b6b482a043d3c105049a34540c69cb8 [file] [log] [blame]
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
NgZone,
OnChanges,
OnDestroy,
Output,
SimpleChanges
} from '@angular/core';
import { editor as MonacoEditor, IDisposable } from 'monaco-editor';
import IStandaloneCodeEditor = MonacoEditor.IStandaloneCodeEditor;
import IEditor = monaco.editor.IEditor;
import { InterpreterBindingItem } from '@zeppelin/sdk';
import { CompletionService, MessageService } from '@zeppelin/services';
import { pt2px } from '@zeppelin/utility/css-unit-conversion';
import { NotebookParagraphControlComponent } from '../control/control.component';
@Component({
selector: 'zeppelin-notebook-paragraph-code-editor',
templateUrl: './code-editor.component.html',
styleUrls: ['./code-editor.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NotebookParagraphCodeEditorComponent implements OnChanges, OnDestroy, AfterViewInit {
// TODO(hsuanxyz):
// 1. cursor position
@Input() readOnly = false;
@Input() language = 'text';
@Input() paragraphControl: NotebookParagraphControlComponent;
@Input() lineNumbers = false;
@Input() focus = false;
@Input() collaborativeMode = false;
@Input() text: string;
@Input() fontSize: number;
@Input() dirty = false;
@Input() interpreterBindings: InterpreterBindingItem[] = [];
@Input() pid: string;
@Output() readonly textChanged = new EventEmitter<string>();
@Output() readonly editorBlur = new EventEmitter<void>();
@Output() readonly editorFocus = new EventEmitter<void>();
private editor: IStandaloneCodeEditor;
private monacoDisposables: IDisposable[] = [];
height = 0;
interpreterName: string;
autoAdjustEditorHeight() {
if (this.editor) {
this.ngZone.run(() => {
this.height =
this.editor.getTopForLineNumber(Number.MAX_SAFE_INTEGER) + this.editor.getConfiguration().lineHeight * 2;
this.editor.layout();
this.cdr.markForCheck();
});
}
}
initEditorListener() {
const editor = this.editor;
this.monacoDisposables.push(
editor.onDidFocusEditorText(() => {
this.editorFocus.emit();
}),
editor.onDidBlurEditorText(() => {
this.editorBlur.emit();
}),
editor.onDidChangeModelContent(() => {
this.ngZone.run(() => {
this.text = editor.getModel().getValue();
this.textChanged.emit(this.text);
this.setParagraphMode(true);
this.autoAdjustEditorHeight();
setTimeout(() => {
this.autoAdjustEditorHeight();
});
});
})
);
}
setEditorValue() {
if (this.editor && this.editor.getModel() && this.editor.getModel().getValue() !== this.text) {
this.editor.getModel().setValue(this.text || '');
}
}
initializedEditor(editor: IEditor) {
this.editor = editor as IStandaloneCodeEditor;
this.editor.addCommand(
monaco.KeyCode.Escape,
() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
},
'!suggestWidgetVisible'
);
this.updateEditorOptions();
this.setParagraphMode();
this.initEditorListener();
this.initEditorFocus();
this.initCompletionService();
this.setEditorValue();
setTimeout(() => {
this.autoAdjustEditorHeight();
});
}
initCompletionService(): void {
this.completionService.registerAsCompletionReceiver(this.editor.getModel(), this.paragraphControl.pid);
}
initEditorFocus() {
if (this.focus && this.editor) {
this.editor.focus();
}
}
updateEditorOptions() {
if (this.editor) {
this.editor.updateOptions({
readOnly: this.readOnly,
fontSize: pt2px(this.fontSize),
renderLineHighlight: this.focus ? 'all' : 'none',
minimap: { enabled: false },
lineNumbers: this.lineNumbers ? 'on' : 'off',
glyphMargin: false,
folding: false,
scrollBeyondLastLine: false,
contextmenu: false,
matchBrackets: false
});
}
}
getInterpreterName(paragraphText: string) {
const match = /^\s*%(.+?)(\s|\()/g.exec(paragraphText);
if (match) {
return match[1].trim();
// get default interpreter name if paragraph text doesn't start with '%'
// TODO(hsuanxyz): dig into the cause what makes interpreterBindings to have no element
} else if (this.interpreterBindings && this.interpreterBindings.length !== 0) {
return this.interpreterBindings[0].name;
}
return '';
}
setParagraphMode(changed = false) {
if (this.editor && !changed) {
const model = this.editor.getModel();
if (this.language) {
// TODO(hsuanxyz): config convertMap
const convertMap = {
sh: 'shell'
};
monaco.editor.setModelLanguage(model, convertMap[this.language] || this.language);
}
} else {
const interpreterName = this.getInterpreterName(this.text);
if (this.interpreterName !== interpreterName) {
this.interpreterName = interpreterName;
this.getEditorSetting(interpreterName);
}
}
}
getEditorSetting(interpreterName: string) {
this.messageService.editorSetting(this.pid, interpreterName);
}
layout() {
if (this.editor) {
setTimeout(() => {
this.editor.layout();
});
}
}
constructor(
private cdr: ChangeDetectorRef,
private ngZone: NgZone,
private messageService: MessageService,
private completionService: CompletionService
) {}
ngOnChanges(changes: SimpleChanges): void {
const { text, interpreterBindings, language, readOnly, focus, lineNumbers, fontSize } = changes;
if (readOnly || focus || lineNumbers || fontSize) {
this.updateEditorOptions();
}
if (focus) {
this.initEditorFocus();
}
if (text) {
this.setEditorValue();
}
if (interpreterBindings || language) {
this.setParagraphMode();
}
if (text || fontSize) {
this.autoAdjustEditorHeight();
}
}
ngOnDestroy(): void {
this.completionService.unregister(this.editor.getModel());
this.monacoDisposables.forEach(d => d.dispose());
}
ngAfterViewInit(): void {}
}