diff --git a/.github/workflows/runUiOctopusTests.yml b/.github/workflows/runUiOctopusTests.yml
index 65314d0fc..65576aef1 100644
--- a/.github/workflows/runUiOctopusTests.yml
+++ b/.github/workflows/runUiOctopusTests.yml
@@ -23,7 +23,7 @@ jobs:
       - name: Run Idea
         run: |
           mkdir -p build/reports
-          gradle runIdeForUiTests -Doctopus.handler=false > build/reports/idea.log &
+          gradle testIdeUi -Doctopus.handler=false > build/reports/idea.log &
       - name: Wait for Idea started
         uses: jtalk/url-health-check-action@v3
         with:
@@ -63,7 +63,7 @@ jobs:
 #          export DISPLAY=:99.0
 #          Xvfb -ac :99 -screen 0 1920x1080x16 &
 #          mkdir -p build/reports
-#          gradle :runIdeForUiTests #> build/reports/idea.log
+#          gradle :testIdeUi #> build/reports/idea.log
 #      - name: Wait for Idea started
 #        uses: jtalk/url-health-check-action@1.5
 #        with:
@@ -78,4 +78,4 @@ jobs:
 #        with:
 #          name: ui-test-fails-report-linux
 #          path: |
-#            ui-test-example/build/reports
\ No newline at end of file
+#            ui-test-example/build/reports
diff --git a/.github/workflows/runUiPyTests.yml b/.github/workflows/runUiPyTests.yml
index e67bd9fd3..2ac837779 100644
--- a/.github/workflows/runUiPyTests.yml
+++ b/.github/workflows/runUiPyTests.yml
@@ -26,7 +26,7 @@ jobs:
       - name: Run Idea
         run: |
           mkdir -p build/reports
-          gradle :runIdeForUiTests -PideaType=PC > build/reports/idea.log &
+          gradle :testIdeUi -PideaType=PC > build/reports/idea.log &
       - name: Wait for Idea started
         uses: jtalk/url-health-check-action@v3
         with:
@@ -49,4 +49,4 @@ jobs:
           path: |
             build/reports
             tests/ui-py-tests/build/reports
-            sandbox-idea-log
\ No newline at end of file
+            sandbox-idea-log
diff --git a/.github/workflows/runUiTests.yml b/.github/workflows/runUiTests.yml
index 26f5f1023..9df972b86 100644
--- a/.github/workflows/runUiTests.yml
+++ b/.github/workflows/runUiTests.yml
@@ -23,7 +23,7 @@ jobs:
       - name: Run Idea
         run: |
           mkdir -p build/reports
-          gradle runIdeForUiTests > build/reports/idea.log &
+          gradle testIdeUi > build/reports/idea.log &
       - name: Wait for Idea started
         uses: jtalk/url-health-check-action@v3
         with:
@@ -63,7 +63,7 @@ jobs:
 #          export DISPLAY=:99.0
 #          Xvfb -ac :99 -screen 0 1920x1080x16 &
 #          mkdir -p build/reports
-#          gradle :runIdeForUiTests #> build/reports/idea.log
+#          gradle :testIdeUi #> build/reports/idea.log
 #      - name: Wait for Idea started
 #        uses: jtalk/url-health-check-action@1.5
 #        with:
@@ -78,4 +78,4 @@ jobs:
 #        with:
 #          name: ui-test-fails-report-linux
 #          path: |
-#            ui-test-example/build/reports
\ No newline at end of file
+#            ui-test-example/build/reports
diff --git a/.gitignore b/.gitignore
index 50ac91409..85c6f93d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 *.swp
 /.gradle/
+/.intellijPlatform/
 
 /.idea/
 !/.idea/scopes
diff --git a/.idea/runConfigurations/IdeaVim_full_verification.xml b/.idea/runConfigurations/IdeaVim_full_verification.xml
index d6b1b41af..9b6ac4c11 100644
--- a/.idea/runConfigurations/IdeaVim_full_verification.xml
+++ b/.idea/runConfigurations/IdeaVim_full_verification.xml
@@ -12,7 +12,7 @@
       <option name="taskNames">
         <list>
           <option value="check" />
-          <option value="runPluginVerifier" />
+          <option value="verifyPlugin" />
         </list>
       </option>
       <option name="vmOptions" value="" />
@@ -20,6 +20,7 @@
     <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
     <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
     <method v="2" />
   </configuration>
 </component>
\ No newline at end of file
diff --git a/.teamcity/_Self/buildTypes/PluginVerifier.kt b/.teamcity/_Self/buildTypes/PluginVerifier.kt
index f25fa97c7..9d607db16 100644
--- a/.teamcity/_Self/buildTypes/PluginVerifier.kt
+++ b/.teamcity/_Self/buildTypes/PluginVerifier.kt
@@ -22,7 +22,7 @@ object PluginVerifier : IdeaVimBuildType({
 
   steps {
     gradle {
-      tasks = "clean runPluginVerifier"
+      tasks = "clean verifyPlugin"
       buildFile = ""
       enableStacktrace = true
     }
diff --git a/build.gradle.kts b/build.gradle.kts
index aa6ac24bb..70024b739 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -32,6 +32,7 @@ import org.eclipse.jgit.api.Git
 import org.eclipse.jgit.lib.RepositoryBuilder
 import org.intellij.markdown.ast.getTextInNode
 import org.jetbrains.changelog.Changelog
+import org.jetbrains.intellij.platform.gradle.TestFrameworkType
 import org.kohsuke.github.GHUser
 import java.net.HttpURLConnection
 import java.net.URL
@@ -67,22 +68,20 @@ plugins {
   kotlin("jvm") version "1.9.22"
   application
   id("java-test-fixtures")
-
-  id("org.jetbrains.intellij") version "1.17.3"
+  id("org.jetbrains.intellij.platform") version "2.0.0-beta7"
   id("org.jetbrains.changelog") version "2.2.0"
-
   id("org.jetbrains.kotlinx.kover") version "0.6.1"
   id("com.dorongold.task-tree") version "4.0.0"
-
   id("com.google.devtools.ksp") version "1.9.22-1.0.17"
 }
 
+val moduleSources by configurations.registering
+
 // Import variables from gradle.properties file
 val javaVersion: String by project
 val kotlinVersion: String by project
 val ideaVersion: String by project
 val ideaType: String by project
-val downloadIdeaSources: String by project
 val instrumentPluginCode: String by project
 val remoteRobotVersion: String by project
 
@@ -94,7 +93,9 @@ val youtrackToken: String by project
 
 repositories {
   mavenCentral()
-  maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
+  intellijPlatform {
+    defaultRepositories()
+  }
 }
 
 dependencies {
@@ -105,9 +106,26 @@ dependencies {
   compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
   compileOnly("org.jetbrains:annotations:24.1.0")
 
-  // --------- Test dependencies ----------
+  intellijPlatform {
+    // Note that it is also possible to use local("...") to compile against a locally installed IDE
+    // E.g. local("/Users/{user}/Applications/IntelliJ IDEA Ultimate.app")
+    // Or something like: intellijIdeaUltimate(ideaVersion)
+    create(ideaType, ideaVersion)
 
-  testImplementation(testFixtures(project(":")))
+    pluginVerifier()
+    zipSigner()
+    instrumentationTools()
+
+    testFramework(TestFrameworkType.Platform)
+    testFramework(TestFrameworkType.JUnit5)
+
+    // AceJump is an optional dependency. We use their SessionManager class to check if it's active
+    plugin("AceJump", "3.8.11")
+  }
+
+  moduleSources(project(":vim-engine", "sourcesJarArtifacts"))
+
+  // --------- Test dependencies ----------
 
   testApi("com.squareup.okhttp3:okhttp:4.12.0")
 
@@ -179,15 +197,28 @@ tasks {
     }
   }
 
+  // Note that this will run the plugin installed in the IDE specified in dependencies. To run in a different IDE, use
+  // a custom task (see below)
   runIde {
     systemProperty("octopus.handler", System.getProperty("octopus.handler") ?: true)
   }
 
-  downloadRobotServerPlugin {
-    version.set(remoteRobotVersion)
-  }
+  // Uncomment to run the plugin in a custom IDE, rather than the IDE specified as a compile target in dependencies
+  // Note that the version must be greater than the plugin's target version, for obvious reasons
+//  val runIdeCustom by registering(CustomRunIdeTask::class) {
+//    type = IntelliJPlatformType.Rider
+//    version = "2024.1.2"
+//  }
 
-  runIdeForUiTests {
+  // Uncomment to run the plugin in a locally installed IDE
+//  val runIdeLocal by registering(CustomRunIdeTask::class) {
+//    localPath = file("/Users/{user}/Applications/WebStorm.app")
+//  }
+
+  // Start the default IDE with both IdeaVim and the robot server plugin installed, ready to run a UI test task. The
+  // robot server plugin is automatically added as a dependency to this task, and Gradle will take care of downloading.
+  // Note that the CustomTestIdeUiTask can be used to run tests against a different IDE
+  testIdeUi {
     systemProperty("robot-server.port", "8082")
     systemProperty("ide.mac.message.dialogs.as.sheets", "false")
     systemProperty("jb.privacy.policy.text", "<!--999.999-->")
@@ -198,28 +229,21 @@ tasks {
   }
 
   // Add plugin open API sources to the plugin ZIP
-  val createOpenApiSourceJar by registering(Jar::class) {
-    // Java sources
-    from(sourceSets.main.get().java) {
-      include("**/com/maddyhome/idea/vim/**/*.java")
-    }
-    from(project(":vim-engine").sourceSets.main.get().java) {
-      include("**/com/maddyhome/idea/vim/**/*.java")
-    }
-    // Kotlin sources
-    from(kotlin.sourceSets.main.get().kotlin) {
-      include("**/com/maddyhome/idea/vim/**/*.kt")
-    }
-    from(project(":vim-engine").kotlin.sourceSets.main.get().kotlin) {
-      include("**/com/maddyhome/idea/vim/**/*.kt")
-    }
+  val sourcesJar by registering(Jar::class) {
+    dependsOn(moduleSources)
     destinationDirectory.set(layout.buildDirectory.dir("libs"))
-    archiveClassifier.set("src")
+    archiveClassifier.set(DocsType.SOURCES)
+    from(sourceSets.main.map { it.kotlin })
+    from(provider {
+      moduleSources.map {
+        it.map { jarFile -> zipTree(jarFile) }
+      }
+    })
   }
 
   buildPlugin {
-    dependsOn(createOpenApiSourceJar)
-    from(createOpenApiSourceJar) { into("lib/src") }
+    dependsOn(sourcesJar)
+    from(sourcesJar) { into("lib/src") }
   }
 }
 
@@ -245,44 +269,41 @@ gradle.projectsEvaluated {
 
 // --- Intellij plugin
 
-intellij {
-  version.set(ideaVersion)
-  type.set(ideaType)
-  pluginName.set("IdeaVim")
+intellijPlatform {
+  pluginConfiguration {
+    name = "IdeaVim"
+    changeNotes.set(
+      """<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>"""
+    )
 
-  updateSinceUntilBuild.set(false)
+    ideaVersion {
+      // Set the since-build value, but leave until-build open ended (default is MAJOR.*)
+      // Don't forget to update plugin.xml
+      // TODO: Do we need this to be here *and* in plugin.xml?
+      sinceBuild.set("241.15989.150")
+      untilBuild.set(provider { null })
+    }
+  }
 
-  downloadSources.set(downloadIdeaSources.toBoolean())
-  instrumentCode.set(instrumentPluginCode.toBoolean())
-  intellijRepository.set("https://www.jetbrains.com/intellij-repository")
-  plugins.set(listOf("AceJump:3.8.11"))
-}
-
-tasks {
-  publishPlugin {
+  publishing {
     channels.set(publishChannels.split(","))
     token.set(publishToken)
   }
 
-  signPlugin {
+  signing {
     certificateChain.set(providers.environmentVariable("CERTIFICATE_CHAIN"))
     privateKey.set(providers.environmentVariable("PRIVATE_KEY"))
     password.set(providers.environmentVariable("PRIVATE_KEY_PASSWORD"))
   }
 
-  runPluginVerifier {
-    downloadDir.set("${project.buildDir}/pluginVerifier/ides")
-    teamCityOutputFormat.set(true)
+  verifyPlugin {
+    teamCityOutputFormat = true
+    ides {
+      recommended()
+    }
   }
 
-  patchPluginXml {
-    // Don't forget to update plugin.xml
-    sinceBuild.set("241.15989.150")
-
-    changeNotes.set(
-      """<a href="https://youtrack.jetbrains.com/issues/VIM?q=State:%20Fixed%20Fix%20versions:%20${version.get()}">Changelog</a>"""
-    )
-  }
+  instrumentCode.set(instrumentPluginCode.toBoolean())
 }
 
 ksp {
@@ -884,12 +905,12 @@ fun changes(): List<Change> {
   println("Start changes processing")
   for (message in messages) {
     println("Processing '$message'...")
-    val lowercaseMessage = message.toLowerCase()
+    val lowercaseMessage = message.lowercase()
     val regex = "^fix\\((vim-\\d+)\\):".toRegex()
     val findResult = regex.find(lowercaseMessage)
     if (findResult != null) {
       println("Message matches")
-      val value = findResult.groups[1]!!.value.toUpperCase()
+      val value = findResult.groups[1]!!.value.uppercase()
       val shortMessage = message.drop(findResult.range.last + 1).trim()
       newFixes += Change(value, shortMessage)
     } else {
diff --git a/gradle.properties b/gradle.properties
index 9b769d771..1b6f8e933 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -8,11 +8,20 @@
 
 # suppress inspection "UnusedProperty" for whole file
 
+# ideaVersion is the version of the IDE that we'll add as a dependency. The format of the version string depends on the
+# value of the `org.jetbrains.intellij.platform.buildFeature.useBinaryReleases` property/build feature.
+# If enabled (default) then the IDE will be a normal, packaged release, which you could install and run like a retail
+# version of the IDE, downloaded from CDN. The `ideaVersion` property should be a marketing version such as `2024.1` or
+# `2024.1.1` (note no trailing `0`). You can find an example list of all versions for IDEA Community here:
+# https://data.services.jetbrains.com/products?code=IC
+# If the build feature is disabled, the IDE will be downloaded from Maven, and should match the format published in
+# Maven, such as `2024.1` (again, no trailing `.0`), `2024.1.1`, `241-EAP-SNAPSHOT`, etc.
+# You can see a list of release versions here: https://www.jetbrains.com/intellij-repository/releases
+# And a list of snapshot versions here: https://www.jetbrains.com/intellij-repository/snapshots
 #ideaVersion=LATEST-EAP-SNAPSHOT
 ideaVersion=2024.1.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
 javaVersion=17
@@ -40,4 +49,12 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
 kotlin.stdlib.default.dependency=false
 
 # Disable incremental annotation processing
-ksp.incremental=false
\ No newline at end of file
+ksp.incremental=false
+
+# Build features
+# Temporarily disable downloading the IDE dependency from CDN, and use Maven The Plugin DevKit plugin is currently
+# unable to download sources when the IDE is packaged as a normal binary release. This only affects developers working
+# with and debugging IdeaVim. Once the fixed version of DevKit is available, IdeaVim developers can update and this flag
+# can be removed.
+# DevKit will be fixed in IDEA 2024.1.4 and a future EAP of 2024.2
+org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false
diff --git a/tests/java-tests/build.gradle.kts b/tests/java-tests/build.gradle.kts
index 0e8f29b89..47287a832 100644
--- a/tests/java-tests/build.gradle.kts
+++ b/tests/java-tests/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.intellij.platform.gradle.TestFrameworkType
+
 /*
  * Copyright 2003-2024 The IdeaVim authors
  *
@@ -9,16 +11,20 @@
 plugins {
   id("java")
   kotlin("jvm")
-  id("org.jetbrains.intellij")
+  id("org.jetbrains.intellij.platform.module")
 }
 
 val kotlinVersion: String by project
+val ideaType: String by project
 val ideaVersion: String by project
 val javaVersion: String by project
 
 repositories {
   mavenCentral()
-  maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
+
+  intellijPlatform {
+    defaultRepositories()
+  }
 }
 
 dependencies {
@@ -26,35 +32,24 @@ dependencies {
   compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
   testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
   testImplementation(testFixtures(project(":"))) // The root project
+
+  intellijPlatform {
+    create(ideaType, ideaVersion)
+    testFramework(TestFrameworkType.Platform)
+    testFramework(TestFrameworkType.JUnit5)
+    bundledPlugins("com.intellij.java", "org.jetbrains.plugins.yaml")
+    instrumentationTools()
+  }
+}
+
+intellijPlatform {
+  buildSearchableOptions = false
 }
 
 tasks {
   test {
     useJUnitPlatform()
   }
-
-  verifyPlugin {
-    enabled = false
-  }
-
-  publishPlugin {
-    enabled = false
-  }
-
-  runIde {
-    enabled = false
-  }
-
-  runPluginVerifier {
-    enabled = false
-  }
-}
-
-intellij {
-  version.set(ideaVersion)
-  type.set("IC")
-  // Yaml is only used for testing. It's part of the IdeaIC distribution, but needs to be included as a reference
-  plugins.set(listOf("java", "yaml"))
 }
 
 java {
diff --git a/tests/long-running-tests/build.gradle.kts b/tests/long-running-tests/build.gradle.kts
index a8d726b33..6b5962fea 100644
--- a/tests/long-running-tests/build.gradle.kts
+++ b/tests/long-running-tests/build.gradle.kts
@@ -1,15 +1,23 @@
+import org.jetbrains.intellij.platform.gradle.TestFrameworkType
+import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform
+import org.jetbrains.intellij.platform.gradle.tasks.CustomTestIdeTask
+
 plugins {
   java
   kotlin("jvm")
-  id("org.jetbrains.intellij")
+  id("org.jetbrains.intellij.platform.module")
 }
 
 repositories {
   mavenCentral()
-  maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
+
+  intellijPlatform {
+    defaultRepositories()
+  }
 }
 
 val kotlinVersion: String by project
+val ideaType: String by project
 val ideaVersion: String by project
 val javaVersion: String by project
 
@@ -18,42 +26,32 @@ dependencies {
   compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
   testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
   testImplementation(testFixtures(project(":"))) // The root project
+
+  intellijPlatform {
+    create(ideaType, ideaVersion)
+    testFramework(TestFrameworkType.Platform)
+    testFramework(TestFrameworkType.JUnit5)
+    instrumentationTools()
+  }
+}
+
+intellijPlatform {
+  buildSearchableOptions = false
 }
 
 tasks {
   // This task is disabled because it should be excluded from `gradle test` run (because it's slow)
   // I didn't find a better way to exclude except disabling and defining a new task with a different name
+  // Note that useJUnitTestPlatform() is required to prevent red code
   test {
     enabled = false
     useJUnitPlatform()
   }
 
-  register<Test>("testLongRunning") {
+  register<CustomTestIdeTask>("testLongRunning") {
     group = "verification"
     useJUnitPlatform()
   }
-
-  verifyPlugin {
-    enabled = false
-  }
-
-  publishPlugin {
-    enabled = false
-  }
-
-  runIde {
-    enabled = false
-  }
-
-  runPluginVerifier {
-    enabled = false
-  }
-}
-
-intellij {
-  version.set(ideaVersion)
-  type.set("IC")
-  plugins.set(listOf("java"))
 }
 
 java {
diff --git a/tests/property-tests/build.gradle.kts b/tests/property-tests/build.gradle.kts
index 7fcbd7767..cd4bf458b 100644
--- a/tests/property-tests/build.gradle.kts
+++ b/tests/property-tests/build.gradle.kts
@@ -1,15 +1,23 @@
+import org.jetbrains.intellij.platform.gradle.TestFrameworkType
+import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform
+import org.jetbrains.intellij.platform.gradle.tasks.CustomTestIdeTask
+
 plugins {
   java
   kotlin("jvm")
-  id("org.jetbrains.intellij")
+  id("org.jetbrains.intellij.platform.module")
 }
 
 repositories {
   mavenCentral()
-  maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
+
+  intellijPlatform {
+    defaultRepositories()
+  }
 }
 
 val kotlinVersion: String by project
+val ideaType: String by project
 val ideaVersion: String by project
 val javaVersion: String by project
 
@@ -18,6 +26,18 @@ dependencies {
   compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
   testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
   testImplementation(testFixtures(project(":"))) // The root project
+
+  intellijPlatform {
+    create(ideaType, ideaVersion)
+    bundledPlugins("com.intellij.java")
+    testFramework(TestFrameworkType.Platform)
+    testFramework(TestFrameworkType.JUnit5)
+    instrumentationTools()
+  }
+}
+
+intellijPlatform {
+  buildSearchableOptions = false
 }
 
 tasks {
@@ -28,32 +48,10 @@ tasks {
     enabled = false
   }
 
-  register<Test>("testPropertyBased") {
+  register<CustomTestIdeTask>("testPropertyBased") {
     group = "verification"
     useJUnitPlatform()
   }
-
-  verifyPlugin {
-    enabled = false
-  }
-
-  publishPlugin {
-    enabled = false
-  }
-
-  runIde {
-    enabled = false
-  }
-
-  runPluginVerifier {
-    enabled = false
-  }
-}
-
-intellij {
-  version.set(ideaVersion)
-  type.set("IC")
-  plugins.set(listOf("java"))
 }
 
 java {
diff --git a/vim-engine/build.gradle.kts b/vim-engine/build.gradle.kts
index 9af976fa1..f5284e33f 100644
--- a/vim-engine/build.gradle.kts
+++ b/vim-engine/build.gradle.kts
@@ -16,6 +16,12 @@ plugins {
     antlr
 }
 
+val sourcesJarArtifacts by configurations.registering {
+  attributes {
+    attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType.SOURCES))
+  }
+}
+
 val kotlinVersion: String by project
 val kotlinxSerializationVersion: String by project
 
@@ -99,6 +105,8 @@ java {
   withJavadocJar()
 }
 
+artifacts.add(sourcesJarArtifacts.name, tasks.named("sourcesJar"))
+
 val spaceUsername: String by project
 val spacePassword: String by project
 val engineVersion: String by project
@@ -122,4 +130,4 @@ publishing {
       }
     }
   }
-}
\ No newline at end of file
+}