Skip to content

Commit

Permalink
VIM-1639 Ctrl-o and Ctrl-i jumping in files of different projects
Browse files Browse the repository at this point in the history
  • Loading branch information
lippfi committed Oct 26, 2023
1 parent a9ba978 commit 06ef1c1
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 88 deletions.
6 changes: 4 additions & 2 deletions src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
68 changes: 41 additions & 27 deletions src/main/java/com/maddyhome/idea/vim/group/VimJumpServiceImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Element?> {
companion object {
Expand All @@ -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<Jump>()
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)
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,72 @@
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 <C-o>, <C-i>
* it's a temporary sticky tape to avoid difficulties with Platform, which counts <C-o>, <C-i> as new jump locations
* 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<Jump>
public fun addJump(jump: Jump, reset: Boolean)
public fun getJump(projectId: String, count: Int): Jump?
public fun getJumps(projectId: String): List<Jump>
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()
}

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<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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Jump> = ArrayList() // todo should it be mutable?
@JvmField
protected var jumpSpot: Int = -1

override fun getJumpSpot(): Int {
return jumpSpot
protected val projectToJumps: MutableMap<String, MutableList<Jump>> = mutableMapOf()
protected val projectToJumpSpot: MutableMap<String, Int> = 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<Jump> {
return projectToJumps[projectId] ?: emptyList()
}

override fun getJumps(): List<Jump> {
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()
}
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ public abstract class VimMarkServiceBase : VimMarkService {
}

@JvmField
protected val globalMarks: java.util.HashMap<Char, Mark> = HashMap()
protected val globalMarks: HashMap<Char, Mark> = HashMap()

// 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 var myTimestamp: Date = Date()
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ->
Expand Down

0 comments on commit 06ef1c1

Please sign in to comment.