1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-30 04:34:08 +02:00
IntelliJ-IdeaVim/src/main/java/com/maddyhome/idea/vim/group/MarkGroup.java
2023-01-10 10:09:25 +02:00

417 lines
16 KiB
Java
Executable File

/*
* 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.ide.bookmark.BookmarkGroup;
import com.intellij.ide.bookmark.BookmarkType;
import com.intellij.ide.bookmark.BookmarksManager;
import com.intellij.ide.bookmark.LineBookmark;
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.Document;
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.event.EditorFactoryEvent;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.VimEditor;
import com.maddyhome.idea.vim.api.VimInjectorKt;
import com.maddyhome.idea.vim.helper.EditorHelper;
import com.maddyhome.idea.vim.helper.HelperKt;
import com.maddyhome.idea.vim.mark.*;
import com.maddyhome.idea.vim.newapi.IjVimEditor;
import com.maddyhome.idea.vim.options.OptionScope;
import com.maddyhome.idea.vim.vimscript.services.IjVimOptionService;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.maddyhome.idea.vim.mark.VimMarkConstants.GLOBAL_MARKS;
import static com.maddyhome.idea.vim.mark.VimMarkConstants.SAVE_FILE_MARKS;
/**
* This class contains all the mark related functionality
*/
@State(name = "VimMarksSettings", storages = {
@Storage(value = "$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)})
public class MarkGroup extends VimMarkGroupBase implements PersistentStateComponent<Element> {
public void editorReleased(@NotNull EditorFactoryEvent event) {
// Save off the last caret position of the file before it is closed
Editor editor = event.getEditor();
setMark(new IjVimEditor(editor), '"', editor.getCaretModel().getOffset());
}
@Override
public void includeCurrentCommandAsNavigation(@NotNull VimEditor editor) {
Project project = ((IjVimEditor)editor).getEditor().getProject();
if (project != null) {
IdeDocumentHistory.getInstance(project).includeCurrentCommandAsNavigation();
}
}
@Override
public @Nullable Mark createSystemMark(char ch, int line, int col, @NotNull VimEditor editor) {
Editor ijEditor = ((IjVimEditor)editor).getEditor();
@Nullable LineBookmark systemMark = SystemMarks.createOrGetSystemMark(ch, line, ijEditor);
if (systemMark == null) {
return null;
}
return new IntellijMark(systemMark, col, ijEditor.getProject());
}
/**
* Gets the map of marks for the specified file
*
* @param doc The editor to get the marks for
* @return The map of marks. The keys are <code>Character</code>s of the mark names, the values are
* <code>Mark</code>s.
*/
private @Nullable FileMarks<Character, Mark> getFileMarks(final @NotNull Document doc) {
VirtualFile vf = FileDocumentManager.getInstance().getFile(doc);
if (vf == null) {
return null;
}
return getFileMarks(vf.getPath());
}
private @Nullable HashMap<Character, Mark> getAllFileMarks(final @NotNull Document doc) {
VirtualFile vf = FileDocumentManager.getInstance().getFile(doc);
if (vf == null) {
return null;
}
HashMap<Character, Mark> res = new HashMap<>();
FileMarks<Character, Mark> fileMarks = getFileMarks(doc);
if (fileMarks != null) {
res.putAll(fileMarks);
}
for (Character ch : globalMarks.keySet()) {
Mark mark = globalMarks.get(ch);
if (vf.getPath().equals(mark.getFilename())) {
res.put(ch, mark);
}
}
return res;
}
public void saveData(@NotNull Element element) {
Element marksElem = new Element("globalmarks");
if (!VimPlugin.getOptionService()
.isSet(OptionScope.GLOBAL.INSTANCE, IjVimOptionService.ideamarksName, IjVimOptionService.ideamarksName)) {
for (Mark mark : globalMarks.values()) {
if (!mark.isClear()) {
Element markElem = new Element("mark");
markElem.setAttribute("key", Character.toString(mark.getKey()));
markElem.setAttribute("line", Integer.toString(mark.getLine()));
markElem.setAttribute("column", Integer.toString(mark.getCol()));
markElem.setAttribute("filename", StringUtil.notNullize(mark.getFilename()));
markElem.setAttribute("protocol", StringUtil.notNullize(mark.getProtocol(), "file"));
marksElem.addContent(markElem);
if (logger.isDebugEnabled()) {
logger.debug("saved mark = " + mark);
}
}
}
}
element.addContent(marksElem);
Element fileMarksElem = new Element("filemarks");
List<FileMarks<Character, Mark>> files = new ArrayList<>(fileMarks.values());
files.sort(Comparator.comparing(FileMarks<Character, Mark>::getMyTimestamp));
if (files.size() > SAVE_MARK_COUNT) {
files = files.subList(files.size() - SAVE_MARK_COUNT, files.size());
}
for (String file : fileMarks.keySet()) {
FileMarks<Character, Mark> marks = fileMarks.get(file);
if (!files.contains(marks)) {
continue;
}
if (marks.size() > 0) {
Element fileMarkElem = new Element("file");
fileMarkElem.setAttribute("name", file);
fileMarkElem.setAttribute("timestamp", Long.toString(marks.getMyTimestamp().getTime()));
for (Mark mark : marks.values()) {
if (!mark.isClear() && !Character.isUpperCase(mark.getKey()) && SAVE_FILE_MARKS.indexOf(mark.getKey()) >= 0) {
Element markElem = new Element("mark");
markElem.setAttribute("key", Character.toString(mark.getKey()));
markElem.setAttribute("line", Integer.toString(mark.getLine()));
markElem.setAttribute("column", Integer.toString(mark.getCol()));
fileMarkElem.addContent(markElem);
}
}
fileMarksElem.addContent(fileMarkElem);
}
}
element.addContent(fileMarksElem);
Element jumpsElem = new Element("jumps");
for (Jump jump : jumps) {
Element jumpElem = new Element("jump");
jumpElem.setAttribute("line", Integer.toString(jump.getLine()));
jumpElem.setAttribute("column", Integer.toString(jump.getCol()));
jumpElem.setAttribute("filename", StringUtil.notNullize(jump.getFilepath()));
jumpsElem.addContent(jumpElem);
if (logger.isDebugEnabled()) {
logger.debug("saved jump = " + jump);
}
}
element.addContent(jumpsElem);
}
public void readData(@NotNull Element element) {
// We need to keep the filename for now and create the virtual file later. Any attempt to call
// LocalFileSystem.getInstance().findFileByPath() results in the following error:
// Read access is allowed from event dispatch thread or inside read-action only
// (see com.intellij.openapi.application.Application.runReadAction())
Element marksElem = element.getChild("globalmarks");
if (marksElem != null &&
!VimPlugin.getOptionService()
.isSet(OptionScope.GLOBAL.INSTANCE, IjVimOptionService.ideamarksName, IjVimOptionService.ideamarksName)) {
List<Element> markList = marksElem.getChildren("mark");
for (Element aMarkList : markList) {
Mark mark = VimMark.create(aMarkList.getAttributeValue("key").charAt(0),
Integer.parseInt(aMarkList.getAttributeValue("line")),
Integer.parseInt(aMarkList.getAttributeValue("column")),
aMarkList.getAttributeValue("filename"), aMarkList.getAttributeValue("protocol"));
if (mark != null) {
globalMarks.put(mark.getKey(), mark);
HashMap<Character, Mark> fmarks = getFileMarks(mark.getFilename());
fmarks.put(mark.getKey(), mark);
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("globalMarks=" + globalMarks);
}
Element fileMarksElem = element.getChild("filemarks");
if (fileMarksElem != null) {
List<Element> fileList = fileMarksElem.getChildren("file");
for (Element aFileList : fileList) {
String filename = aFileList.getAttributeValue("name");
Date timestamp = new Date();
try {
long date = Long.parseLong(aFileList.getAttributeValue("timestamp"));
timestamp.setTime(date);
}
catch (NumberFormatException e) {
// ignore
}
FileMarks<Character, Mark> fmarks = getFileMarks(filename);
List<Element> markList = aFileList.getChildren("mark");
for (Element aMarkList : markList) {
Mark mark = VimMark.create(aMarkList.getAttributeValue("key").charAt(0),
Integer.parseInt(aMarkList.getAttributeValue("line")),
Integer.parseInt(aMarkList.getAttributeValue("column")), filename,
aMarkList.getAttributeValue("protocol"));
if (mark != null) fmarks.put(mark.getKey(), mark);
}
fmarks.setTimestamp(timestamp);
}
}
if (logger.isDebugEnabled()) {
logger.debug("fileMarks=" + fileMarks);
}
jumps.clear();
Element jumpsElem = element.getChild("jumps");
if (jumpsElem != null) {
List<Element> jumpList = jumpsElem.getChildren("jump");
for (Element aJumpList : jumpList) {
Jump jump = new Jump(Integer.parseInt(aJumpList.getAttributeValue("line")),
Integer.parseInt(aJumpList.getAttributeValue("column")),
aJumpList.getAttributeValue("filename"));
jumps.add(jump);
}
}
if (logger.isDebugEnabled()) {
logger.debug("jumps=" + jumps);
}
}
@Nullable
@Override
public Element getState() {
Element element = new Element("marks");
saveData(element);
return element;
}
@Override
public void loadState(@NotNull Element state) {
readData(state);
}
/**
* This class is used to listen to editor document changes
*/
public static class MarkUpdater implements DocumentListener {
public static MarkUpdater INSTANCE = new MarkUpdater();
/**
* Creates the listener for the supplied editor
*/
private MarkUpdater() {
}
/**
* This event indicates that a document is about to be changed. We use this event to update all the
* editor's marks if text is about to be deleted.
*
* @param event The change event
*/
@Override
public void beforeDocumentChange(@NotNull DocumentEvent event) {
if (!VimPlugin.isEnabled()) return;
if (logger.isDebugEnabled()) logger.debug("MarkUpdater before, event = " + event);
if (event.getOldLength() == 0) return;
Document doc = event.getDocument();
Editor anEditor = getAnEditor(doc);
VimInjectorKt.getInjector().getMarkGroup()
.updateMarkFromDelete(anEditor == null ? null : new IjVimEditor(anEditor),
VimPlugin.getMark().getAllFileMarks(doc), event.getOffset(), event.getOldLength());
// TODO - update jumps
}
/**
* This event indicates that a document was just changed. We use this event to update all the editor's
* marks if text was just added.
*
* @param event The change event
*/
@Override
public void documentChanged(@NotNull DocumentEvent event) {
if (!VimPlugin.isEnabled()) return;
if (logger.isDebugEnabled()) logger.debug("MarkUpdater after, event = " + event);
if (event.getNewLength() == 0 || (event.getNewLength() == 1 && event.getNewFragment().charAt(0) != '\n')) return;
Document doc = event.getDocument();
Editor anEditor = getAnEditor(doc);
VimInjectorKt.getInjector().getMarkGroup()
.updateMarkFromInsert(anEditor == null ? null : new IjVimEditor(anEditor),
VimPlugin.getMark().getAllFileMarks(doc), event.getOffset(), event.getNewLength());
// TODO - update jumps
}
private @Nullable Editor getAnEditor(@NotNull Document doc) {
List<Editor> editors = HelperKt.localEditors(doc);
if (editors.size() > 0) {
return editors.get(0);
}
else {
return null;
}
}
}
public static class VimBookmarksListener implements com.intellij.ide.bookmark.BookmarksListener {
private final Project myProject;
public VimBookmarksListener(Project project) {
myProject = project;
}
@Override
public void bookmarkAdded(@NotNull BookmarkGroup group, com.intellij.ide.bookmark.@NotNull Bookmark bookmark) {
if (!VimPlugin.isEnabled()) return;
if (!VimPlugin.getOptionService()
.isSet(OptionScope.GLOBAL.INSTANCE, IjVimOptionService.ideamarksName, IjVimOptionService.ideamarksName)) {
return;
}
if (!(bookmark instanceof LineBookmark)) return;
BookmarksManager bookmarksManager = BookmarksManager.getInstance(myProject);
if (bookmarksManager == null) return;
BookmarkType type = bookmarksManager.getType(bookmark);
if (type == null) return;
char mnemonic = type.getMnemonic();
if (GLOBAL_MARKS.indexOf(mnemonic) == -1) return;
createVimMark((LineBookmark)bookmark, mnemonic);
}
@Override
public void bookmarkRemoved(@NotNull BookmarkGroup group, com.intellij.ide.bookmark.@NotNull Bookmark bookmark) {
if (!VimPlugin.isEnabled()) return;
if (!VimPlugin.getOptionService()
.isSet(OptionScope.GLOBAL.INSTANCE, IjVimOptionService.ideamarksName, IjVimOptionService.ideamarksName)) {
return;
}
if (!(bookmark instanceof LineBookmark)) return;
BookmarksManager bookmarksManager = BookmarksManager.getInstance(myProject);
if (bookmarksManager == null) return;
BookmarkType type = bookmarksManager.getType(bookmark);
if (type == null) return;
char ch = type.getMnemonic();
if (GLOBAL_MARKS.indexOf(ch) != -1) {
FileMarks<Character, Mark> fmarks =
VimPlugin.getMark().getFileMarks(((LineBookmark)bookmark).getFile().getPath());
fmarks.remove(ch);
VimPlugin.getMark().globalMarks.remove(ch);
}
}
private void createVimMark(@NotNull LineBookmark b, char mnemonic) {
int col = 0;
Editor editor = EditorHelper.getEditor(b.getFile());
if (editor != null) col = editor.getCaretModel().getCurrentCaret().getLogicalPosition().column;
IntellijMark mark = new IntellijMark(b, col, myProject);
FileMarks<Character, Mark> fmarks = VimPlugin.getMark().getFileMarks(b.getFile().getPath());
fmarks.put(mnemonic, mark);
VimPlugin.getMark().globalMarks.put(mnemonic, mark);
}
}
/**
* COMPATIBILITY-LAYER: Method added
* Please see: <a href="https://jb.gg/zo8n0r">doc</a>
*
* @deprecated Please use method with VimEditor
*/
@Deprecated
public void saveJumpLocation(Editor editor) {
this.saveJumpLocation(new IjVimEditor(editor));
}
private static final int SAVE_MARK_COUNT = 20;
private static final Logger logger = Logger.getInstance(MarkGroup.class.getName());
}