diff --git a/client/components/codeEditor/autocompleteEmoji.js b/client/components/codeEditor/autocompleteEmoji.js index fc64e7bbd..06bbcc577 100644 --- a/client/components/codeEditor/autocompleteEmoji.js +++ b/client/components/codeEditor/autocompleteEmoji.js @@ -1,3 +1,5 @@ +import { autocompletion } from '@codemirror/autocomplete'; + import diceFont from '@themes/fonts/iconFonts/diceFont.js'; import elderberryInn from '@themes/fonts/iconFonts/elderberryInn.js'; import fontAwesome from '@themes/fonts/iconFonts/fontAwesome.js'; @@ -10,75 +12,61 @@ const emojis = { ...gameIcons }; -const showAutocompleteEmoji = function(CodeMirror, editor) { - CodeMirror.commands.autocomplete = function(editor) { - editor.showHint({ - completeSingle : false, - hint : function(editor) { - const cursor = editor.getCursor(); - const line = cursor.line; - const lineContent = editor.getLine(line); - const start = lineContent.lastIndexOf(':', cursor.ch - 1) + 1; - const end = cursor.ch; - const currentWord = lineContent.slice(start, end); +const emojiCompletionList = (context)=>{ + const word = context.matchBefore(/:[^\s:]*/); + if(!word) return null; + const line = context.state.doc.lineAt(context.pos); + const textToCursor = line.text.slice(0, context.pos - line.from); - const list = Object.keys(emojis).filter(function(emoji) { - return emoji.toLowerCase().indexOf(currentWord.toLowerCase()) >= 0; - }).sort((a, b)=>{ - const lowerA = a.replace(/\d+/g, function(match) { // Temporarily convert any numbers in emoji string - return match.padStart(4, '0'); // to 4-digits, left-padded with 0's, to aid in - }).toLowerCase(); // sorting numbers, i.e., "d6, d10, d20", not "d10, d20, d6" - const lowerB = b.replace(/\d+/g, function(match) { // Also make lowercase for case-insensitive alpha sorting - return match.padStart(4, '0'); - }).toLowerCase(); + if(textToCursor.includes('{')) { + const curlyToCursor = textToCursor.slice(textToCursor.indexOf('{')); + const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g; + if(curlySpanRegex.test(curlyToCursor)) return null; + } - if(lowerA < lowerB) - return -1; - return 1; - }).map(function(emoji) { - return { - text : `${emoji}:`, // Text to output to editor when option is selected - render : function(element, self, data) { // How to display the option in the dropdown - const div = document.createElement('div'); - div.innerHTML = ` ${emoji}`; - element.appendChild(div); - } - }; - }); + const currentWord = word.text.slice(1); // remove ':' - return { - list : list.length ? list : [], - from : CodeMirror.Pos(line, start), - to : CodeMirror.Pos(line, end) - }; - } - }); + const options = Object.keys(emojis) + .filter((e)=>e.toLowerCase().includes(currentWord.toLowerCase())) + .sort((a, b)=>{ + const normalize = (str)=>str.replace(/\d+/g, (m)=>m.padStart(4, '0')).toLowerCase(); + return normalize(a) < normalize(b) ? -1 : 1; + }) + .map((e)=>({ + label : e, + apply : `${e}:`, + type : 'text', + info : ()=>{ + const div = document.createElement('div'); + div.innerHTML = ` ${e}`; + return div; + } + })); + + return { + from : word.from + 1, + options, }; - - editor.on('inputRead', function(instance, change) { - const cursor = editor.getCursor(); - const line = editor.getLine(cursor.line); - - // Get the text from the start of the line to the cursor - const textToCursor = line.slice(0, cursor.ch); - - // Do not autosuggest emojis in curly span/div/injector properties - if(line.includes('{')) { - const curlyToCursor = textToCursor.slice(textToCursor.indexOf(`{`)); - const curlySpanRegex = /{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\1$/g; - - if(curlySpanRegex.test(curlyToCursor)) - return; - } - - // Check if the text ends with ':xyz' - if(/:[^\s:]+$/.test(textToCursor)) { - CodeMirror.commands.autocomplete(editor); - } - }); }; -export default { - showAutocompleteEmoji -}; \ No newline at end of file +export const autocompleteEmoji = autocompletion({ + override : [emojiCompletionList], + activateOnTyping : true, + addToOptions : [ + { + render(completion) { + const e = completion.label; + + const icon = document.createElement('i'); + icon.className = `emojiPreview ${emojis[e]}`; + + // append directly to a DocumentFragment to return a single node + const fragment = document.createDocumentFragment(); + fragment.appendChild(icon); + + return fragment; + } + } + ] +}); \ No newline at end of file diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index 749793bc3..6b4506b01 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -18,6 +18,7 @@ import { defaultKeymap, history, historyField, undo, redo } from '@codemirror/co import { languages } from '@codemirror/language-data'; import { css } from '@codemirror/lang-css'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; +import { autocompleteEmoji } from './autocompleteEmoji.js'; import * as themes from '@uiw/codemirror-themes-all'; const themeCompartment = new Compartment(); @@ -31,20 +32,20 @@ import { legacyCustomHighlightStyle, legacyTokenizeCustomMarkdown } from './lega const createHighlightPlugin = (renderer, tab)=>{ const tokenize = renderer === 'V3' ? tokenizeCustomMarkdown : legacyTokenizeCustomMarkdown; -class countWidget extends WidgetType { - constructor(count) { - super(); - this.count = count; - } - toDOM() { - const span = document.createElement("span"); - span.className = "cm-page-count"; - span.textContent = this.count; - span.style.color = "#989898"; - return span; - } - ignoreEvent() { return true; } -} + class countWidget extends WidgetType { + constructor(count) { + super(); + this.count = count; + } + toDOM() { + const span = document.createElement('span'); + span.className = 'cm-page-count'; + span.textContent = this.count; + span.style.color = '#989898'; + return span; + } + ignoreEvent() { return true; } + } return ViewPlugin.fromClass( class { @@ -154,6 +155,7 @@ const CodeEditor = forwardRef( highlightActiveLine(), highlightActiveLineGutter(), highlightCompartment.of(combinedHighlight), + autocompleteEmoji, ]; }; diff --git a/client/homebrew/editor/editor.less b/client/homebrew/editor/editor.less index ce583fc8b..7caa47561 100644 --- a/client/homebrew/editor/editor.less +++ b/client/homebrew/editor/editor.less @@ -2,11 +2,11 @@ @import '@themes/codeMirror/customEditorStyles.less'; .editor { - position : relative; - width : 100%; - height : 100%; - container : editor / inline-size; - background:white; + position : relative; + width : 100%; + height : 100%; + container : editor / inline-size; + background : white; .codeEditor { height : calc(100% - 25px); .cm-editor { height : 100%; } @@ -14,6 +14,20 @@ background : #33333328; border-top : #333399 solid 1px; } + + .cm-tooltip-autocomplete { + + li { + display : flex; + gap : 10px; + align-items : center; + justify-content : flex-start; + + .cm-completionIcon { display : none; } + .cm-tooltip-autocomplete .cm-completionLabel { translate : 0 -2px; } + } + + } .cm-page-count { float : right; diff --git a/package-lock.json b/package-lock.json index 6d8207510..3330f5094 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", "@babel/runtime": "^7.28.6", + "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", "@codemirror/highlight": "^0.19.8", "@codemirror/lang-css": "^6.3.1", diff --git a/package.json b/package.json index 70dcbcb4a..86bfd4527 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", "@babel/runtime": "^7.28.6", + "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", "@codemirror/highlight": "^0.19.8", "@codemirror/lang-css": "^6.3.1",