From bf6ec3d94ab503325620abead29025fc6e2edfad Mon Sep 17 00:00:00 2001 From: kotaindah55 Date: Tue, 4 Mar 2025 17:05:32 +0200 Subject: [PATCH] build: remake of decoration builder to let the decorations have more behavior --- .../decorator/builder/DecorationBuilder.ts | 308 ++++++++++++------ .../decorator/builder/DecorationHolder.ts | 13 + .../decorator/builder/DelimOmitter.ts | 73 +++++ .../decorator/builder/ExtendedSyntax.ts | 85 ----- .../decorator/builder/LineBreakReplacer.ts | 66 ++++ .../decorator/builder/RangeOmitter.ts | 52 --- .../decorator/builder/TokensCatcher.ts | 18 + src/editor-mode/decorator/builder/index.ts | 6 +- .../decorator/decorations/index.ts | 108 +----- .../decorator/utils/createHlDeco.ts | 4 +- .../decorator/utils/createInlineDecoRange.ts | 8 + .../decorator/utils/createLineDecoRange.ts | 9 + src/editor-mode/decorator/utils/getLineAt.ts | 18 + .../decorator/utils/getTagRange.ts | 7 + src/editor-mode/decorator/utils/index.ts | 11 +- .../decorator/utils/isEditorModeChanged.ts | 8 + src/editor-mode/decorator/utils/iterLine.ts | 8 + .../decorator/utils/iterTokenGroup.ts | 17 + .../decorator/utils/moveTokenIndexCache.ts | 23 ++ .../decorator/utils/sliceStrFromLine.ts | 7 + .../decorator/widgets/ColorButton.ts | 217 ++++++------ .../decorator/widgets/HiddenWidget.ts | 15 +- .../decorator/widgets/LineBreak.ts | 20 ++ src/editor-mode/decorator/widgets/index.ts | 3 +- src/editor-mode/extensions/index.ts | 23 +- src/editor-mode/utils/checkRefreshed.ts | 13 + .../utils/getActiveCanvasEditor.ts | 9 + .../utils/getActiveCanvasNodeCoords.ts | 11 + src/editor-mode/utils/index.ts | 4 + src/editor-mode/utils/isCanvas.ts | 5 + src/editor-mode/view-plugin/BuilderPlugin.ts | 15 + src/editor-mode/view-plugin/index.ts | 1 + 32 files changed, 729 insertions(+), 456 deletions(-) create mode 100644 src/editor-mode/decorator/builder/DecorationHolder.ts create mode 100644 src/editor-mode/decorator/builder/DelimOmitter.ts delete mode 100644 src/editor-mode/decorator/builder/ExtendedSyntax.ts create mode 100644 src/editor-mode/decorator/builder/LineBreakReplacer.ts delete mode 100644 src/editor-mode/decorator/builder/RangeOmitter.ts create mode 100644 src/editor-mode/decorator/builder/TokensCatcher.ts create mode 100644 src/editor-mode/decorator/utils/createInlineDecoRange.ts create mode 100644 src/editor-mode/decorator/utils/createLineDecoRange.ts create mode 100644 src/editor-mode/decorator/utils/getLineAt.ts create mode 100644 src/editor-mode/decorator/utils/getTagRange.ts create mode 100644 src/editor-mode/decorator/utils/isEditorModeChanged.ts create mode 100644 src/editor-mode/decorator/utils/iterLine.ts create mode 100644 src/editor-mode/decorator/utils/iterTokenGroup.ts create mode 100644 src/editor-mode/decorator/utils/moveTokenIndexCache.ts create mode 100644 src/editor-mode/decorator/utils/sliceStrFromLine.ts create mode 100644 src/editor-mode/decorator/widgets/LineBreak.ts create mode 100644 src/editor-mode/utils/checkRefreshed.ts create mode 100644 src/editor-mode/utils/getActiveCanvasEditor.ts create mode 100644 src/editor-mode/utils/getActiveCanvasNodeCoords.ts create mode 100644 src/editor-mode/utils/index.ts create mode 100644 src/editor-mode/utils/isCanvas.ts create mode 100644 src/editor-mode/view-plugin/BuilderPlugin.ts create mode 100644 src/editor-mode/view-plugin/index.ts diff --git a/src/editor-mode/decorator/builder/DecorationBuilder.ts b/src/editor-mode/decorator/builder/DecorationBuilder.ts index b735e7d..1c49cf7 100644 --- a/src/editor-mode/decorator/builder/DecorationBuilder.ts +++ b/src/editor-mode/decorator/builder/DecorationBuilder.ts @@ -1,113 +1,225 @@ -import { EditorState, Range } from "@codemirror/state"; -import { Decoration, DecorationSet, ViewUpdate } from "@codemirror/view"; +import { ChangeDesc, ChangeSet, EditorState, Range, RangeSet, Text, Transaction } from "@codemirror/state"; +import { Decoration, EditorView, ViewUpdate } from "@codemirror/view"; import { Parser } from "src/editor-mode/parser"; -import { Format, TokenRole, TokenStatus } from "src/enums"; -import { createHlDeco } from "src/editor-mode/decorator/utils"; -import { AlignDeco, AlignMarkDeco, ColorTagDeco, ContentDeco, DelimDeco, RevealedSpoiler } from "src/editor-mode/decorator/decorations"; -import { AlignFormat, CharPos, MainFormat } from "src/types"; +import { Format, MarkdownViewMode, TokenStatus } from "src/enums"; +import { REVEALED_SPOILER_DECO } from "src/editor-mode/decorator/decorations"; +import { BlockFormat, PlainRange, InlineFormat, Token, TokenGroup, TokenDecoration, PluginSettings } from "src/types"; import { ColorButton } from "src/editor-mode/decorator/widgets"; +import { BlockRules, InlineRules } from "src/shared-configs"; +import { DecorationHolder, DelimOmitter, LineBreakReplacer, TokensCatcher } from "src/editor-mode/decorator/builder"; +import { createInlineDecoRange, createLineDecoRange, getLineAt, getTagRange, isEditorModeChanged, iterLine, iterTokenGroup, sliceStrFromLine } from "src/editor-mode/decorator/utils"; +import { trimTag } from "src/utils"; +import { SelectionObserver } from "src/editor-mode/observer"; +import { editorLivePreviewField } from "obsidian"; +import { refresherAnnot } from "src/editor-mode/annotations"; -/** Decoration builder (excludes omitted delimiter), should be attached to `ExtendedSyntax` */ export class DecorationBuilder { - /** Attached `Parser` for obtaining tokens quickly */ - parser: Parser; - constructor() { + readonly parser: Parser; + readonly omitter: DelimOmitter; + readonly catcher: TokensCatcher; + readonly lineBreakReplacer: LineBreakReplacer; + readonly selectionObserver: SelectionObserver; + readonly holder: DecorationHolder; + readonly settings: PluginSettings; + readonly indexCaches = { + linePos: { number: 1 }, // 1-based + inlineToken: { number: 0 }, // 0-based + blockToken: { number: 0 } // 0-based + } + constructor(parser: Parser, selectionObserver: SelectionObserver) { + this.parser = parser; + this.selectionObserver = selectionObserver; + this.settings = parser.settings; + this.omitter = new DelimOmitter(this.settings, selectionObserver); + this.catcher = new TokensCatcher(); + this.lineBreakReplacer = new LineBreakReplacer(parser); + this.holder = new DecorationHolder(); } /** - * Produces _necessary_ decoration ranges from tokens provided by - * `Builder.parser`. Therefore, it doesn't reproduce new ranges - * from reused tokens. Doesn't include supplementary and omitted - * delimiter. + * Main decorations hold basic formatting style of the tokens. + * + * Intended to build non-height-altering decorations. So, it doesn't + * include line breaks and fenced div opening omitter. (It runs only in + * the view update) */ - build(state: EditorState) { - let doc = state.doc, - tokens = this.parser.tokens, - mainDecoRanges: Range[] = [], - outerDecoRanges: Range[] = [], - delimiterRanges: Range[] = [], - { startToken, endToken } = this.parser.lastParsed; - for ( - let i = startToken, token = tokens[i]; - i < endToken; - token = tokens[++i] - ) { - if (token.status != TokenStatus.ACTIVE) { continue } - if (token.role == TokenRole.OPEN) { - if (token.type == Format.HIGHLIGHT) { - let color = "", - open = token, - close = tokens[i + token.size - 1], - mayBeColorTag = tokens[i + 2], - hasColorTag = mayBeColorTag?.type == Format.COLOR_TAG; - // gets color tag - if (hasColorTag) { - color = doc.sliceString(mayBeColorTag.from + 1, mayBeColorTag.to - 1); - i++; - } - let hlDeco = createHlDeco(color, { - open: { from: open.from, to: open.to } as CharPos, - close: { from: close.from, to: close.to } as CharPos - }); - // highlight should be pushed in outer decorations - outerDecoRanges.push(hlDeco.range(open.from, close.to)); - if (hasColorTag) { - delimiterRanges.push(ColorTagDeco.range(mayBeColorTag.from, mayBeColorTag.to)); - } - } else { - let content = tokens[++i]; - delimiterRanges.push(DelimDeco[token.type as MainFormat].range(token.from, token.to)); - if (token.type == Format.SPOILER) { // spoiler should be pushed in outer decorations - outerDecoRanges.push(ContentDeco[token.type as MainFormat].range(content.from, content.to)); - } else { - mainDecoRanges.push(ContentDeco[token.type as MainFormat].range(content.from, content.to)); - } - } - } else if (token.role == TokenRole.CLOSE && token.type != Format.HIGHLIGHT) { - delimiterRanges.push(DelimDeco[token.type as MainFormat].range(token.from, token.to)); - } else if (token.role == TokenRole.BLOCK_TAG) { - delimiterRanges.push(AlignMarkDeco[token.type as AlignFormat].range(token.from, token.to)); - mainDecoRanges.push(AlignDeco[token.type as AlignFormat].range(token.from, token.from)); + buildMain(view: EditorView, state: EditorState) { + this.buildInline(state, view.visibleRanges); + this.buildBlock(state, view.visibleRanges); + } + /** + * Supplementary decorations consist omitted delimiter of inline tokens, + * color buttons for the highlight, and revealed spoiler when touched the + * cursor or selection. + * + * Intended to build non-height-altering decorations. So, it doesn't + * include line breaks and fenced div opening omitter. (It runs only in + * the view update) + */ + buildSupplementary(isLivePreview: boolean) { + if (isLivePreview) { + this.omitInlineDelim(this.catcher.activeTokens); + this.createColorBtnWidgets(this.catcher.hlTokens); + this.revealSpoiler(this.catcher.spoilerTokens); + } else { + this.holder.colorBtnSet = this.holder.revealedSpoilerSet = RangeSet.empty; + } + } + /** + * Runs once on editor intialization, should be inside the view update + * (i.e. the ViewPlugin update). + */ + onViewInit(view: EditorView) { + let state = view.state, + isLivePreview = state.field(editorLivePreviewField); + this.buildMain(view, state); + this.buildSupplementary(isLivePreview); + } + onViewUpdate(update: ViewUpdate) { + let state = update.state, + view = update.view, + isLivePreview = state.field(editorLivePreviewField); + if (this.parser.isReparsing || this.parser.isInitializing || update.viewportMoved) { + this.buildMain(view, state); + } + if (this.selectionObserver.isObserving || update.viewportMoved) { + this.buildSupplementary(isLivePreview); + } + } + /** + * Runs once on editor intialization, should be inside the state update + * (i.e. the StateField update). + */ + onStateInit(state: EditorState) { + let isLivePreview = state.field(editorLivePreviewField); + if (isLivePreview) { + this.omitFencedDivOpening(); + } + this.replaceLineBreaks(state.doc); + } + onStateUpdate(transaction: Transaction) { + let state = transaction.state, + isLivePreview = state.field(editorLivePreviewField), + isRefreshed = transaction.annotation(refresherAnnot), + isModeChanged = isEditorModeChanged(state, transaction.startState); + if (this.parser.isReparsing || this.parser.isInitializing) { + this.replaceLineBreaks(transaction.newDoc, transaction.changes); + } + if (isModeChanged || isRefreshed) { + this.selectionObserver.restartObserver(transaction.newSelection, transaction.docChanged); + this.holder.blockOmittedSet = RangeSet.empty; + } + if (!isLivePreview) { + this.removeOmitter(); + } else if (this.selectionObserver.isObserving || isModeChanged) { + this.omitFencedDivOpening(transaction.changes); + } + } + buildInline(state: EditorState, visibleRanges: readonly PlainRange[]) { + let inlineDecoRanges: Range[] = [], + activeTokens: TokenGroup = [], + hlTokens: TokenGroup = [], + spoilerTokens: TokenGroup = []; + iterTokenGroup({ + tokens: this.parser.inlineTokens, + ranges: visibleRanges, + indexCache: this.indexCaches.inlineToken, + callback: (token) => { + if (token.status != TokenStatus.ACTIVE) { return } + if (token.type == Format.HIGHLIGHT) { hlTokens.push(token) } + if (token.type == Format.SPOILER) { spoilerTokens.push(token) } + inlineDecoRanges.push(this.transformInlineToken(token, state.doc)); + activeTokens.push(token); + }, + }); + this.catcher.catch(activeTokens, hlTokens, spoilerTokens); + return this.holder.inlineSet = Decoration.set(inlineDecoRanges); + } + buildBlock(state: EditorState, visibleRanges: readonly PlainRange[]) { + let lineDecoRanges: Range[] = []; + iterTokenGroup({ + tokens: this.parser.blockTokens, + ranges: visibleRanges, + indexCache: this.indexCaches.blockToken, + callback: (token) => { + if (token.status != TokenStatus.ACTIVE) { return } + lineDecoRanges.push(...this.transformBlockToken(token, state.doc)); + }, + }); + return this.holder.blockSet = Decoration.set(lineDecoRanges); + } + transformInlineToken(token: Token, doc: Text) { + let cls = "cm-" + InlineRules[token.type as InlineFormat].class; + if (token.tagLen) { + let line = getLineAt(doc, token.from, this.indexCaches.linePos), + tagRange = getTagRange(token), + tagStr = sliceStrFromLine(line, tagRange.from + 1, tagRange.to - 1); + if (token.type == Format.CUSTOM_SPAN) { + tagStr = trimTag(tagStr); + cls += " " + tagStr; + } else { + cls += " " + cls + "-" + tagStr; } } - return { mainDecoRanges, outerDecoRanges, delimiterRanges }; + return createInlineDecoRange(token, cls); } - /** - * Filters out some ranges from old set that needs to be replaced by the new one, - * resulting new `DecorationSet`. Doesn't intended to filtering supplementary - * and omitted delimiter set. - */ - updateSet(update: ViewUpdate, decoSet: DecorationSet, ranges: Range[]) { - let { startToken: start, endToken: end } = this.parser.lastParsed, - docLen = update.state.doc.length, - filterFrom = (this.parser.tokens[start - 1]?.to ?? 0), - filterTo = (this.parser.tokens[end]?.from ?? docLen); - return decoSet.map(update.changes).update({ - add: ranges, filterFrom, filterTo, - filter(from, to, val) { - return (to == filterFrom || from == filterTo) && ((val.spec.type as Format) <= Format.ALIGN_JUSTIFY && from != 0 || to != from); - } + transformBlockToken(token: Token, doc: Text) { + let baseCls = "cm-" + BlockRules[token.type as BlockFormat].class, + openLine = getLineAt(doc, token.from, this.indexCaches.linePos), + tagRange = getTagRange(token), + tagStr = trimTag(sliceStrFromLine(openLine, tagRange.from, tagRange.to)), + openDelimCls = baseCls + " cm-fenced-div-start", + contentCls = baseCls + " " + tagStr, + ranges: Range[] = [createLineDecoRange(token, openDelimCls, openLine)]; + iterLine({ + doc, fromLn: openLine.number + 1, + callback(line) { + if (line.to > token.to) { return false } + if (line.to == token.to && !line.text.trimEnd()) { return false } + let decoRange = createLineDecoRange(token, contentCls, line); + ranges.push(decoRange); + }, }); + return ranges; } - /** Produces supplementary decorations (color button and reveled text for spoiler). */ - // TODO: Make it incrementally - getSupplemental(state: EditorState, outerDecoSet: DecorationSet) { - let decoRanges: Range[] = [], - selectRanges = state.selection.ranges, - i = 0; - outerDecoSet.between(0, state.doc.length, (from, to, val) => { - while (selectRanges[i] && selectRanges[i].to < from) { i++ } - if (!selectRanges[i]) { return false } - if (selectRanges[i].from > to) { return } - let type = val.spec.type as Format.HIGHLIGHT | Format.SPOILER; - if (type == Format.HIGHLIGHT && this.parser.settings.colorButton) { - let color = val.spec.color as string, - open = (val.spec.open as CharPos), - close = (val.spec.close as CharPos); - decoRanges.push(ColorButton.of(color, open, close)); - } else if (type == Format.SPOILER) { - decoRanges.push(RevealedSpoiler.range(from, to)); + createColorBtnWidgets(hlTokens: TokenGroup) { + if (!this.settings.colorButton) { + return this.holder.colorBtnSet = RangeSet.empty; + } + let btnWidgets: Range[] = []; + for (let i = 0; i < hlTokens.length; i++) { + let token = hlTokens[i]; + if (this.selectionObserver.touchSelection(token.from, token.to)) { + btnWidgets.push(ColorButton.of(token)); } - }); - return Decoration.set(decoRanges); + } + return this.holder.colorBtnSet = Decoration.set(btnWidgets); + } + revealSpoiler(spoilerTokens: TokenGroup) { + let revealedRanges: Range[] = []; + for (let i = 0; i < spoilerTokens.length; i++) { + let token = spoilerTokens[i]; + if (this.selectionObserver.touchSelection(token.from, token.to)) { + revealedRanges.push(REVEALED_SPOILER_DECO.range(token.from, token.to)); + } + } + return this.holder.revealedSpoilerSet = Decoration.set(revealedRanges); + } + omitInlineDelim(activeTokens: TokenGroup) { + return this.holder.inlineOmittedSet = this.omitter.omitInline(activeTokens); + } + /** Executed only in the state update. */ + replaceLineBreaks(doc: Text, changes?: ChangeSet) { + return this.holder.lineBreaksSet = this.lineBreakReplacer.replace(doc, changes); + } + /** Executed only in the state update. */ + omitFencedDivOpening(changes?: ChangeDesc) { + let isOmitted = !(this.settings.alwaysShowFencedDivTag & MarkdownViewMode.EDITOR_MODE); + if (!isOmitted) { + return this.holder.blockOmittedSet = RangeSet.empty; + } + return this.holder.blockOmittedSet = this.omitter.omitBlock(this.holder.blockOmittedSet, changes); + } + removeOmitter() { + this.holder.inlineOmittedSet = this.holder.blockOmittedSet = RangeSet.empty; } } \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/DecorationHolder.ts b/src/editor-mode/decorator/builder/DecorationHolder.ts new file mode 100644 index 0000000..bb831cf --- /dev/null +++ b/src/editor-mode/decorator/builder/DecorationHolder.ts @@ -0,0 +1,13 @@ +import { RangeSet } from "@codemirror/state"; +import { DecorationSet } from "@codemirror/view"; + +export class DecorationHolder { + inlineSet: DecorationSet = RangeSet.empty; + blockSet: DecorationSet = RangeSet.empty; + inlineOmittedSet: DecorationSet = RangeSet.empty; + blockOmittedSet: DecorationSet = RangeSet.empty; + colorBtnSet: DecorationSet = RangeSet.empty; + revealedSpoilerSet: DecorationSet = RangeSet.empty; + lineBreaksSet: DecorationSet = RangeSet.empty; + constructor() {} +} \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/DelimOmitter.ts b/src/editor-mode/decorator/builder/DelimOmitter.ts new file mode 100644 index 0000000..7c467ea --- /dev/null +++ b/src/editor-mode/decorator/builder/DelimOmitter.ts @@ -0,0 +1,73 @@ +import { ChangeDesc, Range } from "@codemirror/state"; +import { Decoration, DecorationSet } from "@codemirror/view"; +import { DisplayBehaviour, Format, TokenLevel, TokenStatus } from "src/enums"; +import { PluginSettings, TokenGroup } from "src/types"; +import { HiddenWidget } from "src/editor-mode/decorator/widgets"; +import { SelectionObserver } from "src/editor-mode/observer"; + +export class DelimOmitter { + selectionObserver: SelectionObserver; + settings: PluginSettings; + constructor(settings: PluginSettings, selectionObserver: SelectionObserver) { + this.settings = settings; + this.selectionObserver = selectionObserver; + } + omitBlock(omittedSet?: DecorationSet, changes?: ChangeDesc) { + let omittedRanges: Range[] = [], + filterRegion = this.selectionObserver.filterRegions[TokenLevel.BLOCK]; + this.selectionObserver.iterateChangedRegion(TokenLevel.BLOCK, (token, index, tokens, inSelection) => { + if (inSelection || token.status != TokenStatus.ACTIVE || !token.validTag) { return } + let openFrom = token.from, + openTo = openFrom + token.openLen + token.tagLen; + if (token.to > openTo) { openTo++ } + omittedRanges.push(HiddenWidget.of(openFrom, openTo, token, true)); + }); + if (!omittedSet?.size) { + omittedSet = Decoration.set(omittedRanges); + } else { + if (changes) { + omittedSet = omittedSet.map(changes); + } + for (let i = 0; i < filterRegion.length; i++) { + let filterRange = filterRegion[i]; + omittedSet = omittedSet.update({ + filterFrom: filterRange.from, + filterTo: filterRange.to, + filter: () => false, + }); + } + omittedSet = omittedSet.update({ add: omittedRanges }); + } + return omittedSet; + } + omitInline(activeTokens: TokenGroup) { + let alwaysShowHlTag = this.settings.hlTagDisplayBehaviour & DisplayBehaviour.ALWAYS, + alwaysShowSpanTag = this.settings.spanTagDisplayBehaviour & DisplayBehaviour.ALWAYS, + showHlTagIfTouched = this.settings.hlTagDisplayBehaviour & DisplayBehaviour.TAG_TOUCHED, + showSpanTagIfTouched = this.settings.spanTagDisplayBehaviour & DisplayBehaviour.TAG_TOUCHED; + let omittedRanges: Range[] = []; + for (let i = 0; i < activeTokens.length; i++) { + let token = activeTokens[i], + openFrom = token.from, + openTo = openFrom + token.openLen, + tagTo = openTo + token.tagLen; + if (this.selectionObserver.touchSelection(token.from, token.to)) { + if ( + token.validTag && !this.selectionObserver.touchSelection(openTo, tagTo) && + (token.type == Format.HIGHLIGHT && showHlTagIfTouched || token.type == Format.CUSTOM_SPAN && showSpanTagIfTouched) + ) { + omittedRanges.push(HiddenWidget.of(openTo, tagTo, token)); + } + } else { + if (token.type == Format.HIGHLIGHT && !alwaysShowHlTag || token.type == Format.CUSTOM_SPAN && !alwaysShowSpanTag) { + openTo = tagTo; + } + omittedRanges.push(HiddenWidget.of(openFrom, openTo, token)); + if (token.closeLen) { + omittedRanges.push(HiddenWidget.of(token.to - token.closeLen, token.to, token)); + } + } + } + return Decoration.set(omittedRanges); + } +} \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/ExtendedSyntax.ts b/src/editor-mode/decorator/builder/ExtendedSyntax.ts deleted file mode 100644 index 1532b27..0000000 --- a/src/editor-mode/decorator/builder/ExtendedSyntax.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Decoration, DecorationSet, EditorView, PluginValue, ViewUpdate } from "@codemirror/view"; -import { Parser } from "src/editor-mode/parser"; -import { DecorationBuilder, RangeOmitter } from "src/editor-mode/decorator/builder"; -import { parserField } from "src/editor-mode/state-fields"; -import { EditorState, RangeSet } from "@codemirror/state"; -import { editorLivePreviewField } from "obsidian"; - -export class ExtendedSyntax implements PluginValue { - /** Insertion, superscript, and subscript. */ - mainDecoSet: DecorationSet; - /** - * Spoiler and highlight, positioned at upper DOM hierarchy and - * not split by other decorations. Useful for making them appear - * continous in the editor, especially when overlap with others. - */ - outerDecoSet: DecorationSet; - /** Any type of tag and delimiter */ - delimiterSet: DecorationSet; - /** - * Any type of tag and delimiter omitted due to touching cursor or selection. - * Used only when in live preview mode. - */ - omittedSet: DecorationSet; - /** - * Color button for highlight and revealed text for spoiler, - * only when selection or cursor are touching them. - */ - supplementalSet: DecorationSet; - combinedSet: DecorationSet; - parser: Parser; - builder: DecorationBuilder; - omitter: RangeOmitter; - noOmit = true; - constructor(view: EditorView) { - this.builder = new DecorationBuilder(); - this.omitter = new RangeOmitter(); - this.parser = view.state.field(parserField); - this.builder.parser = this.parser; - this.init(view.state); - } - init(state: EditorState) { - let { mainDecoRanges, outerDecoRanges, delimiterRanges } = this.builder.build(state); - this.mainDecoSet = Decoration.set(mainDecoRanges); - this.outerDecoSet = Decoration.set(outerDecoRanges); - this.delimiterSet = Decoration.set(delimiterRanges); - this.supplementalSet = this.builder.getSupplemental(state, this.outerDecoSet); - if (state.field(editorLivePreviewField)) { - this.noOmit = false; - this.omittedSet = this.omitter.omit(state, this.delimiterSet); - } else { - this.omittedSet = RangeSet.empty; - } - this.combine(); - } - update(update: ViewUpdate) { - if (update.docChanged || this.parser.isReparsing) { - let { mainDecoRanges, outerDecoRanges, delimiterRanges } = this.builder.build(update.state); - this.mainDecoSet = this.builder.updateSet(update, this.mainDecoSet, mainDecoRanges); - this.outerDecoSet = this.builder.updateSet(update, this.outerDecoSet, outerDecoRanges); - this.delimiterSet = this.builder.updateSet(update, this.delimiterSet, delimiterRanges); - } - if (update.docChanged || update.selectionSet || this.parser.isReparsing) { - this.supplementalSet = this.builder.getSupplemental(update.state, this.outerDecoSet); - } - if (update.state.field(editorLivePreviewField)) { - if (update.docChanged || update.selectionSet || this.parser.isReparsing || this.noOmit) { - this.omittedSet = this.omitter.omit(update.state, this.delimiterSet); - } - this.noOmit = false; - } else { - this.omittedSet = RangeSet.empty; - this.noOmit = true; - } - this.combine(); - this.parser.isReparsing = false; - } - combine() { - this.combinedSet = RangeSet.join([ - this.mainDecoSet, - this.delimiterSet, - this.omittedSet, - this.supplementalSet - ]); - } -} \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/LineBreakReplacer.ts b/src/editor-mode/decorator/builder/LineBreakReplacer.ts new file mode 100644 index 0000000..4f6e2d7 --- /dev/null +++ b/src/editor-mode/decorator/builder/LineBreakReplacer.ts @@ -0,0 +1,66 @@ +import { RangeSet, Range, Text, ChangeSet } from "@codemirror/state"; +import { Decoration } from "@codemirror/view"; +import { Parser } from "src/editor-mode/parser"; +import { IndexCache, RangeSetUpdate, TokenGroup } from "src/types"; +import { getLineAt, iterLine } from "src/editor-mode/decorator/utils"; +import { LineBreak } from "src/editor-mode/decorator/widgets"; +import { TokenLevel, TokenStatus } from "src/enums"; + +export class LineBreakReplacer { + private linePosCache: IndexCache = { number: 1 }; + parser: Parser; + lineBreakSet: RangeSet = RangeSet.empty; + constructor(parser: Parser) { + this.parser = parser; + } + replace(doc: Text, changes?: ChangeSet) { + let reparsedRange = this.parser.reparsedRanges[TokenLevel.BLOCK], + blockTokens = this.parser.blockTokens, + updateSpec: RangeSetUpdate = { + add: this.produceWidgetRanges(doc) + }; + if (!blockTokens.length) { + return this.lineBreakSet = RangeSet.empty; + } + if (reparsedRange.from != reparsedRange.initTo || reparsedRange.from != reparsedRange.changedTo) { + updateSpec.filterFrom = Math.min(blockTokens[reparsedRange.from]?.from ?? this.parser.lastStreamPoint.from, this.parser.lastStreamPoint.from); + updateSpec.filterTo = this.parser.lastStreamPoint.to; + updateSpec.filter = () => false; + } + if (changes) { + this.lineBreakSet = this.lineBreakSet.map(changes); + } + return this.lineBreakSet = this.lineBreakSet.update(updateSpec); + } + private getReparsedBlockTokens(): TokenGroup { + let range = this.parser.reparsedRanges[TokenLevel.BLOCK]; + return this.parser.blockTokens.slice(range.from, range.changedTo); + } + private produceWidgetRanges(doc: Text) { + let tokens = this.getReparsedBlockTokens(), + ranges: Range[] = []; + if (!tokens.length) { return ranges } + let startLine = getLineAt(doc, tokens[0].from, this.linePosCache), + tokenIndex = 0; + iterLine({ + doc, + fromLn: startLine.number, + callback: (line) => { + let curToken = tokens[tokenIndex]; + if (!curToken) { return false } + if (curToken.status != TokenStatus.ACTIVE) { tokenIndex++; return } + if ( + line.from == curToken.from || + line.from - 1 == curToken.from + curToken.openLen + curToken.tagLen + ) { return } + if ( + line.from >= curToken.to || + line.to < curToken.from || + line.to == curToken.to && curToken.closedByBlankLine + ) { tokenIndex++; return } + ranges.push(LineBreak.of(line.from)); + }, + }); + return ranges; + } +} \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/RangeOmitter.ts b/src/editor-mode/decorator/builder/RangeOmitter.ts deleted file mode 100644 index 7e771c9..0000000 --- a/src/editor-mode/decorator/builder/RangeOmitter.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { EditorState, Range } from "@codemirror/state"; -import { Decoration, DecorationSet } from "@codemirror/view"; -import { HiddenWidget } from "src/editor-mode/decorator/widgets"; -import { Format } from "src/enums"; -import { MainFormat } from "src/types"; - -/** - * Omits delimiters that touch the cursor or selection, - * so those don't appear in the editor. - */ -export class RangeOmitter { - constructor() {}; - /** Should be run in preview mode only */ - omit(state: EditorState, delimiterSet: DecorationSet) { - let omittedRanges: Range[] = [], - selectRanges = state.selection.ranges, i = 0, - openOffset: { [T in MainFormat]: null | number } = { - [Format.INSERTION]: null, - [Format.SPOILER]: null, - [Format.SUPERSCRIPT]: null, - [Format.SUBSCRIPT]: null, - [Format.HIGHLIGHT]: null - }; - delimiterSet.between(0, state.doc.length, (from, to, val) => { - let type = val.spec.type as Format; - if (type == Format.COLOR_TAG || type <= Format.ALIGN_JUSTIFY) { - while (i + 1 < selectRanges.length && selectRanges[i].to < from) { i++ } - if (selectRanges[i].from > to || selectRanges[i].to < from) { - omittedRanges.push(HiddenWidget.of(from, to, val)); - return; - } - } - else { - if (openOffset[type as MainFormat] === null) { - openOffset[type as MainFormat] = from; - } else { - let length = to - from, - openFrom = openOffset[type as MainFormat]!; - while (i + 1 < selectRanges.length && selectRanges[i].to < openFrom) { i++ } - if (selectRanges[i].from > to || selectRanges[i].to < openFrom) { - omittedRanges.push( - HiddenWidget.of(openFrom, openFrom + length, val), - HiddenWidget.of(from, to, val) - ); - } - openOffset[type as MainFormat] = null; - } - } - }); - return Decoration.set(omittedRanges, true); - } -} \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/TokensCatcher.ts b/src/editor-mode/decorator/builder/TokensCatcher.ts new file mode 100644 index 0000000..987d325 --- /dev/null +++ b/src/editor-mode/decorator/builder/TokensCatcher.ts @@ -0,0 +1,18 @@ +import { TokenGroup } from "src/types"; + +export class TokensCatcher { + activeTokens: TokenGroup = []; + hlTokens: TokenGroup = []; + spoilerTokens: TokenGroup = []; + constructor () {} + catch(activeTokens: TokenGroup, hlTokens: TokenGroup, spoilerTokens: TokenGroup) { + this.activeTokens = activeTokens; + this.hlTokens = hlTokens; + this.spoilerTokens = spoilerTokens; + } + empty() { + this.activeTokens = []; + this.hlTokens = []; + this.spoilerTokens = []; + } +} \ No newline at end of file diff --git a/src/editor-mode/decorator/builder/index.ts b/src/editor-mode/decorator/builder/index.ts index 48815b6..3f7c8d4 100644 --- a/src/editor-mode/decorator/builder/index.ts +++ b/src/editor-mode/decorator/builder/index.ts @@ -1,3 +1,5 @@ export * from "./DecorationBuilder"; -export * from "./RangeOmitter"; -export * from "./ExtendedSyntax"; \ No newline at end of file +export * from "./DelimOmitter"; +export * from "./TokensCatcher"; +export * from "./LineBreakReplacer"; +export * from "./DecorationHolder"; \ No newline at end of file diff --git a/src/editor-mode/decorator/decorations/index.ts b/src/editor-mode/decorator/decorations/index.ts index c0e83ed..de87092 100644 --- a/src/editor-mode/decorator/decorations/index.ts +++ b/src/editor-mode/decorator/decorations/index.ts @@ -1,111 +1,5 @@ import { Decoration } from "@codemirror/view"; -import { Format } from "src/enums"; -export const AlignDeco = { - [Format.ALIGN_LEFT]: Decoration.line({ - class: "cm-align-left", - type: Format.ALIGN_LEFT - }), - [Format.ALIGN_RIGHT]: Decoration.line({ - class: "cm-align-right", - type: Format.ALIGN_RIGHT - }), - [Format.ALIGN_CENTER]: Decoration.line({ - class: "cm-align-center", - type: Format.ALIGN_CENTER - }), - [Format.ALIGN_JUSTIFY]: Decoration.line({ - class: "cm-align-justify", - type: Format.ALIGN_JUSTIFY - }) -}; - -export const AlignMarkDeco = { - [Format.ALIGN_LEFT]: Decoration.mark({ - class: "cm-align-mark-left", - type: Format.ALIGN_LEFT, - omitted: true - }), - [Format.ALIGN_RIGHT]: Decoration.mark({ - class: "cm-align-mark-right", - type: Format.ALIGN_RIGHT, - omitted: true - }), - [Format.ALIGN_CENTER]: Decoration.mark({ - class: "cm-align-mark-center", - type: Format.ALIGN_CENTER, - omitted: true - }), - [Format.ALIGN_JUSTIFY]: Decoration.mark({ - class: "cm-align-mark-justify", - type: Format.ALIGN_JUSTIFY, - omitted: true - }), -}; - -export const DelimDeco = { - [Format.INSERTION]: Decoration.mark({ - class: "cm-delim cm-ins", - type: Format.INSERTION, - omitted: true - }), - [Format.SPOILER]: Decoration.mark({ - class: "cm-delim cm-spoiler", - type: Format.SPOILER, - omitted: true - }), - [Format.SUPERSCRIPT]: Decoration.mark({ - class: "cm-delim cm-sup", - type: Format.SUPERSCRIPT, - omitted: true - }), - [Format.SUBSCRIPT]: Decoration.mark({ - class: "cm-delim cm-sub", - type: Format.SUBSCRIPT, - omitted: true - }), - [Format.HIGHLIGHT]: Decoration.mark({ - class: "cm-highlight", - type: Format.HIGHLIGHT, - omitted: true - }) -}; - -export const ContentDeco = { - [Format.INSERTION]: Decoration.mark({ - class: "cm-ins", - type: Format.INSERTION, - inclusive: true - }), - [Format.SPOILER]: Decoration.mark({ - class: "cm-spoiler", - type: Format.SPOILER, - inclusive: true - }), - [Format.SUPERSCRIPT]: Decoration.mark({ - class: "cm-sup", - type: Format.SUPERSCRIPT, - inclusive: true - }), - [Format.SUBSCRIPT]: Decoration.mark({ - class: "cm-sub", - type: Format.SUBSCRIPT, - inclusive: true - }), - [Format.HIGHLIGHT]: Decoration.mark({ - class: "cm-highlight", - type: Format.HIGHLIGHT, - inclusive: true - }) -}; - -export const ColorTagDeco = Decoration.mark({ - class: "cm-color-tag", - type: Format.COLOR_TAG, - omitted: true -}); - -export const RevealedSpoiler = Decoration.mark({ +export const REVEALED_SPOILER_DECO = Decoration.mark({ class: "cm-spoiler-revealed", - type: Format.SPOILER }); \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/createHlDeco.ts b/src/editor-mode/decorator/utils/createHlDeco.ts index b1ddf57..e2a1407 100644 --- a/src/editor-mode/decorator/utils/createHlDeco.ts +++ b/src/editor-mode/decorator/utils/createHlDeco.ts @@ -1,12 +1,12 @@ import { Decoration } from "@codemirror/view"; import { Format } from "src/enums"; -export function createHlDeco(color: string, data?: { [P in string]: unknown }) { +export function createHlDeco(color: string, data?: Record) { let spec: Parameters[0] = { class: "cm-custom-highlight cm-custom-highlight-" + (color || "default"), type: Format.HIGHLIGHT, color, - inclusive: true + inclusive: false }; if (data) { for (let prop in data) { spec[prop] = data[prop] } diff --git a/src/editor-mode/decorator/utils/createInlineDecoRange.ts b/src/editor-mode/decorator/utils/createInlineDecoRange.ts new file mode 100644 index 0000000..844fed2 --- /dev/null +++ b/src/editor-mode/decorator/utils/createInlineDecoRange.ts @@ -0,0 +1,8 @@ +import { Decoration } from "@codemirror/view"; +import { Token, TokenDecoration } from "src/types"; + +export function createInlineDecoRange(token: Token, cls: string) { + return (Decoration + .mark({ class: cls, token }) as TokenDecoration) + .range(token.from, token.to); +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/createLineDecoRange.ts b/src/editor-mode/decorator/utils/createLineDecoRange.ts new file mode 100644 index 0000000..82c027c --- /dev/null +++ b/src/editor-mode/decorator/utils/createLineDecoRange.ts @@ -0,0 +1,9 @@ +import { Line } from "@codemirror/state"; +import { Decoration } from "@codemirror/view"; +import { Token, TokenDecoration } from "src/types"; + +export function createLineDecoRange(token: Token, cls: string, line: Line) { + return (Decoration + .line({ class: cls, token }) as TokenDecoration) + .range(line.from); +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/getLineAt.ts b/src/editor-mode/decorator/utils/getLineAt.ts new file mode 100644 index 0000000..b75ccc3 --- /dev/null +++ b/src/editor-mode/decorator/utils/getLineAt.ts @@ -0,0 +1,18 @@ +import { IndexCache } from "src/types"; +import { Text } from "@codemirror/state" + +export function getLineAt(doc: Text, offset: number, linePosCache: IndexCache) { + if (linePosCache.number > doc.lines) { linePosCache.number = doc.lines } + let curLine = doc.line(linePosCache.number); + if (offset < curLine.from) { + do { + curLine = doc.line(curLine.number - 1); + } while (offset < curLine.from) + } else if (offset > curLine.to) { + do { + curLine = doc.line(curLine.number + 1); + } while (offset > curLine.to) + } + linePosCache.number = curLine.number; + return curLine; +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/getTagRange.ts b/src/editor-mode/decorator/utils/getTagRange.ts new file mode 100644 index 0000000..7c1acd1 --- /dev/null +++ b/src/editor-mode/decorator/utils/getTagRange.ts @@ -0,0 +1,7 @@ +import { Token } from "src/types"; + +export function getTagRange(token: Token) { + let from = token.from + token.openLen, + to = from + token.tagLen; + return { from, to }; +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/index.ts b/src/editor-mode/decorator/utils/index.ts index 659b103..8778999 100644 --- a/src/editor-mode/decorator/utils/index.ts +++ b/src/editor-mode/decorator/utils/index.ts @@ -1 +1,10 @@ -export * from "./createHlDeco"; \ No newline at end of file +export * from "./createHlDeco"; +export * from "./createInlineDecoRange"; +export * from "./getLineAt"; +export * from "./sliceStrFromLine"; +export * from "./getTagRange"; +export * from "./createLineDecoRange"; +export * from "./iterLine"; +export * from "./iterTokenGroup"; +export * from "./moveTokenIndexCache"; +export * from "./isEditorModeChanged"; \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/isEditorModeChanged.ts b/src/editor-mode/decorator/utils/isEditorModeChanged.ts new file mode 100644 index 0000000..d7b2c67 --- /dev/null +++ b/src/editor-mode/decorator/utils/isEditorModeChanged.ts @@ -0,0 +1,8 @@ +import { EditorState } from "@codemirror/state"; +import { editorLivePreviewField } from "obsidian"; + +export function isEditorModeChanged(curState: EditorState, prevState: EditorState) { + let isLivePreviewCurrently = curState.field(editorLivePreviewField), + isLivePreviewPreviously = prevState.field(editorLivePreviewField); + return isLivePreviewCurrently != isLivePreviewPreviously; +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/iterLine.ts b/src/editor-mode/decorator/utils/iterLine.ts new file mode 100644 index 0000000..514b702 --- /dev/null +++ b/src/editor-mode/decorator/utils/iterLine.ts @@ -0,0 +1,8 @@ +import { IterLineSpec } from "src/types"; + +export function iterLine(spec: IterLineSpec) { + for (let i = spec.fromLn; i <= (spec.toLn ?? spec.doc.lines); i++) { + let curLine = spec.doc.line(i); + if (spec.callback(curLine, spec.doc) === false) { break } + } +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/iterTokenGroup.ts b/src/editor-mode/decorator/utils/iterTokenGroup.ts new file mode 100644 index 0000000..34879b9 --- /dev/null +++ b/src/editor-mode/decorator/utils/iterTokenGroup.ts @@ -0,0 +1,17 @@ +import { IterTokenGroupSpec } from "src/types"; +import { moveTokenIndexCache } from "src/editor-mode/decorator/utils"; + +export function iterTokenGroup(spec: IterTokenGroupSpec) { + let { tokens, ranges, callback, indexCache } = spec; + moveTokenIndexCache(tokens, ranges[0]?.from ?? 0, indexCache); + for ( + let i = indexCache.number, j = 0; + i < tokens.length && j < ranges.length; + ) { + if (ranges[j].to <= tokens[i].from) { j++; continue } + if (tokens[i].from < ranges[j].to && tokens[i].to > ranges[j].from) { + callback(tokens[i]); + } + i++; + } +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/moveTokenIndexCache.ts b/src/editor-mode/decorator/utils/moveTokenIndexCache.ts new file mode 100644 index 0000000..257cd71 --- /dev/null +++ b/src/editor-mode/decorator/utils/moveTokenIndexCache.ts @@ -0,0 +1,23 @@ +import { IndexCache, TokenGroup } from "src/types"; + +export function moveTokenIndexCache(tokens: TokenGroup, offset: number, indexCache: IndexCache) { + if (tokens.length == 0) { + indexCache.number = 0; + return; + } + if (indexCache.number >= tokens.length) { + indexCache.number = tokens.length - 1; + } + let curIndex = indexCache.number, + curToken = tokens[curIndex]; + if (offset < curToken.from && curIndex != 0) { + do { + curToken = tokens[--curIndex]; + } while (offset < curToken.from && curIndex != 0) + } else if (offset > curToken.to && curIndex != tokens.length - 1) { + do { + curToken = tokens[++curIndex]; + } while (offset > curToken.to && curIndex != tokens.length - 1) + } + indexCache.number = curIndex; +} \ No newline at end of file diff --git a/src/editor-mode/decorator/utils/sliceStrFromLine.ts b/src/editor-mode/decorator/utils/sliceStrFromLine.ts new file mode 100644 index 0000000..5ffd438 --- /dev/null +++ b/src/editor-mode/decorator/utils/sliceStrFromLine.ts @@ -0,0 +1,7 @@ +import { Line } from "@codemirror/state"; + +export function sliceStrFromLine(line: Line, from: number, to: number) { + from -= line.from; + to -= line.from; + return line.text.slice(from, to); +} \ No newline at end of file diff --git a/src/editor-mode/decorator/widgets/ColorButton.ts b/src/editor-mode/decorator/widgets/ColorButton.ts index 38ffa3f..45a58f0 100644 --- a/src/editor-mode/decorator/widgets/ColorButton.ts +++ b/src/editor-mode/decorator/widgets/ColorButton.ts @@ -1,21 +1,8 @@ import { WidgetType, EditorView, Decoration } from "@codemirror/view" -import { MarkdownView, Menu } from "obsidian"; -import { appFacet } from "src/editor-mode/facets"; -import { CharPos } from "src/types"; - -// first is the text in the color tag, second is displayed text on menu -const PREDEFINED_COLORS = [ - ["red", "Red"], - ["orange", "Orange"], - ["yellow", "Yellow"], - ["green", "Green"], - ["cyan", "Cyan"], - ["blue", "Blue"], - ["purple", "Purple"], - ["pink", "Pink"], - ["accent", "Accent"], - ["default", "Default"], -]; +import { Menu } from "obsidian"; +import { appFacet, settingsFacet } from "src/editor-mode/facets"; +import { getActiveCanvasNodeCoords } from "src/editor-mode/utils"; +import { PlainRange, Token } from "src/types"; /** * These code snippets are taken from @@ -24,98 +11,134 @@ const PREDEFINED_COLORS = [ */ export class ColorButton extends WidgetType { menu: Menu; - colorTag: CharPos; - open: CharPos; - close: CharPos; - constructor(color: string, open: CharPos, close: CharPos) { + tagRange: PlainRange; + openRange: PlainRange; + closeRange: PlainRange; + constructor(token: Token) { super(); - this.open = open; - this.close = close; - this.colorTag = color ? - { from: open.to, to: open.to + color.length + 2 } : - { from: open.to, to: open.to }; + this.openRange = { from: token.from, to: token.from + token.openLen }; + this.tagRange = { from: this.openRange.to, to: this.openRange.to + token.tagLen }; + this.closeRange = { from: token.to - token.closeLen, to: token.to }; + } + get openLen() { + return this.openRange.to - this.openRange.from; + } + get closeLen() { + return this.closeRange.to - this.closeRange.from; + } + get tagLen() { + return this.tagRange.to - this.tagRange.from; } eq(other: ColorButton) { return ( - other.colorTag.from == this.colorTag.from && - other.colorTag.to == this.colorTag.to - ); + other.tagRange.from == this.tagRange.from && + other.tagRange.to == this.tagRange.to + ); } toDOM(view: EditorView): HTMLElement { - let btn = document.createElement("span"); + let btn = document.createElement("span"), + settings = view.state.facet(settingsFacet), + colorConfigs = settings.colorConfigs; btn.setAttribute("aria-hidden", "true"); btn.className = "cm-highlight-color-btn"; - btn.onclick = (evt) => { - view.dispatch({ - selection: { - anchor: this.colorTag.from, - head: this.colorTag.to - } - }); - this.menu = new Menu(); + btn.onclick = () => { + view.dispatch({ + selection: { + anchor: this.tagRange.from, + head: this.tagRange.to + } + }); + this.menu = new Menu(); this.menu.dom.addClass("highlight-colors-modal"); - PREDEFINED_COLORS.forEach((color) => { - this.menu.addItem((item) => { - item.setTitle(color[1]); - item.setIcon("palette"); - item.dom.addClass(`menu-item-${color[0] || "default"}`); - item.onClick(() => { this.changeColor(view, color[0]) }); - }); - }); - this.menu.addItem(item => { - item.setTitle("Remove"); - item.setIcon("eraser"); - item.dom.addClass("menu-item-remove-highlight"); - item.onClick(() => { - view.dispatch({ - changes: [{ - from: this.open.from, - to: this.colorTag.to, - insert: "" - }, { - from: this.close.from, - to: this.close.to, - insert: "" - }] - }); - }); - }); - let app = view.state.facet(appFacet.reader), - menuCoord = { x: evt.clientX, y: evt.clientY + 10 }; - if (app.workspace.getMostRecentLeaf()?.view.getViewType() == "canvas") { - let containerEl = (app.workspace.activeEditor as MarkdownView)?.containerEl; - if (containerEl) { - let containerCoord = containerEl.getBoundingClientRect(); - menuCoord.x += containerCoord.x; - menuCoord.y += containerCoord.y; - } - } - this.menu.showAtPosition(menuCoord); - } + colorConfigs.forEach(({ name, tag, showInMenu }) => { + if (!showInMenu) { return } + this.addItem(name, "palette", "menu-item-" + tag, () => { this.changeColor(view, tag) }); + }); + this.addItem("Accent", "palette", "menu-item-accent", () => { this.changeColor(view, "accent") }); + this.addItem("Default", "palette", "menu-item-default", () => { this.toDefault(view) }); + this.addItem("Remove", "eraser", "menu-item-remove-highlight", () => { this.removeColor(view) }); + this.showMenu(view); + } return btn; } - adjustPos(length: number) { - this.colorTag.to += length; - this.close.from += length; - this.close.to += length; - } - changeColor(view: EditorView, color: string) { - view.dispatch({ - changes: { - from: this.colorTag.from, - to: this.colorTag.to, - insert: color != "default" ? `{${color}}` : "" - } - }); - let differ = color == "default" ? - this.colorTag.from - this.colorTag.to : - color.length + 2 - (this.colorTag.to - this.colorTag.from); - this.adjustPos(differ); - } + showMenu(view: EditorView) { + let charPos = this.tagRange.from, + menu = this.menu; + view.requestMeasure({ + read(view) { + let app = view.state.facet(appFacet.reader), + canvasNodeCoords = getActiveCanvasNodeCoords(app), + charCoords = view.coordsForChar(charPos); + return { charCoords, canvasNodeCoords }; + }, + write(measure) { + let { charCoords, canvasNodeCoords } = measure; + if (charCoords) { + let menuCoords = { x: charCoords.left, y: charCoords.bottom }; + if (canvasNodeCoords) { + menuCoords.x += canvasNodeCoords.x; + menuCoords.y += canvasNodeCoords.y; + } + menu.showAtPosition(menuCoords); + } + } + }); + } + adjustPos(differ: number) { + this.tagRange.to += differ; + this.closeRange.from += differ; + this.closeRange.to += differ; + } + changeColor(view: EditorView, color: string) { + view.dispatch({ + changes: { + from: this.tagRange.from, + to: this.tagRange.to, + insert: `{${color}}` + } + }); + let differ = color.length + 2 - (this.tagRange.to - this.tagRange.from); + this.adjustPos(differ); + } + removeColor(view: EditorView) { + view.dispatch({ + changes: [{ + from: this.openRange.from, + to: this.tagRange.to, + insert: "" + }, { + from: this.closeRange.from, + to: this.closeRange.to, + insert: "" + }] + }); + } + toDefault(view: EditorView) { + view.dispatch({ + changes: { + from: this.tagRange.from, + to: this.tagRange.to, + insert: "" + } + }); + let tagLen = this.tagLen; + this.adjustPos(-tagLen); + } + addItem(title: string, icon: string, cls: string, callback: (evt: MouseEvent | KeyboardEvent) => unknown) { + this.menu.addItem(item => { + item.setTitle(title); + item.setIcon(icon); + item.dom.addClass(cls); + item.onClick(callback); + }); + } ignoreEvent() { return false; } - static of(color: string, open: CharPos, close: CharPos) { - return Decoration.widget({ widget: new ColorButton(color, open, close) }).range(open.to); - } + static of(hlToken: Token) { + let btnOffset = hlToken.from + hlToken.openLen; + return Decoration + .widget({ widget: new ColorButton(hlToken), side: 1 }) + .range(btnOffset); + } } \ No newline at end of file diff --git a/src/editor-mode/decorator/widgets/HiddenWidget.ts b/src/editor-mode/decorator/widgets/HiddenWidget.ts index fc5da33..692cdbf 100644 --- a/src/editor-mode/decorator/widgets/HiddenWidget.ts +++ b/src/editor-mode/decorator/widgets/HiddenWidget.ts @@ -1,20 +1,23 @@ import { Decoration, WidgetType } from "@codemirror/view"; +import { Token } from "src/types"; export class HiddenWidget extends WidgetType { - replaced: Decoration; - constructor(replaced: Decoration) { + token: Token; + constructor(replaced: Token) { super(); - this.replaced = replaced; + this.token = replaced; } eq(other: HiddenWidget) { - return other.replaced == this.replaced; + return other.token == this.token; } toDOM(): HTMLElement { return document.createElement("span"); } - static of(from: number, to: number, replaced: Decoration) { + static of(from: number, to: number, token: Token, isBlock = false) { return Decoration.replace({ - widget: new HiddenWidget(replaced) + widget: new HiddenWidget(token), + block: isBlock, + inclusiveEnd: false }).range(from, to); } } \ No newline at end of file diff --git a/src/editor-mode/decorator/widgets/LineBreak.ts b/src/editor-mode/decorator/widgets/LineBreak.ts new file mode 100644 index 0000000..ff7f75d --- /dev/null +++ b/src/editor-mode/decorator/widgets/LineBreak.ts @@ -0,0 +1,20 @@ +import { Decoration, WidgetType } from "@codemirror/view"; + +export class LineBreak extends WidgetType { + offset: number; + constructor(offset: number) { + super(); + this.offset = offset; + } + eq(other: LineBreak) { + return other.offset === this.offset; + } + toDOM(): HTMLElement { + return document.createElement("br"); + } + static of(offset: number) { + return Decoration.replace({ + widget: new LineBreak(offset), + }).range(offset - 1, offset); + } +} \ No newline at end of file diff --git a/src/editor-mode/decorator/widgets/index.ts b/src/editor-mode/decorator/widgets/index.ts index 458fd8d..accfed6 100644 --- a/src/editor-mode/decorator/widgets/index.ts +++ b/src/editor-mode/decorator/widgets/index.ts @@ -1,2 +1,3 @@ export * from "./HiddenWidget"; -export * from "./ColorButton"; \ No newline at end of file +export * from "./ColorButton"; +export * from "./LineBreak"; \ No newline at end of file diff --git a/src/editor-mode/extensions/index.ts b/src/editor-mode/extensions/index.ts index 48f10ed..4dae2c2 100644 --- a/src/editor-mode/extensions/index.ts +++ b/src/editor-mode/extensions/index.ts @@ -1,6 +1,19 @@ -import { ViewPlugin } from "@codemirror/view"; -import { ExtendedSyntax } from "src/editor-mode/decorator/builder"; +import { Extension, RangeSet } from "@codemirror/state"; +import { EditorView, ViewPlugin } from "@codemirror/view"; +import { builderField, decoSetField, parserField, selectionObserverField } from "src/editor-mode/state-fields"; +import { BuilderPlugin } from "src/editor-mode/view-plugin"; -export const editorExtendedSyntax = ViewPlugin.fromClass(ExtendedSyntax, { - decorations: value => value.combinedSet, -}); \ No newline at end of file +const builderPlugin = ViewPlugin.fromClass(BuilderPlugin); + +export const editorExtendedSyntax: Extension = [ + parserField, + selectionObserverField, + builderField, + decoSetField, + builderPlugin, + EditorView.outerDecorations.of(view => view.plugin(builderPlugin)?.builder.holder.inlineSet ?? RangeSet.empty), + EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.blockSet ?? RangeSet.empty), + EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.inlineOmittedSet ?? RangeSet.empty), + EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.colorBtnSet ?? RangeSet.empty), + EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.revealedSpoilerSet ?? RangeSet.empty) +]; \ No newline at end of file diff --git a/src/editor-mode/utils/checkRefreshed.ts b/src/editor-mode/utils/checkRefreshed.ts new file mode 100644 index 0000000..9a69712 --- /dev/null +++ b/src/editor-mode/utils/checkRefreshed.ts @@ -0,0 +1,13 @@ +import { Transaction } from "@codemirror/state"; +import { refresherAnnot } from "src/editor-mode/annotations"; + +export function checkRefreshed(transactions: Transaction[] | readonly Transaction[] | Transaction) { + if (transactions instanceof Array) { + for (let i = 0; i < transactions.length; i++) { + if (transactions[i].annotation(refresherAnnot)) { return true } + } + } else { + if (transactions.annotation(refresherAnnot)) { return true } + } + return false; +} \ No newline at end of file diff --git a/src/editor-mode/utils/getActiveCanvasEditor.ts b/src/editor-mode/utils/getActiveCanvasEditor.ts new file mode 100644 index 0000000..2431271 --- /dev/null +++ b/src/editor-mode/utils/getActiveCanvasEditor.ts @@ -0,0 +1,9 @@ +import { App } from "obsidian"; +import { isCanvas } from "src/editor-mode/utils"; + +export function getActiveCanvasEditor(app: App) { + if (isCanvas(app)) { + return app.workspace.activeEditor; + } + return null; +} \ No newline at end of file diff --git a/src/editor-mode/utils/getActiveCanvasNodeCoords.ts b/src/editor-mode/utils/getActiveCanvasNodeCoords.ts new file mode 100644 index 0000000..6b47744 --- /dev/null +++ b/src/editor-mode/utils/getActiveCanvasNodeCoords.ts @@ -0,0 +1,11 @@ +import { App, MarkdownView } from "obsidian"; +import { getActiveCanvasEditor } from "./getActiveCanvasEditor"; + +export function getActiveCanvasNodeCoords(app: App) { + let canvasEditor = getActiveCanvasEditor(app), + containerEl = (canvasEditor as MarkdownView)?.containerEl; + if (containerEl) { + return containerEl.getBoundingClientRect(); + } + return null; +} \ No newline at end of file diff --git a/src/editor-mode/utils/index.ts b/src/editor-mode/utils/index.ts new file mode 100644 index 0000000..e222876 --- /dev/null +++ b/src/editor-mode/utils/index.ts @@ -0,0 +1,4 @@ +export * from "./checkRefreshed"; +export * from "./getActiveCanvasNodeCoords"; +export * from "./getActiveCanvasEditor"; +export * from "./isCanvas"; \ No newline at end of file diff --git a/src/editor-mode/utils/isCanvas.ts b/src/editor-mode/utils/isCanvas.ts new file mode 100644 index 0000000..2021cd3 --- /dev/null +++ b/src/editor-mode/utils/isCanvas.ts @@ -0,0 +1,5 @@ +import { App } from "obsidian"; + +export function isCanvas(app: App) { + return app.workspace.getMostRecentLeaf()?.view.getViewType() == "canvas"; +} \ No newline at end of file diff --git a/src/editor-mode/view-plugin/BuilderPlugin.ts b/src/editor-mode/view-plugin/BuilderPlugin.ts new file mode 100644 index 0000000..7c1b773 --- /dev/null +++ b/src/editor-mode/view-plugin/BuilderPlugin.ts @@ -0,0 +1,15 @@ +import { EditorView, PluginValue, ViewUpdate } from "@codemirror/view"; +import { DecorationBuilder } from "src/editor-mode/decorator/builder"; +import { builderField } from "src/editor-mode/state-fields"; + +export class BuilderPlugin implements PluginValue { + builder: DecorationBuilder; + constructor(view: EditorView) { + let state = view.state; + this.builder = state.field(builderField); + this.builder.onViewInit(view); + } + update(update: ViewUpdate) { + this.builder.onViewUpdate(update); + } +} \ No newline at end of file diff --git a/src/editor-mode/view-plugin/index.ts b/src/editor-mode/view-plugin/index.ts new file mode 100644 index 0000000..fa4c684 --- /dev/null +++ b/src/editor-mode/view-plugin/index.ts @@ -0,0 +1 @@ +export * from "./BuilderPlugin"; \ No newline at end of file