From 06ef1c1182b42c01080f5b48b164888cc8c99abf Mon Sep 17 00:00:00 2001 From: filipp Date: Thu, 26 Oct 2023 10:23:16 +0300 Subject: [PATCH] VIM-1639 Ctrl-o and Ctrl-i jumping in files of different projects --- .../maddyhome/idea/vim/group/MotionGroup.kt | 6 +- .../idea/vim/group/VimJumpServiceImpl.kt | 68 +++++++++++------- .../maddyhome/idea/vim/newapi/IjVimEditor.kt | 7 ++ .../com/maddyhome/idea/vim/api/VimEditor.kt | 3 + .../maddyhome/idea/vim/api/VimJumpService.kt | 61 +++++++++++++--- .../idea/vim/api/VimJumpServiceBase.kt | 70 +++++++------------ .../idea/vim/api/VimMarkServiceBase.kt | 6 +- .../vimscript/model/commands/JumpsCommand.kt | 6 +- 8 files changed, 139 insertions(+), 88 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt index 6922787642..b5043497a9 100755 --- a/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt @@ -33,6 +33,8 @@ import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimMotionGroupBase import com.maddyhome.idea.vim.api.addJump import com.maddyhome.idea.vim.api.anyNonWhitespace +import com.maddyhome.idea.vim.api.getJump +import com.maddyhome.idea.vim.api.getJumpSpot import com.maddyhome.idea.vim.api.getLeadingCharacterOffset import com.maddyhome.idea.vim.api.getVisualLineCount import com.maddyhome.idea.vim.api.injector @@ -163,8 +165,8 @@ internal class MotionGroup : VimMotionGroupBase() { override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion { val jumpService = injector.jumpService - val spot = jumpService.getJumpSpot() - val (line, col, fileName) = jumpService.getJump(count) ?: return Motion.Error + val spot = jumpService.getJumpSpot(editor) + val (line, col, fileName) = jumpService.getJump(editor, count) ?: return Motion.Error val vf = EditorHelper.getVirtualFile(editor.ij) ?: return Motion.Error val lp = BufferPosition(line, col, false) val lpNative = LogicalPosition(line, col, false) diff --git a/src/main/java/com/maddyhome/idea/vim/group/VimJumpServiceImpl.kt b/src/main/java/com/maddyhome/idea/vim/group/VimJumpServiceImpl.kt index 077740d245..e8f668501a 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/VimJumpServiceImpl.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/VimJumpServiceImpl.kt @@ -15,6 +15,7 @@ import com.intellij.openapi.components.Storage import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.PlaceInfo import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.RecentPlacesListener +import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimJumpServiceBase @@ -26,6 +27,7 @@ import com.maddyhome.idea.vim.newapi.globalIjOptions import com.maddyhome.idea.vim.newapi.ij import org.jdom.Element + @State(name = "VimJumpsSettings", storages = [Storage(value = "\$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)]) internal class VimJumpServiceImpl : VimJumpServiceBase(), PersistentStateComponent { companion object { @@ -40,60 +42,72 @@ internal class VimJumpServiceImpl : VimJumpServiceBase(), PersistentStateCompone } } + // We do not delete old project records. + // Rationale: It's more likely that users will want to review their old projects and access their jump history + // (e.g., recent files), than for the 100 jumps (max number of records) to consume enough space to be noticeable. override fun getState(): Element { - val jumpsElem = Element("jumps") - for (jump in jumps) { - val jumpElem = Element("jump") - jumpElem.setAttribute("line", jump.line.toString()) - jumpElem.setAttribute("column", jump.col.toString()) - jumpElem.setAttribute("filename", StringUtil.notNullize(jump.filepath)) - jumpsElem.addContent(jumpElem) - if (logger.isDebug()) { - logger.debug("saved jump = $jump") + val projectsElem = Element("projects") + for ((project, jumps) in projectToJumps) { + val projectElement = Element("project").setAttribute("id", project) + for (jump in jumps) { + val jumpElem = Element("jump") + jumpElem.setAttribute("line", jump.line.toString()) + jumpElem.setAttribute("column", jump.col.toString()) + jumpElem.setAttribute("filename", StringUtil.notNullize(jump.filepath)) + projectElement.addContent(jumpElem) + if (logger.isDebug()) { + logger.debug("saved jump = $jump") + } } + projectsElem.addContent(projectElement) } - return jumpsElem + return projectsElem } override fun loadState(state: Element) { - val jumpList = state.getChildren("jump") - for (jumpElement in jumpList) { - val jump = Jump( - Integer.parseInt(jumpElement.getAttributeValue("line")), - Integer.parseInt(jumpElement.getAttributeValue("column")), - jumpElement.getAttributeValue("filename"), - ) - jumps.add(jump) - } - - if (logger.isDebug()) { - logger.debug("jumps=$jumps") + val projectElements = state.getChildren("project") + for (projectElement in projectElements) { + val jumps = mutableListOf() + val jumpElements = projectElement.getChildren("jump") + for (jumpElement in jumpElements) { + val jump = Jump( + Integer.parseInt(jumpElement.getAttributeValue("line")), + Integer.parseInt(jumpElement.getAttributeValue("column")), + jumpElement.getAttributeValue("filename"), + ) + jumps.add(jump) + } + if (logger.isDebug()) { + logger.debug("jumps=$jumps") + } + val projectId = projectElement.getAttributeValue("id") + projectToJumps[projectId] = jumps } } } -internal class JumpsListener : RecentPlacesListener { +internal class JumpsListener(val project: Project) : RecentPlacesListener { override fun recentPlaceAdded(changePlace: PlaceInfo, isChanged: Boolean) { if (!injector.globalIjOptions().unifyjumps) return - + val jumpService = injector.jumpService if (!isChanged) { if (changePlace.timeStamp < jumpService.lastJumpTimeStamp) return // this listener is notified asynchronously, and // we do not want jumps that were processed before val jump = buildJump(changePlace) ?: return - jumpService.addJump(jump, true) + jumpService.addJump(project.basePath ?: IjVimEditor.DEFAULT_PROJECT_ID, jump, true) } } override fun recentPlaceRemoved(changePlace: PlaceInfo, isChanged: Boolean) { if (!injector.globalIjOptions().unifyjumps) return - + val jumpService = injector.jumpService if (!isChanged) { if (changePlace.timeStamp < jumpService.lastJumpTimeStamp) return // this listener is notified asynchronously, and // we do not want jumps that were processed before val jump = buildJump(changePlace) ?: return - jumpService.removeJump(jump) + jumpService.removeJump(project.basePath ?: IjVimEditor.DEFAULT_PROJECT_ID, jump) } } diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt index 7f8bf665f1..e1fc2b5036 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt @@ -64,6 +64,11 @@ import java.lang.System.identityHashCode @ApiStatus.Internal internal class IjVimEditor(editor: Editor) : MutableLinearEditor() { + companion object { + // For cases where Editor does not have a project (for some reason) + // It's something IJ Platform related and stored here because of this reason + const val DEFAULT_PROJECT_ID = "no project" + } // All the editor actions should be performed with top level editor!!! // Be careful: all the EditorActionHandler implementation should correctly process InjectedEditors @@ -369,6 +374,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() { return EditorHelper.getVirtualFile(editor)?.getUrl()?.let { VirtualFileManager.extractProtocol(it) } } + override val projectId = editor.project?.basePath ?: DEFAULT_PROJECT_ID + override fun visualPositionToOffset(position: VimVisualPosition): Offset { return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)).offset } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt index 8abca73b64..38a33e4fa4 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt @@ -242,6 +242,9 @@ public interface VimEditor { public fun getPath(): String? public fun extractProtocol(): String? + + // Can be used as a key to store something for specific project + public val projectId: String public fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) public fun exitSelectModeNative(adjustCaret: Boolean) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpService.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpService.kt index 272622adff..ac58aded6e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpService.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpService.kt @@ -9,9 +9,14 @@ package com.maddyhome.idea.vim.api import com.maddyhome.idea.vim.mark.Jump +import org.jetbrains.annotations.TestOnly // todo should it be multicaret? // todo docs +// todo it would be better to have some Vim scope for this purpose (p:), to store things project-wise like for buffers +/** + * This service manages jump lists for different projects + */ public interface VimJumpService { /** * Timestamp (`System.currentTimeMillis()`) of the last Jump command , @@ -19,17 +24,23 @@ public interface VimJumpService { * and messes up our jump list */ public var lastJumpTimeStamp: Long - - public fun includeCurrentCommandAsNavigation(editor: VimEditor) - public fun getJumpSpot(): Int - public fun getJump(count: Int): Jump? - public fun getJumps(): List - public fun addJump(jump: Jump, reset: Boolean) + + public fun getJump(projectId: String, count: Int): Jump? + public fun getJumps(projectId: String): List + public fun getJumpSpot(projectId: String): Int + + public fun addJump(projectId: String, jump: Jump, reset: Boolean) public fun saveJumpLocation(editor: VimEditor) - public fun removeJump(jump: Jump) - public fun dropLastJump() - public fun updateJumpsFromInsert(editor: VimEditor, startOffset: Int, length: Int) - public fun updateJumpsFromDelete(editor: VimEditor, startOffset: Int, length: Int) + + public fun removeJump(projectId: String, jump: Jump) + public fun dropLastJump(projectId: String) + + public fun updateJumpsFromInsert(projectId: String, startOffset: Int, length: Int) + public fun updateJumpsFromDelete(projectId: String, startOffset: Int, length: Int) + + public fun includeCurrentCommandAsNavigation(editor: VimEditor) + + @TestOnly public fun resetJumps() } @@ -37,5 +48,33 @@ public fun VimJumpService.addJump(editor: VimEditor, reset: Boolean) { val path = editor.getPath() ?: return val position = editor.offsetToBufferPosition(editor.currentCaret().offset.point) val jump = Jump(position.line, position.column, path) - addJump(jump, reset) + addJump(editor, jump, reset) +} + +public fun VimJumpService.getJump(editor: VimEditor, count: Int): Jump? { + return getJump(editor.projectId, count) +} +public fun VimJumpService.getJumps(editor: VimEditor): List { + return getJumps(editor.projectId) +} +public fun VimJumpService.getJumpSpot(editor: VimEditor): Int { + return getJumpSpot(editor.projectId) +} + +public fun VimJumpService.addJump(editor: VimEditor, jump: Jump, reset: Boolean) { + return addJump(editor.projectId, jump, reset) +} + +public fun VimJumpService.removeJump(editor: VimEditor, jump: Jump) { + return removeJump(editor.projectId, jump) +} +public fun VimJumpService.dropLastJump(editor: VimEditor) { + return dropLastJump(editor.projectId) +} + +public fun VimJumpService.updateJumpsFromInsert(editor: VimEditor, startOffset: Int, length: Int) { + return updateJumpsFromInsert(editor.projectId, startOffset, length) +} +public fun VimJumpService.updateJumpsFromDelete(editor: VimEditor, startOffset: Int, length: Int) { + return updateJumpsFromDelete(editor.projectId, startOffset, length) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpServiceBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpServiceBase.kt index 78f15883a1..9dc6c35755 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpServiceBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimJumpServiceBase.kt @@ -11,51 +11,36 @@ package com.maddyhome.idea.vim.api import com.maddyhome.idea.vim.mark.Jump public abstract class VimJumpServiceBase : VimJumpService { - @JvmField - protected val jumps: MutableList = ArrayList() // todo should it be mutable? - @JvmField - protected var jumpSpot: Int = -1 - - override fun getJumpSpot(): Int { - return jumpSpot + protected val projectToJumps: MutableMap> = mutableMapOf() + protected val projectToJumpSpot: MutableMap = mutableMapOf() + + override fun getJump(projectId: String, count: Int): Jump? { + val jumps = projectToJumps[projectId] ?: mutableListOf() + projectToJumpSpot.putIfAbsent(projectId, -1) + val index = jumps.size - 1 - (projectToJumpSpot[projectId]!! - count) + return jumps.getOrNull(index)?.also { + projectToJumpSpot[projectId] = projectToJumpSpot[projectId]!! - count + } } - override fun getJump(count: Int): Jump? { - val index = jumps.size - 1 - (jumpSpot - count) - return if (index < 0 || index >= jumps.size) { - null - } else { - jumpSpot -= count - jumps[index] - } + override fun getJumps(projectId: String): List { + return projectToJumps[projectId] ?: emptyList() } - override fun getJumps(): List { - return jumps + override fun getJumpSpot(projectId: String): Int { + return projectToJumpSpot[projectId] ?: -1 } - override fun addJump(jump: Jump, reset: Boolean) { + override fun addJump(projectId: String, jump: Jump, reset: Boolean) { lastJumpTimeStamp = System.currentTimeMillis() - val filename = jump.filepath - - for (i in jumps.indices) { - val j = jumps[i] - if (filename == j.filepath && j.line == jump.line) { - jumps.removeAt(i) - break - } - } - + val jumps = projectToJumps.getOrPut(projectId) { mutableListOf() } + jumps.removeIf { it.filepath == jump.filepath && it.line == jump.line } jumps.add(jump) - if (reset) { - jumpSpot = -1 - } else { - jumpSpot++ - } + projectToJumpSpot[projectId] = if (reset) -1 else (projectToJumpSpot[projectId] ?: -1) + 1 if (jumps.size > SAVE_JUMP_COUNT) { - jumps.removeAt(0) + jumps.removeFirst() } } @@ -65,26 +50,25 @@ public abstract class VimJumpServiceBase : VimJumpService { includeCurrentCommandAsNavigation(editor) } - override fun removeJump(jump: Jump) { - val lastIndex = jumps.withIndex().findLast { it.value == jump }?.index ?: return - jumps.removeAt(lastIndex) + override fun removeJump(projectId: String, jump: Jump) { + projectToJumps[projectId]?.removeIf { it == jump } } - override fun dropLastJump() { - jumps.removeLast() + override fun dropLastJump(projectId: String) { + projectToJumps[projectId]?.removeLastOrNull() } - override fun updateJumpsFromInsert(editor: VimEditor, startOffset: Int, length: Int) { + override fun updateJumpsFromInsert(projectId: String, startOffset: Int, length: Int) { TODO("Not yet implemented") } - override fun updateJumpsFromDelete(editor: VimEditor, startOffset: Int, length: Int) { + override fun updateJumpsFromDelete(projectId: String, startOffset: Int, length: Int) { TODO("Not yet implemented") } override fun resetJumps() { - jumps.clear() - jumpSpot = -1 + projectToJumps.clear() + projectToJumpSpot.clear() } public companion object { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt index c4f3c78c58..e3b350a5a5 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMarkServiceBase.kt @@ -46,10 +46,10 @@ public abstract class VimMarkServiceBase : VimMarkService { } @JvmField - protected val globalMarks: java.util.HashMap = HashMap() + protected val globalMarks: HashMap = HashMap() // marks are stored for primary caret only - protected val filepathToLocalMarks: java.util.HashMap> = HashMap() + protected val filepathToLocalMarks: HashMap> = HashMap() public class LocalMarks : HashMap() { public var myTimestamp: Date = Date() @@ -185,7 +185,7 @@ public abstract class VimMarkServiceBase : VimMarkService { if (caret.isPrimary) { if (mark.key == BEFORE_JUMP_MARK) { val jump = Jump(mark.line, mark.col, mark.filepath) - injector.jumpService.addJump(jump, true) + injector.jumpService.addJump(editor, jump, true) } getLocalMarks(mark.filepath)[markChar] = mark } else { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/JumpsCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/JumpsCommand.kt index cac4440d9d..ad0778740f 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/JumpsCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/JumpsCommand.kt @@ -11,6 +11,8 @@ package com.maddyhome.idea.vim.vimscript.model.commands 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.getJumpSpot +import com.maddyhome.idea.vim.api.getJumps import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.ex.ranges.Ranges @@ -25,8 +27,8 @@ import kotlin.math.absoluteValue public data class JumpsCommand(val ranges: Ranges, val argument: String) : Command.SingleExecution(ranges, argument) { override val argFlags: CommandHandlerFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_FORBIDDEN, Access.READ_ONLY) override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult { - val jumps = injector.jumpService.getJumps() - val spot = injector.jumpService.getJumpSpot() + val jumps = injector.jumpService.getJumps(editor) + val spot = injector.jumpService.getJumpSpot(editor) val text = StringBuilder(" jump line col file/text\n") jumps.forEachIndexed { idx, jump ->