feat(keymap-upgrader): Highlight changes

Updated the keymap upgrader to highlight which lines it changed as well
as indicate when nothing needed to be upgraded.

Also adjusted the line highlight colors to be more readable in both
light and dark color schemes.
This commit is contained in:
Joel Spadin 2024-01-21 19:31:55 -06:00
parent 84e056793b
commit 37fcf190e6
6 changed files with 109 additions and 45 deletions

View file

@ -7,7 +7,11 @@
import React from "react";
import { useAsync } from "react-async";
import { initParser, upgradeKeymap } from "@site/src/keymap-upgrade";
import {
initParser,
upgradeKeymap,
rangesToLineNumbers,
} from "@site/src/keymap-upgrade";
import CodeBlock from "@theme/CodeBlock";
import styles from "./styles.module.css";
@ -28,7 +32,15 @@ export default function KeymapUpgrader() {
function Editor() {
const [keymap, setKeymap] = React.useState("");
const upgraded = upgradeKeymap(keymap);
const { text: upgraded, changedRanges } = upgradeKeymap(keymap);
const highlights = rangesToLineNumbers(upgraded, changedRanges);
let title = "Upgraded Keymap";
if (keymap && upgraded === keymap) {
title += " (No Changes)";
}
return (
<div>
@ -40,7 +52,7 @@ function Editor() {
onChange={(e) => setKeymap(e.target.value)}
></textarea>
<div className={styles.result}>
<CodeBlock language="dts" metastring={'title="Upgraded Keymap"'}>
<CodeBlock language="dts" metastring={`${highlights} title="${title}"`}>
{upgraded}
</CodeBlock>
</div>

View file

@ -15,10 +15,15 @@
--ifm-color-primary-lighter: #0280e3;
--ifm-color-primary-lightest: #0690fc;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgb(0 0 0 / 8%);
}
[data-theme="dark"] {
--docusaurus-highlighted-code-line-bg: rgb(255 255 255 / 8%);
}
.docusaurus-highlight-code-line {
background-color: rgb(72, 77, 91);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);

View file

@ -1,5 +1,5 @@
import { createParser } from "./parser";
import { applyEdits } from "./textedit";
import { applyEdits, Range } from "./textedit";
import { upgradeBehaviors } from "./behaviors";
import { upgradeHeaders } from "./headers";
@ -23,3 +23,37 @@ export function upgradeKeymap(text: string) {
return applyEdits(text, edits);
}
export function rangesToLineNumbers(
text: string,
changedRanges: Range[]
): string {
const lineBreaks = getLineBreakPositions(text);
const changedLines = changedRanges.map((range) => {
const startLine = positionToLineNumber(range.startIndex, lineBreaks);
const endLine = positionToLineNumber(range.endIndex, lineBreaks);
return startLine === endLine ? `${startLine}` : `${startLine}-${endLine}`;
});
return `{${changedLines.join(",")}}`;
}
function getLineBreakPositions(text: string) {
const positions: number[] = [];
let index = 0;
while ((index = text.indexOf("\n", index)) >= 0) {
positions.push(index);
index++;
}
return positions;
}
function positionToLineNumber(position: number, lineBreaks: number[]) {
const line = lineBreaks.findIndex((lineBreak) => position <= lineBreak);
return line < 0 ? 0 : line + 1;
}

View file

@ -101,7 +101,7 @@ export function upgradeKeycodes(tree: Tree) {
function keycodeReplaceHandler(node: SyntaxNode, replacement: string | null) {
if (replacement) {
return [new TextEdit(node, replacement)];
return [TextEdit.fromNode(node, replacement)];
}
const nodes = findBehaviorNodes(node);
@ -110,7 +110,7 @@ function keycodeReplaceHandler(node: SyntaxNode, replacement: string | null) {
console.warn(
`Found deprecated code "${node.text}" but it is not a parameter to a behavior`
);
return [new TextEdit(node, `/* "${node.text}" no longer exists */`)];
return [TextEdit.fromNode(node, `/* "${node.text}" no longer exists */`)];
}
const oldText = nodes.map((n) => n.text).join(" ");

View file

@ -24,9 +24,9 @@ function removeLabels(tree: Tree) {
const node = findCapture("prop", captures);
if (name?.text === "label" && node) {
if (isLayerLabel(node)) {
edits.push(new TextEdit(name, "display-name"));
edits.push(TextEdit.fromNode(name, "display-name"));
} else {
edits.push(new TextEdit(node, ""));
edits.push(TextEdit.fromNode(node, ""));
}
}
}

View file

@ -1,40 +1,23 @@
import type { SyntaxNode } from "web-tree-sitter";
export class TextEdit {
startIndex: number;
endIndex: number;
export class Range {
constructor(public startIndex: number, public endIndex: number) {}
}
export class TextEdit extends Range {
newText: string;
/**
* Creates a text edit to replace a range with new text.
*/
constructor(startIndex: number, endIndex: number, newText: string);
/**
* Creates a text edit to replace a node with new text.
*/
constructor(node: SyntaxNode, newText: string);
constructor(
startIndex: number | SyntaxNode,
endIndex: number | string,
newText?: string
) {
if (typeof startIndex !== "number") {
if (typeof endIndex === "string") {
const node = startIndex;
newText = endIndex;
startIndex = node.startIndex;
endIndex = node.endIndex;
} else {
throw new TypeError();
}
} else if (typeof endIndex !== "number" || typeof newText !== "string") {
throw new TypeError();
}
this.startIndex = startIndex;
this.endIndex = endIndex;
constructor(startIndex: number, endIndex: number, newText: string) {
super(startIndex, endIndex);
this.newText = newText;
}
static fromNode(node: SyntaxNode | Range, newText: string) {
return new TextEdit(node.startIndex, node.endIndex, newText);
}
}
export type MatchFunc = (node: SyntaxNode, text: string) => boolean;
@ -67,7 +50,7 @@ export function getUpgradeEdits(
isMatch?: MatchFunc
) {
const defaultReplace: ReplaceFunc = (node, replacement) => [
new TextEdit(node, replacement ?? ""),
TextEdit.fromNode(node, replacement ?? ""),
];
const defaultMatch: MatchFunc = (node, text) => node.text === text;
@ -89,16 +72,26 @@ function sortEdits(edits: TextEdit[]) {
return edits.sort((a, b) => a.startIndex - b.startIndex);
}
export interface EditResult {
text: string;
changedRanges: Range[];
}
interface TextChunk {
text: string;
changed?: boolean;
}
/**
* Returns a string with text replacements applied.
* Returns a string with text replacements applied and a list of ranges within
* that string that were modified.
*/
export function applyEdits(text: string, edits: TextEdit[]) {
// If we are removing text and it's the only thing on a line, remove the whole line.
edits = edits.map((e) => (e.newText ? e : expandEditToLine(text, e)));
edits = sortEdits(edits);
const chunks: string[] = [];
const chunks: TextChunk[] = [];
let currentIndex = 0;
for (let edit of edits) {
@ -107,14 +100,34 @@ export function applyEdits(text: string, edits: TextEdit[]) {
continue;
}
chunks.push(text.substring(currentIndex, edit.startIndex));
chunks.push(edit.newText);
chunks.push({ text: text.substring(currentIndex, edit.startIndex) });
chunks.push({ text: edit.newText, changed: true });
currentIndex = edit.endIndex;
}
chunks.push(text.substring(currentIndex));
chunks.push({ text: text.substring(currentIndex) });
return chunks.join("");
// Join all of the text chunks while recording the ranges of any chunks that were changed.
return chunks.reduce<EditResult>(
(prev, current) => {
return {
text: prev.text + current.text,
changedRanges: reduceChangedRanges(prev, current),
};
},
{ text: "", changedRanges: [] }
);
}
function reduceChangedRanges(prev: EditResult, current: TextChunk): Range[] {
if (current.changed) {
return [
...prev.changedRanges,
new Range(prev.text.length, prev.text.length + current.text.length),
];
}
return prev.changedRanges;
}
/**