From 44c0ecd6cc9f2bcedbc319d9c5adc132d34dadc7 Mon Sep 17 00:00:00 2001 From: ConorB <11508137+ConorBobbleHat@users.noreply.github.com> Date: Sat, 17 Dec 2022 17:08:56 +0000 Subject: [PATCH] Move logic of useCompareExtension to a Web Worker (#605) Closes #545. --- frontend/.babelrc | 2 +- frontend/next.config.js | 4 + frontend/package.json | 4 +- .../src/lib/codemirror/useCompareExtension.ts | 112 +++++++----------- .../codemirror/useCompareExtension.worker.ts | 14 +++ frontend/yarn.lock | 17 +++ 6 files changed, 82 insertions(+), 71 deletions(-) create mode 100644 frontend/src/lib/codemirror/useCompareExtension.worker.ts diff --git a/frontend/.babelrc b/frontend/.babelrc index 84cbe157..385b98fa 100644 --- a/frontend/.babelrc +++ b/frontend/.babelrc @@ -1,4 +1,4 @@ { "presets": ["next/babel"], - "plugins": [] + "plugins": ["@shopify/web-worker/babel"] } diff --git a/frontend/next.config.js b/frontend/next.config.js index c8f8b33e..c8109b4a 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -28,6 +28,7 @@ const removeImports = require("next-remove-imports")({ //matchImports: "\\.(less|css|scss|sass|styl)$" }) const nextTranslate = require("next-translate") +const { WebWorkerPlugin } = require("@shopify/web-worker/webpack") const mediaUrl = new URL(process.env.MEDIA_URL ?? "http://localhost") @@ -76,6 +77,9 @@ let app = withPlausibleProxy({ use: ["@svgr/webpack"], }) + config.plugins.push(new WebWorkerPlugin()) + config.output.globalObject = "self" + return config }, images: { diff --git a/frontend/package.json b/frontend/package.json index 2516e94b..e19e14ae 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@primer/octicons-react": "^17.2.0", "@react-hook/resize-observer": "^1.2.2", "@replit/codemirror-indentation-markers": "^6.1.0", + "@shopify/web-worker": "^5.0.1", "ansi-to-react": "^6.1.6", "classnames": "^2.3.1", "codemirror": "^6.0.1", @@ -46,7 +47,8 @@ "swr": "^1.3.0", "use-debounce": "^8.0.1", "use-deep-compare-effect": "^1.6.1", - "use-persisted-state": "^0.3.3" + "use-persisted-state": "^0.3.3", + "webpack-virtual-modules": "^0.4.5" }, "devDependencies": { "@babel/core": "^7.17.8", diff --git a/frontend/src/lib/codemirror/useCompareExtension.ts b/frontend/src/lib/codemirror/useCompareExtension.ts index 71281271..397d23ce 100644 --- a/frontend/src/lib/codemirror/useCompareExtension.ts +++ b/frontend/src/lib/codemirror/useCompareExtension.ts @@ -1,64 +1,21 @@ -import { RefObject, useEffect, useRef, useState } from "react" +import { RefObject, useEffect, useState } from "react" -import { EditorState, Compartment, Extension, Facet, Text } from "@codemirror/state" -import { EditorView, gutter, GutterMarker } from "@codemirror/view" -import { diff } from "fast-myers-diff" +import { Compartment, Extension, Facet } from "@codemirror/state" +import { EditorView, gutter, GutterMarker, ViewPlugin, ViewUpdate } from "@codemirror/view" +import { createWorkerFactory } from "@shopify/web-worker" import styles from "./useCompareExtension.module.scss" -function compareNullableText(a: Text | null, b: Text | null): boolean { - if (a === null || b === null) { - return a === b - } else { - return a.eq(b) - } -} - // State for target text to diff doc against const targetString = Facet.define({ combine: values => (values.length ? values[0] : null), }) -const targetText = Facet.define({ - combine: values => (values.length ? values[0] : null), - compare: compareNullableText, - compareInput: compareNullableText, -}) -const targetTextComputer = targetText.compute([targetString], state => { - const s = state.facet(targetString) - if (typeof s === "string") - return Text.of(s.split("\n")) - return null -}) // Computed diff between doc and target type DiffLineMap = Record const diffLineMap = Facet.define({ combine: values => (values.length ? values[0] : {}), }) -const diffLineMapComputer = diffLineMap.compute(["doc", targetString], state => { - const s = state.facet(targetString) - - if (typeof s !== "string") - return {} - - const tokenizeSource = source => { - return source.split("\n").map(i => i.trim()) - } - - const diffsIterator = diff(tokenizeSource(s), tokenizeSource(state.doc.toString())) - const diffs = Array.from(diffsIterator) - - // Convert diff changes to a map of line numbers -> change type - const map: DiffLineMap = {} - - for (const [, , childStartLine, childEndLine] of diffs) { - for (let i = childStartLine; i < childEndLine; i++) { - map[i + 1] = marker - } - } - - return map -}) const marker = new class extends GutterMarker { toDOM() { @@ -88,34 +45,51 @@ const diffGutter = gutter({ } }, lineMarkerChange(update) { - return update.docChanged || !compareNullableText(update.state.facet(targetText), update.startState.facet(targetText)) + return update.docChanged || (update.state.facet(diffLineMap) != update.startState.facet(diffLineMap)) }, initialSpacer: () => marker, }) -export function useDelayedCompareExtension(viewRef: RefObject, compareTo: string, delayMs = 1000): Extension { - const editTime = useRef(0) - const timeSinceEdit = Date.now() - editTime.current - const timeout = useRef() - const [, forceUpdate] = useState({}) +const createDiffWorker = createWorkerFactory(() => import("./useCompareExtension.worker")) - return [ - useCompareExtension(viewRef, timeSinceEdit > delayMs ? compareTo : undefined), - EditorState.transactionExtender.of(tr => { - if (tr.docChanged) { - editTime.current = Date.now() +const diffLineMapCompartment = new Compartment() +const diffLineCalcPlugin = ViewPlugin.fromClass(class { + private worker = createDiffWorker() - if (timeout.current) - clearTimeout(timeout.current) - timeout.current = setTimeout(() => { - forceUpdate({}) - }, delayMs) + constructor(private view: EditorView) { + this.updateDiff() + } + + async update(update: ViewUpdate) { + if (update.docChanged) { + this.updateDiff() + } + } + + async updateDiff() { + const diff = await this.worker.calculateDiff(this.view.state.facet(targetString), this.view.state.doc.toString()) + + // Convert diff changes to a map of line numbers -> change type + let map: DiffLineMap = {} + + for (const [, , childStartLine, childEndLine] of diff) { + for (let i = childStartLine; i < childEndLine; i++) { + map[i + 1] = marker } + } - return null - }), - ] -} + // Has our targetString been updated to a blank, + // and thus we should be showing no diff right now, while + // the view's been updating? + if (typeof this.view.state.facet(targetString) !== "string") { + map = {} + } + + this.view.dispatch({ + effects: diffLineMapCompartment.reconfigure(diffLineMap.of(map)), + }) + } +}) // Extension that highlights lines in the doc that differ from `compareTo`. export default function useCompareExtension(viewRef: RefObject, compareTo: string): Extension { @@ -131,9 +105,9 @@ export default function useCompareExtension(viewRef: RefObject, comp }, [compartment, compareTo, viewRef]) return [ - targetTextComputer, - diffLineMapComputer, diffGutter, compartment.of(targetString.of(compareTo)), + diffLineMapCompartment.of(diffLineMap.of({})), + diffLineCalcPlugin, ] } diff --git a/frontend/src/lib/codemirror/useCompareExtension.worker.ts b/frontend/src/lib/codemirror/useCompareExtension.worker.ts new file mode 100644 index 00000000..2436be0f --- /dev/null +++ b/frontend/src/lib/codemirror/useCompareExtension.worker.ts @@ -0,0 +1,14 @@ +import { diff } from "fast-myers-diff" + +export function calculateDiff(target: string | undefined, current: string): [number, number, number, number][] { + if (typeof target !== "string") { + return [] + } + + const tokenizeSource = source => { + return source.split("\n").map(i => i.trim()) + } + + const diffsIterator = diff(tokenizeSource(target), tokenizeSource(current)) + return Array.from(diffsIterator) +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 9394fb96..9aacfda4 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1393,6 +1393,11 @@ "@react-hook/latest" "^1.0.2" "@react-hook/passive-layout-effect" "^1.2.0" +"@remote-ui/rpc@^1.2.5": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@remote-ui/rpc/-/rpc-1.4.1.tgz#c9a5b69819710c7e35392272c56df6e76322356f" + integrity sha512-YLjZqAeTolxLPvH59jO1W5/A1M0uK8IDtbfnrU8LXEaUPn9kmE7w5z2Isa+Do5m+YjTCn4y5bNHBsUTTsmhO0w== + "@replit/codemirror-indentation-markers@^6.1.0": version "6.1.0" resolved "https://registry.yarnpkg.com/@replit/codemirror-indentation-markers/-/codemirror-indentation-markers-6.1.0.tgz#53ca559f25609c90e2e905624bb0aa57c7b3041a" @@ -1440,6 +1445,13 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== +"@shopify/web-worker@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@shopify/web-worker/-/web-worker-5.0.1.tgz#54b377fae5522b405e004c4971b3924a7b361295" + integrity sha512-fGNcUkzqA9h2dD/3/zBd2YEPCXePN9Mmy53alb6DgpUq8nEgEE1yxj9+PfDi7RsUM+T36d8/QXgReOZ4NvlxuA== + dependencies: + "@remote-ui/rpc" "^1.2.5" + "@surma/rollup-plugin-off-main-thread@^2.2.3": version "2.2.3" resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" @@ -6027,6 +6039,11 @@ webpack-sources@^3.2.3: resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-virtual-modules@^0.4.5: + version "0.4.6" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.4.6.tgz#3e4008230731f1db078d9cb6f68baf8571182b45" + integrity sha512-5tyDlKLqPfMqjT3Q9TAqf2YqjwmnUleZwzJi1A5qXnlBCdj2AtOJ6wAWdglTIDOPgOiOrXeBeFcsQ8+aGQ6QbA== + webpack@5: version "5.75.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.75.0.tgz#1e440468647b2505860e94c9ff3e44d5b582c152"