diff --git a/src/constants.ts b/src/constants.ts index e5b83fa..87a2153 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -19,6 +19,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { post_processor: true, live_preview: true, + alternative_live_preview: false, alternative_cursor_movement: true, }; @@ -26,6 +27,7 @@ export const DEFAULT_SETTINGS: PluginSettings = { export const REQUIRES_FULL_RELOAD: Set = new Set([ "preview_mode", "live_preview", + "alternative_live_preview", "editor_gutter", "hide_empty_gutter", diff --git a/src/editor/renderers/live-preview.ts b/src/editor/renderers/live-preview.ts index 291c18d..71a2c3e 100644 --- a/src/editor/renderers/live-preview.ts +++ b/src/editor/renderers/live-preview.ts @@ -13,33 +13,27 @@ import { WidgetType, } from '@codemirror/view'; -import { - Component, editorEditorField, - editorInfoField, - editorLivePreviewField, - editorViewField, - MarkdownRenderer, - setIcon, -} from 'obsidian'; +import { Component, editorLivePreviewField, MarkdownRenderer, setIcon } from 'obsidian'; import type { PluginSettings } from '../../types'; import { NodeType } from '../../types'; import { nodesInSelection, selectionRangeOverlap } from '../editor-util'; +import { CriticMarkupNode, SubstitutionNode } from '../criticmarkup-nodes'; export const inlineCommentRenderer = StateField.define({ create(state): DecorationSet { return Decoration.none; }, - update(oldState: DecorationSet, tr: Transaction) { + update(oldSet: DecorationSet, tr: Transaction) { const is_livepreview = tr.state.field(editorLivePreviewField); // TODO: Find cleaner way to check if preview was toggled const preview_changed = is_livepreview !== tr.startState.field(editorLivePreviewField); - // TODO: oldState.size is a bit overkill, since notes without any comment nodes will always parse the document? - if (!tr.docChanged && !preview_changed && oldState.size) - return oldState; + // TODO: oldSet.size is a bit overkill, since notes without any comment nodes will always parse the document? + if (!tr.docChanged && !preview_changed && oldSet.size) + return oldSet; if (preview_changed && !is_livepreview) return Decoration.none; @@ -137,6 +131,154 @@ class CommentIconWidget extends WidgetType { } } +function removeBrackets(decorations: Range[], node: CriticMarkupNode) { +decorations.push( + Decoration.replace({ + attributes: { 'data-contents': 'string' }, + }).range(node.from, node.from + 3) + ); + decorations.push( + Decoration.replace({ + attributes: { 'data-contents': 'string' }, + }).range(node.to - 3, node.to) + ); +} + +function removeBracket(decorations: Range[], node: CriticMarkupNode, left: boolean) { + if (left) + decorations.push( + Decoration.replace({ + attributes: { 'data-contents': 'string' }, + }).range(node.from, node.from + 3) + ); + else + decorations.push( + Decoration.replace({ + attributes: { 'data-contents': 'string' }, + }).range(node.to - 3, node.to) + ); +} + +function hideNode(decorations: Range[], node: CriticMarkupNode) { + decorations.push( + Decoration.replace({}).range(node.from, node.to) + ); +} + +function markContents(decorations: Range[], node: CriticMarkupNode, style: string, left: boolean | null = null) { + if (node.type === NodeType.SUBSTITUTION) { + if (left) { + if (!node.part_is_empty(true)) { + decorations.push( + Decoration.mark({ + attributes: { 'data-contents': 'string' }, + class: style, + }).range(node.from + 3, (node as SubstitutionNode).middle), + ); + } + } else { + if (!node.part_is_empty(false)) { + decorations.push( + Decoration.mark({ + attributes: { 'data-contents': 'string' }, + class: style, + }).range((node as SubstitutionNode).middle + 2, node.to - 3), + ); + } + } + } else { + if (!node.empty()) { + decorations.push( + Decoration.mark({ + attributes: { 'data-contents': 'string' }, + class: style, + }).range(node.from + 3, node.to - 3) + ); + } + } +} + + + +export const livePreviewRenderer = (settings: PluginSettings) => StateField.define({ + create(state): DecorationSet { + return Decoration.none; + }, + + update(oldSet: DecorationSet, tr: Transaction) { + const is_livepreview = tr.state.field(editorLivePreviewField); + const tree = tr.state.field(treeParser).tree; + const nodes = nodesInSelection(tree); + + if (!is_livepreview) + return Decoration.none; + + // const builder = new RangeSetBuilder(); + const decorations: Range[] = []; + + for (const node of nodes.nodes) { + if (!settings.preview_mode) { + if (tr.selection?.ranges?.some(range => node.partially_in_range(range.from, range.to))) { + markContents(decorations, node, 'criticmarkup-editing'); + } else if (node.type === NodeType.SUBSTITUTION) { + removeBracket(decorations, node, true); + markContents(decorations, node, 'criticmarkup-editing criticmarkup-inline criticmarkup-deletion criticmarkup-substitution', true) + decorations.push( + Decoration.replace({ + attributes: { 'data-contents': 'string' }, + }).range((node as SubstitutionNode).middle, (node as SubstitutionNode).middle + 2) + ); + markContents(decorations, node, 'criticmarkup-editing criticmarkup-inline criticmarkup-addition criticmarkup-substitution', false) + removeBracket(decorations, node, false); + } else { + removeBracket(decorations, node, true); + markContents(decorations, node, `criticmarkup-editing criticmarkup-inline criticmarkup-${node.repr.toLowerCase()}`); + removeBracket(decorations, node, false); + } + } else if (settings.preview_mode === 1) { + if (node.type === NodeType.ADDITION) { + removeBracket(decorations, node, true); + markContents(decorations, node, 'criticmarkup-accepted') + removeBracket(decorations, node, false); + } else if (node.type === NodeType.DELETION) { + // markContents(decorations, node, 'rejected') + hideNode(decorations, node); + } else if (node.type === NodeType.SUBSTITUTION) { + decorations.push(Decoration.replace({}).range(node.from, node.from + 3)); + markContents(decorations, node, 'criticmarkup-accepted', true) + // markContents(decorations, node, 'rejected', false) + decorations.push(Decoration.replace({}).range((node as SubstitutionNode).middle, node.to)); + } else { + removeBrackets(decorations, node); + } + } else if (settings.preview_mode === 2) { + if (node.type === NodeType.ADDITION) { + // markContents(decorations, node, 'rejected'); + hideNode(decorations, node); + } else if (node.type === NodeType.DELETION) { + removeBracket(decorations, node, true); + markContents(decorations, node, 'criticmarkup-accepted') + removeBracket(decorations, node, false); + } else if (node.type === NodeType.SUBSTITUTION) { + decorations.push(Decoration.replace({}).range(node.from, (node as SubstitutionNode).middle + 2)); + // markContents(decorations, node, 'rejected', true); + markContents(decorations, node, 'criticmarkup-accepted', false); + decorations.push(Decoration.replace({}).range(node.to - 3, node.to)); + } else { + removeBrackets(decorations, node); + } + } + } + return Decoration.set(decorations); + }, + + provide(field: StateField): Extension { + return EditorView.decorations.from(field); + } +}); + + + export function livePreview (settings: PluginSettings): Extension { return ViewPlugin.fromClass( class CriticMarkupViewPlugin implements PluginValue { diff --git a/src/main.ts b/src/main.ts index 5aa7d0d..378910c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ import { change_suggestions } from './editor/context-menu-commands'; import { treeParser } from './editor/tree-parser'; import { nodesInSelection } from './editor/editor-util'; -import { inlineCommentRenderer, livePreview } from './editor/renderers/live-preview'; +import { inlineCommentRenderer, livePreview, livePreviewRenderer } from './editor/renderers/live-preview'; import { postProcess, postProcessorUpdate } from './editor/renderers/post-processor'; import { keybindExtensions } from './editor/suggestion-mode/keybinds'; @@ -56,9 +56,12 @@ export default class CommentatorPlugin extends Plugin { this.editorExtensions.push(treeParser); this.editorExtensions.push(inlineCommentRenderer); - if (this.settings.live_preview) - this.editorExtensions.push(livePreview(this.settings)); - + if (this.settings.live_preview) { + if (this.settings.alternative_live_preview) + this.editorExtensions.push(livePreviewRenderer(this.settings)); + else + this.editorExtensions.push(livePreview(this.settings)); + } // Performance: ~3ms in stress-test if (this.settings.editor_gutter) @@ -84,7 +87,7 @@ export default class CommentatorPlugin extends Plugin { event.clipboardData.setData('text/plain', removed_syntax); event.preventDefault(); } - } + }, })); } @@ -147,7 +150,7 @@ export default class CommentatorPlugin extends Plugin { icon: 'comment', callback: async () => { this.app.vault.setConfig('vimMode', !this.app.vault.getConfig('vimMode')); - } + }, }); for (const command of commands) { @@ -169,7 +172,7 @@ export default class CommentatorPlugin extends Plugin { if (result && !args[0]) this.loadEditorExtensions(); return result; - } + }; }, })); }); diff --git a/src/types.ts b/src/types.ts index ec37771..2ad08aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -72,6 +72,9 @@ export interface PluginSettings { * Enable live preview rendering */ live_preview: boolean; + /** Enable alternative live preview renderer */ + alternative_live_preview: boolean; + /** * Enable corrected cursor movement near/within nodes */ diff --git a/src/ui/settings.ts b/src/ui/settings.ts index f26de41..24e0f87 100644 --- a/src/ui/settings.ts +++ b/src/ui/settings.ts @@ -165,6 +165,17 @@ export class CommentatorSettings extends PluginSettingTab { } )); + new Setting(containerEl) + .setName("Alternative Live preview renderer") + .setDesc("May bring worse performance, but offers a more accurate preview") + .addToggle(toggle => toggle.setValue(this.plugin.settings.alternative_live_preview) + .onChange(async (value) => { + this.plugin.settings.alternative_live_preview = value; + await this.plugin.saveSettings(); + } + )); + + new Setting(containerEl) .setName("Alternative cursor movement") .setDesc("Toggle corrected cursor movement of cursor when CriticMarkup tags are present")