import { AtomDisposableList } from "@web-atoms/core/dist/core/AtomDisposableList"; import { IDisposable, UMD } from "@web-atoms/core/dist/core/types"; import { Inject } from "@web-atoms/core/dist/di/Inject"; import { AtomViewModel, Receive } from "@web-atoms/core/dist/view-model/AtomViewModel"; import Load from "@web-atoms/core/dist/view-model/Load"; import Channels from "../Channels"; import FileModel from "../model/FileModel"; import SourceService from "../services/SourceService"; import MonacoDocument from "./MonacoDocument"; const path = UMD.resolvePath("@web-atoms/online-editor/index.html"); export default class CodeEditorViewModel extends AtomViewModel { /** * File url */ public url: string; public owner: any; public dependencies: { [key: string]: FileModel }; private waitForInit: Promise; private container: any; // this will be set by monaco editor.. private monaco: any; // this will be set by Monaco editor... private editor: monaco.editor.IStandaloneCodeEditor; private disposableList = new AtomDisposableList(); @Inject private sourceService: SourceService; @Receive(Channels.EditorTrigger) public receiveGoToLine(channel: string, d: any) { this.editor.trigger("source", d.id, undefined); } @Receive(Channels.GotoLocation) public gotoLocation(channel: string, d: any) { const model = this.editor.getModel(); const position = model.getPositionAt(d.start); this.editor.focus(); this.editor.setPosition(position); this.editor.revealPosition(position); } @Load({ init: true }) public async load() { const element = this.owner.element; // setup ready... this.waitForInit = new Promise((resolve, reject) => { const iframe = element as HTMLIFrameElement; (iframe as any).parentEditor = { codeEditor: this, onresize: () => this.onResize(), openUrl: (url, name, features) => this.app.broadcast("open-url", { url, name, features }), resolve, reject }; iframe.srcdoc = MonacoDocument; }); this.monaco = await this.waitForInit; } @Load({ watch: true }) public async loadContent() { const file = this.owner.file as FileModel; if (!file) { return; } await this.waitForInit; // const monaco = this.monaco; // store global instance... window.monaco = this.monaco; this.disposableList.dispose(); if (!this.editor) { monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true); // only set this first time... monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ target: monaco.languages.typescript.ScriptTarget.ES2015, module: monaco.languages.typescript.ModuleKind.UMD, sourceMap: true, moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, outDir: "http://a/dist/", rootDir: "http://a/", skipLibCheck: true, experimentalDecorators: true, emitDecoratorMetadata: true, jsx: monaco.languages.typescript.JsxEmit.React, jsxFactory: "XNode.create" }); } if (/\.(ts|tsx)$/i.test(file.name)) { const d = monaco.languages.typescript.typescriptDefaults; const dd = this.generateLibs(file); d.setExtraLibs(dd); } else { if (!file.canEdit) { return; } } this.editor = this.createEditor(this.container, { model: file.model, readOnly: file.isReadOnly }); const editor = this.editor; const model = this.editor.getModel(); // this.disposableList.add(); // if (file.name.endsWith(".tsx")) { // this.disposableList.add(this.editor.onKeyUp((e) => { // if (e.keyCode === monaco.KeyCode.Slash || // (e.shiftKey && e.keyCode === monaco.KeyCode.NUMPAD_SEPARATOR)) { // // this is '>'... // const position = editor.getPosition(); // const left = model.getValueInRange(monaco.Range.fromPositions( // { lineNumber: 1, column: 1 }, position)); // const tagStart = left.lastIndexOf("<"); // if (tagStart === -1) { // return; // } // const tag = left.substring(tagStart + 1); // const match = e.keyCode === monaco.KeyCode.Slash // ? /^[a-z0-9\.]+\/$/i // : /^[a-z0-9\.]+\>$/i; // if (!match.test(tag)) { // return; // } // const text = e.keyCode === monaco.KeyCode.Slash ? ">" : ` { // this.sourceService.position = { // column: e.position.column, // lineNumber: e.position.lineNumber // }; // })); // this.disposableList.add(this.editor); } private createEditor(element, { model, readOnly = false }) { let editor = this.editor; if(!editor) { editor = monaco.editor.create(element, { model, readOnly, domReadOnly: false }); const editorService = (editor as any)._codeEditorService; const openEditorBase = editorService.openCodeEditor.bind(editorService); editorService.openCodeEditor = async (input, source) => { const result = await openEditorBase(input, source); const current = editor.getModel(); if (result?.uri !== current.uri) { // alert("intercepted"); // console.log("Open definition for:", input); // console.log("Corresponding model:", monaco.editor.getModel(input.resource)); // console.log("Source: ", source); // source.setModel(monaco.editor.getModel(input.resource)); this.app.broadcast("open-url", { url: input.resource.toString(true) }); } return result; // always return the base result }; // this.registerDisposable(editor.onKeyUp((e) => { // if (e.keyCode === monaco.KeyCode.Slash || // (e.shiftKey && e.keyCode === monaco.KeyCode.NUMPAD_SEPARATOR)) { // // this is '>'... // const position = editor.getPosition(); // const left = model.getValueInRange(monaco.Range.fromPositions( // { lineNumber: 1, column: 1 }, position)); // const tagStart = left.lastIndexOf("<"); // if (tagStart === -1) { // return; // } // const tag = left.substring(tagStart + 1); // const match = e.keyCode === monaco.KeyCode.Slash // ? /^[a-z0-9\.]+\/$/i // : /^[a-z0-9\.]+\>$/i; // if (!match.test(tag)) { // return; // } // const text = e.keyCode === monaco.KeyCode.Slash ? ">" : ` { for (const { text, rangeLength, rangeOffset } of e.changes) { const i = text.indexOf("/node_modules/"); if (i === -1) { continue; } const si = text.lastIndexOf("\"", i); if (si === -1) { continue; } const rt = text.substring(0, si + 1) + text.substring(i + "/node_modules/".length); const start = model.getPositionAt(rangeOffset); const end = model.getPositionAt(rangeOffset + text.length); const r2 = monaco.Range.fromPositions(start, end); const id = { major: 1, minor: 1}; const op = { identifier: id, range: r2, text: rt, forceMoveMarkers: true }; this.editor.executeEdits("format", [op]); break; } })); this.registerDisposable(editor.onDidChangeCursorPosition((e) => { this.sourceService.position = { column: e.position.column, lineNumber: e.position.lineNumber }; })); this.registerDisposable(editor); } else { editor.setModel(model); editor.updateOptions({ readOnly, domReadOnly: false }); } return editor; } private onResize(): void { if (!this.editor) { return; } this.editor.layout(); } private generateLibs(file: FileModel): {content: string, filePath?: string}[] { const a = []; const a1 = {}; for (const source of this.sourceService.root.get("src").descendants) { if (!/\.(ts|tsx)$/.test(source.name) || source === file) { continue; } a.push({ content: source.content, filePath: `http://a/${source.url}` }); a1[source.url] = source; } for (const d of this.sourceService.root.get("node_modules").children) { if (d.name === "@web-atoms") { for (const wa of d.children) { const dist = wa.get("dist") || wa; for (const wad of dist.descendants) { if (wad.name.endsWith(".d.ts")) { a.push({ content: wad.content, filePath: `http://a/${wad.url}`}); a.push({ content: wad.content, filePath: `http://a/${wad.url.replace("@", "%40")}`}); } } } continue; } for (const dd of d.descendants) { if (dd.name.endsWith(".d.ts")) { const filePath = `http://a/${dd.url}`; a.push({ content: dd.content, filePath }); a1[filePath] = dd; } } } // for (const element of this.sourceService.root.descendants) { // if (element === file) { // continue; // } // const key = element.url; // if (!/\.(ts|tsx)$/.test(key)) { // continue; // } // const isDeclaration = /\.d\.ts$/.test(key); // const name = isDeclaration // ? key.substr(0, key.length - 5) // : key.substr(0, key.length - 3); // if (isDeclaration) { // const tsFile = `${name}.ts`; // if (a1[tsFile]) { // delete a1[tsFile]; // } // a1[key] = element; // } else { // a1[key] = element; // } // } // for (const key in a1) { // if (a1.hasOwnProperty(key)) { // const element = a1[key]; // if (element.url.startsWith("node_modules")) { // } else { // a.push({ content: element.content, filePath: `http://a/${element.url}` }); // } // } // } this.dependencies = a1; return a; } }