import { Component, ViewEncapsulation, Input, Output, OnChanges, OnDestroy, EventEmitter } from '@angular/core'; import { RdLib } from '../../base/rdLib'; import { RdComponent } from '../../base/rdComponent'; import { ScriptLoaderService } from "../../library/script-loader.service"; declare const ace; export interface ICompletorItem { meta: string; value: string; items: Array } @Component({ selector: 'rd-code-editor', template: `

    
`, styles: ["pre#editor div, span{ font-family:inherit }"], encapsulation: ViewEncapsulation.None }) export class RdCodeEditor extends RdComponent implements OnChanges, OnDestroy { constructor(private script: ScriptLoaderService) { super(); this.script.load([ './assets/js/ace-editor/ace.js', './assets/js/ace-editor/theme-chrome.js' ]); } @Input("rd-model") model; @Input("rd-mode") mode: string; @Input("rd-tern") tern: boolean; @Input("rd-height") height = 450; @Input("rd-replaceable-in-range") replaceableInRange = false; @Input("rd-btn-text") btnText = RdLib.localization.translateEn("Run"); @Input("rd-completors") completors: Array = []; @Output("rd-modelChange") modelChange: EventEmitter = new EventEmitter(); @Output("rd-content") contentEvent: EventEmitter = new EventEmitter(); public editor; public editorReady = false; public syntaxError = false; private langTools; private showIntellisense = false; private avaibleModes = ["sql", "java", "csharp", "python", "javascript", "json", "xml", "powershell", "text"]; ngOnChanges(changes) { if (changes.model && this.editor && this.model != this.editor.getValue()) { if (this.replaceableInRange && changes.model.previousValue && changes.model.currentValue) { let preModelLength = changes.model.previousValue.length; let newValue = changes.model.currentValue.substring(preModelLength).trim(); // this.editor.session.insert(this.editor.getCursorPosition(), newValue); this.editor.session.replace(this.editor.selection.getRange(), newValue); } else this.editor.setValue(this.model); } if (changes.mode) { if (this.avaibleModes.includes(this.mode)) { if (this.tern) this.setTernMode(); else this.setLanguageTool(); } else RdLib.screenOperations.toastr.warning("Avaible Modes " + this.avaibleModes); } if (changes.completors && this.completors.length && this.langTools) this.setCompletors(); } ngOnDestroy() { this.editor.destroy(); } setEditor() { this.editor = ace.edit("editor"); this.editor.session.setMode("ace/mode/" + this.mode); if (this.model) this.editor.setValue(this.model); this.editor.setTheme("ace/theme/chrome"); this.editor.getSession().setUseWrapMode(true); this.editor.getSession().setWrapLimitRange(null, null); this.editor.setShowPrintMargin(false); this.editor.$blockScrolling = Infinity; this.editor.session.on('change', function (delta) { // {start:{row,column},end:{row,column},lines:[],action:string} this.modelChange.emit(this.editor.getValue()); }.bind(this)); this.editor.getSession().on("changeAnnotation", _ => { this.syntaxError = this.editor.getSession().getAnnotations().filter((item) => { return item.type == "error" }).length; }); /**__Custom Command__ */ this.editor.commands.addCommand({ name: 'saveRun', bindKey: { win: 'ctrl-s ctrl-r', mac: 'command-s' }, exec: e => { if (!this.syntaxError) this.contentEvent.emit(e.getValue()) }, readOnly: true // false if this command should not apply in readOnly mode }); } setTernMode() { let scriptPaths = ["./assets/js/ace-editor/mode-" + this.mode + ".js"]; if (this.mode == "json" || this.mode == "xml") { scriptPaths.push("./assets/js/ace-editor/worker-" + this.mode + ".js"); } this.script.load(scriptPaths).then(_ => this.setTern()); } setLanguageTool() { this.setEditor(); this.editor.on("input", _ => { if (this.showIntellisense) this.editor.execCommand("startAutocomplete") }) this.editor.on("change", e => { if (e.lines[0].trim()) this.showIntellisense = true; else this.showIntellisense = false; // enter & space & backspace }); this.script.load(["./assets/js/ace-editor/ext-language_tools.js"]).then(() => { this.langTools = ace.require("ace/ext/language_tools"); this.editor.setOptions({ enableBasicAutocompletion: true }); this.setCompletors(); this.editorReady = true; }) } setTern() { this.setEditor(); this.script.load([ './assets/js/ace-editor/ext-tern.js', './assets/js/ace-editor/worker-tern.js' ]) .then(() => { let useWebWorker = window.location.search.toLowerCase().indexOf('noworker') == -1; this.editor.getSession().setUseWorker(useWebWorker); ace.config.loadModule('ace/ext/tern', () => { this.editor.setOptions({ enableTern: { defs: ['browser', 'ecma5'], plugins: { doc_comment: { fullDocs: true } }, useWorker: useWebWorker, startedCb: () => { //tern is enabled, it can be accessed via editor.ternServer this.editorReady = true; // RdLib.screenOperations.toastr.info(this.mode + " Mode Enabled"); } }, enableSnippets: true, enableBasicAutocompletion: true }); }); }) } setCompletors() { this.langTools.setCompleters([]); this.langTools.addCompleter({ getCompletions: function (editor, session, pos, prefix, callback) { callback(null, this.updateCompletors(pos.row)); }.bind(this) }); } updateCompletors(row) { let range = ace.require("ace/range").Range; let rowText = this.editor.getSession().getTextRange(new range(row, 0, row, 255)); // end of row let lastSpaceIdx = rowText.lastIndexOf(" "); let lastDotIdx = rowText.indexOf(".", lastSpaceIdx); // last dot after space let prefix = rowText.substring(lastSpaceIdx + 1, lastDotIdx != -1 ? rowText.length - 1 : rowText.length); let replacedPrefix = prefix.replace(/[()]/g, ""); let matchCompletors = this.completors.filter((item) => { let regexedPrefix = new RegExp(replacedPrefix, 'g'); if (item.value.match(regexedPrefix)) return item }); if (lastDotIdx != -1) { let innerComps = this.completors.filter((item) => { return item.value == prefix }).map((mItem) => { return mItem.items })[0]; return innerComps; } else if (matchCompletors.length) return matchCompletors; else return []; } }