Skip to content

Commit

Permalink
perf: optimize continuous input
Browse files Browse the repository at this point in the history
  • Loading branch information
KazariEX committed Oct 9, 2024
1 parent 7c6cfa3 commit ca15618
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 70 deletions.
54 changes: 27 additions & 27 deletions src/diff.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,87 @@
import type { LoadLine } from "./types";

export function diff(textLines: string[], loadLines: LoadLine[]) {
export function diff(
textLines: string | string[],
dataLines: string | unknown[],
getText?: (index: number) => string,
addition?: (index: number) => boolean
) {
let i = 0;
for (; i < textLines.length; i++) {
if (textLines[i] !== loadLines[i]?.text) {
break;
}
if (loadLines[i]?.loads.some(({ range }) => range.collapsed)) {
const text = getText?.(i) ?? dataLines[i];
if (textLines[i] !== text || addition?.(i)) {
break;
}
}

let j = textLines.length - 1;
let k = loadLines.length - 1;
let k = dataLines.length - 1;
for (; j >= 0 && k >= 0; j--, k--) {
if (textLines[j] !== loadLines[k]?.text) {
break;
}
if (loadLines[k]?.loads.some(({ range }) => range.collapsed)) {
const text = getText?.(k) ?? dataLines[k];
if (textLines[j] !== text || addition?.(k)) {
break;
}
}
j = textLines.length > dataLines.length ? j : k;

if (i > j) {
[i, j] = [j, i];
}
i = Math.max(0, i);
j = Math.min(textLines.length - 1, j) + 1;

if (textLines.length === loadLines.length && i + 1 === j) {
if (textLines.length === dataLines.length && i + 1 === j) {
return [i, j];
}
return [...expand(textLines, i, j)];
return expand([...textLines], i, j);
}

function* expand(textLines: string[], start: number, end: number) {
const side = matchTwoSides(Math.max(0, start + end - textLines.length));
function expand(chars: string[], start: number, end: number) {
const side = matchTwoSides(Math.max(0, start + end - chars.length));
start -= side;
end += side;

function matchTwoSides(offset: number) {
const pos = textLines.slice(0, start).indexOf(textLines[end], offset);
const pos = chars.slice(0, start).indexOf(chars[end], offset);

if (pos === -1) {
return 0;
}
for (let j = pos, k = end; j < start - 1; k++, j++) {
if (textLines[j + 1] !== textLines[k + 1]) {
if (chars[j + 1] !== chars[k + 1]) {
return matchTwoSides(offset + 1);
}
}
return start - pos;
}

const inner = new Set(textLines.slice(start, end));
const inner = new Set(chars.slice(start, end));

const left = [];
for (let i = start - 1, it = end - 1; inner.has(textLines[i]); i--, it--) {
const text = textLines[i];
for (let i = start - 1, it = end - 1; inner.has(chars[i]); i--, it--) {
const text = chars[i];

for (; it >= start; it--) {
if (textLines[it] === text) {
if (chars[it] === text) {
break;
}
}
if (it >= start) {
left.push(text);
}
}
yield start - left.length;

const right = [];
for (let i = end, it = start; inner.has(textLines[i]); i++, it++) {
const text = textLines[i];
for (let i = end, it = start; inner.has(chars[i]); i++, it++) {
const text = chars[i];

for (; it < end; it++) {
if (textLines[it] === text) {
if (chars[it] === text) {
break;
}
}
if (it < end) {
right.push(text);
}
}
yield end + right.length;

return [start - left.length, end + right.length] as const;
}
96 changes: 66 additions & 30 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { BundledLanguage, BundledTheme, CodeToTokensWithThemesOptions, HighlighterCore } from "shiki";
import { diff } from "./diff";
import { debounce, isArrayEqual, once } from "./utils";
import { isArrayEqual, once, throttle } from "./utils";
import type { ColorLoad, LoadLine } from "./types";

export interface MountPlainShikiOptions {
Expand Down Expand Up @@ -40,9 +40,9 @@ export interface MountPlainShikiOptions {
watch?: boolean;

/**
* @description Delay in updating when `watch: true`.
* @description Throttle delay in updating when `watch: true`.
*
* @default 0
* @default 33.4
*/
delay?: number;
}
Expand All @@ -62,36 +62,15 @@ export function createPlainShiki(shiki: HighlighterCore) {
defaultTheme = "light",
selector = (theme) => `.${theme}`,
watch = true,
delay = 0
delay = 16.7
} = options;

const debouncedUpdate = delay > 0 ? debounce(update, { delay }) : update;

const stylesheet = new CSSStyleSheet();
document.adoptedStyleSheets.push(stylesheet);

const colorRanges = new Map<string, Set<Range>>();
const loadLines: LoadLine[] = [];

if (isSupported()) {
watch && el.addEventListener("input", debouncedUpdate);
update();
}

const dispose = once(() => {
watch && el.removeEventListener("input", debouncedUpdate);

const idx = document.adoptedStyleSheets.indexOf(stylesheet);
document.adoptedStyleSheets.splice(idx, 1);

for (const [name, ranges] of colorRanges) {
const highlight = CSS.highlights.get(name);
for (const range of ranges) {
highlight?.delete(range);
}
}
});

function patch(loads: ColorLoad[], oldLoads: ColorLoad[]) {
for (const { range, name } of walkTokens(oldLoads)) {
const highlight = CSS.highlights.get(name);
Expand Down Expand Up @@ -124,12 +103,33 @@ export function createPlainShiki(shiki: HighlighterCore) {
}
}

function update() {
const { innerText } = el;
const textLines = innerText.split("\n");
const textNodes = collectTextNodes(el);
function tempUpdate(textLines: string[], start: number, end: number) {
if (end - start > 1) {
return;
}

const textLine = textLines[start];
const loadLine = loadLines[start];
const result = diff(textLine, loadLine.text);
const left = loadLine.offset + result[0];
const right = loadLine.offset + result[1];

const load = loadLine.loads?.find(({ range }) => left >= range.startOffset && left < range.endOffset);
if (!load) {
return;
}

const [start, end] = diff(textLines, loadLines);
const { range } = load;
range.setEnd(range.endContainer, Math.max(range.endOffset, right));
}

const fullUpdate = throttle((
innerText: string,
textLines: string[],
textNodes: Text[],
start: number,
end: number
) => {
const length = end - textLines.length + loadLines.length;
const chunk = loadLines.splice(length);
for (let i = start; i < length; i++) {
Expand Down Expand Up @@ -170,6 +170,7 @@ export function createPlainShiki(shiki: HighlighterCore) {
patch(loads, loadLine.loads);
loadLine.loads = loads;
loadLine.text = text;
loadLine.offset = offset;

const oldScopes = loadLine.lastGrammarState?.getScopes() ?? [Number.NaN];
const newScopes = tokenResult.grammarState?.getScopes() ?? [Number.NaN];
Expand All @@ -182,6 +183,40 @@ export function createPlainShiki(shiki: HighlighterCore) {
}
else break;
}
}, delay);

if (isSupported()) {
watch && el.addEventListener("input", update);
update();
}

const dispose = once(() => {
watch && el.removeEventListener("input", update);

const idx = document.adoptedStyleSheets.indexOf(stylesheet);
document.adoptedStyleSheets.splice(idx, 1);

for (const [name, ranges] of colorRanges) {
const highlight = CSS.highlights.get(name);
for (const range of ranges) {
highlight?.delete(range);
}
}
});

function update() {
const { innerText } = el;
const textLines = innerText.split("\n");
const textNodes = collectTextNodes(el);

const [start, end] = diff(
textLines,
loadLines,
(i) => loadLines[i]?.text,
(i) => loadLines[i]?.loads.some(({ range }) => range.collapsed)
);
tempUpdate(textLines, start, end);
fullUpdate(innerText, textLines, textNodes, start, end);
}

return {
Expand Down Expand Up @@ -227,6 +262,7 @@ function collectTextNodes(el: HTMLElement) {
function createLoadLine(options: Partial<LoadLine> = {}) {
return {
text: "",
offset: 0,
lastGrammarState: void 0,
loads: [],
...options
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface ColorLoad {

export interface LoadLine {
text: string;
offset: number;
lastGrammarState: GrammarState | undefined;
loads: ColorLoad[];
}
14 changes: 9 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
export function debounce<T extends unknown[]>(func: (...args: T) => void, {
delay = 1500
} = {}) {
export function throttle<T extends unknown[]>(func: (...args: T) => void, delay: number) {
let start: number;
let timer: NodeJS.Timeout | undefined = void 0;
return function(this: unknown, ...args: T) {
!timer && func.apply(this, args);
if (!timer) {
start = performance.now();
func.apply(this, args);
}
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
timer = void 0;
}, delay);
}, delay - (performance.now() - start) / 1000);
};
}

Expand Down
9 changes: 1 addition & 8 deletions test/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,5 @@ it("add N lines have discontinuous sequential subset in adjacent", () => {
});

function diffWith(oldChars: string[], newChars: string[]) {
const textLines = newChars;
const loadLines = oldChars.map((char) => ({
text: char,
lastGrammarState: void 0,
loads: []
} as LoadLine));

return diff(textLines, loadLines);
return diff(newChars, oldChars);
}

0 comments on commit ca15618

Please sign in to comment.