1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-03-06 09:32:50 +01:00

VIM-1558 Support block inlays

This commit is contained in:
Matt Ellis 2019-01-25 12:31:42 +00:00
parent 56c4e3e31f
commit 531a9c28ae
No known key found for this signature in database
GPG Key ID: FA6025D54131324B
15 changed files with 651 additions and 188 deletions

View File

@ -352,6 +352,8 @@
<action id="VimRedo" class="com.maddyhome.idea.vim.action.change.RedoAction" text="Redo"/>
<action id="VimUndo" class="com.maddyhome.idea.vim.action.change.UndoAction" text="Undo"/>
<action id="VimInternalAddInlays" class="com.maddyhome.idea.vim.action.internal.AddInlaysAction" text="Vim (internal) add test inlays" internal="true"/>
<!-- Keys -->
<action id="VimShortcutKeyAction" class="com.maddyhome.idea.vim.action.VimShortcutKeyAction" text="Shortcuts"/>
<action id="VimOperatorAction" class="com.maddyhome.idea.vim.action.change.OperatorAction" text="Operator"/>

View File

@ -0,0 +1,148 @@
package com.maddyhome.idea.vim.action.internal;
import com.intellij.ide.ui.AntialiasingType;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.impl.FontInfo;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.util.Key;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.util.Random;
public class AddInlaysAction extends AnAction {
private static Random myRandom = new Random();
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
DataContext dataContext = e.getDataContext();
Editor editor = getEditor(dataContext);
if (editor == null) return;
InlayModel inlayModel = editor.getInlayModel();
Document document = editor.getDocument();
int lineCount = document.getLineCount();
for (int i = myRandom.nextInt(10); i < lineCount;) {
int offset = document.getLineStartOffset(i);
// Mostly above
boolean above = myRandom.nextInt(10) > 3;
// Mostly do one, but occasionally throw in a bunch
int count = myRandom.nextInt(10) > 7 ? myRandom.nextInt(5) : 1;
for (int j = 0; j < count; j++) {
float factor = Math.max(1.75f * myRandom.nextFloat(), 0.9f);
String text = String.format("---------- %s line %d ----------", above ? "above" : "below", i + 1);
inlayModel.addBlockElement(offset, true, above, 0, new MyBlockRenderer(factor, text));
}
// Every 10 lines +/- 3 lines
i += 10 + (myRandom.nextInt(6) - 3);
}
}
protected Editor getEditor(@NotNull DataContext dataContext) {
return CommonDataKeys.EDITOR.getData(dataContext);
}
private static class MyBlockRenderer implements EditorCustomElementRenderer {
private static Key<MyFontMetrics> HINT_FONT_METRICS = Key.create("DummyInlayFontMetrics");
private float myFactor;
private String myText;
MyBlockRenderer(float factor, String text) {
myFactor = factor;
myText = text;
}
@Override
public int calcWidthInPixels(@NotNull Inlay inlay) {
Editor editor = inlay.getEditor();
FontMetrics fontMetrics = getFontMetrics(editor).metrics;
return doCalcWidth(myText, fontMetrics);
}
@Override
public int calcHeightInPixels(@NotNull Inlay inlay) {
Editor editor = inlay.getEditor();
FontMetrics fontMetrics = getFontMetrics(editor).metrics;
return fontMetrics.getHeight();
}
@Override
public void paint(@NotNull Inlay inlay, @NotNull Graphics g, @NotNull Rectangle targetRegion, @NotNull TextAttributes textAttributes) {
Editor editor = inlay.getEditor();
FontMetrics fontMetrics = getFontMetrics(editor).metrics;
LineMetrics lineMetrics = fontMetrics.getLineMetrics(myText, g);
g.setColor(JBColor.GRAY);
g.setFont(fontMetrics.getFont());
g.drawString(myText, 0, targetRegion.y + (int)(lineMetrics.getHeight() - lineMetrics.getDescent()));
g.setColor(JBColor.LIGHT_GRAY);
g.drawRect(targetRegion.x, targetRegion.y, targetRegion.width, targetRegion.height);
}
private MyFontMetrics getFontMetrics(Editor editor) {
String familyName = UIManager.getFont("Label.font").getFamily();
int size = (int) (Math.max(1, editor.getColorsScheme().getEditorFontSize() - 1) * myFactor);
MyFontMetrics metrics = editor.getUserData(HINT_FONT_METRICS);
if (metrics != null && !metrics.isActual(editor, familyName, size)) {
metrics = null;
}
if (metrics == null) {
metrics = new MyFontMetrics(editor, familyName, size);
editor.putUserData(HINT_FONT_METRICS, metrics);
}
return metrics;
}
private int doCalcWidth(String text, FontMetrics fontMetrics) {
return (text == null) ? 0 : fontMetrics.stringWidth(text);
}
protected class MyFontMetrics {
private FontMetrics metrics;
private int lineHeight;
MyFontMetrics(Editor editor, String familyName, int size) {
Font font = UIUtil.getFontWithFallback(familyName, Font.PLAIN, size);
FontRenderContext context = getCurrentContext(editor);
metrics = FontInfo.getFontMetrics(font, context);
// We assume this will be a better approximation to a real line height for a given font
lineHeight = (int) Math.ceil(font.createGlyphVector(context, "Ap").getVisualBounds().getHeight());
}
public Font getFont() { return metrics.getFont(); }
public boolean isActual(Editor editor, String familyName, int size) {
Font font = metrics.getFont();
if (familyName != font.getFamily() || size != font.getSize()) return false;
FontRenderContext currentContext = getCurrentContext(editor);
return currentContext.equals(metrics.getFontRenderContext());
}
private FontRenderContext getCurrentContext(Editor editor) {
FontRenderContext editorContext = FontInfo.getFontRenderContext(editor.getContentComponent());
return new FontRenderContext(editorContext.getTransform(), AntialiasingType.getKeyForCurrentScope(false),
editor instanceof EditorImpl ? ((EditorImpl) editor).myFractionalMetricsHintValue : RenderingHints.VALUE_FRACTIONALMETRICS_OFF);
}
}
}
}

View File

@ -24,9 +24,7 @@ import com.intellij.openapi.editor.Editor;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.action.motion.MotionEditorAction;
import com.maddyhome.idea.vim.command.Argument;
import com.maddyhome.idea.vim.handler.ExecuteMethodNotOverriddenException;
import com.maddyhome.idea.vim.handler.MotionEditorActionHandler;
import gherkin.lexer.Vi;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

View File

@ -35,7 +35,7 @@ public class MotionScrollFirstScreenLineAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, cmd.getRawCount(), cmd.getCount(), false);
return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, cmd.getRawCount(), false);
}
}
}

View File

@ -36,16 +36,14 @@ public class MotionScrollFirstScreenLinePageStartAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
int raw = cmd.getRawCount();
int cnt = cmd.getCount();
if (raw == 0) {
int lines = EditorHelper.getScreenHeight(editor);
return VimPlugin.getMotion().scrollLine(editor, lines);
}
else {
return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, raw, cnt, true);
int line = cmd.getRawCount();
if (line == 0) {
final int nextVisualLine = EditorHelper.getVisualLineAtBottomOfScreen(editor) + 1;
line = EditorHelper.visualLineToLogicalLine(editor, nextVisualLine) + 1; // rawCount is 1 based
}
return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, line, true);
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollFirstScreenLineStartAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, cmd.getRawCount(), cmd.getCount(), true);
return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, cmd.getRawCount(), true);
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollHalfPageDownAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollHalfPage(editor, 1, cmd.getRawCount());
return VimPlugin.getMotion().scrollScreen(editor, cmd.getRawCount(), true);
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollHalfPageUpAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollHalfPage(editor, -1, cmd.getRawCount());
return VimPlugin.getMotion().scrollScreen(editor, cmd.getRawCount(), false);
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollLastScreenLineAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, cmd.getRawCount(), cmd.getCount(), false);
return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, cmd.getRawCount(), false);
}
}
}

View File

@ -23,6 +23,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.actionSystem.EditorAction;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.command.Command;
import com.maddyhome.idea.vim.group.MotionGroup;
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase;
import com.maddyhome.idea.vim.helper.EditorHelper;
import org.jetbrains.annotations.NotNull;
@ -36,16 +37,26 @@ public class MotionScrollLastScreenLinePageStartAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
int raw = cmd.getRawCount();
int cnt = cmd.getCount();
if (raw == 0) {
int lines = EditorHelper.getScreenHeight(editor);
return VimPlugin.getMotion().scrollLine(editor, -lines);
final MotionGroup motion = VimPlugin.getMotion();
int line = cmd.getRawCount();
if (line == 0) {
final int prevVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor) - 1;
line = EditorHelper.visualLineToLogicalLine(editor, prevVisualLine) + 1; // rawCount is 1 based
return motion.scrollLineToLastScreenLine(editor, line, true);
}
else {
return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, raw, cnt, true);
// [count]z^ first scrolls [count] to the bottom of the window, then moves the caret to the line that is now at
// the top, and then move that line to the bottom of the window
line = EditorHelper.normalizeLine(editor, line);
if (motion.scrollLineToLastScreenLine(editor, line, true)) {
line = EditorHelper.getVisualLineAtTopOfScreen(editor);
line = EditorHelper.visualLineToLogicalLine(editor, line) + 1; // rawCount is 1 based
return motion.scrollLineToLastScreenLine(editor, line, true);
}
return false;
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollLastScreenLineStartAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, cmd.getRawCount(), cmd.getCount(), true);
return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, cmd.getRawCount(), true);
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollMiddleScreenLineAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollLineToMiddleScreenLine(editor, cmd.getRawCount(), cmd.getCount(), false);
return VimPlugin.getMotion().scrollLineToMiddleScreenLine(editor, cmd.getRawCount(), false);
}
}
}

View File

@ -35,7 +35,7 @@ public class MotionScrollMiddleScreenLineStartAction extends EditorAction {
private static class Handler extends EditorActionHandlerBase {
protected boolean execute(@NotNull Editor editor, @NotNull DataContext context, @NotNull Command cmd) {
return VimPlugin.getMotion().scrollLineToMiddleScreenLine(editor, cmd.getRawCount(), cmd.getCount(), true);
return VimPlugin.getMotion().scrollLineToMiddleScreenLine(editor, cmd.getRawCount(), true);
}
}
}

View File

@ -55,6 +55,7 @@ import com.maddyhome.idea.vim.ui.ExEntryPanel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.io.File;
@ -760,20 +761,20 @@ public class MotionGroup {
}
}
public boolean scrollLineToFirstScreenLine(@NotNull Editor editor, int rawCount, int count, boolean start) {
scrollLineToScreenLine(editor, 1, rawCount, count, start);
public boolean scrollLineToFirstScreenLine(@NotNull Editor editor, int rawCount, boolean start) {
scrollLineToScreenLocation(editor, ScreenLocation.TOP, rawCount, start);
return true;
}
public boolean scrollLineToMiddleScreenLine(@NotNull Editor editor, int rawCount, int count, boolean start) {
scrollLineToScreenLine(editor, EditorHelper.getScreenHeight(editor) / 2 + 1, rawCount, count, start);
public boolean scrollLineToMiddleScreenLine(@NotNull Editor editor, int rawCount, boolean start) {
scrollLineToScreenLocation(editor, ScreenLocation.MIDDLE, rawCount, start);
return true;
}
public boolean scrollLineToLastScreenLine(@NotNull Editor editor, int rawCount, int count, boolean start) {
scrollLineToScreenLine(editor, EditorHelper.getScreenHeight(editor), rawCount, count, start);
public boolean scrollLineToLastScreenLine(@NotNull Editor editor, int rawCount, boolean start) {
scrollLineToScreenLocation(editor, ScreenLocation.BOTTOM, rawCount, start);
return true;
}
@ -813,27 +814,30 @@ public class MotionGroup {
false));
}
private void scrollLineToScreenLine(@NotNull Editor editor, int line, int rawCount, int count, boolean start) {
int scrollOffset = ((NumberOption) Options.getInstance().getOption("scrolloff")).value();
int height = EditorHelper.getScreenHeight(editor);
if (scrollOffset > height / 2) {
scrollOffset = height / 2;
}
if (line <= height / 2) {
if (line < scrollOffset + 1) {
line = scrollOffset + 1;
}
}
else {
if (line > height - scrollOffset) {
line = height - scrollOffset;
}
}
// Scrolls current or [count] line to given screen location
// In Vim, [count] refers to a file line, so it's a logical line
private void scrollLineToScreenLocation(@NotNull Editor editor, ScreenLocation screenLocation, int line, boolean start) {
int visualLine = rawCount == 0
final int scrollOffset = getNormalizedScrollOffset(editor);
line = EditorHelper.normalizeLine(editor, line);
int visualLine = line == 0
? editor.getCaretModel().getVisualPosition().line
: EditorHelper.logicalLineToVisualLine(editor, count - 1);
scrollLineToTopOfScreen(editor, EditorHelper.normalizeVisualLine(editor, visualLine - line + 1));
: EditorHelper.logicalLineToVisualLine(editor, line - 1);
// This method moves the current (or [count]) line to the specified screen location
// Scroll offset is applicable, but scroll jump isn't. Offset is applied to screen lines (visual lines)
switch (screenLocation) {
case TOP:
EditorHelper.scrollVisualLineToTopOfScreen(editor, visualLine - scrollOffset);
break;
case MIDDLE:
EditorHelper.scrollVisualLineToMiddleOfScreen(editor, visualLine);
break;
case BOTTOM:
EditorHelper.scrollVisualLineToBottomOfScreen(editor, visualLine + scrollOffset);
break;
}
if (visualLine != editor.getCaretModel().getVisualPosition().line || start) {
int offset;
if (start) {
@ -850,51 +854,45 @@ public class MotionGroup {
}
public int moveCaretToFirstScreenLine(@NotNull Editor editor, int count) {
return moveCaretToScreenLine(editor, count);
return moveCaretToScreenLocation(editor, ScreenLocation.TOP, count);
}
public int moveCaretToLastScreenLine(@NotNull Editor editor, int count) {
return moveCaretToScreenLine(editor, EditorHelper.getScreenHeight(editor) - count + 1);
return moveCaretToScreenLocation(editor, ScreenLocation.BOTTOM, count);
}
public int moveCaretToMiddleScreenLine(@NotNull Editor editor) {
return moveCaretToScreenLine(editor, EditorHelper.getScreenHeight(editor) / 2 + 1);
return moveCaretToScreenLocation(editor, ScreenLocation.MIDDLE, 0);
}
private int moveCaretToScreenLine(@NotNull Editor editor, int line) {
//saveJumpLocation(editor, context);
int scrollOffset = ((NumberOption) Options.getInstance().getOption("scrolloff")).value();
int height = EditorHelper.getScreenHeight(editor);
if (scrollOffset > height / 2) {
scrollOffset = height / 2;
// [count] is a visual line offset, which means it's 1 based. The value is ignored for ScreenLocation.MIDDLE
private int moveCaretToScreenLocation(@NotNull Editor editor, ScreenLocation screenLocation, int visualLineOffset) {
final int scrollOffset = getNormalizedScrollOffset(editor);
int topVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
int bottomVisualLine = EditorHelper.getVisualLineAtBottomOfScreen(editor);
// Don't apply scrolloff if we're at the top or bottom of the file
int offsetTopVisualLine = topVisualLine > 0 ? topVisualLine + scrollOffset : topVisualLine;
int offsetBottomVisualLine = bottomVisualLine < EditorHelper.getVisualLineCount(editor) ? bottomVisualLine - scrollOffset : bottomVisualLine;
// [count]H/[count]L moves caret to that screen line, bounded by top/bottom scroll offsets
int targetVisualLine = 0;
switch (screenLocation) {
case TOP:
targetVisualLine = Math.max(offsetTopVisualLine, topVisualLine + visualLineOffset - 1);
targetVisualLine = Math.min(targetVisualLine, offsetBottomVisualLine);
break;
case MIDDLE:
targetVisualLine = EditorHelper.getVisualLineAtMiddleOfScreen(editor);
break;
case BOTTOM:
targetVisualLine = Math.min(offsetBottomVisualLine, bottomVisualLine - visualLineOffset + 1);
targetVisualLine = Math.max(targetVisualLine, offsetTopVisualLine);
break;
}
int top = EditorHelper.getVisualLineAtTopOfScreen(editor);
if (line > height - scrollOffset && top < EditorHelper.getLineCount(editor) - height) {
line = height - scrollOffset;
}
else if (line <= scrollOffset && top > 0) {
line = scrollOffset + 1;
}
return moveCaretToLineStartSkipLeading(editor, EditorHelper.visualLineToLogicalLine(editor, top + line - 1));
}
public boolean scrollHalfPage(@NotNull Editor editor, int dir, int count) {
NumberOption scroll = (NumberOption) Options.getInstance().getOption("scroll");
int height = EditorHelper.getScreenHeight(editor) / 2;
if (count == 0) {
count = scroll.value();
if (count == 0) {
count = height;
}
}
else {
scroll.set(count);
}
return scrollPage(editor, dir, count, EditorHelper.getCurrentVisualScreenLine(editor), true);
return moveCaretToLineStartSkipLeading(editor, EditorHelper.visualLineToLogicalLine(editor, targetVisualLine));
}
public boolean scrollColumn(@NotNull Editor editor, int columns) {
@ -913,7 +911,7 @@ public class MotionGroup {
int visualLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
visualLine = EditorHelper.normalizeVisualLine(editor, visualLine + lines);
scrollLineToTopOfScreen(editor, visualLine);
EditorHelper.scrollVisualLineToTopOfScreen(editor, visualLine);
moveCaretToView(editor);
@ -921,25 +919,23 @@ public class MotionGroup {
}
private static void moveCaretToView(@NotNull Editor editor) {
int scrollOffset = ((NumberOption) Options.getInstance().getOption("scrolloff")).value();
int sideScrollOffset = ((NumberOption) Options.getInstance().getOption("sidescrolloff")).value();
int height = EditorHelper.getScreenHeight(editor);
int width = EditorHelper.getScreenWidth(editor);
if (scrollOffset > height / 2) {
scrollOffset = height / 2;
final int scrollOffset = getNormalizedScrollOffset(editor);
int topVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
int bottomVisualLine = EditorHelper.getVisualLineAtBottomOfScreen(editor);
int caretVisualLine = editor.getCaretModel().getVisualPosition().line;
int newline = caretVisualLine;
if (caretVisualLine < topVisualLine + scrollOffset) {
newline = EditorHelper.normalizeVisualLine(editor, topVisualLine + scrollOffset);
}
if (sideScrollOffset > width / 2) {
sideScrollOffset = width / 2;
else if (caretVisualLine >= bottomVisualLine - scrollOffset) {
newline = EditorHelper.normalizeVisualLine(editor, bottomVisualLine - scrollOffset);
}
int visualLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
int cline = editor.getCaretModel().getVisualPosition().line;
int newline = cline;
if (cline < visualLine + scrollOffset) {
newline = EditorHelper.normalizeVisualLine(editor, visualLine + scrollOffset);
}
else if (cline >= visualLine + height - scrollOffset) {
newline = EditorHelper.normalizeVisualLine(editor, visualLine + height - scrollOffset - 1);
int sideScrollOffset = ((NumberOption) Options.getInstance().getOption("sidescrolloff")).value();
int width = EditorHelper.getScreenWidth(editor);
if (sideScrollOffset > width / 2) {
sideScrollOffset = width / 2;
}
int col = editor.getCaretModel().getVisualPosition().column;
@ -957,13 +953,13 @@ public class MotionGroup {
newColumn = visualColumn + width - sideScrollOffset - 1;
}
if (newline == cline && newColumn != caretColumn) {
if (newline == caretVisualLine && newColumn != caretColumn) {
col = newColumn;
}
newColumn = EditorHelper.normalizeVisualColumn(editor, newline, newColumn, CommandState.inInsertMode(editor));
if (newline != cline || newColumn != oldColumn) {
if (newline != caretVisualLine || newColumn != oldColumn) {
int offset = EditorHelper.visualPositionToOffset(editor, new VisualPosition(newline, newColumn));
moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), offset);
@ -972,61 +968,116 @@ public class MotionGroup {
}
public boolean scrollFullPage(@NotNull Editor editor, int pages) {
int height = EditorHelper.getScreenHeight(editor);
int line = pages > 0 ? 1 : height;
int caretVisualLine = EditorHelper.scrollFullPage(editor, pages);
if (caretVisualLine != -1) {
final int scrollOffset = getNormalizedScrollOffset(editor);
boolean success = true;
return scrollPage(editor, pages, height - 2, line, false);
if (pages > 0) {
// If the caret is ending up passed the end of the file, we need to beep
if (caretVisualLine > EditorHelper.getVisualLineCount(editor) - 1) {
success = false;
}
int topVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
if (caretVisualLine < topVisualLine + scrollOffset) {
caretVisualLine = EditorHelper.normalizeVisualLine(editor, caretVisualLine + scrollOffset);
}
}
else if (pages < 0) {
int bottomVisualLine = EditorHelper.getVisualLineAtBottomOfScreen( editor);
if (caretVisualLine > bottomVisualLine - scrollOffset) {
caretVisualLine = EditorHelper.normalizeVisualLine(editor, caretVisualLine - scrollOffset);
}
}
int offset = moveCaretToLineStartSkipLeading(editor, EditorHelper.visualLineToLogicalLine(editor, caretVisualLine));
moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), offset);
return success;
}
return false;
}
private boolean scrollPage(@NotNull Editor editor, int pages, int height, int line, boolean partial) {
int visualTopLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
public boolean scrollScreen(@NotNull final Editor editor, int rawCount, boolean down) {
final CaretModel caretModel = editor.getCaretModel();
final int currentLogicalLine = caretModel.getLogicalPosition().line;
int newLine = visualTopLine + pages * height;
int topLine = EditorHelper.normalizeVisualLine(editor, newLine);
boolean moved = scrollLineToTopOfScreen(editor, topLine);
visualTopLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
if (moved && topLine == newLine && topLine == visualTopLine) {
moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), moveCaretToScreenLine(editor, line));
return true;
}
else if (moved && !partial) {
int visualLine = Math.abs(visualTopLine - newLine) % height + 1;
if (pages < 0) {
visualLine = height - visualLine + 3;
}
moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), moveCaretToScreenLine(editor, visualLine));
return true;
}
else if (partial) {
int cline = editor.getCaretModel().getVisualPosition().line;
int visualLine = cline + pages * height;
visualLine = EditorHelper.normalizeVisualLine(editor, visualLine);
if (cline == visualLine) {
return false;
}
int logicalLine = editor.visualToLogicalPosition(new VisualPosition(visualLine, 0)).line;
moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), moveCaretToLineStartSkipLeading(editor, logicalLine));
return true;
}
else {
moveCaret(editor, editor.getCaretModel().getPrimaryCaret(),
moveCaretToLineStartSkipLeading(editor, editor.getCaretModel().getPrimaryCaret()));
if ((!down && currentLogicalLine <= 0) || (down && currentLogicalLine >= EditorHelper.getLineCount(editor) - 1)) {
return false;
}
final ScrollingModel scrollingModel = editor.getScrollingModel();
final Rectangle visibleArea = scrollingModel.getVisibleArea();
int targetCaretVisualLine = getScrollScreenTargetCaretVisualLine(editor, rawCount, down);
// Scroll at most one screen height
final int yInitialCaret = editor.visualLineToY(caretModel.getVisualPosition().line);
final int yTargetVisualLine = editor.visualLineToY(targetCaretVisualLine);
if (Math.abs(yTargetVisualLine - yInitialCaret) > visibleArea.height) {
final int yPrevious = visibleArea.y;
boolean moved;
if (down) {
targetCaretVisualLine = EditorHelper.getVisualLineAtBottomOfScreen(editor) + 1;
moved = EditorHelper.scrollVisualLineToTopOfScreen(editor, targetCaretVisualLine);
} else {
targetCaretVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor) - 1;
moved = EditorHelper.scrollVisualLineToBottomOfScreen(editor, targetCaretVisualLine);
}
if (moved) {
// We'll keep the caret at the same position, although that might not be the same line offset as previously
targetCaretVisualLine = editor.yToVisualLine(yInitialCaret + scrollingModel.getVisibleArea().y - yPrevious);
}
} else {
EditorHelper.scrollVisualLineToCaretLocation(editor, targetCaretVisualLine);
final int scrollOffset = getNormalizedScrollOffset(editor);
final int visualTop = EditorHelper.getVisualLineAtTopOfScreen(editor) + scrollOffset;
final int visualBottom = EditorHelper.getVisualLineAtBottomOfScreen(editor) - scrollOffset;
targetCaretVisualLine = Math.max(visualTop, Math.min(visualBottom, targetCaretVisualLine));
}
int logicalLine = EditorHelper.visualLineToLogicalLine(editor, targetCaretVisualLine);
int caretOffset = moveCaretToLineStartSkipLeading(editor, logicalLine);
moveCaret(editor, caretModel.getPrimaryCaret(), caretOffset);
return true;
}
private static boolean scrollLineToTopOfScreen(@NotNull Editor editor, int line) {
int pos = line * editor.getLineHeight();
int verticalPos = editor.getScrollingModel().getVerticalScrollOffset();
editor.getScrollingModel().scrollVertically(pos);
private static int getScrollScreenTargetCaretVisualLine(@NotNull final Editor editor, int rawCount, boolean down) {
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
final int caretVisualLine = editor.getCaretModel().getVisualPosition().line;
final int scrollOption = getScrollOption(rawCount);
return verticalPos != editor.getScrollingModel().getVerticalScrollOffset();
int targetCaretVisualLine;
if (scrollOption == 0) {
// Scroll up/down half window size by default. We can't use line count here because of block inlays
final int offset = down ? (visibleArea.height / 2) : editor.getLineHeight() - (visibleArea.height / 2);
targetCaretVisualLine = editor.yToVisualLine(editor.visualLineToY(caretVisualLine) + offset);
} else {
targetCaretVisualLine = down ? caretVisualLine + scrollOption : caretVisualLine - scrollOption;
}
return targetCaretVisualLine;
}
private static int getScrollOption(int rawCount) {
NumberOption scroll = (NumberOption) Options.getInstance().getOption("scroll");
if (rawCount == 0) {
return scroll.value();
}
// TODO: This needs to be reset whenever the window size changes
scroll.set(rawCount);
return rawCount;
}
private static int getNormalizedScrollOffset(@NotNull final Editor editor) {
int scrollOffset = ((NumberOption) Options.getInstance().getOption("scrolloff")).value();
return EditorHelper.normalizeScrollOffset(editor, scrollOffset);
}
private static void scrollColumnToLeftOfScreen(@NotNull Editor editor, int column) {
@ -1327,53 +1378,55 @@ public class MotionGroup {
public static void scrollPositionIntoView(@NotNull Editor editor, @NotNull VisualPosition position,
boolean scrollJump) {
final int line = position.line;
final int topVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
final int bottomVisualLine = EditorHelper.getVisualLineAtBottomOfScreen(editor);
final int visualLine = position.line;
final int column = position.column;
final int topLine = EditorHelper.getVisualLineAtTopOfScreen(editor);
int scrollOffset = ((NumberOption) Options.getInstance().getOption("scrolloff")).value();
int scrollOffset = getNormalizedScrollOffset(editor);
int scrollJumpSize = 0;
if (scrollJump) {
scrollJumpSize = Math.max(0, ((NumberOption) Options.getInstance().getOption("scrolljump")).value() - 1);
}
int height = EditorHelper.getScreenHeight(editor);
int visualTop = topLine + scrollOffset;
int visualBottom = topLine + height - scrollOffset;
if (scrollOffset >= height / 2) {
scrollOffset = height / 2;
visualTop = topLine + scrollOffset;
visualBottom = topLine + height - scrollOffset;
if (visualTop == visualBottom) {
visualBottom++;
}
int visualTop = topVisualLine + scrollOffset;
int visualBottom = bottomVisualLine - scrollOffset;
if (visualTop == visualBottom) {
visualBottom++;
}
int diff;
if (line < visualTop) {
diff = line - visualTop;
if (visualLine < visualTop) {
diff = visualLine - visualTop;
scrollJumpSize = -scrollJumpSize;
}
else {
diff = line - visualBottom + 1;
if (diff < 0) {
diff = 0;
}
} else {
diff = Math.max(0, visualLine - visualBottom);
}
if (diff != 0) {
int resLine;
// If we need to move the top line more than a half screen worth then we just center the cursor line
if (Math.abs(diff) > height / 2) {
resLine = line - height / 2 - 1;
}
// Otherwise put the new cursor line "scrolljump" lines from the top/bottom
else {
resLine = topLine + diff + scrollJumpSize;
}
resLine = Math.min(resLine, EditorHelper.getVisualLineCount(editor) - height);
resLine = Math.max(0, resLine);
scrollLineToTopOfScreen(editor, resLine);
// If we need to move the top line more than a half screen worth then we just center the cursor line.
// Block inlays mean that this half screen height isn't a consistent pixel height, and might be larger than line
// height multiplied by number of lines, but it's still a good heuristic to use here
int height = bottomVisualLine - topVisualLine + 1;
if (Math.abs(diff) > height / 2) {
EditorHelper.scrollVisualLineToMiddleOfScreen(editor, visualLine);
}
else {
// Put the new cursor line "scrolljump" lines from the top/bottom. Ensure that the line is fully visible,
// including block inlays above/below the line
if (diff > 0) {
int resLine = bottomVisualLine + diff + scrollJumpSize;
EditorHelper.scrollVisualLineToBottomOfScreen(editor, resLine);
}
else {
int resLine = topVisualLine + diff + scrollJumpSize;
resLine = Math.min(resLine, EditorHelper.getVisualLineCount(editor) - height);
resLine = Math.max(0, resLine);
EditorHelper.scrollVisualLineToTopOfScreen(editor, resLine);
}
}
}
int visualColumn = EditorHelper.getVisualColumnAtLeftOfScreen(editor);
@ -1919,6 +1972,12 @@ public class MotionGroup {
private int endOff;
}
private enum ScreenLocation {
TOP,
MIDDLE,
BOTTOM
}
public int getLastFTCmd() {
return lastFTCmd;
}

View File

@ -48,12 +48,19 @@ import java.util.List;
*/
public class EditorHelper {
public static int getVisualLineAtTopOfScreen(@NotNull final Editor editor) {
int lh = editor.getLineHeight();
return (editor.getScrollingModel().getVerticalScrollOffset() + lh - 1) / lh;
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
return getFullVisualLine(editor, visibleArea.y, visibleArea.y, visibleArea.y + visibleArea.height);
}
public static int getCurrentVisualScreenLine(@NotNull final Editor editor) {
return editor.getCaretModel().getVisualPosition().line - getVisualLineAtTopOfScreen(editor) + 1;
public static int getVisualLineAtMiddleOfScreen(@NotNull final Editor editor) {
final ScrollingModel scrollingModel = editor.getScrollingModel();
final Rectangle visibleArea = scrollingModel.getVisibleArea();
return editor.yToVisualLine(visibleArea.y + (visibleArea.height / 2));
}
public static int getVisualLineAtBottomOfScreen(@NotNull final Editor editor) {
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
return getFullVisualLine(editor, visibleArea.y + visibleArea.height, visibleArea.y, visibleArea.y + visibleArea.height);
}
/**
@ -146,14 +153,35 @@ public class EditorHelper {
return includeEndNewLine || len == 0 || editor.getDocument().getCharsSequence().charAt(len - 1) != '\n' ? len : len - 1;
}
/**
* Best efforts to ensure that scroll offset doesn't overlap itself.
*
* This is a sanity check that works fine if there are no visible block inlays. Otherwise, the screen height depends
* on what block inlays are currently visible in the target scroll area. Given a large enough scroll offset (or small
* enough screen), we can return a scroll offset that takes us over the half way point and causes scrolling issues -
* skipped lines, or unexpected movement.
*
* TODO: Investigate better ways of handling scroll offset
* Perhaps apply scroll offset after the move itself? Calculate a safe offset based on a target area?
*
* @param editor The editor to use to normalize the scroll offset
* @param scrollOffset The value of the 'scrolloff' option
* @return The scroll offset value to use
*/
public static int normalizeScrollOffset(@NotNull final Editor editor, int scrollOffset) {
return Math.min(scrollOffset, getApproximateScreenHeight(editor) / 2);
}
/**
* Gets the number of lines than can be displayed on the screen at one time. This is rounded down to the
* nearest whole line if there is a partial line visible at the bottom of the screen.
*
* Note that this value is only approximate and should be avoided whenever possible!
*
* @param editor The editor
* @return The number of screen lines
*/
public static int getScreenHeight(@NotNull final Editor editor) {
private static int getApproximateScreenHeight(@NotNull final Editor editor) {
int lh = editor.getLineHeight();
int height = editor.getScrollingModel().getVisibleArea().y +
editor.getScrollingModel().getVisibleArea().height -
@ -225,6 +253,7 @@ public class EditorHelper {
*/
public static int logicalLineToVisualLine(@NotNull final Editor editor, final int line) {
if (editor instanceof EditorImpl) {
// This is faster than simply calling Editor#logicalToVisualPosition
return ((EditorImpl) editor).offsetToVisualLine(editor.getDocument().getLineStartOffset(line));
}
return editor.logicalToVisualPosition(new LogicalPosition(line, 0)).line;
@ -593,4 +622,222 @@ public class EditorHelper {
return carets;
}
/**
* Scrolls the editor to put the given visual line at the current caret location, relative to the screen.
*
* Due to block inlays, the caret location is maintained as a scroll offset, rather than the number of lines from the
* top of the screen. This means the line offset can change if the number of inlays above the caret changes during
* scrolling. It also means that after scrolling, the top screen line isn't guaranteed to be aligned to the top of
* the screen, unlike most other motions ('M' is the only other motion that doesn't align the top line).
*
* This method will also move the caret location to ensure that any inlays attached above or below the target line are
* fully visible.
*
* @param editor The editor to scroll
* @param visualLine The visual line to scroll to the current caret location
*/
public static void scrollVisualLineToCaretLocation(@NotNull final Editor editor, int visualLine) {
final ScrollingModel scrollingModel = editor.getScrollingModel();
final Rectangle visibleArea = scrollingModel.getVisibleArea();
final int caretScreenOffset = editor.visualLineToY(editor.getCaretModel().getVisualPosition().line) - visibleArea.y;
final int yVisualLine = editor.visualLineToY(visualLine);
// We try to keep the caret in the same location, but only if there's enough space all around for the line's
// inlays. E.g. caret on top screen line and the line has inlays above, or caret on bottom screen line and has
// inlays below
final int topInlayHeight = EditorHelper.getHeightOfVisualLineInlays(editor, visualLine, true);
final int bottomInlayHeight = EditorHelper.getHeightOfVisualLineInlays(editor, visualLine, false);
int inlayOffset = 0;
if (topInlayHeight > caretScreenOffset) {
inlayOffset = topInlayHeight;
} else if (bottomInlayHeight > visibleArea.height - caretScreenOffset + editor.getLineHeight()) {
inlayOffset = -bottomInlayHeight;
}
scrollingModel.scrollVertically(yVisualLine - caretScreenOffset - inlayOffset);
}
/**
* Scrolls the editor to put the given visual line at the top of the current window. Ensures that any block inlay
* elements above the given line are also visible.
*
* @param editor The editor to scroll
* @param visualLine The visual line to place at the top of the current window
* @return Returns true if the window was moved
*/
public static boolean scrollVisualLineToTopOfScreen(@NotNull final Editor editor, int visualLine) {
final ScrollingModel scrollingModel = editor.getScrollingModel();
int inlayHeight = getHeightOfVisualLineInlays(editor, visualLine, true);
int y = editor.visualLineToY(visualLine) - inlayHeight;
int verticalPos = scrollingModel.getVerticalScrollOffset();
scrollingModel.scrollVertically(y);
return verticalPos != scrollingModel.getVerticalScrollOffset();
}
/**
* Scrolls the editor to place the given visual line in the middle of the current window.
*
* @param editor The editor to scroll
* @param visualLine The visual line to place in the middle of the current window
*/
public static void scrollVisualLineToMiddleOfScreen(@NotNull Editor editor, int visualLine) {
final ScrollingModel scrollingModel = editor.getScrollingModel();
int y = editor.visualLineToY(visualLine);
int lineHeight = editor.getLineHeight();
int height = scrollingModel.getVisibleArea().height;
scrollingModel.scrollVertically(y - ((height - lineHeight) / 2));
}
/**
* Scrolls the editor to place the given visual line at the bottom of the screen.
*
* When we're moving the caret down a few lines and want to scroll to keep this visible, we need to be able to place a
* line at the bottom of the screen. Due to block inlays, we can't do this by specifying a top line to scroll to.
*
* @param editor The editor to scroll
* @param visualLine The visual line to place at the bottom of the current window
* @return True if the editor was scrolled
*/
public static boolean scrollVisualLineToBottomOfScreen(@NotNull Editor editor, int visualLine) {
final ScrollingModel scrollingModel = editor.getScrollingModel();
int inlayHeight = getHeightOfVisualLineInlays(editor, visualLine, false);
int y = editor.visualLineToY(visualLine);
int verticalPos = scrollingModel.getVerticalScrollOffset();
int height = inlayHeight + editor.getLineHeight();
Rectangle visibleArea = scrollingModel.getVisibleArea();
// For consistency, we always try to scroll to keep a whole line (with inlays) aligned at the top of the screen.
// This is inexact, and means we can bounce around, most visibly when the caret is on the last line and we're moving
// down (j) or the caret is on the last line and we're scrolling up (CTRL-Y)
// If we want it to be simpler: scrollingModel.scrollVertically(y - visibleArea.height + height);
int topVisualLine = editor.yToVisualLine(y - visibleArea.height + height);
int topLineInlayHeight = getHeightOfVisualLineInlays(editor, topVisualLine, true);
int topY = editor.visualLineToY(topVisualLine);
if (topY - topLineInlayHeight + visibleArea.height < y + height) {
// There's a pathological edge case here, if topVisualLine has a HUGE inlay, then topVisualLine+1 won't put our
// given line at the bottom of the screen
scrollVisualLineToTopOfScreen(editor, topVisualLine + 1);
} else {
scrollingModel.scrollVertically(topY - topLineInlayHeight);
}
return verticalPos != scrollingModel.getVerticalScrollOffset();
}
/**
* Scrolls the screen up or down one or more pages.
*
* @param editor The editor to scroll
* @param pages The number of pages to scroll. Positive is scroll down (lines move up). Negative is scroll up.
* @return The visual line to place the caret on. -1 if the page wasn't scrolled at all.
*/
public static int scrollFullPage(@NotNull final Editor editor, int pages) {
if (pages > 0) {
return scrollFullPageDown(editor, pages);
}
else if (pages < 0) {
return scrollFullPageUp(editor, pages);
}
return -1; // visual lines are 1-based
}
private static int scrollFullPageDown(@NotNull final Editor editor, int pages) {
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
final int lineCount = getVisualLineCount(editor);
if (editor.getCaretModel().getVisualPosition().line == lineCount - 1)
return -1;
int y = visibleArea.y + visibleArea.height;
int topBound = visibleArea.y;
int bottomBound = visibleArea.y + visibleArea.height;
int line = 0;
int caretLine = -1;
for (int i = 0; i < pages; i++) {
line = getFullVisualLine(editor, y, topBound, bottomBound);
if (line >= lineCount - 1) {
// If we're on the last page, end nicely on the last line, otherwise return the overrun so we can "beep"
if (i == pages - 1) {
caretLine = lineCount - 1;
}
else {
caretLine = line;
}
break;
}
// The help page for 'scrolling' states that a page is the number of lines in the window minus two. Scrolling a
// page adds this page length to the current line. Or in other words, scrolling down a page puts the last but one
// line at the top of the next page.
// E.g. a window showing lines 1-35 has a page size of 33, and scrolling down a page shows 34 as the top line
line--;
y = editor.visualLineToY(line);
topBound = y;
bottomBound = y + visibleArea.height;
y = bottomBound;
caretLine = line;
}
scrollVisualLineToTopOfScreen(editor, line);
return caretLine;
}
private static int scrollFullPageUp(@NotNull final Editor editor, int pages) {
final Rectangle visibleArea = editor.getScrollingModel().getVisibleArea();
final int lineHeight = editor.getLineHeight();
int y = visibleArea.y;
int topBound = visibleArea.y;
int bottomBound = visibleArea.y + visibleArea.height;
int line = 0;
int caretLine = -1;
// We know pages is negative
for (int i = pages; i < 0; i++) {
// E.g. a window showing 73-107 has page size 33. Scrolling up puts 74 at the bottom of the screen
line = getFullVisualLine(editor, y, topBound, bottomBound) + 1;
if (line == 1) {
break;
}
y = editor.visualLineToY(line);
bottomBound = y + lineHeight;
topBound = bottomBound - visibleArea.height;
y = topBound;
caretLine = line;
}
scrollVisualLineToBottomOfScreen(editor, line);
return caretLine;
}
private static int getFullVisualLine(@NotNull final Editor editor, int y, int topBound, int bottomBound) {
int line = editor.yToVisualLine(y);
int yActual = editor.visualLineToY(line);
if (yActual < topBound) {
line++;
}
else if (yActual + editor.getLineHeight() > bottomBound) {
line--;
}
return line;
}
private static int getHeightOfVisualLineInlays(@NotNull final Editor editor, int visualLine, boolean above) {
InlayModel inlayModel = editor.getInlayModel();
List<Inlay> inlays = inlayModel.getBlockElementsForVisualLine(visualLine, above);
int inlayHeight = 0;
for (Inlay inlay : inlays) {
inlayHeight += inlay.getHeightInPixels();
}
return inlayHeight;
}
}