1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-06-17 06:40:00 +02:00
IntelliJ-IdeaVim/src/main/java/com/maddyhome/idea/vim/group/SearchGroup.java
2022-02-28 11:59:36 +03:00

1405 lines
56 KiB
Java

/*
* IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
* Copyright (C) 2003-2022 The IdeaVim authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.maddyhome.idea.vim.group;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.RoamingType;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
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.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.Trinity;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.common.CharacterPosition;
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.newapi.IjVimEditor;
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.ui.ex.ExEntryPanel;
import com.maddyhome.idea.vim.vimscript.model.VimLContext;
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType;
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString;
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression;
import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression;
import com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler;
import com.maddyhome.idea.vim.options.OptionChangeListener;
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser;
import com.maddyhome.idea.vim.options.OptionConstants;
import com.maddyhome.idea.vim.options.OptionScope;
import kotlin.Pair;
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.helper.HelperKt.localEditors;
import static com.maddyhome.idea.vim.helper.SearchHelperKtKt.shouldIgnoreCase;
@State(name = "VimSearchSettings", storages = {
@Storage(value = "$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)
})
public class SearchGroup implements PersistentStateComponent<Element> {
public SearchGroup() {
VimPlugin.getOptionService().addListener(
OptionConstants.hlsearchName,
new OptionChangeListener<VimDataType>() {
@Override
public void processGlobalValueChange(@Nullable VimDataType oldValue) {
resetShowSearchHighlight();
forceUpdateSearchHighlights();
}
},
false
);
final OptionChangeListener<VimDataType> updateHighlightsIfVisible =
new OptionChangeListener<VimDataType>() {
@Override
public void processGlobalValueChange(@Nullable VimDataType oldValue) {
if (showSearchHighlight) {
forceUpdateSearchHighlights();
}
}
};
VimPlugin.getOptionService().addListener(OptionConstants.ignorecaseName, updateHighlightsIfVisible, false);
VimPlugin.getOptionService().addListener(OptionConstants.smartcaseName, updateHighlightsIfVisible, false);
}
public void turnOn() {
updateSearchHighlights();
}
public void turnOff() {
final boolean show = showSearchHighlight;
clearSearchHighlight();
showSearchHighlight = show;
}
@TestOnly
public void resetState() {
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
*/
public @Nullable String getLastSearchPattern() {
return lastSearch;
}
/**
* Get the last pattern used in substitution.
* @return The pattern used for the last substitute command. Can be null
*/
public @Nullable String 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
*/
public @Nullable String getLastUsedPattern() {
switch (lastPatternIdx) {
case RE_SEARCH: return lastSearch;
case RE_SUBST: return lastSubstitute;
}
return null;
}
/**
* Get the last used search direction
*
* <p>This method is used in the AceJump integration plugin</p>
*
* @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
*
* <p>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.</p>
*
* <p>Also saves the text to the search register and history.</p>
*
* @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(RegisterGroup.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(HistoryGroup.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) {
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<TextRange> findAll(@NotNull Editor editor,
@NotNull String pattern,
int startLine,
int endLine,
boolean ignoreCase) {
return SearchHelper.findAll(editor, pattern, startLine, endLine, ignoreCase);
}
/**
* Process the search command, searching for the pattern from the given document offset
*
* <p>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.</p>
*
* <p>Will parse the entire command, including patterns separated by `;`</p>
*
* <p>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.</p>
*
* <p>Equivalent to normal.c:nv_search + search.c:do_search</p>
*
* @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 Offset to the next occurrence of the pattern or -1 if not found
*/
public int processSearchCommand(@NotNull Editor editor, @NotNull String command, int startOffset, @NotNull Direction dir) {
boolean isNewPattern = false;
String pattern = null;
String patternOffset = null;
final char type = dir == Direction.FORWARDS ? '/' : '?';
if (command.length() > 0) {
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) {
if (end.charAt() == type) {
end.inc();
patternOffset = end.toString();
} else {
logger.debug("no offset");
patternOffset = "";
}
}
else {
p.inc();
patternOffset = p.toString();
if (logger.isDebugEnabled()) logger.debug("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.length() == 0) {
pattern = getLastSearchPattern();
patternOffset = lastPatternOffset;
if (pattern == null || pattern.length() == 0) {
isNewPattern = true;
pattern = getLastSubstitutePattern();
if (pattern == null || pattern.length() == 0) {
VimPlugin.showMessage(MessageHelper.message("e_noprevre"));
return -1;
}
}
}
// 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(editor, startOffset, 1, lastDir);
}
/**
* Process the pattern being used as a search range
*
* <p>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.</p>
*
* <p>This method is essentially a wrapper around SearchHelper.findPattern (via findItOffset) that updates state and
* highlighting.</p>
*
* @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 int 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;
}
/**
* Search for the word under the given caret
*
* <p>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.</p>
*
* <p>Equivalent to normal.c:nv_ident</p>
*
* @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
*/
public int searchWord(@NotNull Editor editor, @NotNull Caret caret, int count, boolean whole, Direction dir) {
TextRange range = SearchHelper.findWordUnderCursor(editor, caret);
if (range == null) {
logger.warn("No range was found");
return -1;
}
final String pattern = SearchHelper.makeSearchPattern(EditorHelper.getText(editor, range.getStartOffset(), range.getEndOffset()), 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 int offset = findItOffset(editor, range.getStartOffset(), count, lastDir);
return offset == -1 ? range.getStartOffset() : offset;
}
/**
* Find the next occurrence of the last used pattern
*
* <p>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.</p>
*
* @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
*/
public int searchNext(@NotNull Editor editor, @NotNull Caret caret, int count) {
return searchNextWithDirection(editor, caret, count, lastDir);
}
/**
* Find the next occurrence of the last used pattern
*
* <p>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.</p>
*
* @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
*/
public int searchPrevious(@NotNull Editor editor, @NotNull Caret caret, int count) {
return searchNextWithDirection(editor, caret, 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();
int offset = findItOffset(editor, startOffset, count, dir);
if (offset == 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. */
offset = findItOffset(editor, startOffset, count + 1, dir);
}
return offset;
}
// *******************************************************************************************************************
//
// Substitute
//
// *******************************************************************************************************************
/**
* Parse and execute the substitute command
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>See ex_cmds.c:ex_substitute</p>
*
* @param editor The editor to search in
* @param caret The caret to use for initial search offset, and to move for interactive substitution
* @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
*/
@RWLockLabel.SelfSynchronized
public boolean processSubstituteCommand(@NotNull Editor editor, @NotNull Caret caret, @NotNull LineRange range,
@NotNull @NonNls String excmd, @NonNls String exarg, @NotNull VimLContext parent) {
// Explicitly exit visual mode here, so that visual mode marks don't change when we move the cursor to a match.
List<ExException> exceptions = new ArrayList<>();
if (CommandStateHelper.inVisualMode(editor)) {
ModeHelper.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 = VimPlugin.getOptionService().isSet(new OptionScope.LOCAL(new IjVimEditor(editor)), OptionConstants.gdefaultName, OptionConstants.gdefaultName);
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 = EditorHelper.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<Boolean, Trinity<RegExp.regmmatch_T, String, RegExp>> 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 = booleanregmmatch_tPair.getSecond().getFirst();
String pattern = booleanregmmatch_tPair.getSecond().getSecond();
RegExp sp = 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 = editor.getDocument().getLineStartOffset(line1);
int end = editor.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 = EditorHelper.getLineCount(editor);
Expression expression = null;
int latestOff = -1;
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) {
VimPlugin.getMark().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(editor);
int endoff = endpos.toOffset(editor);
int newend = startoff + match.length();
if (do_all || line != lastLine) {
boolean doReplace = true;
if (do_ask) {
RangeHighlighter hl = SearchHighlightsHelper.addSubstitutionConfirmationHighlight(editor, startoff, endoff);
final ReplaceConfirmationChoice choice = confirmChoice(editor, match, caret, startoff);
editor.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 && startoff != latestOff) {
latestOff = startoff;
SubmatchFunctionHandler.INSTANCE.setLatestMatch(editor.getDocument().getText(new com.intellij.openapi.util.TextRange(startoff, endoff)));
MotionGroup.moveCaret(editor, caret, startoff);
if (expression != null) {
try {
match = expression
.evaluate(editor, EditorDataContext.init(editor, null), parent)
.toInsertableString();
} catch (Exception e) {
exceptions.add((ExException) e);
match = "";
}
}
String finalMatch = match;
ApplicationManager.getApplication().runWriteAction(() -> editor.getDocument().replaceString(startoff, endoff,
finalMatch));
lastMatch = startoff;
newpos = CharacterPosition.Companion.fromOffset(editor, newend);
lnum += newpos.line - endpos.line;
line2 += newpos.line - endpos.line;
}
}
lastLine = line;
lnum += nmatch - 1;
if (do_all && startoff != endoff) {
if (newpos != null) {
lnum = newpos.line;
searchcol = newpos.column;
}
else {
searchcol = endpos.column;
}
}
else {
searchcol = 0;
lnum++;
}
}
else {
lnum++;
searchcol = 0;
}
}
if (!got_quit) {
if (lastMatch != -1) {
MotionGroup.moveCaret(editor, caret,
VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, editor.offsetToLogicalPosition(lastMatch).line));
}
else {
VimPlugin.showMessage(MessageHelper.message(Msg.e_patnotf2, pattern));
}
}
SubmatchFunctionHandler.INSTANCE.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;
}
public Pair<Boolean, Trinity<RegExp.regmmatch_T, String, RegExp>> 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 = null;
switch (which_pat) {
case RE_SEARCH:
pattern = lastSearch;
errorMessage = MessageHelper.message("e_nopresub");
break;
case RE_SUBST:
pattern = lastSubstitute;
errorMessage = MessageHelper.message("e_noprevre");
break;
}
// 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 Trinity<>(regmatch, pattern, sp));
}
private static @NotNull ReplaceConfirmationChoice confirmChoice(@NotNull Editor editor, @NotNull String match, @NotNull Caret caret, int startoff) {
final Ref<ReplaceConfirmationChoice> result = Ref.create(ReplaceConfirmationChoice.QUIT);
final Function1<KeyStroke, Boolean> keyStrokeProcessor = key -> {
final ReplaceConfirmationChoice choice;
final char c = key.getKeyChar();
if (StringHelper.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 <C-E> and <C-Y>
result.set(choice);
return false;
};
if (ApplicationManager.getApplication().isUnitTestMode()) {
MotionGroup.moveCaret(editor, caret, 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 ExEntryPanel exEntryPanel = ExEntryPanel.getInstanceWithoutShortcuts();
exEntryPanel.activate(editor, EditorDataContext.init(editor, null), MessageHelper.message("replace.with.0", match), "", 1);
MotionGroup.moveCaret(editor, caret, startoff);
ModalEntry.INSTANCE.activate(keyStrokeProcessor);
exEntryPanel.deactivate(true, false);
}
return result.get();
}
// *******************************************************************************************************************
//
// gn implementation
//
// *******************************************************************************************************************
/**
* Find the range of the next occurrence of the last used search pattern
*
* <p>Used for the implementation of the gn and gN commands.</p>
*
* <p>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.</p>
*
* @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
*/
public @Nullable TextRange getNextSearchRange(@NotNull Editor editor, int count, boolean forwards) {
editor.getCaretModel().removeSecondaryCarets();
TextRange current = findUnderCaret(editor);
if (current == null || CommandStateHelper.inVisualMode(editor) && atEdgeOfGnRange(current, editor, 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 Editor editor, int count, boolean forwards) {
if (forwards) {
final EnumSet<SearchOptions> searchOptions = EnumSet.of(SearchOptions.WRAP, SearchOptions.WHOLE_FILE);
return SearchHelper.findPattern(editor, getLastUsedPattern(), editor.getCaretModel().getOffset(), count, searchOptions);
} else {
return searchBackward(editor, editor.getCaretModel().getOffset(), count);
}
}
private @Nullable TextRange findUnderCaret(@NotNull Editor editor) {
final TextRange backSearch = searchBackward(editor, editor.getCaretModel().getOffset() + 1, 1);
if (backSearch == null) return null;
return backSearch.contains(editor.getCaretModel().getOffset()) ? backSearch : null;
}
private @Nullable TextRange searchBackward(@NotNull Editor editor, int offset, int count) {
// Backward search returns wrongs end offset for some cases. That's why we should perform additional forward search
final EnumSet<SearchOptions> searchOptions = EnumSet.of(SearchOptions.WRAP, SearchOptions.WHOLE_FILE, SearchOptions.BACKWARDS);
final TextRange foundBackward = SearchHelper.findPattern(editor, getLastUsedPattern(), offset, count, searchOptions);
if (foundBackward == null) return null;
int startOffset = foundBackward.getStartOffset() - 1;
if (startOffset < 0) startOffset = EditorHelperRt.getFileSize(editor);
searchOptions.remove(SearchOptions.BACKWARDS);
return SearchHelper.findPattern(editor, getLastUsedPattern(), startOffset, 1, searchOptions);
}
// *******************************************************************************************************************
//
// Highlighting
//
// *******************************************************************************************************************
//region Search highlights
public void clearSearchHighlight() {
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.
*/
public void resetIncsearchHighlights() {
SearchHighlightsHelper.updateSearchHighlights(getLastUsedPattern(), lastIgnoreSmartCase, showSearchHighlight, true);
}
private void resetShowSearchHighlight() {
showSearchHighlight = VimPlugin.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.hlsearchName, OptionConstants.hlsearchName);
}
private void highlightSearchLines(@NotNull Editor editor, int startLine, int endLine) {
final String pattern = getLastUsedPattern();
if (pattern != null) {
final List<TextRange> results = SearchHelper.findAll(editor, pattern, startLine, endLine,
shouldIgnoreCase(pattern, lastIgnoreSmartCase));
SearchHighlightsHelper.highlightSearchResults(editor, pattern, results, -1);
}
}
/**
* Updates search highlights when the selected editor changes
*/
public static void fileEditorManagerSelectionChangedCallback(@SuppressWarnings("unused") @NotNull FileEditorManagerEvent event) {
VimPlugin.getSearch().updateSearchHighlights();
}
/**
* 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) {
for (Project project : ProjectManager.getInstance().getOpenProjects()) {
final Document document = event.getDocument();
for (Editor editor : localEditors(document, project)) {
Collection<RangeHighlighter> hls = UserDataManager.getVimLastHighlighters(editor);
if (hls == null) {
continue;
}
if (logger.isDebugEnabled()) {
logger.debug("hls=" + hls);
logger.debug("event=" + event);
}
// We can only re-highlight whole lines, so clear any highlights in the affected 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);
final Iterator<RangeHighlighter> iter = hls.iterator();
while (iter.hasNext()) {
final RangeHighlighter highlighter = iter.next();
if (!highlighter.isValid() || (highlighter.getStartOffset() >= startLineOffset && highlighter.getEndOffset() <= endLineOffset)) {
iter.remove();
editor.getMarkupModel().removeHighlighter(highlighter);
}
}
VimPlugin.getSearch().highlightSearchLines(editor, startPosition.line, endPosition.line);
if (logger.isDebugEnabled()) {
hls = UserDataManager.getVimLastHighlighters(editor);
logger.debug("sl=" + startPosition.line + ", el=" + endPosition.line);
logger.debug("hls=" + hls);
}
}
}
}
}
//endregion
// *******************************************************************************************************************
//
// Implementation details
//
// *******************************************************************************************************************
/**
* Searches for the RE_LAST saved pattern, applying the last saved pattern offset. Will loop over trailing search
* commands.
*
* <p>Make sure that RE_LAST has been updated before calling this. wrapscan and ignorecase come from options.</p>
*
* <p>See search.c:do_search (and a little bit of normal.c:normal_search)</p>
*
* @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 The offset to the occurrence or -1 if not found
*/
private int findItOffset(@NotNull Editor editor, int startOffset, int count, Direction dir) {
boolean wrap = VimPlugin.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.wrapscanName, OptionConstants.wrapscanName);
logger.debug("Perform search. Direction: " + dir + " wrap: " + wrap);
int offset = 0;
boolean offsetIsLineOffset = false;
boolean hasEndOffset = false;
ParsePosition pp = new ParsePosition(0);
if (lastPatternOffset.length() > 0) {
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';
}
}
/*
* 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> 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 = SearchHelper.findPattern(editor, getLastUsedPattern(), startOffset, count, searchOptions);
if (range == null) {
logger.warn("No range is found");
return -1;
}
int res = range.getStartOffset();
if (offsetIsLineOffset) {
int line = editor.offsetToLogicalPosition(range.getStartOffset()).line;
int newLine = EditorHelper.normalizeLine(editor, line + offset);
// TODO: Don't move the caret!
res = VimPlugin.getMotion().moveCaretToLineStart(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 res;
}
if (lastPatternOffset.length() - ppos > 2) {
ppos++;
}
res = processSearchCommand(editor, lastPatternOffset.substring(ppos + 1), res, nextDir);
}
return res;
}
// *******************************************************************************************************************
//
// Persistent state
//
// *******************************************************************************************************************
//region Persistent state
public void saveData(@NotNull Element element) {
logger.debug("saveData");
Element search = new Element("search");
addOptionalTextElement(search, "last-search", lastSearch);
addOptionalTextElement(search, "last-substitute", lastSubstitute);
addOptionalTextElement(search, "last-offset", lastPatternOffset.length() > 0 ? lastPatternOffset : null);
addOptionalTextElement(search, "last-replace", lastReplace);
addOptionalTextElement(search, "last-pattern", lastPatternIdx == RE_SEARCH ? lastSearch : lastSubstitute);
addOptionalTextElement(search, "last-dir", Integer.toString(lastDir.toInt()));
addOptionalTextElement(search, "show-last", Boolean.toString(showSearchHighlight));
element.addContent(search);
}
private static void addOptionalTextElement(@NotNull Element element, @NotNull String name, @Nullable String text) {
if (text != null) {
element.addContent(StringHelper.setSafeXmlText(new Element(name), text));
}
}
public void readData(@NotNull Element element) {
logger.debug("readData");
Element search = element.getChild("search");
if (search == null) {
return;
}
lastSearch = getSafeChildText(search, "last-search");
lastSubstitute = getSafeChildText(search, "last-substitute");
lastReplace = getSafeChildText(search, "last-replace");
lastPatternOffset = getSafeChildText(search, "last-offset", "");
final String lastPatternText = getSafeChildText(search, "last-pattern");
if (lastPatternText == null || lastPatternText.equals(lastSearch)) {
lastPatternIdx = RE_SEARCH;
}
else {
lastPatternIdx = RE_SUBST;
}
Element dir = search.getChild("last-dir");
try {
lastDir = Direction.Companion.fromInt(Integer.parseInt(dir.getText()));
}
catch (NumberFormatException e) {
lastDir = Direction.FORWARDS;
}
Element show = search.getChild("show-last");
final String vimInfo = ((VimString) VimPlugin.getOptionService().getOptionValue(OptionScope.GLOBAL.INSTANCE, OptionConstants.viminfoName, OptionConstants.viminfoName)).getValue();
final boolean disableHighlight = Set.of(vimInfo.split(",")).contains("h");
showSearchHighlight = !disableHighlight && Boolean.parseBoolean(show.getText());
if (logger.isDebugEnabled()) {
logger.debug("show=" + show + "(" + show.getText() + ")");
logger.debug("showSearchHighlight=" + showSearchHighlight);
}
}
private static @Nullable String getSafeChildText(@NotNull Element element, @NotNull String name) {
final Element child = element.getChild(name);
return child != null ? StringHelper.getSafeXmlText(child) : null;
}
@SuppressWarnings("SameParameterValue")
private static @NotNull String getSafeChildText(@NotNull Element element, @NotNull String name, @NotNull String defaultValue) {
final Element child = element.getChild(name);
if (child != null) {
final String value = StringHelper.getSafeXmlText(child);
return value != null ? value : defaultValue;
}
return defaultValue;
}
@Nullable
@Override
public Element getState() {
Element element = new Element("search");
saveData(element);
return element;
}
@Override
public void loadState(@NotNull Element state) {
readData(state);
}
//endregion
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 - ~<magic><smartcase><line><end><off>[~]
// 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 = VimPlugin.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.hlsearchName, OptionConstants.hlsearchName);
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());
}