diff --git a/src/autocomplete.js b/src/autocomplete.js index f293b618004..ba8458276d5 100644 --- a/src/autocomplete.js +++ b/src/autocomplete.js @@ -90,6 +90,7 @@ class Autocomplete { }.bind(this)); this.tooltipTimer = lang.delayedCall(this.updateDocTooltip.bind(this), 50); + this.popupTimer = lang.delayedCall(this.$updatePopupPosition.bind(this), 50); this.stickySelectionTimer = lang.delayedCall(function() { this.stickySelection = true; @@ -143,6 +144,7 @@ class Autocomplete { } this.hideDocTooltip(); this.stickySelectionTimer.cancel(); + this.popupTimer.cancel(); this.stickySelection = false; } @@ -160,9 +162,14 @@ class Autocomplete { this.tooltipTimer.call(null, null); return; } + + // Update the popup position after a short wait to account for potential scrolling + this.popupTimer.schedule(); + this.tooltipTimer.schedule(); + } else { + this.popupTimer.call(null, null); + this.tooltipTimer.call(null, null); } - this.$updatePopupPosition(); - this.tooltipTimer.call(null, null); } $onPopupShow(hide) { diff --git a/src/autocomplete/inline_test.js b/src/autocomplete/inline_test.js index fc5705b99ba..5e0ee5f5c3a 100644 --- a/src/autocomplete/inline_test.js +++ b/src/autocomplete/inline_test.js @@ -42,6 +42,14 @@ var completions = [ value: "f should not show inline", score: 0, hideInlinePreview: true + }, + { + value: "long\nlong\nlong\nlong\nlong\nlong", + score: 0 + }, + { + value: "long\nlong\nlong\nlong\nlong\nlong".repeat(100), + score: 0 } ]; @@ -261,6 +269,38 @@ module.exports = { done(); }, + "test: should scroll if inline preview outside": function(done) { + // Fill the editor with new lines to get the cursor to the bottom + // of the container + editor.execCommand("insertstring", "\n".repeat(200)); + + var deltaY; + var initialScrollBy = editor.renderer.scrollBy; + editor.renderer.scrollBy = function(varX, varY) { + deltaY = varY; + }; + + inline.show(editor, completions[6], "l"); + editor.renderer.$loop._flush(); + + setTimeout(() => { + // Should scroll 5 lines to get the inline preview into view + assert.strictEqual(deltaY, 50); + + inline.hide(); + editor.renderer.$loop._flush(); + + inline.show(editor, completions[7], "l"); + editor.renderer.$loop._flush(); + + setTimeout(() => { + // Should scroll as much as possbile while keeping the cursor on screen + assert.strictEqual(deltaY, 490); + editor.renderer.scrollBy = initialScrollBy; + done(); + }, 50); + }, 50); + }, tearDown: function() { inline.destroy(); editor.destroy(); diff --git a/src/virtual_renderer.js b/src/virtual_renderer.js index 3947f44f24f..916b800a27a 100644 --- a/src/virtual_renderer.js +++ b/src/virtual_renderer.js @@ -1620,6 +1620,25 @@ class VirtualRenderer { className: "ace_ghost_text" }; this.session.widgetManager.addLineWidget(this.$ghostTextWidget); + + // Check wether the line widget fits in the part of the screen currently in view + var pixelPosition = this.$cursorLayer.getPixelPosition(insertPosition, true); + var el = this.container; + var height = el.getBoundingClientRect().height; + var ghostTextHeight = textLines.length * this.lineHeight; + var fitsY = ghostTextHeight < height - pixelPosition.top; + + // If it fits, no action needed + if (fitsY) return; + + // If it can fully fit in the screen, scroll down until it fits on the screen + // if it cannot fully fit, scroll so that the cursor is at the top of the screen + // to fit as much as possible. + if (ghostTextHeight < height) { + this.scrollBy(0, (textLines.length - 1) * this.lineHeight); + } else { + this.scrollBy(0, pixelPosition.top); + } } }