diff --git a/frontend/src/lib/components/editor/Editor.svelte b/frontend/src/lib/components/editor/Editor.svelte
index 3cc01c0..9cf3848 100644
--- a/frontend/src/lib/components/editor/Editor.svelte
+++ b/frontend/src/lib/components/editor/Editor.svelte
@@ -1,3 +1,155 @@
+= closeIdx;
+ }
+
+ function ensureRbLanguageFeatures() {
+ if (rbFeaturesRegistered) return;
+ rbFeaturesRegistered = true;
+ const Kind = monacoModule.languages.CompletionItemKind;
+ const AsSnippet = monacoModule.languages.CompletionItemInsertTextRule.InsertAsSnippet;
+
+ monacoModule.languages.registerCompletionItemProvider('html', {
+ triggerCharacters: ['.'],
+ provideCompletionItems(model, position) {
+ const before = model.getValueInRange({
+ startLineNumber: 1,
+ startColumn: 1,
+ endLineNumber: position.lineNumber,
+ endColumn: position.column
+ });
+ if (!rbInScript(before)) {
+ return { suggestions: [] };
+ }
+
+ const lineBefore = before.slice(before.lastIndexOf('\n') + 1);
+ const word = model.getWordUntilPosition(position);
+ const range = {
+ startLineNumber: position.lineNumber,
+ endLineNumber: position.lineNumber,
+ startColumn: word.startColumn,
+ endColumn: word.endColumn
+ };
+
+ // member access on rb / window.rb / remoteBrowser
+ if (/(?:^|[^\w$.])(?:window\s*\.\s*)?(?:rb|remoteBrowser)\s*\.\s*[\w$]*$/.test(lineBefore)) {
+ return {
+ suggestions: RB_MEMBERS.map((m) => ({
+ label: m.name,
+ kind: Kind.Method,
+ insertText: m.insertText,
+ insertTextRules: AsSnippet,
+ range,
+ detail: m.detail,
+ documentation: { value: m.doc }
+ }))
+ };
+ }
+
+ // bare identifier: offer the rb object, but not right after another dot
+ const uptoWord = lineBefore.slice(0, lineBefore.length - word.word.length);
+ if (/\.\s*$/.test(uptoWord)) {
+ return { suggestions: [] };
+ }
+ return {
+ suggestions: [
+ {
+ label: 'rb',
+ kind: Kind.Variable,
+ insertText: 'rb',
+ range,
+ detail: RB_OBJECT_DETAIL,
+ documentation: { value: RB_OBJECT_DOC }
+ }
+ ]
+ };
+ }
+ });
+
+ monacoModule.languages.registerHoverProvider('html', {
+ provideHover(model, position) {
+ const before = model.getValueInRange({
+ startLineNumber: 1,
+ startColumn: 1,
+ endLineNumber: position.lineNumber,
+ endColumn: position.column
+ });
+ if (!rbInScript(before)) return null;
+
+ const w = model.getWordAtPosition(position);
+ if (!w) return null;
+ const lineContent = model.getLineContent(position.lineNumber);
+ const beforeWord = lineContent.slice(0, w.startColumn - 1);
+ const range = new monacoModule.Range(
+ position.lineNumber,
+ w.startColumn,
+ position.lineNumber,
+ w.endColumn
+ );
+
+ // rb / window.rb / remoteBrowser object itself
+ if (w.word === 'rb' || w.word === 'remoteBrowser') {
+ const endsWithDot = /\.\s*$/.test(beforeWord);
+ const isWindowRb = /(?:^|[^\w$])window\s*\.\s*$/.test(beforeWord);
+ if (endsWithDot && !isWindowRb) return null;
+ return { range, contents: [{ value: RB_OBJECT_DOC }] };
+ }
+
+ // a member accessed on rb
+ const m = RB_MEMBERS.find((x) => x.name === w.word);
+ if (
+ m &&
+ /(?:^|[^\w$.])(?:window\s*\.\s*)?(?:rb|remoteBrowser)\s*\.\s*$/.test(beforeWord)
+ ) {
+ return {
+ range,
+ contents: [{ value: '```js\nrb.' + w.word + m.detail + '\n```' }, { value: m.doc }]
+ };
+ }
+ return null;
+ }
+ });
+ }
+
+