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 @@ -5,6 +5,7 @@
"type": "module",
"dependencies": {
"@pierre/diffs": "workspace:*",
"@pierre/icons": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"shiki": "catalog:"
Expand Down
252 changes: 237 additions & 15 deletions apps/demo/src/codeViewDemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import {
type CodeViewItem,
type CodeViewOptions,
type DiffLineAnnotation,
type DiffsEditableComponent,
type DiffsThemeNames,
type FileContents,
type FileDiffMetadata,
type LineAnnotation,
type ParsedPatch,
type SelectedLineRange,
type ThemesType,
} from '@pierre/diffs';
import { Editor } from '@pierre/diffs/editor';
import type { WorkerPoolManager } from '@pierre/diffs/worker';

import { FAKE_DIFF_LINE_ANNOTATIONS, type LineCommentMetadata } from './mocks/';
Expand Down Expand Up @@ -39,6 +43,28 @@ interface CodeViewDemoInstance {
options: CodeViewOptions<CodeViewCommentMetadata>;
}

type CodeViewEditableInstance =
DiffsEditableComponent<CodeViewCommentMetadata> & {
file?: FileContents;
fileDiff?: FileDiffMetadata;
};

interface CodeViewEditableContext {
item: CodeViewItem<CodeViewCommentMetadata>;
instance: CodeViewEditableInstance;
}

interface ActiveCodeViewEditor {
itemId: string;
viewer: CodeView<CodeViewCommentMetadata>;
items: CodeViewItem<CodeViewCommentMetadata>[];
instance: CodeViewEditableInstance;
editor: Editor<CodeViewCommentMetadata>;
dispose: () => void;
toggleInput: HTMLInputElement;
dirty: boolean;
}

type CodeViewDemoAnnotation =
| DiffLineAnnotation<CodeViewCommentMetadata>
| LineAnnotation<CodeViewCommentMetadata>;
Expand All @@ -63,8 +89,11 @@ interface RenderDemoCodeViewOptions {

const codeViewInstances: CodeViewDemoInstance[] = [];
let nextCodeViewCommentKey = 0;
let nextCodeViewEditCacheKey = 0;
let activeCodeViewEditor: ActiveCodeViewEditor | undefined;

export function cleanupCodeView(container: HTMLElement) {
deactivateCodeViewEditor({ publish: false });
for (const { instance } of codeViewInstances) {
instance.cleanUp();
}
Expand All @@ -76,30 +105,25 @@ export function cleanupCodeView(container: HTMLElement) {
export function renderDemoCodeView(
wrapper: HTMLElement,
parsedPatches: ParsedPatch[],
{
diffStyle,
overflow,
theme,
themeType,
workerManager,
}: RenderDemoCodeViewOptions
renderOptions: RenderDemoCodeViewOptions
) {
const { diffStyle, overflow, theme, themeType, workerManager } =
renderOptions;
setupCodeViewWrapper(wrapper);

const items = createCodeViewItems(parsedPatches);
let viewer: CodeView<CodeViewCommentMetadata>;
const options: CodeViewOptions<CodeViewCommentMetadata> = {
const codeViewOptions: CodeViewOptions<CodeViewCommentMetadata> = {
theme,
themeType,
diffStyle,
overflow,
renderAnnotation(annotation) {
return renderCodeViewAnnotation(annotation, viewer, items);
},
lineHoverHighlight: 'both',
renderHeaderMetadata(_file, context) {
return renderCodeViewEditorToggle(viewer, items, context);
},
expansionLineCount: 10,
enableLineSelection: true,
enableGutterUtility: true,
stickyHeaders: true,
layout: { paddingTop: 10, paddingBottom: 24, gap: 12 },
onGutterUtilityClick(range, context) {
Expand All @@ -111,35 +135,232 @@ export function renderDemoCodeView(
onSelectedLinesChange(selection) {
console.log('CodeView selected lines', selection);
},

// These settings are not compatible with editor... is there a way we can
// just ignore their values while editing? CodeView intentionally shares
// most options, so ideally we don't toggle the entire CodeView when
// editing a single file
lineHoverHighlight: 'disabled',
enableLineSelection: false,
enableGutterUtility: false,
Comment on lines +143 to +145

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer not to toggle this globally for CodeView just to support editing, is there a way we can like let these options be whatever, but then disable the functionality while editing?

// I assume once you merge the unified support, this won't be a requirement anymore
diffStyle: diffStyle === 'unified' ? 'split' : diffStyle,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obviously this wont be an issue with #818

useTokenTransformer: true,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this is necessary?

expandUnchanged: true,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we HAVE to force expandUnchanged true?

};

viewer = new CodeView(options, workerManager);
viewer = new CodeView(codeViewOptions, workerManager);
viewer.setup(wrapper);
viewer.setItems(items);
codeViewInstances.push({ instance: viewer, options });
codeViewInstances.push({ instance: viewer, options: codeViewOptions });
}

export function setCodeViewOverflow(overflow: CodeViewOverflow) {
deactivateCodeViewEditor();
for (const codeView of codeViewInstances) {
codeView.options = { ...codeView.options, overflow };
codeView.instance.setOptions(codeView.options);
}
}

export function setCodeViewDiffStyle(diffStyle: CodeViewDiffStyle) {
deactivateCodeViewEditor();
for (const codeView of codeViewInstances) {
codeView.options = { ...codeView.options, diffStyle };
codeView.options = {
...codeView.options,
// Ideally we don't do this...
diffStyle: diffStyle === 'unified' ? 'split' : diffStyle,
};
codeView.instance.setOptions(codeView.options);
}
}

export function setCodeViewThemeType(themeType: CodeViewThemeType) {
deactivateCodeViewEditor();
for (const codeView of codeViewInstances) {
codeView.options = { ...codeView.options, themeType };
codeView.instance.setOptions(codeView.options);
}
}

function renderCodeViewEditorToggle(
viewer: CodeView<CodeViewCommentMetadata>,
items: CodeViewItem<CodeViewCommentMetadata>[],
context: CodeViewEditableContext
) {
if (!canEditCodeViewItem(context.item)) {
return undefined;
}

const label = document.createElement('label');
label.dataset.collapser = '';
label.title = 'Toggle experimental editor mode for this CodeView item';
const input = document.createElement('input');
input.type = 'checkbox';
input.checked = activeCodeViewEditor?.itemId === context.item.id;
input.addEventListener('change', () => {
if (input.checked) {
activateCodeViewEditor(viewer, items, context, input);
} else if (activeCodeViewEditor?.itemId === context.item.id) {
deactivateCodeViewEditor();
}
});
label.addEventListener('click', (event) => {
event.stopPropagation();
});
label.append(input, 'Editable');
return label;
}

function canEditCodeViewItem(item: CodeViewItem<CodeViewCommentMetadata>) {
return item.type === 'file' || !item.fileDiff.isPartial;
}

function activateCodeViewEditor(
viewer: CodeView<CodeViewCommentMetadata>,
items: CodeViewItem<CodeViewCommentMetadata>[],
context: CodeViewEditableContext,
toggleInput: HTMLInputElement
) {
if (activeCodeViewEditor?.itemId === context.item.id) {
activeCodeViewEditor.toggleInput = toggleInput;
toggleInput.checked = true;
return;
}

deactivateCodeViewEditor();

let activeEditor: ActiveCodeViewEditor | undefined;
const editor = new Editor<CodeViewCommentMetadata>({
onAttach(editor) {
editor.setSelections([
{
start: { line: 0, character: 0 },
end: { line: 0, character: 0 },
direction: 'none',
},
]);
queueMicrotask(() => editor.focus({ preventScroll: true }));
},
onChange(file, lineAnnotations) {
queueMicrotask(() => {
if (activeEditor == null || activeCodeViewEditor !== activeEditor) {
return;
}
persistCodeViewEditorChange(activeEditor, file, lineAnnotations);
});
},
});

activeEditor = {
itemId: context.item.id,
viewer,
items,
instance: context.instance,
editor,
dispose: () => editor.cleanUp(),
toggleInput,
dirty: false,
};

try {
activeEditor.dispose = editor.edit(context.instance);
} catch (error) {
console.error('Failed to enable CodeView editor', error);
toggleInput.checked = false;
return;
}

activeCodeViewEditor = activeEditor;
toggleInput.checked = true;
Object.assign(window, { codeViewEditor: editor });
}

function deactivateCodeViewEditor({ publish = true } = {}) {
const activeEditor = activeCodeViewEditor;
if (activeEditor == null) {
return;
}

activeCodeViewEditor = undefined;
activeEditor.toggleInput.checked = false;
activeEditor.dispose();
if (publish && activeEditor.dirty) {
activeEditor.viewer.setItems([...activeEditor.items]);
}
}

function persistCodeViewEditorChange(
activeEditor: ActiveCodeViewEditor,
file: FileContents,
lineAnnotations: DiffLineAnnotation<CodeViewCommentMetadata>[] | undefined
) {
const item = activeEditor.items.find(
(candidate) => candidate.id === activeEditor.itemId
);
if (item == null) {
return;
}

if (item.type === 'file') {
item.file = cloneEditedFile(file);
if (lineAnnotations !== undefined) {
item.annotations =
lineAnnotations as LineAnnotation<CodeViewCommentMetadata>[];
}
} else {
item.fileDiff = cloneEditedFileDiff(
activeEditor.instance.fileDiff ?? item.fileDiff,
file
);
if (lineAnnotations !== undefined) {
item.annotations = lineAnnotations;
}
}

item.version = typeof item.version === 'number' ? item.version + 1 : 1;
activeEditor.dirty = true;
}

function cloneEditedFile(file: FileContents): FileContents {
const nextFile: FileContents = {
name: file.name,
contents: file.contents,
cacheKey: createCodeViewEditCacheKey(file.cacheKey ?? file.name),
};
if (file.lang !== undefined) {
nextFile.lang = file.lang;
}
if (file.header !== undefined) {
nextFile.header = file.header;
}
return nextFile;
}

function cloneEditedFileDiff(
fileDiff: FileDiffMetadata,
file: FileContents
): FileDiffMetadata {
return {
...fileDiff,
additionLines: splitCodeViewFileContents(file.contents),
deletionLines: fileDiff.deletionLines.slice(),
hunks: fileDiff.hunks.map((hunk) => ({
...hunk,
hunkContent: hunk.hunkContent.map((content) => ({ ...content })),
})),
cacheKey: createCodeViewEditCacheKey(fileDiff.cacheKey ?? fileDiff.name),
};
}

function splitCodeViewFileContents(contents: string): string[] {
return contents === '' ? [] : contents.split(/(?<=\n)/);
}

function createCodeViewEditCacheKey(baseKey: string): string {
return `${baseKey}:code-view-edit:${nextCodeViewEditCacheKey++}`;
}

function setupCodeViewWrapper(wrapper: HTMLElement) {
wrapper.dataset.codeView = '';
setRootCodeViewState(wrapper, true);
Expand Down Expand Up @@ -454,6 +675,7 @@ function publishCodeViewItemChange(
items: CodeViewItem<CodeViewCommentMetadata>[],
item: CodeViewDiffItem<CodeViewCommentMetadata>
) {
deactivateCodeViewEditor({ publish: false });
item.version = typeof item.version === 'number' ? item.version + 1 : 1;
viewer.setItems([...items]);
}
Expand Down
Loading