From 26f4410e8d2c5211ee9042b0e5fedf4f2b93a173 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Wed, 10 Sep 2025 13:35:15 +0400 Subject: [PATCH 1/8] WIP: gutter hover tooltip based on hover tooltip --- src/mouse/default_gutter_handler.js | 305 +++++++++++++++++++++++++--- 1 file changed, 272 insertions(+), 33 deletions(-) diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index 0c1735d6d97..f0a3f9aadd7 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -4,7 +4,10 @@ */ var dom = require("../lib/dom"); var event = require("../lib/event"); +const popupManager = require("../tooltip").popupManager; +const MouseEvent = require("./mouse_event").MouseEvent; var Tooltip = require("../tooltip").Tooltip; +var HoverTooltip = require("../tooltip").HoverTooltip; var nls = require("../config").nls; const GUTTER_TOOLTIP_LEFT_OFFSET = 5; @@ -20,7 +23,13 @@ exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET; function GutterHandler(mouseHandler) { var editor = mouseHandler.editor; var gutter = editor.renderer.$gutterLayer; - var tooltip = new GutterTooltip(editor, true); + var tooltip = new GutterHoverTooltip(editor); + tooltip.addToEditor(editor); + + tooltip.setDataProvider(function(e, editor) { + var row = e.getDocumentPosition().row; + tooltip.showTooltip(row); + }); mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) { if (!editor.isFocused() || e.getButton() != 0) @@ -52,23 +61,11 @@ function GutterHandler(mouseHandler) { function showTooltip() { var row = mouseEvent.getDocumentPosition().row; - var maxRow = editor.session.getLength(); - if (row == maxRow) { - var screenRow = editor.renderer.pixelToScreenCoordinates(0, mouseEvent.y).row; - var pos = mouseEvent.$pos; - if (screenRow > editor.session.documentToScreenRow(pos.row, pos.column)) - return hideTooltip(); - } - tooltip.showTooltip(row); if (!tooltip.isOpen) return; - editor.on("mousewheel", hideTooltip); - editor.on("changeSession", hideTooltip); - window.addEventListener("keydown", hideTooltip, true); - if (mouseHandler.$tooltipFollowsMouse) { moveTooltip(mouseEvent); } else { @@ -86,28 +83,11 @@ function GutterHandler(mouseHandler) { } } - function hideTooltip(e) { - // dont close tooltip in case the user wants to copy text from it (Ctrl/Meta + C) - if (e && e.type === "keydown" && (e.ctrlKey || e.metaKey)) - return; - // in case mouse moved but is still on the tooltip, dont close it - if (e && e.type === "mouseout" && (!e.relatedTarget || tooltip.getElement().contains(e.relatedTarget))) - return; - if (tooltipTimeout) - tooltipTimeout = clearTimeout(tooltipTimeout); - if (tooltip.isOpen) { - tooltip.hideTooltip(); - editor.off("mousewheel", hideTooltip); - editor.off("changeSession", hideTooltip); - window.removeEventListener("keydown", hideTooltip, true); - } - } - function moveTooltip(e) { - tooltip.setPosition(e.x, e.y); + //tooltip.setPosition(e.x, e.y); } - mouseHandler.editor.setDefaultHandler("guttermousemove", function(e) { + /* mouseHandler.editor.setDefaultHandler("guttermousemove", function(e) { var target = e.domEvent.target || e.domEvent.srcElement; if (dom.hasCssClass(target, "ace_fold-widget") || dom.hasCssClass(target, "ace_custom-widget")) return hideTooltip(); @@ -134,7 +114,7 @@ function GutterHandler(mouseHandler) { tooltipTimeout = null; hideTooltip(e); }, 50); - }, editor); + }, editor);*/ } exports.GutterHandler = GutterHandler; @@ -351,6 +331,265 @@ class GutterTooltip extends Tooltip { } } +class GutterHoverTooltip extends HoverTooltip { + constructor(editor) { + super(editor.container); + this.id = "gt" + (++GutterTooltip.$uid); + this.editor = editor; + /**@type {Number | Undefined}*/ + this.visibleTooltipRow; + var el = this.getElement(); + el.setAttribute("role", "tooltip"); + el.setAttribute("id", this.id); + el.style.pointerEvents = "auto"; + + this.onDomMouseMove = this.onDomMouseMove.bind(this); + this.onDomMouseOut = this.onDomMouseOut.bind(this); + } + + onDomMouseMove(domEvent) { + const aceEvent = new MouseEvent(domEvent, this.editor); + this.onMouseMove(aceEvent, this.editor); + } + + onDomMouseOut(domEvent) { + const aceEvent = new MouseEvent(domEvent, this.editor); + this.onMouseOut(aceEvent); + } + + addToEditor(editor) { + const gutter = editor.renderer.$gutter; + gutter.addEventListener("mousemove", this.onDomMouseMove); + gutter.addEventListener("mouseout", this.onDomMouseOut); + } + + removeFromEditor(editor) { + const gutter = editor.renderer.$gutter; + gutter.removeEventListener("mousemove", this.onDomMouseMove); + gutter.removeEventListener("mouseout", this.onDomMouseOut); + } + + static get annotationLabels() { + return { + error: { + singular: nls("gutter-tooltip.aria-label.error.singular", "error"), + plural: nls("gutter-tooltip.aria-label.error.plural", "errors") + }, + security: { + singular: nls("gutter-tooltip.aria-label.security.singular", "security finding"), + plural: nls("gutter-tooltip.aria-label.security.plural", "security findings") + }, + warning: { + singular: nls("gutter-tooltip.aria-label.warning.singular", "warning"), + plural: nls("gutter-tooltip.aria-label.warning.plural", "warnings") + }, + info: { + singular: nls("gutter-tooltip.aria-label.info.singular", "information message"), + plural: nls("gutter-tooltip.aria-label.info.plural", "information messages") + }, + hint: { + singular: nls("gutter-tooltip.aria-label.hint.singular", "suggestion"), + plural: nls("gutter-tooltip.aria-label.hint.plural", "suggestions") + } + }; + } + + showTooltip(row) { + var gutter = this.editor.renderer.$gutterLayer; + var annotationsInRow = gutter.$annotations[row]; + var annotation; + + if (annotationsInRow) + annotation = { + displayText: Array.from(annotationsInRow.displayText), + type: Array.from(annotationsInRow.type) + }; + else annotation = {displayText: [], type: []}; + + // If the tooltip is for a row which has a closed fold, check whether there are + // annotations in the folded lines. If so, add a summary to the list of annotations. + var fold = gutter.session.getFoldLine(row); + if (fold && gutter.$showFoldedAnnotations) { + var annotationsInFold = {error: [], security: [], warning: [], info: [], hint: []}; + var severityRank = {error: 1, security: 2, warning: 3, info: 4, hint: 5}; + var mostSevereAnnotationTypeInFold; + + for (let i = row + 1; i <= fold.end.row; i++) { + if (!gutter.$annotations[i]) continue; + + for (var j = 0; j < gutter.$annotations[i].text.length; j++) { + var annotationType = gutter.$annotations[i].type[j]; + annotationsInFold[annotationType].push(gutter.$annotations[i].text[j]); + + if ( + !mostSevereAnnotationTypeInFold || + severityRank[annotationType] < severityRank[mostSevereAnnotationTypeInFold] + ) { + mostSevereAnnotationTypeInFold = annotationType; + } + } + } + + if (["error", "security", "warning"].includes(mostSevereAnnotationTypeInFold)) { + var summaryFoldedAnnotations = `${GutterTooltip.annotationsToSummaryString( + annotationsInFold + )} in folded code.`; + + annotation.displayText.push(summaryFoldedAnnotations); + annotation.type.push(mostSevereAnnotationTypeInFold + "_fold"); + } + } + + if (annotation.displayText.length === 0) return this.hideTooltip(); + + var annotationMessages = {error: [], security: [], warning: [], info: [], hint: []}; + var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon"; + + // Construct the contents of the tooltip. + for (let i = 0; i < annotation.displayText.length; i++) { + var lineElement = dom.createElement("span"); + + var iconElement = dom.createElement("span"); + iconElement.classList.add(...[`ace_${annotation.type[i]}`, iconClassName]); + iconElement.setAttribute( + "aria-label", + `${GutterTooltip.annotationLabels[annotation.type[i].replace("_fold", "")].singular}` + ); + iconElement.setAttribute("role", "img"); + // Set empty content to the img span to get it to show up + iconElement.appendChild(dom.createTextNode(" ")); + + lineElement.appendChild(iconElement); + lineElement.appendChild(dom.createTextNode(annotation.displayText[i])); + lineElement.appendChild(dom.createElement("br")); + + annotationMessages[annotation.type[i].replace("_fold", "")].push(lineElement); + } + + var tooltipElement = dom.createElement("span"); + + // Update the tooltip content + annotationMessages.error.forEach((el) => tooltipElement.appendChild(el)); + annotationMessages.security.forEach((el) => tooltipElement.appendChild(el)); + annotationMessages.warning.forEach((el) => tooltipElement.appendChild(el)); + annotationMessages.info.forEach((el) => tooltipElement.appendChild(el)); + annotationMessages.hint.forEach((el) => tooltipElement.appendChild(el)); + + tooltipElement.setAttribute("aria-live", "polite"); + + const annotationNode = this.$findLinkedAnnotationNode(row); + if (annotationNode) { + annotationNode.setAttribute("aria-describedby", this.id); + } + + this.showForPosition( {row, column: 0}, tooltipElement); + //this.show(); + this.visibleTooltipRow = row; + this.editor._signal("showGutterTooltip", this); + } + + showForPosition(position, domNode) { + var MARGIN = 10; + if (this.isOpen && document.activeElement == this.getElement()) return; + + var renderer = this.editor.renderer; + if (!this.isOpen) { + popupManager.addPopup(this); + this.$registerCloseEvents(); + this.setTheme(renderer.theme); + this.setClassName("ace_gutter-tooltip"); + } + this.isOpen = true; + + //TODO: this.addMarker(range, editor.session); + var position = renderer.textToScreenCoordinates(position.row, position.column); + + var rect = renderer.$gutter.getBoundingClientRect(); + + if (position.pageX > rect.right) + position.pageX = rect.right; + + var element = this.getElement(); + element.innerHTML = ""; + element.appendChild(domNode); + + element.style.maxHeight = ""; + element.style.display = "block"; + + // measure the size of tooltip, without constraints on its height + var labelHeight = element.clientHeight; + var spaceBelow = window.innerHeight - position.pageY - renderer.lineHeight; + + // show tooltip below by default, only show above if there's no space below + let isAbove = false; + if (spaceBelow < labelHeight) { + isAbove = true; + } + //TODO: isAbove rendering logic is out + element.style.maxHeight = (isAbove ? position.pageY : spaceBelow) - MARGIN + "px"; + element.style.top = isAbove ? "" : position.pageY + renderer.lineHeight + "px"; + element.style.bottom = isAbove ? window.innerHeight - position.pageY - renderer.lineHeight + "px" : ""; + + // try to align tooltip left with the range, but keep it on screen + element.style.left = 17 + "px"; + } + + $findLinkedAnnotationNode(row) { + const cell = this.$findCellByRow(row); + if (cell) { + const element = cell.element; + if (element.childNodes.length > 2) { + return element.childNodes[2]; + } + } + } + + $findCellByRow(row) { + return this.editor.renderer.$gutterLayer.$lines.cells.find((el) => el.row === row); + } + + hideTooltip() { + if(!this.isOpen){ + return; + } + this.$element.removeAttribute("aria-live"); + this.hide(); + + if (this.visibleTooltipRow != undefined) { + const annotationNode = this.$findLinkedAnnotationNode(this.visibleTooltipRow); + if (annotationNode) { + annotationNode.removeAttribute("aria-describedby"); + } + } + + this.visibleTooltipRow = undefined; + this.editor._signal("hideGutterTooltip", this); + } + + static annotationsToSummaryString(annotations) { + const summary = []; + const annotationTypes = ["error", "security", "warning", "info", "hint"]; + for (const annotationType of annotationTypes) { + if (!annotations[annotationType].length) continue; + const label = annotations[annotationType].length === 1 ? GutterTooltip.annotationLabels[annotationType].singular : GutterTooltip.annotationLabels[annotationType].plural; + summary.push(`${annotations[annotationType].length} ${label}`); + } + return summary.join(", "); + } + + /** + * Check if cursor is outside gutter + * @param e + * @return {boolean} + */ + isOutsideOfText(e) { + var editor = e.editor; + var rect = editor.renderer.$gutter.getBoundingClientRect(); + return !(e.clientX >= rect.left && e.clientX <= rect.right && + e.clientY >= rect.top && e.clientY <= rect.bottom); + } +} + GutterTooltip.$uid = 0; exports.GutterTooltip = GutterTooltip; From 2ba3927942ee95ed11d7109c0ba73744f10bb725 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Thu, 11 Sep 2025 15:21:35 +0400 Subject: [PATCH 2/8] Refactor gutter tooltip to reuse hover tooltip functionality and align position logic. --- src/mouse/default_gutter_handler.js | 323 +++------------------------- src/tooltip.js | 8 +- 2 files changed, 37 insertions(+), 294 deletions(-) diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index f0a3f9aadd7..5eb6bac774b 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -9,6 +9,7 @@ const MouseEvent = require("./mouse_event").MouseEvent; var Tooltip = require("../tooltip").Tooltip; var HoverTooltip = require("../tooltip").HoverTooltip; var nls = require("../config").nls; +var Range = require("../range").Range; const GUTTER_TOOLTIP_LEFT_OFFSET = 5; const GUTTER_TOOLTIP_TOP_OFFSET = 3; @@ -23,7 +24,7 @@ exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET; function GutterHandler(mouseHandler) { var editor = mouseHandler.editor; var gutter = editor.renderer.$gutterLayer; - var tooltip = new GutterHoverTooltip(editor); + var tooltip = new GutterTooltip(editor); tooltip.addToEditor(editor); tooltip.setDataProvider(function(e, editor) { @@ -55,283 +56,12 @@ function GutterHandler(mouseHandler) { mouseHandler.captureMouse(e); return e.preventDefault(); }); - - var tooltipTimeout, mouseEvent; - - function showTooltip() { - var row = mouseEvent.getDocumentPosition().row; - - tooltip.showTooltip(row); - - if (!tooltip.isOpen) - return; - - if (mouseHandler.$tooltipFollowsMouse) { - moveTooltip(mouseEvent); - } else { - var gutterRow = mouseEvent.getGutterRow(); - var gutterCell = gutter.$lines.get(gutterRow); - if (gutterCell) { - var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation"); - var rect = gutterElement.getBoundingClientRect(); - var style = tooltip.getElement().style; - style.left = (rect.right - GUTTER_TOOLTIP_LEFT_OFFSET) + "px"; - style.top = (rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET) + "px"; - } else { - moveTooltip(mouseEvent); - } - } - } - - function moveTooltip(e) { - //tooltip.setPosition(e.x, e.y); - } - - /* mouseHandler.editor.setDefaultHandler("guttermousemove", function(e) { - var target = e.domEvent.target || e.domEvent.srcElement; - if (dom.hasCssClass(target, "ace_fold-widget") || dom.hasCssClass(target, "ace_custom-widget")) - return hideTooltip(); - - if (tooltip.isOpen && mouseHandler.$tooltipFollowsMouse) - moveTooltip(e); - - mouseEvent = e; - if (tooltipTimeout) - return; - tooltipTimeout = setTimeout(function() { - tooltipTimeout = null; - if (mouseEvent && !mouseHandler.isMousePressed) - showTooltip(); - }, 50); - }); - - event.addListener(editor.renderer.$gutter, "mouseout", function(e) { - mouseEvent = null; - if (!tooltip.isOpen) - return; - - tooltipTimeout = setTimeout(function() { - tooltipTimeout = null; - hideTooltip(e); - }, 50); - }, editor);*/ } exports.GutterHandler = GutterHandler; -class GutterTooltip extends Tooltip { - constructor(editor, isHover = false) { - super(editor.container); - this.id = "gt" + (++GutterTooltip.$uid); - this.editor = editor; - /**@type {Number | Undefined}*/ - this.visibleTooltipRow; - var el = this.getElement(); - el.setAttribute("role", "tooltip"); - el.setAttribute("id", this.id); - el.style.pointerEvents = "auto"; - if (isHover) { - this.onMouseOut = this.onMouseOut.bind(this); - el.addEventListener("mouseout", this.onMouseOut); - } - } - - // handler needed to hide tooltip after mouse hovers from tooltip to editor - onMouseOut(e) { - if (!this.isOpen) return; - - if (!e.relatedTarget || this.getElement().contains(e.relatedTarget)) return; - - if (e && e.currentTarget.contains(e.relatedTarget)) return; - this.hideTooltip(); - } - - setPosition(x, y) { - var windowWidth = window.innerWidth || document.documentElement.clientWidth; - var windowHeight = window.innerHeight || document.documentElement.clientHeight; - var width = this.getWidth(); - var height = this.getHeight(); - x += 15; - y += 15; - if (x + width > windowWidth) { - x -= (x + width) - windowWidth; - } - if (y + height > windowHeight) { - y -= 20 + height; - } - Tooltip.prototype.setPosition.call(this, x, y); - } - - static get annotationLabels() { - return { - error: { - singular: nls("gutter-tooltip.aria-label.error.singular", "error"), - plural: nls("gutter-tooltip.aria-label.error.plural", "errors") - }, - security: { - singular: nls("gutter-tooltip.aria-label.security.singular", "security finding"), - plural: nls("gutter-tooltip.aria-label.security.plural", "security findings") - }, - warning: { - singular: nls("gutter-tooltip.aria-label.warning.singular", "warning"), - plural: nls("gutter-tooltip.aria-label.warning.plural", "warnings") - }, - info: { - singular: nls("gutter-tooltip.aria-label.info.singular", "information message"), - plural: nls("gutter-tooltip.aria-label.info.plural", "information messages") - }, - hint: { - singular: nls("gutter-tooltip.aria-label.hint.singular", "suggestion"), - plural: nls("gutter-tooltip.aria-label.hint.plural", "suggestions") - } - }; - } - - showTooltip(row) { - var gutter = this.editor.renderer.$gutterLayer; - var annotationsInRow = gutter.$annotations[row]; - var annotation; - - if (annotationsInRow) - annotation = { - displayText: Array.from(annotationsInRow.displayText), - type: Array.from(annotationsInRow.type) - }; - else annotation = {displayText: [], type: []}; - - // If the tooltip is for a row which has a closed fold, check whether there are - // annotations in the folded lines. If so, add a summary to the list of annotations. - var fold = gutter.session.getFoldLine(row); - if (fold && gutter.$showFoldedAnnotations) { - var annotationsInFold = {error: [], security: [], warning: [], info: [], hint: []}; - var severityRank = {error: 1, security: 2, warning: 3, info: 4, hint: 5}; - var mostSevereAnnotationTypeInFold; - - for (let i = row + 1; i <= fold.end.row; i++) { - if (!gutter.$annotations[i]) continue; - - for (var j = 0; j < gutter.$annotations[i].text.length; j++) { - var annotationType = gutter.$annotations[i].type[j]; - annotationsInFold[annotationType].push(gutter.$annotations[i].text[j]); - - if ( - !mostSevereAnnotationTypeInFold || - severityRank[annotationType] < severityRank[mostSevereAnnotationTypeInFold] - ) { - mostSevereAnnotationTypeInFold = annotationType; - } - } - } - - if (["error", "security", "warning"].includes(mostSevereAnnotationTypeInFold)) { - var summaryFoldedAnnotations = `${GutterTooltip.annotationsToSummaryString( - annotationsInFold - )} in folded code.`; - - annotation.displayText.push(summaryFoldedAnnotations); - annotation.type.push(mostSevereAnnotationTypeInFold + "_fold"); - } - } - - if (annotation.displayText.length === 0) return this.hideTooltip(); - - var annotationMessages = {error: [], security: [], warning: [], info: [], hint: []}; - var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon"; - - // Construct the contents of the tooltip. - for (let i = 0; i < annotation.displayText.length; i++) { - var lineElement = dom.createElement("span"); - var iconElement = dom.createElement("span"); - iconElement.classList.add(...[`ace_${annotation.type[i]}`, iconClassName]); - iconElement.setAttribute( - "aria-label", - `${GutterTooltip.annotationLabels[annotation.type[i].replace("_fold", "")].singular}` - ); - iconElement.setAttribute("role", "img"); - // Set empty content to the img span to get it to show up - iconElement.appendChild(dom.createTextNode(" ")); - - lineElement.appendChild(iconElement); - lineElement.appendChild(dom.createTextNode(annotation.displayText[i])); - lineElement.appendChild(dom.createElement("br")); - - annotationMessages[annotation.type[i].replace("_fold", "")].push(lineElement); - } - - // Clear the current tooltip content - var tooltipElement = this.getElement(); - dom.removeChildren(tooltipElement); - - // Update the tooltip content - annotationMessages.error.forEach((el) => tooltipElement.appendChild(el)); - annotationMessages.security.forEach((el) => tooltipElement.appendChild(el)); - annotationMessages.warning.forEach((el) => tooltipElement.appendChild(el)); - annotationMessages.info.forEach((el) => tooltipElement.appendChild(el)); - annotationMessages.hint.forEach((el) => tooltipElement.appendChild(el)); - - tooltipElement.setAttribute("aria-live", "polite"); - - if (!this.isOpen) { - this.setTheme(this.editor.renderer.theme); - this.setClassName("ace_gutter-tooltip"); - } - - const annotationNode = this.$findLinkedAnnotationNode(row); - if (annotationNode) { - annotationNode.setAttribute("aria-describedby", this.id); - } - - this.show(); - this.visibleTooltipRow = row; - this.editor._signal("showGutterTooltip", this); - } - - $findLinkedAnnotationNode(row) { - const cell = this.$findCellByRow(row); - if (cell) { - const element = cell.element; - if (element.childNodes.length > 2) { - return element.childNodes[2]; - } - } - } - - $findCellByRow(row) { - return this.editor.renderer.$gutterLayer.$lines.cells.find((el) => el.row === row); - } - - hideTooltip() { - if(!this.isOpen){ - return; - } - this.$element.removeAttribute("aria-live"); - this.hide(); - - if (this.visibleTooltipRow != undefined) { - const annotationNode = this.$findLinkedAnnotationNode(this.visibleTooltipRow); - if (annotationNode) { - annotationNode.removeAttribute("aria-describedby"); - } - } - - this.visibleTooltipRow = undefined; - this.editor._signal("hideGutterTooltip", this); - } - - static annotationsToSummaryString(annotations) { - const summary = []; - const annotationTypes = ["error", "security", "warning", "info", "hint"]; - for (const annotationType of annotationTypes) { - if (!annotations[annotationType].length) continue; - const label = annotations[annotationType].length === 1 ? GutterTooltip.annotationLabels[annotationType].singular : GutterTooltip.annotationLabels[annotationType].plural; - summary.push(`${annotations[annotationType].length} ${label}`); - } - return summary.join(", "); - } -} - -class GutterHoverTooltip extends HoverTooltip { +class GutterTooltip extends HoverTooltip { constructor(editor) { super(editor.container); this.id = "gt" + (++GutterTooltip.$uid); @@ -482,27 +212,25 @@ class GutterHoverTooltip extends HoverTooltip { annotationNode.setAttribute("aria-describedby", this.id); } - this.showForPosition( {row, column: 0}, tooltipElement); - //this.show(); + const range = Range.fromPoints({row, column: 0}, {row, column: 0}); + this.showForRange(this.editor, range, tooltipElement); this.visibleTooltipRow = row; this.editor._signal("showGutterTooltip", this); } - showForPosition(position, domNode) { + /** + * @param {import("../editor").Editor} editor + * @param {Range} range + * @param {HTMLElement} domNode + */ + $setPosition(editor, range, domNode) { var MARGIN = 10; - if (this.isOpen && document.activeElement == this.getElement()) return; - - var renderer = this.editor.renderer; - if (!this.isOpen) { - popupManager.addPopup(this); - this.$registerCloseEvents(); - this.setTheme(renderer.theme); - this.setClassName("ace_gutter-tooltip"); - } - this.isOpen = true; + var renderer = editor.renderer; //TODO: this.addMarker(range, editor.session); - var position = renderer.textToScreenCoordinates(position.row, position.column); + this.range = Range.fromPoints(range.start, range.end); + + var position = renderer.textToScreenCoordinates(range.start.row, range.start.column); var rect = renderer.$gutter.getBoundingClientRect(); @@ -516,6 +244,12 @@ class GutterHoverTooltip extends HoverTooltip { element.style.maxHeight = ""; element.style.display = "block"; + if (editor.getOption("tooltipFollowsMouse")) { + //TODO: + this.setPosition(this.lastEvent.x, this.lastEvent.y); + return; + } + // measure the size of tooltip, without constraints on its height var labelHeight = element.clientHeight; var spaceBelow = window.innerHeight - position.pageY - renderer.lineHeight; @@ -525,13 +259,16 @@ class GutterHoverTooltip extends HoverTooltip { if (spaceBelow < labelHeight) { isAbove = true; } - //TODO: isAbove rendering logic is out + const gutterCell = this.$findCellByRow(range.start.row); + if (gutterCell) { + var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation"); + rect = gutterElement.getBoundingClientRect(); + element.style.left = (rect.width - GUTTER_TOOLTIP_LEFT_OFFSET) + "px"; + + element.style.bottom = isAbove ? rect.height - GUTTER_TOOLTIP_TOP_OFFSET + "px" : "" ; + element.style.top = isAbove ? "" : rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET + "px"; + } element.style.maxHeight = (isAbove ? position.pageY : spaceBelow) - MARGIN + "px"; - element.style.top = isAbove ? "" : position.pageY + renderer.lineHeight + "px"; - element.style.bottom = isAbove ? window.innerHeight - position.pageY - renderer.lineHeight + "px" : ""; - - // try to align tooltip left with the range, but keep it on screen - element.style.left = 17 + "px"; } $findLinkedAnnotationNode(row) { diff --git a/src/tooltip.js b/src/tooltip.js index 6454863bd37..9ca65922050 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -313,7 +313,6 @@ class HoverTooltip extends Tooltip { * @param {MouseEvent} [startingEvent] */ showForRange(editor, range, domNode, startingEvent) { - var MARGIN = 10; if (startingEvent && startingEvent != this.lastEvent) return; if (this.isOpen && document.activeElement == this.getElement()) return; @@ -325,6 +324,13 @@ class HoverTooltip extends Tooltip { } this.isOpen = true; + this.$setPosition(editor, range, domNode); + } + + $setPosition(editor, range, domNode) { + var MARGIN = 10; + var renderer = editor.renderer; + this.addMarker(range, editor.session); this.range = Range.fromPoints(range.start, range.end); var position = renderer.textToScreenCoordinates(range.start.row, range.start.column); From 34193b4c1f47a37a4e770fc5584cd6a57d532871 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Thu, 11 Sep 2025 17:03:08 +0400 Subject: [PATCH 3/8] Adjust gutter tooltip offsets and ensure absolute positioning. --- src/mouse/default_gutter_handler.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index 5eb6bac774b..ecef9fdc23a 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -11,7 +11,7 @@ var HoverTooltip = require("../tooltip").HoverTooltip; var nls = require("../config").nls; var Range = require("../range").Range; -const GUTTER_TOOLTIP_LEFT_OFFSET = 5; +const GUTTER_TOOLTIP_LEFT_OFFSET = 3; const GUTTER_TOOLTIP_TOP_OFFSET = 3; exports.GUTTER_TOOLTIP_LEFT_OFFSET = GUTTER_TOOLTIP_LEFT_OFFSET; exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET; @@ -72,6 +72,7 @@ class GutterTooltip extends HoverTooltip { el.setAttribute("role", "tooltip"); el.setAttribute("id", this.id); el.style.pointerEvents = "auto"; + el.style.position = "absolute"; this.onDomMouseMove = this.onDomMouseMove.bind(this); this.onDomMouseOut = this.onDomMouseOut.bind(this); @@ -91,12 +92,14 @@ class GutterTooltip extends HoverTooltip { const gutter = editor.renderer.$gutter; gutter.addEventListener("mousemove", this.onDomMouseMove); gutter.addEventListener("mouseout", this.onDomMouseOut); + super.addToEditor(editor); } removeFromEditor(editor) { const gutter = editor.renderer.$gutter; gutter.removeEventListener("mousemove", this.onDomMouseMove); gutter.removeEventListener("mouseout", this.onDomMouseOut); + super.removeFromEditor(editor); } static get annotationLabels() { From e050f620879099d710335a8c3dea9f906df11835 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Mon, 15 Sep 2025 17:16:07 +0400 Subject: [PATCH 4/8] refactor gutter tooltip handling: unify hide method, remove unused code, and improve positioning logic. --- src/keyboard/gutter_handler.js | 6 +---- src/mouse/default_gutter_handler.js | 41 ++++++----------------------- src/tooltip.js | 25 ++++++++++++------ 3 files changed, 26 insertions(+), 46 deletions(-) diff --git a/src/keyboard/gutter_handler.js b/src/keyboard/gutter_handler.js index d8f99dd07b6..919e62d0b92 100644 --- a/src/keyboard/gutter_handler.js +++ b/src/keyboard/gutter_handler.js @@ -32,10 +32,6 @@ class GutterKeyboardHandler { // if the tooltip is open, we only want to respond to commands to close it (like a modal) if (this.annotationTooltip.isOpen) { e.preventDefault(); - - if (e.keyCode === keys["escape"]) - this.annotationTooltip.hideTooltip(); - return; } @@ -213,7 +209,7 @@ class GutterKeyboardHandler { } if (this.annotationTooltip.isOpen) - this.annotationTooltip.hideTooltip(); + this.annotationTooltip.hide(); return; } diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index ecef9fdc23a..11e8720f55c 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -3,10 +3,7 @@ * @typedef {import("./mouse_handler").MouseHandler} MouseHandler */ var dom = require("../lib/dom"); -var event = require("../lib/event"); -const popupManager = require("../tooltip").popupManager; const MouseEvent = require("./mouse_event").MouseEvent; -var Tooltip = require("../tooltip").Tooltip; var HoverTooltip = require("../tooltip").HoverTooltip; var nls = require("../config").nls; var Range = require("../range").Range; @@ -63,7 +60,7 @@ exports.GutterHandler = GutterHandler; class GutterTooltip extends HoverTooltip { constructor(editor) { - super(editor.container); + super(); this.id = "gt" + (++GutterTooltip.$uid); this.editor = editor; /**@type {Number | Undefined}*/ @@ -173,7 +170,7 @@ class GutterTooltip extends HoverTooltip { } } - if (annotation.displayText.length === 0) return this.hideTooltip(); + if (annotation.displayText.length === 0) return this.hide(); var annotationMessages = {error: [], security: [], warning: [], info: [], hint: []}; var iconClassName = gutter.$useSvgGutterIcons ? "ace_icon_svg" : "ace_icon"; @@ -221,31 +218,10 @@ class GutterTooltip extends HoverTooltip { this.editor._signal("showGutterTooltip", this); } - /** - * @param {import("../editor").Editor} editor - * @param {Range} range - * @param {HTMLElement} domNode - */ - $setPosition(editor, range, domNode) { + $setPosition(editor, position, range) { var MARGIN = 10; var renderer = editor.renderer; - - //TODO: this.addMarker(range, editor.session); - this.range = Range.fromPoints(range.start, range.end); - - var position = renderer.textToScreenCoordinates(range.start.row, range.start.column); - - var rect = renderer.$gutter.getBoundingClientRect(); - - if (position.pageX > rect.right) - position.pageX = rect.right; - var element = this.getElement(); - element.innerHTML = ""; - element.appendChild(domNode); - - element.style.maxHeight = ""; - element.style.display = "block"; if (editor.getOption("tooltipFollowsMouse")) { //TODO: @@ -265,10 +241,10 @@ class GutterTooltip extends HoverTooltip { const gutterCell = this.$findCellByRow(range.start.row); if (gutterCell) { var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation"); - rect = gutterElement.getBoundingClientRect(); - element.style.left = (rect.width - GUTTER_TOOLTIP_LEFT_OFFSET) + "px"; + const rect = gutterElement.getBoundingClientRect(); + element.style.left = (rect.right - GUTTER_TOOLTIP_LEFT_OFFSET) + "px"; - element.style.bottom = isAbove ? rect.height - GUTTER_TOOLTIP_TOP_OFFSET + "px" : "" ; + element.style.bottom = isAbove ? (window.innerHeight - rect.top - GUTTER_TOOLTIP_TOP_OFFSET) + "px" : "" ; element.style.top = isAbove ? "" : rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET + "px"; } element.style.maxHeight = (isAbove ? position.pageY : spaceBelow) - MARGIN + "px"; @@ -288,12 +264,12 @@ class GutterTooltip extends HoverTooltip { return this.editor.renderer.$gutterLayer.$lines.cells.find((el) => el.row === row); } - hideTooltip() { + hide(e) { + super.hide(e); if(!this.isOpen){ return; } this.$element.removeAttribute("aria-live"); - this.hide(); if (this.visibleTooltipRow != undefined) { const annotationNode = this.$findLinkedAnnotationNode(this.visibleTooltipRow); @@ -301,7 +277,6 @@ class GutterTooltip extends HoverTooltip { annotationNode.removeAttribute("aria-describedby"); } } - this.visibleTooltipRow = undefined; this.editor._signal("hideGutterTooltip", this); } diff --git a/src/tooltip.js b/src/tooltip.js index 9ca65922050..e663a29cf90 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -313,6 +313,7 @@ class HoverTooltip extends Tooltip { * @param {MouseEvent} [startingEvent] */ showForRange(editor, range, domNode, startingEvent) { + if (startingEvent && startingEvent != this.lastEvent) return; if (this.isOpen && document.activeElement == this.getElement()) return; @@ -324,14 +325,6 @@ class HoverTooltip extends Tooltip { } this.isOpen = true; - this.$setPosition(editor, range, domNode); - } - - $setPosition(editor, range, domNode) { - var MARGIN = 10; - var renderer = editor.renderer; - - this.addMarker(range, editor.session); this.range = Range.fromPoints(range.start, range.end); var position = renderer.textToScreenCoordinates(range.start.row, range.start.column); @@ -347,6 +340,22 @@ class HoverTooltip extends Tooltip { element.style.maxHeight = ""; element.style.display = "block"; + this.$setPosition(editor, position, range); + } + + /** + * + * @param {Editor} editor + * @param {{ pageX: number, pageY: number}} position + * @param {Range} [range] + */ + $setPosition(editor, position, range) { + var MARGIN = 10; + this.addMarker(range, editor.session); + + var renderer = editor.renderer; + var element = this.getElement(); + // measure the size of tooltip, without constraints on its height var labelHeight = element.clientHeight; var labelWidth = element.clientWidth; From a06f18ed93e5bd2303400207fb98ca1a439a7139 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Mon, 15 Sep 2025 17:22:38 +0400 Subject: [PATCH 5/8] remove tooltipFollowsMouse option in default gutter handler (for now) --- src/mouse/default_gutter_handler.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index 11e8720f55c..2209921e544 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -223,12 +223,6 @@ class GutterTooltip extends HoverTooltip { var renderer = editor.renderer; var element = this.getElement(); - if (editor.getOption("tooltipFollowsMouse")) { - //TODO: - this.setPosition(this.lastEvent.x, this.lastEvent.y); - return; - } - // measure the size of tooltip, without constraints on its height var labelHeight = element.clientHeight; var spaceBelow = window.innerHeight - position.pageY - renderer.lineHeight; From 145fd4b9c57cfd534eabfd4825fc5a7f377f94fe Mon Sep 17 00:00:00 2001 From: mkslanc Date: Mon, 29 Sep 2025 15:09:43 +0400 Subject: [PATCH 6/8] remove `tooltipFollowsMouse` option and refactor gutter tooltip logic to improve handling and positioning. --- ace-internal.d.ts | 3 +- ace.d.ts | 1 - src/editor.js | 1 - src/ext/options.js | 4 - src/keyboard/gutter_handler.js | 5 - src/keyboard/gutter_handler_test.js | 14 ++- src/mouse/default_gutter_handler.js | 17 +++- src/mouse/default_gutter_handler_test.js | 118 ++++++++++------------- src/mouse/mouse_handler.js | 2 +- src/tooltip.js | 28 +++++- types/ace-ext.d.ts | 4 - types/ace-modules.d.ts | 19 ++-- 12 files changed, 112 insertions(+), 104 deletions(-) diff --git a/ace-internal.d.ts b/ace-internal.d.ts index c0417ec25cd..ac9f46501e8 100644 --- a/ace-internal.d.ts +++ b/ace-internal.d.ts @@ -380,7 +380,6 @@ export namespace Ace { dragDelay: number; dragEnabled: boolean; focusTimeout: number; - tooltipFollowsMouse: boolean; } interface EditorOptions extends EditSessionOptions, @@ -1629,7 +1628,6 @@ declare module "./src/mouse/mouse_event" { declare module "./src/mouse/mouse_handler" { export interface MouseHandler { - $tooltipFollowsMouse?: boolean, cancelDrag?: boolean //from DefaultHandlers $clickSelection?: Ace.Range, @@ -1638,6 +1636,7 @@ declare module "./src/mouse/mouse_handler" { select?: () => void $lastScroll?: { t: number, vx: number, vy: number, allowed: number } selectEnd?: () => void + tooltip?: Ace.GutterTooltip } } diff --git a/ace.d.ts b/ace.d.ts index 04dadcec71a..8f5e42c3a8f 100644 --- a/ace.d.ts +++ b/ace.d.ts @@ -281,7 +281,6 @@ declare module "ace-code" { dragDelay: number; dragEnabled: boolean; focusTimeout: number; - tooltipFollowsMouse: boolean; } interface EditorOptions extends EditSessionOptions, MouseHandlerOptions, VirtualRendererOptions { selectionStyle: "fullLine" | "screenLine" | "text" | "line"; diff --git a/src/editor.js b/src/editor.js index 528f440ec36..3b5f5445f19 100644 --- a/src/editor.js +++ b/src/editor.js @@ -3121,7 +3121,6 @@ config.defineOptions(Editor.prototype, "editor", { dragDelay: "$mouseHandler", dragEnabled: "$mouseHandler", focusTimeout: "$mouseHandler", - tooltipFollowsMouse: "$mouseHandler", firstLineNumber: "session", overwrite: "session", diff --git a/src/ext/options.js b/src/ext/options.js index 6457003f45d..47a1685e49c 100644 --- a/src/ext/options.js +++ b/src/ext/options.js @@ -231,10 +231,6 @@ var optionGroups = { }, "Keyboard Accessibility Mode": { path: "enableKeyboardAccessibility" - }, - "Gutter tooltip follows mouse": { - path: "tooltipFollowsMouse", - defaultValue: true } } }; diff --git a/src/keyboard/gutter_handler.js b/src/keyboard/gutter_handler.js index 919e62d0b92..efda42288e8 100644 --- a/src/keyboard/gutter_handler.js +++ b/src/keyboard/gutter_handler.js @@ -182,11 +182,6 @@ class GutterKeyboardHandler { return; case "annotation": - var gutterElement = this.lines.cells[this.activeRowIndex].element.childNodes[2]; - var rect = gutterElement.getBoundingClientRect(); - var style = this.annotationTooltip.getElement().style; - style.left = rect.right + "px"; - style.top = rect.bottom + "px"; this.annotationTooltip.showTooltip(this.$rowIndexToRow(this.activeRowIndex)); break; } diff --git a/src/keyboard/gutter_handler_test.js b/src/keyboard/gutter_handler_test.js index c59d46738f5..74d8f612eeb 100644 --- a/src/keyboard/gutter_handler_test.js +++ b/src/keyboard/gutter_handler_test.js @@ -22,6 +22,16 @@ function emit(keyCode) { el.dispatchEvent(event); } +function findVisibleTooltip() { + const tooltips = document.body.querySelectorAll(".ace_gutter-tooltip"); + for (let i = 0; i < tooltips.length; i++) { + if (window.getComputedStyle(tooltips[i]).display === "block") { + return tooltips[i]; + } + } + return null; +} + module.exports = { setUp : function(done) { this.editor = new Editor(new VirtualRenderer()); @@ -153,7 +163,7 @@ module.exports = { setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = document.body.querySelector(".ace_gutter-tooltip"); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. @@ -164,7 +174,7 @@ module.exports = { assert.equal(document.activeElement, editor.renderer.$gutter); done(); - }, 20); + }, 400); }, 20); },"test: keyboard annotation: multiple annotations" : function(done) { var editor = this.editor; diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index 2209921e544..062fc7abd58 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -21,12 +21,12 @@ exports.GUTTER_TOOLTIP_TOP_OFFSET = GUTTER_TOOLTIP_TOP_OFFSET; function GutterHandler(mouseHandler) { var editor = mouseHandler.editor; var gutter = editor.renderer.$gutterLayer; - var tooltip = new GutterTooltip(editor); - tooltip.addToEditor(editor); + mouseHandler.tooltip = new GutterTooltip(editor); + mouseHandler.tooltip.addToEditor(editor); - tooltip.setDataProvider(function(e, editor) { + mouseHandler.tooltip.setDataProvider(function(e, editor) { var row = e.getDocumentPosition().row; - tooltip.showTooltip(row); + mouseHandler.tooltip.showTooltip(row); }); mouseHandler.editor.setDefaultHandler("guttermousedown", function(e) { @@ -73,6 +73,8 @@ class GutterTooltip extends HoverTooltip { this.onDomMouseMove = this.onDomMouseMove.bind(this); this.onDomMouseOut = this.onDomMouseOut.bind(this); + + this.setClassName("ace_gutter-tooltip"); } onDomMouseMove(domEvent) { @@ -99,6 +101,11 @@ class GutterTooltip extends HoverTooltip { super.removeFromEditor(editor); } + destroy(editor) { + this.removeFromEditor(editor); + super.destroy(); + } + static get annotationLabels() { return { error: { @@ -259,7 +266,6 @@ class GutterTooltip extends HoverTooltip { } hide(e) { - super.hide(e); if(!this.isOpen){ return; } @@ -273,6 +279,7 @@ class GutterTooltip extends HoverTooltip { } this.visibleTooltipRow = undefined; this.editor._signal("hideGutterTooltip", this); + super.hide(e); } static annotationsToSummaryString(annotations) { diff --git a/src/mouse/default_gutter_handler_test.js b/src/mouse/default_gutter_handler_test.js index e3a0b3af6b4..34eaccc8b0d 100644 --- a/src/mouse/default_gutter_handler_test.js +++ b/src/mouse/default_gutter_handler_test.js @@ -26,6 +26,16 @@ var MouseEvent = function(type, opts){ var editor; +function findVisibleTooltip() { + const tooltips = document.body.querySelectorAll(".ace_gutter-tooltip"); + for (let i = 0; i < tooltips.length; i++) { + if (window.getComputedStyle(tooltips[i]).display === "block") { + return tooltips[i]; + } + } + return null; +} + module.exports = { setUp : function(next) { this.editor = new Editor(new VirtualRenderer()); @@ -52,15 +62,17 @@ module.exports = { assert.ok(/ace_error/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); + assert.ok(/error test/.test(tooltip.textContent)); + annotation.dispatchEvent(new MouseEvent("move", {x: 0, y: 0})); done(); - }, 100); + }, 400); }, "test: gutter security tooltip" : function(done) { var editor = this.editor; @@ -76,15 +88,15 @@ module.exports = { assert.ok(/ace_security/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/security finding test/.test(tooltip.textContent)); done(); - }, 100); + }, 400); }, "test: gutter warning tooltip" : function(done) { var editor = this.editor; @@ -100,15 +112,15 @@ module.exports = { assert.ok(/ace_warning/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/warning test/.test(tooltip.textContent)); done(); - }, 100); + }, 400); }, "test: gutter info tooltip" : function(done) { var editor = this.editor; @@ -124,15 +136,15 @@ module.exports = { assert.ok(/ace_info/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/info test/.test(tooltip.textContent)); done(); - }, 100); + }, 400); }, "test: gutter hint tooltip" : function(done) { var editor = this.editor; @@ -148,15 +160,15 @@ module.exports = { assert.ok(/ace_hint/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/suggestion test/.test(tooltip.textContent)); done(); - }, 100); + }, 400); }, "test: gutter svg icons" : function() { var editor = this.editor; @@ -200,16 +212,16 @@ module.exports = { var annotation = lines.cells[0].element.children[2].firstChild; assert.ok(/ace_error_fold/.test(annotation.className)); - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + var row = lines.cells[0].row; + editor.$mouseHandler.tooltip.showTooltip(row); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error in folded/.test(tooltip.textContent)); done(); - }, 50); + }, 400); }, "test: security show up in fold" : function(done) { var editor = this.editor; @@ -236,16 +248,16 @@ module.exports = { var annotation = lines.cells[0].element.children[2].firstChild; assert.ok(/ace_security_fold/.test(annotation.className)); - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + var row = lines.cells[0].row; + editor.$mouseHandler.tooltip.showTooltip(row); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/security finding in folded/.test(tooltip.textContent)); done(); - }, 100); + }, 400); }, "test: warning show up in fold" : function(done) { var editor = this.editor; @@ -272,16 +284,16 @@ module.exports = { var annotation = lines.cells[0].element.children[2].firstChild; assert.ok(/ace_warning_fold/.test(annotation.className)); - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + var row = lines.cells[0].row; + editor.$mouseHandler.tooltip.showTooltip(row); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/warning in folded/.test(tooltip.textContent)); done(); - }, 100); + }, 400); }, "test: info not show up in fold" : function() { var editor = this.editor; @@ -384,34 +396,6 @@ module.exports = { assert.notOk(/ace_security_fold/.test(firstLineGutterElement.className)); assert.ok(/ace_warning_fold/.test(firstLineGutterElement.className)); }, - "test: sets position correctly when tooltipFollowsMouse false" : function(done) { - var editor = this.editor; - var value = ""; - - editor.session.setMode(new Mode()); - editor.setValue(value, -1); - editor.session.setAnnotations([{row: 0, column: 0, text: "error test", type: "error"}]); - editor.setOption("tooltipFollowsMouse", false); - editor.setOption("useSvgGutterIcons", true); - editor.renderer.$loop._flush(); - - var lines = editor.renderer.$gutterLayer.$lines; - var annotation = lines.cells[0].element.childNodes[2].firstChild; - assert.ok(/ace_error/.test(annotation.className)); - - var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); - - // Wait for the tooltip to appear after its timeout. - setTimeout(function() { - editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); - assert.ok(/error test/.test(tooltip.textContent)); - assert.equal(tooltip.style.left, `${rect.right - GUTTER_TOOLTIP_LEFT_OFFSET}px`); - assert.equal(tooltip.style.top, `${rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET}px`); - done(); - }, 100); - }, "test: gutter tooltip should properly display special characters (\" ' & <)" : function(done) { var editor = this.editor; var value = ""; @@ -426,15 +410,15 @@ module.exports = { assert.ok(/ace_error/.test(annotation.className)); var rect = annotation.getBoundingClientRect(); - annotation.dispatchEvent(new MouseEvent("move", {x: rect.left, y: rect.top})); + annotation.dispatchEvent(new MouseEvent("move", {x: rect.left + rect.width/2, y: rect.top + rect.height/2})); // Wait for the tooltip to appear after its timeout. setTimeout(function() { editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/special characters " ' & Date: Mon, 29 Sep 2025 21:02:58 +0400 Subject: [PATCH 7/8] fix tests --- src/editor_options_test.js | 1 + src/keyboard/gutter_handler.js | 7 +++- src/keyboard/gutter_handler_test.js | 10 ++--- src/test/mockdom.js | 64 ++++++++++++++++++++--------- 4 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/editor_options_test.js b/src/editor_options_test.js index dc80f6a2032..43b1442ab7b 100644 --- a/src/editor_options_test.js +++ b/src/editor_options_test.js @@ -59,6 +59,7 @@ module.exports = { assert.ok(editor.hoverTooltip != null); var nodes = document.querySelectorAll(".ace_tooltip"); + // TODO assert.equal(nodes.length, 2); assert.equal(editor.hoverTooltip.isOpen, true); diff --git a/src/keyboard/gutter_handler.js b/src/keyboard/gutter_handler.js index efda42288e8..afe052fbb91 100644 --- a/src/keyboard/gutter_handler.js +++ b/src/keyboard/gutter_handler.js @@ -32,6 +32,10 @@ class GutterKeyboardHandler { // if the tooltip is open, we only want to respond to commands to close it (like a modal) if (this.annotationTooltip.isOpen) { e.preventDefault(); + + if (e.keyCode === keys["escape"]) + this.annotationTooltip.hide(); + return; } @@ -182,7 +186,8 @@ class GutterKeyboardHandler { return; case "annotation": - this.annotationTooltip.showTooltip(this.$rowIndexToRow(this.activeRowIndex)); + this.annotationTooltip.showTooltip(this.$rowIndexToRow(this.activeRowIndex)); + window.removeEventListener("keydown", this.annotationTooltip.hide, true); break; } return; diff --git a/src/keyboard/gutter_handler_test.js b/src/keyboard/gutter_handler_test.js index 74d8f612eeb..cf160de4837 100644 --- a/src/keyboard/gutter_handler_test.js +++ b/src/keyboard/gutter_handler_test.js @@ -53,7 +53,7 @@ module.exports = { editor.renderer.$loop._flush(); var lines = editor.renderer.$gutterLayer.$lines; - var toggler = lines.cells[0].element.children[1]; + var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); // Set focus to the gutter div. editor.renderer.$gutter.focus(); @@ -163,7 +163,7 @@ module.exports = { setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = document.body.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. @@ -208,7 +208,7 @@ module.exports = { setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. @@ -224,7 +224,7 @@ module.exports = { setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/warning test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. @@ -386,7 +386,7 @@ module.exports = { setTimeout(function() { // Check annotation is rendered. editor.renderer.$loop._flush(); - var tooltip = editor.container.querySelector(".ace_gutter-tooltip"); + var tooltip = findVisibleTooltip(); assert.ok(/error test/.test(tooltip.textContent)); // Press escape to dismiss the tooltip. diff --git a/src/test/mockdom.js b/src/test/mockdom.js index 2c586ae2be8..f0c0b9fc99d 100644 --- a/src/test/mockdom.js +++ b/src/test/mockdom.js @@ -502,36 +502,62 @@ function Node(name) { throw new Error("attempting to add empty listener"); } if (!this._events) this._events = {}; - if (!this._events[name]) this._events[name] = []; - var i = this._events[name].indexOf(listener); + var id = capturing ? "__c__:" + name : name; + if (!this._events[id]) this._events[id] = []; + var i = this._events[id].indexOf(listener); if (i == -1) - this._events[name][capturing ? "unshift" : "push"](listener); + this._events[id].push(listener); }; - this.removeEventListener = function(name, listener) { + this.removeEventListener = function(name, listener, capturing) { if (!this._events) return; - if (!this._events[name]) return; - var i = this._events[name].indexOf(listener); + var id = capturing ? "__c__:" + name : name; + if (!this._events[id]) return; + var i = this._events[id].indexOf(listener); if (i !== -1) - this._events[name].splice(i, 1); + this._events[id].splice(i, 1); }; this.createEvent = function(v) { return new Event(); }; this.dispatchEvent = function(e) { + var parents = []; + var node = this.parentNode; + + while (node) { + parents.push(node); + node = node.parentNode; + } + parents.push(window); + if (!e.target) e.target = this; if (!e.timeStamp) e.timeStamp = Date.now(); - e.currentTarget = this; - var events = this._events && this._events[e.type]; - events && events.slice().forEach(function(listener) { - listener.call(this, e); - }, this); - if (this["on" + e.type]) - this["on" + e.type](e); - if (!e.bubbles || e.stopped) return; - if (this.parentNode) - this.parentNode.dispatchEvent(e); - else if (this != window) - window.dispatchEvent(e); + + e.eventPhase = 1; + for (var i = parents.length - 1; i >= 0; i--) { + var node = parents[i]; + if (call(node, true)) return; + } + e.eventPhase = 2; + if (call(this, true)) return; + if (call(this, false)) return; + e.eventPhase = 3; + for (var i = 0; i < parents.length; i++) { + var node = parents[i]; + if (call(node, false)) return; + } + + function call(node, capturing) { + e.currentTarget = node; + if (!capturing && node["on" + e.type]) + node["on" + e.type](e); + var id = capturing ? "__c__:" + e.type : e.type; + var events = node._events && node._events[id]; + events && events.slice().forEach(function(listener) { + listener.call(node, e); + }); + if (e.stopped) return true; + if (!capturing && !e.bubbles) return true; + } }; this.contains = function(node) { while (node) { From 8dd2fe0cebc013a3b21c01ebc8ac7bc1f1ac1f84 Mon Sep 17 00:00:00 2001 From: mkslanc Date: Tue, 30 Sep 2025 17:59:10 +0400 Subject: [PATCH 8/8] refactor gutter tooltip constructor and positioning logic for improved alignment and consistency. --- src/mouse/default_gutter_handler.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/mouse/default_gutter_handler.js b/src/mouse/default_gutter_handler.js index 062fc7abd58..d74efd14210 100644 --- a/src/mouse/default_gutter_handler.js +++ b/src/mouse/default_gutter_handler.js @@ -59,8 +59,11 @@ exports.GutterHandler = GutterHandler; class GutterTooltip extends HoverTooltip { + /** + * @param {import("../editor").Editor} editor + */ constructor(editor) { - super(); + super(editor.container); this.id = "gt" + (++GutterTooltip.$uid); this.editor = editor; /**@type {Number | Undefined}*/ @@ -70,7 +73,7 @@ class GutterTooltip extends HoverTooltip { el.setAttribute("id", this.id); el.style.pointerEvents = "auto"; el.style.position = "absolute"; - + this.onDomMouseMove = this.onDomMouseMove.bind(this); this.onDomMouseOut = this.onDomMouseOut.bind(this); @@ -86,14 +89,14 @@ class GutterTooltip extends HoverTooltip { const aceEvent = new MouseEvent(domEvent, this.editor); this.onMouseOut(aceEvent); } - + addToEditor(editor) { const gutter = editor.renderer.$gutter; gutter.addEventListener("mousemove", this.onDomMouseMove); gutter.addEventListener("mouseout", this.onDomMouseOut); super.addToEditor(editor); } - + removeFromEditor(editor) { const gutter = editor.renderer.$gutter; gutter.removeEventListener("mousemove", this.onDomMouseMove); @@ -241,12 +244,13 @@ class GutterTooltip extends HoverTooltip { } const gutterCell = this.$findCellByRow(range.start.row); if (gutterCell) { + //TODO: isAbove display is out var gutterElement = gutterCell.element.querySelector(".ace_gutter_annotation"); const rect = gutterElement.getBoundingClientRect(); - element.style.left = (rect.right - GUTTER_TOOLTIP_LEFT_OFFSET) + "px"; - - element.style.bottom = isAbove ? (window.innerHeight - rect.top - GUTTER_TOOLTIP_TOP_OFFSET) + "px" : "" ; - element.style.top = isAbove ? "" : rect.bottom - GUTTER_TOOLTIP_TOP_OFFSET + "px"; + var rootRect = element.offsetParent && element.offsetParent.getBoundingClientRect(); + element.style.left = rect.right - (rootRect ? rootRect.left : 0) + "px"; + element.style.top = isAbove ? "" : rect.bottom - (rootRect ? rootRect.top : 0) + "px"; + element.style.bottom = isAbove ? rect.top + "px" : "" ; } element.style.maxHeight = (isAbove ? position.pageY : spaceBelow) - MARGIN + "px"; }