Move logic of useCompareExtension to a Web Worker (#605)

Closes #545.
This commit is contained in:
ConorB
2022-12-17 17:08:56 +00:00
committed by GitHub
parent ecd12f77fb
commit 44c0ecd6cc
6 changed files with 82 additions and 71 deletions

View File

@@ -1,4 +1,4 @@
{
"presets": ["next/babel"],
"plugins": []
"plugins": ["@shopify/web-worker/babel"]
}

View File

@@ -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: {

View File

@@ -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",

View File

@@ -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<string, string | null>({
combine: values => (values.length ? values[0] : null),
})
const targetText = Facet.define<Text, Text | null>({
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<number, GutterMarker>
const diffLineMap = Facet.define<DiffLineMap, DiffLineMap>({
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<EditorView>, compareTo: string, delayMs = 1000): Extension {
const editTime = useRef(0)
const timeSinceEdit = Date.now() - editTime.current
const timeout = useRef<any>()
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<EditorView>, compareTo: string): Extension {
@@ -131,9 +105,9 @@ export default function useCompareExtension(viewRef: RefObject<EditorView>, comp
}, [compartment, compareTo, viewRef])
return [
targetTextComputer,
diffLineMapComputer,
diffGutter,
compartment.of(targetString.of(compareTo)),
diffLineMapCompartment.of(diffLineMap.of({})),
diffLineCalcPlugin,
]
}

View File

@@ -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)
}

View File

@@ -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"