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",