Repository: ajaxorg/ace
Pull request #5771: Feat: Editing is disabled tooltip in readonly mode
Author: login: mkslanc
Created: 2025-03-14T13:25:34Z | Merged: 2025-03-19T12:08:49Z

*Issue #, if available:*

*Description of changes:*

![image](https://github.com/user-attachments/assets/99b4f582-91d2-4263-9e81-ecc696a2367d)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Pull Request Checklist:
* [x] No backwards incompatible changes were made to Ace's public interface which is defined through the main typings file (`ace.d.ts`) and its references:
    * https://github.com/ajaxorg/ace/blob/master/ace.d.ts 
    * https://github.com/ajaxorg/ace/blob/master/ace-modes.d.ts 
    * https://github.com/ajaxorg/ace/blob/master/ace-extensions.d.ts




Diff:
diff --git a/src/ace_test.js b/src/ace_test.js
index 1917b7ac039..2f1921fe1cf 100644
--- a/src/ace_test.js
+++ b/src/ace_test.js
@@ -151,6 +151,7 @@ module.exports = {
                     assert.ok(editor.renderer.$resizeObserver);
                     if (window.ResizeObserver === mockObserver.$create)
                         window.ResizeObserver = undefined;
+                    editor.destroy();
                     done();
                 }, 15);
             }, 15);
diff --git a/src/autocomplete_test.js b/src/autocomplete_test.js
index 0c36d4a5017..2d26e5369dc 100644
--- a/src/autocomplete_test.js
+++ b/src/autocomplete_test.js
@@ -61,7 +61,7 @@ module.exports = {
         }
     },
     "test: highlighting in the popup": function (done) {
-        var editor = initEditor("\narraysort alooooooooooooooooooooooooooooong_word");
+        editor = initEditor("\narraysort alooooooooooooooooooooooooooooong_word");
         //   editor.container.style.width = "500px";
         //  editor.container.style.height = "500px";
 
@@ -79,8 +79,6 @@ module.exports = {
                     editor.execCommand("insertstring", " looooooooooooooooooooooooooooong_");
                     checkInnerHTML('<d "ace_line ace_selected" role="option" aria-roledescription="item" aria-setsize="1" aria-describedby="doc-tooltip" aria-posinset="1" aria-label="alooooooooooooooooooooooooooooong_word, local" id="suggest-aria-id:0" aria-selected="true"><s "ace_">a
<s "ace_completion-highlight" role="mark">looooooooooooooooooooooooooooong_
<s "ace_">word
<s "ace_completion-spacer"> 
<s "ace_completion-meta">local
</d>', function () {
                         sendKey("Return");
-                        editor.destroy();
-                        editor.container.remove();
                         done();
                     });
                 });
@@ -104,7 +102,7 @@ module.exports = {
         }
     },
     "test: completions range and command properties": function (done) {
-        var editor = initEditor("goods ");
+        editor = initEditor("goods ");
 
         editor.completers = [
             {
@@ -138,14 +136,12 @@ module.exports = {
             afterRenderCheck(popup, function () {
                 editor.onCommandKey(null, 0, 13);
                 assert.equal(editor.getValue(), "goodwill-here");
-                editor.destroy();
-                editor.container.remove();
                 done();
             });
         });
     },
     "test: correct completion replacement range when completion prefix has more than one letter": function (done) {
-        var editor = initEditor("<");
+        editor = initEditor("<");
 
         editor.completers = [
             {
@@ -174,8 +170,6 @@ module.exports = {
             editor.onCommandKey(null, 0, 13);
             check(function () {
                 assert.equal(editor.getValue(), "<dialog");
-                editor.destroy();
-                editor.container.remove();
                 done();
             });
         });
@@ -187,7 +181,7 @@ module.exports = {
         }
     },
     "test: symbols after selection are not removed when replacement range is present": function (done) {
-        var editor = initEditor("{}");
+        editor = initEditor("{}");
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -214,11 +208,12 @@ module.exports = {
             assert.equal(popup.data.length, 2);
             editor.onCommandKey(null, 0, 13);
             assert.equal(editor.getValue(), "{apple: }");
+
             done();
         });
     },
     "test: should set correct aria attributes for popup items": function(done) {
-        var editor = initEditor("");
+        editor = initEditor("");
         var newLineCharacter = editor.session.doc.getNewLineCharacter();
         editor.completers = [
             {
@@ -269,7 +264,7 @@ module.exports = {
         }
     },
     "test: different completers tooltips": function (done) {
-        var editor = initEditor("");
+        editor = initEditor("");
         var firstDoc = "<b>First</b>";
         var secondDoc = "Second";
         editor.completers = [
@@ -319,8 +314,8 @@ module.exports = {
                 id: "secondCompleter"
             }
         ];
-        
-        
+
+
 
         sendKey("c");
         var popup = editor.completer.popup;
@@ -336,8 +331,6 @@ module.exports = {
                     sendKey("Down");
                     check(function () {
                         assert.equal(popup.container.lastChild.innerHTML, secondDoc);
-                        editor.destroy();
-                        editor.container.remove();
                         done();
                     });
                 });
@@ -351,7 +344,7 @@ module.exports = {
         }
     },
     "test: completers tooltip filtering": function (done) {
-        var editor = initEditor("");
+        editor = initEditor("");
         var firstDoc = "First tooltip";
         var secondDoc = "Second tooltip";
         editor.completers = [
@@ -371,14 +364,14 @@ module.exports = {
                 getDocTooltip: function (item) {
                     if (item.value === 'case') {
                         item.docHTML = firstDoc;
-                    } 
+                    }
                     if (item.value === 'catch') {
                         item.docHTML = secondDoc;
                     }
                 }
             }
         ];
-        
+
         sendKey("ca");
         var popup = editor.completer.popup;
 
@@ -392,12 +385,10 @@ module.exports = {
                 assert.equal(popup.data.length, 1);
                 assert.equal(popup.container.lastChild.innerHTML, secondDoc);
 
-                editor.destroy();
-                editor.container.remove();
                 done();
             });
         });
-    
+
         function check(callback) {
             setTimeout(function wait() {
                 callback();
@@ -424,7 +415,7 @@ module.exports = {
             },
             id: "asyncCompleter"
         };
-        var editor = initEditor("");
+        editor = initEditor("");
         editor.completers=[
             syncCompleter,asyncCompleter
         ];
@@ -447,14 +438,12 @@ module.exports = {
             sendKey(" ");
             sendKey("q");
             assert.equal(editor.completer.popup.isOpen, false);
-            
-            editor.destroy();
-            editor.container.remove();
+
             done();
         }, 10);
     },
     "test: trigger autocomplete for specific characters": function (done) {
-        var editor = initEditor("document");
+        editor = initEditor("document");
         var newLineCharacter = editor.session.doc.getNewLineCharacter();
 
         editor.completers = [
@@ -503,7 +492,7 @@ module.exports = {
         }
     },
     "test: empty message if no suggestions available": function(done) {
-        var editor = initEditor("");
+        editor = initEditor("");
         var emptyMessageText = "No suggestions.";
         var autocomplete = Autocomplete.for(editor);
         autocomplete.emptyMessage = () => emptyMessageText;
@@ -522,7 +511,7 @@ module.exports = {
         done();
     },
     "test: no empty message class if suggestions available": function(done) {
-        var editor = initEditor("");
+        editor = initEditor("");
         var emptyMessageText = "No suggestions.";
         var autocomplete = Autocomplete.for(editor);
         autocomplete.emptyMessage = () => emptyMessageText;
@@ -561,12 +550,12 @@ module.exports = {
         done();
     },
     "test: liveAutocompleteDelay": function(done) {
-        var editor = initEditor("hello world ");
+        editor = initEditor("hello world ");
         editor.setOptions({
             liveAutocompletionDelay: 10,
             liveAutocompletionThreshold: 2
         });
-        
+
         editor.completers = [{
                 getCompletions: function(editor, session, pos, prefix, callback) {
                     callback(null,[{
@@ -588,7 +577,7 @@ module.exports = {
                 }
             }
         ];
-        
+
         user.type(" ");
         user.type("t");
         user.type("e");
@@ -612,7 +601,7 @@ module.exports = {
         }, 11);
     },
     "test: scroll and resize": function() {
-        var editor = initEditor("hello world\n");
+        editor = initEditor("hello world\n");
         user.type("Ctrl-Space");
         assert.equal(editor.completer.popup.isOpen, true);
         var called = false;
@@ -621,13 +610,13 @@ module.exports = {
         };
         editor.container.parentNode.dispatchEvent(new CustomEvent("scroll"));
         assert.ok(called);
-        
+
         editor.destroy();
         editor.container.remove();
     },
     "test: selection should follow hovermarker if setSelectOnHover true": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -647,13 +636,13 @@ module.exports = {
                 }
             }
         ];
-        
+
         var completer = Autocomplete.for(editor);
         completer.setSelectOnHover = true;
         user.type("Ctrl-Space");
-        assert.equal(editor.completer.popup.isOpen, true);     
+        assert.equal(editor.completer.popup.isOpen, true);
         assert.equal(completer.popup.getRow(), 0);
-        
+
         var text = completer.popup.renderer.content.childNodes[2];
         var rect = text.getBoundingClientRect();
 
@@ -667,9 +656,9 @@ module.exports = {
         assert.equal(completer.popup.getRow(), 2);
 
         done();
-    }, 
+    },
     "test: selection should not follow hovermarker if setSelectOnHover not set": function(done) {
-        var editor = initEditor("hello world\n");
+        editor = initEditor("hello world\n");
 
         editor.completers = [
             {
@@ -692,11 +681,11 @@ module.exports = {
         ];
 
         var completer = Autocomplete.for(editor);
- 
+
         user.type("Ctrl-Space");
-        assert.equal(editor.completer.popup.isOpen, true);     
+        assert.equal(editor.completer.popup.isOpen, true);
         assert.equal(completer.popup.getRow(), 0);
-        
+
         var text = completer.popup.renderer.content.childNodes[2];
         var rect = text.getBoundingClientRect();
 
@@ -712,8 +701,8 @@ module.exports = {
         done();
     },
     "test: should respect hideInlinePreview": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -752,15 +741,15 @@ module.exports = {
                 }
             }
         ];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
         user.type("Ctrl-Space");
         var inline = completer.inlineRenderer;
 
-        assert.equal(editor.completer.popup.isOpen, true);  
-        
+        assert.equal(editor.completer.popup.isOpen, true);
+
         // Row 0, should hide inline preview.
         assert.equal(completer.popup.getRow(), 0);
         assert.strictEqual(inline.isOpen(), false);
@@ -781,8 +770,8 @@ module.exports = {
         done();
     },
     "test: should maintain selection on fast completer item when slow completer results come in": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var slowCompleter = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -820,25 +809,25 @@ module.exports = {
         };
 
         editor.completers = [fastCompleter, slowCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.stickySelectionDelay = 100;
         user.type("Ctrl-Space");
         assert.equal(completer.popup.isOpen, true);
-        assert.equal(completer.popup.data.length, 2); 
+        assert.equal(completer.popup.data.length, 2);
         assert.equal(completer.popup.getRow(), 0);
 
         setTimeout(() => {
             completer.popup.renderer.$loop._flush();
             assert.equal(completer.popup.data.length, 4);
             assert.equal(completer.popup.getRow(), 1);
-     
+
             done();
-        }, 500);    
+        }, 500);
     },
     "test: should not maintain selection on fast completer item when slow completer results come in when stickySelectionDelay negative": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var slowCompleter = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -876,25 +865,25 @@ module.exports = {
         };
 
         editor.completers = [fastCompleter, slowCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.stickySelectionDelay = -1;
         user.type("Ctrl-Space");
-        assert.equal(completer.popup.isOpen, true);    
-        assert.equal(completer.popup.data.length, 2); 
+        assert.equal(completer.popup.isOpen, true);
+        assert.equal(completer.popup.data.length, 2);
         assert.equal(completer.popup.getRow(), 0);
 
         setTimeout(() => {
             completer.popup.renderer.$loop._flush();
             assert.equal(completer.popup.data.length, 4);
             assert.equal(completer.popup.getRow(), 0);
-     
+
             done();
-        }, 500);    
+        }, 500);
     },
     "test: should filter using caption if ignoreCaption false": function() {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var completer = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -908,17 +897,17 @@ module.exports = {
         };
 
         editor.completers = [completer];
-        
+
         var completer = Autocomplete.for(editor);
 
         // Should filter using the caption if set to false.
         completer.ignoreCaption = false;
         user.type("cap");
-        assert.equal(completer.popup.isOpen, true);   
+        assert.equal(completer.popup.isOpen, true);
     },
     "test: should filter using value if ignoreCaption true": function() {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var completer = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -932,20 +921,20 @@ module.exports = {
         };
 
         editor.completers = [completer];
-        
+
         var completer = Autocomplete.for(editor);
 
         // Should not filter using the caption if set to true.
         completer.ignoreCaption = true;
         user.type("cap");
-        assert.equal(completer.popup, undefined);   
+        assert.equal(completer.popup, undefined);
 
         // Should filter using the value instead.
         user.type(" value");
-        assert.equal(completer.popup.isOpen, true);   
+        assert.equal(completer.popup.isOpen, true);
     },
     "test: should skip filter if skipFilter flag is set to true in completion": function() {
-        var editor = initEditor("hello world\n");
+        editor = initEditor("hello world\n");
 
         var completer = {
             getCompletions: function (editor, session, pos, prefix, callback) {
@@ -968,7 +957,7 @@ module.exports = {
         assert.equal(completer.popup.isOpen, true);
     },
     "test: should use filter if skipFilter flag is set to false in completion": function() {
-        var editor = initEditor("hello world\n");
+        editor = initEditor("hello world\n");
 
         var completer = {
             getCompletions: function (editor, session, pos, prefix, callback) {
@@ -985,20 +974,20 @@ module.exports = {
         editor.completers = [completer];
 
         var completer = Autocomplete.for(editor);
-        
+
         // should do filter out the completion item where skipFilter is false
         user.type("notMatchingText");
         assert.equal(completer.popup, undefined);
-        
+
         // normal filtering mechanism should work fine
         user.type(" ex");
         assert.equal(completer.popup.isOpen, true);
-        assert.equal(completer.popup.data.length, 1); 
+        assert.equal(completer.popup.data.length, 1);
     },
-    
+
     "test: should add inline preview content to aria-describedby": function(done) {
-        var editor = initEditor("fun");
-        
+        editor = initEditor("fun");
+
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -1012,7 +1001,7 @@ module.exports = {
                 }
             }
         ];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
@@ -1020,7 +1009,7 @@ module.exports = {
         var inline = completer.inlineRenderer;
 
         // Popup should be open, with inline text renderered.
-        assert.equal(completer.popup.isOpen, true);  
+        assert.equal(completer.popup.isOpen, true);
         assert.equal(completer.popup.getRow(), 0);
         assert.strictEqual(inline.isOpen(), true);
         assert.strictEqual(editor.renderer.$ghostText.text, "function\nthat does something\ncool");
@@ -1038,7 +1027,7 @@ module.exports = {
         done();
     },
     "test: update popup position only on mouse out when inline enabled and setSelectOnHover true": function() {
-        var editor = initEditor("fun");
+        editor = initEditor("fun");
 
         editor.completers = [
             {
@@ -1076,7 +1065,7 @@ module.exports = {
         assert.equal(completer.popup.getRow(), 0);
         assert.strictEqual(inline.isOpen(), true);
         assert.strictEqual(editor.renderer.$ghostText.text, "function that does something uncool");
- 
+
         var text = completer.popup.renderer.content.childNodes[2];
         var rect = text.getBoundingClientRect();
 
@@ -1084,7 +1073,7 @@ module.exports = {
         text.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top}));
         // Hover over the second row.
         text.dispatchEvent(new MouseEvent("move", {x: rect.left + 1, y: rect.top + 20}));
-    
+
         editor.completer.popup.renderer.$loop._flush();
 
         // Check that the completion item changed to the longer item.
@@ -1105,8 +1094,8 @@ module.exports = {
         editor.container.remove();
     },
     "test: should display loading state": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var slowCompleter = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -1148,7 +1137,7 @@ module.exports = {
         };
 
         editor.completers = [slowCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.stickySelectionDelay = 100;
         completer.showLoadingState = true;
@@ -1160,35 +1149,35 @@ module.exports = {
             assert.equal(completer.popup.data.length, 1);
             assert.ok(isLoading());
             setTimeout(() => {
-                assert.equal(completer.popup.data.length, 2); 
+                assert.equal(completer.popup.data.length, 2);
                 assert.ok(!isLoading());
                 user.type("Escape");
                 assert.ok(!(completer.popup && completer.popup.isOpen));
-                
+
 
                 editor.completers = [fastCompleter, slowCompleter];
                 user.type("Ctrl-Space");
-                assert.equal(completer.popup.data.length, 4); 
+                assert.equal(completer.popup.data.length, 4);
 
                 // Should have top row saying 'Loading...' together with results.
                 assert.ok(isLoading());
-                assert.equal(completer.popup.data[0].caption, "Loading..."); 
+                assert.equal(completer.popup.data[0].caption, "Loading...");
                 setTimeout(() => {
                     completer.popup.renderer.$loop._flush();
                     assert.equal(completer.popup.data.length, 5);
                     assert.ok(!isLoading());
                     done();
-                }, 250);  
+                }, 250);
             }, 150);
         }, 100);
-        
+
         function isLoading() {
             return completer.popup.renderer.container.classList.contains("ace_loading");
         }
     },
     "test: should not display loading state on no suggestion state": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var slowCompleter = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -1209,7 +1198,7 @@ module.exports = {
         };
 
         editor.completers = [slowCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.stickySelectionDelay = 100;
         completer.emptyMessage = "no completions";
@@ -1225,21 +1214,21 @@ module.exports = {
             assert.ok(isLoading());
             setTimeout(() => {
                 // Should show no suggestions state without loading indicator
-                assert.equal(completer.popup.data.length, 1); 
+                assert.equal(completer.popup.data.length, 1);
                 assert.equal(completer.popup.data[0].caption, "no completions");
                 assert.ok(!isLoading());
-    
+
                 done();
             }, 150);
         }, 100);
-        
+
         function isLoading() {
             return completer.popup.renderer.container.classList.contains("ace_loading");
         }
     },
     "test: should display ghost text after loading state if inline preview enabled": function(done) {
-        var editor = initEditor("hello world\n");
-        
+        editor = initEditor("hello world\n");
+
         var slowCompleter = {
             getCompletions: function (editor, session, pos, prefix, callback) {
                 var completions = [
@@ -1260,7 +1249,7 @@ module.exports = {
         };
 
         editor.completers = [slowCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.stickySelectionDelay = 100;
         completer.inlineEnabled = true;
@@ -1274,21 +1263,21 @@ module.exports = {
             assert.equal(completer.popup.data.length, 1);
             assert.ok(isLoading());
             setTimeout(() => {
-                assert.equal(completer.popup.data.length, 2); 
+                assert.equal(completer.popup.data.length, 2);
                 assert.ok(!isLoading());
-                
+
                 assert.strictEqual(editor.renderer.$ghostText.text, "s1");
                 done();
             }, 150);
         }, 100);
-        
+
         function isLoading() {
             return completer.popup.renderer.container.classList.contains("ace_loading");
         }
     },
     "test: when completion gets inserted and call the onInsert method": function (done) {
         var isInserted = false;
-        var editor = initEditor("hello world");
+        editor = initEditor("hello world");
         var completer = {
             onInsert: function (_editor, el) {
                 assert.ok(!isInserted, "should not have inserted something already");
@@ -1316,7 +1305,7 @@ module.exports = {
         editor.completers = [completer];
         user.type("Ctrl-Space");
         editor.completer.popup.renderer.$loop._flush();
-        assert.equal(editor.completer.popup.isOpen, true);     
+        assert.equal(editor.completer.popup.isOpen, true);
         assert.equal(editor.completer.popup.getRow(), 0);
         user.type("Return");
 
@@ -1326,7 +1315,7 @@ module.exports = {
     },
     "test: when completions get shown, call the onSeen method": function (done) {
         var seen = [false, false, false];
-        var editor = initEditor("hello world");
+        editor = initEditor("hello world");
         var completer = {
             onSeen: function (_editor, el) {
                 const index = ["one", "two", "three"].indexOf(el.value);
@@ -1360,7 +1349,7 @@ module.exports = {
         Autocomplete.for(editor).inlineEnabled = true;
         user.type("Ctrl-Space");
         editor.completer.popup.renderer.$loop._flush();
-        assert.equal(editor.completer.popup.isOpen, true);     
+        assert.equal(editor.completer.popup.isOpen, true);
         assert.equal(editor.completer.popup.getRow(), 0);
         assert.deepEqual(seen, [true, false, false]);
         done();
@@ -1368,7 +1357,7 @@ module.exports = {
     "test: when inline completions get shown, call the onSeen method": function (done) {
         var seen = [false, false, false];
         var calledDouble = false;
-        var editor = initEditor("hello world");
+        editor = initEditor("hello world");
         var completer = {
             onSeen: function (_editor, el) {
                 const index = ["one", "two", "three"].indexOf(el.value);
@@ -1401,14 +1390,14 @@ module.exports = {
         ];
         user.type("Ctrl-Space");
         editor.completer.popup.renderer.$loop._flush();
-        assert.equal(editor.completer.popup.isOpen, true);     
+        assert.equal(editor.completer.popup.isOpen, true);
         assert.equal(editor.completer.popup.getRow(), 0);
         assert.deepEqual(seen, [true, true, true]);
         assert.ok(!calledDouble);
         done();
     },
     "test: if there is very long ghost text, popup should be rendered at the bottom of the editor container": function(done) {
-        var editor = initEditor("hello world\n");
+        editor = initEditor("hello world\n");
 
         // Give enough space for the popup to appear below the editor
         var initialDocumentHeight = document.body.style.height;
@@ -1429,7 +1418,7 @@ module.exports = {
         };
 
         editor.completers = [longCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
@@ -1454,7 +1443,7 @@ module.exports = {
         }, 100);
     },
     "test: if there is ghost text, popup should be rendered at the bottom of the ghost text": function(done) {
-        var editor = initEditor("");
+        editor = initEditor("");
 
         var longCompleter = {
             getCompletions: function (editor, session, pos, prefix, callback) {
@@ -1470,7 +1459,7 @@ module.exports = {
         };
 
         editor.completers = [longCompleter];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
@@ -1491,7 +1480,7 @@ module.exports = {
         }, 100);
     },
     "test: should not show loading state when empty completer array is provided": function(done) {
-        var editor = initEditor("");
+        editor = initEditor("");
         editor.completers = [];
         var completer = Autocomplete.for(editor);
         completer.showLoadingState = true;
@@ -1504,8 +1493,8 @@ module.exports = {
         done();
     },
     "test: should update inline preview when typing when it's the only item in the popup": function(done) {
-        var editor = initEditor("");
-        
+        editor = initEditor("");
+
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -1519,7 +1508,7 @@ module.exports = {
                 }
             }
         ];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
@@ -1527,7 +1516,7 @@ module.exports = {
         var inline = completer.inlineRenderer;
 
         // Popup should be open, with inline text renderered.
-        assert.equal(completer.popup.isOpen, true);  
+        assert.equal(completer.popup.isOpen, true);
         assert.equal(completer.popup.getRow(), 0);
         assert.strictEqual(inline.isOpen(), true);
         assert.strictEqual(editor.renderer.$ghostText.text, "unction\nthat does something\ncool");
@@ -1544,14 +1533,14 @@ module.exports = {
             setTimeout(() => {
                 assert.strictEqual(inline.isOpen(), true);
                 assert.strictEqual(editor.renderer.$ghostText.text, "n\nthat does something\ncool");
-    
+
                 done();
             }, 100);
         }, 100);
     },
     "test: should keep showing ghost text when typing ahead with whitespace": function(done) {
-        var editor = initEditor("");
-        
+        editor = initEditor("");
+
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -1564,7 +1553,7 @@ module.exports = {
                 }
             }
         ];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
@@ -1572,7 +1561,7 @@ module.exports = {
         var inline = completer.inlineRenderer;
 
         // Popup should be open, with inline text renderered.
-        assert.equal(completer.popup.isOpen, true);  
+        assert.equal(completer.popup.isOpen, true);
         assert.equal(completer.popup.getRow(), 0);
         assert.strictEqual(inline.isOpen(), true);
         assert.strictEqual(editor.renderer.$ghostText.text, "unction that does something cool");
@@ -1589,32 +1578,32 @@ module.exports = {
             setTimeout(() => {
                 assert.strictEqual(inline.isOpen(), true);
                 assert.strictEqual(editor.renderer.$ghostText.text, "es something cool");
-    
+
                 done();
             }, 100);
         }, 100);
     },
     "test: passing matches from execCommand": function() {
-        var editor = initEditor("");
+        editor = initEditor("");
         editor.execCommand('startAutocomplete', {
             matches: [
                 { value: 'example value' }
             ]
         });
         user.type("\n");
-        
+
         assert.equal(editor.getValue(), "example value");
-        
+
         editor.resize(true);
         editor.insertSnippet("<$1-${1|a1,b2,c3|}>");
         user.type("Down");
         user.type("\n");
-        
+
         assert.equal(editor.getValue(), "example value<b2-b2>");
     },
     "test: should close popup if backspacing until input is fully deleted": function() {
-        var editor = initEditor("");
-        
+        editor = initEditor("");
+
         editor.completers = [
             {
                 getCompletions: function (editor, session, pos, prefix, callback) {
@@ -1627,35 +1616,28 @@ module.exports = {
                 }
             }
         ];
-        
+
         var completer = Autocomplete.for(editor);
         completer.inlineEnabled = true;
 
         user.type("tab");
 
         // Popup should be open
-        assert.equal(completer.popup.isOpen, true);  
+        assert.equal(completer.popup.isOpen, true);
 
         user.type("Backspace");
         user.type("Backspace");
 
         // Popup should still be open
-        assert.equal(completer.popup.isOpen, true);  
+        assert.equal(completer.popup.isOpen, true);
 
         user.type("Backspace");
 
         // Popup should be closed now
-        assert.equal(completer.popup.isOpen, false); 
-    },
-    "test: should set create shared Autocomplete with sharedPopups on": function() {
-        assert.equal(Autocomplete.$sharedInstance == undefined, true);
-        config.set("sharedPopups", true);
-        var editor = initEditor("");
-        var completer = Autocomplete.for(editor);
-        assert.equal(Autocomplete.$sharedInstance == undefined, false);
+        assert.equal(completer.popup.isOpen, false);
     },
     "test: changing completion should render scrollbars correctly": function (done) {
-        var editor = initEditor("document");
+        editor = initEditor("document");
         var newLineCharacter = editor.session.doc.getNewLineCharacter();
         var initialCompletions = [
             {
diff --git a/src/editor.js b/src/editor.js
index eb73bedc401..eb2b6b3f30d 100644
--- a/src/editor.js
+++ b/src/editor.js
@@ -29,6 +29,8 @@ var nls = require("./config").nls;
 var clipboard = require("./clipboard");
 var keys = require('./lib/keys');
 
+var event = require("./lib/event");
+var HoverTooltip = require("./tooltip").HoverTooltip;
 
 /**
  * The main entry point into the Ace functionality.
@@ -2836,9 +2838,32 @@ config.defineOptions(Editor.prototype, "editor", {
         initialValue: true
     },
     readOnly: {
-        set: function(readOnly) {
+        set: function(/**@type{boolean}*/readOnly) {
             this.textInput.setReadOnly(readOnly);
             this.$resetCursorStyle();
+            if (!this.readOnlyCallback) {
+                this.readOnlyCallback = () => {
+                    if (!this.hoverTooltip) {
+                        this.hoverTooltip = new HoverTooltip();
+                    }
+                    const domNode = dom.createElement("div");
+                    domNode.textContent = nls("editor.tooltip.disable-editing", "Editing is disabled");
+                    if (!this.hoverTooltip.isOpen) {
+                        this.hoverTooltip.showForRange(this, this.getSelectionRange(), domNode);
+                    }
+
+                };
+            }
+            const textArea = this.textInput.getElement();
+            if (readOnly) {
+                event.addListener(textArea, "keyup", this.readOnlyCallback, this);
+            } else {
+                event.removeListener(textArea, "keyup", this.readOnlyCallback);
+                if (this.hoverTooltip) {
+                    this.hoverTooltip.destroy();
+                    this.hoverTooltip = null;
+                }
+            }
         },
         initialValue: false
     },
diff --git a/src/editor_options_test.js b/src/editor_options_test.js
new file mode 100644
index 00000000000..dc80f6a2032
--- /dev/null
+++ b/src/editor_options_test.js
@@ -0,0 +1,86 @@
+if (typeof process !== "undefined") {
+    require("amd-loader");
+    require("./test/mockdom");
+}
+
+"use strict";
+
+var assert = require("./test/assertions");
+var sendKey = require("./test/user").type;
+var ace = require("./ace");
+
+var editor;
+
+function mouse(type, pos, properties) {
+    var target = editor.renderer.getMouseEventTarget();
+    var event = new CustomEvent("mouse" + type, {bubbles: true});
+
+    if ("row" in pos) {
+        var pagePos = editor.renderer.textToScreenCoordinates(pos.row, pos.column);
+        event.clientX = pagePos.pageX;
+        event.clientY = pagePos.pageY;
+    }
+    else {
+        target = pos;
+        var rect = target.getBoundingClientRect();
+        event.clientX = rect.left + rect.width / 2;
+        event.clientY = rect.top + rect.height / 2;
+    }
+    Object.assign(event, properties);
+    target.dispatchEvent(event);
+}
+
+module.exports = {
+    setUp: function () {
+        editor = ace.edit(null, {
+            value: "999"
+        });
+        document.body.appendChild(editor.container);
+        editor.container.style.height = "200px";
+        editor.container.style.width = "300px";
+        editor.focus();
+
+    },
+    tearDown: function () {
+        editor.destroy();
+        editor = null;
+    },
+    "test readOnly Option": function (done) {
+        let readOnly = editor.getOption("readOnly");
+        assert.equal(editor.hoverTooltip, null);
+        assert.equal(readOnly, false);
+        editor.setOption("readOnly", true);
+        readOnly = editor.getOption("readOnly");
+        assert.equal(readOnly, true);
+        sendKey("a");
+
+        setTimeout(() => {
+            assert.equal(editor.getValue(), "999");
+            assert.ok(editor.hoverTooltip != null);
+
+            var nodes = document.querySelectorAll(".ace_tooltip");
+            assert.equal(nodes.length, 2);
+            assert.equal(editor.hoverTooltip.isOpen, true);
+
+            mouse("down", editor.container, {button: 0});
+
+            setTimeout(() => {
+                assert.equal(editor.hoverTooltip.isOpen, false);
+
+                editor.setOption("readOnly", false);
+                sendKey("a");
+                setTimeout(() => {
+                    assert.equal(editor.getValue(), "a999");
+                    var nodes = document.querySelectorAll(".ace_tooltip");
+                    assert.equal(nodes.length, 1);
+                    done();
+                }, 6);
+            }, 6);
+        }, 6);
+    }
+};
+
+
+if (typeof module !== "undefined" && module === require.main) {
+    require("asyncjs").test.testcase(module.exports).exec();
+}
diff --git a/src/lib/default_english_messages.js b/src/lib/default_english_messages.js
index 2b3a840a3f2..12e67c2fe36 100644
--- a/src/lib/default_english_messages.js
+++ b/src/lib/default_english_messages.js
@@ -44,7 +44,8 @@ var defaultEnglishMessages = {
     "gutter-tooltip.aria-label.security.singular": "security finding",
     "gutter-tooltip.aria-label.security.plural": "security findings",
     "gutter-tooltip.aria-label.hint.singular": "suggestion",
-    "gutter-tooltip.aria-label.hint.plural": "suggestions"
+    "gutter-tooltip.aria-label.hint.plural": "suggestions",
+    "editor.tooltip.disable-editing": "Editing is disabled"
 }
 
 exports.defaultEnglishMessages = defaultEnglishMessages;
\ No newline at end of file
diff --git a/src/test/all_browser.js b/src/test/all_browser.js
index b23ba6b48db..5112b367111 100644
--- a/src/test/all_browser.js
+++ b/src/test/all_browser.js
@@ -34,6 +34,7 @@ var testNames = [
     "ace/editor_navigation_test",
     "ace/editor_text_edit_test",
     "ace/editor_commands_test",
+    "ace/editor_options_test",
     "ace/ext/command_bar_test",
     "ace/ext/hardwrap_test",
     "ace/ext/inline_autocomplete_test",
diff --git a/src/tooltip.js b/src/tooltip.js
index 1e407427f29..a83fce5ab00 100644
--- a/src/tooltip.js
+++ b/src/tooltip.js
@@ -308,7 +308,7 @@ class HoverTooltip extends Tooltip {
      * @param {Editor} editor
      * @param {Range} range
      * @param {HTMLElement} domNode
-     * @param {MouseEvent} startingEvent
+     * @param {MouseEvent} [startingEvent]
      */
     showForRange(editor, range, domNode, startingEvent) {
         var MARGIN = 10;
diff --git a/types/ace-ext.d.ts b/types/ace-ext.d.ts
index a34d32a108a..d23aee73543 100644
--- a/types/ace-ext.d.ts
+++ b/types/ace-ext.d.ts
@@ -176,6 +176,7 @@ declare module "ace-code/src/ext/searchbox" {
         setEditor(editor: Editor): void;
         editor: Editor;
         searchRange: any;
+        onEditorInput(): void;
         searchBox: HTMLElement;
         replaceBox: HTMLElement;
         searchOption: HTMLInputElement;
diff --git a/types/ace-modules.d.ts b/types/ace-modules.d.ts
index 06cbbba6a7f..930da09c49c 100644
--- a/types/ace-modules.d.ts
+++ b/types/ace-modules.d.ts
@@ -1640,7 +1640,7 @@ declare module "ace-code/src/tooltip" {
         removeFromEditor(editor: Editor): void;
         isOutsideOfText(e: MouseEvent): boolean;
         setDataProvider(value: (event: MouseEvent, editor: Editor) => void): void;
-        showForRange(editor: Editor, range: Range, domNode: HTMLElement, startingEvent: MouseEvent): void;
+        showForRange(editor: Editor, range: Range, domNode: HTMLElement, startingEvent?: MouseEvent): void;
         range: Range;
         addMarker(range: Range, session?: EditSession): void;
         marker: number;
@@ -1682,6 +1682,8 @@ declare module "ace-code/src/mouse/default_gutter_handler" {
     export interface GutterHandler {
     }
     export type MouseHandler = import("ace-code/src/mouse/mouse_handler").MouseHandler;
+    export const GUTTER_TOOLTIP_LEFT_OFFSET: 5;
+    export const GUTTER_TOOLTIP_TOP_OFFSET: 3;
     export class GutterTooltip extends Tooltip {
         static get annotationLabels(): {
             error: {
@@ -1706,9 +1708,10 @@ declare module "ace-code/src/mouse/default_gutter_handler" {
             };
         };
         static annotationsToSummaryString(annotations: any): string;
-        constructor(editor: any);
+        constructor(editor: any, isHover?: boolean);
         editor: any;
         visibleTooltipRow: number | undefined;
+        onMouseOut(e: any): void;
         setPosition(x: any, y: any): void;
         showTooltip(row: any): void;
         hideTooltip(): void;
@@ -1831,6 +1834,7 @@ declare module "ace-code/src/search" {
          * @param {EditSession} session The session to search with
         **/
         findAll(session: EditSession): Range[];
+        parseReplaceString(replaceString: any): any;
         /**
          * Searches for `options.needle` in `input`, and, if found, replaces it with `replacement`.
          * @param {String} input The text to search in
@@ -3337,6 +3341,7 @@ declare module "ace-code/src/search_highlight" {
         constructor(regExp: any, clazz: string, type?: string);
         clazz: string;
         type: string;
+        docLen: number;
         setRegexp(regExp: any): void;
         regExp: any;
         cache: any[];


Reviews:
user: login: akoreman
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T13:27:25Z
user: login: mkslanc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T14:34:28Z
user: login: marinsokol5
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:10:02Z
user: login: akoreman
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T16:12:23Z

Review comments:
diff_hunk: @@ -2836,9 +2838,32 @@ config.defineOptions(Editor.prototype, "editor", {
         initialValue: true
     },
     readOnly: {
-        set: function(readOnly) {
+        set: function(/**@type{boolean}*/readOnly) {
             this.textInput.setReadOnly(readOnly);
             this.$resetCursorStyle();
+            if (!this.readOnlyCallback) {
+                this.readOnlyCallback = () => {
+                    if (!this.hoverTooltip) {
+                        this.hoverTooltip = new HoverTooltip();
+                    }
+                    var domNode = dom.createElement("div");
+                    domNode.innerText = "Editing is disabled";
path: src/editor.js
user: login: akoreman
body: We should use the `nls` utility to allow integrators to translate this string into whatever locale they want.
created_at: 2025-03-14T13:27:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2836,9 +2838,32 @@ config.defineOptions(Editor.prototype, "editor", {
         initialValue: true
     },
     readOnly: {
-        set: function(readOnly) {
+        set: function(/**@type{boolean}*/readOnly) {
             this.textInput.setReadOnly(readOnly);
             this.$resetCursorStyle();
+            if (!this.readOnlyCallback) {
+                this.readOnlyCallback = () => {
+                    if (!this.hoverTooltip) {
+                        this.hoverTooltip = new HoverTooltip();
+                    }
+                    var domNode = dom.createElement("div");
+                    domNode.innerText = "Editing is disabled";
path: src/editor.js
user: login: mkslanc
body: Right, totally forgot about it, added nls message and covered by tests
created_at: 2025-03-14T14:34:28Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-14T13:27:54Z
body: ## [Codecov](https://app.codecov.io/gh/ajaxorg/ace/pull/5771?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg) Report
Attention: Patch coverage is `97.10145%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 87.16%. Comparing base [(`2516181`)](https://app.codecov.io/gh/ajaxorg/ace/commit/2516181c4edf3e638158e5fa4b7aa4c8826794e8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg) to head [(`7fe479e`)](https://app.codecov.io/gh/ajaxorg/ace/commit/7fe479e8c9cf0a300371ceabb77a3c00f5ba53d1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg).
> Report is 1 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/ajaxorg/ace/pull/5771?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg) | Patch % | Lines |
|---|---|---|
| [src/editor\_options\_test.js](https://app.codecov.io/gh/ajaxorg/ace/pull/5771?src=pr&el=tree&filepath=src%2Feditor_options_test.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg#diff-c3JjL2VkaXRvcl9vcHRpb25zX3Rlc3QuanM=) | 92.98% | [4 Missing :warning: ](https://app.codecov.io/gh/ajaxorg/ace/pull/5771?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5771      +/-   ##
==========================================
+ Coverage   87.15%   87.16%   +0.01%     
==========================================
  Files         602      603       +1     
  Lines       44157    44214      +57     
  Branches     7240     7248       +8     
==========================================
+ Hits        38484    38539      +55     
- Misses       5673     5675       +2     
```

| [Flag](https://app.codecov.io/gh/ajaxorg/ace/pull/5771/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/ajaxorg/ace/pull/5771/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg) | `87.16% <97.10%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ajaxorg/ace/pull/5771?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ajaxorg).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: marinsokol5
created_at: 2025-03-17T16:22:16Z
body: It's not related to this change, since we just reuse the HoverTooltip, but my VoiceOver on MacOS in Chrome doesn't read the tooltip content at all. Might work for other browser/OS/SR implementation though.
author_association: CONTRIBUTOR
user: login: akoreman
created_at: 2025-03-19T08:52:08Z
body: @mkslanc Thanks for fixing the failing build! It seems we introduced a merge conflict with this CR, could you resolve that conflict 🙂 
author_association: CONTRIBUTOR
user: login: mkslanc
created_at: 2025-03-19T09:13:18Z
body: > @mkslanc Thanks for fixing the failing build! It seems we introduced a merge conflict with this CR, could you resolve that conflict 🙂

Done :) Hope it helped
author_association: CONTRIBUTOR