1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-08-18 01:31:44 +02:00

Compare commits

..

17 Commits

Author SHA1 Message Date
d6076c719f Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2022-06-24 08:26:18 +03:00
Alex Plate
a3ca1b965b Fix(VIM-2691): Save file on :w 2022-06-24 08:26:02 +03:00
Alex Plate
dd20b480a7 Update changelog 2022-06-24 08:26:02 +03:00
filipp
38292e97af Fix context for function argument evaluation 2022-06-24 03:13:07 +06:00
filipp
46ea752164 Add tolower(), toupper(), join() 2022-06-24 02:58:41 +06:00
Alex Plate
194b744361 Update changelog 2022-06-23 15:29:40 +00:00
b50197f7ce Fix(VIM-2227): Wrong behavior when deleting / changing surround with invalid character 2022-06-23 18:19:28 +03:00
Alex Plate
c00703d1d0 Manually define excluded from qodana files 2022-06-23 13:50:35 +03:00
Alex Plate
6e12377116 Remove generated code from qodana 2022-06-23 13:08:53 +03:00
Alex Plate
b0c4391ad8 Remove some files from qodana inspection 2022-06-22 20:30:14 +03:00
Alex Plate
f43ac2538a Enable dependency checker in qodana 2022-06-22 18:36:08 +03:00
Alex Plate
9eaf8b5d2d Move some other methods to vim-engine 2022-06-22 18:36:08 +03:00
filipp
e365d0b07c Unsubscribe document listener in UndoRedoHelper 2022-06-20 03:26:40 +06:00
filipp
69c273c4a5 Track more actions 2022-06-19 01:07:18 +06:00
filipp
f7950e7adb Fix(VIM-2683) Pasting from system clipboard multiple lines freezes the main thread 2022-06-18 07:31:44 +06:00
filipp
7c1ae9812e Update formatting 2022-06-18 06:26:11 +06:00
filipp
5c794ac40e Fix(VIM-749) Support for :let command 2022-06-18 06:21:26 +06:00
33 changed files with 799 additions and 224 deletions

View File

@@ -55,6 +55,7 @@ usual beta standards.
* [VIM-696](https://youtrack.jetbrains.com/issue/VIM-696/vim-selection-issue-after-undo) Fix selection after undo
* [VIM-744](https://youtrack.jetbrains.com/issue/VIM-744/Use-undoredo-with-count-modifier) Add count to undo/redo
* [VIM-1862](https://youtrack.jetbrains.com/issue/VIM-1862/Ex-commands-executed-in-keymaps-and-macros-are-added-to-the-command-history) Fix command history
* [VIM-2227](https://youtrack.jetbrains.com/issue/VIM-2227) Wrong behavior when deleting / changing surround with invalid character
### Merged PRs:
* [468](https://github.com/JetBrains/ideavim/pull/468) by [Thomas Schouten](https://github.com/PHPirates): Implement UserDataHolder for EditorDataContext
@@ -63,6 +64,7 @@ usual beta standards.
* [493](https://github.com/JetBrains/ideavim/pull/493) by [Matt Ellis](https://github.com/citizenmatt): Improvements to Commentary extension
* [494](https://github.com/JetBrains/ideavim/pull/494) by [Matt Ellis](https://github.com/citizenmatt): Cleanup pre-212 CaretVisualAttributes compatibility code
* [504](https://github.com/JetBrains/ideavim/pull/504) by [Matt Ellis](https://github.com/citizenmatt): Minor bug fixes
* [519](https://github.com/JetBrains/ideavim/pull/519) by [chylex](https://github.com/chylex): Fix(VIM-2227): Wrong behavior when deleting / changing surround with invalid character
## 1.10.0, 2022-02-17

View File

@@ -1,6 +1,8 @@
version: 1.0
profile:
name: Qodana
include:
- name: CheckDependencyLicenses
exclude:
- name: MoveVariableDeclarationIntoWhen
- name: PluginXmlValidity
@@ -9,12 +11,15 @@ exclude:
- name: UnusedReturnValue
- name: All
paths:
- build.gradle
- build.gradle.kts
- gradle/wrapper/gradle-wrapper.properties
- resources/icons/youtrack.svg
- src/com/maddyhome/idea/vim/ex/vimscript/VimScriptCommandHandler.java
- src/com/maddyhome/idea/vim/helper/SearchHelper.java
- src/com/maddyhome/idea/vim/regexp/RegExp.java
- test/org/jetbrains/plugins/ideavim/propertybased/samples/JavaText.kt
- test/org/jetbrains/plugins/ideavim/propertybased/samples/LoremText.kt
- test/org/jetbrains/plugins/ideavim/propertybased/samples/SimpleText.kt
- src/main/resources/icons/youtrack.svg
- src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java
- src/main/java/com/maddyhome/idea/vim/regexp/RegExp.kt
- src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/JavaText.kt
- src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/LoremText.kt
- src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/SimpleText.kt
- src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptListener.java
- src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptLexer.java
- src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptParser.java
- src/main/java/com/maddyhome/idea/vim/vimscript/parser/generated/VimscriptVisitor.java

View File

@@ -146,7 +146,7 @@ rShift: GREATER+;
letCommands:
(WS | COLON)* range? (WS | COLON)* LET WS+ expr WS*
assignmentOperator = (ASSIGN | PLUS_ASSIGN | MINUS_ASSIGN | STAR_ASSIGN | DIV_ASSIGN | MOD_ASSIGN | DOT_ASSIGN)
assignmentOperator
WS* expr WS* ((inline_comment NEW_LINE) | (NEW_LINE | BAR)+)
#Let1Command|
@@ -154,6 +154,21 @@ letCommands:
#Let2Command
;
assignmentOperator:
ASSIGN | plusAssign | minusAssign | startAssign | divAssign | modAssign | dotAssign;
plusAssign:
PLUS ASSIGN;
minusAssign:
MINUS ASSIGN;
startAssign:
STAR ASSIGN;
divAssign:
DIV ASSIGN;
modAssign:
MOD ASSIGN;
dotAssign:
DOT ASSIGN;
shortRange:
((QUESTION (~QUESTION)* QUESTION?) | (DIV (~DIV)* DIV?));
range:
@@ -778,12 +793,12 @@ IS_NOT_CS: 'isnot#';
// Assignment operators
ASSIGN: '=';
PLUS_ASSIGN: '+=';
MINUS_ASSIGN: '-=';
STAR_ASSIGN: '*=';
DIV_ASSIGN: '/=';
MOD_ASSIGN: '%=';
DOT_ASSIGN: '.=';
//PLUS_ASSIGN: '+=';
//MINUS_ASSIGN: '-=';
//STAR_ASSIGN: '*=';
//DIV_ASSIGN: '/=';
//MOD_ASSIGN: '%=';
//DOT_ASSIGN: '.=';
// Escaped chars
ESCAPED_QUESTION: '\\?';

View File

@@ -22,6 +22,7 @@ import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.CommandState
@@ -40,6 +41,7 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegister
import com.maddyhome.idea.vim.extension.VimExtensionHandler
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.mode
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimCaret
import com.maddyhome.idea.vim.newapi.IjVimEditor
@@ -84,22 +86,20 @@ class VimSurroundExtension : VimExtension {
override val isRepeatable = true
override fun execute(editor: VimEditor, context: ExecutionContext) {
setOperatorFunction(Operator())
setOperatorFunction(Operator(supportsMultipleCursors = false)) // TODO
executeNormalWithoutMapping(injector.parser.parseKeys("g@"), editor.ij)
}
}
private class VSurroundHandler : VimExtensionHandler {
override fun execute(editor: VimEditor, context: ExecutionContext) {
val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
// NB: Operator ignores SelectionType anyway
if (!Operator().apply(editor.ij, context.ij, SelectionType.CHARACTER_WISE)) {
if (!Operator(supportsMultipleCursors = true).apply(editor.ij, context.ij, SelectionType.CHARACTER_WISE)) {
return
}
runWriteAction {
// Leave visual mode
executeNormalWithoutMapping(injector.parser.parseKeys("<Esc>"), editor.ij)
editor.ij.caretModel.moveToOffset(selectionStart)
}
}
}
@@ -120,6 +120,10 @@ class VimSurroundExtension : VimExtension {
companion object {
fun change(editor: Editor, charFrom: Char, newSurround: Pair<String, String>?) {
editor.runWithEveryCaretAndRestore { changeAtCaret(editor, charFrom, newSurround) }
}
fun changeAtCaret(editor: Editor, charFrom: Char, newSurround: Pair<String, String>?) {
// We take over the " register, so preserve it
val oldValue: List<KeyStroke>? = getRegister(REGISTER)
// Empty the " register
@@ -184,25 +188,43 @@ class VimSurroundExtension : VimExtension {
}
}
private class Operator : OperatorFunction {
private class Operator(private val supportsMultipleCursors: Boolean) : OperatorFunction {
override fun apply(editor: Editor, context: DataContext, selectionType: SelectionType): Boolean {
val c = getChar(editor)
if (c.code == 0) return true
val pair = getOrInputPair(c, editor) ?: return false
// XXX: Will it work with line-wise or block-wise selections?
val range = getSurroundRange(editor) ?: return false
runWriteAction {
val change = VimPlugin.getChange()
val leftSurround = pair.first
val primaryCaret = editor.caretModel.primaryCaret
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, leftSurround)
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + leftSurround.length, pair.second)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor)
if (supportsMultipleCursors) {
editor.runWithEveryCaretAndRestore {
applyOnce(editor, change, pair)
}
}
else {
applyOnce(editor, change, pair)
// Jump back to start
executeNormalWithoutMapping(injector.parser.parseKeys("`["), editor)
}
}
return true
}
private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: Pair<String, String>) {
// XXX: Will it work with line-wise or block-wise selections?
val range = getSurroundRange(editor)
if (range != null) {
val primaryCaret = editor.caretModel.primaryCaret
change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, pair.first)
change.insertText(
IjVimEditor(editor),
IjVimCaret(primaryCaret),
range.endOffset + pair.first.length,
pair.second
)
}
}
private fun getSurroundRange(editor: Editor): TextRange? = when (editor.mode) {
CommandState.Mode.COMMAND -> VimPlugin.getMark().getChangeMarks(editor.vim)

View File

@@ -39,7 +39,6 @@ import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.util.containers.ContainerUtil;
import com.maddyhome.idea.vim.EventFacade;
import com.maddyhome.idea.vim.RegisterActions;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.*;
import com.maddyhome.idea.vim.command.*;
@@ -123,10 +122,10 @@ public class ChangeGroup extends VimChangeGroupBase {
* @param col The column to indent to
*/
private void insertNewLineBelow(@NotNull VimEditor editor, @NotNull VimCaret caret, int col) {
if (((IjVimEditor) editor).getEditor().isOneLineMode()) return;
if (editor.isOneLineMode()) return;
injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret));
UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
caret.moveToOffset(injector.getMotion().moveCaretToLineEnd(editor, caret));
editor.setVimChangeActionSwitchMode(CommandState.Mode.INSERT);
insertText(editor, caret, "\n" + IndentConfig.create(((IjVimEditor) editor).getEditor()).createIndentBySize(col));
}
@@ -165,41 +164,6 @@ public class ChangeGroup extends VimChangeGroupBase {
}
@Override
public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType(@NotNull VimEditor editor,
@NotNull VimCaret caret,
@NotNull ExecutionContext context,
final @NotNull Argument argument,
boolean isChange,
@NotNull OperatorArguments operatorArguments) {
final TextRange range =
injector.getMotion().getMotionRange(editor, caret, context, argument, operatorArguments);
if (range == null) return null;
// Delete motion commands that are not linewise become linewise if all the following are true:
// 1) The range is across multiple lines
// 2) There is only whitespace before the start of the range
// 3) There is only whitespace after the end of the range
SelectionType type;
if (argument.getMotion().isLinewiseMotion()) {
type = SelectionType.LINE_WISE;
}
else {
type = SelectionType.CHARACTER_WISE;
}
final Command motion = argument.getMotion();
if (!isChange && !motion.isLinewiseMotion()) {
VimLogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset());
VimLogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset());
if (start.getLine() != end.getLine()) {
if (!SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getStartOffset(), -1) &&
!SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getEndOffset(), 1)) {
type = SelectionType.LINE_WISE;
}
}
}
return new Pair<>(range, type);
}
@Override
public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType2(@NotNull VimEditor editor,
@@ -236,60 +200,6 @@ public class ChangeGroup extends VimChangeGroupBase {
return new Pair<>(range, type);
}
/**
* Delete the range of text.
*
* @param editor The editor to delete the text from
* @param caret The caret to be moved after deletion
* @param range The range to delete
* @param type The type of deletion
* @param isChange Is from a change action
* @return true if able to delete the text, false if not
*/
@Override
public boolean deleteRange(@NotNull VimEditor editor,
@NotNull VimCaret caret,
@NotNull TextRange range,
@Nullable SelectionType type,
boolean isChange) {
// Update the last column before we delete, or we might be retrieving the data for a line that no longer exists
UserDataManager.setVimLastColumn(((IjVimCaret) caret).getCaret(), InlayHelperKt.getInlayAwareVisualColumn(((IjVimCaret) caret).getCaret()));
boolean removeLastNewLine = removeLastNewLine(editor, range, type);
final boolean res = deleteText(editor, range, type);
if (removeLastNewLine) {
int textLength = ((IjVimEditor) editor).getEditor().getDocument().getTextLength();
((IjVimEditor) editor).getEditor().getDocument().deleteString(textLength - 1, textLength);
}
if (res) {
int pos = EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), range.getStartOffset(), isChange);
if (type == SelectionType.LINE_WISE) {
pos = VimPlugin.getMotion()
.moveCaretToLineWithStartOfLineOption(editor, editor.offsetToLogicalPosition(pos).getLine(),
caret);
}
injector.getMotion().moveCaret(editor, caret, pos);
}
return res;
}
private boolean removeLastNewLine(@NotNull VimEditor editor, @NotNull TextRange range, @Nullable SelectionType type) {
int endOffset = range.getEndOffset();
int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor());
if (endOffset > fileSize) {
if (injector.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.ideastrictmodeName, OptionConstants.ideastrictmodeName)) {
throw new IllegalStateException("Incorrect offset. File size: " + fileSize + ", offset: " + endOffset);
}
endOffset = fileSize;
}
return type == SelectionType.LINE_WISE &&
range.getStartOffset() != 0 &&
((IjVimEditor) editor).getEditor().getDocument().getCharsSequence().charAt(endOffset - 1) != '\n' &&
endOffset == fileSize;
}
@Override
public void insertLineAround(@NotNull VimEditor editor, @NotNull ExecutionContext context, int shift) {
com.maddyhome.idea.vim.newapi.ChangeGroupKt.insertLineAround(editor, context, shift);
@@ -303,48 +213,6 @@ public class ChangeGroup extends VimChangeGroupBase {
return com.maddyhome.idea.vim.newapi.ChangeGroupKt.deleteRange(editor, caret, range, type);
}
/**
* Delete count characters and then enter insert mode
*
* @param editor The editor to change
* @param caret The caret to be moved
* @param count The number of characters to change
* @return true if able to delete count characters, false if not
*/
@Override
public boolean changeCharacters(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor());
int col = ((IjVimCaret) caret).getCaret().getLogicalPosition().column;
if (col + count >= len) {
return changeEndOfLine(editor, caret, 1);
}
boolean res = deleteCharacter(editor, caret, count, true);
if (res) {
UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
}
return res;
}
/**
* Delete from the cursor to the end of count - 1 lines down and enter insert mode
*
* @param editor The editor to change
* @param caret The caret to perform action on
* @param count The number of lines to change
* @return true if able to delete count lines, false if not
*/
@Override
public boolean changeEndOfLine(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
boolean res = deleteEndOfLine(editor, caret, count);
if (res) {
injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret));
UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
}
return res;
}
/**
* Delete the text covered by the motion command argument and enter insert mode
@@ -368,9 +236,9 @@ public class ChangeGroup extends VimChangeGroupBase {
String id = motion.getAction().getId();
boolean kludge = false;
boolean bigWord = id.equals(VIM_MOTION_BIG_WORD_RIGHT);
final CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
final int offset = ((IjVimCaret) caret).getCaret().getOffset();
int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor());
final CharSequence chars = editor.text();
final int offset = caret.getOffset().getPoint();
int fileSize = ((int)editor.fileSize());
if (fileSize > 0 && offset < fileSize) {
final CharacterHelper.CharacterType charType = CharacterHelper.charType(chars.charAt(offset), bigWord);
if (charType != CharacterHelper.CharacterType.WHITESPACE) {
@@ -379,24 +247,24 @@ public class ChangeGroup extends VimChangeGroupBase {
if (wordMotions.contains(id) && lastWordChar && motion.getCount() == 1) {
final boolean res = deleteCharacter(editor, caret, 1, true);
if (res) {
UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
editor.setVimChangeActionSwitchMode(CommandState.Mode.INSERT);
}
return res;
}
switch (id) {
case VIM_MOTION_WORD_RIGHT:
kludge = true;
motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_WORD_END_RIGHT));
motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT));
break;
case VIM_MOTION_BIG_WORD_RIGHT:
kludge = true;
motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT));
motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT));
break;
case VIM_MOTION_CAMEL_RIGHT:
kludge = true;
motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_CAMEL_END_RIGHT));
motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT));
break;
}
@@ -405,8 +273,8 @@ public class ChangeGroup extends VimChangeGroupBase {
if (kludge) {
int cnt = operatorArguments.getCount1() * motion.getCount();
int pos1 = SearchHelper.findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false);
int pos2 = SearchHelper.findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false);
int pos1 = injector.getSearchHelper().findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false);
int pos2 = injector.getSearchHelper().findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false);
if (logger.isDebugEnabled()) {
logger.debug("pos=" + offset);
logger.debug("pos1=" + pos1);
@@ -427,11 +295,11 @@ public class ChangeGroup extends VimChangeGroupBase {
}
}
if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.experimentalapiName, OptionConstants.experimentalapiName)) {
if (injector.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.experimentalapiName, OptionConstants.experimentalapiName)) {
Pair<TextRange, SelectionType> deleteRangeAndType =
getDeleteRangeAndType2(editor, caret, context, argument, true, operatorArguments.withCount0(count0));
if (deleteRangeAndType == null) return false;
ChangeGroupKt.changeRange(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), deleteRangeAndType.getFirst(), deleteRangeAndType.getSecond(), ((IjExecutionContext) context).getContext());
//ChangeGroupKt.changeRange(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), deleteRangeAndType.getFirst(), deleteRangeAndType.getSecond(), ((IjExecutionContext) context).getContext());
return true;
}
else {
@@ -442,23 +310,6 @@ public class ChangeGroup extends VimChangeGroupBase {
}
}
/**
* Counts number of lines in the visual block.
* <p>
* The result includes empty and short lines which does not have explicit start position (caret).
*
* @param editor The editor the block was selected in
* @param range The range corresponding to the selected block
* @return total number of lines
*/
public static int getLinesCountInVisualBlock(@NotNull VimEditor editor, @NotNull TextRange range) {
final int[] startOffsets = range.getStartOffsets();
if (startOffsets.length == 0) return 0;
final VimLogicalPosition firstStart = editor.offsetToLogicalPosition(startOffsets[0]);
final VimLogicalPosition lastStart = editor.offsetToLogicalPosition(startOffsets[range.size() - 1]);
return lastStart.getLine() - firstStart.getLine() + 1;
}
/**
* Toggles the case of count characters
*
@@ -484,7 +335,7 @@ public class ChangeGroup extends VimChangeGroupBase {
@NotNull TextRange range,
boolean append,
@NotNull OperatorArguments operatorArguments) {
final int lines = getLinesCountInVisualBlock(editor, range);
final int lines = VimChangeGroupBase.Companion.getLinesCountInVisualBlock(editor, range);
final VimLogicalPosition startPosition = editor.offsetToLogicalPosition(range.getStartOffset());
boolean visualBlockMode = operatorArguments.getMode() == CommandState.Mode.VISUAL &&
@@ -589,24 +440,24 @@ public class ChangeGroup extends VimChangeGroupBase {
int col = 0;
int lines = 0;
if (type == SelectionType.BLOCK_WISE) {
lines = getLinesCountInVisualBlock(editor, range);
lines = VimChangeGroupBase.Companion.getLinesCountInVisualBlock(editor, range);
col = editor.offsetToLogicalPosition(range.getStartOffset()).getColumn();
if (UserDataManager.getVimLastColumn(((IjVimCaret) caret).getCaret()) == VimMotionGroupBase.LAST_COLUMN) {
if (caret.getVimLastColumn() == VimMotionGroupBase.LAST_COLUMN) {
col = VimMotionGroupBase.LAST_COLUMN;
}
}
boolean after = range.getEndOffset() >= EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor());
boolean after = range.getEndOffset() >= editor.fileSize();
final VimLogicalPosition lp = editor.offsetToLogicalPosition(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, caret));
final VimLogicalPosition lp = editor.offsetToLogicalPosition(injector.getMotion().moveCaretToLineStartSkipLeading(editor, caret));
boolean res = deleteRange(editor, caret, range, type, true);
if (res) {
if (type == SelectionType.LINE_WISE) {
// Please don't use `getDocument().getText().isEmpty()` because it converts CharSequence into String
if (((IjVimEditor) editor).getEditor().getDocument().getTextLength() == 0) {
if (editor.fileSize() == 0) {
insertBeforeCursor(editor, context);
}
else if (after && !EditorHelperRt.endsWithNewLine(((IjVimEditor) editor).getEditor())) {
else if (after && !EngineEditorHelperKt.endsWithNewLine(editor)) {
insertNewLineBelow(editor, caret, lp.getColumn());
}
else {

View File

@@ -890,6 +890,10 @@ public class SearchGroup extends VimSearchGroupBase implements PersistentStateCo
@Override
public void setLastSearchPattern(@Nullable String lastSearchPattern) {
this.lastSearch = lastSearchPattern;
if (showSearchHighlight) {
resetIncsearchHighlights();
updateSearchHighlights();
}
}
@Override

View File

@@ -22,6 +22,7 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
@@ -106,3 +107,41 @@ val Caret.vimLine: Int
*/
val Editor.vimLine: Int
get() = this.caretModel.currentCaret.vimLine
inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) {
val caretModel = this.caretModel
val carets = if (this.inBlockSubMode) null else caretModel.allCarets
if (carets == null || carets.size == 1) {
action()
}
else {
var initialDocumentSize = this.document.textLength
var documentSizeDifference = 0
val caretOffsets = carets.map { it.selectionStart to it.selectionEnd }
val restoredCarets = mutableListOf<CaretState>()
caretModel.removeSecondaryCarets()
for ((selectionStart, selectionEnd) in caretOffsets) {
if (selectionStart == selectionEnd) {
caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference)
}
else {
caretModel.primaryCaret.setSelection(
selectionStart + documentSizeDifference,
selectionEnd + documentSizeDifference
)
}
action()
restoredCarets.add(caretModel.caretsAndSelections.single())
val documentLength = this.document.textLength
documentSizeDifference += documentLength - initialDocumentSize
initialDocumentSize = documentLength
}
caretModel.caretsAndSelections = restoredCarets
}
}

View File

@@ -21,18 +21,24 @@ package com.maddyhome.idea.vim.helper
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.AnActionResult
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.NlsContexts
import com.intellij.util.SlowOperations
import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction
@@ -95,11 +101,48 @@ class IjActionExecutor : VimActionExecutor {
popup.showInFocusCenter()
return true
} else {
ActionUtil.performActionDumbAwareWithCallbacks(ijAction, event)
performDumbAwareWithCallbacks(ijAction, event) { ijAction.actionPerformed(event) }
return true
}
}
// This is taken directly from ActionUtil.performActionDumbAwareWithCallbacks
// But with one check removed. With this check some actions (like `:w` doesn't work)
// https://youtrack.jetbrains.com/issue/VIM-2691/File-is-not-saved-on-w
private fun performDumbAwareWithCallbacks(
action: AnAction,
event: AnActionEvent,
performRunnable: Runnable,
) {
val project = event.project
var indexError: IndexNotReadyException? = null
val manager = ActionManagerEx.getInstanceEx()
manager.fireBeforeActionPerformed(action, event)
val component = event.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT)
var result: AnActionResult? = null
try {
SlowOperations.allowSlowOperations(SlowOperations.ACTION_PERFORM).use { ignore ->
performRunnable.run()
result = AnActionResult.PERFORMED
}
} catch (ex: IndexNotReadyException) {
indexError = ex
result = AnActionResult.failed(ex)
} catch (ex: RuntimeException) {
result = AnActionResult.failed(ex)
throw ex
} catch (ex: Error) {
result = AnActionResult.failed(ex)
throw ex
} finally {
if (result == null) result = AnActionResult.failed(Throwable())
manager.fireAfterActionPerformed(action, event, result!!)
}
if (indexError != null) {
ActionUtil.showDumbModeWarning(project, event)
}
}
private fun canBePerformed(event: AnActionEvent, action: ActionGroup, context: DataContext): Boolean {
val presentation = event.presentation
return try {
@@ -155,6 +198,10 @@ class IjActionExecutor : VimActionExecutor {
return RegisterActions.findAction(id)
}
override fun findVimActionOrDie(id: String): EditorActionHandlerBase {
return RegisterActions.findActionOrDie(id)
}
override fun getAction(actionId: String): NativeAction? {
return ActionManager.getInstance().getAction(actionId)?.let { IjNativeAction(it) }
}

View File

@@ -143,4 +143,8 @@ class IjEditorHelper : EngineEditorHelper {
override fun getLeadingWhitespace(editor: VimEditor, line: Int): String {
return EditorHelper.getLeadingWhitespace(editor.ij, line)
}
override fun anyNonWhitespace(editor: VimEditor, offset: Int, dir: Int): Boolean {
return SearchHelper.anyNonWhitespace(editor.ij, offset, dir)
}
}

View File

@@ -99,5 +99,6 @@ class UndoRedoHelper : UndoRedoBase() {
while (check() && !changeListener.hasChanged) {
action.run()
}
vimDocument.removeChangeListener(changeListener)
}
}

View File

@@ -32,6 +32,7 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbAwareToggleAction
import com.maddyhome.idea.vim.KeyHandler
@@ -70,7 +71,7 @@ object IdeaSpecifics {
//region Track action id
if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL, OptionConstants.trackactionidsName)) {
if (action !is NotificationService.ActionIdNotifier.CopyActionId && action !is NotificationService.ActionIdNotifier.StopTracking) {
val id: String? = ActionManager.getInstance().getId(action)
val id: String? = ActionManager.getInstance().getId(action) ?: (action.shortcutSet as? ProxyShortcutSet)?.actionId
VimPlugin.getNotifications(dataContext.getData(CommonDataKeys.PROJECT)).notifyActionId(id)
}
}

View File

@@ -28,6 +28,7 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.LineDeleteShift
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimChangeGroupBase
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.injector
@@ -40,7 +41,6 @@ import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimRange
import com.maddyhome.idea.vim.common.including
import com.maddyhome.idea.vim.common.offset
import com.maddyhome.idea.vim.group.ChangeGroup
import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.inlayAwareVisualColumn
@@ -62,7 +62,7 @@ fun changeRange(
var col = 0
var lines = 0
if (type === SelectionType.BLOCK_WISE) {
lines = ChangeGroup.getLinesCountInVisualBlock(IjVimEditor(editor), range)
lines = VimChangeGroupBase.getLinesCountInVisualBlock(IjVimEditor(editor), range)
col = editor.offsetToLogicalPosition(range.startOffset).column
if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) {
col = VimMotionGroupBase.LAST_COLUMN

View File

@@ -54,4 +54,4 @@ class IjVimDocument(private val document: Document) : VimDocument {
override fun getOffsetGuard(offset: Offset): LiveRange? {
return document.getOffsetGuard(offset.point)?.vim
}
}
}

View File

@@ -110,6 +110,17 @@ class IjVimSearchHelper : VimSearchHelper {
)
}
override fun findNextWordEnd(
chars: CharSequence,
pos: Int,
size: Int,
count: Int,
bigWord: Boolean,
spaceWords: Boolean,
): Int {
return SearchHelper.findNextWordEnd(chars, pos, size, count, bigWord, spaceWords)
}
override fun findNextWord(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean): Int {
return SearchHelper.findNextWord(
(editor as IjVimEditor).editor,

View File

@@ -0,0 +1,49 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.maddyhome.idea.vim.vimscript.model.functions.handlers
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.vimscript.model.VimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimList
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler
class JoinFunctionHandler : FunctionHandler() {
override val name: String = "join"
override val minimumNumberOfArguments: Int = 1
override val maximumNumberOfArguments: Int = 2
override fun doFunction(
argumentValues: List<Expression>,
editor: VimEditor,
context: ExecutionContext,
vimContext: VimLContext,
): VimDataType {
val firstArgument = argumentValues[0].evaluate(editor, context, vimContext)
if (firstArgument !is VimList) {
throw ExException("E714: List required")
}
val secondArgument = argumentValues.getOrNull(1)?.evaluate(editor, context, vimContext) ?: VimString(" ")
return VimString(firstArgument.values.joinToString(secondArgument.asString()) { it.toString() })
}
}

View File

@@ -0,0 +1,44 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.maddyhome.idea.vim.vimscript.model.functions.handlers
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.vimscript.model.VimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler
import java.util.*
class TolowerFunctionHandler : FunctionHandler() {
override val name: String = "tolower"
override val minimumNumberOfArguments: Int = 1
override val maximumNumberOfArguments: Int = 1
override fun doFunction(
argumentValues: List<Expression>,
editor: VimEditor,
context: ExecutionContext,
vimContext: VimLContext,
): VimDataType {
val argumentString = argumentValues[0].evaluate(editor, context, vimContext).asString()
return VimString(argumentString.lowercase(Locale.getDefault()))
}
}

View File

@@ -0,0 +1,44 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.maddyhome.idea.vim.vimscript.model.functions.handlers
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.vimscript.model.VimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler
import java.util.*
class ToupperFunctionHandler : FunctionHandler() {
override val name: String = "toupper"
override val minimumNumberOfArguments: Int = 1
override val maximumNumberOfArguments: Int = 1
override fun doFunction(
argumentValues: List<Expression>,
editor: VimEditor,
context: ExecutionContext,
vimContext: VimLContext,
): VimDataType {
val argumentString = argumentValues[0].evaluate(editor, context, vimContext).asString()
return VimString(argumentString.uppercase(Locale.getDefault()))
}
}

View File

@@ -180,7 +180,7 @@ object CommandVisitor : VimscriptBaseVisitor<Command>() {
override fun visitLet1Command(ctx: VimscriptParser.Let1CommandContext): Command {
val ranges: Ranges = parseRanges(ctx.range())
val variable: Expression = expressionVisitor.visit(ctx.expr(0))
val operator = getByValue(ctx.assignmentOperator.text)
val operator = getByValue(ctx.assignmentOperator().text)
val expression: Expression = expressionVisitor.visit(ctx.expr(1))
return LetCommand(ranges, variable, operator, expression, true)
}

View File

@@ -11,5 +11,8 @@
<vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.FuncrefFunctionHandler" name="funcref"/>
<vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.HasFunctionHandler" name="has"/>
<vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler" name="submatch"/>
<vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.TolowerFunctionHandler" name="tolower"/>
<vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.ToupperFunctionHandler" name="toupper"/>
<vimLibraryFunction implementation="com.maddyhome.idea.vim.vimscript.model.functions.handlers.JoinFunctionHandler" name="join"/>
</extensions>
</idea-plugin>

View File

@@ -88,7 +88,7 @@ class UndoActionTest : VimTestCase() {
val after = """
A Discovery
I1 found${c} it in a legendary land
I1 found$c it in a legendary land
all rocks and lavender and tufted grass,
where it was settled on some sodden sand
hard by the torrent of a mountain pass.
@@ -101,4 +101,4 @@ class UndoActionTest : VimTestCase() {
val editor = myFixture.editor
return editor.caretModel.primaryCaret.hasSelection()
}
}
}

View File

@@ -0,0 +1,117 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jetbrains.plugins.ideavim.ex.implementation
import com.maddyhome.idea.vim.api.injector
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.Ignore
class LongerFunctionTest : VimTestCase() {
val script = """
function! IsUppercase(char)
return a:char >=# 'A' && a:char <=# 'Z'
endfunction
function! IsCaps(word)
return a:word ==# toupper(a:word)
endfunction
function! Capitalize(word)
return toupper(a:word[0]) .. a:word[1:]
endfunction
function! Split(text, delimeter)
let parts = []
let part = ''
for char in a:text
if char ==? a:delimeter
let parts += [part]
let part = ''
else
let part .= char
endif
endfor
let parts += [part]
return parts
endfunction
function! ToCamelCase()
let capitalize = 0
normal gv"wy
let wordUnderCaret = @w
let parts = Split(wordUnderCaret, '_')
let result = tolower(parts[0])
let counter = 1
while counter < len(parts)
let result .= Capitalize(tolower(parts[counter]))
let counter += 1
endwhile
execute 'normal gvc' .. result
endfunction
function! ToSnakeCase()
normal gv"wy
let wordUnderCaret = @w
let parts = []
let subword = ''
let atWordStart = 1
for char in wordUnderCaret
if IsUppercase(char) && !atWordStart
let parts += [toupper(subword)]
let subword = char
else
let subword .= char
endif
let atWordStart = char ==? ' '
endfor
let parts += [toupper(subword)]
execute 'normal gvc' .. join(parts, '_')
endfunction
vnoremap u :<C-u>call ToCamelCase()<CR>
vnoremap U :<C-u>call ToSnakeCase()<CR>
""".trimIndent()
// todo normal required
// fun `test 1`() {
// configureByText("""
// const val ${c}VERY_IMPORTANT_VALUE = 42
// """.trimIndent())
// injector.vimscriptExecutor.execute(script)
// typeText(injector.parser.parseKeys("veu"))
// assertState("""
// const val veryImportantValue${c} = 42
// """.trimIndent())
// }
// todo normal required
// fun `test 2`() {
// configureByText("""
// val ${c}myCamelCaseValue = "Hi, I'm a simple value"
// """.trimIndent())
// injector.vimscriptExecutor.execute(script)
// typeText(injector.parser.parseKeys("veU"))
// assertState("""
// val MY_CAMEL_CASE_VALUE${c} = "Hi, I'm a simple value"
// """.trimIndent())
// }
}

View File

@@ -0,0 +1,55 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jetbrains.plugins.ideavim.ex.implementation.functions
import org.jetbrains.plugins.ideavim.VimTestCase
class BasicStringFunctions : VimTestCase() {
fun `test toupper`() {
configureByText("\n")
typeText(commandToKeys("echo toupper('Vim is awesome')"))
assertExOutput("VIM IS AWESOME\n")
}
fun `test tolower`() {
configureByText("\n")
typeText(commandToKeys("echo toupper('Vim is awesome')"))
assertExOutput("vim is awesome\n")
}
fun `test join`() {
configureByText("\n")
typeText(commandToKeys("echo join(['Vim', 'is', 'awesome'], '_')"))
assertExOutput("Vim_is_awesome\n")
}
fun `test join without second argument`() {
configureByText("\n")
typeText(commandToKeys("echo join(['Vim', 'is', 'awesome'])"))
assertExOutput("Vim is awesome\n")
}
fun `test join with wrong first argument type`() {
configureByText("\n")
typeText(commandToKeys("echo join('Vim is awesome')"))
assertPluginError(true)
assertPluginErrorMessageContains("E714: List required")
}
}

View File

@@ -0,0 +1,55 @@
/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jetbrains.plugins.ideavim.ex.parser.commands
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.vimscript.model.commands.LetCommand
import com.maddyhome.idea.vim.vimscript.model.expressions.Register
import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression
import com.maddyhome.idea.vim.vimscript.model.expressions.operators.AssignmentOperator
import junit.framework.TestCase.assertTrue
import org.junit.Test
import kotlin.test.assertEquals
class LetCommandTest {
@Test
fun `let with register is parsed correctly`() {
val script = injector.vimscriptParser.parse("let @+=5")
assertEquals(1, script.units.size)
val command = script.units.first()
assertTrue(command is LetCommand)
command as LetCommand
assertEquals(Register('+'), command.variable)
assertEquals(AssignmentOperator.ASSIGNMENT, command.operator)
assertEquals(SimpleExpression(5), command.expression)
}
@Test
fun `let with register is parsed correctly 2`() {
val script = injector.vimscriptParser.parse("let @--=42")
assertEquals(1, script.units.size)
val command = script.units.first()
assertTrue(command is LetCommand)
command as LetCommand
assertEquals(Register('-'), command.variable)
assertEquals(AssignmentOperator.SUBTRACTION, command.operator)
assertEquals(SimpleExpression(42), command.expression)
}
}

View File

@@ -23,11 +23,12 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.command.SelectionType
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.put.PutData
import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.vimscript.model.Script
import javax.swing.KeyStroke
class InsertRegisterAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT
@@ -84,10 +85,10 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean {
val register: Register? = injector.registerGroup.getRegister(key)
if (register != null) {
val keys: List<KeyStroke> = register.keys
for (k in keys) {
injector.changeGroup.processKey(editor, context, k)
}
val text = register.rawText ?: injector.parser.toPrintableString(register.keys)
val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList())
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
injector.put.putText(editor, context, putData)
return true
}
return false

View File

@@ -33,4 +33,11 @@ interface EngineEditorHelper {
fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition
fun getVisualLineLength(editor: VimEditor, line: Int): Int
fun getLeadingWhitespace(editor: VimEditor, line: Int): String
}
fun anyNonWhitespace(editor: VimEditor, offset: Int, dir: Int): Boolean
}
fun VimEditor.endsWithNewLine(): Boolean {
val textLength = this.fileSize().toInt()
if (textLength == 0) return false
return this.text()[textLength - 1] == '\n'
}

View File

@@ -65,6 +65,7 @@ interface VimActionExecutor {
)
fun findVimAction(id: String): EditorActionHandlerBase?
fun findVimActionOrDie(id: String): EditorActionHandlerBase
fun getAction(actionId: String): NativeAction?
fun getActionIdList(idPrefix: String): List<String>

View File

@@ -1,6 +1,7 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandState
@@ -24,6 +25,8 @@ import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_END
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_START
import com.maddyhome.idea.vim.options.OptionConstants
import com.maddyhome.idea.vim.options.OptionScope
import com.maddyhome.idea.vim.register.RegisterConstants.LAST_INSERTED_TEXT_REGISTER
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@@ -798,6 +801,134 @@ abstract class VimChangeGroupBase : VimChangeGroup {
}
}
override fun getDeleteRangeAndType(
editor: VimEditor,
caret: VimCaret,
context: ExecutionContext,
argument: Argument,
isChange: Boolean,
operatorArguments: OperatorArguments,
): Pair<TextRange, SelectionType>? {
val range = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) ?: return null
// Delete motion commands that are not linewise become linewise if all the following are true:
// 1) The range is across multiple lines
// 2) There is only whitespace before the start of the range
// 3) There is only whitespace after the end of the range
var type: SelectionType = if (argument.motion.isLinewiseMotion()) {
SelectionType.LINE_WISE
} else {
SelectionType.CHARACTER_WISE
}
val motion = argument.motion
if (!isChange && !motion.isLinewiseMotion()) {
val start = editor.offsetToLogicalPosition(range.startOffset)
val end = editor.offsetToLogicalPosition(range.endOffset)
if (start.line != end.line) {
if (!injector.engineEditorHelper.anyNonWhitespace(editor, range.startOffset, -1) &&
!injector.engineEditorHelper.anyNonWhitespace(editor, range.endOffset, 1)
) {
type = SelectionType.LINE_WISE
}
}
}
return Pair(range, type)
}
/**
* Delete the range of text.
*
* @param editor The editor to delete the text from
* @param caret The caret to be moved after deletion
* @param range The range to delete
* @param type The type of deletion
* @param isChange Is from a change action
* @return true if able to delete the text, false if not
*/
override fun deleteRange(
editor: VimEditor,
caret: VimCaret,
range: TextRange,
type: SelectionType?,
isChange: Boolean,
): Boolean {
// Update the last column before we delete, or we might be retrieving the data for a line that no longer exists
caret.vimLastColumn = caret.inlayAwareVisualColumn
val removeLastNewLine = removeLastNewLine(editor, range, type)
val res = deleteText(editor, range, type)
if (removeLastNewLine) {
val textLength = editor.fileSize().toInt()
editor.deleteString(TextRange(textLength - 1, textLength))
}
if (res) {
var pos = injector.engineEditorHelper.normalizeOffset(editor, range.startOffset, isChange)
if (type === SelectionType.LINE_WISE) {
pos = injector.motion
.moveCaretToLineWithStartOfLineOption(
editor, editor.offsetToLogicalPosition(pos).line,
caret
)
}
injector.motion.moveCaret(editor, caret, pos)
}
return res
}
private fun removeLastNewLine(editor: VimEditor, range: TextRange, type: SelectionType?): Boolean {
var endOffset = range.endOffset
val fileSize = editor.fileSize().toInt()
if (endOffset > fileSize) {
check(
!injector.optionService.isSet(
OptionScope.GLOBAL,
OptionConstants.ideastrictmodeName,
OptionConstants.ideastrictmodeName
)
) { "Incorrect offset. File size: $fileSize, offset: $endOffset" }
endOffset = fileSize
}
return (type === SelectionType.LINE_WISE) && range.startOffset != 0 && editor.text()[endOffset - 1] != '\n' && endOffset == fileSize
}
/**
* Delete from the cursor to the end of count - 1 lines down and enter insert mode
*
* @param editor The editor to change
* @param caret The caret to perform action on
* @param count The number of lines to change
* @return true if able to delete count lines, false if not
*/
override fun changeEndOfLine(editor: VimEditor, caret: VimCaret, count: Int): Boolean {
val res = deleteEndOfLine(editor, caret, count)
if (res) {
caret.moveToOffset(injector.motion.moveCaretToLineEnd(editor, caret))
editor.vimChangeActionSwitchMode = CommandState.Mode.INSERT
}
return res
}
/**
* Delete count characters and then enter insert mode
*
* @param editor The editor to change
* @param caret The caret to be moved
* @param count The number of characters to change
* @return true if able to delete count characters, false if not
*/
override fun changeCharacters(editor: VimEditor, caret: VimCaret, count: Int): Boolean {
val len = injector.engineEditorHelper.getLineLength(editor)
val col = caret.getLogicalPosition().column
if (col + count >= len) {
return changeEndOfLine(editor, caret, 1)
}
val res = deleteCharacter(editor, caret, count, true)
if (res) {
editor.vimChangeActionSwitchMode = CommandState.Mode.INSERT
}
return res
}
/**
* Clears all the keystrokes from the current insert command
*
@@ -871,6 +1002,24 @@ abstract class VimChangeGroupBase : VimChangeGroup {
companion object {
private const val MAX_REPEAT_CHARS_COUNT = 10000
private val logger = vimLogger<VimChangeGroupBase>()
/**
* Counts number of lines in the visual block.
*
*
* The result includes empty and short lines which does not have explicit start position (caret).
*
* @param editor The editor the block was selected in
* @param range The range corresponding to the selected block
* @return total number of lines
*/
fun getLinesCountInVisualBlock(editor: VimEditor, range: TextRange): Int {
val startOffsets = range.startOffsets
if (startOffsets.isEmpty()) return 0
val firstStart = editor.offsetToLogicalPosition(startOffsets[0])
val lastStart = editor.offsetToLogicalPosition(startOffsets[range.size() - 1])
return lastStart.line - firstStart.line + 1
}
}
}

View File

@@ -75,6 +75,15 @@ interface VimSearchHelper {
bigWord: Boolean,
): Int
fun findNextWordEnd(
chars: CharSequence,
pos: Int,
size: Int,
count: Int,
bigWord: Boolean,
spaceWords: Boolean,
): Int
fun findNextWord(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean): Int
fun findPattern(

View File

@@ -12,6 +12,7 @@ import com.maddyhome.idea.vim.command.isChar
import com.maddyhome.idea.vim.command.isLine
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.firstOrNull
import com.maddyhome.idea.vim.helper.mode
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import java.util.*
import kotlin.math.abs
@@ -140,7 +141,9 @@ abstract class VimPutBase : VimPut {
}
"postEndOffset" -> caret.moveToOffset(endOffset + 1)
"preLineEndOfEndOffset" -> {
val pos = min(endOffset, editor.getLineEndForOffset(endOffset - 1) - 1)
var rightestPosition = editor.getLineEndForOffset(endOffset - 1)
if (editor.mode != CommandState.Mode.INSERT) --rightestPosition // it's not possible to place a caret at the end of the line in any mode except insert
val pos = min(endOffset, rightestPosition)
caret.moveToOffset(pos)
}
}

View File

@@ -48,6 +48,11 @@ interface VimRegisterGroup {
isDelete: Boolean,
): Boolean
/**
* Stores text to any writable register (used for the let command)
*/
fun storeText(register: Char, text: String): Boolean
/**
* Stores text, character wise, in the given special register
*

View File

@@ -18,6 +18,7 @@ import com.maddyhome.idea.vim.register.RegisterConstants.RECORDABLE_REGISTERS
import com.maddyhome.idea.vim.register.RegisterConstants.SMALL_DELETION_REGISTER
import com.maddyhome.idea.vim.register.RegisterConstants.UNNAMED_REGISTER
import com.maddyhome.idea.vim.register.RegisterConstants.VALID_REGISTERS
import com.maddyhome.idea.vim.register.RegisterConstants.WRITABLE_REGISTERS
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import javax.swing.KeyStroke
@@ -292,6 +293,24 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return true
}
override fun storeText(register: Char, text: String): Boolean {
if (!WRITABLE_REGISTERS.contains(register)) {
return false
}
logger.debug { "register '$register' contains: \"$text\"" }
val textToStore = if (register.isUpperCase()) {
(getRegister(register.lowercaseChar())?.rawText ?: "") + text
} else {
text
}
val reg = Register(register, SelectionType.CHARACTER_WISE, textToStore, ArrayList())
saveRegister(register, reg)
if (register == '/') {
injector.searchGroup.lastSearchPattern = text // todo we should not have this field if we have the "/" register
}
return true
}
private fun guessSelectionType(text: String): SelectionType {
return if (text.endsWith("\n")) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE
}

View File

@@ -21,9 +21,11 @@ package com.maddyhome.idea.vim.vimscript.model.commands
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.options.OptionScope
import com.maddyhome.idea.vim.register.RegisterConstants
import com.maddyhome.idea.vim.vimscript.model.ExecutionResult
import com.maddyhome.idea.vim.vimscript.model.Script
import com.maddyhome.idea.vim.vimscript.model.VimLContext
@@ -56,8 +58,12 @@ data class LetCommand(
val isSyntaxSupported: Boolean,
) : Command.SingleExecution(ranges) {
companion object {
private val logger = vimLogger<LetCommand>()
}
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
@Throws(ExException::class)
override fun processCommand(editor: VimEditor, context: ExecutionContext): ExecutionResult {
if (!isSyntaxSupported) return ExecutionResult.Error
@@ -194,13 +200,19 @@ data class LetCommand(
is EnvVariableExpression -> TODO()
is Register -> {
if (!(variable.char.isLetter() || variable.char.isDigit() || variable.char == '"')) {
throw ExException("Let command supports only 0-9a-zA-Z\" registers at the moment")
if (RegisterConstants.WRITABLE_REGISTERS.contains(variable.char)) {
val result = injector.registerGroup.storeText(variable.char, expression.evaluate(editor, context, vimContext).asString())
if (!result) {
logger.error("""
Error during `let ${variable.originalString} ${operator.value} ${expression.originalString}` command execution.
Could not set register value
""".trimIndent())
}
} else if (RegisterConstants.VALID_REGISTERS.contains(variable.char)) {
throw ExException("E354: Invalid register name: '${variable.char}'")
} else {
throw ExException("E18: Unexpected characters in :let")
}
injector.registerGroup.startRecording(editor, variable.char)
injector.registerGroup.recordText(expression.evaluate(editor, context, vimContext).asString())
injector.registerGroup.finishRecording(editor)
}
else -> throw ExException("E121: Undefined variable")

View File

@@ -61,7 +61,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH
)
)
}
initializeFunctionVariables(argumentValues, editor, context)
initializeFunctionVariables(argumentValues, editor, context, vimContext)
if (function.flags.contains(FunctionFlag.RANGE)) {
val line = (injector.variableService.getNonNullVariableValue(Variable(Scope.FUNCTION_VARIABLE, "firstline"), editor, context, function) as VimInt).value
@@ -131,12 +131,12 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH
return returnValue
}
private fun initializeFunctionVariables(argumentValues: List<Expression>, editor: VimEditor, context: ExecutionContext) {
private fun initializeFunctionVariables(argumentValues: List<Expression>, editor: VimEditor, context: ExecutionContext, functionCallContext: VimLContext) {
// non-optional function arguments
for ((index, name) in function.args.withIndex()) {
injector.variableService.storeVariable(
Variable(Scope.FUNCTION_VARIABLE, name),
argumentValues[index].evaluate(editor, context, function.vimContext),
argumentValues[index].evaluate(editor, context, functionCallContext),
editor,
context,
function
@@ -147,7 +147,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH
val expressionToStore = if (index + function.args.size < argumentValues.size) argumentValues[index + function.args.size] else function.defaultArgs[index].second
injector.variableService.storeVariable(
Variable(Scope.FUNCTION_VARIABLE, function.defaultArgs[index].first),
expressionToStore.evaluate(editor, context, function.vimContext),
expressionToStore.evaluate(editor, context, functionCallContext),
editor,
context,
function
@@ -158,7 +158,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH
val remainingArgs = if (function.args.size + function.defaultArgs.size < argumentValues.size) {
VimList(
argumentValues.subList(function.args.size + function.defaultArgs.size, argumentValues.size)
.map { it.evaluate(editor, context, function.vimContext) }.toMutableList()
.map { it.evaluate(editor, context, functionCallContext) }.toMutableList()
)
} else {
VimList(mutableListOf())