From 3c52eceeb3df2a2f9969ed77513b1de9055211f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20James?= Date: Tue, 2 Jan 2024 08:19:31 +0100 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=8E=A8=20Use=20icons=20in=20the=20too?= =?UTF-8?q?lbar=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeMirror6/Models/CodeMirrorSetup.cs | 2 +- CodeMirror6/NodeLib/src/CmHtml.ts | 4 +-- Examples.BlazorServer/Pages/_Host.cshtml | 1 + Examples.BlazorWasm/wwwroot/index.html | 1 + Examples.Common/Example.razor | 42 ++++++++++++------------ Examples.Common/Example.razor.css | 3 ++ 6 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 Examples.Common/Example.razor.css diff --git a/CodeMirror6/Models/CodeMirrorSetup.cs b/CodeMirror6/Models/CodeMirrorSetup.cs index 0c823ea9..469fab19 100644 --- a/CodeMirror6/Models/CodeMirrorSetup.cs +++ b/CodeMirror6/Models/CodeMirrorSetup.cs @@ -106,7 +106,7 @@ public CodeMirrorSetup() [JsonPropertyName("previewImages")] public bool PreviewImages { get; init; } = true; /// - /// Whether to enable mentions + /// Whether to enable mentions. /// [JsonPropertyName("allowMentions")] public bool AllowMentions { get; init; } = true; } diff --git a/CodeMirror6/NodeLib/src/CmHtml.ts b/CodeMirror6/NodeLib/src/CmHtml.ts index 903563da..c47d51a2 100644 --- a/CodeMirror6/NodeLib/src/CmHtml.ts +++ b/CodeMirror6/NodeLib/src/CmHtml.ts @@ -67,8 +67,8 @@ export const viewInlineHtmlExtension = (enabled: boolean = true): Extension => { create(state) { return decorate(state) }, - update(_references, { state }) { - return decorate(state) + update(_references, transaction) { + return decorate(transaction.state) }, provide(field) { return EditorView.decorations.from(field) diff --git a/Examples.BlazorServer/Pages/_Host.cshtml b/Examples.BlazorServer/Pages/_Host.cshtml index 248fa208..e99970ee 100644 --- a/Examples.BlazorServer/Pages/_Host.cshtml +++ b/Examples.BlazorServer/Pages/_Host.cshtml @@ -12,6 +12,7 @@ + diff --git a/Examples.BlazorWasm/wwwroot/index.html b/Examples.BlazorWasm/wwwroot/index.html index 7a3022b7..568ae2ad 100644 --- a/Examples.BlazorWasm/wwwroot/index.html +++ b/Examples.BlazorWasm/wwwroot/index.html @@ -10,6 +10,7 @@ + diff --git a/Examples.Common/Example.razor b/Examples.Common/Example.razor index 225936e4..d0a31136 100644 --- a/Examples.Common/Example.razor +++ b/Examples.Common/Example.razor @@ -44,54 +44,54 @@ style="max-width: 100%; max-height: 60em; " > -
+
@for (var i = 1; i <= 6; i++) { var headingLevel = i; // Capture the current value of i in a local variable - } - + - -
@@ -200,7 +200,7 @@ Result: private string ButtonClass(CodeMirrorState state, string docStyleTag) => ButtonClass(state.MarkdownStylesAtSelections?.Contains(docStyleTag) == true); private string ButtonClass(bool enabled) => enabled ? "btn btn-primary" - : "btn"; + : "btn btn-outline-secondary"; private async Task ToggleEmojis(CMCommands commands) { diff --git a/Examples.Common/Example.razor.css b/Examples.Common/Example.razor.css new file mode 100644 index 00000000..7b60a2c9 --- /dev/null +++ b/Examples.Common/Example.razor.css @@ -0,0 +1,3 @@ +.sticky-top { + padding-top: 10px; +} From cab367c04efc73b1dc22381d39f4515b0e472220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20James?= Date: Tue, 2 Jan 2024 09:54:39 +0100 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Try=20early=20initiali?= =?UTF-8?q?zation=20for=20Blazor=20WASM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeMirror6/CodeMirror6Wrapper.razor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CodeMirror6/CodeMirror6Wrapper.razor.cs b/CodeMirror6/CodeMirror6Wrapper.razor.cs index 29a56fff..d29e6c44 100644 --- a/CodeMirror6/CodeMirror6Wrapper.razor.cs +++ b/CodeMirror6/CodeMirror6Wrapper.razor.cs @@ -231,7 +231,7 @@ [JSInvokable] public async Task> LintingRequestedFrom /// /// Life-cycle method invoked when the component is initialized. /// - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { Config = new( Doc, @@ -245,6 +245,11 @@ protected override void OnInitialized() AutoFormatMarkdown, ReplaceEmojiCodes ); + try { + await OnAfterRenderAsync(true); // try early initialization for Blazor WASM + } + catch (Exception) { + } } /// From 500b002e4ae68afdd6838576ca69f0ef256f4be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20James?= Date: Tue, 2 Jan 2024 10:03:26 +0100 Subject: [PATCH 3/5] =?UTF-8?q?=E2=9C=A8=20Implement=20ForceRedraw(),=20us?= =?UTF-8?q?e=20it=20after=20late=20update=20of=20available=20mention=20com?= =?UTF-8?q?pletions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeMirror6/CodeMirrorJsInterop.cs | 4 ++++ CodeMirror6/NodeLib/src/index.ts | 12 ++++++++++-- Examples.Common/Example.razor | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CodeMirror6/CodeMirrorJsInterop.cs b/CodeMirror6/CodeMirrorJsInterop.cs index a82f4726..b59494b9 100644 --- a/CodeMirror6/CodeMirrorJsInterop.cs +++ b/CodeMirror6/CodeMirrorJsInterop.cs @@ -368,6 +368,10 @@ internal Task SetMentionCompletions(List mentionCompletion "setMentionCompletions", mentionCompletions ); + + internal Task ForceRedraw() => cmJsInterop.ModuleInvokeVoidAsync( + "forceRedraw" + ); } /// diff --git a/CodeMirror6/NodeLib/src/index.ts b/CodeMirror6/NodeLib/src/index.ts index 7ce7f631..8042ed92 100644 --- a/CodeMirror6/NodeLib/src/index.ts +++ b/CodeMirror6/NodeLib/src/index.ts @@ -3,7 +3,7 @@ import { rectangularSelection, crosshairCursor, ViewUpdate, lineNumbers, highlightActiveLineGutter, placeholder } from "@codemirror/view" -import { EditorState } from "@codemirror/state" +import { EditorState, SelectionRange } from "@codemirror/state" import { indentWithTab, history, historyKeymap, cursorSyntaxLeft, selectSyntaxLeft, selectSyntaxRight, cursorSyntaxRight, deleteLine, @@ -19,7 +19,6 @@ import { import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, Completion } from "@codemirror/autocomplete" import { searchKeymap, highlightSelectionMatches } from "@codemirror/search" import { linter, lintKeymap } from "@codemirror/lint" - import { CmInstance, CMInstances } from "./CmInstance" import { CmConfiguration } from "./CmConfiguration" import { getDynamicHeaderStyling } from "./CmDynamicMarkdownHeaderStyling" @@ -234,6 +233,15 @@ export function setLanguage(id: string, languageName: string) { export function setMentionCompletions(id: string, mentionCompletions: Completion[]) { setCachedCompletions(mentionCompletions) + forceRedraw(id) +} + +export function forceRedraw(id: string) { + const view = CMInstances[id].view + const changes = view.state.changeByRange((range: SelectionRange) => { + return { range } + }) + view.dispatch(view.state.update(changes)) } export function setAutoFormatMarkdown(id: string, autoFormatMarkdown: boolean) { diff --git a/Examples.Common/Example.razor b/Examples.Common/Example.razor index d0a31136..d35968fd 100644 --- a/Examples.Common/Example.razor +++ b/Examples.Common/Example.razor @@ -232,6 +232,7 @@ Result: private static async Task> GetMentionCompletions() { + await Task.Delay(1000); return await Task.FromResult>( [ new CodeMirrorCompletion { From c7e342cec717b10149eac3cf7404c46c3e516328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20James?= Date: Tue, 2 Jan 2024 10:39:21 +0100 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20Allow=20clicking=20inside=20?= =?UTF-8?q?a=20rendered=20html=20span=20to=20edit=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeMirror6/NodeLib/src/CmHtml.ts | 117 ++++++++++++------------------ CodeMirror6/NodeLib/src/index.ts | 6 +- 2 files changed, 51 insertions(+), 72 deletions(-) diff --git a/CodeMirror6/NodeLib/src/CmHtml.ts b/CodeMirror6/NodeLib/src/CmHtml.ts index c47d51a2..71f60b65 100644 --- a/CodeMirror6/NodeLib/src/CmHtml.ts +++ b/CodeMirror6/NodeLib/src/CmHtml.ts @@ -1,82 +1,61 @@ import { syntaxTree } from '@codemirror/language' -import { RangeSet, StateField } from '@codemirror/state' +import { RangeSet, StateField, RangeSetBuilder } from '@codemirror/state' import { Decoration, EditorView, ViewPlugin } from '@codemirror/view' import type { EditorState, Extension, Range } from '@codemirror/state' import type { DecorationSet } from '@codemirror/view' +import { markdownLanguage } from "@codemirror/lang-markdown" import { buildWidget } from './lib/codemirror-kit' -import { isCursorInRange } from './CmHelpers' - -const htmlWidget = (content: string) => buildWidget({ - eq: () => false, - toDOM: () => { - const container = document.createElement('span'); - container.innerHTML = content; // Insert HTML content - return container; - }, -}) - -export const viewInlineHtmlExtension = (enabled: boolean = true): Extension => { - if (!enabled) - return [] - - const htmlDecoration = (content: string) => Decoration.replace({ - widget: htmlWidget(content), +import { isCursorInRange, isInCodeBlock } from './CmHelpers' + + +function createHtmlDecorationWidget(content: string) { + return Decoration.replace({ + widget: buildWidget({ + eq: (other) => other.content === content, + toDOM: () => { + const container = document.createElement('span') + container.innerHTML = content + return container + }, + ignoreEvent: () => false, + content: content + }), }) +} - const decorate = (state: EditorState) => { - const widgets: Range[] = []; - - if (enabled) { - let foundClosingTag = true - let htmlCode = '' - let paragraph = '' - let paragraphFrom = 0 - let paragraphTo = 0 - syntaxTree(state).iterate({ - enter: ({ type, from, to }) => { - const text = state.sliceDoc(from, to) - if (type.name === 'Paragraph') { - paragraph = text - paragraphFrom = from - paragraphTo = to - } - else if (type.name === 'HTMLTag') { - foundClosingTag = !foundClosingTag - htmlCode += text - if (htmlCode !== '' && paragraph !== '' && foundClosingTag) { - if (!isCursorInRange(state, paragraphFrom, paragraphTo)) { - widgets.push(htmlDecoration(paragraph).range(paragraphFrom, paragraphTo)) +export function htmlViewPlugin(enabled: boolean): Extension { + if (!enabled) return [] + return ViewPlugin.define((view: EditorView) => { + return { + update: () => { + const builder = new RangeSetBuilder() + for (const { from, to } of view.visibleRanges) { + const text = view.state.doc.sliceString(from, to) + + if (markdownLanguage.isActiveAt(view.state, from)) { + // recognize html spans (...) and decorate them + const spanRegex = /]*>([^<]*)<\/span>/g + let match + while ((match = spanRegex.exec(text)) !== null) { + const start = from + match.index + const end = start + match[0].length + if (!isCursorInRange(view.state, start, end)) { + const isCode = isInCodeBlock(view.state, start) + if (!isCode) { + const spanText = match[1] + if (!spanText || spanText === "") continue + const widget = createHtmlDecorationWidget(match[0]) + builder.add(start, end, widget) + } } - htmlCode = '' - paragraph = '' } } - else { - if (!foundClosingTag) htmlCode += text - } - }, - }) + } + return builder.finish(); + }, } - - return widgets.length > 0 ? RangeSet.of(widgets) : Decoration.none; - } - - const viewPlugin = ViewPlugin.define(() => ({}), {}) - - const stateField = StateField.define({ - create(state) { - return decorate(state) - }, - update(_references, transaction) { - return decorate(transaction.state) - }, - provide(field) { - return EditorView.decorations.from(field) - }, + }, + { + decorations: plugin => plugin.update() }) - - return [ - viewPlugin, - stateField, - ] } diff --git a/CodeMirror6/NodeLib/src/index.ts b/CodeMirror6/NodeLib/src/index.ts index 8042ed92..31faa99b 100644 --- a/CodeMirror6/NodeLib/src/index.ts +++ b/CodeMirror6/NodeLib/src/index.ts @@ -46,7 +46,7 @@ import { mentionDecorationExtension } from "./CmMentionsView" import { viewEmojiExtension } from "./CmEmojiView" import { emojiCompletionExtension } from "./CmEmojiCompletion" import { indentationMarkers } from '@replit/codemirror-indentation-markers' -import { viewInlineHtmlExtension } from "./CmHtml" +import { htmlViewPlugin } from "./CmHtml" /** * Initialize a new CodeMirror instance @@ -81,7 +81,7 @@ export function initCodeMirror( listsExtension(initialConfig.autoFormatMarkdown), blockquote(), viewEmojiExtension(initialConfig.autoFormatMarkdown), - viewInlineHtmlExtension(initialConfig.autoFormatMarkdown), + htmlViewPlugin(initialConfig.autoFormatMarkdown), ]), CMInstances[id].tabSizeCompartment.of(EditorState.tabSize.of(initialConfig.tabSize)), CMInstances[id].indentUnitCompartment.of(indentUnit.of(" ".repeat(initialConfig.tabSize))), @@ -257,7 +257,7 @@ export function setAutoFormatMarkdown(id: string, autoFormatMarkdown: boolean) { listsExtension(autoFormatMarkdown), blockquote(), viewEmojiExtension(autoFormatMarkdown), - viewInlineHtmlExtension(autoFormatMarkdown), + htmlViewPlugin(autoFormatMarkdown), ]) }) } From a179744f04e86df2649f83316f31b61741a70718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20James?= Date: Tue, 2 Jan 2024 10:40:24 +0100 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=8E=A8=20Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CodeMirror6/NodeLib/src/CmHorizontalRule.ts | 28 ++++++++++----------- CodeMirror6/NodeLib/src/CmMentionsView.ts | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/CodeMirror6/NodeLib/src/CmHorizontalRule.ts b/CodeMirror6/NodeLib/src/CmHorizontalRule.ts index 4a2a06e2..0d9a3f58 100644 --- a/CodeMirror6/NodeLib/src/CmHorizontalRule.ts +++ b/CodeMirror6/NodeLib/src/CmHorizontalRule.ts @@ -6,15 +6,6 @@ import type { DecorationSet } from '@codemirror/view' import { buildWidget } from './lib/codemirror-kit' import { isCursorInRange } from './CmHelpers' -const hrWidget = () => buildWidget({ - eq: () => false, - toDOM: () => { - const hr = document.createElement('hr'); - hr.setAttribute('aria-hidden', 'true'); - return hr; - }, -}) - /** * Return the horizontal rule Extension if the supplied parameter is true @@ -25,8 +16,16 @@ export const dynamicHrExtension = (enabled: boolean = true): Extension => { if (!enabled) return [] - const hrDecoration = () => Decoration.replace({ - widget: hrWidget(), + const createHRDecorationWidget = () => Decoration.replace({ + widget: buildWidget({ + eq: () => false, + toDOM: () => { + const hr = document.createElement('hr') + hr.setAttribute('aria-hidden', 'true') + return hr + }, + ignoreEvent: () => false, + }), }) const decorate = (state: EditorState) => { @@ -41,7 +40,7 @@ export const dynamicHrExtension = (enabled: boolean = true): Extension => { const hrRegex = /^-{3,}$/ if (hrRegex.test(lineText)) { - widgets.push(hrDecoration().range(line.from, line.to)) + widgets.push(createHRDecorationWidget().range(line.from, line.to)) } } }, @@ -52,12 +51,13 @@ export const dynamicHrExtension = (enabled: boolean = true): Extension => { } const viewPlugin = ViewPlugin.define(() => ({}), {}) + const stateField = StateField.define({ create(state) { return decorate(state) }, - update(_references, { state }) { - return decorate(state) + update(_references, transaction) { + return decorate(transaction.state) }, provide(field) { return EditorView.decorations.from(field) diff --git a/CodeMirror6/NodeLib/src/CmMentionsView.ts b/CodeMirror6/NodeLib/src/CmMentionsView.ts index 93ec22ca..67e0c727 100644 --- a/CodeMirror6/NodeLib/src/CmMentionsView.ts +++ b/CodeMirror6/NodeLib/src/CmMentionsView.ts @@ -1,4 +1,4 @@ -import { markdownLanguage } from "@codemirror/lang-markdown"; +import { markdownLanguage } from "@codemirror/lang-markdown" import { EditorState, RangeSetBuilder, Extension } from '@codemirror/state' import { EditorView, ViewPlugin, Decoration } from "@codemirror/view" import { buildWidget } from "./lib/codemirror-kit"