1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-08-23 13:01:04 +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-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-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-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: ### Merged PRs:
* [468](https://github.com/JetBrains/ideavim/pull/468) by [Thomas Schouten](https://github.com/PHPirates): Implement UserDataHolder for EditorDataContext * [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 * [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 * [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 * [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 ## 1.10.0, 2022-02-17

View File

@@ -1,6 +1,8 @@
version: 1.0 version: 1.0
profile: profile:
name: Qodana name: Qodana
include:
- name: CheckDependencyLicenses
exclude: exclude:
- name: MoveVariableDeclarationIntoWhen - name: MoveVariableDeclarationIntoWhen
- name: PluginXmlValidity - name: PluginXmlValidity
@@ -9,12 +11,15 @@ exclude:
- name: UnusedReturnValue - name: UnusedReturnValue
- name: All - name: All
paths: paths:
- build.gradle - build.gradle.kts
- gradle/wrapper/gradle-wrapper.properties - gradle/wrapper/gradle-wrapper.properties
- resources/icons/youtrack.svg - src/main/resources/icons/youtrack.svg
- src/com/maddyhome/idea/vim/ex/vimscript/VimScriptCommandHandler.java - src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java
- src/com/maddyhome/idea/vim/helper/SearchHelper.java - src/main/java/com/maddyhome/idea/vim/regexp/RegExp.kt
- src/com/maddyhome/idea/vim/regexp/RegExp.java - src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/JavaText.kt
- test/org/jetbrains/plugins/ideavim/propertybased/samples/JavaText.kt - src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/LoremText.kt
- test/org/jetbrains/plugins/ideavim/propertybased/samples/LoremText.kt - src/test/java/org/jetbrains/plugins/ideavim/propertybased/samples/SimpleText.kt
- test/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: letCommands:
(WS | COLON)* range? (WS | COLON)* LET WS+ expr WS* (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)+) WS* expr WS* ((inline_comment NEW_LINE) | (NEW_LINE | BAR)+)
#Let1Command| #Let1Command|
@@ -154,6 +154,21 @@ letCommands:
#Let2Command #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: shortRange:
((QUESTION (~QUESTION)* QUESTION?) | (DIV (~DIV)* DIV?)); ((QUESTION (~QUESTION)* QUESTION?) | (DIV (~DIV)* DIV?));
range: range:
@@ -778,12 +793,12 @@ IS_NOT_CS: 'isnot#';
// Assignment operators // Assignment operators
ASSIGN: '='; ASSIGN: '=';
PLUS_ASSIGN: '+='; //PLUS_ASSIGN: '+=';
MINUS_ASSIGN: '-='; //MINUS_ASSIGN: '-=';
STAR_ASSIGN: '*='; //STAR_ASSIGN: '*=';
DIV_ASSIGN: '/='; //DIV_ASSIGN: '/=';
MOD_ASSIGN: '%='; //MOD_ASSIGN: '%=';
DOT_ASSIGN: '.='; //DOT_ASSIGN: '.=';
// Escaped chars // Escaped chars
ESCAPED_QUESTION: '\\?'; ESCAPED_QUESTION: '\\?';

View File

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

View File

@@ -39,7 +39,6 @@ import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiUtilBase; import com.intellij.psi.util.PsiUtilBase;
import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.ContainerUtil;
import com.maddyhome.idea.vim.EventFacade; import com.maddyhome.idea.vim.EventFacade;
import com.maddyhome.idea.vim.RegisterActions;
import com.maddyhome.idea.vim.VimPlugin; import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.*; import com.maddyhome.idea.vim.api.*;
import com.maddyhome.idea.vim.command.*; import com.maddyhome.idea.vim.command.*;
@@ -123,10 +122,10 @@ public class ChangeGroup extends VimChangeGroupBase {
* @param col The column to indent to * @param col The column to indent to
*/ */
private void insertNewLineBelow(@NotNull VimEditor editor, @NotNull VimCaret caret, int col) { 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)); caret.moveToOffset(injector.getMotion().moveCaretToLineEnd(editor, caret));
UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT); editor.setVimChangeActionSwitchMode(CommandState.Mode.INSERT);
insertText(editor, caret, "\n" + IndentConfig.create(((IjVimEditor) editor).getEditor()).createIndentBySize(col)); 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 @Override
public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType2(@NotNull VimEditor editor, 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); 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 @Override
public void insertLineAround(@NotNull VimEditor editor, @NotNull ExecutionContext context, int shift) { public void insertLineAround(@NotNull VimEditor editor, @NotNull ExecutionContext context, int shift) {
com.maddyhome.idea.vim.newapi.ChangeGroupKt.insertLineAround(editor, context, 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); 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 * 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(); String id = motion.getAction().getId();
boolean kludge = false; boolean kludge = false;
boolean bigWord = id.equals(VIM_MOTION_BIG_WORD_RIGHT); boolean bigWord = id.equals(VIM_MOTION_BIG_WORD_RIGHT);
final CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence(); final CharSequence chars = editor.text();
final int offset = ((IjVimCaret) caret).getCaret().getOffset(); final int offset = caret.getOffset().getPoint();
int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()); int fileSize = ((int)editor.fileSize());
if (fileSize > 0 && offset < fileSize) { if (fileSize > 0 && offset < fileSize) {
final CharacterHelper.CharacterType charType = CharacterHelper.charType(chars.charAt(offset), bigWord); final CharacterHelper.CharacterType charType = CharacterHelper.charType(chars.charAt(offset), bigWord);
if (charType != CharacterHelper.CharacterType.WHITESPACE) { if (charType != CharacterHelper.CharacterType.WHITESPACE) {
@@ -379,24 +247,24 @@ public class ChangeGroup extends VimChangeGroupBase {
if (wordMotions.contains(id) && lastWordChar && motion.getCount() == 1) { if (wordMotions.contains(id) && lastWordChar && motion.getCount() == 1) {
final boolean res = deleteCharacter(editor, caret, 1, true); final boolean res = deleteCharacter(editor, caret, 1, true);
if (res) { if (res) {
UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT); editor.setVimChangeActionSwitchMode(CommandState.Mode.INSERT);
} }
return res; return res;
} }
switch (id) { switch (id) {
case VIM_MOTION_WORD_RIGHT: case VIM_MOTION_WORD_RIGHT:
kludge = true; kludge = true;
motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_WORD_END_RIGHT)); motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT));
break; break;
case VIM_MOTION_BIG_WORD_RIGHT: case VIM_MOTION_BIG_WORD_RIGHT:
kludge = true; kludge = true;
motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT)); motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT));
break; break;
case VIM_MOTION_CAMEL_RIGHT: case VIM_MOTION_CAMEL_RIGHT:
kludge = true; kludge = true;
motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_CAMEL_END_RIGHT)); motion.setAction(injector.getActionExecutor().findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT));
break; break;
} }
@@ -405,8 +273,8 @@ public class ChangeGroup extends VimChangeGroupBase {
if (kludge) { if (kludge) {
int cnt = operatorArguments.getCount1() * motion.getCount(); int cnt = operatorArguments.getCount1() * motion.getCount();
int pos1 = SearchHelper.findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false); int pos1 = injector.getSearchHelper().findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false);
int pos2 = SearchHelper.findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false); int pos2 = injector.getSearchHelper().findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false);
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("pos=" + offset); logger.debug("pos=" + offset);
logger.debug("pos1=" + pos1); 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 = Pair<TextRange, SelectionType> deleteRangeAndType =
getDeleteRangeAndType2(editor, caret, context, argument, true, operatorArguments.withCount0(count0)); getDeleteRangeAndType2(editor, caret, context, argument, true, operatorArguments.withCount0(count0));
if (deleteRangeAndType == null) return false; 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; return true;
} }
else { 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 * Toggles the case of count characters
* *
@@ -484,7 +335,7 @@ public class ChangeGroup extends VimChangeGroupBase {
@NotNull TextRange range, @NotNull TextRange range,
boolean append, boolean append,
@NotNull OperatorArguments operatorArguments) { @NotNull OperatorArguments operatorArguments) {
final int lines = getLinesCountInVisualBlock(editor, range); final int lines = VimChangeGroupBase.Companion.getLinesCountInVisualBlock(editor, range);
final VimLogicalPosition startPosition = editor.offsetToLogicalPosition(range.getStartOffset()); final VimLogicalPosition startPosition = editor.offsetToLogicalPosition(range.getStartOffset());
boolean visualBlockMode = operatorArguments.getMode() == CommandState.Mode.VISUAL && boolean visualBlockMode = operatorArguments.getMode() == CommandState.Mode.VISUAL &&
@@ -589,24 +440,24 @@ public class ChangeGroup extends VimChangeGroupBase {
int col = 0; int col = 0;
int lines = 0; int lines = 0;
if (type == SelectionType.BLOCK_WISE) { if (type == SelectionType.BLOCK_WISE) {
lines = getLinesCountInVisualBlock(editor, range); lines = VimChangeGroupBase.Companion.getLinesCountInVisualBlock(editor, range);
col = editor.offsetToLogicalPosition(range.getStartOffset()).getColumn(); 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; 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); boolean res = deleteRange(editor, caret, range, type, true);
if (res) { if (res) {
if (type == SelectionType.LINE_WISE) { if (type == SelectionType.LINE_WISE) {
// Please don't use `getDocument().getText().isEmpty()` because it converts CharSequence into String // 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); insertBeforeCursor(editor, context);
} }
else if (after && !EditorHelperRt.endsWithNewLine(((IjVimEditor) editor).getEditor())) { else if (after && !EngineEditorHelperKt.endsWithNewLine(editor)) {
insertNewLineBelow(editor, caret, lp.getColumn()); insertNewLineBelow(editor, caret, lp.getColumn());
} }
else { else {

View File

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

View File

@@ -22,6 +22,7 @@ package com.maddyhome.idea.vim.helper
import com.intellij.codeWithMe.ClientId import com.intellij.codeWithMe.ClientId
import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.CaretState
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
@@ -106,3 +107,41 @@ val Caret.vimLine: Int
*/ */
val Editor.vimLine: Int val Editor.vimLine: Int
get() = this.caretModel.currentCaret.vimLine 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.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.AnActionResult
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
import com.intellij.openapi.actionSystem.PlatformDataKeys import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.command.UndoConfirmationPolicy import com.intellij.openapi.command.UndoConfirmationPolicy
import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.actionSystem.DocCommandGroupId import com.intellij.openapi.editor.actionSystem.DocCommandGroupId
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.util.SlowOperations
import com.maddyhome.idea.vim.RegisterActions import com.maddyhome.idea.vim.RegisterActions
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.NativeAction import com.maddyhome.idea.vim.api.NativeAction
@@ -95,11 +101,48 @@ class IjActionExecutor : VimActionExecutor {
popup.showInFocusCenter() popup.showInFocusCenter()
return true return true
} else { } else {
ActionUtil.performActionDumbAwareWithCallbacks(ijAction, event) performDumbAwareWithCallbacks(ijAction, event) { ijAction.actionPerformed(event) }
return true 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 { private fun canBePerformed(event: AnActionEvent, action: ActionGroup, context: DataContext): Boolean {
val presentation = event.presentation val presentation = event.presentation
return try { return try {
@@ -155,6 +198,10 @@ class IjActionExecutor : VimActionExecutor {
return RegisterActions.findAction(id) return RegisterActions.findAction(id)
} }
override fun findVimActionOrDie(id: String): EditorActionHandlerBase {
return RegisterActions.findActionOrDie(id)
}
override fun getAction(actionId: String): NativeAction? { override fun getAction(actionId: String): NativeAction? {
return ActionManager.getInstance().getAction(actionId)?.let { IjNativeAction(it) } 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 { override fun getLeadingWhitespace(editor: VimEditor, line: Int): String {
return EditorHelper.getLeadingWhitespace(editor.ij, line) 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) { while (check() && !changeListener.hasChanged) {
action.run() 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.CommonDataKeys
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.ex.AnActionListener import com.intellij.openapi.actionSystem.ex.AnActionListener
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbAwareToggleAction import com.intellij.openapi.project.DumbAwareToggleAction
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
@@ -70,7 +71,7 @@ object IdeaSpecifics {
//region Track action id //region Track action id
if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL, OptionConstants.trackactionidsName)) { if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL, OptionConstants.trackactionidsName)) {
if (action !is NotificationService.ActionIdNotifier.CopyActionId && action !is NotificationService.ActionIdNotifier.StopTracking) { 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) 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.ExecutionContext
import com.maddyhome.idea.vim.api.LineDeleteShift import com.maddyhome.idea.vim.api.LineDeleteShift
import com.maddyhome.idea.vim.api.VimCaret 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.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.injector 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.VimRange
import com.maddyhome.idea.vim.common.including import com.maddyhome.idea.vim.common.including
import com.maddyhome.idea.vim.common.offset 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.group.MotionGroup
import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.inlayAwareVisualColumn import com.maddyhome.idea.vim.helper.inlayAwareVisualColumn
@@ -62,7 +62,7 @@ fun changeRange(
var col = 0 var col = 0
var lines = 0 var lines = 0
if (type === SelectionType.BLOCK_WISE) { if (type === SelectionType.BLOCK_WISE) {
lines = ChangeGroup.getLinesCountInVisualBlock(IjVimEditor(editor), range) lines = VimChangeGroupBase.getLinesCountInVisualBlock(IjVimEditor(editor), range)
col = editor.offsetToLogicalPosition(range.startOffset).column col = editor.offsetToLogicalPosition(range.startOffset).column
if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) { if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) {
col = VimMotionGroupBase.LAST_COLUMN col = VimMotionGroupBase.LAST_COLUMN

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 { override fun findNextWord(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean): Int {
return SearchHelper.findNextWord( return SearchHelper.findNextWord(
(editor as IjVimEditor).editor, (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 { override fun visitLet1Command(ctx: VimscriptParser.Let1CommandContext): Command {
val ranges: Ranges = parseRanges(ctx.range()) val ranges: Ranges = parseRanges(ctx.range())
val variable: Expression = expressionVisitor.visit(ctx.expr(0)) 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)) val expression: Expression = expressionVisitor.visit(ctx.expr(1))
return LetCommand(ranges, variable, operator, expression, true) 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.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.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.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> </extensions>
</idea-plugin> </idea-plugin>

View File

@@ -88,7 +88,7 @@ class UndoActionTest : VimTestCase() {
val after = """ val after = """
A Discovery 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, all rocks and lavender and tufted grass,
where it was settled on some sodden sand where it was settled on some sodden sand
hard by the torrent of a mountain pass. hard by the torrent of a mountain pass.

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

View File

@@ -33,4 +33,11 @@ interface EngineEditorHelper {
fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition
fun getVisualLineLength(editor: VimEditor, line: Int): Int fun getVisualLineLength(editor: VimEditor, line: Int): Int
fun getLeadingWhitespace(editor: VimEditor, line: Int): String 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 findVimAction(id: String): EditorActionHandlerBase?
fun findVimActionOrDie(id: String): EditorActionHandlerBase
fun getAction(actionId: String): NativeAction? fun getAction(actionId: String): NativeAction?
fun getActionIdList(idPrefix: String): List<String> fun getActionIdList(idPrefix: String): List<String>

View File

@@ -1,6 +1,7 @@
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.KeyHandler 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.Command
import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.CommandState 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_END
import com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS 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.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 com.maddyhome.idea.vim.register.RegisterConstants.LAST_INSERTED_TEXT_REGISTER
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.KeyStroke 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 * Clears all the keystrokes from the current insert command
* *
@@ -871,6 +1002,24 @@ abstract class VimChangeGroupBase : VimChangeGroup {
companion object { companion object {
private const val MAX_REPEAT_CHARS_COUNT = 10000 private const val MAX_REPEAT_CHARS_COUNT = 10000
private val logger = vimLogger<VimChangeGroupBase>() 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, bigWord: Boolean,
): Int ): 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 findNextWord(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean): Int
fun findPattern( 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.command.isLine
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.firstOrNull 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 com.maddyhome.idea.vim.mark.VimMarkConstants.MARK_CHANGE_POS
import java.util.* import java.util.*
import kotlin.math.abs import kotlin.math.abs
@@ -140,7 +141,9 @@ abstract class VimPutBase : VimPut {
} }
"postEndOffset" -> caret.moveToOffset(endOffset + 1) "postEndOffset" -> caret.moveToOffset(endOffset + 1)
"preLineEndOfEndOffset" -> { "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) caret.moveToOffset(pos)
} }
} }

View File

@@ -48,6 +48,11 @@ interface VimRegisterGroup {
isDelete: Boolean, isDelete: Boolean,
): 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 * 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.SMALL_DELETION_REGISTER
import com.maddyhome.idea.vim.register.RegisterConstants.UNNAMED_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.VALID_REGISTERS
import com.maddyhome.idea.vim.register.RegisterConstants.WRITABLE_REGISTERS
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import javax.swing.KeyStroke import javax.swing.KeyStroke
@@ -292,6 +293,24 @@ abstract class VimRegisterGroupBase : VimRegisterGroup {
return true 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 { private fun guessSelectionType(text: String): SelectionType {
return if (text.endsWith("\n")) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE 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.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector 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.ExException
import com.maddyhome.idea.vim.ex.ranges.Ranges import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.options.OptionScope 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.ExecutionResult
import com.maddyhome.idea.vim.vimscript.model.Script import com.maddyhome.idea.vim.vimscript.model.Script
import com.maddyhome.idea.vim.vimscript.model.VimLContext import com.maddyhome.idea.vim.vimscript.model.VimLContext
@@ -56,8 +58,12 @@ data class LetCommand(
val isSyntaxSupported: Boolean, val isSyntaxSupported: Boolean,
) : Command.SingleExecution(ranges) { ) : Command.SingleExecution(ranges) {
companion object {
private val logger = vimLogger<LetCommand>()
}
override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY) override val argFlags = flags(RangeFlag.RANGE_FORBIDDEN, ArgumentFlag.ARGUMENT_OPTIONAL, Access.READ_ONLY)
@Throws(ExException::class) @Throws(ExException::class)
override fun processCommand(editor: VimEditor, context: ExecutionContext): ExecutionResult { override fun processCommand(editor: VimEditor, context: ExecutionContext): ExecutionResult {
if (!isSyntaxSupported) return ExecutionResult.Error if (!isSyntaxSupported) return ExecutionResult.Error
@@ -194,13 +200,19 @@ data class LetCommand(
is EnvVariableExpression -> TODO() is EnvVariableExpression -> TODO()
is Register -> { is Register -> {
if (!(variable.char.isLetter() || variable.char.isDigit() || variable.char == '"')) { if (RegisterConstants.WRITABLE_REGISTERS.contains(variable.char)) {
throw ExException("Let command supports only 0-9a-zA-Z\" registers at the moment") 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") 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)) { if (function.flags.contains(FunctionFlag.RANGE)) {
val line = (injector.variableService.getNonNullVariableValue(Variable(Scope.FUNCTION_VARIABLE, "firstline"), editor, context, function) as VimInt).value 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 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 // non-optional function arguments
for ((index, name) in function.args.withIndex()) { for ((index, name) in function.args.withIndex()) {
injector.variableService.storeVariable( injector.variableService.storeVariable(
Variable(Scope.FUNCTION_VARIABLE, name), Variable(Scope.FUNCTION_VARIABLE, name),
argumentValues[index].evaluate(editor, context, function.vimContext), argumentValues[index].evaluate(editor, context, functionCallContext),
editor, editor,
context, context,
function 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 val expressionToStore = if (index + function.args.size < argumentValues.size) argumentValues[index + function.args.size] else function.defaultArgs[index].second
injector.variableService.storeVariable( injector.variableService.storeVariable(
Variable(Scope.FUNCTION_VARIABLE, function.defaultArgs[index].first), Variable(Scope.FUNCTION_VARIABLE, function.defaultArgs[index].first),
expressionToStore.evaluate(editor, context, function.vimContext), expressionToStore.evaluate(editor, context, functionCallContext),
editor, editor,
context, context,
function function
@@ -158,7 +158,7 @@ data class DefinedFunctionHandler(val function: FunctionDeclaration) : FunctionH
val remainingArgs = if (function.args.size + function.defaultArgs.size < argumentValues.size) { val remainingArgs = if (function.args.size + function.defaultArgs.size < argumentValues.size) {
VimList( VimList(
argumentValues.subList(function.args.size + function.defaultArgs.size, argumentValues.size) 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 { } else {
VimList(mutableListOf()) VimList(mutableListOf())