Skip to content
Closed
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
779 changes: 75 additions & 704 deletions apps/snow-leopard/components/document/text-editor.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ACTIVATE_SUGGESTION_CONTEXT,
DEACTIVATE_SUGGESTION_CONTEXT,
SET_SUGGESTION_LOADING_STATE
} from '@/lib/editor/selection-context-plugin';
} from '@/lib/editor/suggestion-plugin';

interface SuggestionOverlayContextType {
openSuggestionOverlay: (options: {
Expand Down
29 changes: 0 additions & 29 deletions apps/snow-leopard/lib/editor/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { textblockTypeInputRule } from 'prosemirror-inputrules';
import { Schema } from 'prosemirror-model';
import { schema } from 'prosemirror-schema-basic';
import { addListNodes } from 'prosemirror-schema-list';
import type { Transaction } from 'prosemirror-state';
import type { EditorView } from 'prosemirror-view';
import type { MutableRefObject } from 'react';
import OrderedMap from 'orderedmap';
import { DiffType } from './diff';

import { buildContentFromDocument } from './functions';

const diffMarkSpec = {
Expand Down Expand Up @@ -43,28 +39,3 @@ export function headingRule(level: number) {
() => ({ level }),
);
}

export const handleTransaction = ({
transaction,
editorRef,
onSaveContent,
}: {
transaction: Transaction;
editorRef: MutableRefObject<EditorView | null>;
onSaveContent: (updatedContent: string, debounce: boolean) => void;
}) => {
if (!editorRef || !editorRef.current) return;

const newState = editorRef.current.state.apply(transaction);
editorRef.current.updateState(newState);

if (transaction.docChanged && !transaction.getMeta('no-save')) {
const updatedContent = buildContentFromDocument(newState.doc);

if (transaction.getMeta('no-debounce')) {
onSaveContent(updatedContent, false);
} else {
onSaveContent(updatedContent, true);
}
}
};
119 changes: 119 additions & 0 deletions apps/snow-leopard/lib/editor/diff-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Plugin, PluginKey } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { buildContentFromDocument, buildDocumentFromContent } from './functions';
import { diffEditor } from './diff';
import { documentSchema } from './config';

export const diffPluginKey = new PluginKey('diff');

export function diffPlugin(documentId: string): Plugin {
let previewOriginalContentRef: string | null = null;
let previewActiveRef: boolean = false;
let lastPreviewContentRef: string | null = null;

return new Plugin({
key: diffPluginKey,
view(editorView: EditorView) {
const handlePreviewUpdate = (event: CustomEvent) => {
if (!event.detail) return;
const { documentId: previewDocId, newContent } = event.detail;
if (previewDocId !== documentId) return;

if (lastPreviewContentRef === newContent) return;

if (!previewActiveRef) {
previewOriginalContentRef = buildContentFromDocument(editorView.state.doc);
}

const oldContent = previewOriginalContentRef ?? buildContentFromDocument(editorView.state.doc);
if (newContent === oldContent) return;

const oldDocNode = buildDocumentFromContent(oldContent);
const newDocNode = buildDocumentFromContent(newContent);

const diffedDoc = diffEditor(documentSchema, oldDocNode.toJSON(), newDocNode.toJSON());

const tr = editorView.state.tr
.replaceWith(0, editorView.state.doc.content.size, diffedDoc.content)
.setMeta('external', true)
.setMeta('addToHistory', false);

requestAnimationFrame(() => editorView.dispatch(tr));

previewActiveRef = true;
lastPreviewContentRef = newContent;
};

const handleCancelPreview = (event: CustomEvent) => {
if (!event.detail) return;
const { documentId: cancelDocId } = event.detail;
if (cancelDocId !== documentId) return;
if (!previewActiveRef || previewOriginalContentRef === null) return;

const originalDocNode = buildDocumentFromContent(previewOriginalContentRef);
const tr = editorView.state.tr.replaceWith(0, editorView.state.doc.content.size, originalDocNode.content);
editorView.dispatch(tr);

previewActiveRef = false;
previewOriginalContentRef = null;
lastPreviewContentRef = null;
};

const handleApply = (event: CustomEvent) => {
if (!event.detail) return;
const { documentId: applyDocId } = event.detail;
if (applyDocId !== documentId) return;

const animationDuration = 500;

const finalizeApply = async () => {
const { state } = editorView;
let tr = state.tr;
const diffMarkType = state.schema.marks.diffMark;
const { DiffType } = await import('./diff');

const rangesToDelete: { from: number; to: number }[] = [];
state.doc.descendants((node, pos) => {
if (!node.isText) return;

const deletedMark = node.marks.find(
(mark) => mark.type === diffMarkType && mark.attrs.type === DiffType.Deleted
);
if (deletedMark) {
rangesToDelete.push({ from: pos, to: pos + node.nodeSize });
}
});

for (let i = rangesToDelete.length - 1; i >= 0; i--) {
const { from, to } = rangesToDelete[i];
tr.delete(from, to);
}

tr.removeMark(0, tr.doc.content.size, diffMarkType);
tr.setMeta('addToHistory', false);
editorView.dispatch(tr);
editorView.dom.classList.remove('applying-changes');

previewActiveRef = false;
previewOriginalContentRef = null;
lastPreviewContentRef = null;
};

editorView.dom.classList.add('applying-changes');
setTimeout(finalizeApply, animationDuration);
};

window.addEventListener('preview-document-update', handlePreviewUpdate as EventListener);
window.addEventListener('cancel-document-update', handleCancelPreview as EventListener);
window.addEventListener('apply-document-update', handleApply as EventListener);

return {
destroy() {
window.removeEventListener('preview-document-update', handlePreviewUpdate as EventListener);
window.removeEventListener('cancel-document-update', handleCancelPreview as EventListener);
window.removeEventListener('apply-document-update', handleApply as EventListener);
},
};
},
});
}
44 changes: 44 additions & 0 deletions apps/snow-leopard/lib/editor/editor-plugins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { exampleSetup } from 'prosemirror-example-setup';
import { inputRules } from 'prosemirror-inputrules';
import { Plugin } from 'prosemirror-state';

import { documentSchema, headingRule } from './config';
import { creationStreamingPlugin } from './creation-streaming-plugin';
import { placeholderPlugin } from './placeholder-plugin';
import { inlineSuggestionPlugin } from './inline-suggestion-plugin';
import { selectionContextPlugin } from './suggestion-plugin';
import { synonymsPlugin } from './synonym-plugin';
import { diffPlugin } from './diff-plugin';
import { formatPlugin } from './format-plugin';
import { savePlugin } from './save-plugin';

export interface EditorPluginOptions {
documentId: string;
initialLastSaved: Date | null;
placeholder?: string;
performSave: (content: string) => Promise<any>;
requestInlineSuggestion: (state: any) => void;
setActiveFormats: (formats: any) => void;
}
Comment on lines +15 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace 'any' types with more specific types.

Using any reduces type safety. Consider defining proper types for these parameters.

+import type { EditorState } from 'prosemirror-state';
+
+interface SaveResult {
+  // Define the expected save result structure
+  success: boolean;
+  error?: string;
+}
+
+interface ActiveFormats {
+  // Define the format structure based on your format plugin
+  bold?: boolean;
+  italic?: boolean;
+  // ... other formats
+}
+
 export interface EditorPluginOptions {
   documentId: string;
   initialLastSaved: Date | null;
   placeholder?: string;
-  performSave: (content: string) => Promise<any>;
-  requestInlineSuggestion: (state: any) => void;
-  setActiveFormats: (formats: any) => void;
+  performSave: (content: string) => Promise<SaveResult>;
+  requestInlineSuggestion: (state: EditorState) => void;
+  setActiveFormats: (formats: ActiveFormats) => void;
 }

Would you like me to help define the proper types based on your plugin implementations?

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export interface EditorPluginOptions {
documentId: string;
initialLastSaved: Date | null;
placeholder?: string;
performSave: (content: string) => Promise<any>;
requestInlineSuggestion: (state: any) => void;
setActiveFormats: (formats: any) => void;
}
import type { EditorState } from 'prosemirror-state';
interface SaveResult {
// Define the expected save result structure
success: boolean;
error?: string;
}
interface ActiveFormats {
// Define the format structure based on your format plugin
bold?: boolean;
italic?: boolean;
// ... other formats
}
export interface EditorPluginOptions {
documentId: string;
initialLastSaved: Date | null;
placeholder?: string;
performSave: (content: string) => Promise<SaveResult>;
requestInlineSuggestion: (state: EditorState) => void;
setActiveFormats: (formats: ActiveFormats) => void;
}
🤖 Prompt for AI Agents
In apps/snow-leopard/lib/editor/editor-plugins.ts around lines 15 to 22, the
EditorPluginOptions interface uses 'any' for the types of the parameters in
requestInlineSuggestion and setActiveFormats, which reduces type safety. To fix
this, analyze the actual data structures or types used in your plugin
implementations for these parameters and replace 'any' with those specific types
or interfaces. This will improve type safety and code clarity.


export function createEditorPlugins(opts: EditorPluginOptions): Plugin[] {
return [
creationStreamingPlugin(opts.documentId),
placeholderPlugin(opts.placeholder ?? (opts.documentId === 'init' ? 'Start typing' : 'Start typing...')),
...exampleSetup({ schema: documentSchema, menuBar: false }),
inputRules({
rules: [1, 2, 3, 4, 5, 6].map((level) => headingRule(level)),
}),
inlineSuggestionPlugin({ requestSuggestion: opts.requestInlineSuggestion }),
selectionContextPlugin(opts.documentId),
synonymsPlugin(),
diffPlugin(opts.documentId),
formatPlugin(opts.setActiveFormats),
savePlugin({
saveFunction: opts.performSave,
initialLastSaved: opts.initialLastSaved,
debounceMs: 200,
documentId: opts.documentId,
}),
];
}
88 changes: 88 additions & 0 deletions apps/snow-leopard/lib/editor/format-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Plugin, PluginKey, EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { documentSchema } from './config';

const { nodes, marks } = documentSchema;

export interface FormatState {
h1: boolean;
h2: boolean;
p: boolean;
bulletList: boolean;
orderedList: boolean;
bold: boolean;
italic: boolean;
}

export const formatPluginKey = new PluginKey<FormatState>('format');

function isMarkActive(state: EditorState, type: any): boolean {
const { from, $from, to, empty } = state.selection;
if (empty) {
return !!type.isInSet(state.storedMarks || $from.marks());
} else {
return state.doc.rangeHasMark(from, to, type);
}
}

function isBlockActive(state: EditorState, type: any, attrs: Record<string, any> = {}): boolean {
const { $from } = state.selection;
const node = $from.node($from.depth);
return node?.hasMarkup(type, attrs);
}

function isListActive(state: EditorState, type: any): boolean {
const { $from } = state.selection;
for (let d = $from.depth; d > 0; d--) {
if ($from.node(d).type === type) {
return true;
}
}
return false;
}

function getActiveFormats(state: EditorState): FormatState {
return {
h1: isBlockActive(state, nodes.heading, { level: 1 }),
h2: isBlockActive(state, nodes.heading, { level: 2 }),
p: isBlockActive(state, nodes.paragraph),
bulletList: isListActive(state, nodes.bullet_list),
orderedList: isListActive(state, nodes.ordered_list),
bold: isMarkActive(state, marks.strong),
italic: isMarkActive(state, marks.em),
};
}

export function formatPlugin(onFormatChange: (formats: FormatState) => void): Plugin<FormatState> {
return new Plugin<FormatState>({
key: formatPluginKey,
state: {
init(_, state): FormatState {
return getActiveFormats(state);
},
apply(tr, pluginState, oldState, newState): FormatState {
if (tr.selectionSet || tr.docChanged) {
return getActiveFormats(newState);
}
return pluginState;
},
},
view(editorView: EditorView) {
const initialState = formatPluginKey.getState(editorView.state);
if (initialState) {
onFormatChange(initialState);
}

return {
update(view: EditorView, prevState: EditorState) {
const newState = formatPluginKey.getState(view.state);
const oldState = formatPluginKey.getState(prevState);

if (newState && oldState && newState !== oldState) {
onFormatChange(newState);
}
},
};
},
});
}
Loading