/* * 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()); }