1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-06-17 06:40:00 +02:00

VIM-1639 Ctrl-o and Ctrl-i jumping in files of different projects

This commit is contained in:
filipp 2023-10-26 10:23:16 +03:00
parent a9ba9789fd
commit 06ef1c1182
8 changed files with 139 additions and 88 deletions
src/main/java/com/maddyhome/idea/vim
vim-engine/src/main/kotlin/com/maddyhome/idea/vim

View File

@ -33,6 +33,8 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimMotionGroupBase import com.maddyhome.idea.vim.api.VimMotionGroupBase
import com.maddyhome.idea.vim.api.addJump import com.maddyhome.idea.vim.api.addJump
import com.maddyhome.idea.vim.api.anyNonWhitespace 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.getLeadingCharacterOffset
import com.maddyhome.idea.vim.api.getVisualLineCount import com.maddyhome.idea.vim.api.getVisualLineCount
import com.maddyhome.idea.vim.api.injector 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 { override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
val jumpService = injector.jumpService val jumpService = injector.jumpService
val spot = jumpService.getJumpSpot() val spot = jumpService.getJumpSpot(editor)
val (line, col, fileName) = jumpService.getJump(count) ?: return Motion.Error val (line, col, fileName) = jumpService.getJump(editor, count) ?: return Motion.Error
val vf = EditorHelper.getVirtualFile(editor.ij) ?: return Motion.Error val vf = EditorHelper.getVirtualFile(editor.ij) ?: return Motion.Error
val lp = BufferPosition(line, col, false) val lp = BufferPosition(line, col, false)
val lpNative = LogicalPosition(line, col, false) val lpNative = LogicalPosition(line, col, false)

View File

@ -15,6 +15,7 @@ import com.intellij.openapi.components.Storage
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.PlaceInfo import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.PlaceInfo
import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.RecentPlacesListener import com.intellij.openapi.fileEditor.impl.IdeDocumentHistoryImpl.RecentPlacesListener
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimJumpServiceBase 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 com.maddyhome.idea.vim.newapi.ij
import org.jdom.Element import org.jdom.Element
@State(name = "VimJumpsSettings", storages = [Storage(value = "\$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)]) @State(name = "VimJumpsSettings", storages = [Storage(value = "\$APP_CONFIG$/vim_settings_local.xml", roamingType = RoamingType.DISABLED)])
internal class VimJumpServiceImpl : VimJumpServiceBase(), PersistentStateComponent<Element?> { internal class VimJumpServiceImpl : VimJumpServiceBase(), PersistentStateComponent<Element?> {
companion object { 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 { override fun getState(): Element {
val jumpsElem = Element("jumps") val projectsElem = Element("projects")
for (jump in jumps) { for ((project, jumps) in projectToJumps) {
val jumpElem = Element("jump") val projectElement = Element("project").setAttribute("id", project)
jumpElem.setAttribute("line", jump.line.toString()) for (jump in jumps) {
jumpElem.setAttribute("column", jump.col.toString()) val jumpElem = Element("jump")
jumpElem.setAttribute("filename", StringUtil.notNullize(jump.filepath)) jumpElem.setAttribute("line", jump.line.toString())
jumpsElem.addContent(jumpElem) jumpElem.setAttribute("column", jump.col.toString())
if (logger.isDebug()) { jumpElem.setAttribute("filename", StringUtil.notNullize(jump.filepath))
logger.debug("saved jump = $jump") projectElement.addContent(jumpElem)
if (logger.isDebug()) {
logger.debug("saved jump = $jump")
}
} }
projectsElem.addContent(projectElement)
} }
return jumpsElem return projectsElem
} }
override fun loadState(state: Element) { override fun loadState(state: Element) {
val jumpList = state.getChildren("jump") val projectElements = state.getChildren("project")
for (jumpElement in jumpList) { for (projectElement in projectElements) {
val jump = Jump( val jumps = mutableListOf<Jump>()
Integer.parseInt(jumpElement.getAttributeValue("line")), val jumpElements = projectElement.getChildren("jump")
Integer.parseInt(jumpElement.getAttributeValue("column")), for (jumpElement in jumpElements) {
jumpElement.getAttributeValue("filename"), val jump = Jump(
) Integer.parseInt(jumpElement.getAttributeValue("line")),
jumps.add(jump) Integer.parseInt(jumpElement.getAttributeValue("column")),
} jumpElement.getAttributeValue("filename"),
)
if (logger.isDebug()) { jumps.add(jump)
logger.debug("jumps=$jumps") }
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) { override fun recentPlaceAdded(changePlace: PlaceInfo, isChanged: Boolean) {
if (!injector.globalIjOptions().unifyjumps) return if (!injector.globalIjOptions().unifyjumps) return
val jumpService = injector.jumpService val jumpService = injector.jumpService
if (!isChanged) { if (!isChanged) {
if (changePlace.timeStamp < jumpService.lastJumpTimeStamp) return // this listener is notified asynchronously, and if (changePlace.timeStamp < jumpService.lastJumpTimeStamp) return // this listener is notified asynchronously, and
// we do not want jumps that were processed before // we do not want jumps that were processed before
val jump = buildJump(changePlace) ?: return 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) { override fun recentPlaceRemoved(changePlace: PlaceInfo, isChanged: Boolean) {
if (!injector.globalIjOptions().unifyjumps) return if (!injector.globalIjOptions().unifyjumps) return
val jumpService = injector.jumpService val jumpService = injector.jumpService
if (!isChanged) { if (!isChanged) {
if (changePlace.timeStamp < jumpService.lastJumpTimeStamp) return // this listener is notified asynchronously, and if (changePlace.timeStamp < jumpService.lastJumpTimeStamp) return // this listener is notified asynchronously, and
// we do not want jumps that were processed before // we do not want jumps that were processed before
val jump = buildJump(changePlace) ?: return val jump = buildJump(changePlace) ?: return
jumpService.removeJump(jump) jumpService.removeJump(project.basePath ?: IjVimEditor.DEFAULT_PROJECT_ID, jump)
} }
} }

View File

@ -64,6 +64,11 @@ import java.lang.System.identityHashCode
@ApiStatus.Internal @ApiStatus.Internal
internal class IjVimEditor(editor: Editor) : MutableLinearEditor() { 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!!! // All the editor actions should be performed with top level editor!!!
// Be careful: all the EditorActionHandler implementation should correctly process InjectedEditors // 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) } return EditorHelper.getVirtualFile(editor)?.getUrl()?.let { VirtualFileManager.extractProtocol(it) }
} }
override val projectId = editor.project?.basePath ?: DEFAULT_PROJECT_ID
override fun visualPositionToOffset(position: VimVisualPosition): Offset { override fun visualPositionToOffset(position: VimVisualPosition): Offset {
return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)).offset return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)).offset
} }

View File

@ -242,6 +242,9 @@ public interface VimEditor {
public fun getPath(): String? public fun getPath(): String?
public fun extractProtocol(): 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 exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments)
public fun exitSelectModeNative(adjustCaret: Boolean) public fun exitSelectModeNative(adjustCaret: Boolean)

View File

@ -9,9 +9,14 @@
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.mark.Jump import com.maddyhome.idea.vim.mark.Jump
import org.jetbrains.annotations.TestOnly
// todo should it be multicaret? // todo should it be multicaret?
// todo docs // 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 { public interface VimJumpService {
/** /**
* Timestamp (`System.currentTimeMillis()`) of the last Jump command <C-o>, <C-i> * Timestamp (`System.currentTimeMillis()`) of the last Jump command <C-o>, <C-i>
@ -19,17 +24,23 @@ public interface VimJumpService {
* and messes up our jump list * and messes up our jump list
*/ */
public var lastJumpTimeStamp: Long public var lastJumpTimeStamp: Long
public fun includeCurrentCommandAsNavigation(editor: VimEditor) public fun getJump(projectId: String, count: Int): Jump?
public fun getJumpSpot(): Int public fun getJumps(projectId: String): List<Jump>
public fun getJump(count: Int): Jump? public fun getJumpSpot(projectId: String): Int
public fun getJumps(): List<Jump>
public fun addJump(jump: Jump, reset: Boolean) public fun addJump(projectId: String, jump: Jump, reset: Boolean)
public fun saveJumpLocation(editor: VimEditor) public fun saveJumpLocation(editor: VimEditor)
public fun removeJump(jump: Jump)
public fun dropLastJump() public fun removeJump(projectId: String, jump: Jump)
public fun updateJumpsFromInsert(editor: VimEditor, startOffset: Int, length: Int) public fun dropLastJump(projectId: String)
public fun updateJumpsFromDelete(editor: VimEditor, startOffset: Int, length: Int)
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() public fun resetJumps()
} }
@ -37,5 +48,33 @@ public fun VimJumpService.addJump(editor: VimEditor, reset: Boolean) {
val path = editor.getPath() ?: return val path = editor.getPath() ?: return
val position = editor.offsetToBufferPosition(editor.currentCaret().offset.point) val position = editor.offsetToBufferPosition(editor.currentCaret().offset.point)
val jump = Jump(position.line, position.column, path) 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<Jump> {
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)
} }

View File

@ -11,51 +11,36 @@ package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.mark.Jump import com.maddyhome.idea.vim.mark.Jump
public abstract class VimJumpServiceBase : VimJumpService { public abstract class VimJumpServiceBase : VimJumpService {
@JvmField protected val projectToJumps: MutableMap<String, MutableList<Jump>> = mutableMapOf()
protected val jumps: MutableList<Jump> = ArrayList() // todo should it be mutable? protected val projectToJumpSpot: MutableMap<String, Int> = mutableMapOf()
@JvmField
protected var jumpSpot: Int = -1
override fun getJumpSpot(): Int { override fun getJump(projectId: String, count: Int): Jump? {
return jumpSpot val jumps = projectToJumps[projectId] ?: mutableListOf()
} projectToJumpSpot.putIfAbsent(projectId, -1)
val index = jumps.size - 1 - (projectToJumpSpot[projectId]!! - count)
override fun getJump(count: Int): Jump? { return jumps.getOrNull(index)?.also {
val index = jumps.size - 1 - (jumpSpot - count) projectToJumpSpot[projectId] = projectToJumpSpot[projectId]!! - count
return if (index < 0 || index >= jumps.size) {
null
} else {
jumpSpot -= count
jumps[index]
} }
} }
override fun getJumps(): List<Jump> { override fun getJumps(projectId: String): List<Jump> {
return jumps return projectToJumps[projectId] ?: emptyList()
} }
override fun addJump(jump: Jump, reset: Boolean) { override fun getJumpSpot(projectId: String): Int {
return projectToJumpSpot[projectId] ?: -1
}
override fun addJump(projectId: String, jump: Jump, reset: Boolean) {
lastJumpTimeStamp = System.currentTimeMillis() lastJumpTimeStamp = System.currentTimeMillis()
val filename = jump.filepath val jumps = projectToJumps.getOrPut(projectId) { mutableListOf() }
jumps.removeIf { it.filepath == jump.filepath && it.line == jump.line }
for (i in jumps.indices) {
val j = jumps[i]
if (filename == j.filepath && j.line == jump.line) {
jumps.removeAt(i)
break
}
}
jumps.add(jump) jumps.add(jump)
if (reset) { projectToJumpSpot[projectId] = if (reset) -1 else (projectToJumpSpot[projectId] ?: -1) + 1
jumpSpot = -1
} else {
jumpSpot++
}
if (jumps.size > SAVE_JUMP_COUNT) { if (jumps.size > SAVE_JUMP_COUNT) {
jumps.removeAt(0) jumps.removeFirst()
} }
} }
@ -65,26 +50,25 @@ public abstract class VimJumpServiceBase : VimJumpService {
includeCurrentCommandAsNavigation(editor) includeCurrentCommandAsNavigation(editor)
} }
override fun removeJump(jump: Jump) { override fun removeJump(projectId: String, jump: Jump) {
val lastIndex = jumps.withIndex().findLast { it.value == jump }?.index ?: return projectToJumps[projectId]?.removeIf { it == jump }
jumps.removeAt(lastIndex)
} }
override fun dropLastJump() { override fun dropLastJump(projectId: String) {
jumps.removeLast() 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") 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") TODO("Not yet implemented")
} }
override fun resetJumps() { override fun resetJumps() {
jumps.clear() projectToJumps.clear()
jumpSpot = -1 projectToJumpSpot.clear()
} }
public companion object { public companion object {

View File

@ -46,10 +46,10 @@ public abstract class VimMarkServiceBase : VimMarkService {
} }
@JvmField @JvmField
protected val globalMarks: java.util.HashMap<Char, Mark> = HashMap() protected val globalMarks: HashMap<Char, Mark> = HashMap()
// marks are stored for primary caret only // marks are stored for primary caret only
protected val filepathToLocalMarks: java.util.HashMap<String, LocalMarks<Char, Mark>> = HashMap() protected val filepathToLocalMarks: HashMap<String, LocalMarks<Char, Mark>> = HashMap()
public class LocalMarks<K, V> : HashMap<K, V>() { public class LocalMarks<K, V> : HashMap<K, V>() {
public var myTimestamp: Date = Date() public var myTimestamp: Date = Date()
@ -185,7 +185,7 @@ public abstract class VimMarkServiceBase : VimMarkService {
if (caret.isPrimary) { if (caret.isPrimary) {
if (mark.key == BEFORE_JUMP_MARK) { if (mark.key == BEFORE_JUMP_MARK) {
val jump = Jump(mark.line, mark.col, mark.filepath) 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 getLocalMarks(mark.filepath)[markChar] = mark
} else { } else {

View File

@ -11,6 +11,8 @@ package com.maddyhome.idea.vim.vimscript.model.commands
import com.intellij.vim.annotations.ExCommand import com.intellij.vim.annotations.ExCommand
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor 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.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ranges.Ranges 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) { 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 val argFlags: CommandHandlerFlags = flags(RangeFlag.RANGE_OPTIONAL, ArgumentFlag.ARGUMENT_FORBIDDEN, Access.READ_ONLY)
override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult { override fun processCommand(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments): ExecutionResult {
val jumps = injector.jumpService.getJumps() val jumps = injector.jumpService.getJumps(editor)
val spot = injector.jumpService.getJumpSpot() val spot = injector.jumpService.getJumpSpot(editor)
val text = StringBuilder(" jump line col file/text\n") val text = StringBuilder(" jump line col file/text\n")
jumps.forEachIndexed { idx, jump -> jumps.forEachIndexed { idx, jump ->