diff --git a/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx b/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx index 2bdca9e7..ed277943 100644 --- a/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx +++ b/editor-packages/editor-canvas/canvas-event-target/canvas-event-target.tsx @@ -194,6 +194,8 @@ export function CanvasEventTarget({ overflow: "hidden", touchAction: "none", cursor: isSpacebarPressed ? "grab" : "default", + userSelect: "none", + WebkitUserSelect: "none", }} id="gesture-event-listener" ref={interactionEventTargetRef} diff --git a/editor-packages/editor-services-dart/package.json b/editor-packages/editor-services-dart/package.json new file mode 100644 index 00000000..f1d7a29f --- /dev/null +++ b/editor-packages/editor-services-dart/package.json @@ -0,0 +1,6 @@ +{ + "name": "@code-editor/dart-services", + "description": "dart build services for flutter framework", + "version": "0.0.0", + "private": false +} \ No newline at end of file diff --git a/editor-packages/editor-services-esbuild/contributing.md b/editor-packages/editor-services-esbuild/contributing.md new file mode 100644 index 00000000..69d907cc --- /dev/null +++ b/editor-packages/editor-services-esbuild/contributing.md @@ -0,0 +1,5 @@ +# About esbuild-wasm version. + +The wasm file downloaded over network must match the locally installed esbuild-wasm package, so always use exact version. (not ^v, ^v0.0.0, etc.) + +currently using `0.14.34` diff --git a/editor-packages/editor-services-esbuild/examples/.gitignore b/editor-packages/editor-services-esbuild/examples/.gitignore new file mode 100644 index 00000000..78b733f9 --- /dev/null +++ b/editor-packages/editor-services-esbuild/examples/.gitignore @@ -0,0 +1 @@ +*.out.js \ No newline at end of file diff --git a/editor-packages/editor-services-esbuild/fetch.plugin.ts b/editor-packages/editor-services-esbuild/fetch.plugin.ts new file mode 100644 index 00000000..7969f68b --- /dev/null +++ b/editor-packages/editor-services-esbuild/fetch.plugin.ts @@ -0,0 +1,78 @@ +import { OnLoadResult, PluginBuild } from "esbuild-wasm"; +import axios from "axios"; +import localforage from "localforage"; +import { normalizeCss } from "."; + +const fileCache = localforage.createInstance({ + name: "filecache", +}); + +export const fetchPlugin = ( + inputCode: string, + lang: OnLoadResult["loader"] +) => ({ + name: "fetch-plugin", + + setup(build: PluginBuild) { + build.onLoad({ filter: /^index\.js$/ }, () => { + return { + loader: lang, + contents: inputCode, + }; + }); + + build.onLoad({ filter: /.*/ }, async (args: any) => { + /** + * Check if module is already in filecache + * if yes? return it immediately + * + * if not, fetch it from unpkg and cache it + * and return the result + */ + const cachedResult = await fileCache.getItem(args.path); + + if (cachedResult) { + return cachedResult; + } + + return null; + }); + + build.onLoad({ filter: /.css$/ }, async (args: any) => { + const { data, request } = await axios.get(args.path); + + const contents = normalizeCss(data); + + const result: OnLoadResult = { + loader: "jsx", + contents, + resolveDir: new URL("./", request.responseURL).pathname, + }; + + await fileCache.setItem(args.path, result); + + return result; + }); + + build.onLoad({ filter: /.*/ }, async (args: any) => { + const { data, request } = await axios.get(args.path); + + const result: OnLoadResult = { + loader: "jsx", + contents: data, + resolveDir: new URL("./", request.responseURL).pathname, + }; + + await fileCache.setItem(args.path, result); + + return result; + }); + }, +}); + +// const libSource = ReactTypes.toString() + +// const libUri = "ts:filename/facts.d.ts"; +// monaco.languages.typescript.javascriptDefaults.addExtraLib(libSource, libUri); + +// monaco.editor.createModel(libSource, "typescript", monaco.Uri.parse(libUri)); diff --git a/editor-packages/editor-services-esbuild/index.ts b/editor-packages/editor-services-esbuild/index.ts new file mode 100644 index 00000000..724ac3e3 --- /dev/null +++ b/editor-packages/editor-services-esbuild/index.ts @@ -0,0 +1,120 @@ +import { Monaco } from "@monaco-editor/react"; +import { nanoid } from "nanoid"; +import { build, initialize, Loader } from "esbuild-wasm"; +import { fetchPlugin } from "./fetch.plugin"; +import { unpkgPathPlugin } from "./unpkg-path.plugin"; + +declare const window: { + monaco: Monaco; +}; + +let serviceLoaded: boolean | null = null; + +const bundler = async (rawCode: string, lang: Loader) => { + if (!serviceLoaded) { + await initialize({ + wasmURL: "https://unpkg.com/esbuild-wasm@0.14.34/esbuild.wasm", + worker: true, + }); + console.log("esbuild-wasm initialized"); + serviceLoaded = true; + } + + try { + const result = await build({ + entryPoints: ["index.js"], + bundle: true, + write: false, + metafile: true, + legalComments: "none", + plugins: [unpkgPathPlugin(), fetchPlugin(rawCode, lang)], + define: { + "process.env.NODE_ENV": `"production"`, + global: "window", + }, + }); + + const imports = result.metafile?.inputs["a:index.js"].imports + .map((el) => el.path.replace("a:https://unpkg.com/", "")) + .filter((e) => !e.includes("/")); + + loadTypes(imports); + + // console.log("esbuild result: ", result); + + return { code: result.outputFiles[0].text, err: null }; + } catch (error: any) { + console.error("esbuild error: ", error); + return { + code: null, + err: { method: "error", data: [error.message], id: nanoid() }, + }; + } +}; + +export const normalizeCss = (data: string) => { + /** + * Function to remove any new lines, quotes from imported css packages. + */ + const escaped = data + .replace(/\n/g, "") + .replace(/"/g, '\\"') + .replace(/'/g, "\\'"); + return `const style = document.createElement('style') + style.innerText = '${escaped}'; + document.head.appendChild(style)`; +}; + +export default bundler; + +let typesWorker; + +const loadTypes = (types) => { + const disposables: any = []; + const monaco = window && window.monaco; + + const dependencies = types.map((e) => ({ name: e, version: "latest" })) || []; + + if (!typesWorker) { + typesWorker = new Worker( + new URL("./workers/fetch-types.worker.js", import.meta.url) + ); + } + + dependencies.forEach((dep) => { + typesWorker.postMessage({ + name: dep.name, + version: dep.version, + }); + }); + + typesWorker.addEventListener("message", (event) => { + // name, + // version, + // typings: result, + const key = `node_modules/${event.data.name}/index.d.ts`; + const source = event.data.typings[key]; + + // const path = `${MONACO_LIB_PREFIX}${event.data.name}`; + const libUri = `file:///node_modules/@types/${event.data.name}/index.d.ts`; + + disposables.push( + monaco.languages.typescript.javascriptDefaults.addExtraLib(source, libUri) + ); + disposables.push( + monaco.languages.typescript.typescriptDefaults.addExtraLib(source, libUri) + ); + + // When resolving definitions and references, the editor will try to use created models. + // Creating a model for the library allows "peek definition/references" commands to work with the library. + }); + + return { + dispose() { + disposables.forEach((d) => d.dispose()); + if (typesWorker) { + typesWorker.terminate(); + } + }, + }; +}; diff --git a/editor-packages/editor-services-esbuild/package.json b/editor-packages/editor-services-esbuild/package.json new file mode 100644 index 00000000..006a6573 --- /dev/null +++ b/editor-packages/editor-services-esbuild/package.json @@ -0,0 +1,14 @@ +{ + "name": "@code-editor/esbuild-services", + "version": "0.0.0", + "private": false, + "dependencies": { + "esbuild-wasm": "0.14.34", + "localforage": "^1.10.0" + }, + "peerDependencies": { + "@monaco-editor/react": "^4.4.1", + "axios": "^0.26.1", + "nanoid": "^3.3.2" + } +} \ No newline at end of file diff --git a/editor-packages/editor-services-esbuild/unpkg-path.plugin.ts b/editor-packages/editor-services-esbuild/unpkg-path.plugin.ts new file mode 100644 index 00000000..dbfbcbe7 --- /dev/null +++ b/editor-packages/editor-services-esbuild/unpkg-path.plugin.ts @@ -0,0 +1,36 @@ +import { PluginBuild } from "esbuild-wasm"; + +const unpkg_path = "https://unpkg.com"; + +export const unpkgPathPlugin = () => ({ + name: "unpkg-path-plugin", + setup(build: PluginBuild) { + /** + * Resolve the entry file eg. `index.js` + */ + build.onResolve({ filter: /^index\.js$/ }, (args: any) => { + return { path: args.path, namespace: "a" }; + }); + + /** + * Resolve relative modules imports + */ + build.onResolve({ filter: /^\.+\// }, (args: any) => { + const url = new URL(args.path, unpkg_path + args.resolveDir + "/").href; + return { + namespace: "a", + path: url, + }; + }); + + /** + * Resolve main module files + */ + build.onResolve({ filter: /.*/ }, async (args: any) => { + return { + namespace: "a", + path: new URL(args.path, unpkg_path + "/").href, + }; + }); + }, +}); diff --git a/editor-packages/editor-services-esbuild/workers/fetch-types.worker.js b/editor-packages/editor-services-esbuild/workers/fetch-types.worker.js new file mode 100644 index 00000000..24f559ee --- /dev/null +++ b/editor-packages/editor-services-esbuild/workers/fetch-types.worker.js @@ -0,0 +1,282 @@ +/** + * Worker to fetch typescript definitions for dependencies. + * Credits to @CompuIves & @sidwebworks + * https://github.com/CompuIves/codesandbox-client/blob/dcdb4169bcbe3e5aeaebae19ff1d45940c1af834/packages/app/src/app/components/CodeEditor/Monaco/workers/fetch-dependency-typings.js + * + */ + +import path from "path"; + +self.importScripts( + "https://cdnjs.cloudflare.com/ajax/libs/typescript/2.4.2/typescript.min.js" +); + +const ROOT_URL = `https://cdn.jsdelivr.net/`; + +const fetchCache = new Map(); + +const doFetch = (url) => { + const cached = fetchCache.get(url); + + if (cached) { + return cached; + } + + const promise = fetch(url) + .then((response) => { + if (response.status >= 200 && response.status < 300) { + return Promise.resolve(response); + } + + const error = new Error(response.statusText || response.status); + + return Promise.reject(error); + }) + .then((response) => response.text()); + + fetchCache.set(url, promise); + + return promise; +}; + +const fetchFromDefinitelyTyped = (dependency, version, fetchedPaths) => + doFetch( + `${ROOT_URL}npm/@types/${dependency + .replace("@", "") + .replace(/\//g, "__")}/index.d.ts` + ).then((typings) => { + fetchedPaths[`node_modules/${dependency}/index.d.ts`] = typings; + }); + +const getRequireStatements = (title, code) => { + const requires = []; + + const sourceFile = self.ts.createSourceFile( + title, + code, + self.ts.ScriptTarget.Latest, + true, + self.ts.ScriptKind.TS + ); + + self.ts.forEachChild(sourceFile, (node) => { + switch (node.kind) { + case self.ts.SyntaxKind.ImportDeclaration: { + requires.push(node.moduleSpecifier.text); + break; + } + case self.ts.SyntaxKind.ExportDeclaration: { + // For syntax 'export ... from '...''' + if (node.moduleSpecifier) { + requires.push(node.moduleSpecifier.text); + } + break; + } + default: { + /* */ + } + } + }); + + return requires; +}; + +const tempTransformFiles = (files) => { + const finalObj = {}; + + files.forEach((d) => { + finalObj[d.name] = d; + }); + + return finalObj; +}; + +const transformFiles = (dir) => + dir.files + ? dir.files.reduce((prev, next) => { + if (next.type === "file") { + return { ...prev, [next.path]: next }; + } + + return { ...prev, ...transformFiles(next) }; + }, {}) + : {}; + +const getFileMetaData = (dependency, version, depPath) => + doFetch( + `https://data.jsdelivr.com/v1/package/npm/${dependency}@${version}/flat` + ) + .then((response) => JSON.parse(response)) + .then((response) => + response.files.filter((f) => f.name.startsWith(depPath)) + ) + .then(tempTransformFiles); + +const resolveAppropiateFile = (fileMetaData, relativePath) => { + const absolutePath = `/${relativePath}`; + + if (fileMetaData[`${absolutePath}.d.ts`]) return `${relativePath}.d.ts`; + if (fileMetaData[`${absolutePath}.ts`]) return `${relativePath}.ts`; + if (fileMetaData[absolutePath]) return relativePath; + if (fileMetaData[`${absolutePath}/index.d.ts`]) + return `${relativePath}/index.d.ts`; + + return relativePath; +}; + +const getFileTypes = ( + depUrl, + dependency, + depPath, + fetchedPaths, + fileMetaData +) => { + const virtualPath = path.join("node_modules", dependency, depPath); + + if (fetchedPaths[virtualPath]) return null; + + return doFetch(`${depUrl}/${depPath}`).then((typings) => { + if (fetchedPaths[virtualPath]) return null; + + fetchedPaths[virtualPath] = typings; + + // Now find all require statements, so we can download those types too + return Promise.all( + getRequireStatements(depPath, typings) + .filter( + // Don't add global deps + (dep) => dep.startsWith(".") + ) + .map((relativePath) => path.join(path.dirname(depPath), relativePath)) + .map((relativePath) => + resolveAppropiateFile(fileMetaData, relativePath) + ) + .map((nextDepPath) => + getFileTypes( + depUrl, + dependency, + nextDepPath, + fetchedPaths, + fileMetaData + ) + ) + ); + }); +}; + +function fetchFromMeta(dependency, version, fetchedPaths) { + const depUrl = `https://data.jsdelivr.com/v1/package/npm/${dependency}@${version}/flat`; + + return doFetch(depUrl) + .then((response) => JSON.parse(response)) + .then((meta) => { + const filterAndFlatten = (files, filter) => + files.reduce((paths, file) => { + if (filter.test(file.name)) { + paths.push(file.name); + } + return paths; + }, []); + + let dtsFiles = filterAndFlatten(meta.files, /\.d\.ts$/); + if (dtsFiles.length === 0) { + // if no .d.ts files found, fallback to .ts files + dtsFiles = filterAndFlatten(meta.files, /\.ts$/); + } + + if (dtsFiles.length === 0) { + throw new Error(`No inline typings found for ${dependency}@${version}`); + } + + dtsFiles.forEach((file) => { + doFetch(`https://cdn.jsdelivr.net/npm/${dependency}@${version}${file}`) + .then((dtsFile) => { + fetchedPaths[`node_modules/${dependency}${file}`] = dtsFile; + }) + .catch(() => {}); + }); + }); +} + +function fetchFromTypings(dependency, version, fetchedPaths) { + const depUrl = `${ROOT_URL}npm/${dependency}@${version}`; + + return doFetch(`${depUrl}/package.json`) + .then((response) => JSON.parse(response)) + .then((packageJSON) => { + const types = packageJSON.typings || packageJSON.types; + if (types) { + // Add package.json, since this defines where all types lie + fetchedPaths[`node_modules/${dependency}/package.json`] = + JSON.stringify(packageJSON); + + // get all files in the specified directory + return getFileMetaData( + dependency, + version, + path.join("/", path.dirname(types)) + ).then((fileData) => + getFileTypes( + depUrl, + dependency, + resolveAppropiateFile(fileData, types), + fetchedPaths, + fileData + ) + ); + } + + throw new Error( + `No typings field in package.json for ${dependency}@${version}` + ); + }); +} + +function fetchDefinitions(name, version) { + if (!version) { + return Promise.reject(new Error(`No version specified for ${name}`)); + } + + // Query cache for the defintions + const key = `${name}@${version}`; + + // If result is empty, fetch from remote + const fetchedPaths = {}; + + return fetchFromTypings(name, version, fetchedPaths) + .catch(() => + // not available in package.json, try checking meta for inline .d.ts files + fetchFromMeta(name, version, fetchedPaths) + ) + .catch(() => + // Not available in package.json or inline from meta, try checking in @types/ + fetchFromDefinitelyTyped(name, version, fetchedPaths) + ) + .then(() => { + if (Object.keys(fetchedPaths).length) { + // Also cache the definitions + + return fetchedPaths; + } else { + throw new Error(`Type definitions are empty for ${key}`); + } + }); +} + +self.addEventListener("message", (event) => { + const { name, version } = event.data; + + fetchDefinitions(name, version) + .then((result) => { + self.postMessage({ + name, + version, + typings: result, + }); + }) + .catch((error) => { + if (process.env.NODE_ENV !== "production") { + console.error(error); + } + }); +}); diff --git a/editor-packages/editor-services-jsx-syntax-highlight/index.ts b/editor-packages/editor-services-jsx-syntax-highlight/index.ts new file mode 100644 index 00000000..6f8b0a0f --- /dev/null +++ b/editor-packages/editor-services-jsx-syntax-highlight/index.ts @@ -0,0 +1,57 @@ +import { Monaco } from "@monaco-editor/react"; +import { createWorkerQueue } from "@code-editor/webworker-services-core"; + +import type { editor } from "monaco-editor"; +export function registerJsxHighlighter( + editor: editor.IStandaloneCodeEditor, + monaco: Monaco +) { + const { worker: syntaxWorker } = createWorkerQueue( + new Worker(new URL("./workers/syntax-highlight.worker.js", import.meta.url)) + ); + + const highlightHandler = () => { + const title = "app.js"; + const model = editor.getModel(); + const version = model?.getVersionId(); + const lang = model?.getLanguageId(); + + if (lang === "javascript" || "typescript") { + const code = model?.getValue(); + syntaxWorker.postMessage({ + code, + title, + version, + }); + } + }; + + editor.onDidChangeModel(highlightHandler); + + editor.onDidChangeModelContent(highlightHandler); + + let oldDecor = editor.getModel()?.getAllDecorations(); + + syntaxWorker.addEventListener("message", (event) => { + const { classifications } = event.data; + + requestAnimationFrame(() => { + const decorations = classifications.map((classification) => ({ + range: new monaco.Range( + classification.startLine, + classification.start, + classification.endLine, + classification.end + ), + options: { + inlineClassName: classification.type + ? `${classification.kind} ${classification.type}-of-${classification.parentKind}` + : classification.kind, + }, + })); + + // @ts-ignore + oldDecor = editor.deltaDecorations(oldDecor, decorations); + }); + }); +} diff --git a/editor-packages/editor-services-jsx-syntax-highlight/package.json b/editor-packages/editor-services-jsx-syntax-highlight/package.json new file mode 100644 index 00000000..d9e4f53f --- /dev/null +++ b/editor-packages/editor-services-jsx-syntax-highlight/package.json @@ -0,0 +1,9 @@ +{ + "name": "@code-editor/jsx-syntax-highlight-services", + "version": "0.0.0", + "private": false, + "dependencies": {}, + "peerDependencies": { + "@monaco-editor/react": "^4.4.1" + } +} \ No newline at end of file diff --git a/editor-packages/editor-services-jsx-syntax-highlight/workers/syntax-highlight.worker.js b/editor-packages/editor-services-jsx-syntax-highlight/workers/syntax-highlight.worker.js new file mode 100644 index 00000000..52d0c714 --- /dev/null +++ b/editor-packages/editor-services-jsx-syntax-highlight/workers/syntax-highlight.worker.js @@ -0,0 +1,109 @@ +self.importScripts([ + "https://cdnjs.cloudflare.com/ajax/libs/typescript/2.4.2/typescript.min.js", +]); + +function getLineNumberAndOffset(start, lines) { + let line = 0; + let offset = 0; + while (offset + lines[line] < start) { + offset += lines[line] + 1; + line += 1; + } + + return { line: line + 1, offset }; +} + +function nodeToRange(node) { + if ( + typeof node.getStart === "function" && + typeof node.getEnd === "function" + ) { + return [node.getStart(), node.getEnd()]; + } else if ( + typeof node.pos !== "undefined" && + typeof node.end !== "undefined" + ) { + return [node.pos, node.end]; + } + return [0, 0]; +} + +function getNodeType(parent, node) { + return Object.keys(parent).find((key) => parent[key] === node); +} + +function getParentRanges(node) { + const ranges = []; + const [start, end] = nodeToRange(node); + let lastEnd = start; + + self.ts.forEachChild(node, (child) => { + const [start, end] = nodeToRange(child); + + ranges.push({ + start: lastEnd, + end: start, + }); + lastEnd = end; + }); + + if (lastEnd !== end) { + ranges.push({ + start: lastEnd, + end, + }); + } + + return ranges; +} + +function addChildNodes(node, lines, classifications) { + const parentKind = ts.SyntaxKind[node.kind]; + + self.ts.forEachChild(node, (id) => { + const type = getNodeType(node, id); + + classifications.push( + ...getParentRanges(id).map(({ start, end }) => { + const { offset, line: startLine } = getLineNumberAndOffset( + start, + lines + ); + const { line: endLine } = getLineNumberAndOffset(end, lines); + + return { + start: start + 1 - offset, + end: end + 1 - offset, + kind: ts.SyntaxKind[id.kind], + parentKind, + type, + startLine, + endLine, + }; + }) + ); + + addChildNodes(id, lines, classifications); + }); +} + +// Respond to message from parent thread +self.addEventListener("message", (event) => { + let { code, title, version } = event.data; + try { + const classifications = []; + const sourceFile = self.ts.createSourceFile( + title, + code, + self.ts.ScriptTarget.ES6, + true + ); + const lines = code.split("\n").map((line) => line.length); + + addChildNodes(sourceFile, lines, classifications); + + self.postMessage({ classifications, version }, { targetOrigin: "*" }); + } catch (e) { + /* Ignore error */ + } +}); diff --git a/editor/utils/dart/dart-format.ts b/editor-packages/editor-services-prettier/dart.ts similarity index 72% rename from editor/utils/dart/dart-format.ts rename to editor-packages/editor-services-prettier/dart.ts index 9d8a7599..373d1a27 100644 --- a/editor/utils/dart/dart-format.ts +++ b/editor-packages/editor-services-prettier/dart.ts @@ -1,9 +1,8 @@ -/// THIS IS A DUPLICATE CODE FROM ASSISTANT/APP/UTILS/DART-FORMAT -/// considering it's size, we decided to use it as a duplicate function. - import { formatCode } from "dart-style"; - +// THIS IS A DUPLICATE CODE FROM ASSISTANT/APP/UTILS/DART-FORMAT +// considering it's size, we decided to use it as a duplicate function. // formatter contains some issue. https://github.com/Dart-Code/Dart-Code/issues/2822 + export function format(code: string): string { if (code === undefined) { return code; diff --git a/editor-packages/editor-services-prettier/index.ts b/editor-packages/editor-services-prettier/index.ts new file mode 100644 index 00000000..847f96d5 --- /dev/null +++ b/editor-packages/editor-services-prettier/index.ts @@ -0,0 +1,95 @@ +import * as monaco from "monaco-editor"; +import { formatCode as formatDartCode } from "dart-style"; +import { createWorkerQueue } from "@code-editor/webworker-services-core"; + +export let __dangerous__lastFormattedValue__global: string; + +export function registerDocumentPrettier(editor, monaco) { + const disposables: monaco.IDisposable[] = []; + let prettierWorker; + + const dartFormattingEditProvider = { + provideDocumentFormattingEdits: (model, options, token) => { + const raw = model.getValue(); + const { code, error } = formatDartCode(raw); + if (error) return []; + __dangerous__lastFormattedValue__global = code; + return [ + { + range: model.getFullModelRange(), + text: code, + }, + ]; + }, + }; + + const prettierFormattingEditProvider = { + async provideDocumentFormattingEdits(model, _options, _token) { + if (!prettierWorker) { + prettierWorker = createWorkerQueue( + new Worker(new URL("./workers/prettier.worker.js", import.meta.url)) + ); + } + + const { canceled, error, pretty } = await prettierWorker?.emit({ + text: model.getValue(), + language: model._languageId, + }); + + if (canceled || error) return []; + __dangerous__lastFormattedValue__global = pretty; + return [ + { + range: model.getFullModelRange(), + text: pretty, + }, + ]; + }, + }; + + disposables.push( + monaco.languages.registerDocumentFormattingEditProvider( + "javascript", + prettierFormattingEditProvider + ) + ); + + disposables.push( + monaco.languages.registerDocumentFormattingEditProvider( + "typescript", + prettierFormattingEditProvider + ) + ); + + disposables.push( + monaco.languages.registerDocumentFormattingEditProvider( + "dart", + dartFormattingEditProvider + ) + ); + + disposables.push( + monaco.languages.registerDocumentFormattingEditProvider( + "html", + prettierFormattingEditProvider + ) + ); + + disposables.push( + monaco.languages.registerDocumentFormattingEditProvider( + "css", + prettierFormattingEditProvider + ) + ); + + editor.getAction("editor.action.formatDocument").run(); + + return { + dispose() { + disposables.forEach((disposable) => disposable.dispose()); + if (prettierWorker) { + prettierWorker.terminate(); + } + }, + }; +} diff --git a/editor-packages/editor-services-prettier/package.json b/editor-packages/editor-services-prettier/package.json new file mode 100644 index 00000000..6c303437 --- /dev/null +++ b/editor-packages/editor-services-prettier/package.json @@ -0,0 +1,11 @@ +{ + "name": "@code-editor/prettier-services", + "version": "0.0.0", + "private": false, + "dependencies": { + "dart-style": "^1.3.2-dev" + }, + "peerDependencies": { + "@monaco-editor/react": "^4.4.1" + } +} \ No newline at end of file diff --git a/editor-packages/editor-services-prettier/workers/prettier.worker.js b/editor-packages/editor-services-prettier/workers/prettier.worker.js new file mode 100644 index 00000000..e17d4350 --- /dev/null +++ b/editor-packages/editor-services-prettier/workers/prettier.worker.js @@ -0,0 +1,61 @@ +import prettier from "prettier"; + +const options = { + html: async () => ({ + parser: "html", + plugins: [await import("prettier/parser-html")], + printWidth: 90, + }), + + css: async () => ({ + parser: "css", + plugins: [await import("prettier/parser-postcss")], + printWidth: 100, + }), + + javascript: async () => ({ + parser: "babel", + plugins: [await import("prettier/parser-babel")], + printWidth: 100, + semi: true, + useTabs: false, + singleQuote: true, + }), + typescript: async () => ({ + parser: "babel", + plugins: [await import("prettier/parser-babel")], + printWidth: 100, + semi: true, + useTabs: false, + singleQuote: true, + }), +}; + +let current; + +addEventListener("message", async (event) => { + if (event.data._current) { + current = event.data._current; + return; + } + + function respond(data) { + setTimeout(() => { + if (event.data._id === current) { + postMessage({ _id: event.data._id, ...data }); + } else { + postMessage({ _id: event.data._id, canceled: true }); + } + }, 0); + } + + const opts = await options[event.data.language](); + + try { + respond({ + pretty: prettier.format(event.data.text, opts), + }); + } catch (error) { + respond({ error }); + } +}); diff --git a/editor-packages/editor-services-webworker-core/index.ts b/editor-packages/editor-services-webworker-core/index.ts new file mode 100644 index 00000000..e4fee1cc --- /dev/null +++ b/editor-packages/editor-services-webworker-core/index.ts @@ -0,0 +1,41 @@ +import PQueue from "p-queue"; + +export function createWorkerQueue(worker) { + const queue = new PQueue({ concurrency: 1 }); + return { + worker, + emit(data) { + queue.clear(); + const _id = Math.random().toString(36).substr(2, 5); + worker.postMessage({ _current: _id }); + return queue.add( + () => + new Promise((resolve) => { + function onMessage(event) { + if (event.data._id !== _id) return; + worker.removeEventListener("message", onMessage); + resolve(event.data); + } + worker.addEventListener("message", onMessage); + worker.postMessage({ ...data, _id }); + }) + ); + }, + terminate() { + worker.terminate(); + }, + }; +} + +export function requestResponse(worker, data) { + return new Promise((resolve) => { + const _id = Math.random().toString(36).substr(2, 5); + function onMessage(event) { + if (event.data._id !== _id) return; + worker.removeEventListener("message", onMessage); + resolve(event.data); + } + worker.addEventListener("message", onMessage); + worker.postMessage({ ...data, _id }); + }); +} diff --git a/editor-packages/editor-services-webworker-core/package.json b/editor-packages/editor-services-webworker-core/package.json new file mode 100644 index 00000000..c38f1014 --- /dev/null +++ b/editor-packages/editor-services-webworker-core/package.json @@ -0,0 +1,8 @@ +{ + "name": "@code-editor/webworker-services-core", + "version": "0.0.0", + "private": false, + "dependencies": { + "p-queue": "^7.2.0" + } +} \ No newline at end of file diff --git a/editor/components/app-runner/app-runner.tsx b/editor/components/app-runner/app-runner.tsx index 121e88d7..07980532 100644 --- a/editor/components/app-runner/app-runner.tsx +++ b/editor/components/app-runner/app-runner.tsx @@ -94,14 +94,15 @@ function DedicatedFrameworkRunner({ name: `${_name}.dart`, }) .then((r) => { - const qlurl = features.quicklook.buildConsoleQuicklookUrl({ - id: nanoid(), - framework: types.AppFramework.flutter, - language: types.AppLanguage.dart, - url: r.url, - name: _name, - }); - open(qlurl); + throw "flutter preview disabled"; + // const qlurl = features.quicklook.buildConsoleQuicklookUrl({ + // id: nanoid(), + // framework: types.AppFramework.flutter, + // language: types.AppLanguage.dart, + // url: r.url, + // name: _name, + // }); + // open(qlurl); }); }} > diff --git a/editor/components/app-runner/index.ts b/editor/components/app-runner/index.ts index 856d919b..837b1d5f 100644 --- a/editor/components/app-runner/index.ts +++ b/editor/components/app-runner/index.ts @@ -2,3 +2,6 @@ export * from "./app-runner"; export * from "./csb-runner"; export * from "./flutter-app-runner"; export * from "./react-app-runner"; +export * from "./loading-indicator"; +export * from "./vanilla-esbuild-app-runner"; +export * from "./vanilla-dedicated-preview-renderer"; diff --git a/editor/components/app-runner/loading-indicator.tsx b/editor/components/app-runner/loading-indicator.tsx new file mode 100644 index 00000000..fd59e833 --- /dev/null +++ b/editor/components/app-runner/loading-indicator.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import styled from "@emotion/styled"; +import { LoadingOneDotFadeInAndOutInfinite } from "components/loading"; + +export function RunnerLoadingIndicator({ size = 24 }: { size?: number }) { + return ( + + + + + + ); +} + +const Container = styled.div<{ size: number }>` + background-color: black; + width: ${(props) => props.size}px; + height: ${(props) => props.size}px; + border-radius: 4px; + position: relative; + box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.25); +`; + +const Dot = styled.div` + width: 4px; + height: 4px; + position: absolute; + left: calc((calc((50% + 0px)) - 2px)); + top: calc((calc((50% + 0px)) - 2px)); +`; diff --git a/editor/components/app-runner/vanilla-app-runner.tsx b/editor/components/app-runner/vanilla-app-runner.tsx index eb646acb..fce45076 100644 --- a/editor/components/app-runner/vanilla-app-runner.tsx +++ b/editor/components/app-runner/vanilla-app-runner.tsx @@ -1,25 +1,31 @@ -import React, { useEffect, useRef } from "react"; +import React, { ReactEventHandler, useEffect, useRef } from "react"; -export function VanillaRunner({ - width, - height, - source, - enableInspector = true, - style, -}: { - width: string; - height: string; - source: string; - componentName: string; - enableInspector?: boolean; - style?: React.CSSProperties; -}) { - const ref = useRef(); +export const VanillaRunner = React.forwardRef(function ( + { + width = "100%", + height = "100%", + source, + onLoad, + enableInspector = true, + style, + }: { + width?: React.CSSProperties["width"]; + height?: React.CSSProperties["height"]; + source: string; + onLoad?: ReactEventHandler; + componentName: string; + enableInspector?: boolean; + style?: React.CSSProperties; + }, + ref: React.MutableRefObject +) { + const lref = useRef(null); + const cref = ref || lref; useEffect(() => { - if (ref.current) { + if (cref.current) { function disablezoom() { - ref.current.contentWindow.addEventListener( + cref.current.contentWindow.addEventListener( "wheel", (event) => { const { ctrlKey } = event; @@ -31,20 +37,21 @@ export function VanillaRunner({ { passive: false } ); } - ref.current.contentWindow.addEventListener( + cref.current.contentWindow.addEventListener( "DOMContentLoaded", disablezoom, false ); } - }, [ref.current]); + }, [cref.current]); useEffect(() => { - if (ref.current && enableInspector) { - ref.current.onload = () => { - const matches = ref.current.contentDocument.querySelectorAll( - "div, span, img, image, svg" // button, input - disabled due to interaction testing (for users) - ); + const cb = (e) => { + if (enableInspector) { + const matches = + cref.current?.contentDocument?.querySelectorAll( + "div, span, img, image, svg" // button, input - disabled due to interaction testing (for users) + ) ?? []; matches.forEach((el) => { const tint = "rgba(20, 0, 255, 0.2)"; const tintl = "rgba(20, 0, 255, 0.5)"; @@ -70,17 +77,26 @@ export function VanillaRunner({ } }); - ref.current.contentWindow.addEventListener("click", (e) => { - console.log("click", e); - }); - }; + // cref.current.contentWindow.addEventListener("click", (e) => { + // console.log("click", e); + // }); + } + }; + + if (cref.current) { + cref.current.onload = cb; } - }, [ref.current, enableInspector]); + + return () => { + cref?.current?.onload && (cref.current.onload = () => {}); + }; + }, [cref.current, enableInspector]); const inlinesource = source || `
`; return (