mirror of
				https://github.com/chylex/IntelliJ-IdeaVim.git
				synced 2025-11-02 23:31:30 +01:00 
			
		
		
		
	Compare commits
	
		
			69 Commits
		
	
	
		
			a978505530
			...
			customized
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						063ed0aa84
	
				 | 
					
					
						|||
| 
						
						
							
						
						fccfe0c2ea
	
				 | 
					
					
						|||
| 
						
						
							
						
						45ed79d865
	
				 | 
					
					
						|||
| 
						
						
							
						
						527eb4cbb3
	
				 | 
					
					
						|||
| 
						
						
							
						
						e32ac125b2
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d1d3b697c
	
				 | 
					
					
						|||
| 
						
						
							
						
						3246832528
	
				 | 
					
					
						|||
| 
						
						
							
						
						6505bfc9aa
	
				 | 
					
					
						|||
| 
						
						
							
						
						0c63890e9d
	
				 | 
					
					
						|||
| 
						
						
							
						
						259958e702
	
				 | 
					
					
						|||
| 
						
						
							
						
						4916545b53
	
				 | 
					
					
						|||
| 
						
						
							
						
						b8a9bddfa9
	
				 | 
					
					
						|||
| 
						
						
							
						
						95688b33c8
	
				 | 
					
					
						|||
| 
						
						
							
						
						07f44f1c93
	
				 | 
					
					
						|||
| 
						
						
							
						
						2ce6239ad6
	
				 | 
					
					
						|||
| 
						
						
							
						
						a0d2d64237
	
				 | 
					
					
						|||
| 
						
						
							
						
						2e4e8c058b
	
				 | 
					
					
						|||
| 
						
						
							
						
						f464d25844
	
				 | 
					
					
						|||
| 
						
						
							
						
						acc12c5b17
	
				 | 
					
					
						|||
| 
						
						
							
						
						0c1bbd5e92
	
				 | 
					
					
						|||
| 
						
						
							
						
						f330e220ad
	
				 | 
					
					
						|||
| 
						 | 
					b86ec03dc4 | ||
| 
						 | 
					ae75498f8a | ||
| 
						 | 
					9d0b68b0f8 | ||
| 
						 | 
					eeb5939e59 | ||
| 
						 | 
					ef235a47bf | ||
| 
						 | 
					b66da76880 | ||
| 
						 | 
					54d6119784 | ||
| 
						 | 
					0b8c081425 | ||
| 
						 | 
					209052ffa6 | ||
| 
						 | 
					fe9a6b5cfb | ||
| 
						 | 
					9c0f74369f | ||
| 
						 | 
					cd27e5229b | ||
| 
						 | 
					472732905c | ||
| 
						 | 
					485d9f81cd | ||
| 
						 | 
					8cf136ce4c | ||
| 
						 | 
					116a8ac9d2 | ||
| 
						 | 
					fda310bda6 | ||
| 
						 | 
					e55619ea33 | ||
| 
						 | 
					b952b20128 | ||
| 
						 | 
					62d1f85648 | ||
| 
						 | 
					5e3c8c0e92 | ||
| 
						 | 
					b58dddf2ff | ||
| 
						 | 
					78d351a0b0 | ||
| 
						 | 
					61dbc948cc | ||
| 
						 | 
					c4d92ebe73 | ||
| 
						 | 
					d0cf827638 | ||
| 
						 | 
					6a6a92b6b9 | ||
| 
						 | 
					9869b8a34e | ||
| 
						 | 
					60fbf88322 | ||
| 
						 | 
					fae3924062 | ||
| 
						 | 
					dc2ce64823 | ||
| 
						 | 
					d0d86d9178 | ||
| 
						 | 
					f417af6148 | ||
| 
						 | 
					2fe2860a09 | ||
| 
						 | 
					cb40426976 | ||
| 
						 | 
					423ed390a2 | ||
| 
						 | 
					7652b16ca6 | ||
| 
						 | 
					618a010c15 | ||
| 
						 | 
					d44a34ed9b | ||
| 
						 | 
					c84fc996db | ||
| 
						 | 
					43f232543b | ||
| 
						 | 
					3f65d1d99a | ||
| 
						 | 
					bfcf706ca7 | ||
| 
						 | 
					8c1103c461 | ||
| 
						 | 
					ab75ace8db | ||
| 
						 | 
					4a58e6a282 | ||
| 
						 | 
					ac9e4f69b4 | ||
| 
						 | 
					581edba7fd | 
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
* text=auto eol=lf
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/runUiOctopusTests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/runUiOctopusTests.yml
									
									
									
									
										vendored
									
									
								
							@@ -15,11 +15,7 @@ jobs:
 | 
			
		||||
          distribution: zulu
 | 
			
		||||
          java-version: 17
 | 
			
		||||
      - name: Setup FFmpeg
 | 
			
		||||
        uses: FedericoCarboni/setup-ffmpeg@v3
 | 
			
		||||
        with:
 | 
			
		||||
          # Not strictly necessary, but it may prevent rate limit
 | 
			
		||||
          # errors especially on GitHub-hosted macos machines.
 | 
			
		||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        run: brew install ffmpeg
 | 
			
		||||
      - name: Setup Gradle
 | 
			
		||||
        uses: gradle/gradle-build-action@v2.4.2
 | 
			
		||||
      - name: Build Plugin
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/runUiPyTests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/runUiPyTests.yml
									
									
									
									
										vendored
									
									
								
							@@ -18,11 +18,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.10'
 | 
			
		||||
      - name: Setup FFmpeg
 | 
			
		||||
        uses: FedericoCarboni/setup-ffmpeg@v3
 | 
			
		||||
        with:
 | 
			
		||||
          # Not strictly necessary, but it may prevent rate limit
 | 
			
		||||
          # errors especially on GitHub-hosted macos machines.
 | 
			
		||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        run: brew install ffmpeg
 | 
			
		||||
      - name: Setup Gradle
 | 
			
		||||
        uses: gradle/gradle-build-action@v2.4.2
 | 
			
		||||
      - name: Build Plugin
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/runUiTests.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/runUiTests.yml
									
									
									
									
										vendored
									
									
								
							@@ -15,11 +15,7 @@ jobs:
 | 
			
		||||
          distribution: zulu
 | 
			
		||||
          java-version: 17
 | 
			
		||||
      - name: Setup FFmpeg
 | 
			
		||||
        uses: FedericoCarboni/setup-ffmpeg@v3
 | 
			
		||||
        with:
 | 
			
		||||
          # Not strictly necessary, but it may prevent rate limit
 | 
			
		||||
          # errors especially on GitHub-hosted macos machines.
 | 
			
		||||
          github-token: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
        run: brew install ffmpeg
 | 
			
		||||
      - name: Setup Gradle
 | 
			
		||||
        uses: gradle/gradle-build-action@v2.4.2
 | 
			
		||||
      - name: Build Plugin
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								.github/workflows/updateAffectedRate.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/updateAffectedRate.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,34 +0,0 @@
 | 
			
		||||
# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
 | 
			
		||||
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
 | 
			
		||||
 | 
			
		||||
# This workflow syncs changes from the docs folder of IdeaVim to the IdeaVim.wiki repository
 | 
			
		||||
 | 
			
		||||
name: Update Affected Rate field on YouTrack
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '0 8 * * *'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    if: github.repository == 'JetBrains/ideavim'
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Fetch origin repo
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
 | 
			
		||||
      - name: Set up JDK 17
 | 
			
		||||
        uses: actions/setup-java@v2
 | 
			
		||||
        with:
 | 
			
		||||
          java-version: '17'
 | 
			
		||||
          distribution: 'adopt'
 | 
			
		||||
          server-id: github # Value of the distributionManagement/repository/id field of the pom.xml
 | 
			
		||||
          settings-path: ${{ github.workspace }} # location for the settings.xml file
 | 
			
		||||
 | 
			
		||||
      - name: Update YouTrack
 | 
			
		||||
        run: ./gradlew scripts:updateAffectedRates
 | 
			
		||||
        env:
 | 
			
		||||
          YOUTRACK_TOKEN: ${{ secrets.YOUTRACK_TOKEN }}
 | 
			
		||||
							
								
								
									
										1
									
								
								.teamcity/_Self/Project.kt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.teamcity/_Self/Project.kt
									
									
									
									
										vendored
									
									
								
							@@ -25,6 +25,7 @@ object Project : Project({
 | 
			
		||||
  // Active tests
 | 
			
		||||
  buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT"))
 | 
			
		||||
  buildType(TestingBuildType("2023.3", "<default>", version = "2023.3"))
 | 
			
		||||
  buildType(TestingBuildType("2024.1", "<default>"))
 | 
			
		||||
  buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT"))
 | 
			
		||||
 | 
			
		||||
  buildType(PropertyBased)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ repositories {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
  compileOnly("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.19")
 | 
			
		||||
  compileOnly("com.google.devtools.ksp:symbol-processing-api:1.9.23-1.0.20")
 | 
			
		||||
  implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion") {
 | 
			
		||||
    // kotlin stdlib is provided by IJ, so there is no need to include it into the distribution
 | 
			
		||||
    exclude("org.jetbrains.kotlin", "kotlin-stdlib")
 | 
			
		||||
 
 | 
			
		||||
@@ -51,11 +51,11 @@ buildscript {
 | 
			
		||||
    classpath("org.eclipse.jgit:org.eclipse.jgit.ssh.apache:6.9.0.202403050737-r")
 | 
			
		||||
    classpath("org.kohsuke:github-api:1.305")
 | 
			
		||||
 | 
			
		||||
    classpath("io.ktor:ktor-client-core:2.3.9")
 | 
			
		||||
    classpath("io.ktor:ktor-client-cio:2.3.9")
 | 
			
		||||
    classpath("io.ktor:ktor-client-auth:2.3.9")
 | 
			
		||||
    classpath("io.ktor:ktor-client-content-negotiation:2.3.9")
 | 
			
		||||
    classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
 | 
			
		||||
    classpath("io.ktor:ktor-client-core:2.3.10")
 | 
			
		||||
    classpath("io.ktor:ktor-client-cio:2.3.10")
 | 
			
		||||
    classpath("io.ktor:ktor-client-auth:2.3.10")
 | 
			
		||||
    classpath("io.ktor:ktor-client-content-negotiation:2.3.10")
 | 
			
		||||
    classpath("io.ktor:ktor-serialization-kotlinx-json:2.3.10")
 | 
			
		||||
 | 
			
		||||
    // This comes from the changelog plugin
 | 
			
		||||
//        classpath("org.jetbrains:markdown:0.3.1")
 | 
			
		||||
@@ -69,11 +69,11 @@ plugins {
 | 
			
		||||
  application
 | 
			
		||||
  id("java-test-fixtures")
 | 
			
		||||
 | 
			
		||||
  id("org.jetbrains.intellij") version "1.17.2"
 | 
			
		||||
  id("org.jetbrains.intellij") version "1.17.3"
 | 
			
		||||
  id("org.jetbrains.changelog") version "2.2.0"
 | 
			
		||||
 | 
			
		||||
  id("org.jetbrains.kotlinx.kover") version "0.6.1"
 | 
			
		||||
  id("com.dorongold.task-tree") version "2.1.1"
 | 
			
		||||
  id("com.dorongold.task-tree") version "3.0.0"
 | 
			
		||||
 | 
			
		||||
  id("com.google.devtools.ksp") version "1.9.22-1.0.17"
 | 
			
		||||
}
 | 
			
		||||
@@ -141,7 +141,7 @@ dependencies {
 | 
			
		||||
  testFixturesImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
 | 
			
		||||
 | 
			
		||||
  // https://mvnrepository.com/artifact/org.mockito.kotlin/mockito-kotlin
 | 
			
		||||
  testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
 | 
			
		||||
  testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
 | 
			
		||||
 | 
			
		||||
  testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2")
 | 
			
		||||
  testImplementation("org.junit.jupiter:junit-jupiter-engine:5.10.2")
 | 
			
		||||
@@ -264,9 +264,6 @@ tasks {
 | 
			
		||||
  runPluginVerifier {
 | 
			
		||||
    downloadDir.set("${project.buildDir}/pluginVerifier/ides")
 | 
			
		||||
    teamCityOutputFormat.set(true)
 | 
			
		||||
 | 
			
		||||
    // The latest version of the plugin verifier is broken, so temporally use the stable version
 | 
			
		||||
    verifierVersion = "1.307"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  generateGrammarSource {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,16 +9,17 @@
 | 
			
		||||
# suppress inspection "UnusedProperty" for whole file
 | 
			
		||||
 | 
			
		||||
#ideaVersion=LATEST-EAP-SNAPSHOT
 | 
			
		||||
ideaVersion=2023.3.2
 | 
			
		||||
ideaVersion=2024.1
 | 
			
		||||
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
 | 
			
		||||
ideaType=IC
 | 
			
		||||
downloadIdeaSources=true
 | 
			
		||||
instrumentPluginCode=true
 | 
			
		||||
version=SNAPSHOT
 | 
			
		||||
version=chylex-32
 | 
			
		||||
javaVersion=17
 | 
			
		||||
remoteRobotVersion=0.11.22
 | 
			
		||||
antlrVersion=4.10.1
 | 
			
		||||
 | 
			
		||||
kotlin.incremental.useClasspathSnapshot=false
 | 
			
		||||
 | 
			
		||||
# Please don't forget to update kotlin version in buildscript section
 | 
			
		||||
# Also update kotlinxSerializationVersion version
 | 
			
		||||
 
 | 
			
		||||
@@ -22,11 +22,11 @@ repositories {
 | 
			
		||||
dependencies {
 | 
			
		||||
  compileOnly("org.jetbrains.kotlin:kotlin-stdlib:1.9.23")
 | 
			
		||||
 | 
			
		||||
  implementation("io.ktor:ktor-client-core:2.3.9")
 | 
			
		||||
  implementation("io.ktor:ktor-client-cio:2.3.9")
 | 
			
		||||
  implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
 | 
			
		||||
  implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
 | 
			
		||||
  implementation("io.ktor:ktor-client-auth:2.3.9")
 | 
			
		||||
  implementation("io.ktor:ktor-client-core:2.3.10")
 | 
			
		||||
  implementation("io.ktor:ktor-client-cio:2.3.10")
 | 
			
		||||
  implementation("io.ktor:ktor-client-content-negotiation:2.3.10")
 | 
			
		||||
  implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.10")
 | 
			
		||||
  implementation("io.ktor:ktor-client-auth:2.3.10")
 | 
			
		||||
  implementation("org.eclipse.jgit:org.eclipse.jgit:6.6.0.202305301015-r")
 | 
			
		||||
 | 
			
		||||
  // This is needed for jgit to connect to ssh
 | 
			
		||||
@@ -58,13 +58,6 @@ tasks.register("checkNewPluginDependencies", JavaExec::class) {
 | 
			
		||||
  classpath = sourceSets["main"].runtimeClasspath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.register("updateAffectedRates", JavaExec::class) {
 | 
			
		||||
  group = "verification"
 | 
			
		||||
  description = "This job updates Affected Rate field on YouTrack"
 | 
			
		||||
  mainClass.set("scripts.YouTrackUsersAffectedKt")
 | 
			
		||||
  classpath = sourceSets["main"].runtimeClasspath
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks.register("calculateNewVersion", JavaExec::class) {
 | 
			
		||||
  group = "release"
 | 
			
		||||
  mainClass.set("scripts.release.CalculateNewVersionKt")
 | 
			
		||||
 
 | 
			
		||||
@@ -49,17 +49,13 @@ suspend fun main() {
 | 
			
		||||
  }
 | 
			
		||||
  val output = response.body<List<String>>().toSet()
 | 
			
		||||
  println(output)
 | 
			
		||||
  if (knownPlugins != output) {
 | 
			
		||||
    val newPlugins = (output - knownPlugins).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
 | 
			
		||||
    val removedPlugins = (knownPlugins - output.toSet()).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
 | 
			
		||||
  val newPlugins = (output - knownPlugins).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
 | 
			
		||||
  if (newPlugins.isNotEmpty()) {
 | 
			
		||||
//    val removedPlugins = (knownPlugins - output.toSet()).map { it to (getPluginLinkByXmlId(it) ?: "Can't find plugin link") }
 | 
			
		||||
    error(
 | 
			
		||||
      """
 | 
			
		||||
        
 | 
			
		||||
      Unregistered plugins:
 | 
			
		||||
      ${if (newPlugins.isNotEmpty()) newPlugins.joinToString(separator = "\n") { it.first + "(" + it.second + ")" } else "No unregistered plugins"}
 | 
			
		||||
      
 | 
			
		||||
      Removed plugins:
 | 
			
		||||
      ${if (removedPlugins.isNotEmpty()) removedPlugins.joinToString(separator = "\n") { it.first + "(" + it.second + ")" } else "No removed plugins"}
 | 
			
		||||
      ${newPlugins.joinToString(separator = "\n") { it.first + "(" + it.second + ")" }}
 | 
			
		||||
    """.trimIndent()
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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 scripts
 | 
			
		||||
 | 
			
		||||
import io.ktor.client.call.*
 | 
			
		||||
import kotlinx.serialization.json.JsonArray
 | 
			
		||||
import kotlinx.serialization.json.jsonObject
 | 
			
		||||
import kotlinx.serialization.json.jsonPrimitive
 | 
			
		||||
import kotlinx.serialization.json.put
 | 
			
		||||
 | 
			
		||||
val areaWeights = setOf(
 | 
			
		||||
  Triple("118-53212", "Plugins", 50),
 | 
			
		||||
  Triple("118-53220", "Vim Script", 30),
 | 
			
		||||
  Triple("118-54084", "Esc", 100),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
suspend fun updateRates() {
 | 
			
		||||
  println("Updating rates of the issues")
 | 
			
		||||
  areaWeights.forEach { (id, name, weight) ->
 | 
			
		||||
    val unmappedIssues = unmappedIssues(name)
 | 
			
		||||
    println("Got ${unmappedIssues.size} for $name area")
 | 
			
		||||
 | 
			
		||||
    unmappedIssues.forEach { issueId ->
 | 
			
		||||
      print("Trying to update issue $issueId: ")
 | 
			
		||||
      val response = updateCustomField(issueId) {
 | 
			
		||||
        put("name", "Affected Rate")
 | 
			
		||||
        put("\$type", "SimpleIssueCustomField")
 | 
			
		||||
        put("value", weight)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      println(response)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private suspend fun unmappedIssues(area: String): List<String> {
 | 
			
		||||
  val areaProcessed = if (" " in area) "{$area}" else area
 | 
			
		||||
  val res = issuesQuery(
 | 
			
		||||
    query = "project: VIM Affected Rate: {No affected rate} Area: $areaProcessed #Unresolved",
 | 
			
		||||
    fields = "id,idReadable"
 | 
			
		||||
  )
 | 
			
		||||
  return res.body<JsonArray>().map { it.jsonObject }.map { it["idReadable"]!!.jsonPrimitive.content }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun getAreasWithoutWeight(): Set<Pair<String, String>> {
 | 
			
		||||
  val allAreas = getAreaValues()
 | 
			
		||||
  return allAreas
 | 
			
		||||
    .filterNot { it.key in areaWeights.map { it.first }.toSet() }
 | 
			
		||||
    .entries
 | 
			
		||||
    .map { it.key to it.value }
 | 
			
		||||
    .toSet()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
suspend fun main() {
 | 
			
		||||
  updateRates()
 | 
			
		||||
}
 | 
			
		||||
@@ -77,7 +77,7 @@ public class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedActio
 | 
			
		||||
      val modifiers = if (charTyped == ' ' && VimKeyListener.isSpaceShift) KeyEvent.SHIFT_DOWN_MASK else 0
 | 
			
		||||
      val keyStroke = KeyStroke.getKeyStroke(charTyped, modifiers)
 | 
			
		||||
      val startTime = if (traceTime) System.currentTimeMillis() else null
 | 
			
		||||
      handler.handleKey(editor.vim, keyStroke, injector.executionContextManager.onEditor(editor.vim, context.vim), handler.keyHandlerState)
 | 
			
		||||
      handler.handleKey(editor.vim, keyStroke, context.vim, handler.keyHandlerState)
 | 
			
		||||
      if (startTime != null) {
 | 
			
		||||
        val duration = System.currentTimeMillis() - startTime
 | 
			
		||||
        LOG.info("VimTypedAction '$charTyped': $duration ms")
 | 
			
		||||
 
 | 
			
		||||
@@ -79,12 +79,7 @@ public class VimShortcutKeyAction : AnAction(), DumbAware/*, LightEditCompatible
 | 
			
		||||
      try {
 | 
			
		||||
        val start = if (traceTime) System.currentTimeMillis() else null
 | 
			
		||||
        val keyHandler = KeyHandler.getInstance()
 | 
			
		||||
        keyHandler.handleKey(
 | 
			
		||||
          editor.vim,
 | 
			
		||||
          keyStroke,
 | 
			
		||||
          injector.executionContextManager.onEditor(editor.vim, e.dataContext.vim),
 | 
			
		||||
          keyHandler.keyHandlerState,
 | 
			
		||||
        )
 | 
			
		||||
        keyHandler.handleKey(editor.vim, keyStroke, e.dataContext.vim, keyHandler.keyHandlerState)
 | 
			
		||||
        if (start != null) {
 | 
			
		||||
          val duration = System.currentTimeMillis() - start
 | 
			
		||||
          LOG.info("VimShortcut update '$keyStroke': $duration ms")
 | 
			
		||||
@@ -381,6 +376,10 @@ private class ActionEnableStatus(
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun toString(): String {
 | 
			
		||||
    return "ActionEnableStatus(isEnabled=$isEnabled, message='$message', logLevel=$logLevel)"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    private val LOG = logger<ActionEnableStatus>()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ public class DeleteJoinLinesSpacesAction : ChangeEditorActionHandler.SingleExecu
 | 
			
		||||
    }
 | 
			
		||||
    injector.editorGroup.notifyIdeaJoin(editor)
 | 
			
		||||
    var res = true
 | 
			
		||||
    editor.nativeCarets().sortedByDescending { it.offset.point }.forEach { caret ->
 | 
			
		||||
    editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret ->
 | 
			
		||||
      if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true, operatorArguments)) {
 | 
			
		||||
        res = false
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ public class DeleteJoinVisualLinesAction : VisualOperatorActionHandler.SingleExe
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    var res = true
 | 
			
		||||
    editor.nativeCarets().sortedByDescending { it.offset.point }.forEach { caret ->
 | 
			
		||||
    editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret ->
 | 
			
		||||
      if (!caret.isValid) return@forEach
 | 
			
		||||
      val range = caretsAndSelections[caret] ?: return@forEach
 | 
			
		||||
      if (!injector.changeGroup.deleteJoinRange(
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ public class DeleteJoinVisualLinesSpacesAction : VisualOperatorActionHandler.Sin
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    var res = true
 | 
			
		||||
    editor.carets().sortedByDescending { it.offset.point }.forEach { caret ->
 | 
			
		||||
    editor.carets().sortedByDescending { it.offset }.forEach { caret ->
 | 
			
		||||
      if (!caret.isValid) return@forEach
 | 
			
		||||
      val range = caretsAndSelections[caret] ?: return@forEach
 | 
			
		||||
      if (!injector.changeGroup.deleteJoinRange(
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,6 @@
 | 
			
		||||
package com.maddyhome.idea.vim.common
 | 
			
		||||
 | 
			
		||||
import com.intellij.application.options.CodeStyle
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.actionSystem.PlatformDataKeys
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.project.Project
 | 
			
		||||
import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions
 | 
			
		||||
@@ -39,13 +37,12 @@ internal class IndentConfig private constructor(indentOptions: IndentOptions) {
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    @JvmStatic
 | 
			
		||||
    fun create(editor: Editor, context: DataContext): IndentConfig {
 | 
			
		||||
      return create(editor, PlatformDataKeys.PROJECT.getData(context))
 | 
			
		||||
    fun create(editor: Editor): IndentConfig {
 | 
			
		||||
      return create(editor, editor.project)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @JvmStatic
 | 
			
		||||
    @JvmOverloads
 | 
			
		||||
    fun create(editor: Editor, project: Project? = editor.project): IndentConfig {
 | 
			
		||||
    fun create(editor: Editor, project: Project?): IndentConfig {
 | 
			
		||||
      val indentOptions = if (project != null) {
 | 
			
		||||
        CodeStyle.getIndentOptions(project, editor.document)
 | 
			
		||||
      } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
 */
 | 
			
		||||
package com.maddyhome.idea.vim.extension
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.openapi.components.service
 | 
			
		||||
import com.intellij.openapi.diagnostic.logger
 | 
			
		||||
@@ -142,7 +143,7 @@ public object VimExtensionFacade {
 | 
			
		||||
   */
 | 
			
		||||
  @JvmStatic
 | 
			
		||||
  public fun executeNormalWithoutMapping(keys: List<KeyStroke>, editor: Editor) {
 | 
			
		||||
    val context = injector.executionContextManager.onEditor(editor.vim)
 | 
			
		||||
    val context = injector.executionContextManager.getEditorExecutionContext(editor.vim)
 | 
			
		||||
    val keyHandler = KeyHandler.getInstance()
 | 
			
		||||
    keys.forEach { keyHandler.handleKey(editor.vim, it, context, false, false, keyHandler.keyHandlerState) }
 | 
			
		||||
  }
 | 
			
		||||
@@ -181,8 +182,8 @@ public object VimExtensionFacade {
 | 
			
		||||
 | 
			
		||||
  /** Returns a string typed in the input box similar to 'input()'. */
 | 
			
		||||
  @JvmStatic
 | 
			
		||||
  public fun inputString(editor: Editor, prompt: String, finishOn: Char?): String {
 | 
			
		||||
    return service<CommandLineHelper>().inputString(editor.vim, prompt, finishOn) ?: ""
 | 
			
		||||
  public fun inputString(editor: Editor, context: DataContext, prompt: String, finishOn: Char?): String {
 | 
			
		||||
    return service<CommandLineHelper>().inputString(editor.vim, context.vim, prompt, finishOn) ?: ""
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** Get the current contents of the given register similar to 'getreg()'. */
 | 
			
		||||
 
 | 
			
		||||
@@ -233,7 +233,7 @@ private object FileTypePatterns {
 | 
			
		||||
    } else if (fileTypeName == "CMakeLists.txt" || fileName == "CMakeLists") {
 | 
			
		||||
      this.cMakePatterns
 | 
			
		||||
    } else {
 | 
			
		||||
      return null
 | 
			
		||||
      this.htmlPatterns
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,25 +8,21 @@
 | 
			
		||||
 | 
			
		||||
package com.maddyhome.idea.vim.extension.nerdtree
 | 
			
		||||
 | 
			
		||||
import com.intellij.ide.projectView.ProjectView
 | 
			
		||||
import com.intellij.ide.projectView.impl.AbstractProjectViewPane
 | 
			
		||||
import com.intellij.ide.projectView.impl.ProjectViewImpl
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionManager
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
 | 
			
		||||
import com.intellij.openapi.actionSystem.AnActionEvent
 | 
			
		||||
import com.intellij.openapi.actionSystem.CommonDataKeys
 | 
			
		||||
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
 | 
			
		||||
import com.intellij.openapi.actionSystem.PlatformDataKeys
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.openapi.diagnostic.logger
 | 
			
		||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
 | 
			
		||||
import com.intellij.openapi.project.DumbAwareAction
 | 
			
		||||
import com.intellij.openapi.project.Project
 | 
			
		||||
import com.intellij.openapi.project.ProjectManager
 | 
			
		||||
import com.intellij.openapi.startup.ProjectActivity
 | 
			
		||||
import com.intellij.openapi.wm.ToolWindow
 | 
			
		||||
import com.intellij.openapi.ui.getUserData
 | 
			
		||||
import com.intellij.openapi.ui.putUserData
 | 
			
		||||
import com.intellij.openapi.util.Key
 | 
			
		||||
import com.intellij.openapi.wm.ToolWindowId
 | 
			
		||||
import com.intellij.openapi.wm.ex.ToolWindowManagerEx
 | 
			
		||||
import com.intellij.openapi.wm.ex.ToolWindowManagerListener
 | 
			
		||||
import com.intellij.ui.KeyStrokeAdapter
 | 
			
		||||
import com.intellij.ui.TreeExpandCollapse
 | 
			
		||||
import com.intellij.ui.speedSearch.SpeedSearchSupply
 | 
			
		||||
@@ -53,6 +49,8 @@ import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
 | 
			
		||||
import java.awt.event.KeyEvent
 | 
			
		||||
import javax.swing.JComponent
 | 
			
		||||
import javax.swing.JTree
 | 
			
		||||
import javax.swing.KeyStroke
 | 
			
		||||
import javax.swing.SwingConstants
 | 
			
		||||
 | 
			
		||||
@@ -132,7 +130,6 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
 | 
			
		||||
    synchronized(Util.monitor) {
 | 
			
		||||
      Util.commandsRegistered = true
 | 
			
		||||
      ProjectManager.getInstance().openProjects.forEach { project -> installDispatcher(project) }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -164,39 +161,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  class ProjectViewListener(private val project: Project) : ToolWindowManagerListener {
 | 
			
		||||
    override fun toolWindowShown(toolWindow: ToolWindow) {
 | 
			
		||||
      if (ToolWindowId.PROJECT_VIEW != toolWindow.id) return
 | 
			
		||||
 | 
			
		||||
      val dispatcher = NerdDispatcher.getInstance(project)
 | 
			
		||||
      if (dispatcher.speedSearchListenerInstalled) return
 | 
			
		||||
 | 
			
		||||
      // I specify nullability explicitly as we've got a lot of exceptions saying this property is null
 | 
			
		||||
      val currentProjectViewPane: AbstractProjectViewPane? = ProjectView.getInstance(project).currentProjectViewPane
 | 
			
		||||
      val tree = currentProjectViewPane?.tree ?: return
 | 
			
		||||
      val supply = SpeedSearchSupply.getSupply(tree, true) ?: return
 | 
			
		||||
 | 
			
		||||
      // NB: Here might be some issues with concurrency, but it's not really bad, I think
 | 
			
		||||
      dispatcher.speedSearchListenerInstalled = true
 | 
			
		||||
      supply.addChangeListener {
 | 
			
		||||
        dispatcher.waitForSearch = false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO I'm not sure is this activity runs at all? Should we use [RunOnceUtil] instead?
 | 
			
		||||
  class NerdStartupActivity : ProjectActivity {
 | 
			
		||||
    override suspend fun execute(project: Project) {
 | 
			
		||||
      synchronized(Util.monitor) {
 | 
			
		||||
        if (!Util.commandsRegistered) return
 | 
			
		||||
        installDispatcher(project)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  class NerdDispatcher : DumbAwareAction() {
 | 
			
		||||
    internal var waitForSearch = false
 | 
			
		||||
    internal var speedSearchListenerInstalled = false
 | 
			
		||||
 | 
			
		||||
    override fun actionPerformed(e: AnActionEvent) {
 | 
			
		||||
      var keyStroke = getKeyStroke(e) ?: return
 | 
			
		||||
@@ -244,10 +210,6 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
      fun getInstance(project: Project): NerdDispatcher {
 | 
			
		||||
        return project.getService(NerdDispatcher::class.java)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      private const val ESCAPE_KEY_CODE = 27
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -283,19 +245,14 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapActivateNode",
 | 
			
		||||
      "o",
 | 
			
		||||
      NerdAction.Code { project, dataContext, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, dataContext, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
 | 
			
		||||
        val array = CommonDataKeys.NAVIGATABLE_ARRAY.getData(dataContext)?.filter { it.canNavigateToSource() }
 | 
			
		||||
        if (array.isNullOrEmpty()) {
 | 
			
		||||
          val row = tree.selectionRows?.getOrNull(0) ?: return@Code
 | 
			
		||||
          if (tree.isExpanded(row)) {
 | 
			
		||||
            tree.collapseRow(row)
 | 
			
		||||
          } else {
 | 
			
		||||
            tree.expandRow(row)
 | 
			
		||||
          }
 | 
			
		||||
        val row = tree.selectionRows?.getOrNull(0) ?: return@Code
 | 
			
		||||
        if (tree.isExpanded(row)) {
 | 
			
		||||
          tree.collapseRow(row)
 | 
			
		||||
        } else {
 | 
			
		||||
          array.forEach { it.navigate(true) }
 | 
			
		||||
          tree.expandRow(row)
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    )
 | 
			
		||||
@@ -374,8 +331,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapOpenRecursively",
 | 
			
		||||
      "O",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        TreeExpandCollapse.expandAll(tree)
 | 
			
		||||
        tree.selectionPath?.let {
 | 
			
		||||
          TreeUtil.scrollToVisible(tree, it, false)
 | 
			
		||||
@@ -385,8 +342,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapCloseChildren",
 | 
			
		||||
      "X",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        TreeExpandCollapse.collapse(tree)
 | 
			
		||||
        tree.selectionPath?.let {
 | 
			
		||||
          TreeUtil.scrollToVisible(tree, it, false)
 | 
			
		||||
@@ -396,8 +353,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapCloseDir",
 | 
			
		||||
      "x",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        val currentPath = tree.selectionPath ?: return@Code
 | 
			
		||||
        if (tree.isExpanded(currentPath)) {
 | 
			
		||||
          tree.collapsePath(currentPath)
 | 
			
		||||
@@ -415,8 +372,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapJumpParent",
 | 
			
		||||
      "p",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        val currentPath = tree.selectionPath ?: return@Code
 | 
			
		||||
        val parentPath = currentPath.parentPath ?: return@Code
 | 
			
		||||
        if (parentPath.parentPath != null) {
 | 
			
		||||
@@ -429,8 +386,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapJumpFirstChild",
 | 
			
		||||
      "K",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        val currentPath = tree.selectionPath ?: return@Code
 | 
			
		||||
        val parent = currentPath.parentPath ?: return@Code
 | 
			
		||||
        val row = tree.getRowForPath(parent)
 | 
			
		||||
@@ -442,8 +399,8 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "NERDTreeMapJumpLastChild",
 | 
			
		||||
      "J",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val tree = ProjectView.getInstance(project).currentProjectViewPane.tree
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        val currentPath = tree.selectionPath ?: return@Code
 | 
			
		||||
 | 
			
		||||
        val currentPathCount = currentPath.pathCount
 | 
			
		||||
@@ -488,20 +445,26 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "/",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        NerdDispatcher.getInstance(project).waitForSearch = true
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        tree.getUserData(KEY)?.waitForSearch = true
 | 
			
		||||
      },
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    registerCommand(
 | 
			
		||||
      "<ESC>",
 | 
			
		||||
      NerdAction.Code { project, _, _ ->
 | 
			
		||||
        val instance = NerdDispatcher.getInstance(project)
 | 
			
		||||
        if (instance.waitForSearch) {
 | 
			
		||||
          instance.waitForSearch = false
 | 
			
		||||
        }
 | 
			
		||||
      NerdAction.Code { _, _, e ->
 | 
			
		||||
        val tree = getTree(e) ?: return@Code
 | 
			
		||||
        tree.getUserData(KEY)?.waitForSearch = false
 | 
			
		||||
      },
 | 
			
		||||
    )
 | 
			
		||||
    
 | 
			
		||||
    for (c in ('a'..'z') + ('A'..'Z')) {
 | 
			
		||||
      val ks = KeyStroke.getKeyStroke(c)
 | 
			
		||||
      if (ks !in actionsRoot) {
 | 
			
		||||
        registerCommand(c.toString(), NerdAction.Code { _, _, _ -> })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  object Util {
 | 
			
		||||
@@ -526,6 +489,21 @@ internal class NerdTree : VimExtension {
 | 
			
		||||
  companion object {
 | 
			
		||||
    const val pluginName = "NERDTree"
 | 
			
		||||
    private val LOG = logger<NerdTree>()
 | 
			
		||||
    private val KEY = Key.create<NerdDispatcher>("IdeaVim-NerdTree-Dispatcher")
 | 
			
		||||
 | 
			
		||||
    fun installDispatcher(component: JComponent) {
 | 
			
		||||
      if (component.getUserData(KEY) != null) return
 | 
			
		||||
 | 
			
		||||
      val dispatcher = NerdDispatcher()
 | 
			
		||||
      component.putUserData(KEY, dispatcher)
 | 
			
		||||
 | 
			
		||||
      val shortcuts = collectShortcuts(actionsRoot).map { RequiredShortcut(it, MappingOwner.Plugin.get(pluginName)) }
 | 
			
		||||
      dispatcher.registerCustomShortcutSet(KeyGroup.toShortcutSet(shortcuts), component)
 | 
			
		||||
 | 
			
		||||
      SpeedSearchSupply.getSupply(component, true)?.addChangeListener {
 | 
			
		||||
        dispatcher.waitForSearch = false
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -560,12 +538,6 @@ private fun collectShortcuts(node: Node<NerdAction>): Set<KeyStroke> {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun installDispatcher(project: Project) {
 | 
			
		||||
  val dispatcher = NerdTree.NerdDispatcher.getInstance(project)
 | 
			
		||||
  val shortcuts =
 | 
			
		||||
    collectShortcuts(actionsRoot).map { RequiredShortcut(it, MappingOwner.Plugin.get(NerdTree.pluginName)) }
 | 
			
		||||
  dispatcher.registerCustomShortcutSet(
 | 
			
		||||
    KeyGroup.toShortcutSet(shortcuts),
 | 
			
		||||
    (ProjectView.getInstance(project) as ProjectViewImpl).component,
 | 
			
		||||
  )
 | 
			
		||||
private fun getTree(e: AnActionEvent): JTree? {
 | 
			
		||||
  return e.dataContext.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT) as? JTree
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
package com.maddyhome.idea.vim.extension.nerdtree
 | 
			
		||||
 | 
			
		||||
import com.intellij.ide.ApplicationInitializedListener
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.util.ui.StartupUiUtil
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import java.awt.AWTEvent
 | 
			
		||||
import java.awt.event.FocusEvent
 | 
			
		||||
import javax.swing.JTree
 | 
			
		||||
 | 
			
		||||
@Suppress("UnstableApiUsage")
 | 
			
		||||
internal class NerdTreeApplicationListener : ApplicationInitializedListener {
 | 
			
		||||
  override suspend fun execute(asyncScope: CoroutineScope) {
 | 
			
		||||
    StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, ApplicationManager.getApplication().getService(NerdTreeDisposableService::class.java))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun handleEvent(event: AWTEvent) {
 | 
			
		||||
    if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED) {
 | 
			
		||||
      val source = event.source
 | 
			
		||||
      if (source is JTree) {
 | 
			
		||||
        NerdTree.installDispatcher(source)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
package com.maddyhome.idea.vim.extension.nerdtree
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.Disposable
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
internal class NerdTreeDisposableService : Disposable {
 | 
			
		||||
  override fun dispose() {}
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
 | 
			
		||||
package com.maddyhome.idea.vim.extension.replacewithregister
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
@@ -64,7 +65,7 @@ internal class ReplaceWithRegister : VimExtension {
 | 
			
		||||
        val selectionEnd = caret.selectionEnd
 | 
			
		||||
 | 
			
		||||
        val visualSelection = caret to VimSelection.create(selectionStart, selectionEnd - 1, typeInEditor, editor)
 | 
			
		||||
        doReplace(editor.ij, caret, PutData.VisualSelection(mapOf(visualSelection), typeInEditor))
 | 
			
		||||
        doReplace(editor.ij, context.ij, caret, PutData.VisualSelection(mapOf(visualSelection), typeInEditor))
 | 
			
		||||
      }
 | 
			
		||||
      editor.exitVisualMode()
 | 
			
		||||
    }
 | 
			
		||||
@@ -92,7 +93,7 @@ internal class ReplaceWithRegister : VimExtension {
 | 
			
		||||
        val visualSelection = caret to VimSelection.create(lineStart, lineEnd, SelectionType.LINE_WISE, editor)
 | 
			
		||||
        caretsAndSelections += visualSelection
 | 
			
		||||
 | 
			
		||||
        doReplace(editor.ij, caret, PutData.VisualSelection(mapOf(visualSelection), SelectionType.LINE_WISE))
 | 
			
		||||
        doReplace(editor.ij, context.ij, caret, PutData.VisualSelection(mapOf(visualSelection), SelectionType.LINE_WISE))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      editor.sortedCarets().forEach { caret ->
 | 
			
		||||
@@ -120,7 +121,7 @@ internal class ReplaceWithRegister : VimExtension {
 | 
			
		||||
        selectionType ?: SelectionType.CHARACTER_WISE,
 | 
			
		||||
      )
 | 
			
		||||
      // todo multicaret
 | 
			
		||||
      doReplace(ijEditor, editor.primaryCaret(), visualSelection)
 | 
			
		||||
      doReplace(ijEditor, context.ij, editor.primaryCaret(), visualSelection)
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -140,7 +141,7 @@ internal class ReplaceWithRegister : VimExtension {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun doReplace(editor: Editor, caret: ImmutableVimCaret, visualSelection: PutData.VisualSelection) {
 | 
			
		||||
private fun doReplace(editor: Editor, context: DataContext, caret: ImmutableVimCaret, visualSelection: PutData.VisualSelection) {
 | 
			
		||||
  val registerGroup = injector.registerGroup
 | 
			
		||||
  val lastRegisterChar = if (editor.caretModel.caretCount == 1) registerGroup.currentRegister else registerGroup.getCurrentRegisterForMulticaret()
 | 
			
		||||
  val savedRegister = caret.registerStorage.getRegister(lastRegisterChar) ?: return
 | 
			
		||||
@@ -168,7 +169,7 @@ private fun doReplace(editor: Editor, caret: ImmutableVimCaret, visualSelection:
 | 
			
		||||
  ClipboardOptionHelper.IdeaputDisabler().use {
 | 
			
		||||
    VimPlugin.getPut().putText(
 | 
			
		||||
      vimEditor,
 | 
			
		||||
      injector.executionContextManager.onEditor(editor.vim),
 | 
			
		||||
      context.vim,
 | 
			
		||||
      putData,
 | 
			
		||||
      operatorArguments = OperatorArguments(
 | 
			
		||||
        editor.vimStateMachine?.isOperatorPending(vimEditor.mode) ?: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -118,7 +118,7 @@ internal class IdeaVimSneakExtension : VimExtension {
 | 
			
		||||
    var lastSymbols: String = ""
 | 
			
		||||
    fun jumpTo(editor: VimEditor, charone: Char, chartwo: Char, sneakDirection: Direction): TextRange? {
 | 
			
		||||
      val caret = editor.primaryCaret()
 | 
			
		||||
      val position = caret.offset.point
 | 
			
		||||
      val position = caret.offset
 | 
			
		||||
      val chars = editor.text()
 | 
			
		||||
      val foundPosition = sneakDirection.findBiChar(editor, chars, position, charone, chartwo)
 | 
			
		||||
      if (foundPosition != null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,30 @@
 | 
			
		||||
package com.maddyhome.idea.vim.extension.surround
 | 
			
		||||
 | 
			
		||||
import com.intellij.util.text.CharSequenceSubSequence
 | 
			
		||||
 | 
			
		||||
internal data class RepeatedCharSequence(val text: CharSequence, val count: Int) : CharSequence {
 | 
			
		||||
  override val length = text.length * count
 | 
			
		||||
 | 
			
		||||
  override fun get(index: Int): Char {
 | 
			
		||||
    if (index < 0 || index >= length) throw IndexOutOfBoundsException()
 | 
			
		||||
    return text[index % text.length]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun subSequence(startIndex: Int, endIndex: Int): CharSequence {
 | 
			
		||||
    return CharSequenceSubSequence(this, startIndex, endIndex)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun toString(): String {
 | 
			
		||||
    return text.repeat(count)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  companion object {
 | 
			
		||||
    fun of(text: CharSequence, count: Int): CharSequence {
 | 
			
		||||
      return when (count) {
 | 
			
		||||
        0 -> ""
 | 
			
		||||
        1 -> text
 | 
			
		||||
        else -> RepeatedCharSequence(text, count)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,12 +7,14 @@
 | 
			
		||||
 */
 | 
			
		||||
package com.maddyhome.idea.vim.extension.surround
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.application.runWriteAction
 | 
			
		||||
import com.intellij.openapi.diagnostic.logger
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimChangeGroup
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.endsWithNewLine
 | 
			
		||||
import com.maddyhome.idea.vim.api.getLeadingCharacterOffset
 | 
			
		||||
@@ -33,7 +35,11 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMa
 | 
			
		||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing
 | 
			
		||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret
 | 
			
		||||
import com.maddyhome.idea.vim.extension.exportOperatorFunction
 | 
			
		||||
import com.maddyhome.idea.vim.group.findBlockRange
 | 
			
		||||
import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore
 | 
			
		||||
import com.maddyhome.idea.vim.key.OperatorFunction
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper
 | 
			
		||||
@@ -76,7 +82,7 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
      putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator())
 | 
			
		||||
    VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false, count = 1)) // TODO
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private class YSurroundHandler : ExtensionHandler {
 | 
			
		||||
@@ -95,7 +101,7 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
      val ijEditor = editor.ij
 | 
			
		||||
      val c = getChar(ijEditor)
 | 
			
		||||
      if (c.code == 0) return
 | 
			
		||||
      val pair = getOrInputPair(c, ijEditor) ?: return
 | 
			
		||||
      val pair = getOrInputPair(c, ijEditor, context.ij) ?: return
 | 
			
		||||
 | 
			
		||||
      editor.forEachCaret {
 | 
			
		||||
        val line = it.getBufferPosition().line
 | 
			
		||||
@@ -104,7 +110,7 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
        val lastNonWhiteSpaceOffset = getLastNonWhitespaceCharacterOffset(editor.text(), lineStartOffset, lineEndOffset)
 | 
			
		||||
        if (lastNonWhiteSpaceOffset != null) {
 | 
			
		||||
          val range = TextRange(lineStartOffset, lastNonWhiteSpaceOffset + 1)
 | 
			
		||||
          performSurround(pair, range, it)
 | 
			
		||||
          performSurround(pair, range, it, count = operatorArguments.count1)
 | 
			
		||||
        }
 | 
			
		||||
//        it.moveToOffset(lineStartOffset)
 | 
			
		||||
      }
 | 
			
		||||
@@ -124,15 +130,13 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
 | 
			
		||||
  private class VSurroundHandler : ExtensionHandler {
 | 
			
		||||
    override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
 | 
			
		||||
      val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart
 | 
			
		||||
      // NB: Operator ignores SelectionType anyway
 | 
			
		||||
      if (!Operator().apply(editor, context, editor.mode.selectionType)) {
 | 
			
		||||
      if (!Operator(supportsMultipleCursors = true, count = operatorArguments.count1).apply(editor, context, editor.mode.selectionType)) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      runWriteAction {
 | 
			
		||||
        // Leave visual mode
 | 
			
		||||
        executeNormalWithoutMapping(injector.parser.parseKeys("<Esc>"), editor.ij)
 | 
			
		||||
        editor.ij.caretModel.moveToOffset(selectionStart)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -147,12 +151,16 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
      val charTo = getChar(editor.ij)
 | 
			
		||||
      if (charTo.code == 0) return
 | 
			
		||||
 | 
			
		||||
      val newSurround = getOrInputPair(charTo, editor.ij) ?: return
 | 
			
		||||
      val newSurround = getOrInputPair(charTo, editor.ij, context.ij) ?: return
 | 
			
		||||
      runWriteAction { change(editor, context, charFrom, newSurround) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
      fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: Pair<String, String>?) {
 | 
			
		||||
        editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: Pair<String, String>?) {
 | 
			
		||||
        // Save old register values for carets
 | 
			
		||||
        val surroundings = editor.sortedCarets()
 | 
			
		||||
          .map {
 | 
			
		||||
@@ -220,12 +228,12 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
        val searchHelper = injector.searchHelper
 | 
			
		||||
        return when (char) {
 | 
			
		||||
          't' -> searchHelper.findBlockTagRange(editor, caret, 1, true)
 | 
			
		||||
          '(', ')', 'b' -> searchHelper.findBlockRange(editor, caret, '(', 1, true)
 | 
			
		||||
          '[', ']' -> searchHelper.findBlockRange(editor, caret, '[', 1, true)
 | 
			
		||||
          '{', '}', 'B' -> searchHelper.findBlockRange(editor, caret, '{', 1, true)
 | 
			
		||||
          '<', '>' -> searchHelper.findBlockRange(editor, caret, '<', 1, true)
 | 
			
		||||
          '(', ')', 'b' -> findBlockRange(editor, caret, '(', 1, true)
 | 
			
		||||
          '[', ']' -> findBlockRange(editor, caret, '[', 1, true)
 | 
			
		||||
          '{', '}', 'B' -> findBlockRange(editor, caret, '{', 1, true)
 | 
			
		||||
          '<', '>' -> findBlockRange(editor, caret, '<', 1, true)
 | 
			
		||||
          '`', '\'', '"' -> {
 | 
			
		||||
            val caretOffset = caret.offset.point
 | 
			
		||||
            val caretOffset = caret.offset
 | 
			
		||||
            val text = editor.text()
 | 
			
		||||
            if (text.getOrNull(caretOffset - 1) == char && text.getOrNull(caretOffset) == char) {
 | 
			
		||||
              TextRange(caretOffset - 1, caretOffset + 1)
 | 
			
		||||
@@ -260,21 +268,42 @@ internal class VimSurroundExtension : VimExtension {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private class Operator : OperatorFunction {
 | 
			
		||||
    override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
 | 
			
		||||
      val ijEditor = editor.ij
 | 
			
		||||
  private class Operator(private val supportsMultipleCursors: Boolean, private val count: Int) : OperatorFunction {
 | 
			
		||||
    override fun apply(vimEditor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean {
 | 
			
		||||
      val ijEditor = vimEditor.ij
 | 
			
		||||
      val c = getChar(ijEditor)
 | 
			
		||||
      if (c.code == 0) return true
 | 
			
		||||
 | 
			
		||||
      val pair = getOrInputPair(c, ijEditor) ?: return false
 | 
			
		||||
      // XXX: Will it work with line-wise or block-wise selections?
 | 
			
		||||
      val range = getSurroundRange(editor.currentCaret()) ?: return false
 | 
			
		||||
      performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE)
 | 
			
		||||
      // Jump back to start
 | 
			
		||||
      executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
 | 
			
		||||
      val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false
 | 
			
		||||
 | 
			
		||||
      runWriteAction {
 | 
			
		||||
        val change = VimPlugin.getChange()
 | 
			
		||||
        if (supportsMultipleCursors) {
 | 
			
		||||
          ijEditor.runWithEveryCaretAndRestore {
 | 
			
		||||
            applyOnce(ijEditor, change, pair, count)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
          applyOnce(ijEditor, change, pair, count)
 | 
			
		||||
          // Jump back to start
 | 
			
		||||
          executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: Pair<String, String>, count: Int) {
 | 
			
		||||
      // XXX: Will it work with line-wise or block-wise selections?
 | 
			
		||||
      val primaryCaret = editor.caretModel.primaryCaret
 | 
			
		||||
      val range = getSurroundRange(primaryCaret.vim)
 | 
			
		||||
      if (range != null) {
 | 
			
		||||
        val start = RepeatedCharSequence.of(pair.first, count)
 | 
			
		||||
        val end = RepeatedCharSequence.of(pair.second, count)
 | 
			
		||||
        change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, start)
 | 
			
		||||
        change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.endOffset + start.length, end)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun getSurroundRange(caret: VimCaret): TextRange? {
 | 
			
		||||
      val editor = caret.editor
 | 
			
		||||
      val ijEditor = editor.ij
 | 
			
		||||
@@ -319,8 +348,8 @@ private fun getSurroundPair(c: Char): Pair<String, String>? = if (c in SURROUND_
 | 
			
		||||
  null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun inputTagPair(editor: Editor): Pair<String, String>? {
 | 
			
		||||
  val tagInput = inputString(editor, "<", '>')
 | 
			
		||||
private fun inputTagPair(editor: Editor, context: DataContext): Pair<String, String>? {
 | 
			
		||||
  val tagInput = inputString(editor, context, "<", '>')
 | 
			
		||||
  val matcher = tagNameAndAttributesCapturePattern.matcher(tagInput)
 | 
			
		||||
  return if (matcher.find()) {
 | 
			
		||||
    val tagName = matcher.group(1)
 | 
			
		||||
@@ -333,17 +362,18 @@ private fun inputTagPair(editor: Editor): Pair<String, String>? {
 | 
			
		||||
 | 
			
		||||
private fun inputFunctionName(
 | 
			
		||||
  editor: Editor,
 | 
			
		||||
  context: DataContext,
 | 
			
		||||
  withInternalSpaces: Boolean,
 | 
			
		||||
): Pair<String, String>? {
 | 
			
		||||
  val functionNameInput = inputString(editor, "function: ", null)
 | 
			
		||||
  val functionNameInput = inputString(editor, context, "function: ", null)
 | 
			
		||||
  if (functionNameInput.isEmpty()) return null
 | 
			
		||||
  return if (withInternalSpaces) "$functionNameInput( " to " )" else "$functionNameInput(" to ")"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun getOrInputPair(c: Char, editor: Editor): Pair<String, String>? = when (c) {
 | 
			
		||||
  '<', 't' -> inputTagPair(editor)
 | 
			
		||||
  'f' -> inputFunctionName(editor, false)
 | 
			
		||||
  'F' -> inputFunctionName(editor, true)
 | 
			
		||||
private fun getOrInputPair(c: Char, editor: Editor, context: DataContext): Pair<String, String>? = when (c) {
 | 
			
		||||
  '<', 't' -> inputTagPair(editor, context)
 | 
			
		||||
  'f' -> inputFunctionName(editor, context, false)
 | 
			
		||||
  'F' -> inputFunctionName(editor, context, true)
 | 
			
		||||
  else -> getSurroundPair(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -359,15 +389,15 @@ private fun getChar(editor: Editor): Char {
 | 
			
		||||
  return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private fun performSurround(pair: Pair<String, String>, range: TextRange, caret: VimCaret, tagsOnNewLines: Boolean = false) {
 | 
			
		||||
private fun performSurround(pair: Pair<String, String>, range: TextRange, caret: VimCaret, count: Int, tagsOnNewLines: Boolean = false) {
 | 
			
		||||
  runWriteAction {
 | 
			
		||||
    val editor = caret.editor
 | 
			
		||||
    val change = VimPlugin.getChange()
 | 
			
		||||
    val leftSurround = pair.first + if (tagsOnNewLines) "\n" else ""
 | 
			
		||||
    val leftSurround = RepeatedCharSequence.of(pair.first + if (tagsOnNewLines) "\n" else "", count)
 | 
			
		||||
 | 
			
		||||
    val isEOF = range.endOffset == editor.text().length
 | 
			
		||||
    val hasNewLine = editor.endsWithNewLine()
 | 
			
		||||
    val rightSurround = if (tagsOnNewLines) {
 | 
			
		||||
    val rightSurround = (if (tagsOnNewLines) {
 | 
			
		||||
      if (isEOF && !hasNewLine) {
 | 
			
		||||
        "\n" + pair.second
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -375,7 +405,7 @@ private fun performSurround(pair: Pair<String, String>, range: TextRange, caret:
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      pair.second
 | 
			
		||||
    }
 | 
			
		||||
    }).let { RepeatedCharSequence.of(it, count) }
 | 
			
		||||
 | 
			
		||||
    change.insertText(editor, caret, range.startOffset, leftSurround)
 | 
			
		||||
    change.insertText(editor, caret, range.endOffset + leftSurround.length, rightSurround)
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,6 @@ import java.math.BigInteger
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.function.Consumer
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides all the insert/replace related functionality
 | 
			
		||||
@@ -198,7 +197,7 @@ public class ChangeGroup : VimChangeGroupBase() {
 | 
			
		||||
    val allowWrap = injector.options(editor).whichwrap.contains("~")
 | 
			
		||||
    var motion = injector.motion.getHorizontalMotion(editor, caret, count, true, allowWrap)
 | 
			
		||||
    if (motion is Motion.Error) return false
 | 
			
		||||
    changeCase(editor, caret, caret.offset.point, (motion as AbsoluteOffset).offset, CharacterHelper.CASE_TOGGLE)
 | 
			
		||||
    changeCase(editor, caret, caret.offset, (motion as AbsoluteOffset).offset, CharacterHelper.CASE_TOGGLE)
 | 
			
		||||
    motion = injector.motion.getHorizontalMotion(
 | 
			
		||||
      editor,
 | 
			
		||||
      caret,
 | 
			
		||||
@@ -236,8 +235,7 @@ public class ChangeGroup : VimChangeGroupBase() {
 | 
			
		||||
      }
 | 
			
		||||
      val lineLength = editor.lineLength(line)
 | 
			
		||||
      if (column < VimMotionGroupBase.LAST_COLUMN && lineLength < column) {
 | 
			
		||||
        val pad =
 | 
			
		||||
          EditorHelper.pad((editor as IjVimEditor).editor, (context as IjEditorExecutionContext).context, line, column)
 | 
			
		||||
        val pad = EditorHelper.pad((editor as IjVimEditor).editor, line, column)
 | 
			
		||||
        val offset = editor.getLineEndOffset(line)
 | 
			
		||||
        insertText(editor, caret, offset, pad)
 | 
			
		||||
      }
 | 
			
		||||
@@ -396,6 +394,7 @@ public class ChangeGroup : VimChangeGroupBase() {
 | 
			
		||||
    context: ExecutionContext,
 | 
			
		||||
    range: TextRange,
 | 
			
		||||
  ) {
 | 
			
		||||
    val startPos = editor.offsetToBufferPosition(caret.offset)
 | 
			
		||||
    val startOffset = editor.getLineStartForOffset(range.startOffset)
 | 
			
		||||
    val endOffset = editor.getLineEndForOffset(range.endOffset)
 | 
			
		||||
    val ijEditor = (editor as IjVimEditor).editor
 | 
			
		||||
@@ -420,11 +419,7 @@ public class ChangeGroup : VimChangeGroupBase() {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    val afterAction = {
 | 
			
		||||
      val firstLine = editor.offsetToBufferPosition(
 | 
			
		||||
        min(startOffset.toDouble(), endOffset.toDouble()).toInt()
 | 
			
		||||
      ).line
 | 
			
		||||
      val newOffset = injector.motion.moveCaretToLineStartSkipLeading(editor, firstLine)
 | 
			
		||||
      caret.moveToOffset(newOffset)
 | 
			
		||||
      caret.moveToOffset(injector.motion.moveCaretToLineStartSkipLeading(editor, startPos.line))
 | 
			
		||||
      restoreCursor(editor, caret, (caret as IjVimCaret).caret.logicalPosition.line)
 | 
			
		||||
    }
 | 
			
		||||
    if (project != null) {
 | 
			
		||||
@@ -455,7 +450,7 @@ public class ChangeGroup : VimChangeGroupBase() {
 | 
			
		||||
    dir: Int,
 | 
			
		||||
    operatorArguments: OperatorArguments,
 | 
			
		||||
  ) {
 | 
			
		||||
    val start = caret.offset.point
 | 
			
		||||
    val start = caret.offset
 | 
			
		||||
    val end = injector.motion.moveCaretToRelativeLineEnd(editor, caret, lines - 1, true)
 | 
			
		||||
    indentRange(editor, caret, context, TextRange(start, end), 1, dir, operatorArguments)
 | 
			
		||||
  }
 | 
			
		||||
@@ -489,7 +484,7 @@ public class ChangeGroup : VimChangeGroupBase() {
 | 
			
		||||
 | 
			
		||||
    // Remember the current caret column
 | 
			
		||||
    val intendedColumn = caret.vimLastColumn
 | 
			
		||||
    val indentConfig = create((editor as IjVimEditor).editor, (context as IjEditorExecutionContext).context)
 | 
			
		||||
    val indentConfig = create((editor as IjVimEditor).editor)
 | 
			
		||||
    val sline = editor.offsetToBufferPosition(range.startOffset).line
 | 
			
		||||
    val endLogicalPosition = editor.offsetToBufferPosition(range.endOffset)
 | 
			
		||||
    val eline = if (endLogicalPosition.column == 0) max((endLogicalPosition.line - 1).toDouble(), 0.0)
 | 
			
		||||
 
 | 
			
		||||
@@ -235,7 +235,7 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
 | 
			
		||||
    // Note that we need a similar check in `VimEditor.isWritable` to allow Escape to work to exit insert mode. We need
 | 
			
		||||
    // to know that a read-only editor that is hosting a console view with a running process can be treated as writable.
 | 
			
		||||
    Runnable switchToInsertMode = () -> {
 | 
			
		||||
      ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(editor), null);
 | 
			
		||||
      ExecutionContext context = injector.getExecutionContextManager().getEditorExecutionContext(new IjVimEditor(editor));
 | 
			
		||||
      VimPlugin.getChange().insertBeforeCursor(new IjVimEditor(editor), context);
 | 
			
		||||
      KeyHandler.getInstance().reset(new IjVimEditor(editor));
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,12 @@ import com.intellij.openapi.fileEditor.impl.EditorsSplitters;
 | 
			
		||||
import com.intellij.openapi.fileTypes.FileType;
 | 
			
		||||
import com.intellij.openapi.fileTypes.FileTypeManager;
 | 
			
		||||
import com.intellij.openapi.project.Project;
 | 
			
		||||
import com.intellij.openapi.project.ProjectManager;
 | 
			
		||||
import com.intellij.openapi.roots.ProjectRootManager;
 | 
			
		||||
import com.intellij.openapi.vfs.LocalFileSystem;
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFile;
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFileManager;
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFileSystem;
 | 
			
		||||
import com.intellij.psi.search.FilenameIndex;
 | 
			
		||||
import com.intellij.psi.search.GlobalSearchScope;
 | 
			
		||||
import com.intellij.psi.search.ProjectScope;
 | 
			
		||||
@@ -44,6 +47,7 @@ import org.jetbrains.annotations.NotNull;
 | 
			
		||||
import org.jetbrains.annotations.Nullable;
 | 
			
		||||
 | 
			
		||||
import java.io.File;
 | 
			
		||||
import java.util.Arrays;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
 | 
			
		||||
import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
 | 
			
		||||
@@ -445,4 +449,28 @@ public class FileGroup extends VimFileBase {
 | 
			
		||||
      LastTabService.getInstance(event.getManager().getProject()).setLastTab(event.getOldFile());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Nullable
 | 
			
		||||
  @Override
 | 
			
		||||
  public VimEditor selectEditor(@NotNull String projectId, @NotNull String documentPath, @Nullable String protocol) {
 | 
			
		||||
    VirtualFileSystem fileSystem = VirtualFileManager.getInstance().getFileSystem(protocol);
 | 
			
		||||
    if (fileSystem == null) return null;
 | 
			
		||||
    VirtualFile virtualFile = fileSystem.findFileByPath(documentPath);
 | 
			
		||||
    if (virtualFile == null) return null;
 | 
			
		||||
 | 
			
		||||
    Project project = Arrays.stream(ProjectManager.getInstance().getOpenProjects())
 | 
			
		||||
      .filter(p -> injector.getFile().getProjectId(p).equals(projectId))
 | 
			
		||||
      .findFirst().orElseThrow();
 | 
			
		||||
 | 
			
		||||
    Editor editor = selectEditor(project, virtualFile);
 | 
			
		||||
    if (editor == null) return null;
 | 
			
		||||
    return new IjVimEditor(editor);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @NotNull
 | 
			
		||||
  @Override
 | 
			
		||||
  public String getProjectId(@NotNull Object project) {
 | 
			
		||||
    if (!(project instanceof Project)) throw new IllegalArgumentException();
 | 
			
		||||
    return ((Project) project).getName();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ public object IjOptions {
 | 
			
		||||
  public val closenotebooks: ToggleOption = addOption(ToggleOption("closenotebooks", GLOBAL, "closenotebooks", true, isHidden = true))
 | 
			
		||||
  public val commandOrMotionAnnotation: ToggleOption = addOption(ToggleOption("commandormotionannotation", GLOBAL, "commandormotionannotation", true, isHidden = true))
 | 
			
		||||
  public val exCommandAnnotation: ToggleOption = addOption(ToggleOption("excommandannotation", GLOBAL, "excommandannotation", true, isHidden = true))
 | 
			
		||||
  public val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", false, isHidden = true))
 | 
			
		||||
  public val oldundo: ToggleOption = addOption(ToggleOption("oldundo", GLOBAL, "oldundo", true, isHidden = true))
 | 
			
		||||
  public val unifyjumps: ToggleOption = addOption(ToggleOption("unifyjumps", GLOBAL, "unifyjumps", true, isHidden = true))
 | 
			
		||||
  public val useNewRegex: ToggleOption = addOption(ToggleOption("usenewregex", GLOBAL, "usenewregex", true, isHidden = true))
 | 
			
		||||
  public val vimscriptFunctionAnnotation: ToggleOption = addOption(ToggleOption("vimscriptfunctionannotation", GLOBAL, "vimscriptfunctionannotation", true, isHidden = true))
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Copyright 2003-2024 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.lang.CodeDocumentationAwareCommenter
 | 
			
		||||
import com.intellij.lang.LanguageCommenters
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
import com.intellij.psi.PsiComment
 | 
			
		||||
import com.intellij.psi.util.PsiTreeUtil
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimPsiService
 | 
			
		||||
import com.maddyhome.idea.vim.common.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.helper.PsiHelper
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
public class IjVimPsiService: VimPsiService {
 | 
			
		||||
  override fun getCommentAtPos(editor: VimEditor, pos: Int): Pair<TextRange, Pair<String, String>?>? {
 | 
			
		||||
    val psiFile = PsiHelper.getFile(editor.ij) ?: return null
 | 
			
		||||
    val psiElement = psiFile.findElementAt(pos) ?: return null
 | 
			
		||||
    val language = psiElement.language
 | 
			
		||||
    val commenter = LanguageCommenters.INSTANCE.forLanguage(language)
 | 
			
		||||
    val psiComment = PsiTreeUtil.getParentOfType(psiElement, PsiComment::class.java, false) ?: return null
 | 
			
		||||
    val commentText = psiComment.text
 | 
			
		||||
 | 
			
		||||
    val blockCommentPrefix = commenter.blockCommentPrefix
 | 
			
		||||
    val blockCommentSuffix = commenter.blockCommentSuffix
 | 
			
		||||
 | 
			
		||||
    val docCommentPrefix = (commenter as? CodeDocumentationAwareCommenter)?.documentationCommentPrefix
 | 
			
		||||
    val docCommentSuffix = (commenter as? CodeDocumentationAwareCommenter)?.documentationCommentSuffix
 | 
			
		||||
 | 
			
		||||
    val prefixToSuffix: Pair<String, String>? =
 | 
			
		||||
      if (docCommentPrefix != null && docCommentSuffix != null && commentText.startsWith(docCommentPrefix) && commentText.endsWith(docCommentSuffix)) {
 | 
			
		||||
        docCommentPrefix to docCommentSuffix
 | 
			
		||||
      }
 | 
			
		||||
      else if (blockCommentPrefix != null && blockCommentSuffix != null && commentText.startsWith(blockCommentPrefix) && commentText.endsWith(blockCommentSuffix)) {
 | 
			
		||||
        blockCommentPrefix to blockCommentSuffix
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        null
 | 
			
		||||
      }
 | 
			
		||||
    return Pair(psiComment.textRange.vim, prefixToSuffix)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getDoubleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
 | 
			
		||||
    // TODO[ideavim] It wasn't implemented before, but implementing it will significantly improve % motion
 | 
			
		||||
    return getDoubleQuotesRangeNoPSI(editor.text(), pos, isInner)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getSingleQuotedString(editor: VimEditor, pos: Int, isInner: Boolean): TextRange? {
 | 
			
		||||
    // TODO[ideavim] It wasn't implemented before, but implementing it will significantly improve % motion
 | 
			
		||||
    return getSingleQuotesRangeNoPSI(editor.text(), pos, isInner)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
package com.maddyhome.idea.vim.group
 | 
			
		||||
 | 
			
		||||
import com.intellij.codeInsight.daemon.ReferenceImporter
 | 
			
		||||
import com.intellij.openapi.actionSystem.CommonDataKeys
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.openapi.application.ReadAction
 | 
			
		||||
import com.intellij.openapi.command.WriteCommandAction
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileDocumentManager
 | 
			
		||||
import com.intellij.openapi.progress.ProgressIndicator
 | 
			
		||||
import com.intellij.openapi.progress.ProgressManager
 | 
			
		||||
import com.intellij.openapi.progress.Task
 | 
			
		||||
import com.intellij.psi.PsiDocumentManager
 | 
			
		||||
import com.intellij.psi.PsiElement
 | 
			
		||||
import com.intellij.psi.PsiRecursiveElementWalkingVisitor
 | 
			
		||||
import java.util.function.BooleanSupplier
 | 
			
		||||
 | 
			
		||||
internal object MacroAutoImport {
 | 
			
		||||
  fun run(editor: Editor, dataContext: DataContext) {
 | 
			
		||||
    val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return
 | 
			
		||||
    val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: return
 | 
			
		||||
 | 
			
		||||
    if (!FileDocumentManager.getInstance().requestWriting(editor.document, project)) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val importers = ReferenceImporter.EP_NAME.extensionList
 | 
			
		||||
    if (importers.isEmpty()) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ProgressManager.getInstance().run(object : Task.Backgroundable(project, "Auto import", true) {
 | 
			
		||||
      override fun run(indicator: ProgressIndicator) {
 | 
			
		||||
        val fixes = ReadAction.nonBlocking<List<BooleanSupplier>> {
 | 
			
		||||
          val fixes = mutableListOf<BooleanSupplier>()
 | 
			
		||||
 | 
			
		||||
          file.accept(object : PsiRecursiveElementWalkingVisitor() {
 | 
			
		||||
            override fun visitElement(element: PsiElement) {
 | 
			
		||||
              for (reference in element.references) {
 | 
			
		||||
                if (reference.resolve() != null) {
 | 
			
		||||
                  continue
 | 
			
		||||
                }
 | 
			
		||||
                for (importer in importers) {
 | 
			
		||||
                  importer.computeAutoImportAtOffset(editor, file, element.textRange.startOffset, true)
 | 
			
		||||
                    ?.let(fixes::add)
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              super.visitElement(element)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          return@nonBlocking fixes
 | 
			
		||||
        }.executeSynchronously()
 | 
			
		||||
 | 
			
		||||
        ApplicationManager.getApplication().invokeAndWait {
 | 
			
		||||
          WriteCommandAction.writeCommandAction(project)
 | 
			
		||||
            .withName("Auto Import")
 | 
			
		||||
            .withGroupId("IdeaVimAutoImportAfterMacro")
 | 
			
		||||
            .shouldRecordActionForActiveDocument(true)
 | 
			
		||||
            .run<RuntimeException> {
 | 
			
		||||
              fixes.forEach { it.asBoolean }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -22,6 +22,7 @@ import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.helper.MessageHelper.message
 | 
			
		||||
import com.maddyhome.idea.vim.macro.VimMacroBase
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Used to handle playback of macros
 | 
			
		||||
@@ -93,6 +94,9 @@ internal class MacroGroup : VimMacroBase() {
 | 
			
		||||
        } finally {
 | 
			
		||||
          keyStack.removeFirst()
 | 
			
		||||
        }
 | 
			
		||||
        if (!isInternalMacro) {
 | 
			
		||||
          MacroAutoImport.run(editor.ij, context.ij)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (isInternalMacro) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,41 +11,24 @@ import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
import com.intellij.openapi.editor.Caret
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.editor.LogicalPosition
 | 
			
		||||
import com.intellij.openapi.editor.VisualPosition
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
 | 
			
		||||
import com.intellij.openapi.fileEditor.TextEditor
 | 
			
		||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
 | 
			
		||||
import com.intellij.openapi.fileEditor.impl.EditorWindow
 | 
			
		||||
import com.intellij.openapi.project.Project
 | 
			
		||||
import com.intellij.openapi.vfs.LocalFileSystem
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFile
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFileManager
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFileSystem
 | 
			
		||||
import com.intellij.util.MathUtil.clamp
 | 
			
		||||
import com.maddyhome.idea.vim.KeyHandler
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.BufferPosition
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimChangeGroupBase
 | 
			
		||||
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
 | 
			
		||||
import com.maddyhome.idea.vim.api.lineLength
 | 
			
		||||
import com.maddyhome.idea.vim.api.normalizeColumn
 | 
			
		||||
import com.maddyhome.idea.vim.api.normalizeLine
 | 
			
		||||
import com.maddyhome.idea.vim.api.normalizeOffset
 | 
			
		||||
import com.maddyhome.idea.vim.api.normalizeVisualColumn
 | 
			
		||||
import com.maddyhome.idea.vim.api.normalizeVisualLine
 | 
			
		||||
import com.maddyhome.idea.vim.api.options
 | 
			
		||||
import com.maddyhome.idea.vim.api.visualLineToBufferLine
 | 
			
		||||
import com.maddyhome.idea.vim.command.Argument
 | 
			
		||||
import com.maddyhome.idea.vim.command.MotionType
 | 
			
		||||
@@ -54,12 +37,9 @@ import com.maddyhome.idea.vim.common.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.ex.ExOutputModel
 | 
			
		||||
import com.maddyhome.idea.vim.handler.Motion
 | 
			
		||||
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
 | 
			
		||||
import com.maddyhome.idea.vim.handler.Motion.AdjustedOffset
 | 
			
		||||
import com.maddyhome.idea.vim.handler.MotionActionHandler
 | 
			
		||||
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
 | 
			
		||||
import com.maddyhome.idea.vim.handler.toMotionOrError
 | 
			
		||||
import com.maddyhome.idea.vim.helper.EditorHelper
 | 
			
		||||
import com.maddyhome.idea.vim.helper.SearchHelper
 | 
			
		||||
import com.maddyhome.idea.vim.helper.exitVisualMode
 | 
			
		||||
import com.maddyhome.idea.vim.helper.fileSize
 | 
			
		||||
import com.maddyhome.idea.vim.helper.getNormalizedScrollOffset
 | 
			
		||||
@@ -67,17 +47,13 @@ import com.maddyhome.idea.vim.helper.getNormalizedSideScrollOffset
 | 
			
		||||
import com.maddyhome.idea.vim.helper.isEndAllowed
 | 
			
		||||
import com.maddyhome.idea.vim.helper.vimLastColumn
 | 
			
		||||
import com.maddyhome.idea.vim.listener.AppCodeTemplates
 | 
			
		||||
import com.maddyhome.idea.vim.mark.Mark
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.VimStateMachine
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
 | 
			
		||||
import org.jetbrains.annotations.Range
 | 
			
		||||
import java.io.File
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
import kotlin.math.min
 | 
			
		||||
 | 
			
		||||
@@ -90,24 +66,6 @@ internal class MotionGroup : VimMotionGroupBase() {
 | 
			
		||||
    AppCodeTemplates.onMovement(editor.ij, caret.ij, oldOffset < offset)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun selectEditor(project: Project, mark: Mark): Editor? {
 | 
			
		||||
    val virtualFile = markToVirtualFile(mark) ?: return null
 | 
			
		||||
    return selectEditor(project, virtualFile)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun markToVirtualFile(mark: Mark): VirtualFile? {
 | 
			
		||||
    val protocol = mark.protocol
 | 
			
		||||
    val fileSystem: VirtualFileSystem? = VirtualFileManager.getInstance().getFileSystem(protocol)
 | 
			
		||||
    return fileSystem?.findFileByPath(mark.filepath)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun selectEditor(project: Project?, file: VirtualFile) =
 | 
			
		||||
    VimPlugin.getFile().selectEditor(project, file)
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion {
 | 
			
		||||
    return SearchHelper.findMatchingPairOnCurrentLine(editor.ij, caret.ij).toMotionOrError()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToFirstDisplayLine(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
@@ -130,85 +88,12 @@ internal class MotionGroup : VimMotionGroupBase() {
 | 
			
		||||
    return moveCaretToScreenLocation(editor.ij, caret.ij, ScreenLocation.MIDDLE, 0, false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToMark(caret: ImmutableVimCaret, ch: Char, toLineStart: Boolean): Motion {
 | 
			
		||||
    val markService = injector.markService
 | 
			
		||||
    val mark = markService.getMark(caret, ch) ?: return Motion.Error
 | 
			
		||||
 | 
			
		||||
    val caretEditor = caret.editor
 | 
			
		||||
    val caretVirtualFile = EditorHelper.getVirtualFile((caretEditor as IjVimEditor).editor)
 | 
			
		||||
 | 
			
		||||
    val line = mark.line
 | 
			
		||||
 | 
			
		||||
    if (caretVirtualFile!!.path == mark.filepath) {
 | 
			
		||||
      val offset = if (toLineStart) {
 | 
			
		||||
        moveCaretToLineStartSkipLeading(caretEditor, line)
 | 
			
		||||
      } else {
 | 
			
		||||
        caretEditor.bufferPositionToOffset(BufferPosition(line, mark.col, false))
 | 
			
		||||
      }
 | 
			
		||||
      return offset.toMotionOrError()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val project = caretEditor.editor.project
 | 
			
		||||
    val markEditor = selectEditor(project!!, mark)
 | 
			
		||||
    if (markEditor != null) {
 | 
			
		||||
      // todo should we move all the carets or only one?
 | 
			
		||||
      for (carett in markEditor.caretModel.allCarets) {
 | 
			
		||||
        val offset = if (toLineStart) {
 | 
			
		||||
          moveCaretToLineStartSkipLeading(IjVimEditor(markEditor), line)
 | 
			
		||||
        } else {
 | 
			
		||||
          // todo should it be the same as getting offset above?
 | 
			
		||||
          markEditor.logicalPositionToOffset(LogicalPosition(line, mark.col))
 | 
			
		||||
        }
 | 
			
		||||
        IjVimCaret(carett!!).moveToOffset(offset)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return Motion.Error
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
 | 
			
		||||
    val jumpService = injector.jumpService
 | 
			
		||||
    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)
 | 
			
		||||
    return if (vf.path != fileName) {
 | 
			
		||||
      val newFile = LocalFileSystem.getInstance().findFileByPath(fileName.replace(File.separatorChar, '/'))
 | 
			
		||||
        ?: return Motion.Error
 | 
			
		||||
      selectEditor(editor.ij.project, newFile)?.let { newEditor ->
 | 
			
		||||
        if (spot == -1) {
 | 
			
		||||
          jumpService.addJump(editor, false)
 | 
			
		||||
        }
 | 
			
		||||
        newEditor.vim.let {
 | 
			
		||||
          it.currentCaret().moveToOffset(it.normalizeOffset(newEditor.logicalPositionToOffset(lpNative), false))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      Motion.Error
 | 
			
		||||
    } else {
 | 
			
		||||
      if (spot == -1) {
 | 
			
		||||
        jumpService.addJump(editor, false)
 | 
			
		||||
      }
 | 
			
		||||
      editor.bufferPositionToOffset(lp).toMotionOrError()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToCurrentDisplayLineMiddle(editor: VimEditor, caret: ImmutableVimCaret): Motion {
 | 
			
		||||
    val width = EditorHelper.getApproximateScreenWidth(editor.ij) / 2
 | 
			
		||||
    val len = editor.lineLength(editor.currentCaret().getBufferPosition().line)
 | 
			
		||||
    return moveCaretToColumn(editor, caret, max(0, min(len - 1, width)), false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToColumn(editor: VimEditor, caret: ImmutableVimCaret, count: Int, allowEnd: Boolean): Motion {
 | 
			
		||||
    val line = caret.getLine().line
 | 
			
		||||
    val column = editor.normalizeColumn(line, count, allowEnd)
 | 
			
		||||
    val offset = editor.bufferPositionToOffset(BufferPosition(line, column, false))
 | 
			
		||||
    return if (column != count) {
 | 
			
		||||
      AdjustedOffset(offset, count)
 | 
			
		||||
    } else {
 | 
			
		||||
      AbsoluteOffset(offset)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToCurrentDisplayLineStart(editor: VimEditor, caret: ImmutableVimCaret): Motion {
 | 
			
		||||
    val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
 | 
			
		||||
    return moveCaretToColumn(editor, caret, col, false)
 | 
			
		||||
@@ -219,7 +104,7 @@ internal class MotionGroup : VimMotionGroupBase() {
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
  ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
 | 
			
		||||
    val col = EditorHelper.getVisualColumnAtLeftOfDisplay(editor.ij, caret.getVisualPosition().line)
 | 
			
		||||
    val bufferLine = caret.getLine().line
 | 
			
		||||
    val bufferLine = caret.getLine()
 | 
			
		||||
    return editor.getLeadingCharacterOffset(bufferLine, col)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -232,36 +117,6 @@ internal class MotionGroup : VimMotionGroupBase() {
 | 
			
		||||
    return moveCaretToColumn(editor, caret, col, allowEnd)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToLineWithSameColumn(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    line: Int,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
  ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
 | 
			
		||||
    var c = caret.vimLastColumn
 | 
			
		||||
    var l = line
 | 
			
		||||
    if (l < 0) {
 | 
			
		||||
      l = 0
 | 
			
		||||
      c = 0
 | 
			
		||||
    } else if (l >= editor.lineCount()) {
 | 
			
		||||
      l = editor.normalizeLine(editor.lineCount() - 1)
 | 
			
		||||
      c = editor.lineLength(l)
 | 
			
		||||
    }
 | 
			
		||||
    val newPos = BufferPosition(l, editor.normalizeColumn(l, c, false))
 | 
			
		||||
    return editor.bufferPositionToOffset(newPos)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToLineWithStartOfLineOption(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    line: Int,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
  ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
 | 
			
		||||
    return if (injector.options(editor).startofline) {
 | 
			
		||||
      moveCaretToLineStartSkipLeading(editor, line)
 | 
			
		||||
    } else {
 | 
			
		||||
      moveCaretToLineWithSameColumn(editor, line, caret)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * If 'absolute' is true, then set tab index to 'value', otherwise add 'value' to tab index with wraparound.
 | 
			
		||||
   */
 | 
			
		||||
@@ -279,30 +134,18 @@ internal class MotionGroup : VimMotionGroupBase() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretGotoPreviousTab(editor: VimEditor, context: ExecutionContext, rawCount: Int): Int {
 | 
			
		||||
    val project = editor.ij.project ?: return editor.currentCaret().offset.point
 | 
			
		||||
    val project = editor.ij.project ?: return editor.currentCaret().offset
 | 
			
		||||
    val currentWindow = FileEditorManagerEx.getInstanceEx(project).splitters.currentWindow
 | 
			
		||||
    switchEditorTab(currentWindow, if (rawCount >= 1) -rawCount else -1, false)
 | 
			
		||||
    return editor.currentCaret().offset.point
 | 
			
		||||
    return editor.currentCaret().offset
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretGotoNextTab(editor: VimEditor, context: ExecutionContext, rawCount: Int): Int {
 | 
			
		||||
    val absolute = rawCount >= 1
 | 
			
		||||
    val project = editor.ij.project ?: return editor.currentCaret().offset.point
 | 
			
		||||
    val project = editor.ij.project ?: return editor.currentCaret().offset
 | 
			
		||||
    val currentWindow = FileEditorManagerEx.getInstanceEx(project).splitters.currentWindow
 | 
			
		||||
    switchEditorTab(currentWindow, if (absolute) rawCount - 1 else 1, absolute)
 | 
			
		||||
    return editor.currentCaret().offset.point
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun moveCaretToLinePercent(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
    count: Int,
 | 
			
		||||
  ): @Range(from = 0, to = Int.MAX_VALUE.toLong()) Int {
 | 
			
		||||
    return moveCaretToLineWithStartOfLineOption(
 | 
			
		||||
      editor,
 | 
			
		||||
      editor.normalizeLine((editor.lineCount() * clamp(count, 0, 100) + 99) / 100 - 1),
 | 
			
		||||
      caret,
 | 
			
		||||
    )
 | 
			
		||||
    return editor.currentCaret().offset
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private enum class ScreenLocation {
 | 
			
		||||
 
 | 
			
		||||
@@ -210,8 +210,8 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
   * @param patternOffset   The pattern offset, e.g. `/{pattern}/{offset}`
 | 
			
		||||
   * @param direction       The direction to search
 | 
			
		||||
   */
 | 
			
		||||
  @TestOnly
 | 
			
		||||
  public void setLastSearchState(@SuppressWarnings("unused") @NotNull Editor editor, @NotNull String pattern,
 | 
			
		||||
  @Override
 | 
			
		||||
  public void setLastSearchState(@SuppressWarnings("unused") @NotNull VimEditor editor, @NotNull String pattern,
 | 
			
		||||
                                 @NotNull String patternOffset, Direction direction) {
 | 
			
		||||
    if (globalIjOptions(injector).getUseNewRegex()) {
 | 
			
		||||
      super.setLastSearchState(pattern, patternOffset, direction);
 | 
			
		||||
@@ -538,20 +538,24 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
   *
 | 
			
		||||
   * @param editor  The editor to search in
 | 
			
		||||
   * @param caret   The caret to use for initial search offset, and to move for interactive substitution
 | 
			
		||||
   * @param context
 | 
			
		||||
   * @param range   Only search and substitute within the given line range. Must be valid
 | 
			
		||||
   * @param excmd   The command part of the ex command line, e.g. `s` or `substitute`, or `~`
 | 
			
		||||
   * @param exarg   The argument to the substitute command, such as `/{pattern}/{string}/[flags]`
 | 
			
		||||
   * @return        True if the substitution succeeds, false on error. Will succeed even if nothing is modified
 | 
			
		||||
   * @return True if the substitution succeeds, false on error. Will succeed even if nothing is modified
 | 
			
		||||
   */
 | 
			
		||||
  @Override
 | 
			
		||||
  @RWLockLabel.SelfSynchronized
 | 
			
		||||
  public boolean processSubstituteCommand(@NotNull VimEditor editor,
 | 
			
		||||
                                          @NotNull VimCaret caret,
 | 
			
		||||
                                          @NotNull ExecutionContext context,
 | 
			
		||||
                                          @NotNull LineRange range,
 | 
			
		||||
                                          @NotNull @NonNls String excmd,
 | 
			
		||||
                                          @NotNull @NonNls String exarg,
 | 
			
		||||
                                          @NotNull VimLContext parent) {
 | 
			
		||||
    if (globalIjOptions(injector).getUseNewRegex()) return super.processSubstituteCommand(editor, caret, range, excmd, exarg, parent);
 | 
			
		||||
    if (globalIjOptions(injector).getUseNewRegex()) {
 | 
			
		||||
      return super.processSubstituteCommand(editor, caret, context, range, excmd, exarg, parent);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Explicitly exit visual mode here, so that visual mode marks don't change when we move the cursor to a match.
 | 
			
		||||
    List<ExException> exceptions = new ArrayList<>();
 | 
			
		||||
@@ -808,7 +812,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
            RangeHighlighter hl =
 | 
			
		||||
              SearchHighlightsHelper.addSubstitutionConfirmationHighlight(((IjVimEditor)editor).getEditor(), startoff,
 | 
			
		||||
                                                                          endoff);
 | 
			
		||||
            final ReplaceConfirmationChoice choice = confirmChoice(((IjVimEditor)editor).getEditor(), match, ((IjVimCaret)caret).getCaret(), startoff);
 | 
			
		||||
            final ReplaceConfirmationChoice choice = confirmChoice(((IjVimEditor)editor).getEditor(), context, match, ((IjVimCaret)caret).getCaret(), startoff);
 | 
			
		||||
            ((IjVimEditor)editor).getEditor().getMarkupModel().removeHighlighter(hl);
 | 
			
		||||
            switch (choice) {
 | 
			
		||||
              case SUBSTITUTE_THIS:
 | 
			
		||||
@@ -837,8 +841,7 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
            caret.moveToOffset(startoff);
 | 
			
		||||
            if (expression != null) {
 | 
			
		||||
              try {
 | 
			
		||||
                match =
 | 
			
		||||
                  expression.evaluate(editor, injector.getExecutionContextManager().onEditor(editor, null), parent).toInsertableString();
 | 
			
		||||
                match = expression.evaluate(editor, context, parent).toInsertableString();
 | 
			
		||||
              }
 | 
			
		||||
              catch (Exception e) {
 | 
			
		||||
                exceptions.add((ExException)e);
 | 
			
		||||
@@ -989,7 +992,9 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
    return new Pair<>(true, new Triple<>(regmatch, pattern, sp));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static @NotNull ReplaceConfirmationChoice confirmChoice(@NotNull Editor editor, @NotNull String match, @NotNull Caret caret, int startoff) {
 | 
			
		||||
  private static @NotNull ReplaceConfirmationChoice confirmChoice(@NotNull Editor editor,
 | 
			
		||||
                                                                  @NotNull ExecutionContext context,
 | 
			
		||||
                                                                  @NotNull String match, @NotNull Caret caret, int startoff) {
 | 
			
		||||
    final Ref<ReplaceConfirmationChoice> result = Ref.create(ReplaceConfirmationChoice.QUIT);
 | 
			
		||||
    final Function1<KeyStroke, Boolean> keyStrokeProcessor = key -> {
 | 
			
		||||
      final ReplaceConfirmationChoice choice;
 | 
			
		||||
@@ -1023,7 +1028,6 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
    else {
 | 
			
		||||
      // XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for this method
 | 
			
		||||
      final ExEntryPanel exEntryPanel = ExEntryPanel.getInstanceWithoutShortcuts();
 | 
			
		||||
      ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(editor), null);
 | 
			
		||||
      exEntryPanel.activate(editor, ((IjEditorExecutionContext)context).getContext(), MessageHelper.message("replace.with.0", match), "", 1);
 | 
			
		||||
      new IjVimCaret(caret).moveToOffset(startoff);
 | 
			
		||||
      ModalEntry.INSTANCE.activate(new IjVimEditor(editor), keyStrokeProcessor);
 | 
			
		||||
@@ -1081,9 +1085,9 @@ public class SearchGroup extends IjVimSearchGroup implements PersistentStateComp
 | 
			
		||||
  private @Nullable TextRange findNextSearchForGn(@NotNull VimEditor editor, int count, boolean forwards) {
 | 
			
		||||
    if (forwards) {
 | 
			
		||||
      final EnumSet<SearchOptions> searchOptions = EnumSet.of(SearchOptions.WRAP, SearchOptions.WHOLE_FILE);
 | 
			
		||||
      return VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), editor.primaryCaret().getOffset().getPoint(), count, searchOptions);
 | 
			
		||||
      return VimInjectorKt.getInjector().getSearchHelper().findPattern(editor, getLastUsedPattern(), editor.primaryCaret().getOffset(), count, searchOptions);
 | 
			
		||||
    } else {
 | 
			
		||||
      return searchBackward(editor, editor.primaryCaret().getOffset().getPoint(), count);
 | 
			
		||||
      return searchBackward(editor, editor.primaryCaret().getOffset(), count);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,7 @@ internal object IdeaSelectionControl {
 | 
			
		||||
      is Mode.VISUAL -> VimPlugin.getVisualMotion().enterVisualMode(editor.vim, mode.selectionType)
 | 
			
		||||
      is Mode.SELECT -> VimPlugin.getVisualMotion().enterSelectMode(editor.vim, mode.selectionType)
 | 
			
		||||
      is Mode.INSERT -> VimPlugin.getChange()
 | 
			
		||||
        .insertBeforeCursor(editor.vim, injector.executionContextManager.onEditor(editor.vim))
 | 
			
		||||
        .insertBeforeCursor(editor.vim, injector.executionContextManager.getEditorExecutionContext(editor.vim))
 | 
			
		||||
 | 
			
		||||
      is Mode.NORMAL -> Unit
 | 
			
		||||
      else -> error("Unexpected mode: $mode")
 | 
			
		||||
 
 | 
			
		||||
@@ -337,7 +337,7 @@ internal abstract class VimKeyHandler(nextHandler: EditorActionHandler?) : Octop
 | 
			
		||||
 | 
			
		||||
  override fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?) {
 | 
			
		||||
    val enterKey = key(key)
 | 
			
		||||
    val context = injector.executionContextManager.onEditor(editor.vim, dataContext?.vim)
 | 
			
		||||
    val context = dataContext?.vim ?: injector.executionContextManager.getEditorExecutionContext(editor.vim)
 | 
			
		||||
    val keyHandler = KeyHandler.getInstance()
 | 
			
		||||
    keyHandler.handleKey(editor.vim, enterKey, context, keyHandler.keyHandlerState)
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -90,6 +90,8 @@ private fun Editor.updatePrimaryCaretVisualAttributes() {
 | 
			
		||||
 | 
			
		||||
  // Make sure the caret is visible as soon as it's set. It might be invisible while blinking
 | 
			
		||||
  // NOTE: At the moment, this causes project leak in tests
 | 
			
		||||
  // IJPL-928 - this will be fixed in 2024.2
 | 
			
		||||
  // [VERSION UPDATE] 2024.2 - remove if wrapping
 | 
			
		||||
  if (!ApplicationManager.getApplication().isUnitTestMode) {
 | 
			
		||||
    (this as? EditorEx)?.setCaretVisible(true)
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ package com.maddyhome.idea.vim.helper
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
import com.maddyhome.idea.vim.action.change.Extension
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.ui.ModalEntry
 | 
			
		||||
@@ -23,7 +23,7 @@ import javax.swing.KeyStroke
 | 
			
		||||
@Service
 | 
			
		||||
internal class CommandLineHelper : VimCommandLineHelper {
 | 
			
		||||
 | 
			
		||||
  override fun inputString(vimEditor: VimEditor, prompt: String, finishOn: Char?): String? {
 | 
			
		||||
  override fun inputString(vimEditor: VimEditor, context: ExecutionContext, prompt: String, finishOn: Char?): String? {
 | 
			
		||||
    val editor = vimEditor.ij
 | 
			
		||||
    if (vimEditor.vimStateMachine.isDotRepeatInProgress) {
 | 
			
		||||
      val input = Extension.consumeString()
 | 
			
		||||
@@ -53,7 +53,7 @@ internal class CommandLineHelper : VimCommandLineHelper {
 | 
			
		||||
      var text: String? = null
 | 
			
		||||
      // XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for input()
 | 
			
		||||
      val exEntryPanel = ExEntryPanel.getInstanceWithoutShortcuts()
 | 
			
		||||
      exEntryPanel.activate(editor, injector.executionContextManager.onEditor(editor.vim).ij, prompt.ifEmpty { " " }, "", 1)
 | 
			
		||||
      exEntryPanel.activate(editor, context.ij, prompt.ifEmpty { " " }, "", 1)
 | 
			
		||||
      ModalEntry.activate(editor.vim) { key: KeyStroke ->
 | 
			
		||||
        return@activate when {
 | 
			
		||||
          key.isCloseKeyStroke() -> {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import com.intellij.openapi.editor.ex.util.EditorUtil
 | 
			
		||||
import com.intellij.openapi.util.Key
 | 
			
		||||
import com.intellij.openapi.util.UserDataHolder
 | 
			
		||||
 | 
			
		||||
@Deprecated("Do not use context wrappers, use existing provided contexts. If no context available, use `injector.getExecutionContextManager().getEditorExecutionContext(editor)`")
 | 
			
		||||
internal class EditorDataContext @Deprecated("Please use `init` method") constructor(
 | 
			
		||||
  private val editor: Editor,
 | 
			
		||||
  private val editorContext: DataContext,
 | 
			
		||||
 
 | 
			
		||||
@@ -211,15 +211,12 @@ public class EditorHelper {
 | 
			
		||||
    return injector.getEditorGroup().getEditors(new IjVimDocument(doc)).stream().findFirst().orElse(null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public static @NotNull String pad(final @NotNull Editor editor,
 | 
			
		||||
                                    @NotNull DataContext context,
 | 
			
		||||
                                    int line,
 | 
			
		||||
                                    final int to) {
 | 
			
		||||
  public static @NotNull String pad(final @NotNull Editor editor, int line, final int to) {
 | 
			
		||||
    final int len = EngineEditorHelperKt.lineLength(new IjVimEditor(editor), line);
 | 
			
		||||
    if (len >= to) return "";
 | 
			
		||||
 | 
			
		||||
    final int limit = to - len;
 | 
			
		||||
    return IndentConfig.create(editor, context).createIndentBySize(limit);
 | 
			
		||||
    return IndentConfig.create(editor).createIndentBySize(limit);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -326,7 +323,7 @@ public class EditorHelper {
 | 
			
		||||
 | 
			
		||||
    final int offset = y - ((screenHeight - lineHeight) / lineHeight / 2 * lineHeight);
 | 
			
		||||
    @NotNull final VimEditor editor1 = new IjVimEditor(editor);
 | 
			
		||||
    final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) - 1;
 | 
			
		||||
    final int lastVisualLine = EngineEditorHelperKt.getVisualLineCount(editor1) + editor.getSettings().getAdditionalLinesCount();
 | 
			
		||||
    final int offsetForLastLineAtBottom = getOffsetToScrollVisualLineToBottomOfScreen(editor, lastVisualLine);
 | 
			
		||||
 | 
			
		||||
    // For `zz`, we want to use virtual space and move any line, including the last one, to the middle of the screen.
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ package com.maddyhome.idea.vim.helper
 | 
			
		||||
 | 
			
		||||
import com.intellij.codeWithMe.ClientId
 | 
			
		||||
import com.intellij.openapi.editor.Caret
 | 
			
		||||
import com.intellij.openapi.editor.CaretState
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.editor.ex.util.EditorUtil
 | 
			
		||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
 | 
			
		||||
@@ -20,6 +21,8 @@ import com.maddyhome.idea.vim.api.StringListOptionValue
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.group.IjOptionConstants
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.inBlockSelection
 | 
			
		||||
import java.awt.Component
 | 
			
		||||
import javax.swing.JComponent
 | 
			
		||||
import javax.swing.JTable
 | 
			
		||||
@@ -96,3 +99,41 @@ internal val Caret.vimLine: Int
 | 
			
		||||
 */
 | 
			
		||||
internal val Editor.vimLine: Int
 | 
			
		||||
  get() = this.caretModel.currentCaret.vimLine
 | 
			
		||||
 | 
			
		||||
internal inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) {
 | 
			
		||||
  val caretModel = this.caretModel
 | 
			
		||||
  val carets = if (this.vim.inBlockSelection) null else caretModel.allCarets
 | 
			
		||||
  if (carets == null || carets.size == 1) {
 | 
			
		||||
    action()
 | 
			
		||||
  }
 | 
			
		||||
  else {
 | 
			
		||||
    var initialDocumentSize = this.document.textLength
 | 
			
		||||
    var documentSizeDifference = 0
 | 
			
		||||
 | 
			
		||||
    val caretOffsets = carets.map { it.selectionStart to it.selectionEnd }
 | 
			
		||||
    val restoredCarets = mutableListOf<CaretState>()
 | 
			
		||||
 | 
			
		||||
    caretModel.removeSecondaryCarets()
 | 
			
		||||
    
 | 
			
		||||
    for ((selectionStart, selectionEnd) in caretOffsets) {
 | 
			
		||||
      if (selectionStart == selectionEnd) {
 | 
			
		||||
        caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference)
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        caretModel.primaryCaret.setSelection(
 | 
			
		||||
          selectionStart + documentSizeDifference,
 | 
			
		||||
          selectionEnd + documentSizeDifference
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      action()
 | 
			
		||||
      restoredCarets.add(caretModel.caretsAndSelections.single())
 | 
			
		||||
 | 
			
		||||
      val documentLength = this.document.textLength
 | 
			
		||||
      documentSizeDifference += documentLength - initialDocumentSize
 | 
			
		||||
      initialDocumentSize = documentLength
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    caretModel.caretsAndSelections = restoredCarets
 | 
			
		||||
  } 
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import com.intellij.openapi.actionSystem.PlatformDataKeys
 | 
			
		||||
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
 | 
			
		||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
 | 
			
		||||
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
 | 
			
		||||
import com.intellij.openapi.actionSystem.impl.Utils
 | 
			
		||||
import com.intellij.openapi.command.CommandProcessor
 | 
			
		||||
import com.intellij.openapi.command.UndoConfirmationPolicy
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
@@ -34,7 +35,6 @@ import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.NativeAction
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimActionExecutor
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.command.OperatorArguments
 | 
			
		||||
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjNativeAction
 | 
			
		||||
@@ -78,6 +78,7 @@ internal class IjActionExecutor : VimActionExecutor {
 | 
			
		||||
    val dataContext = DataContextWrapper(context.ij)
 | 
			
		||||
    dataContext.putUserData(runFromVimKey, true)
 | 
			
		||||
 | 
			
		||||
    val actionId = ActionManager.getInstance().getId(ijAction)
 | 
			
		||||
    val event = AnActionEvent(
 | 
			
		||||
      null,
 | 
			
		||||
      dataContext,
 | 
			
		||||
@@ -86,12 +87,20 @@ internal class IjActionExecutor : VimActionExecutor {
 | 
			
		||||
      ActionManager.getInstance(),
 | 
			
		||||
      0,
 | 
			
		||||
    )
 | 
			
		||||
    Utils.initUpdateSession(event)
 | 
			
		||||
    // beforeActionPerformedUpdate should be called to update the action. It fixes some rider-specific problems.
 | 
			
		||||
    //   because rider use async update method. See VIM-1819.
 | 
			
		||||
    // This method executes inside of lastUpdateAndCheckDumb
 | 
			
		||||
    // Another related issue: VIM-2604
 | 
			
		||||
    ijAction.beforeActionPerformedUpdate(event)
 | 
			
		||||
    if (!event.presentation.isEnabled) return false
 | 
			
		||||
 | 
			
		||||
    // This is a hack to fix the tests and fix VIM-3332
 | 
			
		||||
    // We should get rid of it in VIM-3376
 | 
			
		||||
    if (actionId == "RunClass" || actionId == IdeActions.ACTION_COMMENT_LINE || actionId == IdeActions.ACTION_COMMENT_BLOCK) {
 | 
			
		||||
      ijAction.beforeActionPerformedUpdate(event)
 | 
			
		||||
      if (!event.presentation.isEnabled) return false
 | 
			
		||||
    } else {
 | 
			
		||||
      if (!ActionUtil.lastUpdateAndCheckDumb(ijAction, event, false)) return false
 | 
			
		||||
    }
 | 
			
		||||
    if (ijAction is ActionGroup && !event.presentation.isPerformGroup) {
 | 
			
		||||
      // Some ActionGroups should not be performed, but shown as a popup
 | 
			
		||||
      val popup = JBPopupFactory.getInstance()
 | 
			
		||||
@@ -215,7 +224,7 @@ internal class IjActionExecutor : VimActionExecutor {
 | 
			
		||||
    CommandProcessor.getInstance()
 | 
			
		||||
      .executeCommand(
 | 
			
		||||
        editor.ij.project,
 | 
			
		||||
        { cmd.execute(editor, injector.executionContextManager.onEditor(editor, context), operatorArguments) },
 | 
			
		||||
        { cmd.execute(editor, context, operatorArguments) },
 | 
			
		||||
        cmd.id,
 | 
			
		||||
        DocCommandGroupId.noneGroupId(editor.ij.document),
 | 
			
		||||
        UndoConfirmationPolicy.DEFAULT,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import com.intellij.openapi.editor.VisualPosition
 | 
			
		||||
import com.intellij.openapi.editor.actionSystem.EditorActionManager
 | 
			
		||||
import com.intellij.openapi.editor.ex.util.EditorUtil
 | 
			
		||||
import com.maddyhome.idea.vim.api.EngineEditorHelper
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimVisualPosition
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
@@ -51,8 +50,8 @@ internal class IjEditorHelper : EngineEditorHelper {
 | 
			
		||||
    return EditorHelper.getVisualLineAtBottomOfScreen(editor.ij)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun pad(editor: VimEditor, context: ExecutionContext, line: Int, to: Int): String {
 | 
			
		||||
    return EditorHelper.pad(editor.ij, context.ij, line, to)
 | 
			
		||||
  override fun pad(editor: VimEditor, line: Int, to: Int): String {
 | 
			
		||||
    return EditorHelper.pad(editor.ij, line, to)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun inlayAwareOffsetToVisualPosition(editor: VimEditor, offset: Int): VimVisualPosition {
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ internal object ScrollViewHelper {
 | 
			
		||||
    // that this needs to be replaced as a more or less dumb line for line rewrite.
 | 
			
		||||
    val topLine = getVisualLineAtTopOfScreen(editor)
 | 
			
		||||
    val bottomLine = getVisualLineAtBottomOfScreen(editor)
 | 
			
		||||
    val lastLine = vimEditor.getVisualLineCount() - 1
 | 
			
		||||
    val lastLine = vimEditor.getVisualLineCount() + editor.settings.additionalLinesCount
 | 
			
		||||
 | 
			
		||||
    // We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred
 | 
			
		||||
    val scrollOffset = injector.options(vimEditor).scrolloff
 | 
			
		||||
 
 | 
			
		||||
@@ -632,113 +632,6 @@ public class SearchHelper {
 | 
			
		||||
    return new TextRange(bstart, bend + 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static int findMatchingBlockCommentPair(@NotNull PsiComment comment,
 | 
			
		||||
                                                  int pos,
 | 
			
		||||
                                                  @Nullable String prefix,
 | 
			
		||||
                                                  @Nullable String suffix) {
 | 
			
		||||
    if (prefix != null && suffix != null) {
 | 
			
		||||
      // TODO: Try to get rid of `getText()` because it takes a lot of time to calculate the string
 | 
			
		||||
      final String commentText = comment.getText();
 | 
			
		||||
      if (commentText.startsWith(prefix) && commentText.endsWith(suffix)) {
 | 
			
		||||
        final int endOffset = comment.getTextOffset() + comment.getTextLength();
 | 
			
		||||
        if (pos < comment.getTextOffset() + prefix.length()) {
 | 
			
		||||
          return endOffset;
 | 
			
		||||
        }
 | 
			
		||||
        else if (pos >= endOffset - suffix.length()) {
 | 
			
		||||
          return comment.getTextOffset();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return -1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static int findMatchingBlockCommentPair(@NotNull PsiElement element, int pos) {
 | 
			
		||||
    final Language language = element.getLanguage();
 | 
			
		||||
    final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language);
 | 
			
		||||
    final PsiComment comment = PsiTreeUtil.getParentOfType(element, PsiComment.class, false);
 | 
			
		||||
    if (comment != null) {
 | 
			
		||||
      final int ret = findMatchingBlockCommentPair(comment, pos, commenter.getBlockCommentPrefix(),
 | 
			
		||||
                                                   commenter.getBlockCommentSuffix());
 | 
			
		||||
      if (ret >= 0) {
 | 
			
		||||
        return ret;
 | 
			
		||||
      }
 | 
			
		||||
      if (commenter instanceof CodeDocumentationAwareCommenter docCommenter) {
 | 
			
		||||
        return findMatchingBlockCommentPair(comment, pos, docCommenter.getDocumentationCommentPrefix(),
 | 
			
		||||
                                            docCommenter.getDocumentationCommentSuffix());
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return -1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This looks on the current line, starting at the cursor position for one of {, }, (, ), [, or ]. It then searches
 | 
			
		||||
   * forward or backward, as appropriate for the associated match pair. String in double quotes are skipped over.
 | 
			
		||||
   * Single characters in single quotes are skipped too.
 | 
			
		||||
   *
 | 
			
		||||
   * @param editor The editor to search in
 | 
			
		||||
   * @return The offset within the editor of the found character or -1 if no match was found or none of the characters
 | 
			
		||||
   * were found on the remainder of the current line.
 | 
			
		||||
   */
 | 
			
		||||
  public static int findMatchingPairOnCurrentLine(@NotNull Editor editor, @NotNull Caret caret) {
 | 
			
		||||
    int pos = caret.getOffset();
 | 
			
		||||
 | 
			
		||||
    final int commentPos = findMatchingComment(editor, pos);
 | 
			
		||||
    if (commentPos >= 0) {
 | 
			
		||||
      return commentPos;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int line = caret.getLogicalPosition().line;
 | 
			
		||||
    final IjVimEditor vimEditor = new IjVimEditor(editor);
 | 
			
		||||
    int end = EngineEditorHelperKt.getLineEndOffset(vimEditor, line, true);
 | 
			
		||||
 | 
			
		||||
    // To handle the case where visual mode allows the user to go past the end of the line,
 | 
			
		||||
    // which will prevent loc from finding a pairable character below
 | 
			
		||||
    if (pos > 0 && pos == end) {
 | 
			
		||||
      pos = end - 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    final String pairChars = parseMatchPairsOption(vimEditor);
 | 
			
		||||
 | 
			
		||||
    CharSequence chars = editor.getDocument().getCharsSequence();
 | 
			
		||||
    int loc = -1;
 | 
			
		||||
    // Search the remainder of the current line for one of the candidate characters
 | 
			
		||||
    while (pos < end) {
 | 
			
		||||
      loc = pairChars.indexOf(chars.charAt(pos));
 | 
			
		||||
      if (loc >= 0) {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      pos++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    int res = -1;
 | 
			
		||||
    // If we found one ...
 | 
			
		||||
    if (loc >= 0) {
 | 
			
		||||
      // What direction should we go now (-1 is backward, 1 is forward)
 | 
			
		||||
      Direction dir = loc % 2 == 0 ? Direction.FORWARDS : Direction.BACKWARDS;
 | 
			
		||||
      // Which character did we find and which should we now search for
 | 
			
		||||
      char found = pairChars.charAt(loc);
 | 
			
		||||
      char match = pairChars.charAt(loc + dir.toInt());
 | 
			
		||||
      res = findBlockLocation(chars, found, match, dir, pos, 1, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return res;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * If on the start/end of a block comment, jump to the matching of that comment, or vice versa.
 | 
			
		||||
   */
 | 
			
		||||
  private static int findMatchingComment(@NotNull Editor editor, int pos) {
 | 
			
		||||
    final PsiFile psiFile = PsiHelper.getFile(editor);
 | 
			
		||||
    if (psiFile != null) {
 | 
			
		||||
      final PsiElement element = psiFile.findElementAt(pos);
 | 
			
		||||
      if (element != null) {
 | 
			
		||||
        return findMatchingBlockCommentPair(element, pos);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return -1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static int findBlockLocation(@NotNull CharSequence chars,
 | 
			
		||||
                                       char found,
 | 
			
		||||
                                       char match,
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,14 @@ package com.maddyhome.idea.vim.helper
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.actionSystem.PlatformDataKeys
 | 
			
		||||
import com.intellij.openapi.application.ApplicationInfo
 | 
			
		||||
import com.intellij.openapi.command.CommandProcessor
 | 
			
		||||
import com.intellij.openapi.command.undo.UndoManager
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
import com.intellij.openapi.fileEditor.TextEditor
 | 
			
		||||
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider
 | 
			
		||||
import com.intellij.openapi.util.registry.Registry
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
@@ -21,6 +25,8 @@ import com.maddyhome.idea.vim.common.ChangesListener
 | 
			
		||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.SelectionType
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.inVisualMode
 | 
			
		||||
import com.maddyhome.idea.vim.undo.UndoRedoBase
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -37,22 +43,11 @@ internal class UndoRedoHelper : UndoRedoBase() {
 | 
			
		||||
      val scrollingModel = editor.getScrollingModel()
 | 
			
		||||
      scrollingModel.accumulateViewportChanges()
 | 
			
		||||
 | 
			
		||||
      if (injector.globalIjOptions().oldundo) {
 | 
			
		||||
        SelectionVimListenerSuppressor.lock().use { undoManager.undo(fileEditor) }
 | 
			
		||||
      // [VERSION UPDATE] 241+ remove this if
 | 
			
		||||
      if (ApplicationInfo.getInstance().build.baselineVersion >= 241) {
 | 
			
		||||
        undoFor241plus(editor, undoManager, fileEditor)
 | 
			
		||||
      } else {
 | 
			
		||||
        // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
 | 
			
		||||
        editor.runWithChangeTracking {
 | 
			
		||||
          undoManager.undo(fileEditor)
 | 
			
		||||
 | 
			
		||||
          // We execute undo one more time if the previous one just restored selection
 | 
			
		||||
          if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
 | 
			
		||||
            undoManager.undo(fileEditor)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
          removeSelections(editor)
 | 
			
		||||
        }
 | 
			
		||||
        undoForLessThan241(undoManager, fileEditor, editor)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      scrollingModel.flushViewportChanges()
 | 
			
		||||
@@ -62,6 +57,61 @@ internal class UndoRedoHelper : UndoRedoBase() {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun undoForLessThan241(
 | 
			
		||||
    undoManager: UndoManager,
 | 
			
		||||
    fileEditor: TextEditor,
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
  ) {if (injector.globalIjOptions().oldundo) {
 | 
			
		||||
        SelectionVimListenerSuppressor.lock().use { undoManager.undo(fileEditor) }
 | 
			
		||||
        restoreVisualMode(editor)
 | 
			
		||||
      } else {
 | 
			
		||||
        // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
 | 
			
		||||
        editor.runWithChangeTracking {
 | 
			
		||||
          undoManager.undo(fileEditor)
 | 
			
		||||
 | 
			
		||||
        // We execute undo one more time if the previous one just restored selection
 | 
			
		||||
        if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
 | 
			
		||||
          undoManager.undo(fileEditor)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        removeSelections(editor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  private fun undoFor241plus(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    undoManager: UndoManager,
 | 
			
		||||
    fileEditor: TextEditor,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (injector.globalIjOptions().oldundo) {
 | 
			
		||||
      // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
 | 
			
		||||
      editor.runWithChangeTracking {
 | 
			
		||||
        undoManager.undo(fileEditor)
 | 
			
		||||
 | 
			
		||||
        // We execute undo one more time if the previous one just restored selection
 | 
			
		||||
        if (!hasChanges && hasSelection(editor) && undoManager.isUndoAvailable(fileEditor)) {
 | 
			
		||||
          undoManager.undo(fileEditor)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        removeSelections(editor)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
 | 
			
		||||
        undoManager.undo(fileEditor)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        removeSelections(editor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun hasSelection(editor: VimEditor): Boolean {
 | 
			
		||||
    return editor.primaryCaret().ij.hasSelection()
 | 
			
		||||
  }
 | 
			
		||||
@@ -72,8 +122,25 @@ internal class UndoRedoHelper : UndoRedoBase() {
 | 
			
		||||
    val fileEditor = TextEditorProvider.getInstance().getTextEditor(editor.ij)
 | 
			
		||||
    val undoManager = UndoManager.getInstance(project)
 | 
			
		||||
    if (undoManager.isRedoAvailable(fileEditor)) {
 | 
			
		||||
      if (injector.globalIjOptions().oldundo) {
 | 
			
		||||
      // [VERSION UPDATE] 241+ remove this if
 | 
			
		||||
      if (ApplicationInfo.getInstance().build.baselineVersion >= 241) {
 | 
			
		||||
        redoFor241Plus(undoManager, fileEditor, editor)
 | 
			
		||||
      } else {
 | 
			
		||||
        redoForLessThan241(undoManager, fileEditor, editor)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun redoForLessThan241(
 | 
			
		||||
    undoManager: UndoManager,
 | 
			
		||||
    fileEditor: TextEditor,
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
  ) {if (injector.globalIjOptions().oldundo) {
 | 
			
		||||
        SelectionVimListenerSuppressor.lock().use { undoManager.redo(fileEditor) }
 | 
			
		||||
        restoreVisualMode(editor)
 | 
			
		||||
      } else {
 | 
			
		||||
        undoManager.redo(fileEditor)
 | 
			
		||||
        CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
@@ -83,19 +150,50 @@ internal class UndoRedoHelper : UndoRedoBase() {
 | 
			
		||||
        editor.runWithChangeTracking {
 | 
			
		||||
          undoManager.redo(fileEditor)
 | 
			
		||||
 | 
			
		||||
          // We execute undo one more time if the previous one just restored selection
 | 
			
		||||
          if (!hasChanges && hasSelection(editor) && undoManager.isRedoAvailable(fileEditor)) {
 | 
			
		||||
            undoManager.redo(fileEditor)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
          removeSelections(editor)
 | 
			
		||||
        // We execute undo one more time if the previous one just restored selection
 | 
			
		||||
        if (!hasChanges && hasSelection(editor) && undoManager.isRedoAvailable(fileEditor)) {
 | 
			
		||||
          undoManager.redo(fileEditor)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return true
 | 
			
		||||
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        removeSelections(editor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun redoFor241Plus(
 | 
			
		||||
    undoManager: UndoManager,
 | 
			
		||||
    fileEditor: TextEditor,
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (injector.globalIjOptions().oldundo) {
 | 
			
		||||
      undoManager.redo(fileEditor)
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        editor.carets().forEach { it.ij.removeSelection() }
 | 
			
		||||
      }
 | 
			
		||||
      // TODO refactor me after VIM-308 when restoring selection and caret movement will be ignored by undo
 | 
			
		||||
      editor.runWithChangeTracking {
 | 
			
		||||
        undoManager.redo(fileEditor)
 | 
			
		||||
 | 
			
		||||
        // We execute undo one more time if the previous one just restored selection
 | 
			
		||||
        if (!hasChanges && hasSelection(editor) && undoManager.isRedoAvailable(fileEditor)) {
 | 
			
		||||
          undoManager.redo(fileEditor)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        removeSelections(editor)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      runWithBooleanRegistryOption("ide.undo.transparent.caret.movement", true) {
 | 
			
		||||
        undoManager.redo(fileEditor)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      CommandProcessor.getInstance().runUndoTransparentAction {
 | 
			
		||||
        removeSelections(editor)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun removeSelections(editor: VimEditor) {
 | 
			
		||||
@@ -109,6 +207,17 @@ internal class UndoRedoHelper : UndoRedoBase() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun runWithBooleanRegistryOption(option: String, value: Boolean, block: () -> Unit) {
 | 
			
		||||
    val registry = Registry.get(option)
 | 
			
		||||
    val oldValue = registry.asBoolean()
 | 
			
		||||
    registry.setValue(value)
 | 
			
		||||
    try {
 | 
			
		||||
      block()
 | 
			
		||||
    } finally {
 | 
			
		||||
      registry.setValue(oldValue)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun VimEditor.runWithChangeTracking(block: ChangeTracker.() -> Unit) {
 | 
			
		||||
    val tracker = ChangeTracker(this)
 | 
			
		||||
    tracker.block()
 | 
			
		||||
@@ -131,4 +240,21 @@ internal class UndoRedoHelper : UndoRedoBase() {
 | 
			
		||||
    val hasChanges: Boolean
 | 
			
		||||
      get() = changeListener.hasChanged || initialPath != editor.getPath()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun restoreVisualMode(editor: VimEditor) {
 | 
			
		||||
    if (!editor.inVisualMode && editor.getSelectionModel().hasSelection()) {
 | 
			
		||||
      val detectedMode = VimPlugin.getVisualMotion().autodetectVisualSubmode(editor)
 | 
			
		||||
      
 | 
			
		||||
      // Visual block selection is restored into multiple carets, so multi-carets that form a block are always
 | 
			
		||||
      // identified as visual block mode, leading to false positives.
 | 
			
		||||
      // Since I use visual block mode much less often than multi-carets, this is a judgment call to never restore
 | 
			
		||||
      // visual block mode.
 | 
			
		||||
      val wantedMode = if (detectedMode == SelectionType.BLOCK_WISE)
 | 
			
		||||
        SelectionType.CHARACTER_WISE
 | 
			
		||||
      else
 | 
			
		||||
        detectedMode
 | 
			
		||||
      
 | 
			
		||||
      VimPlugin.getVisualMotion().enterVisualMode(editor, wantedMode)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * 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.helper
 | 
			
		||||
 | 
			
		||||
import com.intellij.ide.plugins.StandalonePluginUpdateChecker
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
import com.intellij.openapi.components.service
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.group.NotificationService
 | 
			
		||||
import com.maddyhome.idea.vim.icons.VimIcons
 | 
			
		||||
 | 
			
		||||
@Service(Service.Level.APP)
 | 
			
		||||
internal class VimStandalonePluginUpdateChecker : StandalonePluginUpdateChecker(
 | 
			
		||||
  VimPlugin.getPluginId(),
 | 
			
		||||
  updateTimestampProperty = PROPERTY_NAME,
 | 
			
		||||
  NotificationService.IDEAVIM_STICKY_GROUP,
 | 
			
		||||
  VimIcons.IDEAVIM,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
  override fun skipUpdateCheck(): Boolean = VimPlugin.isNotEnabled() || "dev" in VimPlugin.getVersion()
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    private const val PROPERTY_NAME = "ideavim.statistics.timestamp"
 | 
			
		||||
    val instance: VimStandalonePluginUpdateChecker = service()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -19,6 +19,7 @@ import com.intellij.codeInsight.template.TemplateManagerListener
 | 
			
		||||
import com.intellij.codeInsight.template.impl.TemplateState
 | 
			
		||||
import com.intellij.find.FindModelListener
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionManager
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
 | 
			
		||||
import com.intellij.openapi.actionSystem.AnAction
 | 
			
		||||
import com.intellij.openapi.actionSystem.AnActionEvent
 | 
			
		||||
import com.intellij.openapi.actionSystem.AnActionResult
 | 
			
		||||
@@ -27,6 +28,7 @@ import com.intellij.openapi.actionSystem.CommonDataKeys
 | 
			
		||||
import com.intellij.openapi.actionSystem.ex.AnActionListener
 | 
			
		||||
import com.intellij.openapi.actionSystem.impl.ProxyShortcutSet
 | 
			
		||||
import com.intellij.openapi.editor.Editor
 | 
			
		||||
import com.intellij.openapi.editor.impl.ScrollingModelImpl
 | 
			
		||||
import com.intellij.openapi.project.DumbAwareToggleAction
 | 
			
		||||
import com.intellij.openapi.util.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.KeyHandler
 | 
			
		||||
@@ -56,6 +58,7 @@ internal object IdeaSpecifics {
 | 
			
		||||
    private val surrounderAction =
 | 
			
		||||
      "com.intellij.codeInsight.generation.surroundWith.SurroundWithHandler\$InvokeSurrounderAction"
 | 
			
		||||
    private var editor: Editor? = null
 | 
			
		||||
    private var caretOffset = -1
 | 
			
		||||
    private var completionPrevDocumentLength: Int? = null
 | 
			
		||||
    private var completionPrevDocumentOffset: Int? = null
 | 
			
		||||
    override fun beforeActionPerformed(action: AnAction, event: AnActionEvent) {
 | 
			
		||||
@@ -64,6 +67,7 @@ internal object IdeaSpecifics {
 | 
			
		||||
      val hostEditor = event.dataContext.getData(CommonDataKeys.HOST_EDITOR)
 | 
			
		||||
      if (hostEditor != null) {
 | 
			
		||||
        editor = hostEditor
 | 
			
		||||
        caretOffset = hostEditor.caretModel.offset
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      val isVimAction = (action as? AnActionWrapper)?.delegate is VimShortcutKeyAction
 | 
			
		||||
@@ -95,43 +99,58 @@ internal object IdeaSpecifics {
 | 
			
		||||
      if (VimPlugin.isNotEnabled()) return
 | 
			
		||||
 | 
			
		||||
      val editor = editor
 | 
			
		||||
      if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
 | 
			
		||||
        val prevDocumentLength = completionPrevDocumentLength
 | 
			
		||||
        val prevDocumentOffset = completionPrevDocumentOffset
 | 
			
		||||
      if (editor != null) {
 | 
			
		||||
        if (action is ChooseItemAction && injector.registerGroup.isRecording) {
 | 
			
		||||
          val prevDocumentLength = completionPrevDocumentLength
 | 
			
		||||
          val prevDocumentOffset = completionPrevDocumentOffset
 | 
			
		||||
 | 
			
		||||
        if (prevDocumentLength != null && prevDocumentOffset != null) {
 | 
			
		||||
          val register = VimPlugin.getRegister()
 | 
			
		||||
          val addedTextLength = editor.document.textLength - prevDocumentLength
 | 
			
		||||
          val caretShift = addedTextLength - (editor.caretModel.primaryCaret.offset - prevDocumentOffset)
 | 
			
		||||
          val leftArrow = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)
 | 
			
		||||
          if (prevDocumentLength != null && prevDocumentOffset != null) {
 | 
			
		||||
            val register = VimPlugin.getRegister()
 | 
			
		||||
            val addedTextLength = editor.document.textLength - prevDocumentLength
 | 
			
		||||
            val caretShift = addedTextLength - (editor.caretModel.primaryCaret.offset - prevDocumentOffset)
 | 
			
		||||
            val leftArrow = KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0)
 | 
			
		||||
 | 
			
		||||
          register.recordText(editor.document.getText(TextRange(prevDocumentOffset, prevDocumentOffset + addedTextLength)))
 | 
			
		||||
          repeat(caretShift.coerceAtLeast(0)) {
 | 
			
		||||
            register.recordKeyStroke(leftArrow)
 | 
			
		||||
            register.recordText(editor.document.getText(TextRange(prevDocumentOffset, prevDocumentOffset + addedTextLength)))
 | 
			
		||||
            repeat(caretShift.coerceAtLeast(0)) {
 | 
			
		||||
              register.recordKeyStroke(leftArrow)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.completionPrevDocumentLength = null
 | 
			
		||||
          this.completionPrevDocumentOffset = null
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        //region Enter insert mode after surround with if
 | 
			
		||||
        if (surrounderAction == action.javaClass.name && surrounderItems.any {
 | 
			
		||||
            action.templatePresentation.text.endsWith(
 | 
			
		||||
              it,
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        ) {
 | 
			
		||||
          editor?.let {
 | 
			
		||||
            val commandState = it.vim.vimStateMachine
 | 
			
		||||
            it.vim.mode = Mode.NORMAL()
 | 
			
		||||
            VimPlugin.getChange().insertBeforeCursor(it.vim, event.dataContext.vim)
 | 
			
		||||
            KeyHandler.getInstance().reset(it.vim)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        //endregion
 | 
			
		||||
 | 
			
		||||
        this.completionPrevDocumentLength = null
 | 
			
		||||
        this.completionPrevDocumentOffset = null
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      //region Enter insert mode after surround with if
 | 
			
		||||
      if (surrounderAction == action.javaClass.name && surrounderItems.any {
 | 
			
		||||
          action.templatePresentation.text.endsWith(
 | 
			
		||||
            it,
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      ) {
 | 
			
		||||
        editor?.let {
 | 
			
		||||
          val commandState = it.vim.vimStateMachine
 | 
			
		||||
          it.vim.mode = Mode.NORMAL()
 | 
			
		||||
          VimPlugin.getChange().insertBeforeCursor(it.vim, event.dataContext.vim)
 | 
			
		||||
          KeyHandler.getInstance().reset(it.vim)
 | 
			
		||||
        if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {
 | 
			
		||||
          val scrollModel = editor.scrollingModel as ScrollingModelImpl
 | 
			
		||||
          if (scrollModel.isScrollingNow) {
 | 
			
		||||
            val v = scrollModel.verticalScrollOffset
 | 
			
		||||
            val h = scrollModel.horizontalScrollOffset
 | 
			
		||||
            scrollModel.finishAnimation()
 | 
			
		||||
            scrollModel.scroll(h, v)
 | 
			
		||||
            scrollModel.finishAnimation()
 | 
			
		||||
          }
 | 
			
		||||
          injector.scroll.scrollCaretIntoView(editor.vim)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      //endregion
 | 
			
		||||
 | 
			
		||||
      this.editor = null
 | 
			
		||||
      this.caretOffset = -1
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -163,7 +182,7 @@ internal object IdeaSpecifics {
 | 
			
		||||
          if (editor.vim.inNormalMode) {
 | 
			
		||||
            VimPlugin.getChange().insertBeforeCursor(
 | 
			
		||||
              editor.vim,
 | 
			
		||||
              injector.executionContextManager.onEditor(editor.vim),
 | 
			
		||||
              injector.executionContextManager.getEditorExecutionContext(editor.vim),
 | 
			
		||||
            )
 | 
			
		||||
            KeyHandler.getInstance().reset(editor.vim)
 | 
			
		||||
          }
 | 
			
		||||
@@ -213,5 +232,7 @@ internal class FindActionIdAction : DumbAwareToggleAction() {
 | 
			
		||||
  override fun setSelected(e: AnActionEvent, state: Boolean) {
 | 
			
		||||
    injector.globalIjOptions().trackactionids = !injector.globalIjOptions().trackactionids
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
 | 
			
		||||
}
 | 
			
		||||
//endregion
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,7 @@ import com.intellij.openapi.editor.ex.DocumentEx
 | 
			
		||||
import com.intellij.openapi.editor.ex.EditorEventMulticasterEx
 | 
			
		||||
import com.intellij.openapi.editor.ex.FocusChangeListener
 | 
			
		||||
import com.intellij.openapi.editor.impl.EditorComponentImpl
 | 
			
		||||
import com.intellij.openapi.editor.impl.EditorImpl
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManager
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
 | 
			
		||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
 | 
			
		||||
@@ -45,11 +46,14 @@ import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider
 | 
			
		||||
import com.intellij.openapi.fileEditor.impl.EditorComposite
 | 
			
		||||
import com.intellij.openapi.fileEditor.impl.EditorWindow
 | 
			
		||||
import com.intellij.openapi.project.ProjectManager
 | 
			
		||||
import com.intellij.openapi.rd.createLifetime
 | 
			
		||||
import com.intellij.openapi.rd.createNestedDisposable
 | 
			
		||||
import com.intellij.openapi.util.Disposer
 | 
			
		||||
import com.intellij.openapi.util.Key
 | 
			
		||||
import com.intellij.openapi.util.removeUserData
 | 
			
		||||
import com.intellij.openapi.vfs.VirtualFile
 | 
			
		||||
import com.intellij.util.ExceptionUtil
 | 
			
		||||
import com.jetbrains.rd.util.lifetime.Lifetime
 | 
			
		||||
import com.maddyhome.idea.vim.EventFacade
 | 
			
		||||
import com.maddyhome.idea.vim.KeyHandler
 | 
			
		||||
import com.maddyhome.idea.vim.KeyHandlerStateResetter
 | 
			
		||||
@@ -79,7 +83,6 @@ import com.maddyhome.idea.vim.handler.keyCheckRequests
 | 
			
		||||
import com.maddyhome.idea.vim.helper.CaretVisualAttributesListener
 | 
			
		||||
import com.maddyhome.idea.vim.helper.GuicursorChangeListener
 | 
			
		||||
import com.maddyhome.idea.vim.helper.StrictMode
 | 
			
		||||
import com.maddyhome.idea.vim.helper.VimStandalonePluginUpdateChecker
 | 
			
		||||
import com.maddyhome.idea.vim.helper.exitSelectMode
 | 
			
		||||
import com.maddyhome.idea.vim.helper.exitVisualMode
 | 
			
		||||
import com.maddyhome.idea.vim.helper.forceBarCursor
 | 
			
		||||
@@ -92,6 +95,7 @@ import com.maddyhome.idea.vim.helper.vimDisabled
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.inSelectMode
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.selectionType
 | 
			
		||||
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
 | 
			
		||||
@@ -101,7 +105,6 @@ import com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetListener
 | 
			
		||||
import com.maddyhome.idea.vim.ui.widgets.macro.macroWidgetOptionListener
 | 
			
		||||
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetListener
 | 
			
		||||
import com.maddyhome.idea.vim.ui.widgets.mode.modeWidgetOptionListener
 | 
			
		||||
import com.maddyhome.idea.vim.vimDisposable
 | 
			
		||||
import java.awt.event.MouseAdapter
 | 
			
		||||
import java.awt.event.MouseEvent
 | 
			
		||||
import javax.swing.SwingUtilities
 | 
			
		||||
@@ -264,12 +267,10 @@ internal object VimListenerManager {
 | 
			
		||||
      // TODO: If the user changes the 'ideavimsupport' option, existing editors won't be initialised
 | 
			
		||||
      if (vimDisabled(editor)) return
 | 
			
		||||
 | 
			
		||||
      // As I understand, there is no need to pass a disposable that also disposes on editor close
 | 
			
		||||
      //   because all editor resources will be garbage collected anyway on editor close
 | 
			
		||||
      // Note that this uses the plugin's main disposable, rather than VimPlugin.onOffDisposable, because we don't need
 | 
			
		||||
      // to - we explicitly call VimListenerManager.removeAll from VimPlugin.turnOffPlugin, and this disposes each
 | 
			
		||||
      // editor's disposable individually.
 | 
			
		||||
      val disposable = editor.project?.vimDisposable ?: return
 | 
			
		||||
      val pluginLifetime = VimPlugin.getInstance().createLifetime()
 | 
			
		||||
      val editorLifetime = (editor as EditorImpl).disposable.createLifetime()
 | 
			
		||||
      val disposable =
 | 
			
		||||
        Lifetime.intersect(pluginLifetime, editorLifetime).createNestedDisposable("MyLifetimedDisposable")
 | 
			
		||||
 | 
			
		||||
      val listenersDisposable = Disposer.newDisposable(disposable)
 | 
			
		||||
      editor.putUserData(editorListenersDisposableKey, listenersDisposable)
 | 
			
		||||
@@ -359,6 +360,15 @@ internal object VimListenerManager {
 | 
			
		||||
      // We can't rely on being passed a non-null editor, so check for Code With Me scenarios explicitly
 | 
			
		||||
      if (VimPlugin.isNotEnabled() || !ClientId.isCurrentlyUnderLocalId) return
 | 
			
		||||
      
 | 
			
		||||
      val newEditor = event.newEditor
 | 
			
		||||
      if (newEditor is TextEditor) {
 | 
			
		||||
        val editor = newEditor.editor
 | 
			
		||||
        if (editor.isInsertMode) {
 | 
			
		||||
          editor.vim.mode = Mode.NORMAL()
 | 
			
		||||
          KeyHandler.getInstance().reset(editor.vim)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      MotionGroup.fileEditorManagerSelectionChangedCallback(event)
 | 
			
		||||
      FileGroup.fileEditorManagerSelectionChangedCallback(event)
 | 
			
		||||
      VimPlugin.getSearch().fileEditorManagerSelectionChangedCallback(event)
 | 
			
		||||
@@ -429,8 +439,6 @@ internal object VimListenerManager {
 | 
			
		||||
 | 
			
		||||
        event.editor.putUserData(openingEditorKey, OpeningEditor(openingEditor, owningEditorWindow, isPreview, canBeReused))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      VimStandalonePluginUpdateChecker.instance.pluginUsed()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun editorReleased(event: EditorFactoryEvent) {
 | 
			
		||||
@@ -531,15 +539,15 @@ internal object VimListenerManager {
 | 
			
		||||
          // When starting on an empty line and dragging vertically upwards onto
 | 
			
		||||
          // another line, the selection should include the entirety of the empty line
 | 
			
		||||
          caret.setSelection(
 | 
			
		||||
            ijVimEditor.coerceOffset(endOffset + 1).point,
 | 
			
		||||
            ijVimEditor.coerceOffset(startOffset).point,
 | 
			
		||||
            ijVimEditor.coerceOffset(endOffset + 1),
 | 
			
		||||
            ijVimEditor.coerceOffset(startOffset),
 | 
			
		||||
          )
 | 
			
		||||
        } else if (lineEnd == startOffset + 1 && startOffset == endOffset) {
 | 
			
		||||
          // When dragging left from EOL on a non-empty line, the selection
 | 
			
		||||
          // should include the last character on the line
 | 
			
		||||
          caret.setSelection(
 | 
			
		||||
            ijVimEditor.coerceOffset(lineEnd).point,
 | 
			
		||||
            ijVimEditor.coerceOffset(lineEnd - 1).point,
 | 
			
		||||
            ijVimEditor.coerceOffset(lineEnd),
 | 
			
		||||
            ijVimEditor.coerceOffset(lineEnd - 1),
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -12,16 +12,8 @@ import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.util.Key
 | 
			
		||||
import com.intellij.openapi.util.UserDataHolder
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
 | 
			
		||||
internal open class IjEditorExecutionContext(override val context: DataContext) : ExecutionContext.Editor {
 | 
			
		||||
  override fun updateEditor(editor: VimEditor): ExecutionContext {
 | 
			
		||||
    return IjEditorExecutionContext(injector.executionContextManager.onEditor(editor, context.vim).ij)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
internal class IjCaretAndEditorExecutionContext(override val context: DataContext) : IjEditorExecutionContext(context), ExecutionContext.CaretAndEditor
 | 
			
		||||
internal open class IjEditorExecutionContext(override val context: DataContext) : ExecutionContext
 | 
			
		||||
 | 
			
		||||
// This key is stored in data context when the action is started from vim
 | 
			
		||||
internal val runFromVimKey = Key.create<Boolean>("RunFromVim")
 | 
			
		||||
 
 | 
			
		||||
@@ -9,23 +9,15 @@
 | 
			
		||||
package com.maddyhome.idea.vim.newapi
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.components.Service
 | 
			
		||||
import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext
 | 
			
		||||
import com.intellij.openapi.editor.ex.util.EditorUtil
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContextManagerBase
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.helper.EditorDataContext
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
internal class IjExecutionContextManager : ExecutionContextManagerBase() {
 | 
			
		||||
  override fun onEditor(editor: VimEditor, prevContext: ExecutionContext?): ExecutionContext.Editor {
 | 
			
		||||
    if (prevContext is ExecutionContext.CaretAndEditor) {
 | 
			
		||||
      return prevContext
 | 
			
		||||
    }
 | 
			
		||||
    return IjEditorExecutionContext(EditorDataContext.init((editor as IjVimEditor).editor, prevContext?.ij))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun onCaret(caret: VimCaret, prevContext: ExecutionContext.Editor): ExecutionContext.CaretAndEditor {
 | 
			
		||||
    return IjCaretAndEditorExecutionContext(CaretSpecificDataContext.create(prevContext.ij, caret.ij))
 | 
			
		||||
  override fun getEditorExecutionContext(editor: VimEditor): ExecutionContext {
 | 
			
		||||
    return EditorUtil.getEditorDataContext(editor.ij).vim
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,10 @@ package com.maddyhome.idea.vim.newapi
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.editor.RangeMarker
 | 
			
		||||
import com.maddyhome.idea.vim.common.LiveRange
 | 
			
		||||
import com.maddyhome.idea.vim.common.Offset
 | 
			
		||||
import com.maddyhome.idea.vim.common.offset
 | 
			
		||||
 | 
			
		||||
internal class IjLiveRange(val marker: RangeMarker) : LiveRange {
 | 
			
		||||
  override val startOffset: Offset
 | 
			
		||||
    get() = marker.startOffset.offset
 | 
			
		||||
  override val startOffset: Int
 | 
			
		||||
    get() = marker.startOffset
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public val RangeMarker.vim: LiveRange
 | 
			
		||||
 
 | 
			
		||||
@@ -34,4 +34,8 @@ internal class IjNativeActionManager : NativeActionManager {
 | 
			
		||||
public val AnAction.vim: IjNativeAction
 | 
			
		||||
  get() = IjNativeAction(this)
 | 
			
		||||
 | 
			
		||||
public class IjNativeAction(override val action: AnAction) : NativeAction
 | 
			
		||||
public class IjNativeAction(override val action: AnAction) : NativeAction {
 | 
			
		||||
  override fun toString(): String {
 | 
			
		||||
    return "IjNativeAction(action=$action)"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,7 @@ import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaretBase
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimVisualPosition
 | 
			
		||||
import com.maddyhome.idea.vim.common.EditorLine
 | 
			
		||||
import com.maddyhome.idea.vim.common.LiveRange
 | 
			
		||||
import com.maddyhome.idea.vim.common.Offset
 | 
			
		||||
import com.maddyhome.idea.vim.common.offset
 | 
			
		||||
import com.maddyhome.idea.vim.group.visual.VisualChange
 | 
			
		||||
import com.maddyhome.idea.vim.helper.lastSelectionInfo
 | 
			
		||||
import com.maddyhome.idea.vim.helper.markStorage
 | 
			
		||||
@@ -78,8 +75,8 @@ internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
 | 
			
		||||
    }
 | 
			
		||||
  override val editor: VimEditor
 | 
			
		||||
    get() = IjVimEditor(caret.editor)
 | 
			
		||||
  override val offset: Offset
 | 
			
		||||
    get() = caret.offset.offset
 | 
			
		||||
  override val offset: Int
 | 
			
		||||
    get() = caret.offset
 | 
			
		||||
  override var vimLastColumn: Int
 | 
			
		||||
    get() = caret.vimLastColumn
 | 
			
		||||
    set(value) {
 | 
			
		||||
@@ -118,8 +115,8 @@ internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
 | 
			
		||||
    this.caret.moveToLogicalPosition(LogicalPosition(position.line, position.column, position.leansForward))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getLine(): EditorLine.Pointer {
 | 
			
		||||
    return EditorLine.Pointer.init(caret.logicalPosition.line, editor)
 | 
			
		||||
  override fun getLine(): Int {
 | 
			
		||||
    return caret.logicalPosition.line
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun hasSelection(): Boolean {
 | 
			
		||||
@@ -164,8 +161,8 @@ internal class IjVimCaret(val caret: Caret) : VimCaretBase() {
 | 
			
		||||
    return this
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun setSelection(start: Offset, end: Offset) {
 | 
			
		||||
    caret.setSelection(start.point, end.point)
 | 
			
		||||
  override fun setSelection(start: Int, end: Int) {
 | 
			
		||||
    caret.setSelection(start, end)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun removeSelection() {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import com.intellij.openapi.editor.event.DocumentListener
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimDocument
 | 
			
		||||
import com.maddyhome.idea.vim.common.ChangesListener
 | 
			
		||||
import com.maddyhome.idea.vim.common.LiveRange
 | 
			
		||||
import com.maddyhome.idea.vim.common.Offset
 | 
			
		||||
 | 
			
		||||
internal class IjVimDocument(val document: Document) : VimDocument {
 | 
			
		||||
 | 
			
		||||
@@ -41,7 +40,7 @@ internal class IjVimDocument(val document: Document) : VimDocument {
 | 
			
		||||
    document.removeDocumentListener(nativeListener)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getOffsetGuard(offset: Offset): LiveRange? {
 | 
			
		||||
    return document.getOffsetGuard(offset.point)?.vim
 | 
			
		||||
  override fun getOffsetGuard(offset: Int): LiveRange? {
 | 
			
		||||
    return document.getOffsetGuard(offset)?.vim
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -35,13 +35,11 @@ import com.maddyhome.idea.vim.api.VimScrollingModel
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimSelectionModel
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimVisualPosition
 | 
			
		||||
import com.maddyhome.idea.vim.api.VirtualFile
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.command.OperatorArguments
 | 
			
		||||
import com.maddyhome.idea.vim.common.EditorLine
 | 
			
		||||
import com.maddyhome.idea.vim.common.IndentConfig
 | 
			
		||||
import com.maddyhome.idea.vim.common.LiveRange
 | 
			
		||||
import com.maddyhome.idea.vim.common.Offset
 | 
			
		||||
import com.maddyhome.idea.vim.common.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.common.offset
 | 
			
		||||
import com.maddyhome.idea.vim.group.visual.vimSetSystemBlockSelectionSilently
 | 
			
		||||
import com.maddyhome.idea.vim.helper.EditorHelper
 | 
			
		||||
import com.maddyhome.idea.vim.helper.StrictMode
 | 
			
		||||
@@ -90,18 +88,18 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
    return editor.document.lineCount
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun deleteRange(leftOffset: Offset, rightOffset: Offset) {
 | 
			
		||||
    editor.document.deleteString(leftOffset.point, rightOffset.point)
 | 
			
		||||
  override fun deleteRange(leftOffset: Int, rightOffset: Int) {
 | 
			
		||||
    editor.document.deleteString(leftOffset, rightOffset)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun addLine(atPosition: EditorLine.Offset): EditorLine.Pointer {
 | 
			
		||||
    val offset: Int = if (atPosition.line < lineCount()) {
 | 
			
		||||
  override fun addLine(atPosition: Int): Int {
 | 
			
		||||
    val offset: Int = if (atPosition < lineCount()) {
 | 
			
		||||
      // The new line character is inserted before the new line char of the previous line. So it works line an enter
 | 
			
		||||
      //   on a line end. I believe that the correct implementation would be to insert the new line char after the
 | 
			
		||||
      //   \n of the previous line, however at the moment this won't update the mark on this line.
 | 
			
		||||
      //   https://youtrack.jetbrains.com/issue/IDEA-286587
 | 
			
		||||
 | 
			
		||||
      val lineStart = (editor.document.getLineStartOffset(atPosition.line) - 1).coerceAtLeast(0)
 | 
			
		||||
      val lineStart = (editor.document.getLineStartOffset(atPosition) - 1).coerceAtLeast(0)
 | 
			
		||||
      val guard = editor.document.getOffsetGuard(lineStart)
 | 
			
		||||
      if (guard != null && guard.endOffset == lineStart + 1) {
 | 
			
		||||
        // Dancing around guarded blocks. It may happen that this concrete position is locked, but the next
 | 
			
		||||
@@ -116,11 +114,11 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
      fileSize().toInt()
 | 
			
		||||
    }
 | 
			
		||||
    editor.document.insertString(offset, "\n")
 | 
			
		||||
    return EditorLine.Pointer.init(atPosition.line, this)
 | 
			
		||||
    return atPosition
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun insertText(atPosition: Offset, text: CharSequence) {
 | 
			
		||||
    editor.document.insertString(atPosition.point, text)
 | 
			
		||||
  override fun insertText(atPosition: Int, text: CharSequence) {
 | 
			
		||||
    editor.document.insertString(atPosition, text)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun replaceString(start: Int, end: Int, newString: String) {
 | 
			
		||||
@@ -128,13 +126,13 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // TODO: 30.12.2021 Is end offset inclusive?
 | 
			
		||||
  override fun getLineRange(line: EditorLine.Pointer): Pair<Offset, Offset> {
 | 
			
		||||
  override fun getLineRange(line: Int): Pair<Int, Int> {
 | 
			
		||||
    // TODO: 30.12.2021 getLineEndOffset returns the same value for "xyz" and "xyz\n"
 | 
			
		||||
    return editor.document.getLineStartOffset(line.line).offset to editor.document.getLineEndOffset(line.line).offset
 | 
			
		||||
    return editor.document.getLineStartOffset(line) to editor.document.getLineEndOffset(line)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getLine(offset: Offset): EditorLine.Pointer {
 | 
			
		||||
    return EditorLine.Pointer.init(editor.offsetToLogicalPosition(offset.point).line, this)
 | 
			
		||||
  override fun getLine(offset: Int): Int {
 | 
			
		||||
    return editor.offsetToLogicalPosition(offset).line
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun carets(): List<VimCaret> {
 | 
			
		||||
@@ -203,15 +201,15 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
    return editor.isOneLineMode
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun getText(left: Offset, right: Offset): CharSequence {
 | 
			
		||||
    return editor.document.charsSequence.subSequence(left.point, right.point)
 | 
			
		||||
  override fun getText(left: Int, right: Int): CharSequence {
 | 
			
		||||
    return editor.document.charsSequence.subSequence(left, right)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun search(
 | 
			
		||||
    pair: Pair<Offset, Offset>,
 | 
			
		||||
    pair: Pair<Int, Int>,
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    shiftType: LineDeleteShift,
 | 
			
		||||
  ): Pair<Pair<Offset, Offset>, LineDeleteShift>? {
 | 
			
		||||
  ): Pair<Pair<Int, Int>, LineDeleteShift>? {
 | 
			
		||||
    val ijEditor = (editor as IjVimEditor).editor
 | 
			
		||||
    return when (shiftType) {
 | 
			
		||||
      LineDeleteShift.NO_NL -> if (pair.noGuard(ijEditor)) return pair to shiftType else null
 | 
			
		||||
@@ -358,10 +356,10 @@ 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 val projectId = editor.project?.let { injector.file.getProjectId(it) } ?: DEFAULT_PROJECT_ID
 | 
			
		||||
 | 
			
		||||
  override fun visualPositionToOffset(position: VimVisualPosition): Offset {
 | 
			
		||||
    return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)).offset
 | 
			
		||||
  override fun visualPositionToOffset(position: VimVisualPosition): Int {
 | 
			
		||||
    return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) {
 | 
			
		||||
@@ -417,8 +415,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
    return visualPosition.run { VimVisualPosition(line, column, leansRight) }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun createLiveMarker(start: Offset, end: Offset): LiveRange {
 | 
			
		||||
    return editor.document.createRangeMarker(start.point, end.point).vim
 | 
			
		||||
  override fun createLiveMarker(start: Int, end: Int): LiveRange {
 | 
			
		||||
    return editor.document.createRangeMarker(start, end).vim
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -456,10 +454,10 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
            ijFoldRegion.isExpanded = value
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      override val startOffset: Offset
 | 
			
		||||
        get() = Offset(ijFoldRegion.startOffset)
 | 
			
		||||
      override val endOffset: Offset
 | 
			
		||||
        get() = Offset(ijFoldRegion.endOffset)
 | 
			
		||||
      override val startOffset: Int
 | 
			
		||||
        get() = ijFoldRegion.startOffset
 | 
			
		||||
      override val endOffset: Int
 | 
			
		||||
        get() = ijFoldRegion.endOffset
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -468,17 +466,17 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
 | 
			
		||||
    return caret
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun Pair<Offset, Offset>.noGuard(editor: Editor): Boolean {
 | 
			
		||||
    return editor.document.getRangeGuard(this.first.point, this.second.point) == null
 | 
			
		||||
  private fun Pair<Int, Int>.noGuard(editor: Editor): Boolean {
 | 
			
		||||
    return editor.document.getRangeGuard(this.first, this.second) == null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private inline fun Pair<Offset, Offset>.shift(
 | 
			
		||||
  private inline fun Pair<Int, Int>.shift(
 | 
			
		||||
    shiftStart: Int = 0,
 | 
			
		||||
    shiftEnd: Int = 0,
 | 
			
		||||
    action: Pair<Offset, Offset>.() -> Unit,
 | 
			
		||||
    action: Pair<Int, Int>.() -> Unit,
 | 
			
		||||
  ) {
 | 
			
		||||
    val data =
 | 
			
		||||
      (this.first.point + shiftStart).coerceAtLeast(0).offset to (this.second.point + shiftEnd).coerceAtLeast(0).offset
 | 
			
		||||
      (this.first + shiftStart).coerceAtLeast(0) to (this.second + shiftEnd).coerceAtLeast(0)
 | 
			
		||||
    data.action()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -500,3 +498,6 @@ public val Editor.vim: VimEditor
 | 
			
		||||
  get() = IjVimEditor(this)
 | 
			
		||||
public val VimEditor.ij: Editor
 | 
			
		||||
  get() = (this as IjVimEditor).editor
 | 
			
		||||
 | 
			
		||||
public val com.intellij.openapi.util.TextRange.vim: TextRange
 | 
			
		||||
  get() = TextRange(this.startOffset, this.endOffset)
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,7 @@ import com.maddyhome.idea.vim.api.VimMessages
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimMotionGroup
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimOptionGroup
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimProcessGroup
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimPsiService
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimRegexpService
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimScrollGroup
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimSearchGroup
 | 
			
		||||
@@ -65,6 +66,7 @@ import com.maddyhome.idea.vim.group.FileGroup
 | 
			
		||||
import com.maddyhome.idea.vim.group.GlobalIjOptions
 | 
			
		||||
import com.maddyhome.idea.vim.group.HistoryGroup
 | 
			
		||||
import com.maddyhome.idea.vim.group.IjVimOptionGroup
 | 
			
		||||
import com.maddyhome.idea.vim.group.IjVimPsiService
 | 
			
		||||
import com.maddyhome.idea.vim.group.MacroGroup
 | 
			
		||||
import com.maddyhome.idea.vim.group.MotionGroup
 | 
			
		||||
import com.maddyhome.idea.vim.group.SearchGroup
 | 
			
		||||
@@ -147,6 +149,8 @@ internal class IjVimInjector : VimInjectorBase() {
 | 
			
		||||
    get() = service<MacroGroup>()
 | 
			
		||||
  override val undo: VimUndoRedo
 | 
			
		||||
    get() = service<UndoRedoHelper>()
 | 
			
		||||
  override val psiService: VimPsiService
 | 
			
		||||
    get() = service<IjVimPsiService>()
 | 
			
		||||
  override val commandLineHelper: VimCommandLineHelper
 | 
			
		||||
    get() = service<CommandLineHelper>()
 | 
			
		||||
  override val nativeActionManager: NativeActionManager
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ package com.maddyhome.idea.vim.newapi
 | 
			
		||||
import com.intellij.openapi.application.ApplicationManager
 | 
			
		||||
import com.intellij.openapi.util.Ref
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.Options
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
@@ -26,13 +27,11 @@ import com.maddyhome.idea.vim.helper.shouldIgnoreCase
 | 
			
		||||
import com.maddyhome.idea.vim.helper.updateSearchHighlights
 | 
			
		||||
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
 | 
			
		||||
import com.maddyhome.idea.vim.ui.ModalEntry
 | 
			
		||||
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
 | 
			
		||||
import com.maddyhome.idea.vim.vimscript.model.functions.handlers.SubmatchFunctionHandler
 | 
			
		||||
import com.maddyhome.idea.vim.vimscript.parser.VimscriptParser.parseExpression
 | 
			
		||||
import org.jetbrains.annotations.TestOnly
 | 
			
		||||
import javax.swing.KeyStroke
 | 
			
		||||
 | 
			
		||||
public open class IjVimSearchGroup : VimSearchGroupBase() {
 | 
			
		||||
public abstract class IjVimSearchGroup : VimSearchGroupBase() {
 | 
			
		||||
 | 
			
		||||
  init {
 | 
			
		||||
    // TODO: Investigate migrating these listeners to use the effective value change listener
 | 
			
		||||
@@ -82,6 +81,7 @@ public open class IjVimSearchGroup : VimSearchGroupBase() {
 | 
			
		||||
 | 
			
		||||
  override fun confirmChoice(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    context: ExecutionContext,
 | 
			
		||||
    match: String,
 | 
			
		||||
    caret: VimCaret,
 | 
			
		||||
    startOffset: Int,
 | 
			
		||||
@@ -121,7 +121,6 @@ public open class IjVimSearchGroup : VimSearchGroupBase() {
 | 
			
		||||
      // XXX: The Ex entry panel is used only for UI here, its logic might be inappropriate for this method
 | 
			
		||||
      val exEntryPanel: com.maddyhome.idea.vim.ui.ex.ExEntryPanel =
 | 
			
		||||
        com.maddyhome.idea.vim.ui.ex.ExEntryPanel.getInstanceWithoutShortcuts()
 | 
			
		||||
      val context = injector.executionContextManager.onEditor(editor, null)
 | 
			
		||||
      exEntryPanel.activate(
 | 
			
		||||
        editor.ij,
 | 
			
		||||
        (context as IjEditorExecutionContext).context,
 | 
			
		||||
@@ -136,10 +135,6 @@ public open class IjVimSearchGroup : VimSearchGroupBase() {
 | 
			
		||||
    return result.get()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun parseVimScriptExpression(expressionString: String): Expression? {
 | 
			
		||||
    return parseExpression(expressionString)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun addSubstitutionConfirmationHighlight(editor: VimEditor, startOffset: Int, endOffset: Int) {
 | 
			
		||||
    val hl = addSubstitutionConfirmationHighlight(
 | 
			
		||||
      (editor as IjVimEditor).editor,
 | 
			
		||||
 
 | 
			
		||||
@@ -13,144 +13,31 @@ import com.intellij.openapi.diagnostic.Logger
 | 
			
		||||
import com.maddyhome.idea.vim.api.ImmutableVimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimSearchHelperBase
 | 
			
		||||
import com.maddyhome.idea.vim.api.anyNonWhitespace
 | 
			
		||||
import com.maddyhome.idea.vim.api.getLineEndOffset
 | 
			
		||||
import com.maddyhome.idea.vim.api.getLineStartForOffset
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.api.normalizeOffset
 | 
			
		||||
import com.maddyhome.idea.vim.common.Direction
 | 
			
		||||
import com.maddyhome.idea.vim.common.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.helper.CharacterHelper
 | 
			
		||||
import com.maddyhome.idea.vim.helper.CharacterHelper.charType
 | 
			
		||||
import com.maddyhome.idea.vim.helper.PsiHelper
 | 
			
		||||
import com.maddyhome.idea.vim.helper.SearchHelper
 | 
			
		||||
import com.maddyhome.idea.vim.helper.SearchOptions
 | 
			
		||||
import com.maddyhome.idea.vim.helper.checkInString
 | 
			
		||||
import com.maddyhome.idea.vim.helper.fileSize
 | 
			
		||||
import com.maddyhome.idea.vim.state.VimStateMachine.Companion.getInstance
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntComparator
 | 
			
		||||
import it.unimi.dsi.fastutil.ints.IntComparators
 | 
			
		||||
import java.util.*
 | 
			
		||||
import java.util.function.Function
 | 
			
		||||
import java.util.regex.Pattern
 | 
			
		||||
import kotlin.math.abs
 | 
			
		||||
import kotlin.math.max
 | 
			
		||||
 | 
			
		||||
@Service
 | 
			
		||||
internal class IjVimSearchHelper : VimSearchHelperBase() {
 | 
			
		||||
 | 
			
		||||
  companion object {
 | 
			
		||||
    private const val BLOCK_CHARS = "{}()[]<>"
 | 
			
		||||
    private val logger = Logger.getInstance(IjVimSearchHelper::class.java.name)
 | 
			
		||||
  }
 | 
			
		||||
  override fun findSection(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
    type: Char,
 | 
			
		||||
    direction: Int,
 | 
			
		||||
    count: Int,
 | 
			
		||||
  )
 | 
			
		||||
  : Int {
 | 
			
		||||
    val documentText: CharSequence = editor.ij.document.charsSequence
 | 
			
		||||
    var currentLine: Int = caret.ij.logicalPosition.line + direction
 | 
			
		||||
    var resultOffset = -1
 | 
			
		||||
    var remainingTargets = count
 | 
			
		||||
 | 
			
		||||
    while (currentLine in 1 until editor.lineCount() && remainingTargets > 0) {
 | 
			
		||||
      val lineStartOffset = editor.getLineStartOffset(currentLine)
 | 
			
		||||
      if (lineStartOffset < documentText.length) {
 | 
			
		||||
        val currentChar = documentText[lineStartOffset]
 | 
			
		||||
        if (currentChar == type || currentChar == '\u000C') {
 | 
			
		||||
          resultOffset = lineStartOffset
 | 
			
		||||
          remainingTargets--
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      currentLine += direction
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (resultOffset == -1) {
 | 
			
		||||
      resultOffset = if (direction < 0) 0 else documentText.length - 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return resultOffset
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findMethodEnd(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int {
 | 
			
		||||
    // TODO add it to PsiService
 | 
			
		||||
    return PsiHelper.findMethodEnd(editor.ij, caret.ij.offset, count)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findMethodStart(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int {
 | 
			
		||||
    // TODO add it to PsiService
 | 
			
		||||
    return PsiHelper.findMethodStart(editor.ij, caret.ij.offset, count)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findUnmatchedBlock(editor: VimEditor, caret: ImmutableVimCaret, type: Char, count: Int): Int {
 | 
			
		||||
    val chars: CharSequence = editor.ij.document.charsSequence
 | 
			
		||||
    var pos: Int = caret.ij.offset
 | 
			
		||||
    val loc = BLOCK_CHARS.indexOf(type)
 | 
			
		||||
    // What direction should we go now (-1 is backward, 1 is forward)
 | 
			
		||||
    val dir = if (loc % 2 == 0) Direction.BACKWARDS else Direction.FORWARDS
 | 
			
		||||
    // Which character did we find and which should we now search for
 | 
			
		||||
    val match = BLOCK_CHARS[loc]
 | 
			
		||||
    val found = BLOCK_CHARS[loc - dir.toInt()]
 | 
			
		||||
 | 
			
		||||
    if (pos < chars.length && chars[pos] == type) {
 | 
			
		||||
      pos += dir.toInt()
 | 
			
		||||
    }
 | 
			
		||||
    return findBlockLocation(chars, found, match, dir, pos, count)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun findBlockLocation(
 | 
			
		||||
    chars: CharSequence,
 | 
			
		||||
    found: Char,
 | 
			
		||||
    match: Char,
 | 
			
		||||
    dir: Direction,
 | 
			
		||||
    pos: Int,
 | 
			
		||||
    cnt: Int,
 | 
			
		||||
  ): Int {
 | 
			
		||||
    var position = pos
 | 
			
		||||
    var count = cnt
 | 
			
		||||
    var res = -1
 | 
			
		||||
    val initialPos = position
 | 
			
		||||
    val initialInString = checkInString(chars, position, true)
 | 
			
		||||
    val inCheckPosF =
 | 
			
		||||
      Function { x: Int -> if (dir === Direction.BACKWARDS && x > 0) x - 1 else x + 1 }
 | 
			
		||||
    val inCheckPos = inCheckPosF.apply(position)
 | 
			
		||||
    var inString = checkInString(chars, inCheckPos, true)
 | 
			
		||||
    var inChar = checkInString(chars, inCheckPos, false)
 | 
			
		||||
    var stack = 0
 | 
			
		||||
    // Search to start or end of file, as appropriate
 | 
			
		||||
    val charsToSearch: Set<Char> = HashSet(listOf('\'', '"', '\n', match, found))
 | 
			
		||||
    while (position >= 0 && position < chars.length && count > 0) {
 | 
			
		||||
      val (c, second) = SearchHelper.findPositionOfFirstCharacter(chars, position, charsToSearch, true, dir) ?: return -1
 | 
			
		||||
      position = second
 | 
			
		||||
      // If we found a match and we're not in a string...
 | 
			
		||||
      if (c == match && (!inString) && !inChar) {
 | 
			
		||||
        // We found our match
 | 
			
		||||
        if (stack == 0) {
 | 
			
		||||
          res = position
 | 
			
		||||
          count--
 | 
			
		||||
        } else {
 | 
			
		||||
          stack--
 | 
			
		||||
        }
 | 
			
		||||
      } else if (c == '\n') {
 | 
			
		||||
        inString = false
 | 
			
		||||
        inChar = false
 | 
			
		||||
      } else if (position != initialPos) {
 | 
			
		||||
        // We found another character like our original - belongs to another pair
 | 
			
		||||
        if (!inString && !inChar && c == found) {
 | 
			
		||||
          stack++
 | 
			
		||||
        } else if (!inChar) {
 | 
			
		||||
          inString = checkInString(chars, inCheckPosF.apply(position), true)
 | 
			
		||||
        } else if (!inString) {
 | 
			
		||||
          inChar = checkInString(chars, inCheckPosF.apply(position), false)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      position += dir.toInt()
 | 
			
		||||
    }
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findPattern(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    pattern: String?,
 | 
			
		||||
@@ -173,525 +60,6 @@ internal class IjVimSearchHelper : VimSearchHelperBase() {
 | 
			
		||||
    else SearchHelper.findAll(editor.ij, pattern, startLine, endLine, ignoreCase)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findNextCharacterOnLine(editor: VimEditor, caret: ImmutableVimCaret, count: Int, ch: Char): Int {
 | 
			
		||||
    val line: Int = caret.ij.logicalPosition.line
 | 
			
		||||
    val start = editor.getLineStartOffset(line)
 | 
			
		||||
    val end = editor.getLineEndOffset(line, true)
 | 
			
		||||
    val chars: CharSequence = editor.ij.document.charsSequence
 | 
			
		||||
    var found = 0
 | 
			
		||||
    val step = if (count >= 0) 1 else -1
 | 
			
		||||
    var pos: Int = caret.ij.offset + step
 | 
			
		||||
    while (pos in start until end && pos < chars.length) {
 | 
			
		||||
      if (chars[pos] == ch) {
 | 
			
		||||
        found++
 | 
			
		||||
        if (found == abs(count)) {
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      pos += step
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return if (found == abs(count)) {
 | 
			
		||||
      pos
 | 
			
		||||
    } else {
 | 
			
		||||
      -1
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findWordUnderCursor(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
    count: Int,
 | 
			
		||||
    dir: Int,
 | 
			
		||||
    isOuter: Boolean,
 | 
			
		||||
    isBig: Boolean,
 | 
			
		||||
    hasSelection: Boolean,
 | 
			
		||||
  ): TextRange {
 | 
			
		||||
    if (logger.isDebugEnabled) {
 | 
			
		||||
      logger.debug("count=$count")
 | 
			
		||||
      logger.debug("dir=$dir")
 | 
			
		||||
      logger.debug("isOuter=$isOuter")
 | 
			
		||||
      logger.debug("isBig=$isBig")
 | 
			
		||||
      logger.debug("hasSelection=$hasSelection")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val chars: CharSequence = editor.ij.document.charsSequence
 | 
			
		||||
    //int min = EditorHelper.getLineStartOffset(editor, EditorHelper.getCurrentLogicalLine(editor));
 | 
			
		||||
    //int max = EditorHelper.getLineEndOffset(editor, EditorHelper.getCurrentLogicalLine(editor), true);
 | 
			
		||||
    val min = 0
 | 
			
		||||
    val max: Int = editor.ij.fileSize
 | 
			
		||||
    if (max == 0) return TextRange(0, 0)
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) {
 | 
			
		||||
      logger.debug("min=$min")
 | 
			
		||||
      logger.debug("max=$max")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    val pos: Int = caret.ij.offset
 | 
			
		||||
    if (chars.length <= pos) return TextRange(chars.length - 1, chars.length - 1)
 | 
			
		||||
 | 
			
		||||
    val startSpace = charType(editor, chars[pos], isBig) === CharacterHelper.CharacterType.WHITESPACE
 | 
			
		||||
    // Find word start
 | 
			
		||||
    val onWordStart = pos == min ||
 | 
			
		||||
      charType(editor, chars[pos - 1], isBig) !==
 | 
			
		||||
      charType(editor, chars[pos], isBig)
 | 
			
		||||
    var start = pos
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) {
 | 
			
		||||
      logger.debug("pos=$pos")
 | 
			
		||||
      logger.debug("onWordStart=$onWordStart")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!onWordStart && !(startSpace && isOuter) || hasSelection || count > 1 && dir == -1) {
 | 
			
		||||
      start = if (dir == 1) {
 | 
			
		||||
        findNextWord(editor, pos, -1, isBig, !isOuter)
 | 
			
		||||
      } else {
 | 
			
		||||
        findNextWord(
 | 
			
		||||
          editor,
 | 
			
		||||
          pos,
 | 
			
		||||
          -(count - if (onWordStart && !hasSelection) 1 else 0),
 | 
			
		||||
          isBig,
 | 
			
		||||
          !isOuter
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      start = editor.normalizeOffset(start, false)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) logger.debug("start=$start")
 | 
			
		||||
 | 
			
		||||
    // Find word end
 | 
			
		||||
 | 
			
		||||
    // Find word end
 | 
			
		||||
    val onWordEnd = pos >= max - 1 ||
 | 
			
		||||
      charType(editor, chars[pos + 1], isBig) !==
 | 
			
		||||
      charType(editor, chars[pos], isBig)
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) logger.debug("onWordEnd=$onWordEnd")
 | 
			
		||||
 | 
			
		||||
    var end = pos
 | 
			
		||||
    if (!onWordEnd || hasSelection || count > 1 && dir == 1 || startSpace && isOuter) {
 | 
			
		||||
      end = if (dir == 1) {
 | 
			
		||||
        val c = count - if (onWordEnd && !hasSelection && (!(startSpace && isOuter) || startSpace && !isOuter)) 1 else 0
 | 
			
		||||
        findNextWordEnd(editor, pos, c, isBig, !isOuter)
 | 
			
		||||
      } else {
 | 
			
		||||
        findNextWordEnd(editor, pos, 1, isBig, !isOuter)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) logger.debug("end=$end")
 | 
			
		||||
 | 
			
		||||
    var goBack = startSpace && !hasSelection || !startSpace && hasSelection && !onWordStart
 | 
			
		||||
    if (dir == 1 && isOuter) {
 | 
			
		||||
      var firstEnd = end
 | 
			
		||||
      if (count > 1) {
 | 
			
		||||
        firstEnd = findNextWordEnd(editor, pos, 1, isBig, false)
 | 
			
		||||
      }
 | 
			
		||||
      if (firstEnd < max - 1) {
 | 
			
		||||
        if (charType(editor, chars[firstEnd + 1], false) !== CharacterHelper.CharacterType.WHITESPACE) {
 | 
			
		||||
          goBack = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (dir == -1 && isOuter && startSpace) {
 | 
			
		||||
      if (pos > min) {
 | 
			
		||||
        if (charType(editor, chars[pos - 1], false) !== CharacterHelper.CharacterType.WHITESPACE) {
 | 
			
		||||
          goBack = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var goForward = dir == 1 && isOuter && (!startSpace && !onWordEnd || startSpace && onWordEnd && hasSelection)
 | 
			
		||||
    if (!goForward && dir == 1 && isOuter) {
 | 
			
		||||
      var firstEnd = end
 | 
			
		||||
      if (count > 1) {
 | 
			
		||||
        firstEnd = findNextWordEnd(editor, pos, 1, isBig, false)
 | 
			
		||||
      }
 | 
			
		||||
      if (firstEnd < max - 1) {
 | 
			
		||||
        if (charType(editor, chars[firstEnd + 1], false) !== CharacterHelper.CharacterType.WHITESPACE) {
 | 
			
		||||
          goForward = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!goForward && dir == 1 && isOuter && !startSpace && !hasSelection) {
 | 
			
		||||
      if (end < max - 1) {
 | 
			
		||||
        if (charType(editor, chars[end + 1], !isBig) !==
 | 
			
		||||
          charType(editor, chars[end], !isBig)
 | 
			
		||||
        ) {
 | 
			
		||||
          goForward = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) {
 | 
			
		||||
      logger.debug("goBack=$goBack")
 | 
			
		||||
      logger.debug("goForward=$goForward")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (goForward) {
 | 
			
		||||
      if (editor.anyNonWhitespace(end, 1)) {
 | 
			
		||||
        while (end + 1 < max &&
 | 
			
		||||
          charType(editor, chars[end + 1], false) === CharacterHelper.CharacterType.WHITESPACE
 | 
			
		||||
        ) {
 | 
			
		||||
          end++
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (goBack) {
 | 
			
		||||
      if (editor.anyNonWhitespace(start, -1)) {
 | 
			
		||||
        while (start > min &&
 | 
			
		||||
          charType(editor, chars[start - 1], false) === CharacterHelper.CharacterType.WHITESPACE
 | 
			
		||||
        ) {
 | 
			
		||||
          start--
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (logger.isDebugEnabled) {
 | 
			
		||||
      logger.debug("start=$start")
 | 
			
		||||
      logger.debug("end=$end")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // End offset is exclusive
 | 
			
		||||
    return TextRange(start, end + 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findBlockTagRange(editor: VimEditor, caret: ImmutableVimCaret, count: Int, isOuter: Boolean): TextRange? {
 | 
			
		||||
    var counter = count
 | 
			
		||||
    var isOuterVariable = isOuter
 | 
			
		||||
    val position: Int = caret.ij.offset
 | 
			
		||||
    val sequence: CharSequence = editor.ij.document.charsSequence
 | 
			
		||||
 | 
			
		||||
    val selectionStart: Int = caret.ij.selectionStart
 | 
			
		||||
    val selectionEnd: Int = caret.ij.selectionEnd
 | 
			
		||||
 | 
			
		||||
    val isRangeSelection = selectionEnd - selectionStart > 1
 | 
			
		||||
 | 
			
		||||
    var searchStartPosition: Int
 | 
			
		||||
    searchStartPosition = if (!isRangeSelection) {
 | 
			
		||||
      val line: Int = caret.ij.logicalPosition.line
 | 
			
		||||
      val lineBegin: Int = editor.ij.document.getLineStartOffset(line)
 | 
			
		||||
      ignoreWhitespaceAtLineStart(sequence, lineBegin, position)
 | 
			
		||||
    } else {
 | 
			
		||||
      selectionEnd
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isInHTMLTag(sequence, searchStartPosition, false)) {
 | 
			
		||||
      // caret is inside opening tag. Move to closing '>'.
 | 
			
		||||
      while (searchStartPosition < sequence.length && sequence[searchStartPosition] != '>') {
 | 
			
		||||
        searchStartPosition++
 | 
			
		||||
      }
 | 
			
		||||
    } else if (isInHTMLTag(sequence, searchStartPosition, true)) {
 | 
			
		||||
      // caret is inside closing tag. Move to starting '<'.
 | 
			
		||||
      while (searchStartPosition > 0 && sequence[searchStartPosition] != '<') {
 | 
			
		||||
        searchStartPosition--
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while (true) {
 | 
			
		||||
      val (closingTagTextRange, tagName) = findUnmatchedClosingTag(sequence, searchStartPosition, counter)
 | 
			
		||||
        ?: return null
 | 
			
		||||
      val openingTag = findUnmatchedOpeningTag(sequence, closingTagTextRange.startOffset, tagName)
 | 
			
		||||
        ?: return null
 | 
			
		||||
      if (isRangeSelection && openingTag.endOffset - 1 >= selectionStart) {
 | 
			
		||||
        // If there was already some text selected and the new selection would not extend further, we try again
 | 
			
		||||
        searchStartPosition = closingTagTextRange.endOffset
 | 
			
		||||
        counter = 1
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
      var selectionEndWithoutNewline = selectionEnd
 | 
			
		||||
      while (selectionEndWithoutNewline < sequence.length && sequence[selectionEndWithoutNewline] == '\n') {
 | 
			
		||||
        selectionEndWithoutNewline++
 | 
			
		||||
      }
 | 
			
		||||
      val mode = getInstance(editor).mode
 | 
			
		||||
      if (mode is VISUAL) {
 | 
			
		||||
        if (closingTagTextRange.startOffset == selectionEndWithoutNewline &&
 | 
			
		||||
          openingTag.endOffset == selectionStart
 | 
			
		||||
        ) {
 | 
			
		||||
          // Special case: if the inner tag is already selected we should like isOuter is active
 | 
			
		||||
          // Note that we need to ignore newlines, because their selection is lost between multiple "it" invocations
 | 
			
		||||
          isOuterVariable = true
 | 
			
		||||
        } else if (openingTag.endOffset == closingTagTextRange.startOffset &&
 | 
			
		||||
          selectionStart == openingTag.endOffset
 | 
			
		||||
        ) {
 | 
			
		||||
          // Special case: for an empty tag pair (e.g. <a></a>) the whole tag is selected if the caret is in the middle.
 | 
			
		||||
          isOuterVariable = true
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return if (isOuterVariable) {
 | 
			
		||||
        TextRange(openingTag.startOffset, closingTagTextRange.endOffset)
 | 
			
		||||
      } else {
 | 
			
		||||
        TextRange(openingTag.endOffset, closingTagTextRange.startOffset)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * returns new position which ignore whitespaces at beginning of the line
 | 
			
		||||
   */
 | 
			
		||||
  private fun ignoreWhitespaceAtLineStart(seq: CharSequence, lineStart: Int, pos: Int): Int {
 | 
			
		||||
    var position = pos
 | 
			
		||||
    if (seq.subSequence(lineStart, position).chars().allMatch { codePoint: Int ->
 | 
			
		||||
        Character.isWhitespace(
 | 
			
		||||
          codePoint
 | 
			
		||||
        )
 | 
			
		||||
      }) {
 | 
			
		||||
      while (position < seq.length && seq[position] != '\n' && Character.isWhitespace(seq[position])) {
 | 
			
		||||
        position++
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return position
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns true if there is a html at the given position. Ignores tags with a trailing slash like <aaa></aaa>.
 | 
			
		||||
   */
 | 
			
		||||
  private fun isInHTMLTag(sequence: CharSequence, position: Int, isEndtag: Boolean): Boolean {
 | 
			
		||||
    var openingBracket = -1
 | 
			
		||||
    run {
 | 
			
		||||
      var i = position
 | 
			
		||||
      while (i >= 0 && i < sequence.length) {
 | 
			
		||||
        if (sequence[i] == '<') {
 | 
			
		||||
          openingBracket = i
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        if (sequence[i] == '>' && i != position) {
 | 
			
		||||
          return false
 | 
			
		||||
        }
 | 
			
		||||
        i--
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (openingBracket == -1) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    val hasSlashAfterOpening = openingBracket + 1 < sequence.length && sequence[openingBracket + 1] == '/'
 | 
			
		||||
    if (isEndtag && !hasSlashAfterOpening || !isEndtag && hasSlashAfterOpening) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    var closingBracket = -1
 | 
			
		||||
    for (i in openingBracket until sequence.length) {
 | 
			
		||||
      if (sequence[i] == '>') {
 | 
			
		||||
        closingBracket = i
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return closingBracket != -1 && sequence[closingBracket - 1] != '/'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun findUnmatchedOpeningTag(
 | 
			
		||||
    sequence: CharSequence,
 | 
			
		||||
    position: Int,
 | 
			
		||||
    tagName: String,
 | 
			
		||||
  ): TextRange? {
 | 
			
		||||
    val quotedTagName = Pattern.quote(tagName)
 | 
			
		||||
    val patternString = ("(</%s>)" // match closing tags
 | 
			
		||||
      +
 | 
			
		||||
      "|(<%s" // or opening tags starting with tagName
 | 
			
		||||
      +
 | 
			
		||||
      "(\\s([^>]*" // After at least one whitespace there might be additional text in the tag. E.g. <html lang="en">
 | 
			
		||||
      +
 | 
			
		||||
      "[^/])?)?>)") // Slash is not allowed as last character (this would be a self closing tag).
 | 
			
		||||
    val tagPattern =
 | 
			
		||||
      Pattern.compile(String.format(patternString, quotedTagName, quotedTagName), Pattern.CASE_INSENSITIVE)
 | 
			
		||||
    val matcher = tagPattern.matcher(sequence.subSequence(0, position + 1))
 | 
			
		||||
    val openTags: Deque<TextRange> = ArrayDeque()
 | 
			
		||||
    while (matcher.find()) {
 | 
			
		||||
      val match = TextRange(matcher.start(), matcher.end())
 | 
			
		||||
      if (sequence[matcher.start() + 1] == '/') {
 | 
			
		||||
        if (!openTags.isEmpty()) {
 | 
			
		||||
          openTags.pop()
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        openTags.push(match)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return if (openTags.isEmpty()) {
 | 
			
		||||
      null
 | 
			
		||||
    } else {
 | 
			
		||||
      openTags.pop()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun findUnmatchedClosingTag(
 | 
			
		||||
    sequence: CharSequence,
 | 
			
		||||
    position: Int,
 | 
			
		||||
    count: Int,
 | 
			
		||||
  ): Pair<TextRange, String>? {
 | 
			
		||||
    // The tag name may contain any characters except slashes, whitespace and '>'
 | 
			
		||||
    var counter = count
 | 
			
		||||
    val tagNamePattern = "([^/\\s>]+)"
 | 
			
		||||
    // An opening tag consists of '<' followed by a tag name, optionally some additional text after whitespace and a '>'
 | 
			
		||||
    val openingTagPattern = String.format("<%s(?:\\s[^>]*)?>", tagNamePattern)
 | 
			
		||||
    val closingTagPattern = String.format("</%s>", tagNamePattern)
 | 
			
		||||
    val tagPattern = Pattern.compile(String.format("(?:%s)|(?:%s)", openingTagPattern, closingTagPattern))
 | 
			
		||||
    val matcher = tagPattern.matcher(sequence.subSequence(position, sequence.length))
 | 
			
		||||
    val openTags: Deque<String> = ArrayDeque()
 | 
			
		||||
    while (matcher.find()) {
 | 
			
		||||
      val isClosingTag = matcher.group(1) == null
 | 
			
		||||
      if (isClosingTag) {
 | 
			
		||||
        val tagName = matcher.group(2)
 | 
			
		||||
        // Ignore unmatched open tags. Either the file is malformed or it might be a tag like <br> that does not need to be closed.
 | 
			
		||||
        while (!openTags.isEmpty() && !openTags.peek().equals(tagName, ignoreCase = true)) {
 | 
			
		||||
          openTags.pop()
 | 
			
		||||
        }
 | 
			
		||||
        if (openTags.isEmpty()) {
 | 
			
		||||
          if (counter <= 1) {
 | 
			
		||||
            return Pair(TextRange(position + matcher.start(), position + matcher.end()), tagName)
 | 
			
		||||
          } else {
 | 
			
		||||
            counter--
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          openTags.pop()
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        val tagName = matcher.group(1)
 | 
			
		||||
        openTags.push(tagName)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findBlockRange(
 | 
			
		||||
    editor: VimEditor,
 | 
			
		||||
    caret: ImmutableVimCaret,
 | 
			
		||||
    type: Char,
 | 
			
		||||
    count: Int,
 | 
			
		||||
    isOuter: Boolean,
 | 
			
		||||
  ): TextRange? {
 | 
			
		||||
    val chars: CharSequence = editor.ij.document.charsSequence
 | 
			
		||||
    var pos: Int = caret.ij.offset
 | 
			
		||||
    var start: Int = caret.ij.selectionStart
 | 
			
		||||
    var end: Int = caret.ij.selectionEnd
 | 
			
		||||
 | 
			
		||||
    val loc = BLOCK_CHARS.indexOf(type)
 | 
			
		||||
    val close = BLOCK_CHARS[loc + 1]
 | 
			
		||||
 | 
			
		||||
    // extend the range for blank line after type and before close, as they are excluded when inner match
 | 
			
		||||
    if (!isOuter) {
 | 
			
		||||
      if (start > 1 && chars[start - 2] == type && chars[start - 1] == '\n') {
 | 
			
		||||
        start--
 | 
			
		||||
      }
 | 
			
		||||
      if (end < chars.length && chars[end] == '\n') {
 | 
			
		||||
        var isSingleLineAllWhiteSpaceUntilClose = false
 | 
			
		||||
        var countWhiteSpaceCharacter = 1
 | 
			
		||||
        while (end + countWhiteSpaceCharacter < chars.length) {
 | 
			
		||||
          if (Character.isWhitespace(chars[end + countWhiteSpaceCharacter]) &&
 | 
			
		||||
            chars[end + countWhiteSpaceCharacter] != '\n'
 | 
			
		||||
          ) {
 | 
			
		||||
            countWhiteSpaceCharacter++
 | 
			
		||||
            continue
 | 
			
		||||
          }
 | 
			
		||||
          if (chars[end + countWhiteSpaceCharacter] == close) {
 | 
			
		||||
            isSingleLineAllWhiteSpaceUntilClose = true
 | 
			
		||||
          }
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        if (isSingleLineAllWhiteSpaceUntilClose) {
 | 
			
		||||
          end += countWhiteSpaceCharacter
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    var rangeSelection = end - start > 1
 | 
			
		||||
    if (rangeSelection && start == 0) // early return not only for optimization
 | 
			
		||||
    {
 | 
			
		||||
      return null // but also not to break the interval semantic on this edge case (see below)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* In case of successive inner selection. We want to break out of
 | 
			
		||||
     * the block delimiter of the current inner selection.
 | 
			
		||||
     * In other terms, for the rest of the algorithm, a previous inner selection of a block
 | 
			
		||||
     * if equivalent to an outer one. */
 | 
			
		||||
 | 
			
		||||
    /* In case of successive inner selection. We want to break out of
 | 
			
		||||
     * the block delimiter of the current inner selection.
 | 
			
		||||
     * In other terms, for the rest of the algorithm, a previous inner selection of a block
 | 
			
		||||
     * if equivalent to an outer one. */if (!isOuter && start - 1 >= 0 && type == chars[start - 1] && end < chars.length && close == chars[end]) {
 | 
			
		||||
      start -= 1
 | 
			
		||||
      pos = start
 | 
			
		||||
      rangeSelection = true
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* when one char is selected, we want to find the enclosing block of (start,end]
 | 
			
		||||
     * although when a range of characters is selected, we want the enclosing block of [start, end]
 | 
			
		||||
     * shifting the position allow to express which kind of interval we work on */
 | 
			
		||||
 | 
			
		||||
    /* when one char is selected, we want to find the enclosing block of (start,end]
 | 
			
		||||
     * although when a range of characters is selected, we want the enclosing block of [start, end]
 | 
			
		||||
     * shifting the position allow to express which kind of interval we work on */if (rangeSelection) pos =
 | 
			
		||||
      max(0.0, (start - 1).toDouble()).toInt()
 | 
			
		||||
 | 
			
		||||
    val initialPosIsInString = checkInString(chars, pos, true)
 | 
			
		||||
 | 
			
		||||
    var bstart = -1
 | 
			
		||||
    var bend = -1
 | 
			
		||||
 | 
			
		||||
    var startPosInStringFound = false
 | 
			
		||||
 | 
			
		||||
    if (initialPosIsInString) {
 | 
			
		||||
      val quoteRange = injector.searchHelper
 | 
			
		||||
        .findBlockQuoteInLineRange(editor, caret, '"', false)
 | 
			
		||||
      if (quoteRange != null) {
 | 
			
		||||
        val startOffset = quoteRange.startOffset
 | 
			
		||||
        val endOffset = quoteRange.endOffset
 | 
			
		||||
        val subSequence = chars.subSequence(startOffset, endOffset)
 | 
			
		||||
        val inQuotePos = pos - startOffset
 | 
			
		||||
        var inQuoteStart =
 | 
			
		||||
          findBlockLocation(subSequence, close, type, Direction.BACKWARDS, inQuotePos, count)
 | 
			
		||||
        if (inQuoteStart == -1) {
 | 
			
		||||
          inQuoteStart =
 | 
			
		||||
            findBlockLocation(subSequence, close, type, Direction.FORWARDS, inQuotePos, count)
 | 
			
		||||
        }
 | 
			
		||||
        if (inQuoteStart != -1) {
 | 
			
		||||
          startPosInStringFound = true
 | 
			
		||||
          val inQuoteEnd =
 | 
			
		||||
            findBlockLocation(subSequence, type, close, Direction.FORWARDS, inQuoteStart, 1)
 | 
			
		||||
          if (inQuoteEnd != -1) {
 | 
			
		||||
            bstart = inQuoteStart + startOffset
 | 
			
		||||
            bend = inQuoteEnd + startOffset
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!startPosInStringFound) {
 | 
			
		||||
      bstart = findBlockLocation(chars, close, type, Direction.BACKWARDS, pos, count)
 | 
			
		||||
      if (bstart == -1) {
 | 
			
		||||
        bstart = findBlockLocation(chars, close, type, Direction.FORWARDS, pos, count)
 | 
			
		||||
      }
 | 
			
		||||
      if (bstart != -1) {
 | 
			
		||||
        bend = findBlockLocation(chars, type, close, Direction.FORWARDS, bstart, 1)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (bstart == -1 || bend == -1) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!isOuter) {
 | 
			
		||||
      bstart++
 | 
			
		||||
      // exclude first line break after start for inner match
 | 
			
		||||
      if (chars[bstart] == '\n') {
 | 
			
		||||
        bstart++
 | 
			
		||||
      }
 | 
			
		||||
      val o = editor.getLineStartForOffset(bend)
 | 
			
		||||
      var allWhite = true
 | 
			
		||||
      for (i in o until bend) {
 | 
			
		||||
        if (!Character.isWhitespace(chars[i])) {
 | 
			
		||||
          allWhite = false
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (allWhite) {
 | 
			
		||||
        bend = o - 2
 | 
			
		||||
      } else {
 | 
			
		||||
        bend--
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // End offset exclusive
 | 
			
		||||
    return TextRange(bstart, bend + 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override fun findMisspelledWord(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Int {
 | 
			
		||||
    val startOffset: Int
 | 
			
		||||
    val endOffset: Int
 | 
			
		||||
@@ -700,17 +68,18 @@ internal class IjVimSearchHelper : VimSearchHelperBase() {
 | 
			
		||||
 | 
			
		||||
    if (count < 0) {
 | 
			
		||||
      startOffset = 0
 | 
			
		||||
      endOffset = caret.offset.point - 1
 | 
			
		||||
      endOffset = caret.offset - 1
 | 
			
		||||
      skipCount = -count - 1
 | 
			
		||||
      offsetOrdering = IntComparators.OPPOSITE_COMPARATOR
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      startOffset = caret.offset.point + 1
 | 
			
		||||
      startOffset = caret.offset + 1
 | 
			
		||||
      endOffset = editor.ij.document.textLength
 | 
			
		||||
      skipCount = count - 1
 | 
			
		||||
      offsetOrdering = IntComparators.NATURAL_COMPARATOR
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO add it to PsiService
 | 
			
		||||
    return SearchHelper.findMisspelledWords(editor.ij, startOffset, endOffset, skipCount, offsetOrdering)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -307,7 +307,7 @@ public class ExOutputPanel extends JPanel {
 | 
			
		||||
                    KeyHandler.getInstance().getKeyStack().dump());
 | 
			
		||||
        }
 | 
			
		||||
        KeyHandler.getInstance().getKeyStack().addKeys(keys);
 | 
			
		||||
        ExecutionContext.Editor context = injector.getExecutionContextManager().onEditor(new IjVimEditor(myEditor), null);
 | 
			
		||||
        ExecutionContext context = injector.getExecutionContextManager().getEditorExecutionContext(new IjVimEditor(myEditor));
 | 
			
		||||
        VimPlugin.getMacro().playbackKeys(new IjVimEditor(myEditor), context, 1);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.ui.ex
 | 
			
		||||
import com.intellij.openapi.diagnostic.debug
 | 
			
		||||
import com.intellij.openapi.diagnostic.logger
 | 
			
		||||
import com.maddyhome.idea.vim.KeyHandler
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import org.jetbrains.annotations.NonNls
 | 
			
		||||
import java.awt.event.ActionEvent
 | 
			
		||||
@@ -126,12 +125,7 @@ internal object ExEditorKit : DefaultEditorKit() {
 | 
			
		||||
              val entry = ExEntryPanel.getInstance().entry
 | 
			
		||||
              val editor = entry.editor
 | 
			
		||||
              val keyHandler = KeyHandler.getInstance()
 | 
			
		||||
              keyHandler.handleKey(
 | 
			
		||||
                editor.vim,
 | 
			
		||||
                key,
 | 
			
		||||
                injector.executionContextManager.onEditor(editor.vim, entry.context.vim),
 | 
			
		||||
                keyHandler.keyHandlerState,
 | 
			
		||||
              )
 | 
			
		||||
              keyHandler.handleKey(editor.vim, key, entry.context.vim, keyHandler.keyHandlerState)
 | 
			
		||||
            } else {
 | 
			
		||||
              val event = ActionEvent(e.source, e.id, c.toString(), e.getWhen(), e.modifiers)
 | 
			
		||||
              super.actionPerformed(event)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,6 @@ import com.intellij.openapi.actionSystem.AnActionEvent
 | 
			
		||||
import com.intellij.openapi.actionSystem.KeyboardShortcut
 | 
			
		||||
import com.intellij.openapi.project.DumbAwareAction
 | 
			
		||||
import com.maddyhome.idea.vim.KeyHandler
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import java.awt.event.KeyEvent
 | 
			
		||||
import javax.swing.KeyStroke
 | 
			
		||||
@@ -37,12 +36,12 @@ internal class ExShortcutKeyAction(private val exEntryPanel: ExEntryPanel) : Dum
 | 
			
		||||
    if (keyStroke != null) {
 | 
			
		||||
      val editor = exEntryPanel.entry.editor
 | 
			
		||||
      val keyHandler = KeyHandler.getInstance()
 | 
			
		||||
      keyHandler.handleKey(
 | 
			
		||||
        editor.vim,
 | 
			
		||||
        keyStroke,
 | 
			
		||||
        injector.executionContextManager.onEditor(editor.vim, e.dataContext.vim),
 | 
			
		||||
        keyHandler.keyHandlerState
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      // About the context: we use the context of the main editor to execute actions on it.
 | 
			
		||||
      //   e.dataContext will refer to the ex-entry editor and commands will be executed on it,
 | 
			
		||||
      //   thus it should not be used. For example, `:action EditorSelectWord` will not work with this context
 | 
			
		||||
      val mainEditorContext = exEntryPanel.entry.context.vim
 | 
			
		||||
      keyHandler.handleKey(editor.vim, keyStroke, mainEditorContext, keyHandler.keyHandlerState)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -74,8 +74,7 @@ internal class Executor : VimScriptExecutorBase() {
 | 
			
		||||
            VimPlugin.indicateError()
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e: Exception) {
 | 
			
		||||
          logger.warn("Caught: ${e.message}")
 | 
			
		||||
          logger.warn(e.stackTrace.toString())
 | 
			
		||||
          logger.warn(e)
 | 
			
		||||
          if (injector.application.isUnitTest()) {
 | 
			
		||||
            throw e
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -113,9 +113,9 @@ internal data class GlobalCommand(val ranges: Ranges, val argument: String, val
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (globalBusy) {
 | 
			
		||||
        val match = regex.findInLine(editor, editor.currentCaret().getLine().line)
 | 
			
		||||
        val match = regex.findInLine(editor, editor.currentCaret().getLine())
 | 
			
		||||
        if (match is VimMatchResult.Success == !invert) {
 | 
			
		||||
          globalExecuteOne(editor, context, editor.getLineStartOffset(editor.currentCaret().getLine().line), cmd.toString())
 | 
			
		||||
          globalExecuteOne(editor, context, editor.getLineStartOffset(editor.currentCaret().getLine()), cmd.toString())
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        val line1 = range.startLine
 | 
			
		||||
@@ -164,8 +164,8 @@ internal data class GlobalCommand(val ranges: Ranges, val argument: String, val
 | 
			
		||||
      val searchcol = 0
 | 
			
		||||
      if (globalBusy) {
 | 
			
		||||
        val offset = editor.currentCaret().offset
 | 
			
		||||
        val lineStartOffset = editor.getLineStartForOffset(offset.point)
 | 
			
		||||
        match = sp.vim_regexec_multi(regmatch, editor, lcount, editor.currentCaret().getLine().line, searchcol)
 | 
			
		||||
        val lineStartOffset = editor.getLineStartForOffset(offset)
 | 
			
		||||
        match = sp.vim_regexec_multi(regmatch, editor, lcount, editor.currentCaret().getLine(), searchcol)
 | 
			
		||||
        if ((!invert && match > 0) || (invert && match <= 0)) {
 | 
			
		||||
          globalExecuteOne(editor, context, lineStartOffset, cmd.toString())
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -136,11 +136,7 @@
 | 
			
		||||
 | 
			
		||||
  <!--  IdeaVim extensions-->
 | 
			
		||||
  <extensions defaultExtensionNs="com.intellij">
 | 
			
		||||
    <projectService serviceImplementation="com.maddyhome.idea.vim.extension.nerdtree.NerdTree$NerdDispatcher"/>
 | 
			
		||||
    <postStartupActivity implementation="com.maddyhome.idea.vim.extension.nerdtree.NerdTree$NerdStartupActivity"/>
 | 
			
		||||
    <applicationService serviceImplementation="com.maddyhome.idea.vim.extension.nerdtree.NerdTree$NerdDispatcher"/>
 | 
			
		||||
    <applicationInitializedListener implementation="com.maddyhome.idea.vim.extension.nerdtree.NerdTreeApplicationListener"/>
 | 
			
		||||
  </extensions>
 | 
			
		||||
  <projectListeners>
 | 
			
		||||
    <listener class="com.maddyhome.idea.vim.extension.nerdtree.NerdTree$ProjectViewListener"
 | 
			
		||||
              topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"/>
 | 
			
		||||
  </projectListeners>
 | 
			
		||||
</idea-plugin>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,4 @@
 | 
			
		||||
<!--
 | 
			
		||||
  ~ 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.
 | 
			
		||||
  -->
 | 
			
		||||
 | 
			
		||||
<idea-plugin url="https://plugins.jetbrains.com/plugin/164" xmlns:xi="http://www.w3.org/2001/XInclude">
 | 
			
		||||
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
 | 
			
		||||
  <name>IdeaVim</name>
 | 
			
		||||
  <id>IdeaVIM</id>
 | 
			
		||||
  <description><![CDATA[
 | 
			
		||||
@@ -21,7 +13,7 @@
 | 
			
		||||
        <li><a href="https://youtrack.jetbrains.com/issues/VIM">Issue tracker</a>: feature requests and bug reports</li>
 | 
			
		||||
      </ul>
 | 
			
		||||
    ]]></description>
 | 
			
		||||
  <version>SNAPSHOT</version>
 | 
			
		||||
  <version>chylex</version>
 | 
			
		||||
  <vendor>JetBrains</vendor>
 | 
			
		||||
 | 
			
		||||
  <!-- Please search for "[VERSION UPDATE]" in project in case you update the since-build version -->
 | 
			
		||||
@@ -159,5 +151,6 @@
 | 
			
		||||
    </group>
 | 
			
		||||
 | 
			
		||||
    <action id="VimFindActionIdAction" class="com.maddyhome.idea.vim.listener.FindActionIdAction"/>
 | 
			
		||||
    <action id="VimJumpToSource" class="com.intellij.diff.actions.impl.OpenInEditorAction" />
 | 
			
		||||
  </actions>
 | 
			
		||||
</idea-plugin>
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.SelectionType
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
import org.jetbrains.plugins.ideavim.VimBehaviorDiffers
 | 
			
		||||
import org.jetbrains.plugins.ideavim.VimTestCase
 | 
			
		||||
import org.junit.jupiter.api.Test
 | 
			
		||||
 | 
			
		||||
@@ -381,10 +382,14 @@ class MotionActionTest : VimTestCase() {
 | 
			
		||||
 | 
			
		||||
  // VIM-1287 |d| |v_i{|
 | 
			
		||||
  @Test
 | 
			
		||||
  @VimBehaviorDiffers(
 | 
			
		||||
    originalVimAfter = "{\"{foo, ${c}bar\", baz}}",
 | 
			
		||||
    description = "We have PSI and can resolve this case correctly. I'm not sure if it should be fixed"
 | 
			
		||||
  )
 | 
			
		||||
  fun testBadlyNestedBlockInsideString() {
 | 
			
		||||
    val before = "{\"{foo, ${c}bar\", baz}}"
 | 
			
		||||
    val keys = listOf("di{")
 | 
			
		||||
    val after = "{\"{foo, ${c}bar\", baz}}"
 | 
			
		||||
    val after = "{}}"
 | 
			
		||||
    doTest(keys, before, after, Mode.NORMAL())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -406,6 +411,14 @@ class MotionActionTest : VimTestCase() {
 | 
			
		||||
    doTest(keys, before, after, Mode.INSERT)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  fun testDeletingInnerBlockWhenItIsPresentInString() {
 | 
			
		||||
    val before = "let variable = ('abc' .. \"br${c}aces ( with content )\")"
 | 
			
		||||
    val keys = listOf("di(")
 | 
			
		||||
    val after = "let variable = ()"
 | 
			
		||||
    doTest(keys, before, after, Mode.NORMAL())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // VIM-1008 |c| |v_i{|
 | 
			
		||||
  @Test
 | 
			
		||||
  fun testDeleteInsideSingleQuotesSurroundedBlock() {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,10 +42,10 @@ class YankMotionActionTest : VimTestCase() {
 | 
			
		||||
    """.trimIndent()
 | 
			
		||||
    configureByText(file)
 | 
			
		||||
 | 
			
		||||
    val initialOffset = fixture.editor.caretModel.offset
 | 
			
		||||
    val initialOffset = fixture.editor.caretModel
 | 
			
		||||
    typeText("yy")
 | 
			
		||||
 | 
			
		||||
    kotlin.test.assertEquals(initialOffset, fixture.editor.caretModel.offset)
 | 
			
		||||
    kotlin.test.assertEquals(initialOffset, fixture.editor.caretModel)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Suppress("DANGEROUS_CHARACTERS")
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ package org.jetbrains.plugins.ideavim.action.motion.gn
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.common.Direction
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
@@ -85,7 +86,7 @@ class GnNextTextObjectTest : VimTestCase() {
 | 
			
		||||
 | 
			
		||||
  private fun doTestWithSearch(keys: List<KeyStroke>, before: String, after: String) {
 | 
			
		||||
    configureByText(before)
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS)
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
 | 
			
		||||
    typeText(keys)
 | 
			
		||||
    assertState(after)
 | 
			
		||||
    assertState(Mode.NORMAL())
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ package org.jetbrains.plugins.ideavim.action.motion.gn
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.common.Direction
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
@@ -63,7 +64,7 @@ class GnPreviousTextObjectTest : VimTestCase() {
 | 
			
		||||
 | 
			
		||||
  private fun doTestWithSearch(keys: List<KeyStroke>, before: String, after: String) {
 | 
			
		||||
    configureByText(before)
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS)
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
 | 
			
		||||
    typeText(keys)
 | 
			
		||||
    assertState(after)
 | 
			
		||||
    assertState(Mode.NORMAL())
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.action.motion.search.SearchWholeWordForwardAction
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.common.Direction
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.SelectionType
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
@@ -57,7 +58,7 @@ class VisualSelectNextSearchTest : VimTestCase() {
 | 
			
		||||
  @Test
 | 
			
		||||
  fun testWithoutSpaces() {
 | 
			
		||||
    configureByText("test<caret>test")
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS)
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
 | 
			
		||||
    typeText(injector.parser.parseKeys("gn"))
 | 
			
		||||
    assertOffset(7)
 | 
			
		||||
    assertSelection("test")
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.action.motion.search.SearchWholeWordForwardAction
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.common.Direction
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.SelectionType
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
@@ -54,7 +55,7 @@ class VisualSelectPreviousSearchTest : VimTestCase() {
 | 
			
		||||
  @Test
 | 
			
		||||
  fun testWithoutSpaces() {
 | 
			
		||||
    configureByText("tes<caret>ttest")
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor, "test", "", Direction.FORWARDS)
 | 
			
		||||
    VimPlugin.getSearch().setLastSearchState(fixture.editor.vim, "test", "", Direction.FORWARDS)
 | 
			
		||||
    typeText(injector.parser.parseKeys("gN"))
 | 
			
		||||
    assertOffset(0)
 | 
			
		||||
    assertSelection("test")
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ package org.jetbrains.plugins.ideavim.action.motion.search
 | 
			
		||||
 | 
			
		||||
import com.maddyhome.idea.vim.VimPlugin
 | 
			
		||||
import com.maddyhome.idea.vim.common.Direction
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
import org.jetbrains.plugins.ideavim.VimTestCase
 | 
			
		||||
@@ -167,7 +168,7 @@ class SearchAgainPreviousActionTest : VimTestCase() {
 | 
			
		||||
 | 
			
		||||
  private fun doTestWithSearch(keys: String, before: String, after: String) {
 | 
			
		||||
    doTest(keys, before, after) {
 | 
			
		||||
      VimPlugin.getSearch().setLastSearchState(it, "all", "", Direction.FORWARDS)
 | 
			
		||||
      VimPlugin.getSearch().setLastSearchState(it.vim, "all", "", Direction.FORWARDS)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ class SelectKeyHandlerTest : VimTestCase() {
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +67,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -92,7 +92,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -117,7 +117,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -143,7 +143,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -169,7 +169,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -195,7 +195,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -221,7 +221,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -247,7 +247,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -283,7 +283,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -309,7 +309,7 @@ Mode.INSERT,
 | 
			
		||||
                where it was settled on some sodden sand
 | 
			
		||||
                hard by the torrent of a mountain pass.
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
Mode.INSERT,
 | 
			
		||||
      Mode.INSERT,
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -98,15 +98,6 @@ class MotionUnmatchedBraceOpenActionTest : VimTestCase() {
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @VimBehaviorDiffers(
 | 
			
		||||
    originalVimAfter = """
 | 
			
		||||
      class Xxx $c{
 | 
			
		||||
        int main() {
 | 
			
		||||
          
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
  """,
 | 
			
		||||
  )
 | 
			
		||||
  @Test
 | 
			
		||||
  fun `test go to next next bracket with great count`() {
 | 
			
		||||
    doTest(
 | 
			
		||||
@@ -119,9 +110,9 @@ class MotionUnmatchedBraceOpenActionTest : VimTestCase() {
 | 
			
		||||
      }
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
      """
 | 
			
		||||
      class Xxx {
 | 
			
		||||
      class Xxx $c{
 | 
			
		||||
        int main() {
 | 
			
		||||
          $c
 | 
			
		||||
          
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      """.trimIndent(),
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,12 @@
 | 
			
		||||
 | 
			
		||||
package org.jetbrains.plugins.ideavim.action.motion.updown
 | 
			
		||||
 | 
			
		||||
import com.intellij.idea.TestFor
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
import org.jetbrains.plugins.ideavim.VimTestCase
 | 
			
		||||
import org.junit.jupiter.api.Disabled
 | 
			
		||||
import org.junit.jupiter.api.Test
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -143,6 +145,7 @@ class MotionPercentOrMatchActionTest : VimTestCase() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @Disabled("It will work after implementing all of the methods in VimPsiService")
 | 
			
		||||
  fun `test motion outside text`() {
 | 
			
		||||
    doTest(
 | 
			
		||||
      "%",
 | 
			
		||||
@@ -207,41 +210,45 @@ class MotionPercentOrMatchActionTest : VimTestCase() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
 | 
			
		||||
  fun `test motion in text with escape (outer forward)`() {
 | 
			
		||||
    doTest(
 | 
			
		||||
      "%",
 | 
			
		||||
      """ debugPrint$c(\(var)) """,
 | 
			
		||||
      """ debugPrint(\(var)$c) """,
 | 
			
		||||
      """ debugPrint(\(var$c)) """,
 | 
			
		||||
      Mode.NORMAL(),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
 | 
			
		||||
  fun `test motion in text with escape (outer backward)`() {
 | 
			
		||||
    doTest(
 | 
			
		||||
      "%",
 | 
			
		||||
      """ debugPrint(\(var)$c) """,
 | 
			
		||||
      """ debugPrint$c(\(var)) """,
 | 
			
		||||
      """ debugPrint(\(var)$c) """,
 | 
			
		||||
      Mode.NORMAL(),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
 | 
			
		||||
  fun `test motion in text with escape (inner forward)`() {
 | 
			
		||||
    doTest(
 | 
			
		||||
      "%",
 | 
			
		||||
      """ debugPrint(\$c(var)) """,
 | 
			
		||||
      """ debugPrint(\(var$c)) """,
 | 
			
		||||
      """ debugPrint(\$c(var)) """,
 | 
			
		||||
      Mode.NORMAL(),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.BUG_IN_NEOVIM)
 | 
			
		||||
  fun `test motion in text with escape (inner backward)`() {
 | 
			
		||||
    doTest(
 | 
			
		||||
      "%",
 | 
			
		||||
      """ debugPrint(\$c(var)) """,
 | 
			
		||||
      """ debugPrint(\(var$c)) """,
 | 
			
		||||
      """ debugPrint(\$c(var)) """,
 | 
			
		||||
      Mode.NORMAL(),
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
@@ -332,4 +339,28 @@ class MotionPercentOrMatchActionTest : VimTestCase() {
 | 
			
		||||
    )
 | 
			
		||||
    assertOffset(10)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestFor(issues = ["VIM-3294"])
 | 
			
		||||
  fun `test matching with braces inside of string`() {
 | 
			
		||||
    configureByText("""
 | 
			
		||||
$c("("")")
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("""
 | 
			
		||||
("("")"$c)
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestFor(issues = ["VIM-3294"])
 | 
			
		||||
  fun `test matching with braces inside of string 2`() {
 | 
			
		||||
    configureByText("""
 | 
			
		||||
("("")"$c)
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("""
 | 
			
		||||
$c("("")")
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,6 @@ package org.jetbrains.plugins.ideavim.common.editor
 | 
			
		||||
 | 
			
		||||
import com.intellij.openapi.application.runWriteAction
 | 
			
		||||
import com.intellij.openapi.command.WriteCommandAction
 | 
			
		||||
import com.maddyhome.idea.vim.common.offset
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
@@ -25,7 +24,7 @@ class VimEditorTest : VimTestCase() {
 | 
			
		||||
    val vimEditor = IjVimEditor(fixture.editor)
 | 
			
		||||
    WriteCommandAction.runWriteCommandAction(fixture.project) {
 | 
			
		||||
      runWriteAction {
 | 
			
		||||
        vimEditor.deleteRange(0.offset, 5.offset)
 | 
			
		||||
        vimEditor.deleteRange(0, 5)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    assertState("567890")
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
 | 
			
		||||
package org.jetbrains.plugins.ideavim.extension.entiretextobj
 | 
			
		||||
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
@@ -57,6 +58,14 @@ class VimTextObjEntireExtensionTest : VimTestCase() {
 | 
			
		||||
  @Test
 | 
			
		||||
  fun testYankEntireBuffer() {
 | 
			
		||||
    doTest("yae", poem, "<caret>$poemNoCaret")
 | 
			
		||||
    assertRegisterString(injector.registerGroup.defaultRegister, poemNoCaret)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // |y| |ae|
 | 
			
		||||
  @Test
 | 
			
		||||
  fun testYankEntireBufferWithCustomRegister() {
 | 
			
		||||
    doTest("\"kyae", poem, "<caret>$poemNoCaret")
 | 
			
		||||
    assertRegisterString('k', poemNoCaret)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // |gU| |ie|
 | 
			
		||||
 
 | 
			
		||||
@@ -203,6 +203,36 @@ class ReplaceWithRegisterTest : VimTestCase() {
 | 
			
		||||
    assertEquals("one", VimPlugin.getRegister().lastRegister?.text)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  fun `with specific register`() {
 | 
			
		||||
    val text = "one ${c}two three four"
 | 
			
		||||
 | 
			
		||||
    configureByText(text)
 | 
			
		||||
    VimPlugin.getRegister().setKeys('k', injector.parser.parseKeys("one"))
 | 
			
		||||
    typeText(injector.parser.parseKeys("\"kgriw"))
 | 
			
		||||
    assertState("one on${c}e three four")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  fun `with specific register in visual mode`() {
 | 
			
		||||
    val text = "one ${c}two three four"
 | 
			
		||||
 | 
			
		||||
    configureByText(text)
 | 
			
		||||
    VimPlugin.getRegister().setKeys('k', injector.parser.parseKeys("one"))
 | 
			
		||||
    typeText(injector.parser.parseKeys("ve\"kgr"))
 | 
			
		||||
    assertState("one on${c}e three four")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  fun `with specific register in line mode`() {
 | 
			
		||||
    val text = "one ${c}two three four"
 | 
			
		||||
 | 
			
		||||
    configureByText(text)
 | 
			
		||||
    VimPlugin.getRegister().setKeys('k', injector.parser.parseKeys("one"))
 | 
			
		||||
    typeText(injector.parser.parseKeys("\"kgrr"))
 | 
			
		||||
    assertState("${c}one\n")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // --------------------------------------- grr --------------------------
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
 
 | 
			
		||||
@@ -322,7 +322,7 @@ class CaretVisualAttributesHelperTest : VimTestCase() {
 | 
			
		||||
    )
 | 
			
		||||
    injector.actionExecutor.executeAction(
 | 
			
		||||
      "EditorCloneCaretBelow",
 | 
			
		||||
      injector.executionContextManager.onEditor(fixture.editor.vim),
 | 
			
		||||
      injector.executionContextManager.getEditorExecutionContext(fixture.editor.vim),
 | 
			
		||||
    )
 | 
			
		||||
    kotlin.test.assertEquals(2, fixture.editor.caretModel.caretCount)
 | 
			
		||||
    assertCaretVisualAttributes(CaretVisualAttributes.Shape.BLOCK, 0f)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ package org.jetbrains.plugins.ideavim.helper
 | 
			
		||||
 | 
			
		||||
import com.maddyhome.idea.vim.api.injector
 | 
			
		||||
import com.maddyhome.idea.vim.common.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.group.findBlockRange
 | 
			
		||||
import com.maddyhome.idea.vim.helper.checkInString
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.Mode
 | 
			
		||||
@@ -246,8 +247,7 @@ class SearchHelperTest : VimTestCase() {
 | 
			
		||||
  fun findBlockRange(testCase: FindBlockRangeTestCase) {
 | 
			
		||||
    val (_, text, type, count, isOuter, expected) = (testCase)
 | 
			
		||||
    configureByText(text)
 | 
			
		||||
    val actual =
 | 
			
		||||
      injector.searchHelper.findBlockRange(fixture.editor.vim, fixture.editor.vim.currentCaret(), type, count, isOuter)
 | 
			
		||||
    val actual = findBlockRange(fixture.editor.vim, fixture.editor.vim.currentCaret(), type, count, isOuter)
 | 
			
		||||
    kotlin.test.assertEquals(expected, actual)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -294,8 +294,8 @@ class SearchHelperTest : VimTestCase() {
 | 
			
		||||
        FindBlockRangeTestCase("outer match exclude start paren in string when caret at start of quote", "(${c}\"(aa\")", '(', 1, isOuter = true, expected = TextRange(0, 7)),
 | 
			
		||||
        FindBlockRangeTestCase("inner match exclude start paren in string when caret at end of quote", "(\"(aa${c}\")", '(', 1, isOuter = false, expected = TextRange(1, 6)),
 | 
			
		||||
        FindBlockRangeTestCase("outer match exclude start paren in string when caret at end of quote", "(\"(aa${c}\")", '(', 1, isOuter = true, expected = TextRange(0, 7)),
 | 
			
		||||
        FindBlockRangeTestCase("inner match not exclude start paren in string when caret in between quote", "(\"(a${c}a\")", '(', 1, isOuter = false, expected = null),
 | 
			
		||||
        FindBlockRangeTestCase("outer match not exclude start paren in string when caret in between quote", "(\"(a${c}a\")", '(', 1, isOuter = true, expected = null),
 | 
			
		||||
        FindBlockRangeTestCase("inner match not exclude start paren in string when caret in between quote", "(\"(a${c}a\")", '(', 1, isOuter = false, expected = TextRange(1, 6)), // Vim behavior differs, but we have some PSI magic and can resolve such cases
 | 
			
		||||
        FindBlockRangeTestCase("outer match not exclude start paren in string when caret in between quote", "(\"(a${c}a\")", '(', 1, isOuter = true, expected = TextRange(0, 7)), // Vim behavior differs, but we have some PSI magic and can resolve such cases
 | 
			
		||||
        FindBlockRangeTestCase("inner match exclude end paren in string when caret at start of quote", "(${c}\"aa)\")", '(', 1, isOuter = false, expected = TextRange(1, 6)),
 | 
			
		||||
        FindBlockRangeTestCase("outer match exclude end paren in string when caret at start of quote", "(${c}\"aa)\")", '(', 1, isOuter = true, expected = TextRange(0, 7)),
 | 
			
		||||
        FindBlockRangeTestCase("inner match exclude end paren in string when caret at end of quote", "(\"aa)${c}\")", '(', 1, isOuter = false, expected = TextRange(1, 6)),
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,7 @@ class VimRegexEngineTest : VimTestCase() {
 | 
			
		||||
    configureByText("Lor${c}em ${c}Ipsum")
 | 
			
		||||
    val editor = fixture.editor.vim
 | 
			
		||||
    val mark = VimMark.create('m', 0, 0, editor.getPath(), editor.extractProtocol())!!
 | 
			
		||||
    val secondCaret = editor.carets().maxByOrNull { it.offset.point }!!
 | 
			
		||||
    val secondCaret = editor.carets().maxByOrNull { it.offset }!!
 | 
			
		||||
    secondCaret.markStorage.setMark(mark)
 | 
			
		||||
 | 
			
		||||
    val result = findAll("\\%>'m\\%#.")
 | 
			
		||||
 
 | 
			
		||||
@@ -245,6 +245,9 @@ enum class SkipNeovimReason {
 | 
			
		||||
 | 
			
		||||
  GUARDED_BLOCKS,
 | 
			
		||||
  CTRL_CODES,
 | 
			
		||||
 | 
			
		||||
  BUG_IN_NEOVIM,
 | 
			
		||||
  PSI,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun LogicalPosition.toVimCoords(): VimCoords {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,9 @@ import com.intellij.openapi.actionSystem.ActionManager
 | 
			
		||||
import com.intellij.openapi.actionSystem.ActionPlaces
 | 
			
		||||
import com.intellij.openapi.actionSystem.AnActionEvent
 | 
			
		||||
import com.intellij.openapi.actionSystem.DataContext
 | 
			
		||||
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
 | 
			
		||||
import com.intellij.openapi.actionSystem.ex.ActionUtil
 | 
			
		||||
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
 | 
			
		||||
import com.intellij.openapi.application.PathManager
 | 
			
		||||
import com.intellij.openapi.application.WriteAction
 | 
			
		||||
import com.intellij.openapi.editor.CaretVisualAttributes
 | 
			
		||||
@@ -25,6 +27,7 @@ import com.intellij.openapi.editor.LogicalPosition
 | 
			
		||||
import com.intellij.openapi.editor.VisualPosition
 | 
			
		||||
import com.intellij.openapi.editor.colors.EditorColors
 | 
			
		||||
import com.intellij.openapi.editor.ex.EditorEx
 | 
			
		||||
import com.intellij.openapi.editor.ex.util.EditorUtil
 | 
			
		||||
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
 | 
			
		||||
import com.intellij.openapi.fileTypes.FileType
 | 
			
		||||
import com.intellij.openapi.fileTypes.PlainTextFileType
 | 
			
		||||
@@ -64,7 +67,6 @@ import com.maddyhome.idea.vim.key.ToKeysMappingInfo
 | 
			
		||||
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.IjVimEditor
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.globalIjOptions
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ij
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.ijOptions
 | 
			
		||||
import com.maddyhome.idea.vim.newapi.vim
 | 
			
		||||
import com.maddyhome.idea.vim.options.OptionAccessScope
 | 
			
		||||
@@ -452,6 +454,11 @@ abstract class VimTestCase {
 | 
			
		||||
    assertEquals(expected, actual, "Wrong register contents")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected fun assertRegisterString(char: Char, expected: String?) {
 | 
			
		||||
    val actual = injector.registerGroup.getRegister(char)?.keys?.let(injector.parser::toPrintableString)
 | 
			
		||||
    assertEquals(expected, actual, "Wrong register contents")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected fun assertState(modeAfter: Mode) {
 | 
			
		||||
    assertMode(modeAfter)
 | 
			
		||||
    assertCaretsVisualAttributes()
 | 
			
		||||
@@ -755,9 +762,13 @@ abstract class VimTestCase {
 | 
			
		||||
          val event =
 | 
			
		||||
            KeyEvent(editor.component, KeyEvent.KEY_PRESSED, Date().time, key.modifiers, key.keyCode, key.keyChar)
 | 
			
		||||
 | 
			
		||||
          val context = SimpleDataContext.builder()
 | 
			
		||||
            .setParent(EditorUtil.getEditorDataContext(editor))
 | 
			
		||||
            .add(PlatformCoreDataKeys.CONTEXT_COMPONENT, editor.component)
 | 
			
		||||
            .build()
 | 
			
		||||
          val e = AnActionEvent(
 | 
			
		||||
            event,
 | 
			
		||||
            injector.executionContextManager.onEditor(editor.vim).ij,
 | 
			
		||||
            context,
 | 
			
		||||
            ActionPlaces.KEYBOARD_SHORTCUT,
 | 
			
		||||
            VimShortcutKeyAction.instance.templatePresentation.clone(),
 | 
			
		||||
            ActionManager.getInstance(),
 | 
			
		||||
@@ -810,7 +821,7 @@ abstract class VimTestCase {
 | 
			
		||||
 | 
			
		||||
    fun typeText(keys: List<KeyStroke?>, editor: Editor, project: Project?) {
 | 
			
		||||
      val keyHandler = KeyHandler.getInstance()
 | 
			
		||||
      val dataContext = injector.executionContextManager.onEditor(editor.vim)
 | 
			
		||||
      val dataContext = injector.executionContextManager.getEditorExecutionContext(editor.vim)
 | 
			
		||||
      TestInputModel.getInstance(editor).setKeyStrokes(keys.filterNotNull())
 | 
			
		||||
      runWriteCommand(
 | 
			
		||||
        project,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
 | 
			
		||||
package org.jetbrains.plugins.ideavim.action.motion.updown
 | 
			
		||||
 | 
			
		||||
import com.intellij.idea.TestFor
 | 
			
		||||
import org.jetbrains.plugins.ideavim.SkipNeovimReason
 | 
			
		||||
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
 | 
			
		||||
import org.jetbrains.plugins.ideavim.VimJavaTestCase
 | 
			
		||||
@@ -66,4 +67,98 @@ class MotionPercentOrMatchActionJavaTest : VimJavaTestCase() {
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("/* foo $c */")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestFor(issues = ["VIM-1399"])
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.PSI)
 | 
			
		||||
  fun `test percent ignores brace inside comment`() {
 | 
			
		||||
    configureByJavaText("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      TokenStream result = new EmptyTokenFilter(in); /* $c{
 | 
			
		||||
              * some text
 | 
			
		||||
              */
 | 
			
		||||
      result = new LowerCaseFilter(result);
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      TokenStream result = new EmptyTokenFilter(in); /* $c{
 | 
			
		||||
              * some text
 | 
			
		||||
              */
 | 
			
		||||
      result = new LowerCaseFilter(result);
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestFor(issues = ["VIM-1399"])
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.PSI)
 | 
			
		||||
  fun `test percent doesnt match brace inside comment`() {
 | 
			
		||||
    configureByJavaText("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) $c{
 | 
			
		||||
      TokenStream result = new EmptyTokenFilter(in); /* {
 | 
			
		||||
              * some text
 | 
			
		||||
              */
 | 
			
		||||
      result = new LowerCaseFilter(result);
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      TokenStream result = new EmptyTokenFilter(in); /* {
 | 
			
		||||
              * some text
 | 
			
		||||
              */
 | 
			
		||||
      result = new LowerCaseFilter(result);
 | 
			
		||||
      return result;
 | 
			
		||||
    $c}
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.PSI)
 | 
			
		||||
  fun `test matching works with a sequence of single-line comments`() {
 | 
			
		||||
    configureByJavaText("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      // $c{
 | 
			
		||||
      // result = new LowerCaseFilter(result);
 | 
			
		||||
      // }
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      // {
 | 
			
		||||
      // result = new LowerCaseFilter(result);
 | 
			
		||||
      // $c}
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Test
 | 
			
		||||
  @TestWithoutNeovim(SkipNeovimReason.PSI)
 | 
			
		||||
  fun `test matching doesn't work if a sequence of single-line comments is broken`() {
 | 
			
		||||
    configureByJavaText("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      // $c{
 | 
			
		||||
        result = new LowerCaseFilter(result);
 | 
			
		||||
      // }
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
    typeText("%")
 | 
			
		||||
    assertState("""
 | 
			
		||||
      protected TokenStream normalize(String fieldName, TokenStream in) {
 | 
			
		||||
      // $c{
 | 
			
		||||
        result = new LowerCaseFilter(result);
 | 
			
		||||
      // }
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
    """.trimIndent())
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -10,7 +10,9 @@ package ui
 | 
			
		||||
 | 
			
		||||
import com.automation.remarks.junit5.Video
 | 
			
		||||
import com.intellij.remoterobot.RemoteRobot
 | 
			
		||||
import com.intellij.remoterobot.fixtures.ComponentFixture
 | 
			
		||||
import com.intellij.remoterobot.fixtures.ContainerFixture
 | 
			
		||||
import com.intellij.remoterobot.search.locators.byXpath
 | 
			
		||||
import com.intellij.remoterobot.steps.CommonSteps
 | 
			
		||||
import com.intellij.remoterobot.stepsProcessing.step
 | 
			
		||||
import com.intellij.remoterobot.utils.keyboard
 | 
			
		||||
@@ -120,6 +122,9 @@ class UiTests {
 | 
			
		||||
      Thread.sleep(5000)
 | 
			
		||||
      wrapWithIf(javaEditor)
 | 
			
		||||
      testTrackActionId(javaEditor)
 | 
			
		||||
      testActionGenerate(javaEditor)
 | 
			
		||||
      testActionNewElementSamePlace(javaEditor)
 | 
			
		||||
      testActionCopy(javaEditor)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -238,6 +243,66 @@ class UiTests {
 | 
			
		||||
    vimExit()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun IdeaFrame.testActionGenerate(editor: Editor) {
 | 
			
		||||
    val label = findAll<ComponentFixture>(byXpath("//div[@class='EngravedLabel']"))
 | 
			
		||||
    assertTrue(label.isEmpty())
 | 
			
		||||
 | 
			
		||||
    keyboard {
 | 
			
		||||
      enterText(":action Generate")
 | 
			
		||||
      enter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    waitFor {
 | 
			
		||||
      val generateDialog = findAll<ComponentFixture>(byXpath("//div[@class='EngravedLabel']"))
 | 
			
		||||
      if (generateDialog.size == 1) {
 | 
			
		||||
        return@waitFor generateDialog.single().hasText("Generate")
 | 
			
		||||
      }
 | 
			
		||||
      return@waitFor false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    keyboard { escape() }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun IdeaFrame.testActionNewElementSamePlace(editor: Editor) {
 | 
			
		||||
    val label = findAll<ComponentFixture>(byXpath("//div[@class='EngravedLabel']"))
 | 
			
		||||
    assertTrue(label.isEmpty())
 | 
			
		||||
 | 
			
		||||
    keyboard {
 | 
			
		||||
      enterText(":action NewElementSamePlace")
 | 
			
		||||
      enter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    waitFor {
 | 
			
		||||
      val generateDialog = findAll<ComponentFixture>(byXpath("//div[@class='EngravedLabel']"))
 | 
			
		||||
      if (generateDialog.size == 1) {
 | 
			
		||||
        return@waitFor generateDialog.single().hasText("New in This Directory")
 | 
			
		||||
      }
 | 
			
		||||
      return@waitFor false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    keyboard { escape() }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun IdeaFrame.testActionCopy(editor: Editor) {
 | 
			
		||||
    val label = findAll<ComponentFixture>(byXpath("//div[@class='EngravedLabel']"))
 | 
			
		||||
    assertTrue(label.isEmpty())
 | 
			
		||||
 | 
			
		||||
    keyboard {
 | 
			
		||||
      enterText(":action CopyReferencePopupGroup")
 | 
			
		||||
      enter()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    waitFor {
 | 
			
		||||
      val generateDialog = findAll<ComponentFixture>(byXpath("//div[@class='EngravedLabel']"))
 | 
			
		||||
      if (generateDialog.size == 1) {
 | 
			
		||||
        return@waitFor generateDialog.single().hasText("Copy Path/Reference…")
 | 
			
		||||
      }
 | 
			
		||||
      return@waitFor false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    keyboard { escape() }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private fun IdeaFrame.createFile(fileName: String, remoteRobot: RemoteRobot) {
 | 
			
		||||
    step("Create $fileName file") {
 | 
			
		||||
      with(projectViewTree) {
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,11 @@ class PyCharmTest {
 | 
			
		||||
        findAllText("Python Packages").isNotEmpty() &&
 | 
			
		||||
        isSmartMode()
 | 
			
		||||
    }
 | 
			
		||||
    findText("Python Console").click()
 | 
			
		||||
 | 
			
		||||
    // Open tool window by id.
 | 
			
		||||
    // id taken from PythonConsoleToolWindowFactory.ID but it's not resolved in robot by some reason
 | 
			
		||||
    // the last 'x' is just to return some serializable value
 | 
			
		||||
    callJs<String>("com.intellij.openapi.wm.ToolWindowManager.getInstance(component.project).getToolWindow('Python Console').activate(null, true); 'x'", true)
 | 
			
		||||
 | 
			
		||||
    Thread.sleep(10_000)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ dependencies {
 | 
			
		||||
    compileOnly(project(":annotation-processors"))
 | 
			
		||||
    compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion")
 | 
			
		||||
 | 
			
		||||
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
 | 
			
		||||
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tasks {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command
 | 
			
		||||
import com.maddyhome.idea.vim.command.OperatorArguments
 | 
			
		||||
import com.maddyhome.idea.vim.handler.VimActionHandler
 | 
			
		||||
 | 
			
		||||
@CommandOrMotion(keys = ["<C-R>"], modes = [Mode.NORMAL])
 | 
			
		||||
@CommandOrMotion(keys = ["U", "<C-R>"], modes = [Mode.NORMAL, Mode.VISUAL])
 | 
			
		||||
public class RedoAction : VimActionHandler.SingleExecution() {
 | 
			
		||||
  override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command
 | 
			
		||||
import com.maddyhome.idea.vim.command.OperatorArguments
 | 
			
		||||
import com.maddyhome.idea.vim.handler.VimActionHandler
 | 
			
		||||
 | 
			
		||||
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL])
 | 
			
		||||
@CommandOrMotion(keys = ["u", "<Undo>"], modes = [Mode.NORMAL, Mode.VISUAL])
 | 
			
		||||
public class UndoAction : VimActionHandler.SingleExecution() {
 | 
			
		||||
  override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@
 | 
			
		||||
package com.maddyhome.idea.vim.action.change.change
 | 
			
		||||
 | 
			
		||||
import com.intellij.vim.annotations.CommandOrMotion
 | 
			
		||||
import com.intellij.vim.annotations.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
@@ -22,7 +21,7 @@ import com.maddyhome.idea.vim.helper.CharacterHelper
 | 
			
		||||
/**
 | 
			
		||||
 * @author vlan
 | 
			
		||||
 */
 | 
			
		||||
@CommandOrMotion(keys = ["u"], modes = [Mode.VISUAL])
 | 
			
		||||
@CommandOrMotion(keys = [], modes = [])
 | 
			
		||||
public class ChangeCaseLowerVisualAction : VisualOperatorActionHandler.ForEachCaret() {
 | 
			
		||||
  override val type: Command.Type = Command.Type.CHANGE
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@
 | 
			
		||||
package com.maddyhome.idea.vim.action.change.change
 | 
			
		||||
 | 
			
		||||
import com.intellij.vim.annotations.CommandOrMotion
 | 
			
		||||
import com.intellij.vim.annotations.Mode
 | 
			
		||||
import com.maddyhome.idea.vim.api.ExecutionContext
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimCaret
 | 
			
		||||
import com.maddyhome.idea.vim.api.VimEditor
 | 
			
		||||
@@ -22,7 +21,7 @@ import com.maddyhome.idea.vim.helper.CharacterHelper
 | 
			
		||||
/**
 | 
			
		||||
 * @author vlan
 | 
			
		||||
 */
 | 
			
		||||
@CommandOrMotion(keys = ["U"], modes = [Mode.VISUAL])
 | 
			
		||||
@CommandOrMotion(keys = [], modes = [])
 | 
			
		||||
public class ChangeCaseUpperVisualAction : VisualOperatorActionHandler.ForEachCaret() {
 | 
			
		||||
  override val type: Command.Type = Command.Type.CHANGE
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ private fun changeCharacter(editor: VimEditor, caret: VimCaret, count: Int, ch:
 | 
			
		||||
  val col = caret.getBufferPosition().column
 | 
			
		||||
  // TODO: Is this correct? Should we really use only current caret? We have a caret as an argument
 | 
			
		||||
  val len = editor.lineLength(editor.currentCaret().getBufferPosition().line)
 | 
			
		||||
  val offset = caret.offset.point
 | 
			
		||||
  val offset = caret.offset
 | 
			
		||||
  if (len - col < count) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,6 @@ public class ChangeLastGlobalSearchReplaceAction : ChangeEditorActionHandler.Sin
 | 
			
		||||
  ): Boolean {
 | 
			
		||||
    val range = LineRange(0, editor.lineCount() - 1)
 | 
			
		||||
    return injector.searchGroup
 | 
			
		||||
      .processSubstituteCommand(editor, editor.primaryCaret(), range, "s", "//~/&", Script(listOf()))
 | 
			
		||||
      .processSubstituteCommand(editor, editor.primaryCaret(), context, range, "s", "//~/&", Script(listOf()))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ public class ChangeLastSearchReplaceAction : ChangeEditorActionHandler.SingleExe
 | 
			
		||||
    for (caret in editor.carets()) {
 | 
			
		||||
      val line = caret.getBufferPosition().line
 | 
			
		||||
      if (!injector.searchGroup
 | 
			
		||||
          .processSubstituteCommand(editor, caret, LineRange(line, line), "s", "//~/", Script(listOf()))
 | 
			
		||||
          .processSubstituteCommand(editor, caret, context, LineRange(line, line), "s", "//~/", Script(listOf()))
 | 
			
		||||
      ) {
 | 
			
		||||
        result = false
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ public class FilterVisualLinesAction : VimActionHandler.SingleExecution() {
 | 
			
		||||
  override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_MOT_LINEWISE)
 | 
			
		||||
 | 
			
		||||
  override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
 | 
			
		||||
    injector.markService.setVisualSelectionMarks(editor)
 | 
			
		||||
    injector.processGroup.startFilterCommand(editor, context, cmd)
 | 
			
		||||
    editor.exitVisualMode()
 | 
			
		||||
    return true
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ private fun insertCharacterAroundCursor(editor: VimEditor, caret: VimCaret, dir:
 | 
			
		||||
  vp = VimVisualPosition(vp.line + dir, vp.column, false)
 | 
			
		||||
  val len = editor.lineLength(editor.visualLineToBufferLine(vp.line))
 | 
			
		||||
  if (vp.column < len) {
 | 
			
		||||
    val offset = editor.visualPositionToOffset(VimVisualPosition(vp.line, vp.column, false)).point
 | 
			
		||||
    val offset = editor.visualPositionToOffset(VimVisualPosition(vp.line, vp.column, false))
 | 
			
		||||
    val charsSequence = editor.text()
 | 
			
		||||
    if (offset < charsSequence.length) {
 | 
			
		||||
      val ch = charsSequence[offset]
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,10 @@ import com.maddyhome.idea.vim.command.Argument
 | 
			
		||||
import com.maddyhome.idea.vim.command.Command
 | 
			
		||||
import com.maddyhome.idea.vim.command.CommandFlags
 | 
			
		||||
import com.maddyhome.idea.vim.command.OperatorArguments
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.SelectionType
 | 
			
		||||
import com.maddyhome.idea.vim.common.Offset
 | 
			
		||||
import com.maddyhome.idea.vim.common.TextRange
 | 
			
		||||
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
 | 
			
		||||
import com.maddyhome.idea.vim.helper.enumSetOf
 | 
			
		||||
import com.maddyhome.idea.vim.state.mode.SelectionType
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
@CommandOrMotion(keys = ["<C-U>"], modes = [Mode.INSERT])
 | 
			
		||||
@@ -57,13 +56,13 @@ private fun insertDeleteInsertedText(
 | 
			
		||||
  var deleteTo = caret.vimInsertStart.startOffset
 | 
			
		||||
  val offset = caret.offset
 | 
			
		||||
  if (offset == deleteTo) {
 | 
			
		||||
    deleteTo = Offset(injector.motion.moveCaretToCurrentLineStartSkipLeading(editor, caret))
 | 
			
		||||
    deleteTo = injector.motion.moveCaretToCurrentLineStartSkipLeading(editor, caret)
 | 
			
		||||
  }
 | 
			
		||||
  if (deleteTo.point != -1) {
 | 
			
		||||
  if (deleteTo != -1) {
 | 
			
		||||
    injector.changeGroup.deleteRange(
 | 
			
		||||
      editor,
 | 
			
		||||
      caret,
 | 
			
		||||
      TextRange(deleteTo.point, offset.point),
 | 
			
		||||
      TextRange(deleteTo, offset),
 | 
			
		||||
      SelectionType.CHARACTER_WISE,
 | 
			
		||||
      false,
 | 
			
		||||
      operatorArguments,
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user