From 238942c4719ff1699fab8940930ea1c8e9bd8bb0 Mon Sep 17 00:00:00 2001
From: chylex <contact@chylex.com>
Date: Tue, 17 Oct 2023 03:34:42 +0200
Subject: [PATCH] Add actions to go to next/previous type in file

---
 build.gradle.kts                              |  2 +
 .../gotoType/AbstractGotoTypeInFileAction.kt  | 34 +++++++
 .../gotoType/GotoNextTypeInFileAction.kt      |  9 ++
 .../gotoType/GotoPreviousTypeInFileAction.kt  |  9 ++
 .../action/gotoType/GotoTypeInFileHandler.kt  | 97 +++++++++++++++++++
 .../META-INF/KeyboardMaster-Java.xml          | 15 +++
 src/main/resources/META-INF/plugin.xml        |  1 +
 7 files changed, 167 insertions(+)
 create mode 100644 src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/AbstractGotoTypeInFileAction.kt
 create mode 100644 src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoNextTypeInFileAction.kt
 create mode 100644 src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoPreviousTypeInFileAction.kt
 create mode 100644 src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoTypeInFileHandler.kt
 create mode 100644 src/main/resources/META-INF/KeyboardMaster-Java.xml

diff --git a/build.gradle.kts b/build.gradle.kts
index f103584..9cbc4d2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -19,6 +19,8 @@ intellij {
 	version.set("2023.2")
 	updateSinceUntilBuild.set(false)
 	
+	plugins.add("com.intellij.java")
+	
 	if (System.getenv("IDEAVIM") == "1") {
 		plugins.add("IdeaVIM:0.66")
 	}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/AbstractGotoTypeInFileAction.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/AbstractGotoTypeInFileAction.kt
new file mode 100644
index 0000000..7dc9406
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/AbstractGotoTypeInFileAction.kt
@@ -0,0 +1,34 @@
+package com.chylex.intellij.keyboardmaster.feature.action.gotoType
+
+import com.intellij.codeInsight.actions.BaseCodeInsightAction
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
+import com.intellij.lang.LanguageStructureViewBuilder
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.DumbAware
+import com.intellij.openapi.project.IndexNotReadyException
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiFile
+
+abstract class AbstractGotoTypeInFileAction : BaseCodeInsightAction(), DumbAware {
+	init {
+		isEnabledInModalContext = true
+	}
+	
+	final override fun isValidForLookup(): Boolean {
+		return true
+	}
+	
+	final override fun isValidForFile(project: Project, editor: Editor, file: PsiFile): Boolean {
+		return checkValidForFile(file)
+	}
+	
+	private companion object {
+		fun checkValidForFile(file: PsiFile): Boolean {
+			return try {
+				LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(file) is TreeBasedStructureViewBuilder
+			} catch (e: IndexNotReadyException) {
+				false
+			}
+		}
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoNextTypeInFileAction.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoNextTypeInFileAction.kt
new file mode 100644
index 0000000..78732ef
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoNextTypeInFileAction.kt
@@ -0,0 +1,9 @@
+package com.chylex.intellij.keyboardmaster.feature.action.gotoType
+
+import com.intellij.codeInsight.CodeInsightActionHandler
+
+class GotoNextTypeInFileAction : AbstractGotoTypeInFileAction() {
+	override fun getHandler(): CodeInsightActionHandler {
+		return GotoTypeInFileHandler(forward = true)
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoPreviousTypeInFileAction.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoPreviousTypeInFileAction.kt
new file mode 100644
index 0000000..795dd35
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoPreviousTypeInFileAction.kt
@@ -0,0 +1,9 @@
+package com.chylex.intellij.keyboardmaster.feature.action.gotoType
+
+import com.intellij.codeInsight.CodeInsightActionHandler
+
+class GotoPreviousTypeInFileAction : AbstractGotoTypeInFileAction() {
+	override fun getHandler(): CodeInsightActionHandler {
+		return GotoTypeInFileHandler(forward = false)
+	}
+}
diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoTypeInFileHandler.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoTypeInFileHandler.kt
new file mode 100644
index 0000000..fb187dc
--- /dev/null
+++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/action/gotoType/GotoTypeInFileHandler.kt
@@ -0,0 +1,97 @@
+package com.chylex.intellij.keyboardmaster.feature.action.gotoType
+
+import com.intellij.codeInsight.CodeInsightActionHandler
+import com.intellij.codeInsight.lookup.LookupManager
+import com.intellij.ide.structureView.StructureViewTreeElement
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
+import com.intellij.lang.LanguageStructureViewBuilder
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.ScrollType
+import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Disposer
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import it.unimi.dsi.fastutil.ints.IntArrayList
+
+class GotoTypeInFileHandler(private val forward: Boolean) : CodeInsightActionHandler {
+	override fun invoke(project: Project, editor: Editor, file: PsiFile) {
+		LookupManager.getInstance(project).hideActiveLookup()
+		
+		val caretOffset = editor.caretModel.offset
+		val caretLine = editor.caretModel.logicalPosition.line
+		
+		val searchedOffsetRange = if (forward)
+			caretOffset + 1..file.textLength
+		else
+			0 until caretOffset
+		
+		val navigationOffsets = getNavigationOffsets(file, searchedOffsetRange)
+		if (!forward) {
+			navigationOffsets.reverse()
+		}
+		
+		val direction = if (forward) 1 else -1
+		for (offset in navigationOffsets) {
+			val line = editor.offsetToLogicalPosition(offset).line
+			if (line.compareTo(caretLine) * direction > 0) {
+				editor.caretModel.removeSecondaryCarets()
+				editor.caretModel.moveToOffset(offset)
+				editor.selectionModel.removeSelection()
+				editor.scrollingModel.scrollToCaret(if (forward) ScrollType.CENTER_DOWN else ScrollType.CENTER_UP)
+				IdeDocumentHistory.getInstance(project).includeCurrentCommandAsNavigation()
+				break
+			}
+		}
+	}
+	
+	override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement? {
+		return null
+	}
+	
+	private companion object {
+		fun getNavigationOffsets(file: PsiFile, searchedOffsetRange: IntRange): IntArray {
+			val structureViewBuilder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(file)
+			if (structureViewBuilder !is TreeBasedStructureViewBuilder) {
+				return intArrayOf()
+			}
+			
+			val elements = mutableSetOf<PsiElement>()
+			val model = structureViewBuilder.createStructureViewModel(null)
+			
+			try {
+				addStructureViewElements(elements, model.root, file)
+			} finally {
+				Disposer.dispose(model)
+			}
+			
+			return offsetsFromElements(elements, searchedOffsetRange)
+		}
+		
+		private fun addStructureViewElements(result: MutableSet<PsiElement>, parent: StructureViewTreeElement, file: PsiFile) {
+			for (child in parent.children) {
+				val value = (child as StructureViewTreeElement).value
+				if (value is PsiClass && file == value.containingFile) {
+					result.add(value)
+				}
+				
+				addStructureViewElements(result, child, file)
+			}
+		}
+		
+		private fun offsetsFromElements(elements: Collection<PsiElement>, searchedOffsetRange: IntRange): IntArray {
+			val offsets = IntArrayList(elements.size)
+			
+			for (element in elements) {
+				val offset = element.textOffset
+				if (offset in searchedOffsetRange) {
+					offsets.add(offset)
+				}
+			}
+			
+			offsets.sort(null)
+			return offsets.toIntArray()
+		}
+	}
+}
diff --git a/src/main/resources/META-INF/KeyboardMaster-Java.xml b/src/main/resources/META-INF/KeyboardMaster-Java.xml
new file mode 100644
index 0000000..16d55da
--- /dev/null
+++ b/src/main/resources/META-INF/KeyboardMaster-Java.xml
@@ -0,0 +1,15 @@
+<idea-plugin>
+  <actions>
+    <!-- Go to Type in File -->
+    <action id="KM.GotoNextTypeInFile"
+            text="Next Type"
+            class="com.chylex.intellij.keyboardmaster.feature.action.gotoType.GotoNextTypeInFileAction">
+      <add-to-group group-id="NavigateInFileGroup" anchor="after" relative-to-action="MethodUp" />
+    </action>
+    <action id="KM.GotoPreviousTypeInFile"
+            text="Previous Type"
+            class="com.chylex.intellij.keyboardmaster.feature.action.gotoType.GotoPreviousTypeInFileAction">
+      <add-to-group group-id="NavigateInFileGroup" anchor="after" relative-to-action="KM.GotoNextTypeInFile" />
+    </action>
+  </actions>
+</idea-plugin>
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 8f4af26..0878298 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -12,6 +12,7 @@
   ]]></description>
   
   <depends>com.intellij.modules.platform</depends>
+  <depends optional="true" config-file="KeyboardMaster-Java.xml">com.intellij.java</depends>
   
   <projectListeners>
     <listener class="com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupListener" topic="com.intellij.codeInsight.lookup.LookupManagerListener" />