diff --git a/css/styles.css b/css/styles.css index 243fa35..5dc0d72 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,3 +1,7 @@ -span.sagecell-error { +.sagecell-error { color: red; +} + +.sagecell-output pre { + margin: 0.5em 0; } \ No newline at end of file diff --git a/manifest.json b/manifest.json index 34f6a57..9f895d8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-sagecell", "name": "SageCell", - "version": "1.0.2", + "version": "1.0.3", "description": "Execute Sage computations in notes.", "author": "Eric Rafaloff", "isDesktopOnly": false diff --git a/src/client.ts b/src/client.ts index 304fb10..87e07c3 100644 --- a/src/client.ts +++ b/src/client.ts @@ -9,20 +9,23 @@ export default class Client { sessionId: string; cellSessionId: string; ws: any; + queue: string[]; outputWriters: any; constructor(settings: any) { this.serverUrl = settings.serverUrl; + this.queue = []; this.outputWriters = {}; } - async connect(): Promise { + connect(): Promise { return new Promise((resolve, reject) => { - if (this.connected) { return resolve(); } + if (this.connected) { return reject(); } this.connected = false; this.sessionId = null; this.cellSessionId = uuidv4(); + this.queue = []; this.outputWriters = {}; this.ws = null; @@ -37,15 +40,17 @@ export default class Client { this.connected = true; resolve(); } - this.ws.onmessage = (msg: any) => { this.handleReplyWithSession(msg); } + this.ws.onmessage = (msg: any) => { this.handleReply(msg); } this.ws.onclose = () => { this.disconnect(); } + this.ws.onerror = () => { this.disconnect(); } }).catch((e) => { + this.disconnect(); reject(e); }); }); } - execute(code: string, outputEl: HTMLElement) { + enqueue(code: string, outputEl: HTMLElement) { const msgId = uuidv4(); const payload = JSON.stringify({ header: { @@ -67,48 +72,55 @@ export default class Client { parent_header: {} }); this.outputWriters[msgId] = new OutputWriter(outputEl); + this.queue.push(payload) + } + + send() { + const payload = this.queue.shift(); this.ws.send(`${this.sessionId}/channels,${payload}`); } - handleReplyWithSession(msg: any) { + async handleReply(msg: any) { const data = JSON.parse(msg.data.substring(46)); const msgType = data.header.msg_type; const msgId = data.parent_header.msg_id; const content = data.content; - if (msgType == 'status' && content.execution_state) { - if (content.execution_state == 'dead') return this.disconnect(); - return; - } if (msgType == 'stream' && content.text) { this.outputWriters[msgId].appendText(content.text); - return; } if (msgType == 'display_data' && content.data['text/image-filename']) { this.outputWriters[msgId].appendImage(this.getFileUrl(content.data['text/image-filename'])); - return; } if (msgType == 'display_data' && content.data['text/html']) { this.outputWriters[msgId].appendSafeHTML(content.data['text/html']); - return; } if (msgType == 'error') { this.outputWriters[msgId].appendError(content); - return; + } + if (msgType == 'execute_reply') { + if (this.queue.length > 0) { + this.send(); + } else { + this.disconnect(); + } } } - disconnect() { - if (this.ws) this.ws.close(); - this.connected = false; - this.sessionId = null; - this.cellSessionId = null; - this.outputWriters = {}; - this.ws = null; + disconnect(): Promise { + return new Promise((resolve, reject) => { + if (this.ws) this.ws.close(); + this.connected = false; + this.sessionId = null; + this.cellSessionId = null; + this.outputWriters = {}; + this.ws = null; + resolve(); + }); } getKernelUrl(): string { - return `${this.serverUrl}/kernel?CellSessionID=${this.cellSessionId}` + return `${this.serverUrl}/kernel?CellSessionID=${this.cellSessionId}&timeout=inf` } getWSUrl(): string { diff --git a/src/main.ts b/src/main.ts index b00ed4b..cc30de3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,26 +21,31 @@ export default class SageCellPlugin extends Plugin { } }); + this.client = new Client(this.settings); this.configurePrismAndCodeMirror(); this.loadMathJax(); } - executeCurrentDoc = () => { + async executeCurrentDoc() { const activeView = this.getActiveView(); const activeFile = activeView.file; const currentMode = activeView.currentMode; const contentEl = activeView.contentEl; - if(activeFile.extension == 'md' && currentMode.type == 'preview') { - contentEl.querySelectorAll('code.is-loaded.language-sage').forEach((codeEl: HTMLElement) => { - const client = new Client(this.settings); - client.connect().then(() => { - const code = codeEl.innerText; - codeEl.innerText = ''; - client.execute(code, codeEl); - }); - }); - } + if (activeFile.extension != 'md' || currentMode.type != 'preview') return; + + await this.client.connect(); + + contentEl.querySelectorAll('code.is-loaded.language-sage').forEach((codeEl: HTMLElement) => { + let outputEl = codeEl.parentNode.parentNode.querySelector('.sagecell-output'); + if (outputEl) outputEl.remove(); + outputEl = document.createElement('div'); + outputEl.className = 'sagecell-output'; + + codeEl.parentNode.parentNode.insertBefore(outputEl, codeEl.nextSibling); + this.client.enqueue(codeEl.innerText, outputEl); + }); + this.client.send(); } getActiveView = (): any => { diff --git a/src/output-writer.ts b/src/output-writer.ts index 1df3a83..d2de32b 100644 --- a/src/output-writer.ts +++ b/src/output-writer.ts @@ -1,14 +1,25 @@ export default class OutputWriter { outputEl: HTMLElement + lastType: string constructor(target: HTMLElement) { this.outputEl = target; + this.lastType = ""; } appendText(text: string) { - const spanEl = document.createElement("span"); - spanEl.innerText = text; - this.outputEl.appendChild(spanEl); + if (this.lastType == 'text') { + const previousPreEl = this.outputEl.querySelectorAll('pre'); + + if (previousPreEl.length > 0) { + previousPreEl[previousPreEl.length-1].innerText += text; + } + } else { + const preEl = document.createElement("pre"); + preEl.innerText = text; + this.outputEl.appendChild(preEl); + } + this.lastType = 'text'; } appendImage(url: string) { @@ -18,6 +29,7 @@ export default class OutputWriter { this.outputEl.appendChild(imgEl); this.outputEl.appendChild(document.createTextNode("\n")); + this.lastType = 'image'; } appendSafeHTML(html: string) { @@ -43,6 +55,7 @@ export default class OutputWriter { }); this.outputEl.innerHTML += safeDoc.body.innerHTML; + this.lastType = 'html'; MathJax.startup.document.clear(); MathJax.startup.document.updateDocument(); @@ -54,5 +67,6 @@ export default class OutputWriter { spanEl.innerText =`${error.ename}: ${error.evalue}`; this.outputEl.appendChild(spanEl); + this.lastType = 'error'; } } \ No newline at end of file