Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion apps/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,28 @@
<input id="wrap-lines" type="checkbox" />
Wrap
</label>
<label>
<input id="lag-radar" type="checkbox" />
Lag Radar
</label>
</div>
</div>
<div id="wrapper" class="wrapper" />
<div id="wrapper" class="wrapper"></div>
</div>
<!-- lag radar -->
<div
id="radar"
style="
display: none;
position: fixed;
bottom: 4px;
right: 4px;
width: 100px;
height: 100px;
z-index: 100;
pointer-events: none;
"
></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
1 change: 1 addition & 0 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@pierre/diffs": "workspace:*",
"@pierre/icons": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"shiki": "catalog:"
Expand Down
246 changes: 242 additions & 4 deletions apps/demo/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ import {
VirtualizedFileDiff,
Virtualizer,
} from '@pierre/diffs';
import { Editor } from '@pierre/diffs/editor';
import type { WorkerPoolManager } from '@pierre/diffs/worker';
import {
IconCiFailedOctagonFill,
IconCiWarningFill,
IconInfoFill,
} from '@pierre/icons';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';

import {
cleanupCodeView,
Expand Down Expand Up @@ -58,6 +66,33 @@ const CRAZY_FILE = false;
const LARGE_CONFLICT_FILE = false;
const CODE_VIEW_OLD_NEW_FILE = true;

// Pre-render the @pierre/icons SVG markup once so it can be embedded into the
// `message.html` strings the editor injects for markers. The icons default to
// `fill: currentcolor`, so each one inherits the surrounding text color.
const MARKER_INFO_ICON = renderToStaticMarkup(
createElement(IconInfoFill, { size: 16 })
);
const MARKER_WARNING_ICON = renderToStaticMarkup(
createElement(IconCiWarningFill, { size: 16 })
);
const MARKER_ERROR_ICON = renderToStaticMarkup(
createElement(IconCiFailedOctagonFill, { size: 16 })
);

// Builds the HTML for a single marker overlay: a severity-colored leading icon
// next to a message, with an additional description indented below the message
// (aligned with the message text, not the icon).
function markerMessage(opts: {
color: string;
icon: string;
message: string;
description: string;
}): string {
const iconCol = `<span style="color:${opts.color};display:inline-flex;flex:none;margin-top:2px">${opts.icon}</span>`;
const textCol = `<div style="display:flex;flex-direction:column;gap:2px">${opts.message}<div style="color:gray">${opts.description}</div></div>`;
return `<div style="display:flex;align-items:flex-start;gap:8px">${iconCol}${textCol}</div>`;
}

const FileStreamCodeConfigs: FileStreamCodeConfigsItem[] = [
{
content: tsContent,
Expand Down Expand Up @@ -108,8 +143,18 @@ function cleanupInstances(container: HTMLElement) {
cleanupCodeView(container);
container.textContent = '';
delete container.dataset.diff;
editShortcutCallback = undefined;
}

let editShortcutCallback: (() => boolean | void) | undefined;
document.addEventListener('keydown', (event) => {
if (event.key === 'e') {
if (editShortcutCallback?.() === false) {
event.preventDefault();
}
}
});

let loadingPatch: Promise<string> | undefined;
async function loadPatchContent() {
loadingPatch =
Expand Down Expand Up @@ -239,18 +284,23 @@ function renderDiff(parsedPatches: ParsedPatch[], manager?: WorkerPoolManager) {
const patchAnnotations = FAKE_DIFF_LINE_ANNOTATIONS[patchIndex] ?? [];
let hunkIndex = 0;
for (const fileDiff of parsedPatch.files) {
const editor = new Editor<LineCommentMetadata>({
__debug: true,
});
const fileAnnotations = patchAnnotations[hunkIndex];
let instance:
| FileDiff<LineCommentMetadata>
| VirtualizedFileDiff<LineCommentMetadata>;
let isEditing = false;
const options: FileDiffOptions<LineCommentMetadata> = {
theme: DEMO_THEME,
themeType,
diffStyle: unified ? 'unified' : 'split',
overflow: wrap ? 'wrap' : 'scroll',
renderAnnotation: renderDiffAnnotation,
renderHeaderMetadata() {
return createCollapsedToggle(
const collapseToggle = createToggle(
'Collapse',
instance?.options.collapsed ?? false,
(checked) => {
instance?.setOptions({
Expand All @@ -262,6 +312,45 @@ function renderDiff(parsedPatches: ParsedPatch[], manager?: WorkerPoolManager) {
}
}
);
const editableToggle = createToggle(
'Editable',
isEditing,
(checked) => {
isEditing = checked;
if (isEditing) {
editor.edit(instance);
editor.setSelections([
{
start: {
line: 3,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
end: {
line: 3,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
direction: 'none',
},
]);
} else {
editor.cleanUp();
}
}
);
editShortcutCallback = (): boolean | void => {
if (!isEditing) {
editableToggle.querySelector('input')?.click();
return false;
}
};
const div = document.createElement('div');
div.style.display = 'flex';
div.style.gap = '8px';
div.append(collapseToggle);
if (!fileDiff.isPartial) {
div.append(editableToggle);
}
return div;
},
lineHoverHighlight: 'both',
expansionLineCount: 10,
Expand Down Expand Up @@ -745,18 +834,43 @@ if (renderFileButton != null) {

virtualizer?.setup(globalThis.document);
const wrap = getWrapped();
const editor = new Editor<LineCommentMetadata>({
enabledSelectionAction: true,
renderSelectionAction: (ctx) => {
const div = document.createElement('div');
const button = document.createElement('button');
button.innerText = `Comment the selection`;
button.addEventListener('click', () => {
const lines = ctx.getSelectionText().split('\n');
const comment = lines
.map((line) => (line.startsWith('//') ? line : `// ${line}`))
.join('\n');
ctx.replaceSelectionText(comment);
ctx.close();
});
div.style.marginBlock = '4px';
div.appendChild(button);
return div;
},
onChange: (file, lineAnnotations) => {
console.log('change', file, lineAnnotations);
},
__debug: true,
});
const fileContainer = document.createElement(DIFFS_TAG_NAME);
wrapper.appendChild(fileContainer);
let instance:
| File<LineCommentMetadata>
| VirtualizedFile<LineCommentMetadata>;
let isEditing = false;
const options: FileOptions<LineCommentMetadata> = {
overflow: wrap ? 'wrap' : 'scroll',
theme: DEMO_THEME,
themeType: getThemeType(),
renderAnnotation,
renderHeaderMetadata() {
return createCollapsedToggle(
const collapsedToggle = createToggle(
'Collapse',
instance?.options.collapsed ?? false,
(checked) => {
instance?.setOptions({
Expand All @@ -768,6 +882,103 @@ if (renderFileButton != null) {
}
}
);
const editableToggle = createToggle(
'Editable',
isEditing,
(checked) => {
isEditing = checked;
if (isEditing) {
editor.edit(instance);
editor.setSelections([
{
start: {
line: 0,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
end: {
line: 0,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
direction: 'none',
},
]);
requestAnimationFrame(() => {
editor.setMarkers([
{
start: {
line: 1,
character: 2,
},
end: {
line: 1,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
severity: 'info',
message: {
html: markerMessage({
color: 'var(--diffs-editor-info-fg, #3794ff)',
icon: MARKER_INFO_ICON,
message: '<code>CodeOptionsMultipleThemes</code>',
description: 'Code options of multiple themes.',
}),
},
},
{
start: {
line: 2,
character: 2,
},
end: {
line: 2,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
severity: 'warning',
message: {
html: markerMessage({
color: 'var(--diffs-editor-warning-fg, #cca700)',
icon: MARKER_WARNING_ICON,
message: '<code>CodeToHastOptions</code>',
description: 'Code to Hast Options is deprecated.',
}),
},
},
{
start: {
line: 3,
character: 2,
},
end: {
line: 3,
character: 1000, // will be normalized to the end of the line(< 1000 chars)
},
severity: 'error',
message: {
html: markerMessage({
color: 'var(--diffs-editor-error-fg, red)',
icon: MARKER_ERROR_ICON,
message: '<code>DecorationItem</code>',
description: 'Type not defined.',
}),
},
},
]);
});
} else {
editor.cleanUp();
}
}
);
editShortcutCallback = (): boolean | void => {
if (!isEditing) {
editableToggle.querySelector('input')?.click();
return false;
}
};
const div = document.createElement('div');
div.style.display = 'flex';
div.style.gap = '8px';
div.append(collapsedToggle, editableToggle);
return div;
},

// Line selection stuff
Expand Down Expand Up @@ -955,7 +1166,34 @@ cleanButton?.addEventListener('click', () => {
cleanupInstances(container);
});

function createCollapsedToggle(
const lagRadarCheckbox = document.getElementById('lag-radar');
const radar = document.getElementById('radar');
if (lagRadarCheckbox != null && radar != null) {
const { default: lagRadar } =
// @ts-expect-error dynamic import
await import('https://mobz.github.io/lag-radar/lag-radar.js');
let dispose: (() => void) | undefined;
lagRadarCheckbox.addEventListener('change', () => {
if (
lagRadarCheckbox instanceof HTMLInputElement &&
lagRadarCheckbox.checked
) {
dispose = lagRadar({
parent: radar,
size: 100,
frames: 60,
});
radar.style.display = 'block';
} else {
dispose?.();
dispose = undefined;
radar.style.display = 'none';
}
});
}

function createToggle(
labelText: string,
checked: boolean,
onChange: (checked: boolean) => void
): HTMLElement {
Expand All @@ -968,7 +1206,7 @@ function createCollapsedToggle(
});
label.dataset.collapser = '';
label.appendChild(input);
label.append(' Collapse');
label.appendChild(document.createTextNode(` ${labelText}`));
return label;
}

Expand Down
8 changes: 8 additions & 0 deletions apps/demo/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,11 @@ diffs-container {
align-items: center;
gap: 4px;
}

[data-selection-action-slot] button {
border-color: rgba(128, 128, 128, 0.4);

&:hover {
border-color: #646cff;
}
}
1 change: 1 addition & 0 deletions apps/docs/app/(diffs)/_components/WorkerPoolContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const HighlighterOptions: WorkerInitializationRenderOptions = {
'zig',
],
preferredHighlighter: 'shiki-wasm',
useTokenTransformer: true,
};

interface WorkerPoolProps {
Expand Down
Loading
Loading