From a30304de2e67cd4873243a6c3df7340ff740e5cd Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 12 Sep 2025 13:37:19 +1000 Subject: [PATCH 1/5] `ct-code-editor` backlink support --- deno.lock | 2 + packages/patterns/chatbot-note.tsx | 2 +- packages/ui/deno.json | 3 +- .../ct-code-editor/ct-code-editor.ts | 260 +++++++++++++++++- .../v2/components/ct-code-editor/styles.ts | 18 ++ 5 files changed, 281 insertions(+), 4 deletions(-) diff --git a/deno.lock b/deno.lock index 6811a38e8..308164e67 100644 --- a/deno.lock +++ b/deno.lock @@ -67,6 +67,7 @@ "npm:@arizeai/openinference-semantic-conventions@^1.1.0": "1.1.0", "npm:@arizeai/openinference-vercel@^2.0.1": "2.3.1_@opentelemetry+api@1.9.0", "npm:@babel/standalone@^7.28.2": "7.28.2", + "npm:@codemirror/autocomplete@^6.15.0": "6.18.6", "npm:@codemirror/lang-css@^6.3.1": "6.3.1", "npm:@codemirror/lang-html@^6.4.9": "6.4.9", "npm:@codemirror/lang-javascript@^6.2.2": "6.2.4", @@ -2153,6 +2154,7 @@ }, "packages/ui": { "dependencies": [ + "npm:@codemirror/autocomplete@^6.15.0", "npm:@codemirror/lang-css@^6.3.1", "npm:@codemirror/lang-html@^6.4.9", "npm:@codemirror/lang-javascript@^6.2.2", diff --git a/packages/patterns/chatbot-note.tsx b/packages/patterns/chatbot-note.tsx index 77018fcd7..631b9612c 100644 --- a/packages/patterns/chatbot-note.tsx +++ b/packages/patterns/chatbot-note.tsx @@ -57,7 +57,7 @@ export const Note = recipe( diff --git a/packages/ui/deno.json b/packages/ui/deno.json index cc8d0e81e..f1035bd8a 100644 --- a/packages/ui/deno.json +++ b/packages/ui/deno.json @@ -22,6 +22,7 @@ "@codemirror/lang-css": "npm:@codemirror/lang-css@^6.3.1", "@codemirror/lang-html": "npm:@codemirror/lang-html@^6.4.9", "@codemirror/lang-json": "npm:@codemirror/lang-json@^6.0.1", - "@codemirror/theme-one-dark": "npm:@codemirror/theme-one-dark@^6.1.2" + "@codemirror/theme-one-dark": "npm:@codemirror/theme-one-dark@^6.1.2", + "@codemirror/autocomplete": "npm:@codemirror/autocomplete@^6.15.0" } } diff --git a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts index 9e5026604..c6f52aa0a 100644 --- a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts +++ b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts @@ -2,7 +2,7 @@ import { html, PropertyValues } from "lit"; import { BaseElement } from "../../core/base-element.ts"; import { styles } from "./styles.ts"; import { basicSetup } from "codemirror"; -import { EditorView, placeholder } from "@codemirror/view"; +import { EditorView, placeholder, keymap } from "@codemirror/view"; import { Compartment, EditorState, Extension } from "@codemirror/state"; import { LanguageSupport } from "@codemirror/language"; import { javascript as createJavaScript } from "@codemirror/lang-javascript"; @@ -11,9 +11,12 @@ import { css as createCss } from "@codemirror/lang-css"; import { html as createHtml } from "@codemirror/lang-html"; import { json as createJson } from "@codemirror/lang-json"; import { oneDark } from "@codemirror/theme-one-dark"; -import { type Cell } from "@commontools/runner"; +import { autocompletion, CompletionContext, CompletionResult, Completion } from "@codemirror/autocomplete"; +import { Decoration, DecorationSet, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view"; +import { type Cell, getEntityId, NAME } from "@commontools/runner"; import { type InputTimingOptions } from "../../core/input-timing-controller.ts"; import { createStringCellController } from "../../core/cell-controller.ts"; +import { Charm } from "@commontools/charm"; /** * Supported MIME types for syntax highlighting @@ -66,10 +69,12 @@ const getLangExtFromMimeType = (mime: MimeType) => { * @attr {string} placeholder - Placeholder text when empty * @attr {string} timingStrategy - Input timing strategy: "immediate" | "debounce" | "throttle" | "blur" * @attr {number} timingDelay - Delay in milliseconds for debounce/throttle (default: 500) + * @attr {Array} mentionable - Array of mentionable items with Charm structure for backlink autocomplete * * @fires ct-change - Fired when content changes with detail: { value, oldValue, language } * @fires ct-focus - Fired on focus * @fires ct-blur - Fired on blur + * @fires backlink-click - Fired when a backlink is clicked with Cmd/Ctrl+Enter with detail: { text, charm } * * @example * @@ -85,6 +90,7 @@ export class CTCodeEditor extends BaseElement { placeholder: { type: String }, timingStrategy: { type: String }, timingDelay: { type: Number }, + mentionable: { type: Array }, }; declare value: Cell | string; @@ -94,6 +100,7 @@ export class CTCodeEditor extends BaseElement { declare placeholder: string; declare timingStrategy: InputTimingOptions["strategy"]; declare timingDelay: number; + declare mentionable: Cell; private _editorView: EditorView | undefined; private _lang = new Compartment(); @@ -124,6 +131,244 @@ export class CTCodeEditor extends BaseElement { this.timingDelay = 500; } + /** + * Create a backlink completion source for [[backlinks]] + */ + private createBacklinkCompletionSource() { + return (context: CompletionContext): CompletionResult | null => { + console.log("Completion source called, context:", context.pos, context.explicit); + + // Look for incomplete backlinks: [[ followed by optional text + const backlink = context.matchBefore(/\[\[([^\]]*)?/); + console.log("Backlink match:", backlink); + + if (!backlink) { + // Also try a simpler pattern to debug + const simpleMatch = context.matchBefore(/\[\[/); + console.log("Simple [[ match:", simpleMatch); + return null; + } + + // Check what comes after the cursor + const afterCursor = context.state.doc.sliceString(context.pos, context.pos + 2); + console.log("After cursor:", afterCursor); + + // Allow completion inside existing backlinks - we'll replace the content between [[ and ]] + const query = backlink.text.slice(2); // Remove [[ prefix + + // Debug logging to see what's happening + console.log("Backlink completion triggered:", { query, mentionableExists: !!this.mentionable }); + + const mentionable = this.getFilteredMentionable(query); + console.log("Filtered mentionable items:", mentionable); + + if (mentionable.length === 0) return null; + + // Determine the completion range and apply text based on whether ]] exists + let applyText: string; + let completionTo: number; + + if (afterCursor === "]]") { + // We're inside existing backlinks like [[llm|]], just replace the content + applyText = (text: string) => text; + completionTo = context.pos; + } else { + // We're in incomplete backlinks like [[llm, add the closing ]] + applyText = (text: string) => text + "]]"; + completionTo = context.pos; + } + + const options: Completion[] = mentionable.map(charm => { + const charmIdObj = getEntityId(charm); + const charmId = charmIdObj["/"] || ""; + const charmName = charm[NAME] || ""; + const insertText = `${charmName} (${charmId})`; + return { + label: charmName, + apply: afterCursor === "]]" ? insertText : insertText + "]]", + type: "text", + info: "Backlink to " + charmName, + }; + }); + + console.log("Completion options:", options); + + return { + from: backlink.from + 2, // Start after [[ + to: afterCursor === "]]" ? context.pos : undefined, + options, + validFor: /^[^\]]*$/, + }; + }; + } + + /** + * Get filtered mentionable items based on query + */ + private getFilteredMentionable(query: string): Charm[] { + console.log("getFilteredMentionable called with query:", query); + + if (!this.mentionable) { + console.log("No mentionable property"); + return []; + } + + const mentionableData = this.mentionable.getAsQueryResult(); + console.log("Mentionable data:", mentionableData); + + if (!mentionableData || mentionableData.length === 0) { + console.log("No mentionable data or empty array"); + return []; + } + + const queryLower = query.toLowerCase(); + const matches = []; + + // Filter mentionable items by name matching query + for (let i = 0; i < mentionableData.length; i++) { + const mention = this.mentionable.key(i).getAsQueryResult(); + console.log(`Mention ${i}:`, mention, "NAME:", mention?.[NAME]); + if (mention && mention[NAME]?.toLowerCase()?.includes(queryLower)) { + matches.push(mention); + } + } + + console.log("Final matches:", matches); + return matches; + } + + /** + * Handle backlink clicks with Cmd/Ctrl+Click + */ + private createBacklinkClickHandler() { + return EditorView.domEventHandlers({ + click: (event, view) => { + if (event.ctrlKey || event.metaKey) { + return this.handleBacklinkActivation(view, event); + } + return false; + }, + }); + } + + /** + * Handle backlink activation (Cmd/Ctrl+Click on a backlink) + */ + private handleBacklinkActivation(view: EditorView, event?: MouseEvent): boolean { + const state = view.state; + const pos = state.selection.main.head; + const doc = state.doc; + + // Find backlinks around cursor position + const lineStart = doc.lineAt(pos).from; + const lineEnd = doc.lineAt(pos).to; + const lineText = doc.sliceString(lineStart, lineEnd); + + // Find all [[...]] patterns in the line + const backlinkRegex = /\[\[([^\]]+)\]\]/g; + let match; + + while ((match = backlinkRegex.exec(lineText)) !== null) { + const matchStart = lineStart + match.index; + const matchEnd = matchStart + match[0].length; + + // Check if cursor is within this backlink + if (pos >= matchStart && pos <= matchEnd) { + const backlinkText = match[1]; // This is "Name (id)" format + // Extract ID from "Name (id)" format + const idMatch = backlinkText.match(/\(([^)]+)\)$/); + const backlinkId = idMatch ? idMatch[1] : backlinkText; + const charm = this.findCharmById(backlinkId); + + if (charm) { + this.emit("backlink-click", { + id: backlinkId, + text: backlinkText, + charm: charm, + }); + return true; + } + } + } + + return false; + } + + /** + * Find a charm by ID in the mentionable list + */ + private findCharmById(id: string): Charm | null { + if (!this.mentionable) return null; + + const mentionableData = this.mentionable.getAsQueryResult(); + if (!mentionableData) return null; + + for (let i = 0; i < mentionableData.length; i++) { + const charm = this.mentionable.key(i).getAsQueryResult(); + if (charm) { + const charmIdObj = getEntityId(charm); + const charmId = charmIdObj["/"] || ""; + if (charmId === id) { + return charm; + } + } + } + + return null; + } + + /** + * Create a plugin to decorate backlinks with special styling + */ + private createBacklinkDecorationPlugin() { + const backlinkMark = Decoration.mark({ class: "cm-backlink" }); + + return ViewPlugin.fromClass(class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.getBacklinkDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.getBacklinkDecorations(update.view); + } + } + + getBacklinkDecorations(view: EditorView) { + const decorations: any[] = []; + const doc = view.state.doc; + const backlinkRegex = /\[\[([^\]]+)\]\]/g; + + for (const { from, to } of view.visibleRanges) { + for (let pos = from; pos <= to;) { + const line = doc.lineAt(pos); + const text = line.text; + let match; + + backlinkRegex.lastIndex = 0; // Reset regex + while ((match = backlinkRegex.exec(text)) !== null) { + const start = line.from + match.index; + const end = start + match[0].length; + + // Only decorate if within visible range + if (start >= from && end <= to) { + decorations.push(backlinkMark.range(start, end)); + } + } + + pos = line.to + 1; + } + } + + return Decoration.set(decorations); + } + }, { + decorations: (v) => v.decorations, + }); + } + private getValue(): string { return this._cellController.getValue(); } @@ -279,6 +524,17 @@ export class CTCodeEditor extends BaseElement { return false; }, }), + // Add backlink click handler for Cmd/Ctrl+Click + this.createBacklinkClickHandler(), + // Add backlink decoration plugin to visually style [[backlinks]] + this.createBacklinkDecorationPlugin(), + // Always add autocompletion with backlink support (handles case where mentionable is not set) + // Use activateOnTyping: false to disable default word completion and only show our completions + autocompletion({ + override: [this.createBacklinkCompletionSource()], + activateOnTyping: true, + closeOnBlur: true, + }), ]; // Add placeholder extension if specified diff --git a/packages/ui/src/v2/components/ct-code-editor/styles.ts b/packages/ui/src/v2/components/ct-code-editor/styles.ts index fc3ce23b3..7af4b91cc 100644 --- a/packages/ui/src/v2/components/ct-code-editor/styles.ts +++ b/packages/ui/src/v2/components/ct-code-editor/styles.ts @@ -49,4 +49,22 @@ export const styles = css` .cm-editor { border-radius: var(--radius, 0.375rem); } + + /* Backlink styling - make [[backlinks]] visually distinct */ + .cm-content .cm-line { + position: relative; + } + + /* Style for backlinks - we'll use a highlight mark */ + .cm-backlink { + background-color: var(--ring-alpha, hsla(212, 100%, 47%, 0.1)); + border-radius: 0.25rem; + padding: 0.125rem 0.25rem; + cursor: pointer; + transition: background-color 0.2s; + } + + .cm-backlink:hover { + background-color: var(--ring-alpha, hsla(212, 100%, 47%, 0.2)); + } `; From deb875f4a359b03e9ce528b3c24e034cca5668f9 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:56:42 +1000 Subject: [PATCH 2/5] Fix type errors --- .../ui/src/v2/components/ct-code-editor/ct-code-editor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts index c6f52aa0a..83d4c925d 100644 --- a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts +++ b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts @@ -165,9 +165,9 @@ export class CTCodeEditor extends BaseElement { if (mentionable.length === 0) return null; // Determine the completion range and apply text based on whether ]] exists - let applyText: string; + let applyText: (text: string) => string; let completionTo: number; - + if (afterCursor === "]]") { // We're inside existing backlinks like [[llm|]], just replace the content applyText = (text: string) => text; @@ -180,7 +180,7 @@ export class CTCodeEditor extends BaseElement { const options: Completion[] = mentionable.map(charm => { const charmIdObj = getEntityId(charm); - const charmId = charmIdObj["/"] || ""; + const charmId = charmIdObj?.["/"] || ""; const charmName = charm[NAME] || ""; const insertText = `${charmName} (${charmId})`; return { @@ -307,7 +307,7 @@ export class CTCodeEditor extends BaseElement { const charm = this.mentionable.key(i).getAsQueryResult(); if (charm) { const charmIdObj = getEntityId(charm); - const charmId = charmIdObj["/"] || ""; + const charmId = charmIdObj?.["/"] || ""; if (charmId === id) { return charm; } From 66cb7ede4c9a0715e49abd8ed051fc345a2a9969 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:58:09 +1000 Subject: [PATCH 3/5] Format pass --- .../ct-code-editor/ct-code-editor.ts | 149 +++++++++++------- .../v2/components/ct-code-editor/styles.ts | 4 +- 2 files changed, 90 insertions(+), 63 deletions(-) diff --git a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts index 83d4c925d..b6efede5b 100644 --- a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts +++ b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts @@ -2,7 +2,7 @@ import { html, PropertyValues } from "lit"; import { BaseElement } from "../../core/base-element.ts"; import { styles } from "./styles.ts"; import { basicSetup } from "codemirror"; -import { EditorView, placeholder, keymap } from "@codemirror/view"; +import { EditorView, keymap, placeholder } from "@codemirror/view"; import { Compartment, EditorState, Extension } from "@codemirror/state"; import { LanguageSupport } from "@codemirror/language"; import { javascript as createJavaScript } from "@codemirror/lang-javascript"; @@ -11,8 +11,19 @@ import { css as createCss } from "@codemirror/lang-css"; import { html as createHtml } from "@codemirror/lang-html"; import { json as createJson } from "@codemirror/lang-json"; import { oneDark } from "@codemirror/theme-one-dark"; -import { autocompletion, CompletionContext, CompletionResult, Completion } from "@codemirror/autocomplete"; -import { Decoration, DecorationSet, ViewPlugin, ViewUpdate, WidgetType } from "@codemirror/view"; +import { + autocompletion, + Completion, + CompletionContext, + CompletionResult, +} from "@codemirror/autocomplete"; +import { + Decoration, + DecorationSet, + ViewPlugin, + ViewUpdate, + WidgetType, +} from "@codemirror/view"; import { type Cell, getEntityId, NAME } from "@commontools/runner"; import { type InputTimingOptions } from "../../core/input-timing-controller.ts"; import { createStringCellController } from "../../core/cell-controller.ts"; @@ -136,29 +147,39 @@ export class CTCodeEditor extends BaseElement { */ private createBacklinkCompletionSource() { return (context: CompletionContext): CompletionResult | null => { - console.log("Completion source called, context:", context.pos, context.explicit); - + console.log( + "Completion source called, context:", + context.pos, + context.explicit, + ); + // Look for incomplete backlinks: [[ followed by optional text const backlink = context.matchBefore(/\[\[([^\]]*)?/); console.log("Backlink match:", backlink); - + if (!backlink) { // Also try a simpler pattern to debug const simpleMatch = context.matchBefore(/\[\[/); console.log("Simple [[ match:", simpleMatch); return null; } - + // Check what comes after the cursor - const afterCursor = context.state.doc.sliceString(context.pos, context.pos + 2); + const afterCursor = context.state.doc.sliceString( + context.pos, + context.pos + 2, + ); console.log("After cursor:", afterCursor); - + // Allow completion inside existing backlinks - we'll replace the content between [[ and ]] const query = backlink.text.slice(2); // Remove [[ prefix - + // Debug logging to see what's happening - console.log("Backlink completion triggered:", { query, mentionableExists: !!this.mentionable }); - + console.log("Backlink completion triggered:", { + query, + mentionableExists: !!this.mentionable, + }); + const mentionable = this.getFilteredMentionable(query); console.log("Filtered mentionable items:", mentionable); @@ -178,7 +199,7 @@ export class CTCodeEditor extends BaseElement { completionTo = context.pos; } - const options: Completion[] = mentionable.map(charm => { + const options: Completion[] = mentionable.map((charm) => { const charmIdObj = getEntityId(charm); const charmId = charmIdObj?.["/"] || ""; const charmName = charm[NAME] || ""; @@ -207,7 +228,7 @@ export class CTCodeEditor extends BaseElement { */ private getFilteredMentionable(query: string): Charm[] { console.log("getFilteredMentionable called with query:", query); - + if (!this.mentionable) { console.log("No mentionable property"); return []; @@ -215,7 +236,7 @@ export class CTCodeEditor extends BaseElement { const mentionableData = this.mentionable.getAsQueryResult(); console.log("Mentionable data:", mentionableData); - + if (!mentionableData || mentionableData.length === 0) { console.log("No mentionable data or empty array"); return []; @@ -254,7 +275,10 @@ export class CTCodeEditor extends BaseElement { /** * Handle backlink activation (Cmd/Ctrl+Click on a backlink) */ - private handleBacklinkActivation(view: EditorView, event?: MouseEvent): boolean { + private handleBacklinkActivation( + view: EditorView, + event?: MouseEvent, + ): boolean { const state = view.state; const pos = state.selection.main.head; const doc = state.doc; @@ -263,15 +287,15 @@ export class CTCodeEditor extends BaseElement { const lineStart = doc.lineAt(pos).from; const lineEnd = doc.lineAt(pos).to; const lineText = doc.sliceString(lineStart, lineEnd); - + // Find all [[...]] patterns in the line const backlinkRegex = /\[\[([^\]]+)\]\]/g; let match; - + while ((match = backlinkRegex.exec(lineText)) !== null) { const matchStart = lineStart + match.index; const matchEnd = matchStart + match[0].length; - + // Check if cursor is within this backlink if (pos >= matchStart && pos <= matchEnd) { const backlinkText = match[1]; // This is "Name (id)" format @@ -279,7 +303,7 @@ export class CTCodeEditor extends BaseElement { const idMatch = backlinkText.match(/\(([^)]+)\)$/); const backlinkId = idMatch ? idMatch[1] : backlinkText; const charm = this.findCharmById(backlinkId); - + if (charm) { this.emit("backlink-click", { id: backlinkId, @@ -290,7 +314,7 @@ export class CTCodeEditor extends BaseElement { } } } - + return false; } @@ -322,51 +346,54 @@ export class CTCodeEditor extends BaseElement { */ private createBacklinkDecorationPlugin() { const backlinkMark = Decoration.mark({ class: "cm-backlink" }); - - return ViewPlugin.fromClass(class { - decorations: DecorationSet; - - constructor(view: EditorView) { - this.decorations = this.getBacklinkDecorations(view); - } - - update(update: ViewUpdate) { - if (update.docChanged || update.viewportChanged) { - this.decorations = this.getBacklinkDecorations(update.view); + + return ViewPlugin.fromClass( + class { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = this.getBacklinkDecorations(view); } - } - - getBacklinkDecorations(view: EditorView) { - const decorations: any[] = []; - const doc = view.state.doc; - const backlinkRegex = /\[\[([^\]]+)\]\]/g; - - for (const { from, to } of view.visibleRanges) { - for (let pos = from; pos <= to;) { - const line = doc.lineAt(pos); - const text = line.text; - let match; - - backlinkRegex.lastIndex = 0; // Reset regex - while ((match = backlinkRegex.exec(text)) !== null) { - const start = line.from + match.index; - const end = start + match[0].length; - - // Only decorate if within visible range - if (start >= from && end <= to) { - decorations.push(backlinkMark.range(start, end)); + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = this.getBacklinkDecorations(update.view); + } + } + + getBacklinkDecorations(view: EditorView) { + const decorations: any[] = []; + const doc = view.state.doc; + const backlinkRegex = /\[\[([^\]]+)\]\]/g; + + for (const { from, to } of view.visibleRanges) { + for (let pos = from; pos <= to;) { + const line = doc.lineAt(pos); + const text = line.text; + let match; + + backlinkRegex.lastIndex = 0; // Reset regex + while ((match = backlinkRegex.exec(text)) !== null) { + const start = line.from + match.index; + const end = start + match[0].length; + + // Only decorate if within visible range + if (start >= from && end <= to) { + decorations.push(backlinkMark.range(start, end)); + } } + + pos = line.to + 1; } - - pos = line.to + 1; } + + return Decoration.set(decorations); } - - return Decoration.set(decorations); - } - }, { - decorations: (v) => v.decorations, - }); + }, + { + decorations: (v) => v.decorations, + }, + ); } private getValue(): string { diff --git a/packages/ui/src/v2/components/ct-code-editor/styles.ts b/packages/ui/src/v2/components/ct-code-editor/styles.ts index 7af4b91cc..394bb3c15 100644 --- a/packages/ui/src/v2/components/ct-code-editor/styles.ts +++ b/packages/ui/src/v2/components/ct-code-editor/styles.ts @@ -54,7 +54,7 @@ export const styles = css` .cm-content .cm-line { position: relative; } - + /* Style for backlinks - we'll use a highlight mark */ .cm-backlink { background-color: var(--ring-alpha, hsla(212, 100%, 47%, 0.1)); @@ -63,7 +63,7 @@ export const styles = css` cursor: pointer; transition: background-color 0.2s; } - + .cm-backlink:hover { background-color: var(--ring-alpha, hsla(212, 100%, 47%, 0.2)); } From d037cb7a3e74668c4bc67ed1faca5f85fecffbce Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:14:52 +1000 Subject: [PATCH 4/5] Update layout --- packages/patterns/chatbot-note.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/patterns/chatbot-note.tsx b/packages/patterns/chatbot-note.tsx index 631b9612c..ec06bc8b0 100644 --- a/packages/patterns/chatbot-note.tsx +++ b/packages/patterns/chatbot-note.tsx @@ -170,11 +170,7 @@ export default recipe( /> - - - - - + {ifElse( From 7b183147e2a4bd14f66de4a07c73b7e74ac4ff33 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:26:01 +1000 Subject: [PATCH 5/5] Clean up logging and code --- .../ct-code-editor/ct-code-editor.ts | 49 ++----------------- 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts index b6efede5b..cb156835c 100644 --- a/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts +++ b/packages/ui/src/v2/components/ct-code-editor/ct-code-editor.ts @@ -147,20 +147,10 @@ export class CTCodeEditor extends BaseElement { */ private createBacklinkCompletionSource() { return (context: CompletionContext): CompletionResult | null => { - console.log( - "Completion source called, context:", - context.pos, - context.explicit, - ); - // Look for incomplete backlinks: [[ followed by optional text const backlink = context.matchBefore(/\[\[([^\]]*)?/); - console.log("Backlink match:", backlink); if (!backlink) { - // Also try a simpler pattern to debug - const simpleMatch = context.matchBefore(/\[\[/); - console.log("Simple [[ match:", simpleMatch); return null; } @@ -169,36 +159,14 @@ export class CTCodeEditor extends BaseElement { context.pos, context.pos + 2, ); - console.log("After cursor:", afterCursor); // Allow completion inside existing backlinks - we'll replace the content between [[ and ]] const query = backlink.text.slice(2); // Remove [[ prefix - // Debug logging to see what's happening - console.log("Backlink completion triggered:", { - query, - mentionableExists: !!this.mentionable, - }); - const mentionable = this.getFilteredMentionable(query); - console.log("Filtered mentionable items:", mentionable); if (mentionable.length === 0) return null; - // Determine the completion range and apply text based on whether ]] exists - let applyText: (text: string) => string; - let completionTo: number; - - if (afterCursor === "]]") { - // We're inside existing backlinks like [[llm|]], just replace the content - applyText = (text: string) => text; - completionTo = context.pos; - } else { - // We're in incomplete backlinks like [[llm, add the closing ]] - applyText = (text: string) => text + "]]"; - completionTo = context.pos; - } - const options: Completion[] = mentionable.map((charm) => { const charmIdObj = getEntityId(charm); const charmId = charmIdObj?.["/"] || ""; @@ -212,8 +180,6 @@ export class CTCodeEditor extends BaseElement { }; }); - console.log("Completion options:", options); - return { from: backlink.from + 2, // Start after [[ to: afterCursor === "]]" ? context.pos : undefined, @@ -227,34 +193,26 @@ export class CTCodeEditor extends BaseElement { * Get filtered mentionable items based on query */ private getFilteredMentionable(query: string): Charm[] { - console.log("getFilteredMentionable called with query:", query); - if (!this.mentionable) { - console.log("No mentionable property"); return []; } const mentionableData = this.mentionable.getAsQueryResult(); - console.log("Mentionable data:", mentionableData); if (!mentionableData || mentionableData.length === 0) { - console.log("No mentionable data or empty array"); return []; } const queryLower = query.toLowerCase(); - const matches = []; + const matches: Charm[] = []; - // Filter mentionable items by name matching query for (let i = 0; i < mentionableData.length; i++) { const mention = this.mentionable.key(i).getAsQueryResult(); - console.log(`Mention ${i}:`, mention, "NAME:", mention?.[NAME]); if (mention && mention[NAME]?.toLowerCase()?.includes(queryLower)) { matches.push(mention); } } - console.log("Final matches:", matches); return matches; } @@ -362,7 +320,7 @@ export class CTCodeEditor extends BaseElement { } getBacklinkDecorations(view: EditorView) { - const decorations: any[] = []; + const decorations = []; const doc = view.state.doc; const backlinkRegex = /\[\[([^\]]+)\]\]/g; @@ -555,8 +513,7 @@ export class CTCodeEditor extends BaseElement { this.createBacklinkClickHandler(), // Add backlink decoration plugin to visually style [[backlinks]] this.createBacklinkDecorationPlugin(), - // Always add autocompletion with backlink support (handles case where mentionable is not set) - // Use activateOnTyping: false to disable default word completion and only show our completions + // Add autocompletion with backlink support autocompletion({ override: [this.createBacklinkCompletionSource()], activateOnTyping: true,