From 2bea88ea387714deb809d446d31befd2deb7fb9c Mon Sep 17 00:00:00 2001 From: Filipp Vakhitov Date: Mon, 3 Jun 2024 19:16:17 +0300 Subject: [PATCH 01/10] Remove SearchGroup from GlobalCommand --- .../vimscript/model/commands/GlobalCommand.kt | 181 ++++-------------- .../idea/vim/api/VimSearchGroupBase.kt | 42 ++++ .../maddyhome/idea/vim/regexp/CharPointer.kt | 2 +- 3 files changed, 83 insertions(+), 142 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt index e52768e7d3..153f4b6139 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt +++ b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt @@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.vimscript.model.commands import com.intellij.openapi.editor.RangeMarker import com.intellij.vim.annotations.ExCommand -import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimSearchGroupBase @@ -19,17 +18,7 @@ import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.ex.ranges.LineRange import com.maddyhome.idea.vim.ex.ranges.Range -import com.maddyhome.idea.vim.group.SearchGroup -import com.maddyhome.idea.vim.group.SearchGroup.RE_BOTH -import com.maddyhome.idea.vim.group.SearchGroup.RE_LAST -import com.maddyhome.idea.vim.group.SearchGroup.RE_SEARCH -import com.maddyhome.idea.vim.group.SearchGroup.RE_SUBST -import com.maddyhome.idea.vim.helper.MessageHelper.message -import com.maddyhome.idea.vim.helper.Msg -import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.ij -import com.maddyhome.idea.vim.regexp.CharPointer -import com.maddyhome.idea.vim.regexp.RegExp import com.maddyhome.idea.vim.regexp.VimRegexException import com.maddyhome.idea.vim.regexp.match.VimMatchResult import com.maddyhome.idea.vim.vimscript.model.ExecutionResult @@ -37,6 +26,7 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult /** * see "h :global" / "h :vglobal" */ +// FIXME: I'm such a mess, please refactor me, responsible developer @ExCommand(command = "g[lobal],v[global]") internal data class GlobalCommand(val range: Range, val argument: String, val invert: Boolean) : Command.SingleExecution(range, argument) { @@ -63,151 +53,60 @@ internal data class GlobalCommand(val range: Range, val argument: String, val in context: ExecutionContext, range: LineRange, ): Boolean { + val messages = injector.messages // When nesting the command works on one line. This allows for // ":g/found/v/notfound/command". if (globalBusy && (range.startLine != 0 || range.endLine != editor.lineCount() - 1)) { - VimPlugin.showMessage(message("E147")) - VimPlugin.indicateError() + messages.showStatusBarMessage(null, messages.message("E147")) + messages.indicateError() return false } - var cmd = CharPointer(StringBuffer(argument)) - val pat: CharPointer - val delimiter: Char - var whichPat = RE_LAST + val search = injector.searchGroup as VimSearchGroupBase + val globalCommandArguments = search.parseGlobalCommand(argument) ?: return false - /* - * undocumented vi feature: - * "\/" and "\?": use previous search pattern. - * "\&": use previous substitute pattern. - */ - if (argument.isEmpty()) { - VimPlugin.showMessage(message("E148")) - VimPlugin.indicateError() + val regex = try { + search.prepareRegex(globalCommandArguments.pattern, globalCommandArguments.whichPattern, 2) + } catch (e: VimRegexException) { + messages.showStatusBarMessage(editor, e.message) return false - } else if (cmd.charAt() == '\\') { - cmd.inc() - if ("/?&".indexOf(cmd.charAt()) == -1) { - VimPlugin.showMessage(message(Msg.e_backslash)) - return false - } - whichPat = if (cmd.charAt() == '&') RE_SUBST else RE_SEARCH - cmd.inc() - pat = CharPointer("") /* empty search pattern */ - } else { - delimiter = cmd.charAt() /* get the delimiter */ - cmd.inc() - pat = cmd.ref(0) /* remember start of pattern */ - cmd = RegExp.skip_regexp(cmd, delimiter, true) - if (cmd.charAt() == delimiter) { /* end delimiter found */ - cmd.set('\u0000').inc() /* replace it with a NUL */ - } } - if (injector.globalIjOptions().useNewRegex) { - val regex = try { - (injector.searchGroup as VimSearchGroupBase).prepareRegex(pat, whichPat, RE_BOTH) - } catch (e: VimRegexException) { - injector.messages.showStatusBarMessage(editor, e.message) - return false - } - - if (globalBusy) { - val match = regex.findInLine(editor, editor.currentCaret().getLine()) - if (match is VimMatchResult.Success == !invert) { - globalExecuteOne(editor, context, editor.getLineStartOffset(editor.currentCaret().getLine()), cmd.toString()) - } - } else { - val line1 = range.startLine - val line2 = range.endLine - if (line1 < 0 || line2 < 0) { - return false - } - val matches = regex.findAll( - editor, - editor.getLineStartOffset(line1), - editor.getLineEndOffset(line2), - ) - val marks = if (!invert) matches.map { - editor.ij.document.createRangeMarker(editor.getLineStartForOffset(it.range.startOffset), editor.getLineStartForOffset(it.range.startOffset)) - // filter out lines that contain a match - } else (line1..line2).filterNot { line -> - matches.map { match -> - editor.offsetToBufferPosition(match.range.startOffset).line - }.contains(line) - }.map { editor.ij.document.createRangeMarker(editor.getLineStartOffset(it), editor.getLineStartOffset(it)) } - - if (gotInt) { - VimPlugin.showMessage(message("e_interr")) - } else if (marks.isEmpty()) { - if (invert) { - VimPlugin.showMessage(message("global.command.not.found.v", pat.toString())) - } else { - VimPlugin.showMessage(message("global.command.not.found.g", pat.toString())) - } - } else { - globalExe(editor, context, marks, cmd.toString()) - } + if (globalBusy) { + val match = regex.findInLine(editor, editor.currentCaret().getLine()) + if (match is VimMatchResult.Success == !invert) { + globalExecuteOne(editor, context, editor.getLineStartOffset(editor.currentCaret().getLine()), globalCommandArguments.command) } } else { - val (first, second) = (injector.searchGroup as SearchGroup).search_regcomp(pat, whichPat, RE_BOTH) - if (!first) { - VimPlugin.showMessage(message(Msg.e_invcmd)) - VimPlugin.indicateError() + val line1 = range.startLine + val line2 = range.endLine + if (line1 < 0 || line2 < 0) { return false } - val regmatch = second.first as RegExp.regmmatch_T - val sp = second.third as RegExp - - var match: Int - val lcount = editor.lineCount() - val searchcol = 0 - if (globalBusy) { - val offset = editor.currentCaret().offset - val lineStartOffset = editor.getLineStartForOffset(offset) - match = sp.vim_regexec_multi(regmatch, editor, lcount, editor.currentCaret().getLine(), searchcol) - if ((!invert && match > 0) || (invert && match <= 0)) { - globalExecuteOne(editor, context, lineStartOffset, cmd.toString()) - } - } else { - // pass 1: set marks for each (not) matching line - val line1 = range.startLine - val line2 = range.endLine - //region search_regcomp implementation - // We don't need to worry about lastIgnoreSmartCase, it's always false. Vim resets after checking, and it only sets - // it to true when searching for a word with `*`, `#`, `g*`, etc. - - if (line1 < 0 || line2 < 0) { - return false - } - - var ndone = 0 - val marks = mutableListOf() - for (lnum in line1..line2) { - if (gotInt) break - - // a match on this line? - match = sp.vim_regexec_multi(regmatch, editor, lcount, lnum, searchcol) - if ((!invert && match > 0) || (invert && match <= 0)) { - val lineStartOffset = editor.getLineStartOffset(lnum) - marks += editor.ij.document.createRangeMarker(lineStartOffset, lineStartOffset) - ndone += 1 - } - // TODO: 25.05.2021 Check break - } - - // pass 2: execute the command for each line that has been marked - if (gotInt) { - VimPlugin.showMessage(message("e_interr")) - } else if (ndone == 0) { - if (invert) { - VimPlugin.showMessage(message("global.command.not.found.v", pat.toString())) - } else { - VimPlugin.showMessage(message("global.command.not.found.g", pat.toString())) - } + val matches = regex.findAll( + editor, + editor.getLineStartOffset(line1), + editor.getLineEndOffset(line2), + ) + val marks = if (!invert) matches.map { + editor.ij.document.createRangeMarker(editor.getLineStartForOffset(it.range.startOffset), editor.getLineStartForOffset(it.range.startOffset)) + // filter out lines that contain a match + } else (line1..line2).filterNot { line -> + matches.map { match -> + editor.offsetToBufferPosition(match.range.startOffset).line + }.contains(line) + }.map { editor.ij.document.createRangeMarker(editor.getLineStartOffset(it), editor.getLineStartOffset(it)) } + + if (gotInt) { + messages.showStatusBarMessage(null, messages.message("e_interr")) + } else if (marks.isEmpty()) { + if (invert) { + messages.showStatusBarMessage(null, messages.message("global.command.not.found.v", globalCommandArguments.pattern.toString())) } else { - globalExe(editor, context, marks, cmd.toString()) + messages.showStatusBarMessage(null, messages.message("global.command.not.found.g", globalCommandArguments.pattern.toString())) } + } else { + globalExe(editor, context, marks, globalCommandArguments.command) } } return true @@ -235,7 +134,7 @@ internal data class GlobalCommand(val range: Range, val argument: String, val in private fun globalExecuteOne(editor: VimEditor, context: ExecutionContext, lineStartOffset: Int, cmd: String?) { // TODO: 26.05.2021 What about folds? editor.currentCaret().moveToOffset(lineStartOffset) - if (cmd == null || cmd.isEmpty() || (cmd.length == 1 && cmd[0] == '\n')) { + if (cmd.isNullOrEmpty() || (cmd.length == 1 && cmd[0] == '\n')) { injector.vimscriptExecutor.execute("p", editor, context, skipHistory = true, indicateErrors = true, this.vimContext) } else { injector.vimscriptExecutor.execute(cmd, editor, context, skipHistory = true, indicateErrors = true, this.vimContext) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt index eb6b08217d..b9feb63690 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt @@ -261,6 +261,48 @@ public abstract class VimSearchGroupBase : VimSearchGroup { return VimRegex(pat.toString()) } + // TODO I think that this method (and the method above) should be part of the global command + public fun parseGlobalCommand(argument: String): GlobalCommandArguments? { + var cmd = CharPointer(StringBuffer(argument)) + + val pat: CharPointer + val delimiter: Char + var whichPat = 2 // RE_LAST + + val messages = injector.messages + /* + * undocumented vi feature: + * "\/" and "\?": use previous search pattern. + * "\&": use previous substitute pattern. + */ + if (argument.isEmpty()) { + messages.showStatusBarMessage(null, messages.message("E148")) + messages.indicateError() + return null + } else if (cmd.charAt() == '\\') { + cmd.inc() + if ("/?&".indexOf(cmd.charAt()) == -1) { + messages.showStatusBarMessage(null, messages.message(Msg.e_backslash)) + return null + } + whichPat = if (cmd.charAt() == '&') 1 /* RE_SUBST */ else 0 /* RE_SEARCH */ + cmd.inc() + pat = CharPointer("") /* empty search pattern */ + } else { + delimiter = cmd.charAt() /* get the delimiter */ + cmd.inc() + pat = cmd.ref(0) /* remember start of pattern */ + val endOfPattern = findEndOfPattern(cmd.toString(), delimiter) + if (cmd.charAt(endOfPattern) == delimiter) { + cmd.set('\u0000', endOfPattern) + } + cmd.pointer = endOfPattern + 2 + } + return GlobalCommandArguments(pat, whichPat, cmd.toString()) + } + + public data class GlobalCommandArguments(val pattern: CharPointer, val whichPattern: Int, val command: String) + /****************************************************************************/ /* Search related methods */ /****************************************************************************/ diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/CharPointer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/CharPointer.kt index a10de98513..0faac1d297 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/CharPointer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/CharPointer.kt @@ -13,7 +13,7 @@ import java.util.* @Deprecated("Remove once old regex engine is removed") public class CharPointer { private var seq: CharSequence - private var pointer = 0 + public var pointer: Int = 0 private var readonly: Boolean public constructor(text: String) { From 477e4b6d489d849c065e4f8491d45e847841b808 Mon Sep 17 00:00:00 2001 From: Filipp Vakhitov Date: Mon, 3 Jun 2024 19:47:58 +0300 Subject: [PATCH 02/10] Move DocumentSearchListener to IjVimSearchGroup --- .../maddyhome/idea/vim/group/SearchGroup.java | 65 ------------------- .../idea/vim/listener/VimListenerManager.kt | 5 +- .../idea/vim/newapi/IjVimSearchGroup.kt | 60 +++++++++++++++++ .../idea/vim/api/VimSearchGroupBase.kt | 2 +- 4 files changed, 64 insertions(+), 68 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java b/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java index 2d8c0940bd..f5881c9e44 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java +++ b/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java @@ -1196,71 +1196,6 @@ public Direction getLastSearchDirection() { return lastDir; } - /** - * Removes and adds highlights for current search pattern when the document is edited - */ - public static class DocumentSearchListener implements DocumentListener { - - public static DocumentSearchListener INSTANCE = new DocumentSearchListener(); - - @Contract(pure = true) - private DocumentSearchListener() { - } - - @Override - public void documentChanged(@NotNull DocumentEvent event) { - // Loop over all local editors for the changed document, across all projects, and update search highlights. - // Note that the change may have come from a remote guest in Code With Me scenarios (in which case - // ClientId.current will be a guest ID), but we don't care - we still need to add/remove highlights for the - // changed text. Make sure we only update local editors, though. - final Document document = event.getDocument(); - for (VimEditor vimEditor : injector.getEditorGroup().getEditors(new IjVimDocument(document))) { - final Editor editor = ((IjVimEditor)vimEditor).getEditor(); - Collection existingHighlighters = UserDataManager.getVimLastHighlighters(editor); - if (existingHighlighters == null) { - continue; - } - - if (logger.isDebugEnabled()) { - logger.debug("hls=" + existingHighlighters); - logger.debug("event=" + event); - } - - // We can only re-highlight whole lines, so clear any highlights in the affected lines. - // If we're deleting lines, this will clear + re-highlight the new current line, which hasn't been modified. - // However, we still want to re-highlight this line in case any highlights cross the line boundaries. - // If we're adding lines, this will clear + re-highlight all new lines. - final LogicalPosition startPosition = editor.offsetToLogicalPosition(event.getOffset()); - final LogicalPosition endPosition = editor.offsetToLogicalPosition(event.getOffset() + event.getNewLength()); - final int startLineOffset = document.getLineStartOffset(startPosition.line); - final int endLineOffset = document.getLineEndOffset(endPosition.line); - - // Remove any highlights that have already been deleted, and remove + clear those that intersect with the change - final Iterator iter = existingHighlighters.iterator(); - while (iter.hasNext()) { - final RangeHighlighter highlighter = iter.next(); - if (!highlighter.isValid()) { - iter.remove(); - } - else if (highlighter.getTextRange().intersects(startLineOffset, endLineOffset)) { - iter.remove(); - editor.getMarkupModel().removeHighlighter(highlighter); - } - } - - VimPlugin.getSearch().highlightSearchLines(editor, startPosition.line, endPosition.line); - - if (logger.isDebugEnabled()) { - existingHighlighters = UserDataManager.getVimLastHighlighters(editor); - logger.debug("sl=" + startPosition.line + ", el=" + endPosition.line); - logger.debug("hls=" + existingHighlighters); - } - } - } - } - //endregion - - // ******************************************************************************************************************* // // Implementation details diff --git a/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt b/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt index fd8a8f3852..e19d8ec693 100644 --- a/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt +++ b/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt @@ -94,6 +94,7 @@ import com.maddyhome.idea.vim.helper.vimDisabled import com.maddyhome.idea.vim.helper.vimInitialised import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.InsertTimeRecorder +import com.maddyhome.idea.vim.newapi.IjVimSearchGroup import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.state.mode.inSelectMode @@ -351,13 +352,13 @@ internal object VimListenerManager { private object VimDocumentListener : DocumentListener { override fun beforeDocumentChange(event: DocumentEvent) { VimMarkServiceImpl.MarkUpdater.beforeDocumentChange(event) - SearchGroup.DocumentSearchListener.INSTANCE.beforeDocumentChange(event) + IjVimSearchGroup.DocumentSearchListener.INSTANCE.beforeDocumentChange(event) IjVimRedrawService.RedrawListener.beforeDocumentChange(event) } override fun documentChanged(event: DocumentEvent) { VimMarkServiceImpl.MarkUpdater.documentChanged(event) - SearchGroup.DocumentSearchListener.INSTANCE.documentChanged(event) + IjVimSearchGroup.DocumentSearchListener.INSTANCE.documentChanged(event) IjVimRedrawService.RedrawListener.documentChanged(event) } } diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt index 04deb37558..ed93599a67 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt @@ -14,6 +14,8 @@ import com.intellij.openapi.components.RoamingType import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.util.Ref import com.maddyhome.idea.vim.VimPlugin @@ -34,10 +36,12 @@ import com.maddyhome.idea.vim.helper.highlightSearchResults import com.maddyhome.idea.vim.helper.isCloseKeyStroke import com.maddyhome.idea.vim.helper.shouldIgnoreCase import com.maddyhome.idea.vim.helper.updateSearchHighlights +import com.maddyhome.idea.vim.helper.vimLastHighlighters import com.maddyhome.idea.vim.options.GlobalOptionChangeListener import com.maddyhome.idea.vim.ui.ModalEntry import com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler import org.jdom.Element +import org.jetbrains.annotations.Contract import org.jetbrains.annotations.TestOnly import javax.swing.KeyStroke @@ -277,4 +281,60 @@ public open class IjVimSearchGroup : VimSearchGroupBase(), PersistentStateCompon editor.markupModel.removeHighlighter(highlighter) } } + + + /** + * Removes and adds highlights for current search pattern when the document is edited + */ + public class DocumentSearchListener @Contract(pure = true) private constructor() : DocumentListener { + public override fun documentChanged(event: DocumentEvent) { + // Loop over all local editors for the changed document, across all projects, and update search highlights. + // Note that the change may have come from a remote guest in Code With Me scenarios (in which case + // ClientId.current will be a guest ID), but we don't care - we still need to add/remove highlights for the + // changed text. Make sure we only update local editors, though. + val document = event.document + for (vimEditor in injector.editorGroup.getEditors(IjVimDocument(document))) { + val editor = (vimEditor as IjVimEditor).editor + var existingHighlighters = editor.vimLastHighlighters ?: continue + + if (logger.isDebug()) { + logger.debug("hls=$existingHighlighters") + logger.debug("event=$event") + } + + // We can only re-highlight whole lines, so clear any highlights in the affected lines. + // If we're deleting lines, this will clear + re-highlight the new current line, which hasn't been modified. + // However, we still want to re-highlight this line in case any highlights cross the line boundaries. + // If we're adding lines, this will clear + re-highlight all new lines. + val startPosition = editor.offsetToLogicalPosition(event.offset) + val endPosition = editor.offsetToLogicalPosition(event.offset + event.newLength) + val startLineOffset = document.getLineStartOffset(startPosition.line) + val endLineOffset = document.getLineEndOffset(endPosition.line) + + // Remove any highlights that have already been deleted, and remove + clear those that intersect with the change + val iter = existingHighlighters.iterator() + while (iter.hasNext()) { + val highlighter = iter.next() + if (!highlighter.isValid) { + iter.remove() + } else if (highlighter.textRange.intersects(startLineOffset, endLineOffset)) { + iter.remove() + editor.markupModel.removeHighlighter(highlighter) + } + } + + (injector.searchGroup as VimSearchGroupBase).highlightSearchLines(editor.vim, startPosition.line, endPosition.line) + + if (logger.isDebug()) { + existingHighlighters = editor.vimLastHighlighters!! + logger.debug("sl=" + startPosition.line + ", el=" + endPosition.line) + logger.debug("hls=$existingHighlighters") + } + } + } + + public companion object { + public var INSTANCE: DocumentSearchListener = DocumentSearchListener() + } + } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt index b9feb63690..13c2c044ec 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt @@ -94,7 +94,7 @@ public abstract class VimSearchGroupBase : VimSearchGroup { * @param startLine The number of the line where to start highlighting (inclusive) * @param endLine The number of the line where to stop highlighting (inclusive) */ - protected abstract fun highlightSearchLines( + public abstract fun highlightSearchLines( editor: VimEditor, startLine: Int, endLine: Int, From 06de3168200adde7afa047c6d0d9f46c854f3071 Mon Sep 17 00:00:00 2001 From: Filipp Vakhitov Date: Mon, 3 Jun 2024 20:08:17 +0300 Subject: [PATCH 03/10] Remove deprecated SearchGroup.java --- .../com/maddyhome/idea/vim/VimPlugin.java | 11 +- .../idea/vim/group/IjOptionProperties.kt | 1 - .../com/maddyhome/idea/vim/group/IjOptions.kt | 1 - .../maddyhome/idea/vim/group/SearchGroup.java | 1376 ----------------- .../idea/vim/helper/SearchHelper.java | 72 +- .../idea/vim/listener/VimListenerManager.kt | 1 - .../idea/vim/newapi/IjVimInjector.kt | 3 +- .../idea/vim/newapi/IjVimSearchGroup.kt | 19 + .../idea/vim/newapi/IjVimSearchHelper.kt | 22 - .../META-INF/includes/ApplicationServices.xml | 2 +- .../action/motion/gn/GnNextTextObjectTest.kt | 2 +- .../motion/gn/GnPreviousTextObjectTest.kt | 2 +- .../motion/gn/VisualSelectNextSearchTest.kt | 2 +- .../gn/VisualSelectPreviousSearchTest.kt | 2 +- .../search/SearchAgainPreviousActionTest.kt | 2 +- .../commands/SubstituteCommandTest.kt | 6 - .../plugins/ideavim/TestOptionConstants.kt | 3 - .../idea/vim/api/VimSearchGroupBase.kt | 2 +- 18 files changed, 47 insertions(+), 1482 deletions(-) delete mode 100644 src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java diff --git a/src/main/java/com/maddyhome/idea/vim/VimPlugin.java b/src/main/java/com/maddyhome/idea/vim/VimPlugin.java index 101803c0aa..df36af7dd0 100644 --- a/src/main/java/com/maddyhome/idea/vim/VimPlugin.java +++ b/src/main/java/com/maddyhome/idea/vim/VimPlugin.java @@ -37,6 +37,7 @@ import com.maddyhome.idea.vim.helper.MacKeyRepeat; import com.maddyhome.idea.vim.listener.VimListenerManager; import com.maddyhome.idea.vim.newapi.IjVimInjector; +import com.maddyhome.idea.vim.newapi.IjVimSearchGroup; import com.maddyhome.idea.vim.ui.StatusBarIconFactory; import com.maddyhome.idea.vim.vimscript.services.VariableService; import com.maddyhome.idea.vim.yank.YankGroupBase; @@ -123,12 +124,12 @@ public class VimPlugin implements PersistentStateComponent, Disposable return (FileGroup)VimInjectorKt.getInjector().getFile(); } - public static @NotNull SearchGroup getSearch() { - return ApplicationManager.getApplication().getService(SearchGroup.class); + public static @NotNull IjVimSearchGroup getSearch() { + return ApplicationManager.getApplication().getService(IjVimSearchGroup.class); } - public static @Nullable SearchGroup getSearchIfCreated() { - return ApplicationManager.getApplication().getServiceIfCreated(SearchGroup.class); + public static @Nullable IjVimSearchGroup getSearchIfCreated() { + return ApplicationManager.getApplication().getServiceIfCreated(IjVimSearchGroup.class); } public static @NotNull ProcessGroup getProcess() { @@ -345,7 +346,7 @@ private void turnOnPlugin() { } private void turnOffPlugin(boolean unsubscribe) { - SearchGroup searchGroup = getSearchIfCreated(); + IjVimSearchGroup searchGroup = getSearchIfCreated(); if (searchGroup != null) { searchGroup.turnOff(); } diff --git a/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt b/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt index 9ed6a5a0ba..75fe2aa952 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/IjOptionProperties.kt @@ -34,7 +34,6 @@ public open class GlobalIjOptions(scope: OptionAccessScope) : OptionsPropertiesB public var commandOrMotionAnnotation: Boolean by optionProperty(IjOptions.commandOrMotionAnnotation) public var oldundo: Boolean by optionProperty(IjOptions.oldundo) public var unifyjumps: Boolean by optionProperty(IjOptions.unifyjumps) - public var useNewRegex: Boolean by optionProperty(IjOptions.useNewRegex) public var vimscriptFunctionAnnotation: Boolean by optionProperty(IjOptions.vimscriptFunctionAnnotation) } diff --git a/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt b/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt index e48699f87d..51b7839e00 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/IjOptions.kt @@ -141,7 +141,6 @@ public object IjOptions { public val commandOrMotionAnnotation: ToggleOption = addOption(ToggleOption("commandormotionannotation", GLOBAL, "commandormotionannotation", true, isHidden = true)) public val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true)) public val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true)) - public val useNewRegex: ToggleOption = addOption(ToggleOption("usenewregex", GLOBAL, "usenewregex", true, isHidden = true)) public val vimscriptFunctionAnnotation: ToggleOption = addOption(ToggleOption("vimscriptfunctionannotation", GLOBAL, "vimscriptfunctionannotation", true, isHidden = true)) // This needs to be Option so that it can work with derived option types, such as NumberOption, which diff --git a/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java b/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java deleted file mode 100644 index f5881c9e44..0000000000 --- a/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java +++ /dev/null @@ -1,1376 +0,0 @@ -/* - * Copyright 2003-2023 The IdeaVim authors - * - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE.txt file or at - * https://opensource.org/licenses/MIT. - */ -package com.maddyhome.idea.vim.group; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Caret; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.LogicalPosition; -import com.intellij.openapi.editor.event.DocumentEvent; -import com.intellij.openapi.editor.event.DocumentListener; -import com.intellij.openapi.editor.markup.RangeHighlighter; -import com.intellij.openapi.fileEditor.FileEditorManagerEvent; -import com.intellij.openapi.util.Ref; -import com.maddyhome.idea.vim.VimPlugin; -import com.maddyhome.idea.vim.api.*; -import com.maddyhome.idea.vim.command.MotionType; -import com.maddyhome.idea.vim.common.CharacterPosition; -import com.maddyhome.idea.vim.common.Direction; -import com.maddyhome.idea.vim.common.TextRange; -import com.maddyhome.idea.vim.ex.ExException; -import com.maddyhome.idea.vim.ex.ranges.LineRange; -import com.maddyhome.idea.vim.helper.*; -import com.maddyhome.idea.vim.history.HistoryConstants; -import com.maddyhome.idea.vim.newapi.*; -import com.maddyhome.idea.vim.options.GlobalOptionChangeListener; -import com.maddyhome.idea.vim.regexp.CharPointer; -import com.maddyhome.idea.vim.regexp.CharacterClasses; -import com.maddyhome.idea.vim.regexp.RegExp; -import com.maddyhome.idea.vim.ui.ModalEntry; -import com.maddyhome.idea.vim.vimscript.model.VimLContext; -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.expressions.SimpleExpression; -import com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler; -import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser; -import kotlin.Pair; -import kotlin.Triple; -import kotlin.jvm.functions.Function1; -import org.jdom.Element; -import org.jetbrains.annotations.*; - -import javax.swing.*; -import java.text.NumberFormat; -import java.text.ParsePosition; -import java.util.*; - -import static com.maddyhome.idea.vim.api.VimInjectorKt.*; -import static com.maddyhome.idea.vim.helper.SearchHelperKtKt.shouldIgnoreCase; -import static com.maddyhome.idea.vim.newapi.IjVimInjectorKt.globalIjOptions; -import static com.maddyhome.idea.vim.register.RegisterConstants.LAST_SEARCH_REGISTER; - -/** - * @deprecated Replace with IjVimSearchGroup - */ -@Deprecated -public class SearchGroup extends IjVimSearchGroup { - public SearchGroup() { - super(); - if (!globalIjOptions(injector).getUseNewRegex()) { - // We use the global option listener instead of the effective listener that gets called for each affected editor - // because we handle updating the affected editors ourselves (e.g., we can filter for visible windows). - VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.hlsearch, () -> { - resetShowSearchHighlight(); - forceUpdateSearchHighlights(); - }); - - final GlobalOptionChangeListener updateHighlightsIfVisible = () -> { - if (showSearchHighlight) { - forceUpdateSearchHighlights(); - } - }; - VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.ignorecase, updateHighlightsIfVisible); - VimPlugin.getOptionGroup().addGlobalOptionChangeListener(Options.smartcase, updateHighlightsIfVisible); - } - } - - public void turnOn() { - if (globalIjOptions(injector).getUseNewRegex()) { - super.updateSearchHighlights(false); - return; - } - updateSearchHighlights(); - } - - public void turnOff() { - final boolean show = showSearchHighlight; - clearSearchHighlight(); - showSearchHighlight = show; - } - - @TestOnly - @Override - public void resetState() { - if (globalIjOptions(injector).getUseNewRegex()) { - super.resetState(); - return; - } - lastPatternIdx = RE_SEARCH; - lastSearch = lastSubstitute = lastReplace = null; - lastPatternOffset = ""; - lastIgnoreSmartCase = false; - lastDir = Direction.FORWARDS; - resetShowSearchHighlight(); - } - - /** - * Get the last pattern used for searching. Does not include pattern used in substitution - * - * @return The pattern used for last search. Can be null - */ - @Override - public @Nullable String getLastSearchPattern() { - if (globalIjOptions(injector).getUseNewRegex()) return super.getLastSearchPattern(); - return lastSearch; - } - - /** - * Get the last pattern used in substitution. - * @return The pattern used for the last substitute command. Can be null - */ - @Override - public @Nullable String getLastSubstitutePattern() { - if (globalIjOptions(injector).getUseNewRegex()) return super.getLastSubstitutePattern(); - return lastSubstitute; - } - - /** - * Get the pattern last used for either searching or substitution. - * - * @return The pattern last used for either searching or substitution. Can be null - */ - @Override - protected @Nullable String getLastUsedPattern() { - if (globalIjOptions(injector).getUseNewRegex()) return super.getLastUsedPattern(); - return switch (lastPatternIdx) { - case RE_SEARCH -> lastSearch; - case RE_SUBST -> lastSubstitute; - default -> null; - }; - } - - /** - * Get the last used search direction - * - *

This method is used in the AceJump integration plugin

- * - * @return Returns the integer value of Direction. 1 for FORWARDS, -1 for BACKWARDS - */ - @SuppressWarnings("unused") - public int getLastDir() { - return lastDir.toInt(); - } - - /** - * Set the last used pattern - * - *

Only updates the last used flag if the pattern is new. This prevents incorrectly setting the last used pattern - * when search or substitute doesn't explicitly set the pattern but uses the last saved value. It also ensures the - * last used pattern is updated when a new pattern with the same value is used.

- * - *

Also saves the text to the search register and history.

- * - * @param pattern The pattern to remember - * @param which_pat Which pattern to save - RE_SEARCH, RE_SUBST or RE_BOTH - * @param isNewPattern Flag to indicate if the pattern is new, or comes from a last used pattern. True means to - * update the last used pattern index - */ - private void setLastUsedPattern(@NotNull String pattern, int which_pat, boolean isNewPattern) { - // Only update the last pattern with a new input pattern. Do not update if we're reusing the last pattern - if ((which_pat == RE_SEARCH || which_pat == RE_BOTH) && isNewPattern) { - lastSearch = pattern; - lastPatternIdx = RE_SEARCH; - } - if ((which_pat == RE_SUBST || which_pat == RE_BOTH) && isNewPattern) { - lastSubstitute = pattern; - lastPatternIdx = RE_SUBST; - } - - // Vim never actually sets this register, but looks it up on request - VimPlugin.getRegister().storeTextSpecial(LAST_SEARCH_REGISTER, pattern); - - // This will remove an existing entry and add it back to the end, and is expected to do so even if the string value - // is the same - VimPlugin.getHistory().addEntry(HistoryConstants.SEARCH, pattern); - } - - /** - * Sets the last search state, purely for tests - * - * @param editor The editor to update - * @param pattern The pattern to save. This is the last search pattern, not the last substitute pattern - * @param patternOffset The pattern offset, e.g. `/{pattern}/{offset}` - * @param direction The direction to search - */ - @TestOnly - public void setLastSearchState(@SuppressWarnings("unused") @NotNull Editor editor, @NotNull String pattern, - @NotNull String patternOffset, Direction direction) { - if (globalIjOptions(injector).getUseNewRegex()) { - super.setLastSearchState(pattern, patternOffset, direction); - return; - } - setLastUsedPattern(pattern, RE_SEARCH, true); - lastIgnoreSmartCase = false; - lastPatternOffset = patternOffset; - lastDir = direction; - } - - - // ******************************************************************************************************************* - // - // Search - // - // ******************************************************************************************************************* - - /** - * Find all occurrences of the pattern - * - * @deprecated Use SearchHelper#findAll instead. Kept for compatibility with existing plugins - * - * @param editor The editor to search in - * @param pattern The pattern to search for - * @param startLine The start line of the range to search for - * @param endLine The end line of the range to search for, or -1 for the whole document - * @param ignoreCase Case sensitive or insensitive searching - * @return A list of TextRange objects representing the results - */ - @Deprecated() - public static @NotNull List findAll(@NotNull Editor editor, - @NotNull String pattern, - int startLine, - int endLine, - boolean ignoreCase) { - return injector.getSearchHelper().findAll(new IjVimEditor(editor), pattern, startLine, endLine, ignoreCase); - } - - /** - * Process the search command, searching for the pattern from the given document offset - * - *

Parses the pattern from the search command and will search for the given pattern, immediately setting RE_SEARCH - * and RE_LAST. Updates the search register and history and search highlights. Also updates last pattern offset and - * direction. scanwrap and ignorecase come from options. Will ensure that RE_LAST is valid if the given pattern is - * empty by using the existing RE_SEARCH or falling back to RE_SUBST. Will error if both are unset.

- * - *

Will parse the entire command, including patterns separated by `;`

- * - *

Note that this method should only be called when the ex command argument should be parsed, and start should be - * updated. I.e. only for the search commands. Consider using SearchHelper.findPattern to find text.

- * - *

Equivalent to normal.c:nv_search + search.c:do_search

- * - * @param editor The editor to search in - * @param startOffset The offset to start searching from - * @param command The command text entered into the Ex entry panel. Does not include the leading `/` or `?`. - * Can include a trailing offset, e.g. /{pattern}/{offset}, or multiple commands separated by a semicolon. - * If the pattern is empty, the last used (search? substitute?) pattern (and offset?) is used. - * @param dir The direction to search - * @return Pair containing the offset to the next occurrence of the pattern, and the [MotionType] based on - * the search offset. The value will be `null` if no result is found. - */ - @Nullable - @Override - public Pair processSearchCommand(@NotNull VimEditor editor, - @NotNull String command, - int startOffset, - int count1, - @NotNull Direction dir) { - - if (globalIjOptions(injector).getUseNewRegex()) return super.processSearchCommand(editor, command, startOffset, count1, dir); - - boolean isNewPattern = false; - String pattern = null; - String patternOffset = null; - - final char type = dir == Direction.FORWARDS ? '/' : '?'; - - if (!command.isEmpty()) { - if (command.charAt(0) != type) { - CharPointer p = new CharPointer(command); - CharPointer end = RegExp.skip_regexp(p.ref(0), type, true); - - pattern = p.substring(end.pointer() - p.pointer()); - isNewPattern = true; - - if (logger.isDebugEnabled()) logger.debug("pattern=" + pattern); - - if (p.charAt() == type) { - p.inc(); - patternOffset = p.toString(); - if (logger.isDebugEnabled()) logger.debug("offset=" + patternOffset); - } - if (end.charAt(0) == type) { - end.inc(); - patternOffset = end.toString(); - if (logger.isDebugEnabled()) logger.debug("Pattern contains offset " + patternOffset); - } - else { - logger.debug("no offset"); - patternOffset = ""; - } - } - else if (command.length() == 1) { - patternOffset = ""; - } - else { - patternOffset = command.substring(1); - if (logger.isDebugEnabled()) logger.debug("offset=" + patternOffset); - } - } - - // Vim's logic is spread over several methods (do_search -> searchit -> search_regcomp), and rather tricky to follow - // When searching, it will search for the given pattern or RE_LAST. Pattern offset always come from RE_SEARCH. - // If the pattern is explicitly entered, this is saved as RE_SEARCH and this becomes RE_LAST. - // Pattern offset is also parsed, and is saved (to RE_SEARCH) - // If the pattern is missing, Vim checks RE_SEARCH: - // If RE_SEARCH is set, the given pattern is set to an empty string, meaning search will use RE_LAST. - // If RE_LAST is unset, then error e_noprevre (searchit -> search_regcomp) - // BUT: RE_LAST is *always* set. The default is RE_SEARCH, which we know is valid. If it's RE_SUBST, it's been - // explicitly set and is valid. - // Pattern offset always comes from RE_SEARCH. - // If RE_SEARCH is unset, fall back to RE_SUBST: - // If RE_SUBST is set, save this as RE_SEARCH, which becomes RE_LAST - // RE_SUBST does not have pattern offsets to save, so pattern offset will be RE_SEARCH - unset/default - // If RE_SUBST is unset, error e_noprevre - // Pattern offset is always used from RE_SEARCH. Only saved when explicitly entered - // Direction is saved in do_search - // IgnoreSmartCase is only ever set for searching words (`*`, `#`, `g*`, etc.) and is reset for all other operations - - if (pattern == null || pattern.isEmpty()) { - pattern = getLastSearchPattern(); - if (pattern == null || pattern.isEmpty()) { - isNewPattern = true; - pattern = getLastSubstitutePattern(); - if (pattern == null || pattern.isEmpty()) { - VimPlugin.showMessage(MessageHelper.message("e_noprevre")); - return null; - } - } - if (patternOffset == null || patternOffset.isEmpty()) { - patternOffset = lastPatternOffset; - } - } - - // Save the pattern. If it's explicitly entered, or comes from RE_SUBST, isNewPattern is true, and this becomes - // RE_LAST. If it comes from RE_SEARCH, then a) it's not null and b) we know that RE_LAST is already valid. - setLastUsedPattern(pattern, RE_SEARCH, isNewPattern); - lastIgnoreSmartCase = false; - lastPatternOffset = patternOffset; // This might include extra search patterns separated by `;` - lastDir = dir; - - if (logger.isDebugEnabled()) { - logger.debug("lastSearch=" + lastSearch); - logger.debug("lastOffset=" + lastPatternOffset); - logger.debug("lastDir=" + lastDir); - } - - resetShowSearchHighlight(); - forceUpdateSearchHighlights(); - - return findItOffset(((IjVimEditor)editor).getEditor(), startOffset, count1, lastDir); - } - - /** - * Process the pattern being used as a search range - * - *

Find the next offset of the search pattern, without processing the pattern further. This is not a full search - * pattern, as handled by processSearchCommand. It does not contain a pattern offset and there are not multiple - * patterns separated by `;`. Ranges do support multiple patterns, separation with both `;` and `,` and a `+/-{num}` - * suffix, but these are all handled by the range itself.

- * - *

This method is essentially a wrapper around SearchHelper.findPattern (via findItOffset) that updates state and - * highlighting.

- * - * @param editor The editor to search in - * @param pattern The pattern to search for. Does not include leading or trailing `/` and `?` characters - * @param patternOffset The offset applied to the range. Not used during searching, but used to populate lastPatternOffset - * @param startOffset The offset to start searching from - * @param direction The direction to search in - * @return The offset of the match or -1 if not found - */ - public int processSearchRange(@NotNull Editor editor, @NotNull String pattern, int patternOffset, int startOffset, @NotNull Direction direction) { - - // Will set RE_LAST, required by findItOffset - // IgnoreSmartCase and Direction are always reset. - // PatternOffset is cleared before searching. ExRanges will add/subtract the line offset from the final search range - // pattern, but we need the value to update lastPatternOffset for future searches. - // TODO: Consider improving pattern offset handling - setLastUsedPattern(pattern, RE_SEARCH, true); - lastIgnoreSmartCase = false; - lastPatternOffset = ""; // Do not apply a pattern offset yet! - lastDir = direction; - - if (logger.isDebugEnabled()) { - logger.debug("lastSearch=" + lastSearch); - logger.debug("lastOffset=" + lastPatternOffset); - logger.debug("lastDir=" + lastDir); - } - - resetShowSearchHighlight(); - forceUpdateSearchHighlights(); - - final Pair result = findItOffset(editor, startOffset, 1, lastDir); - - // Set lastPatternOffset AFTER searching so it doesn't affect the result - lastPatternOffset = patternOffset != 0 ? Integer.toString(patternOffset) : ""; - - if (logger.isDebugEnabled()) { - logger.debug("lastSearch=" + lastSearch); - logger.debug("lastOffset=" + lastPatternOffset); - logger.debug("lastDir=" + lastDir); - } - - return result != null ? result.getFirst() : -1; - } - - /** - * Search for the word under the given caret - * - *

Updates RE_SEARCH and RE_LAST, last pattern offset and direction. Ignore smart case is set to true. Highlights - * are updated. scanwrap and ignorecase come from options.

- * - *

Equivalent to normal.c:nv_ident

- * - * @param editor The editor to search in - * @param caret The caret to use to look for the current word - * @param count Search for the nth occurrence of the current word - * @param whole Include word boundaries in the search pattern - * @param dir Which direction to search - * @return The offset of the result or the start of the word under the caret if not found. Returns -1 on error - */ - @Override - public int searchWord(@NotNull VimEditor editor, @NotNull ImmutableVimCaret caret, int count, boolean whole, @NotNull Direction dir) { - if (globalIjOptions(injector).getUseNewRegex()) return super.searchWord(editor, caret, count, whole, dir); - TextRange range = SearchHelper.findWordUnderCursor(((IjVimEditor)editor).getEditor(), ((IjVimCaret)caret).getCaret()); - if (range == null) { - logger.warn("No range was found"); - return -1; - } - - @NotNull final Editor editor1 = ((IjVimEditor)editor).getEditor(); - final int start = range.getStartOffset(); - final int end = range.getEndOffset(); - final String pattern = SearchHelper.makeSearchPattern( - EngineEditorHelperKt.getText(new IjVimEditor(editor1), start, end), whole); - - // Updates RE_LAST, ready for findItOffset - // Direction is always saved - // IgnoreSmartCase is always set to true - // There is no pattern offset available - setLastUsedPattern(pattern, RE_SEARCH, true); - lastIgnoreSmartCase = true; - lastPatternOffset = ""; - lastDir = dir; - - resetShowSearchHighlight(); - forceUpdateSearchHighlights(); - - final Pair offsetAndMotion = - findItOffset(((IjVimEditor)editor).getEditor(), range.getStartOffset(), count, lastDir); - return offsetAndMotion == null ? range.getStartOffset() : offsetAndMotion.getFirst(); - } - - /** - * Find the next occurrence of the last used pattern - * - *

Searches for RE_LAST, including last used pattern offset. Direction is the same as the last used direction. - * E.g. `?foo` followed by `n` will search backwards. scanwrap and ignorecase come from options.

- * - * @param editor The editor to search in - * @param caret Used to get the offset to start searching from - * @param count Find the nth occurrence - * @return The offset of the next match, or -1 if not found - */ - @Override - public int searchNext(@NotNull VimEditor editor, @NotNull ImmutableVimCaret caret, int count) { - if (globalIjOptions(injector).getUseNewRegex()) return super.searchNext(editor, caret, count); - return searchNextWithDirection(((IjVimEditor)editor).getEditor(), ((IjVimCaret)caret).getCaret(), count, lastDir); - } - - /** - * Find the next occurrence of the last used pattern - * - *

Searches for RE_LAST, including last used pattern offset. Direction is the opposite of the last used direction. - * E.g. `?foo` followed by `N` will be forwards. scanwrap and ignorecase come from options.

- * - * @param editor The editor to search in - * @param caret Used to get the offset to starting searching from - * @param count Find the nth occurrence - * @return The offset of the next match, or -1 if not found - */ - @Override - public int searchPrevious(@NotNull VimEditor editor, @NotNull ImmutableVimCaret caret, int count) { - if (globalIjOptions(injector).getUseNewRegex()) return super.searchPrevious(editor, caret, count); - return searchNextWithDirection(((IjVimEditor)editor).getEditor(), ((IjVimCaret)caret).getCaret(), count, - lastDir.reverse()); - } - - // See normal.c:nv_next - private int searchNextWithDirection(@NotNull Editor editor, @NotNull Caret caret, int count, Direction dir) { - resetShowSearchHighlight(); - updateSearchHighlights(); - final int startOffset = caret.getOffset(); - Pair offsetAndMotion = findItOffset(editor, startOffset, count, dir); - if (offsetAndMotion != null && offsetAndMotion.getFirst() == startOffset) { - /* Avoid getting stuck on the current cursor position, which can - * happen when an offset is given and the cursor is on the last char - * in the buffer: Repeat with count + 1. */ - offsetAndMotion = findItOffset(editor, startOffset, count + 1, dir); - } - return offsetAndMotion != null ? offsetAndMotion.getFirst() : -1; - } - - - // ******************************************************************************************************************* - // - // Substitute - // - // ******************************************************************************************************************* - - /** - * Parse and execute the substitute command - * - *

Updates state for the last substitute pattern (RE_SUBST and RE_LAST) and last replacement text. Updates search - * history and register. Also updates stored substitution flags.

- * - *

Saves the current location as a jump location and restores caret location after completion. If confirmation is - * enabled and the substitution is abandoned, the current caret location is kept, and the original location is not - * restored.

- * - *

See ex_cmds.c:ex_substitute

- * - * @param editor The editor to search in - * @param caret The caret to use for initial search offset, and to move for interactive substitution - * @param context - * @param range Only search and substitute within the given line range. Must be valid - * @param excmd The command part of the ex command line, e.g. `s` or `substitute`, or `~` - * @param exarg The argument to the substitute command, such as `/{pattern}/{string}/[flags]` - * @return True if the substitution succeeds, false on error. Will succeed even if nothing is modified - */ - @Override - @RWLockLabel.SelfSynchronized - public boolean processSubstituteCommand(@NotNull VimEditor editor, - @NotNull VimCaret caret, - @NotNull ExecutionContext context, - @NotNull LineRange range, - @NotNull @NonNls String excmd, - @NotNull @NonNls String exarg, - @NotNull VimLContext parent) { - if (globalIjOptions(injector).getUseNewRegex()) { - return super.processSubstituteCommand(editor, caret, context, range, excmd, exarg, parent); - } - - // Explicitly exit visual mode here, so that visual mode marks don't change when we move the cursor to a match. - List exceptions = new ArrayList<>(); - if (CommandStateHelper.inVisualMode(((IjVimEditor) editor).getEditor())) { - EngineModeExtensionsKt.exitVisualMode(editor); - } - - CharPointer cmd = new CharPointer(new StringBuffer(exarg)); - - int which_pat; - if ("~".equals(excmd)) { - which_pat = RE_LAST; /* use last used regexp */ - } - else { - which_pat = RE_SUBST; /* use last substitute regexp */ - } - - CharPointer pat; - CharPointer sub; - char delimiter; - /* new pattern and substitution */ - if (excmd.charAt(0) == 's' && !cmd.isNul() && !Character.isWhitespace( - cmd.charAt()) && "0123456789cegriIp|\"".indexOf(cmd.charAt()) == -1) { - /* don't accept alphanumeric for separator */ - if (CharacterClasses.isAlpha(cmd.charAt())) { - VimPlugin.showMessage(MessageHelper.message(Msg.E146)); - return false; - } - /* - * undocumented vi feature: - * "\/sub/" and "\?sub?" use last used search pattern (almost like - * //sub/r). "\&sub&" use last substitute pattern (like //sub/). - */ - if (cmd.charAt() == '\\') { - cmd.inc(); - if ("/?&".indexOf(cmd.charAt()) == -1) { - VimPlugin.showMessage(MessageHelper.message(Msg.e_backslash)); - return false; - } - if (cmd.charAt() != '&') { - which_pat = RE_SEARCH; /* use last '/' pattern */ - } - pat = new CharPointer(""); /* empty search pattern */ - delimiter = cmd.charAt(); /* remember delimiter character */ - cmd.inc(); - } - else { - /* find the end of the regexp */ - which_pat = RE_LAST; /* use last used regexp */ - delimiter = cmd.charAt(); /* remember delimiter character */ - cmd.inc(); - pat = cmd.ref(0); /* remember start of search pat */ - cmd = RegExp.skip_regexp(cmd, delimiter, true); - if (cmd.charAt() == delimiter) /* end delimiter found */ { - cmd.set('\u0000').inc(); /* replace it with a NUL */ - } - } - - /* - * Small incompatibility: vi sees '\n' as end of the command, but in - * Vim we want to use '\n' to find/substitute a NUL. - */ - sub = cmd.ref(0); /* remember the start of the substitution */ - - while (!cmd.isNul()) { - if (cmd.charAt() == delimiter) /* end delimiter found */ { - cmd.set('\u0000').inc(); /* replace it with a NUL */ - break; - } - if (cmd.charAt(0) == '\\' && cmd.charAt(1) != 0) /* skip escaped characters */ { - cmd.inc(); - } - cmd.inc(); - } - } - else { - /* use previous pattern and substitution */ - if (lastReplace == null) { - /* there is no previous command */ - VimPlugin.showMessage(MessageHelper.message(Msg.e_nopresub)); - return false; - } - pat = null; /* search_regcomp() will use previous pattern */ - sub = new CharPointer(lastReplace); - } - - /* - * Find trailing options. When '&' is used, keep old options. - */ - if (cmd.charAt() == '&') { - cmd.inc(); - } - else { - // :h :&& - "Note that :s and :& don't keep the flags" - do_all = options(injector, editor).getGdefault(); - do_ask = false; - do_error = true; - do_ic = 0; - } - while (!cmd.isNul()) { - /* - * Note that 'g' and 'c' are always inverted, also when p_ed is off. - * 'r' is never inverted. - */ - if (cmd.charAt() == 'g') { - do_all = !do_all; - } - else if (cmd.charAt() == 'c') { - do_ask = !do_ask; - } - else if (cmd.charAt() == 'e') { - do_error = !do_error; - } - else if (cmd.charAt() == 'r') { - /* use last used regexp */ - which_pat = RE_LAST; - } - else if (cmd.charAt() == 'i') { - /* ignore case */ - do_ic = 'i'; - } - else if (cmd.charAt() == 'I') { - /* don't ignore case */ - do_ic = 'I'; - } - else if (cmd.charAt() != 'p' && cmd.charAt() != 'l' && cmd.charAt() != '#' && cmd.charAt() != 'n') { - // TODO: Support printing last changed line, with options for line number/list format - // TODO: Support 'n' to report number of matches without substituting - break; - } - cmd.inc(); - } - - int line1 = range.startLine; - int line2 = range.endLine; - - if (line1 < 0 || line2 < 0) { - return false; - } - - /* - * check for a trailing count - */ - cmd.skipWhitespaces(); - if (Character.isDigit(cmd.charAt())) { - int i = cmd.getDigits(); - if (i <= 0 && do_error) { - VimPlugin.showMessage(MessageHelper.message(Msg.e_zerocount)); - return false; - } - line1 = line2; - line2 = EngineEditorHelperKt.normalizeLine(editor, line1 + i - 1); - } - - /* - * check for trailing command or garbage - */ - cmd.skipWhitespaces(); - if (!cmd.isNul() && cmd.charAt() != '"') { - /* if not end-of-line or comment */ - VimPlugin.showMessage(MessageHelper.message(Msg.e_trailing)); - return false; - } - - Pair> booleanregmmatch_tPair = search_regcomp(pat, which_pat, RE_SUBST); - if (!booleanregmmatch_tPair.getFirst()) { - if (do_error) { - VimPlugin.showMessage(MessageHelper.message(Msg.e_invcmd)); - VimPlugin.indicateError(); - } - return false; - } - RegExp.regmmatch_T regmatch = (RegExp.regmmatch_T)booleanregmmatch_tPair.getSecond().getFirst(); - String pattern = booleanregmmatch_tPair.getSecond().getSecond(); - RegExp sp = (RegExp)booleanregmmatch_tPair.getSecond().getThird(); - - /* the 'i' or 'I' flag overrules 'ignorecase' and 'smartcase' */ - if (do_ic == 'i') { - regmatch.rmm_ic = true; - } - else if (do_ic == 'I') { - regmatch.rmm_ic = false; - } - - /* - * ~ in the substitute pattern is replaced with the old pattern. - * We do it here once to avoid it to be replaced over and over again. - * But don't do it when it starts with "\=", then it's an expression. - */ - if (!(sub.charAt(0) == '\\' && sub.charAt(1) == '=') && lastReplace != null) { - StringBuffer tmp = new StringBuffer(sub.toString()); - int pos = 0; - while ((pos = tmp.indexOf("~", pos)) != -1) { - if (pos == 0 || tmp.charAt(pos - 1) != '\\') { - tmp.replace(pos, pos + 1, lastReplace); - pos += lastReplace.length(); - } - pos++; - } - sub = new CharPointer(tmp); - } - - lastReplace = sub.toString(); - - resetShowSearchHighlight(); - forceUpdateSearchHighlights(); - - int start = ((IjVimEditor)editor).getEditor().getDocument().getLineStartOffset(line1); - int end = ((IjVimEditor)editor).getEditor().getDocument().getLineEndOffset(line2); - - if (logger.isDebugEnabled()) { - logger.debug("search range=[" + start + "," + end + "]"); - logger.debug("pattern=" + pattern + ", replace=" + sub); - } - - int lastMatch = -1; - int lastLine = -1; - int searchcol = 0; - boolean firstMatch = true; - boolean got_quit = false; - int lcount = editor.lineCount(); - Expression expression = null; - for (int lnum = line1; lnum <= line2 && !got_quit; ) { - CharacterPosition newpos = null; - int nmatch = sp.vim_regexec_multi(regmatch, editor, lcount, lnum, searchcol); - if (nmatch > 0) { - if (firstMatch) { - VimInjectorKt.injector.getJumpService().saveJumpLocation(editor); - firstMatch = false; - } - - String match = sp.vim_regsub_multi(regmatch, lnum, sub, 1, false); - if (sub.charAt(0) == '\\' && sub.charAt(1) == '=') { - String exprString = sub.toString().substring(2); - expression = VimscriptParser.INSTANCE.parseExpression(exprString); - if (expression == null) { - exceptions.add(new ExException("E15: Invalid expression: " + exprString)); - expression = new SimpleExpression(new VimString("")); - } - } - else if (match == null) { - return false; - } - - int line = lnum + regmatch.startpos[0].lnum; - CharacterPosition startpos = new CharacterPosition(lnum + regmatch.startpos[0].lnum, regmatch.startpos[0].col); - CharacterPosition endpos = new CharacterPosition(lnum + regmatch.endpos[0].lnum, regmatch.endpos[0].col); - int startoff = startpos.toOffset(((IjVimEditor)editor).getEditor()); - int endoff = (endpos.line >= lcount) ? (int) editor.fileSize() : endpos.toOffset(((IjVimEditor)editor).getEditor()); - - if (do_all || line != lastLine) { - boolean doReplace = true; - if (do_ask) { - RangeHighlighter hl = - SearchHighlightsHelper.addSubstitutionConfirmationHighlight(((IjVimEditor)editor).getEditor(), startoff, - endoff); - final ReplaceConfirmationChoice choice = confirmChoice(((IjVimEditor)editor).getEditor(), context, match, ((IjVimCaret)caret).getCaret(), startoff); - ((IjVimEditor)editor).getEditor().getMarkupModel().removeHighlighter(hl); - switch (choice) { - case SUBSTITUTE_THIS: - doReplace = true; - break; - case SKIP: - doReplace = false; - break; - case SUBSTITUTE_ALL: - do_ask = false; - break; - case QUIT: - doReplace = false; - got_quit = true; - break; - case SUBSTITUTE_LAST: - do_all = false; - line2 = lnum; - doReplace = true; - break; - } - } - if (doReplace) { - SubmatchFunctionHandler.Companion.getInstance().setLatestMatch( - ((IjVimEditor)editor).getEditor().getDocument().getText(new com.intellij.openapi.util.TextRange(startoff, endoff))); - caret.moveToOffset(startoff); - if (expression != null) { - try { - match = expression.evaluate(editor, context, parent).toInsertableString(); - } - catch (Exception e) { - exceptions.add((ExException)e); - match = ""; - } - } - - String finalMatch = match; - ApplicationManager.getApplication().runWriteAction( - () -> ((IjVimEditor)editor).getEditor().getDocument().replaceString(startoff, endoff, finalMatch)); - lastMatch = startoff; - int newend = startoff + match.length(); - newpos = CharacterPosition.Companion.fromOffset(((IjVimEditor)editor).getEditor(), newend); - - lnum += newpos.line - endpos.line; - line2 += newpos.line - endpos.line; - } - } - - lastLine = line; - - if (do_all && startoff != endoff) { - if (newpos != null) { - lnum = newpos.line; - searchcol = newpos.column; - } - else { - lnum += Math.max(1, nmatch - 1); - searchcol = endpos.column; - } - } - else { - lnum += Math.max(1, nmatch - 1); - searchcol = 0; - } - } - else { - lnum += Math.max(1, nmatch - 1); - searchcol = 0; - } - } - - if (!got_quit) { - if (lastMatch != -1) { - caret.moveToOffset( - VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, editor.offsetToBufferPosition(lastMatch).getLine())); - } - else { - // E486: Pattern not found: {0} - VimPlugin.showMessage(MessageHelper.message("E486", pattern)); - } - } - - SubmatchFunctionHandler.Companion.getInstance().setLatestMatch(""); - - // todo throw multiple exceptions at once - if (!exceptions.isEmpty()) { - VimPlugin.indicateError(); - VimPlugin.showMessage(exceptions.get(0).toString()); - } - - // TODO: Support reporting number of changes (:help 'report') - - return true; - } - - @Override - public void setLastSearchPattern(@Nullable String lastSearchPattern) { - if (globalIjOptions(injector).getUseNewRegex()) { - super.setLastSearchPattern(lastSearchPattern); - return; - } - this.lastSearch = lastSearchPattern; - if (showSearchHighlight) { - resetIncsearchHighlights(); - updateSearchHighlights(); - } - } - - @Override - public void setLastSubstitutePattern(@Nullable String lastSubstitutePattern) { - if (globalIjOptions(injector).getUseNewRegex()) { - super.setLastSubstitutePattern(lastSubstitutePattern); - return; - } - this.lastSubstitute = lastSubstitutePattern; - } - - @Override - public int processSearchRange(@NotNull VimEditor editor, - @NotNull String pattern, - int patternOffset, - int startOffset, - @NotNull Direction direction) { - if (globalIjOptions(injector).getUseNewRegex()) return super.processSearchRange(editor, pattern, patternOffset, startOffset, direction); - return processSearchRange(((IjVimEditor) editor).getEditor(), pattern, patternOffset, startOffset, direction); - } - - //public Pair> search_regcomp(CharPointer pat, - public Pair> search_regcomp(CharPointer pat, - int which_pat, - int patSave) { - // We don't need to worry about lastIgnoreSmartCase, it's always false. Vim resets after checking, and it only sets - // it to true when searching for a word with `*`, `#`, `g*`, etc. - boolean isNewPattern = true; - String pattern = ""; - if (pat == null || pat.isNul()) { - isNewPattern = false; - if (which_pat == RE_LAST) { - which_pat = lastPatternIdx; - } - String errorMessage = switch (which_pat) { - case RE_SEARCH -> { - pattern = lastSearch; - yield MessageHelper.message("e_nopresub"); - } - case RE_SUBST -> { - pattern = lastSubstitute; - yield MessageHelper.message("e_noprevre"); - } - default -> null; - }; - - // Pattern was never defined - if (pattern == null) { - VimPlugin.showMessage(errorMessage); - return new Pair<>(false, null); - } - } - else { - pattern = pat.toString(); - } - - // Set RE_SUBST and RE_LAST, but only for explicitly typed patterns. Reused patterns are not saved/updated - setLastUsedPattern(pattern, patSave, isNewPattern); - - // Always reset after checking, only set for nv_ident - lastIgnoreSmartCase = false; - // Substitute does NOT reset last direction or pattern offset! - - RegExp sp; - RegExp.regmmatch_T regmatch = new RegExp.regmmatch_T(); - regmatch.rmm_ic = shouldIgnoreCase(pattern, false); - sp = new RegExp(); - regmatch.regprog = sp.vim_regcomp(pattern, 1); - if (regmatch.regprog == null) { - return new Pair<>(false, null); - } - return new Pair<>(true, new Triple<>(regmatch, pattern, sp)); - } - - private static @NotNull ReplaceConfirmationChoice confirmChoice(@NotNull Editor editor, - @NotNull ExecutionContext context, - @NotNull String match, @NotNull Caret caret, int startoff) { - final Ref result = Ref.create(ReplaceConfirmationChoice.QUIT); - final Function1 keyStrokeProcessor = key -> { - final ReplaceConfirmationChoice choice; - final char c = key.getKeyChar(); - if (StringAndKeysKt.isCloseKeyStroke(key) || c == 'q') { - choice = ReplaceConfirmationChoice.QUIT; - } else if (c == 'y') { - choice = ReplaceConfirmationChoice.SUBSTITUTE_THIS; - } else if (c == 'l') { - choice = ReplaceConfirmationChoice.SUBSTITUTE_LAST; - } else if (c == 'n') { - choice = ReplaceConfirmationChoice.SKIP; - } else if (c == 'a') { - choice = ReplaceConfirmationChoice.SUBSTITUTE_ALL; - } else { - return true; - } - // TODO: Handle and - result.set(choice); - return false; - }; - if (ApplicationManager.getApplication().isUnitTestMode()) { - new IjVimCaret(caret).moveToOffset(startoff); - final TestInputModel inputModel = TestInputModel.getInstance(editor); - for (KeyStroke key = inputModel.nextKeyStroke(); key != null; key = inputModel.nextKeyStroke()) { - if (!keyStrokeProcessor.invoke(key)) { - break; - } - } - } - else { - // XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for this method - final VimCommandLine exEntryPanel = injector.getCommandLine().createWithoutShortcuts( - new IjVimEditor(editor), - context, - MessageHelper.message("replace.with.0", match), - "" - ); - new IjVimCaret(caret).moveToOffset(startoff); - ModalEntry.INSTANCE.activate(new IjVimEditor(editor), keyStrokeProcessor); - exEntryPanel.deactivate(true, false); - } - return result.get(); - } - - - // ******************************************************************************************************************* - // - // gn implementation - // - // ******************************************************************************************************************* - - /** - * Find the range of the next occurrence of the last used search pattern - * - *

Used for the implementation of the gn and gN commands.

- * - *

Searches for the range of the next occurrence of the last used search pattern (RE_LAST). If the current primary - * caret is inside the range of an occurrence, will return that instance. Uses the last used search pattern. Does not - * update any other state. Direction is explicit, not from state.

- * - * @param editor The editor to search in - * @param count Find the nth occurrence - * @param forwards Search forwards or backwards - * @return The TextRange of the next occurrence or null if not found - */ - @Override - public @Nullable TextRange getNextSearchRange(@NotNull VimEditor editor, int count, boolean forwards) { - if (globalIjOptions(injector).getUseNewRegex()) return super.getNextSearchRange(editor, count, forwards); - editor.removeSecondaryCarets(); - TextRange current = findUnderCaret(editor); - - if (current == null || CommandStateHelper.inVisualMode(((IjVimEditor)editor).getEditor()) && atEdgeOfGnRange(current, ((IjVimEditor)editor).getEditor(), forwards)) { - current = findNextSearchForGn(editor, count, forwards); - } - else if (count > 1) { - current = findNextSearchForGn(editor, count - 1, forwards); - } - return current; - } - - private boolean atEdgeOfGnRange(@NotNull TextRange nextRange, @NotNull Editor editor, boolean forwards) { - int currentPosition = editor.getCaretModel().getOffset(); - if (forwards) { - return nextRange.getEndOffset() - VimPlugin.getVisualMotion().getSelectionAdj() == currentPosition; - } - else { - return nextRange.getStartOffset() == currentPosition; - } - } - - private @Nullable TextRange findNextSearchForGn(@NotNull VimEditor editor, int count, boolean forwards) { - if (forwards) { - final EnumSet searchOptions = EnumSet.of(SearchOptions.WRAP, SearchOptions.WHOLE_FILE); - return VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), editor.primaryCaret().getOffset(), count, searchOptions); - } else { - return searchBackward(editor, editor.primaryCaret().getOffset(), count); - } - } - - @Override - @Nullable - public TextRange searchBackward(@NotNull VimEditor editor, int offset, int count) { - if (globalIjOptions(injector).getUseNewRegex()) return super.searchBackward(editor, offset, count); - // Backward search returns wrongs end offset for some cases. That's why we should perform additional forward search - final EnumSet searchOptions = EnumSet.of(SearchOptions.WRAP, SearchOptions.WHOLE_FILE, SearchOptions.BACKWARDS); - final TextRange foundBackward = VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), offset, count, searchOptions); - if (foundBackward == null) return null; - int startOffset = foundBackward.getStartOffset() - 1; - if (startOffset < 0) startOffset = (int)editor.fileSize(); - searchOptions.remove(SearchOptions.BACKWARDS); - return VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), startOffset, 1, searchOptions); - } - - - // ******************************************************************************************************************* - // - // Highlighting - // - // ******************************************************************************************************************* - //region Search highlights - @Override - public void clearSearchHighlight() { - if (globalIjOptions(injector).getUseNewRegex()) { - super.clearSearchHighlight(); - return; - } - showSearchHighlight = false; - updateSearchHighlights(); - } - - private void forceUpdateSearchHighlights() { - // Sync the search highlights to the current state, potentially hiding or showing highlights. Will always update, - // even if the pattern hasn't changed. - SearchHighlightsHelper.updateSearchHighlights(getLastUsedPattern(), lastIgnoreSmartCase, showSearchHighlight, true); - } - - private void updateSearchHighlights() { - // Sync the search highlights to the current state, potentially hiding or showing highlights. Will only update if - // the pattern has changed. - SearchHighlightsHelper.updateSearchHighlights(getLastUsedPattern(), lastIgnoreSmartCase, showSearchHighlight, false); - } - - /** - * Reset the search highlights to the last used pattern after highlighting incsearch results. - */ - @Override - public void resetIncsearchHighlights() { - if (globalIjOptions(injector).getUseNewRegex()) { - super.resetIncsearchHighlights(); - return; - } - SearchHighlightsHelper.updateSearchHighlights(getLastUsedPattern(), lastIgnoreSmartCase, showSearchHighlight, true); - } - - private void resetShowSearchHighlight() { - showSearchHighlight = globalOptions(injector).getHlsearch(); - } - - private void highlightSearchLines(@NotNull Editor editor, int startLine, int endLine) { - if (globalIjOptions(injector).getUseNewRegex()) { - super.highlightSearchLines(new IjVimEditor(editor), startLine, endLine); - return; - } - final String pattern = getLastUsedPattern(); - if (pattern != null) { - final List results = injector.getSearchHelper().findAll(new IjVimEditor(editor), pattern, startLine, endLine, - shouldIgnoreCase(pattern, lastIgnoreSmartCase)); - SearchHighlightsHelper.highlightSearchResults(editor, pattern, results, -1); - } - } - - /** - * Updates search highlights when the selected editor changes - */ - public void fileEditorManagerSelectionChangedCallback(@SuppressWarnings("unused") @NotNull FileEditorManagerEvent event) { - if (globalIjOptions(injector).getUseNewRegex()) { - super.updateSearchHighlights(false); - return; - } - VimPlugin.getSearch().updateSearchHighlights(); - } - - @Override - public Integer findDecimalNumber(@NotNull String line) { - if (globalIjOptions(injector).getUseNewRegex()) return super.findDecimalNumber(line); - Pair searchResult = SearchHelper.findNumberInText(line, 0, false, false, false); - if (searchResult != null) { - TextRange range = searchResult.component1(); - return Integer.parseInt(line.substring(range.getStartOffset(), range.getEndOffset())); - } - return null; - } - - @NotNull - @Override - public Direction getLastSearchDirection() { - if (globalIjOptions(injector).getUseNewRegex()) return super.getLastSearchDirection(); - return lastDir; - } - - // ******************************************************************************************************************* - // - // Implementation details - // - // ******************************************************************************************************************* - - /** - * Searches for the RE_LAST saved pattern, applying the last saved pattern offset. Will loop over trailing search - * commands. - * - *

Make sure that RE_LAST has been updated before calling this. wrapscan and ignorecase come from options.

- * - *

See search.c:do_search (and a little bit of normal.c:normal_search)

- * - * @param editor The editor to search in - * @param startOffset The offset to search from - * @param count Find the nth occurrence - * @param dir The direction to search in - * @return Pair containing the offset to the next occurrence of the pattern, and the [MotionType] based - * on the search offset. The value will be `null` if no result is found. - */ - @Nullable - private Pair findItOffset(@NotNull Editor editor, int startOffset, int count, Direction dir) { - boolean wrap = globalOptions(injector).getWrapscan(); - logger.debug("Perform search. Direction: " + dir + " wrap: " + wrap); - - int offset = 0; - boolean offsetIsLineOffset = false; - boolean hasEndOffset = false; - - ParsePosition pp = new ParsePosition(0); - - if (!lastPatternOffset.isEmpty()) { - if (Character.isDigit(lastPatternOffset.charAt(0)) || lastPatternOffset.charAt(0) == '+' || lastPatternOffset.charAt(0) == '-') { - offsetIsLineOffset = true; - - if (lastPatternOffset.equals("+")) { - offset = 1; - } else if (lastPatternOffset.equals("-")) { - offset = -1; - } else { - if (lastPatternOffset.charAt(0) == '+') { - lastPatternOffset = lastPatternOffset.substring(1); - } - NumberFormat nf = NumberFormat.getIntegerInstance(); - pp = new ParsePosition(0); - Number num = nf.parse(lastPatternOffset, pp); - if (num != null) { - offset = num.intValue(); - } - } - } else if ("ebs".indexOf(lastPatternOffset.charAt(0)) != -1) { - if (lastPatternOffset.length() >= 2) { - if ("+-".indexOf(lastPatternOffset.charAt(1)) != -1) { - offset = 1; - } - NumberFormat nf = NumberFormat.getIntegerInstance(); - pp = new ParsePosition(lastPatternOffset.charAt(1) == '+' ? 2 : 1); - Number num = nf.parse(lastPatternOffset, pp); - if (num != null) { - offset = num.intValue(); - } - } - - hasEndOffset = lastPatternOffset.charAt(0) == 'e'; - } - } - - // `/{pattern}/{offset}` is inclusive if offset contains `e`, and linewise if there's a line offset - final MotionType motionType; - if (offset != 0 && !hasEndOffset) { - motionType = MotionType.LINE_WISE; - } - else if (hasEndOffset) { - motionType = MotionType.INCLUSIVE; - } - else { - motionType = MotionType.EXCLUSIVE; - } - - /* - * If there is a character offset, subtract it from the current - * position, so we don't get stuck at "?pat?e+2" or "/pat/s-2". - * Skip this if pos.col is near MAXCOL (closed fold). - * This is not done for a line offset, because then we would not be vi - * compatible. - */ - if (!offsetIsLineOffset && offset != 0) { - startOffset = Math.max(0, Math.min(startOffset - offset, EditorHelperRt.getFileSize(editor) - 1)); - } - - EnumSet searchOptions = EnumSet.of(SearchOptions.SHOW_MESSAGES, SearchOptions.WHOLE_FILE); - if (dir == Direction.BACKWARDS) searchOptions.add(SearchOptions.BACKWARDS); - if (lastIgnoreSmartCase) searchOptions.add(SearchOptions.IGNORE_SMARTCASE); - if (wrap) searchOptions.add(SearchOptions.WRAP); - if (hasEndOffset) searchOptions.add(SearchOptions.WANT_ENDPOS); - - // Uses RE_LAST. We know this is always set before being called - TextRange range = injector.getSearchHelper().findPattern(new IjVimEditor(editor), getLastUsedPattern(), startOffset, count, searchOptions); - if (range == null) { - logger.warn("No range is found"); - return null; - } - - int res = range.getStartOffset(); - - if (offsetIsLineOffset) { - int line = editor.offsetToLogicalPosition(range.getStartOffset()).line; - int newLine = EngineEditorHelperKt.normalizeLine(new IjVimEditor(editor), line + offset); - res = VimPlugin.getMotion().moveCaretToLineStart(new IjVimEditor(editor), newLine); - } - else if (hasEndOffset || offset != 0) { - int base = hasEndOffset ? range.getEndOffset() - 1 : range.getStartOffset(); - res = Math.max(0, Math.min(base + offset, EditorHelperRt.getFileSize(editor) - 1)); - } - - int ppos = pp.getIndex(); - if (ppos < lastPatternOffset.length() - 1 && lastPatternOffset.charAt(ppos) == ';') { - final Direction nextDir; - if (lastPatternOffset.charAt(ppos + 1) == '/') { - nextDir = Direction.FORWARDS; - } - else if (lastPatternOffset.charAt(ppos + 1) == '?') { - nextDir = Direction.BACKWARDS; - } - else { - return new Pair(res, motionType); - } - - if (lastPatternOffset.length() - ppos > 2) { - ppos++; - } - - Pair offsetAndMotion = - processSearchCommand(new IjVimEditor(editor), lastPatternOffset.substring(ppos + 1), res, 1, nextDir); - res = offsetAndMotion != null ? offsetAndMotion.getFirst() : -1; - } - - return new Pair(res, motionType); - } - - private enum ReplaceConfirmationChoice { - SUBSTITUTE_THIS, - SUBSTITUTE_LAST, - SKIP, - QUIT, - SUBSTITUTE_ALL, - } - - // Vim saves the patterns used for searching (`/`) and substitution (`:s`) separately - // viminfo records them as `# Last Search Pattern` and `# Last Substitute Search Pattern` respectively - // Vim also saves flags in viminfo - ~[~] - // The trailing tilde tracks which was the last used pattern, but line/end/off is only used for search, not substitution - // Search values can contain new lines, etc. Vim saves these as CTRL chars, e.g. ^M - // Before saving, Vim reads existing viminfo, merges and writes - private @Nullable String lastSearch; // Pattern used for last search command (`/`) - private @Nullable String lastSubstitute; // Pattern used for last substitute command (`:s`) - private int lastPatternIdx; // Which pattern was used last? RE_SEARCH or RE_SUBST? - private @Nullable String lastReplace; // `# Last Substitute String` from viminfo - private @NotNull String lastPatternOffset = ""; // /{pattern}/{offset}. Do not confuse with caret offset! - private boolean lastIgnoreSmartCase; - private @NotNull Direction lastDir = Direction.FORWARDS; - private boolean showSearchHighlight = globalOptions(injector).getHlsearch(); - - private boolean do_all = false; /* do multiple substitutions per line */ - private boolean do_ask = false; /* ask for confirmation */ - private boolean do_error = true; /* if false, ignore errors */ - //private boolean do_print = false; /* print last line with subs. */ - private char do_ic = 0; /* ignore case flag */ - - // Matching the values defined in Vim. Do not change these values, they are used as indexes - public static final int RE_SEARCH = 0; // Save/use search pattern - public static final int RE_SUBST = 1; // Save/use substitute pattern - public static final int RE_BOTH = 2; // Save to both patterns - public static final int RE_LAST = 2; // Use last used pattern if "pat" is NULL - - private static final Logger logger = Logger.getInstance(SearchGroup.class.getName()); -} diff --git a/src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java b/src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java index 0359664210..a886561f00 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java +++ b/src/main/java/com/maddyhome/idea/vim/helper/SearchHelper.java @@ -44,7 +44,6 @@ import static com.maddyhome.idea.vim.api.VimInjectorKt.*; import static com.maddyhome.idea.vim.helper.SearchHelperKtKt.checkInString; import static com.maddyhome.idea.vim.helper.SearchHelperKtKt.shouldIgnoreCase; -import static com.maddyhome.idea.vim.newapi.IjVimInjectorKt.globalIjOptions; /** * Helper methods for searching text @@ -375,65 +374,22 @@ else if (lnum <= 0) { boolean ignoreCase) { final List results = Lists.newArrayList(); - if (globalIjOptions(injector).getUseNewRegex()) { - final EnumSet options = EnumSet.noneOf(VimRegexOptions.class); - if (globalOptions(injector).getSmartcase()) options.add(VimRegexOptions.SMART_CASE); - if (globalOptions(injector).getIgnorecase()) options.add(VimRegexOptions.IGNORE_CASE); - VimEditor vimEditor = new IjVimEditor(editor); - try { - // TODO: we shouldn't care about the ignoreCase argument, and instead just look into the editor options. - // It would require a refactor, so for now prepend \c or \C to "force" ignoreCase - String newPattern = (ignoreCase ? "\\c" : "\\C") + pattern; - VimRegex regex = new VimRegex(newPattern); - List foundMatches = regex.findAll(vimEditor, vimEditor.getLineStartOffset(startLine), vimEditor.getLineEndOffset(endLine == -1 ? vimEditor.lineCount() - 1 : endLine) + 1, options); - for (VimMatchResult.Success match : foundMatches) results.add(match.getRange()); - return results; - } catch (VimRegexException e) { - injector.getMessages().showStatusBarMessage(vimEditor, e.getMessage()); - return results; - } - } - - final int lineCount = new IjVimEditor(editor).lineCount(); - final int actualEndLine = endLine == -1 ? lineCount - 1 : endLine; - - final RegExp.regmmatch_T regMatch = new RegExp.regmmatch_T(); - final RegExp regExp = new RegExp(); - regMatch.regprog = regExp.vim_regcomp(pattern, 1); - if (regMatch.regprog == null) { + final EnumSet options = EnumSet.noneOf(VimRegexOptions.class); + if (globalOptions(injector).getSmartcase()) options.add(VimRegexOptions.SMART_CASE); + if (globalOptions(injector).getIgnorecase()) options.add(VimRegexOptions.IGNORE_CASE); + VimEditor vimEditor = new IjVimEditor(editor); + try { + // TODO: we shouldn't care about the ignoreCase argument, and instead just look into the editor options. + // It would require a refactor, so for now prepend \c or \C to "force" ignoreCase + String newPattern = (ignoreCase ? "\\c" : "\\C") + pattern; + VimRegex regex = new VimRegex(newPattern); + List foundMatches = regex.findAll(vimEditor, vimEditor.getLineStartOffset(startLine), vimEditor.getLineEndOffset(endLine == -1 ? vimEditor.lineCount() - 1 : endLine) + 1, options); + for (VimMatchResult.Success match : foundMatches) results.add(match.getRange()); + return results; + } catch (VimRegexException e) { + injector.getMessages().showStatusBarMessage(vimEditor, e.getMessage()); return results; } - - regMatch.rmm_ic = ignoreCase; - - int col = 0; - for (int line = startLine; line <= actualEndLine; ) { - int matchedLines = regExp.vim_regexec_multi(regMatch, new IjVimEditor(editor), lineCount, line, col); - if (matchedLines > 0) { - final CharacterPosition startPos = new CharacterPosition(line + regMatch.startpos[0].lnum, - regMatch.startpos[0].col); - final CharacterPosition endPos = new CharacterPosition(line + regMatch.endpos[0].lnum, - regMatch.endpos[0].col); - int start = startPos.toOffset(editor); - int end = endPos.line >= lineCount ? editor.getDocument().getTextLength() : endPos.toOffset(editor); - results.add(new TextRange(start, end)); - - if (start != end) { - line += matchedLines - 1; - col = endPos.column; - } - else { - line += matchedLines; - col = 0; - } - } - else { - line++; - col = 0; - } - } - - return results; } /** diff --git a/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt b/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt index e19d8ec693..c62fa2a373 100644 --- a/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt +++ b/src/main/java/com/maddyhome/idea/vim/listener/VimListenerManager.kt @@ -71,7 +71,6 @@ import com.maddyhome.idea.vim.group.IjVimRedrawService import com.maddyhome.idea.vim.group.MotionGroup import com.maddyhome.idea.vim.group.OptionGroup import com.maddyhome.idea.vim.group.ScrollGroup -import com.maddyhome.idea.vim.group.SearchGroup import com.maddyhome.idea.vim.group.VimMarkServiceImpl import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl import com.maddyhome.idea.vim.group.visual.VimVisualTimer diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimInjector.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimInjector.kt index 2471e1c478..203e3633c1 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimInjector.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimInjector.kt @@ -70,7 +70,6 @@ import com.maddyhome.idea.vim.group.IjVimOptionGroup import com.maddyhome.idea.vim.group.IjVimPsiService import com.maddyhome.idea.vim.group.MacroGroup import com.maddyhome.idea.vim.group.MotionGroup -import com.maddyhome.idea.vim.group.SearchGroup import com.maddyhome.idea.vim.group.TabService import com.maddyhome.idea.vim.group.VimWindowGroup import com.maddyhome.idea.vim.group.WindowGroup @@ -133,7 +132,7 @@ internal class IjVimInjector : VimInjectorBase() { override val templateManager: VimTemplateManager get() = service() override val searchGroup: VimSearchGroup - get() = service() + get() = service() override val put: VimPut get() = service() override val window: VimWindowGroup diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt index ed93599a67..211e70709b 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchGroup.kt @@ -17,6 +17,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.editor.markup.RangeHighlighter +import com.intellij.openapi.fileEditor.FileEditorManagerEvent import com.intellij.openapi.util.Ref import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext @@ -274,6 +275,24 @@ public open class IjVimSearchGroup : VimSearchGroupBase(), PersistentStateCompon public override fun loadState(state: Element) { readData(state) } + + /** + * Updates search highlights when the selected editor changes + */ + public fun fileEditorManagerSelectionChangedCallback(@Suppress("unused") event: FileEditorManagerEvent) { + updateSearchHighlights(false) + } + + public fun turnOn() { + updateSearchHighlights(false) + } + + public fun turnOff() { + val show = showSearchHighlight + clearSearchHighlight() + showSearchHighlight = show + } + private class IjSearchHighlight(private val editor: Editor, private val highlighter: RangeHighlighter) : SearchHighlight() { diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchHelper.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchHelper.kt index b6b9437656..f52065463b 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchHelper.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimSearchHelper.kt @@ -38,28 +38,6 @@ internal class IjVimSearchHelper : VimSearchHelperBase() { return PsiHelper.findMethodStart(editor.ij, caret.ij.offset, count) } - override fun findPattern( - editor: VimEditor, - pattern: String?, - startOffset: Int, - count: Int, - searchOptions: EnumSet?, - ): TextRange? { - return if (injector.globalIjOptions().useNewRegex) super.findPattern(editor, pattern, startOffset, count, searchOptions) - else SearchHelper.findPattern(editor.ij, pattern, startOffset, count, searchOptions) - } - - override fun findAll( - editor: VimEditor, - pattern: String, - startLine: Int, - endLine: Int, - ignoreCase: Boolean, - ): List { - return if (injector.globalIjOptions().useNewRegex) super.findAll(editor, pattern, startLine, endLine, ignoreCase) - else SearchHelper.findAll(editor.ij, pattern, startLine, endLine, ignoreCase) - } - override fun findMisspelledWord(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int { val startOffset: Int val endOffset: Int diff --git a/src/main/resources/META-INF/includes/ApplicationServices.xml b/src/main/resources/META-INF/includes/ApplicationServices.xml index 7a2e5f2e6e..c671b0baf4 100644 --- a/src/main/resources/META-INF/includes/ApplicationServices.xml +++ b/src/main/resources/META-INF/includes/ApplicationServices.xml @@ -19,7 +19,7 @@ - + , before: String, after: String) { configureByText(before) - VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) + VimPlugin.getSearch().setLastSearchState("test", "", Direction.FORWARDS) typeText(keys) assertState(after) assertState(Mode.NORMAL()) diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/GnPreviousTextObjectTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/GnPreviousTextObjectTest.kt index 5f6c89ce6e..c2e15a1acd 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/GnPreviousTextObjectTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/GnPreviousTextObjectTest.kt @@ -63,7 +63,7 @@ class GnPreviousTextObjectTest : VimTestCase() { private fun doTestWithSearch(keys: List, before: String, after: String) { configureByText(before) - VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) + VimPlugin.getSearch().setLastSearchState("test", "", Direction.FORWARDS) typeText(keys) assertState(after) assertState(Mode.NORMAL()) diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectNextSearchTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectNextSearchTest.kt index 663ca8d887..d3219a2405 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectNextSearchTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectNextSearchTest.kt @@ -57,7 +57,7 @@ class VisualSelectNextSearchTest : VimTestCase() { @Test fun testWithoutSpaces() { configureByText("testtest") - VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) + VimPlugin.getSearch().setLastSearchState("test", "", Direction.FORWARDS) typeText(injector.parser.parseKeys("gn")) assertOffset(7) assertSelection("test") diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectPreviousSearchTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectPreviousSearchTest.kt index 5b2e532174..c14c7d97e3 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectPreviousSearchTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/gn/VisualSelectPreviousSearchTest.kt @@ -54,7 +54,7 @@ class VisualSelectPreviousSearchTest : VimTestCase() { @Test fun testWithoutSpaces() { configureByText("testtest") - VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS) + VimPlugin.getSearch().setLastSearchState("test", "", Direction.FORWARDS) typeText(injector.parser.parseKeys("gN")) assertOffset(0) assertSelection("test") diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchAgainPreviousActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchAgainPreviousActionTest.kt index abf16d64d1..414f6682cd 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchAgainPreviousActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchAgainPreviousActionTest.kt @@ -203,7 +203,7 @@ class SearchAgainPreviousActionTest : VimTestCase() { private fun doTestWithSearch(keys: String, before: String, after: String) { doTest(keys, before, after) { - VimPlugin.getSearch().setLastSearchState(it, "all", "", Direction.FORWARDS) + VimPlugin.getSearch().setLastSearchState("all", "", Direction.FORWARDS) } } } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SubstituteCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SubstituteCommandTest.kt index 6b108975df..171191c513 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SubstituteCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SubstituteCommandTest.kt @@ -278,7 +278,6 @@ class SubstituteCommandTest : VimTestCase() { // Tests two things. Firstly, VIM-698, which was a bug in the old regex engine that would skip lines when substituting // with newlines and secondly to test the special case of '\n' matching end of file @OptionTest( - VimOption(TestOptionConstants.usenewregex), VimOption(TestOptionConstants.ignorecase, doesntAffectTest = true), VimOption(TestOptionConstants.smartcase, doesntAffectTest = true), ) @@ -307,7 +306,6 @@ class SubstituteCommandTest : VimTestCase() { // VIM-2141 @OptionTest( - VimOption(TestOptionConstants.usenewregex), VimOption(TestOptionConstants.ignorecase, doesntAffectTest = true), VimOption(TestOptionConstants.smartcase, doesntAffectTest = true), ) @@ -342,7 +340,6 @@ class SubstituteCommandTest : VimTestCase() { // VIM-2141 @OptionTest( - VimOption(TestOptionConstants.usenewregex), VimOption(TestOptionConstants.ignorecase, doesntAffectTest = true), VimOption(TestOptionConstants.smartcase, doesntAffectTest = true), ) @@ -370,7 +367,6 @@ class SubstituteCommandTest : VimTestCase() { // VIM-2141 @OptionTest( - VimOption(TestOptionConstants.usenewregex), VimOption(TestOptionConstants.ignorecase, doesntAffectTest = true), VimOption(TestOptionConstants.smartcase, doesntAffectTest = true), ) @@ -404,7 +400,6 @@ class SubstituteCommandTest : VimTestCase() { } @OptionTest( - VimOption(TestOptionConstants.usenewregex), VimOption(TestOptionConstants.ignorecase, doesntAffectTest = true), VimOption(TestOptionConstants.smartcase, doesntAffectTest = true), ) @@ -428,7 +423,6 @@ class SubstituteCommandTest : VimTestCase() { } @OptionTest( - VimOption(TestOptionConstants.usenewregex), VimOption(TestOptionConstants.ignorecase, doesntAffectTest = true), VimOption(TestOptionConstants.smartcase, doesntAffectTest = true), ) diff --git a/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/TestOptionConstants.kt b/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/TestOptionConstants.kt index 285f73a0e6..8b7c497f8b 100644 --- a/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/TestOptionConstants.kt +++ b/src/testFixtures/kotlin/org/jetbrains/plugins/ideavim/TestOptionConstants.kt @@ -35,9 +35,6 @@ class TestOptionConstants { // IdeaVim specific const val ideaglobalmode = "ideaglobalmode" const val ideatracetime = "ideatracetime" - - // Temporary - const val usenewregex = "usenewregex" } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt index 13c2c044ec..234a924751 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchGroupBase.kt @@ -110,7 +110,7 @@ public abstract class VimSearchGroupBase : VimSearchGroup { /** * Reset the search highlights to the last used pattern after highlighting incsearch results. */ - protected abstract fun resetIncsearchHighlights() + public abstract fun resetIncsearchHighlights() /** * Asks the user how to deal with a substitution confirmation choice. From 4dcb290c725573893a745746bd39c5e8621826ed Mon Sep 17 00:00:00 2001 From: Filipp Vakhitov Date: Tue, 4 Jun 2024 14:10:30 +0300 Subject: [PATCH 04/10] Add VimRangeMarker --- .../maddyhome/idea/vim/helper/IjEditorHelper.kt | 15 +++++++++++++++ .../vim/vimscript/model/commands/GlobalCommand.kt | 9 ++++----- .../maddyhome/idea/vim/api/EngineEditorHelper.kt | 8 ++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/helper/IjEditorHelper.kt b/src/main/java/com/maddyhome/idea/vim/helper/IjEditorHelper.kt index c71627e203..71385ef257 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/IjEditorHelper.kt +++ b/src/main/java/com/maddyhome/idea/vim/helper/IjEditorHelper.kt @@ -15,6 +15,7 @@ import com.intellij.openapi.editor.actionSystem.EditorActionManager import com.intellij.openapi.editor.ex.util.EditorUtil import com.maddyhome.idea.vim.api.EngineEditorHelper import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.api.VimRangeMarker import com.maddyhome.idea.vim.api.VimVisualPosition import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.ij @@ -57,4 +58,18 @@ internal class IjEditorHelper : EngineEditorHelper { override fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition { return EditorUtil.inlayAwareOffsetToVisualPosition(editor.ij, offset).vim } + + override fun createRangeMarker(editor: VimEditor, startOffset: Int, endOffset: Int): VimRangeMarker { + val ijRangeMarker = editor.ij.document.createRangeMarker(startOffset, endOffset) + return object : VimRangeMarker { + override val startOffset: Int + get() = ijRangeMarker.startOffset + override val endOffset: Int + get() = ijRangeMarker.endOffset + + override fun dispose() { + ijRangeMarker.dispose() + } + } + } } diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt index 153f4b6139..c5bd799c92 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt +++ b/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt @@ -8,17 +8,16 @@ package com.maddyhome.idea.vim.vimscript.model.commands -import com.intellij.openapi.editor.RangeMarker import com.intellij.vim.annotations.ExCommand import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.api.VimRangeMarker import com.maddyhome.idea.vim.api.VimSearchGroupBase import com.maddyhome.idea.vim.api.getLineStartForOffset import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.ex.ranges.LineRange import com.maddyhome.idea.vim.ex.ranges.Range -import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.regexp.VimRegexException import com.maddyhome.idea.vim.regexp.match.VimMatchResult import com.maddyhome.idea.vim.vimscript.model.ExecutionResult @@ -89,13 +88,13 @@ internal data class GlobalCommand(val range: Range, val argument: String, val in editor.getLineEndOffset(line2), ) val marks = if (!invert) matches.map { - editor.ij.document.createRangeMarker(editor.getLineStartForOffset(it.range.startOffset), editor.getLineStartForOffset(it.range.startOffset)) + injector.engineEditorHelper.createRangeMarker(editor, editor.getLineStartForOffset(it.range.startOffset), editor.getLineStartForOffset(it.range.startOffset)) // filter out lines that contain a match } else (line1..line2).filterNot { line -> matches.map { match -> editor.offsetToBufferPosition(match.range.startOffset).line }.contains(line) - }.map { editor.ij.document.createRangeMarker(editor.getLineStartOffset(it), editor.getLineStartOffset(it)) } + }.map { injector.engineEditorHelper.createRangeMarker(editor, editor.getLineStartOffset(it), editor.getLineStartOffset(it)) } if (gotInt) { messages.showStatusBarMessage(null, messages.message("e_interr")) @@ -112,7 +111,7 @@ internal data class GlobalCommand(val range: Range, val argument: String, val in return true } - private fun globalExe(editor: VimEditor, context: ExecutionContext, marks: List, cmd: String) { + private fun globalExe(editor: VimEditor, context: ExecutionContext, marks: List, cmd: String) { globalBusy = true try { for (mark in marks) { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/EngineEditorHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/EngineEditorHelper.kt index 6f60f6337e..dc4dcb0769 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/EngineEditorHelper.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/EngineEditorHelper.kt @@ -22,6 +22,7 @@ public interface EngineEditorHelper { public fun handleWithReadonlyFragmentModificationHandler(editor: VimEditor, exception: java.lang.Exception) public fun pad(editor: VimEditor, line: Int, to: Int): String public fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition + public fun createRangeMarker(editor: VimEditor, startOffset: Int, endOffset: Int): VimRangeMarker } public fun VimEditor.endsWithNewLine(): Boolean { @@ -299,3 +300,10 @@ public fun VimEditor.coerceOffset(offset: Int): Int { if (offset > this.fileSize()) return this.fileSize().toInt() return offset } + +public interface VimRangeMarker { + public val startOffset: Int + public val endOffset: Int + + public fun dispose() +} \ No newline at end of file From 0079eb2352ca8f415668e53a795f5418b6c6dbe0 Mon Sep 17 00:00:00 2001 From: Filipp Vakhitov Date: Tue, 4 Jun 2024 14:12:57 +0300 Subject: [PATCH 05/10] Move GlobalCommand to vim-engine --- .../idea/vim/vimscript/model/commands/GlobalCommand.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename {src/main/java => vim-engine/src/main/kotlin}/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt (93%) diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt similarity index 93% rename from src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt index c5bd799c92..9ef804f4ed 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/GlobalCommand.kt @@ -1,5 +1,5 @@ /* - * Copyright 2003-2023 The IdeaVim authors + * Copyright 2003-2024 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at @@ -27,14 +27,14 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult */ // FIXME: I'm such a mess, please refactor me, responsible developer @ExCommand(command = "g[lobal],v[global]") -internal data class GlobalCommand(val range: Range, val argument: String, val invert: Boolean) : Command.SingleExecution(range, argument) { +public data class GlobalCommand(val range: Range, val argument: String, val invert: Boolean) : Command.SingleExecution(range, argument) { init { // Most commands have a default range of the current line ("."). Global has a default range of the whole file defaultRange = "%" } - override val argFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_OPTIONAL, Access.SELF_SYNCHRONIZED) + override val argFlags: CommandHandlerFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_OPTIONAL, Access.SELF_SYNCHRONIZED) override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult { var result: ExecutionResult = ExecutionResult.Success @@ -140,10 +140,10 @@ internal data class GlobalCommand(val range: Range, val argument: String, val in } } - companion object { + public companion object { private var globalBusy = false // Interrupted. Not used at the moment - var gotInt: Boolean = false + public var gotInt: Boolean = false } } From 3b0df0f6f056a40437c61fb6395fa0732311d4d2 Mon Sep 17 00:00:00 2001 From: Filipp Vakhitov Date: Tue, 4 Jun 2024 14:40:18 +0300 Subject: [PATCH 06/10] Move Vimscript visitors to vim-engine --- .../idea/vim/vimscript/parser/VimscriptParser.kt | 2 +- vim-engine/build.gradle.kts | 2 ++ .../com/maddyhome/idea/vim/api/VimscriptParser.kt | 1 + .../vim/vimscript/parser/visitors/CommandVisitor.kt | 10 +++++----- .../vim/vimscript/parser/visitors/ExecutableVisitor.kt | 4 ++-- .../vim/vimscript/parser/visitors/ExpressionVisitor.kt | 4 ++-- .../vim/vimscript/parser/visitors/ScriptVisitor.kt | 4 ++-- .../vim/vimscript/parser/visitors/VisitorHelper.kt | 0 8 files changed, 15 insertions(+), 12 deletions(-) rename {src/main/java => vim-engine/src/main/kotlin}/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt (96%) rename {src/main/java => vim-engine/src/main/kotlin}/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt (98%) rename {src/main/java => vim-engine/src/main/kotlin}/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt (99%) rename {src/main/java => vim-engine/src/main/kotlin}/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt (87%) rename {src/main/java => vim-engine/src/main/kotlin}/com/maddyhome/idea/vim/vimscript/parser/visitors/VisitorHelper.kt (100%) diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/VimscriptParser.kt b/src/main/java/com/maddyhome/idea/vim/vimscript/parser/VimscriptParser.kt index 003c51fb87..f0f9210b7a 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/VimscriptParser.kt +++ b/src/main/java/com/maddyhome/idea/vim/vimscript/parser/VimscriptParser.kt @@ -96,7 +96,7 @@ internal object VimscriptParser : com.maddyhome.idea.vim.api.VimscriptParser { } } - fun parseLetCommand(text: String): Command? { + override fun parseLetCommand(text: String): Command? { val parser = getParser(addNewlineIfMissing(text), true) val AST: ParseTree = parser.letCommands() if (linesWithErrors.isNotEmpty()) { diff --git a/vim-engine/build.gradle.kts b/vim-engine/build.gradle.kts index f5284e33f7..b482373359 100644 --- a/vim-engine/build.gradle.kts +++ b/vim-engine/build.gradle.kts @@ -61,6 +61,8 @@ dependencies { compileOnly(project(":annotation-processors")) compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion") + implementation(kotlin("reflect")) + testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1") } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimscriptParser.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimscriptParser.kt index e32a3b63e9..9d34109776 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimscriptParser.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimscriptParser.kt @@ -17,6 +17,7 @@ public interface VimscriptParser { public val exCommands: ExCommandTree public fun parse(script: String): Script + public fun parseLetCommand(text: String): Command? public fun parseCommand(command: String): Command? public fun parseExpression(expression: String): Expression? } diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt similarity index 96% rename from src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt index 3d6613b8d0..53dd554f6b 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/CommandVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2003-2023 The IdeaVim authors + * Copyright 2003-2024 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at @@ -8,8 +8,8 @@ package com.maddyhome.idea.vim.vimscript.parser.visitors -import com.intellij.openapi.diagnostic.logger 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.Address import com.maddyhome.idea.vim.ex.ranges.Address.Companion.createRangeAddresses @@ -51,9 +51,9 @@ import kotlin.reflect.KClass import kotlin.reflect.full.createType import kotlin.reflect.full.primaryConstructor -internal object CommandVisitor : VimscriptBaseVisitor() { +public object CommandVisitor : VimscriptBaseVisitor() { - private val logger = logger() + private val logger = vimLogger() private val expressionVisitor: ExpressionVisitor = ExpressionVisitor private fun parseRangeOffset(ctx: RangeOffsetContext?): Int { @@ -249,7 +249,7 @@ internal object CommandVisitor : VimscriptBaseVisitor() { } override fun visitLetCommand(ctx: VimscriptParser.LetCommandContext): Command { - val command = com.maddyhome.idea.vim.vimscript.parser.VimscriptParser.parseLetCommand(ctx.text) ?: LetCommand(Range(), SimpleExpression(0), AssignmentOperator.ASSIGNMENT, SimpleExpression(0), false) + val command = injector.vimscriptParser.parseLetCommand(ctx.text) ?: LetCommand(Range(), SimpleExpression(0), AssignmentOperator.ASSIGNMENT, SimpleExpression(0), false) command.rangeInScript = ctx.getTextRange() return command } diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt similarity index 98% rename from src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt index 704eb3aa32..59a3c4918b 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ExecutableVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2003-2023 The IdeaVim authors + * Copyright 2003-2024 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at @@ -33,7 +33,7 @@ import com.maddyhome.idea.vim.vimscript.model.statements.loops.WhileLoop import com.maddyhome.idea.vim.parser.generated.VimscriptBaseVisitor import com.maddyhome.idea.vim.parser.generated.VimscriptParser -internal object ExecutableVisitor : VimscriptBaseVisitor() { +public object ExecutableVisitor : VimscriptBaseVisitor() { override fun visitBlockMember(ctx: VimscriptParser.BlockMemberContext): Executable? { return when { diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt similarity index 99% rename from src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt index c79060bec4..128d06ab38 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ExpressionVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2003-2023 The IdeaVim authors + * Copyright 2003-2024 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at @@ -57,7 +57,7 @@ import com.maddyhome.idea.vim.parser.generated.VimscriptParser.VariableExpressio import com.maddyhome.idea.vim.parser.generated.VimscriptParser.WrappedExpressionContext import org.antlr.v4.runtime.ParserRuleContext -internal object ExpressionVisitor : VimscriptBaseVisitor() { +public object ExpressionVisitor : VimscriptBaseVisitor() { override fun visitDictionaryExpression(ctx: DictionaryExpressionContext): Expression { val dict: LinkedHashMap = LinkedHashMap() diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt similarity index 87% rename from src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt index aa97822746..b225693e3c 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/parser/visitors/ScriptVisitor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2003-2023 The IdeaVim authors + * Copyright 2003-2024 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at @@ -12,7 +12,7 @@ import com.maddyhome.idea.vim.vimscript.model.Script import com.maddyhome.idea.vim.parser.generated.VimscriptBaseVisitor import com.maddyhome.idea.vim.parser.generated.VimscriptParser -internal object ScriptVisitor : VimscriptBaseVisitor