Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1bff645
Introduce related field effects and states
chengluyu Oct 24, 2025
d863eef
Merge branch 'origin/main'
chengluyu Oct 24, 2025
721b59f
Initialize block metadata using runtime
chengluyu Oct 24, 2025
89fda22
Implement automatic block updates when the user changes
chengluyu Oct 28, 2025
2c954e9
Merge branch 'origin/main'
chengluyu Oct 28, 2025
7cc57c2
Fix failed tests due to changes in `onChanges`
chengluyu Oct 29, 2025
ddb5cd5
Add the transaction viewer to the test page
chengluyu Oct 29, 2025
f3a2202
Implement the playground using React
chengluyu Nov 14, 2025
bd2e1fd
Configure TypeScript in the repo
chengluyu Nov 16, 2025
1bd634c
WIP
chengluyu Nov 19, 2025
d23ee70
Add block section to the sidebar
chengluyu Nov 19, 2025
6f25d05
Fix not displaying debug decorations
chengluyu Nov 19, 2025
7b59df5
Merge branch 'origin/main'
chengluyu Dec 8, 2025
aa66f2c
Add an example for sorting algorithms
chengluyu Dec 8, 2025
c27cad2
Improve tracking blocks
chengluyu Dec 12, 2025
342dbc3
Reimplement the block shifting computation
chengluyu Dec 13, 2025
477713f
Improve the UI of Recho Playground
chengluyu Dec 13, 2025
949c269
Fix duplicated blocks
chengluyu Dec 13, 2025
a7337b3
Minor refactor
chengluyu Dec 14, 2025
a2f03eb
Configure ESLint for TypeScript
chengluyu Dec 14, 2025
c63d551
Merge branch 'origin/main'
chengluyu Dec 14, 2025
2ced079
Fix type errors and code style
chengluyu Dec 14, 2025
da66991
Improve the transaction viewer
chengluyu Dec 14, 2025
858b6de
Provide types for Observable's runtime
chengluyu Dec 14, 2025
e1461c0
Extract output related logic
chengluyu Dec 14, 2025
12635fc
Support fast switching examples in the playground
chengluyu Dec 14, 2025
9b6c871
Optimize the playground UI
chengluyu Dec 17, 2025
c8cc314
Update `@codemirror/view`
chengluyu Dec 18, 2025
d40de59
Assign a random `id` to each block
chengluyu Dec 18, 2025
6f4db16
Refactor the data flow of the playground
chengluyu Dec 17, 2025
66047d6
Fix code style
chengluyu Dec 18, 2025
239753d
Fix type errors
chengluyu Dec 18, 2025
acd4f0b
Display indent and dedent transactions
chengluyu Dec 20, 2025
00b61e1
Gather block tracking related source files
chengluyu Dec 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 274 additions & 0 deletions app/examples/sorting-algorithms.recho.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
/**
* @title Sorting Algorithms
* @author Luyu Cheng
* @created 2025-12-09
* @pull_request 86
* @github chengluyu
* @label Algorithm
*/

/**
* ============================================================================
* = Sorting Algorithms =
* ============================================================================
*/

//➜ ▁▂▃
//➜ ▂▄▅▅▇▇███
//➜ ▁█████████
//➜ ▃▅▆▇▇██████████
//➜ ▁▃▇████████████████
//➜ ▆▇▇███████████████████
//➜ ▁▄███████████████████████
//➜ ▂▂▅▆█████████████████████████
//➜ ▁▅█████████████████████████████
//➜ ▃▆▇▇███████████████████████████████
//➜ ▁▅▆▆▇███████████████████████████████████
//➜ ▁▇████████████████████████████████████████
//➜ ▄▇▇██████████████████████████████████████████
//➜
//➜ ──────────────────────────────────────────────
{ echo.set("compact", true); echo(renderNumbers(numbers.data, numbers.highlight)); }

const maximum = recho.number(100);
const length = recho.number(46);
const swapsPerSecond = recho.number(43);

recho.button("Insertion Sort", () => {
const numbers = randomArray(maximum, length);
setNumbers({data: numbers, highlight: null});
play(insertionSortSwaps(numbers));
});

recho.button("Selection Sort", () => {
const numbers = randomArray(maximum, length);
setNumbers({data: numbers, highlight: null});
play(selectionSortSwaps(numbers));
});

recho.button("Merge Sort", () => {
const numbers = randomArray(maximum, length);
setNumbers({data: numbers, highlight: null});
play(mergeSortSwaps(numbers));
});

recho.button("Quick Sort", () => {
const numbers = randomArray(maximum, length);
setNumbers({data: numbers, highlight: null});
play(quickSortSwaps(numbers));
});

function insertionSortSwaps(arr) {
const swaps = [];
const copy = [...arr]; // work with a copy
for (let i = 1; i < copy.length; i++) {
let j = i;
// Move element at position i to its correct position
while (j > 0 && copy[j] < copy[j - 1]) {
swaps.push([j - 1, j]);
// Swap elements
[copy[j - 1], copy[j]] = [copy[j], copy[j - 1]];
j--;
}
}
return swaps;
}

function selectionSortSwaps(arr) {
const swaps = [];
const copy = [...arr];

for (let i = 0; i < copy.length - 1; i++) {
let minIndex = i;
// Find minimum element in remaining unsorted portion
for (let j = i + 1; j < copy.length; j++) {
swaps.push([j])
if (copy[j] < copy[minIndex]) {
minIndex = j;
}
}
// Swap if minimum is not at current position
if (minIndex !== i) {
swaps.push([i, minIndex]);
[copy[i], copy[minIndex]] = [copy[minIndex], copy[i]];
}
}

return swaps;
}

function mergeSortSwaps(arr) {
const swaps = [];
const copy = [...arr];

function merge(left, mid, right) {
const leftArr = copy.slice(left, mid + 1);
const rightArr = copy.slice(mid + 1, right + 1);

let i = 0, j = 0, k = left;

while (i < leftArr.length && j < rightArr.length) {
if (leftArr[i] <= rightArr[j]) {
copy[k] = leftArr[i];
i++;
} else {
// Element from right half needs to move before elements from left half
// This represents multiple swaps in the original array
copy[k] = rightArr[j];
// Record swaps: right element moves past remaining left elements
const sourcePos = mid + 1 + j;
for (let pos = sourcePos; pos > k; pos--) {
swaps.push([pos - 1, pos]);
}
j++;
}
k++;
}

// Copy remaining elements (no swaps needed as they're already in place)
while (i < leftArr.length) {
copy[k] = leftArr[i];
i++;
k++;
}
while (j < rightArr.length) {
copy[k] = rightArr[j];
j++;
k++;
}
}

function mergeSort(left, right) {
if (left < right) {
const mid = Math.floor((left + right) / 2);
mergeSort(left, mid);
mergeSort(mid + 1, right);
merge(left, mid, right);
}
}

mergeSort(0, copy.length - 1);
return swaps;
}

function quickSortSwaps(arr) {
const swaps = [];
const copy = [...arr];

function medianOfThree(left, right) {
const mid = Math.floor((left + right) / 2);
const a = copy[left], b = copy[mid], c = copy[right];

// Find median and return its index
if ((a <= b && b <= c) || (c <= b && b <= a)) return mid;
if ((b <= a && a <= c) || (c <= a && a <= b)) return left;
return right;
}

function partition(left, right) {
// Choose pivot using median-of-three
const pivotIndex = medianOfThree(left, right);

// Move pivot to end
if (pivotIndex !== right) {
swaps.push([pivotIndex, right]);
[copy[pivotIndex], copy[right]] = [copy[right], copy[pivotIndex]];
}

const pivot = copy[right];
let i = left - 1;

for (let j = left; j < right; j++) {
if (copy[j] <= pivot) {
i++;
if (i !== j) {
swaps.push([i, j]);
[copy[i], copy[j]] = [copy[j], copy[i]];
}
}
}

// Move pivot to its final position
i++;
if (i !== right) {
swaps.push([i, right]);
[copy[i], copy[right]] = [copy[right], copy[i]];
}

return i;
}

function quickSort(left, right) {
if (left < right) {
const pivotIndex = partition(left, right);
quickSort(left, pivotIndex - 1);
quickSort(pivotIndex + 1, right);
}
}

quickSort(0, copy.length - 1);
return swaps;
}

function play(swaps) {
if (playing !== null) {
clearInterval(playing);
}
let current = 0;
const id = setInterval(takeStep, Math.floor(1000 / swapsPerSecond));
setPlaying(id);
function takeStep() {
if (current >= swaps.length) {
clearInterval(playing);
setPlaying(null);
return;
}
const swap = swaps[current];
if (swap.length === 2) {
const [left, right] = swap;
setNumbers(({ data }) => {
const cloned = structuredClone(data);
const temp = cloned[left];
cloned[left] = cloned[right];
cloned[right] = temp;
return { data: cloned, highlight: null };
});
} else if (swap.length === 1) {
const [index] = swap;
setNumbers(({ data }) => {
return { data, highlight: index};
});
}
current++;
}
}

function randomArray(maximum, length) {
const buffer = [];
const gen = d3.randomInt(maximum);
for (let i = 0; i < length; i++) {
buffer.push(gen());
}
return buffer;
}

function renderNumbers(numbers, highlight) {
const min = d3.min(numbers), max = d3.max(numbers);
const segmentCount = (max >>> 3) + ((max & 7) === 0 ? 0 : 1);
const buffer = d3.transpose(numbers.map((n, i) => {
const head = (n & 7) === 0 ? "" : blocks[n & 7];
const body = fullBlock.repeat(n >>> 3);
const padding = " ".repeat(segmentCount - (head.length + body.length));
const ending = i === highlight ? "╻┸" : " ─";
return padding + head + body + ending;
}));
return buffer.map(xs => xs.join("")).join("\n");
}

const [playing, setPlaying] = recho.state(null);
const [numbers, setNumbers] = recho.state({ data: [], highlight: null })

const blocks = Array.from(" ▁▂▃▄▅▆▇");
const fullBlock = "█";

const d3 = recho.require("d3");
54 changes: 54 additions & 0 deletions editor/blocks/BlockMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {Transaction} from "@codemirror/state";

export type Range = {from: number; to: number};

export class BlockMetadata {
/**
* Create a new `BlockMetadata` instance.
* @param name a descriptive name of this block
* @param output the range of the output region
* @param source the range of the source region
* @param attributes any user-customized attributes of this block
* @param error whether this block has an error
*/
public constructor(
public readonly id: string,
public readonly name: string,
public readonly output: Range | null,
public readonly source: Range,
public attributes: Record<string, unknown> = {},
public error: boolean = false,
) {}

/**
* Get the start position (inclusive) of this block.
*/
get from() {
return this.output?.from ?? this.source.from;
}

/**
* Get the end position (exclusive) of this block.
*/
get to() {
return this.source.to;
}

public map(tr: Transaction): BlockMetadata {
// If no changes were made to the document, return the current instance.
if (!tr.docChanged) return this;

// Otherwise, map the output and source ranges.
const output = this.output
? {
from: tr.changes.mapPos(this.output.from, -1),
to: tr.changes.mapPos(this.output.to, 1),
}
: null;
const source = {
from: tr.changes.mapPos(this.source.from, 1),
to: tr.changes.mapPos(this.source.to, 1),
};
return new BlockMetadata(this.id, this.name, output, source, this.attributes, this.error);
}
}
49 changes: 49 additions & 0 deletions editor/blocks/compact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {Decoration, ViewPlugin, ViewUpdate, EditorView, type DecorationSet} from "@codemirror/view";
import {blockMetadataField} from "./state.ts";
import {BlockMetadata} from "./BlockMetadata.ts";
import {RangeSetBuilder} from "@codemirror/state";

const compactLineDecoration = Decoration.line({attributes: {class: "cm-output-line cm-compact-line"}});

export const compactDecoration = ViewPlugin.fromClass(
class {
#decorations: DecorationSet;

get decorations() {
return this.#decorations;
}

/** @param {EditorView} view */
constructor(view: EditorView) {
const blockMetadata = view.state.field(blockMetadataField);
this.#decorations = this.createDecorations(blockMetadata, view.state);
}

/** @param {ViewUpdate} update */
update(update: ViewUpdate) {
const blockMetadata = update.state.field(blockMetadataField);
// A possible optimization would be to only update the changed lines.
this.#decorations = this.createDecorations(blockMetadata, update.state);
}

createDecorations(blockMetadata: BlockMetadata[], state: EditorView["state"]) {
const builder = new RangeSetBuilder<Decoration>();
// Add block attribute decorations
for (const {output, attributes} of blockMetadata) {
if (output === null) continue;
// Apply decorations to each line in the block range
const startLine = state.doc.lineAt(output.from);
const endLine = state.doc.lineAt(output.to);
const endLineNumber = endLine.from < output.to ? endLine.number + 1 : endLine.number;
if (attributes.compact === true) {
for (let lineNum = startLine.number; lineNum < endLineNumber; lineNum++) {
const line = state.doc.line(lineNum);
builder.add(line.from, line.from, compactLineDecoration);
}
}
}
return builder.finish();
}
},
{decorations: (v) => v.decorations},
);
Loading