diff --git a/config/webpack.dev.js b/config/webpack.dev.js index a7b798f1..122e8cce 100644 --- a/config/webpack.dev.js +++ b/config/webpack.dev.js @@ -48,6 +48,15 @@ module.exports = (env, argv) => { }, }, compress: emulateProdServer, + //TODO: Remove this when a more sensible paradigm for code-running is established + proxy: [ + { + context: ["/api/v4"], + target: "https://7f67beaa.compilers.sphere-engine.com", + secure: false, + changeOrigin: true + } + ] }, plugins: [ // create an html page for every item in ./site/views diff --git a/src/rich-text/commands/index.ts b/src/rich-text/commands/index.ts index 0c66224b..a3dba4eb 100644 --- a/src/rich-text/commands/index.ts +++ b/src/rich-text/commands/index.ts @@ -21,6 +21,7 @@ import { inTable } from "./tables"; export * from "./tables"; export * from "./list"; +export * from "./run-code" // indent code with four [SPACE] characters (hope you aren't a "tabs" person) const CODE_INDENT_STR = " "; @@ -505,6 +506,27 @@ export function nodeTypeActive( }; } +export function nodeTypeNotIn( + nodeTypes: NodeType[] +){ + return function (state: EditorState) { + const { from, to } = state.selection; + let isExcluded = false; + const nodeTypeNames = nodeTypes.map(nt => nt.name); + + // check all nodes in the selection for the right type + state.doc.nodesBetween(from, to, (node) => { + isExcluded = nodeTypeNames.includes(node.type.name); + // stop recursing if the current node is in the exclusion list + if(isExcluded){ + return false; + } + }); + + return !isExcluded; + }; +} + /** * Creates an `active` method that returns true of the current selection has the passed mark * @param mark The mark to check for diff --git a/src/rich-text/commands/run-code.ts b/src/rich-text/commands/run-code.ts new file mode 100644 index 00000000..239d5a2d --- /dev/null +++ b/src/rich-text/commands/run-code.ts @@ -0,0 +1,83 @@ +import {EditorState, Transaction} from "prosemirror-state"; +import {EditorView} from "prosemirror-view"; +import {schema} from "prosemirror-markdown"; +import {Node} from "prosemirror-model"; +import {log} from "../../shared/logger"; +import {MenuCommand} from "../../shared/menu"; + +export const runCodeBlockCommand: MenuCommand = ( + state: EditorState, + dispatch: (tr: Transaction) => void, + view?: EditorView +): boolean => { + const { from, to } = state.selection; + let isCodeblock = false; + let codeBlockNode: Node; + + state.doc.nodesBetween(from, to, (node) => { + isCodeblock = node.type.name === schema.nodes.code_block.name; + codeBlockNode = node; + return !isCodeblock; + }); + + if(!isCodeblock){ + return false; + } + + //Time to run some code boys + if(dispatch) { + const sourceCode = codeBlockNode.textContent;//.replace(/"/g, '\\"'); + log("runCodeBlockCommand - codeblock source", sourceCode) + + //TODO: Hey, probs should move this out to a Stack owned server first. + //TODO - in fact... CORS will fuck you immediately doing this. + fetch("/api/v4/submissions?access_token=",{ + method: "POST", + headers: { + "content-type": "application/json;charset=UTF-8", + "Access-Control-Allow-Origin": "*" + }, + body: JSON.stringify({ + "compilerId": 86, + "compilerVersionId": 7, + "source": sourceCode + }) + }) + .then((res) => res.json()) + .then((data) => { + log("runCodeBlockCommand submission result!", data) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return data.id as number; + }) + .then((subId) => { + //wait 3 secs + return new Promise(resolve => setTimeout(() => resolve(subId), 8000)) + }) + .then((subId: number) => fetch(`/api/v4/submissions/${subId}?access_token=`, { + headers: { + "Access-Control-Allow-Origin": "*" + } + })) + .then((res) => res.json()) + .then((data) => { + log("runCodeBlockCommand execution result!", data) + //Essentially, Is this done, is it finished, and do we have a place to grab the result? + if(!data.executing && data.result.status.code == 15 && data.result.streams.output.uri){ + const fetchUri: string = data.result.streams.output.uri.slice(44); + log(`runCodeBlockCommand result uri ${fetchUri}`); + return fetch(fetchUri, { + headers: { + "Access-Control-Allow-Origin": "*" + } + }); + } + return new Response(JSON.stringify({error: "no results"})); + }) + .then((res) => res.text()) + .then((data) => { + log(`runCodeBlockCommand result`, data) + }) + .catch(err => log("runCodeBlockCommand err", err)) + } + return true; +} diff --git a/src/rich-text/node-views/code-block.ts b/src/rich-text/node-views/code-block.ts index 1b8c7618..498591d1 100644 --- a/src/rich-text/node-views/code-block.ts +++ b/src/rich-text/node-views/code-block.ts @@ -4,6 +4,7 @@ import type { IExternalPluginProvider } from "../../shared/editor-plugin"; import { getBlockLanguage } from "../../shared/highlighting/highlight-plugin"; import { _t } from "../../shared/localization"; import { escapeHTML, generateRandomId } from "../../shared/utils"; +import { log } from "../../shared/logger"; type getPosParam = boolean | (() => number); @@ -22,6 +23,7 @@ export class CodeBlockView implements NodeView { getPos: getPosParam, private additionalProcessors: IExternalPluginProvider["codeblockProcessors"] ) { + log("CodeView", "Constructor entered") this.dom = document.createElement("div"); this.dom.classList.add("ps-relative", "p0", "ws-normal", "ow-normal"); this.render(view, getPos); @@ -37,6 +39,8 @@ export class CodeBlockView implements NodeView { const rawLanguage = this.getLanguageFromBlock(node); + log("CodeView", `Detected language : ${rawLanguage}`); + const processorApplies = this.getValidProcessorResult( rawLanguage, node @@ -109,12 +113,16 @@ export class CodeBlockView implements NodeView { let autodetectedLanguage = node.attrs .detectedHighlightLanguage as string; + log("Codeview", `Autodetected: ${autodetectedLanguage}`) + if (autodetectedLanguage) { autodetectedLanguage = _t("nodes.codeblock_lang_auto", { lang: autodetectedLanguage, }); } + log("Codeview", `Autodetected pp: ${autodetectedLanguage}`) + return autodetectedLanguage || getBlockLanguage(node); } diff --git a/src/shared/code-execution/code-execution-options.ts b/src/shared/code-execution/code-execution-options.ts new file mode 100644 index 00000000..74141481 --- /dev/null +++ b/src/shared/code-execution/code-execution-options.ts @@ -0,0 +1,37 @@ +//TODO: Type the source language to ones we support execution for +type SourceLanguage = string; + +/** Execution context for a snippet - a language and block of source code */ +interface SnippetExecutionContext { + /** Language the source code will be compiled in */ + compilerLanguage: SourceLanguage; + source: string; +} + +/** A single attachment that will be loaded in a multi-file context */ +interface SourceAttachment { + /** Name of the file within execution context */ + file: string + /** Folder structure where the file is found in execution context */ + filepath: string; + /** Contents of the file */ + source: string; +} + +/** Execution context for multiple files - a language and a file/folder structure */ +interface MultifileExecutionContext { + compilerLanguage: SourceLanguage; + files: SourceAttachment[] +} + +type ExecutionContext = SnippetExecutionContext | MultifileExecutionContext; + +export interface CodeExecutionProvider { + /** Responsible for submitting the code for execution, returning a URL to get the results */ + submissionHandler: (context: ExecutionContext) => Promise; +} + +export function isSnippetContext(context: ExecutionContext): context is SnippetExecutionContext { + const snippet = context as SnippetExecutionContext; + return snippet.source !== undefined && typeof snippet.source == "string"; +} diff --git a/src/shared/localization.ts b/src/shared/localization.ts index 6218087b..216b96c4 100644 --- a/src/shared/localization.ts +++ b/src/shared/localization.ts @@ -21,6 +21,10 @@ export const defaultStrings = { title: shortcut("Code block"), description: "Multiline block of code with syntax highlighting", }, + run_code_block: { + title: shortcut("Run"), + description: "Run the current code block" + }, emphasis: shortcut("Italic"), heading: { dropdown: shortcut("Heading"), diff --git a/src/shared/menu/entries.ts b/src/shared/menu/entries.ts index 03a2a2f5..141ada99 100644 --- a/src/shared/menu/entries.ts +++ b/src/shared/menu/entries.ts @@ -41,6 +41,8 @@ import { insertRichTextHorizontalRuleCommand, insertRichTextTableCommand, toggleList, + nodeTypeNotIn, + runCodeBlockCommand } from "../../rich-text/commands"; import { _t } from "../localization"; import { makeMenuButton, makeMenuDropdown } from "./helpers"; @@ -112,7 +114,7 @@ const headingDropdown = (schema: Schema) => "Header", _t("commands.heading.dropdown", { shortcut: getShortcut("Mod-H") }), "heading-dropdown", - () => true, + nodeTypeNotIn([schema.nodes.code_block]), nodeTypeActive(schema.nodes.heading), makeDropdownItem( _t("commands.heading.entry", { level: 1 }), @@ -363,6 +365,25 @@ export const createMenuEntries = ( "code-block-btn" ), }, + //TODO: Make addIf, should probably lean on plugins more? (how does Image Upload do this, for example?) + { + key: "runCodeblock", + richText: { + command: runCodeBlockCommand, + visible: nodeTypeActive(schema.nodes.code_block) + }, + commonmark: null, + display: makeMenuButton( + "Play", + { + title: _t("commands.run_code_block.title", { + shortcut: getShortcut("Mod-9") + }), + description: _t("commands.run_code_block.description") + }, + "code-block-run-btn" + ) + } ], }, { @@ -397,7 +418,10 @@ export const createMenuEntries = ( addIf( { key: "insertImage", - richText: insertRichTextImageCommand, + richText: { + command: insertRichTextImageCommand, + visible: nodeTypeNotIn([schema.nodes.code_block]) + }, commonmark: insertCommonmarkImageCommand, display: makeMenuButton( "Image", @@ -414,8 +438,10 @@ export const createMenuEntries = ( key: "insertTable", richText: { command: insertRichTextTableCommand, - visible: (state: EditorState) => - !inTable(state.schema, state.selection), + visible: (state: EditorState) => { + return nodeTypeNotIn([schema.nodes.code_block])(state) + && !inTable(state.schema, state.selection) + }, }, commonmark: insertCommonmarkTableCommand, display: makeMenuButton( @@ -476,7 +502,10 @@ export const createMenuEntries = ( }, { key: "insertRule", - richText: insertRichTextHorizontalRuleCommand, + richText: { + command: insertRichTextHorizontalRuleCommand, + visible: nodeTypeNotIn([schema.nodes.code_block]) + }, commonmark: insertCommonmarkHorizontalRuleCommand, display: makeMenuButton( "HorizontalRule", diff --git a/src/shared/menu/plugin.ts b/src/shared/menu/plugin.ts index b0ce8c41..0a83a544 100644 --- a/src/shared/menu/plugin.ts +++ b/src/shared/menu/plugin.ts @@ -1,6 +1,6 @@ import { EditorState, - Plugin, + Plugin, PluginKey, PluginView, Transaction, } from "prosemirror-state"; @@ -16,6 +16,7 @@ import { makeMenuButton, } from "./helpers"; import { hidePopover } from "@stackoverflow/stacks"; +import {log} from "../logger"; /** NoOp to use in place of missing commands */ const commandNoOp = () => false; @@ -399,6 +400,8 @@ export class MenuView implements PluginView { } } +export const MenuKey = new PluginKey("MenuPlugin"); + /** * Creates a menu plugin with the passed in entries * @param blocks The entries to use on the generated menu @@ -410,6 +413,7 @@ export function createMenuPlugin( containerFn: (view: EditorView) => Node, editorType: EditorType ): Plugin { + log("createMenuPlugin blocks", blocks); return new Plugin({ view(editorView) { const menuView = new MenuView(blocks, editorView, editorType); diff --git a/src/shared/view.ts b/src/shared/view.ts index 39a95971..7c14a5d2 100644 --- a/src/shared/view.ts +++ b/src/shared/view.ts @@ -6,6 +6,7 @@ import type { EditorView } from "prosemirror-view"; import { EditorPlugin } from "./editor-plugin"; import type { ImageUploadOptions } from "./prosemirror-plugins/image-upload"; import { setAttributesOnElement, stackOverflowValidateLink } from "./utils"; +import {CodeExecutionProvider} from "./code-execution/code-execution-options"; /** Describes each distinct editor type the StacksEditor handles */ export enum EditorType { @@ -46,6 +47,7 @@ export interface CommonViewOptions { imageUpload?: ImageUploadOptions; /** Externally written plugins to add to the editor */ editorPlugins?: EditorPlugin[]; + codeExecutionProvider?: CodeExecutionProvider; } /** Configuration options for parsing and rendering [tag:*] and [meta-tag:*] syntax */ diff --git a/src/stacks-editor/editor.ts b/src/stacks-editor/editor.ts index 8151970f..fbaba57e 100644 --- a/src/stacks-editor/editor.ts +++ b/src/stacks-editor/editor.ts @@ -154,9 +154,11 @@ export class StacksEditor implements View { enabled: false, renderer: null, }, + codeExecutionProvider: null }, richTextOptions: { classList: commonClasses, + codeExecutionProvider: null }, }; }