fix: Add file tests + migrate to IntelliJ Platform Gradle Plugin v2
The tests use fixtures and operate on real files in a temp directory. This change will allow us to confidently migrate from java.io.File to VFS. Note: These tests are headless and starts the Continue binary. This gives us many possibilities because we can write fast and valuable tests without runIdeForUiTests. The IntelliJ Platform plugin already includes classes like BasePlatformTestCase written in JUnit 3, which we should be using for unit testing. We also have JUnit 5 for e2e testing. As a result, we had two different runners, which caused problems because we couldn’t use classes like UsefulTest since they required a different runner. In order to fix that, I had to add `org.junit.vintage:junit-vintage-engine` to enable running both JUnit 3 and JUnit 5 tests. Note: ideally, we should have a sourceSet named testIntegration with its own dependencies for E2E (robot, JUnit 5, video recorder). That would give us a clean separation between E2E tests and unit tests. That would prevent accidental use of JUnit 5 classes in unit tests. We should do that in the future. The new platform simplifies plugin config and allows us to remove some of the custom code. I left the original Qodana and Kover configs, but looking at our workflow, i think it's currently dead code. If we don’t plan to use them, I think we can remove these dependencies in the future. Note: along with this migration, I had to bump a wrapper to latest version. For some reason, we included the Kotlin standard library with the plugin, even though we had a property specifying we didn’t want to. I saw no objections to removing this dependency, so I decided to do that. From now on, we rely on older stdlib that's bundled with 2022.3+. More context: https://github.com/continuedev/continue/pull/6431#issuecomment-3031838189 This proves my point regarding the removal of the standard library dependency. I added plugin verification against versions 2022.3.3 (oldest) and 2025.1.4 (newest). Everything is working. Plugin can be verified with `./gradlew verifyPlugin`. We can consider adding this to CI later. I made a few small improvements that simplify the build scripts: * Some properties were removed because they duplicated content from `plugin.xml` (e.g., supported version numbers, plugin ID, and similar - all were unnecessarily overridden inside `intellij {}`/`intellijPlatform {}`). * I replaced `prepareSandbox` with a separate task that is now a dependency of all `runIde` tasks, so there is no need to declare each case individually. * From now on, properties are read via delegation instead of calling our custom `properties(...)` function. This is type-safe (if a property is not declared, the build will fail). * I removed the `kotlin("test")` dependency, which we were only using for `assertTrue`. The e2e Autocomplete tests are very flaky, and unfortunately, I haven’t been able to find a solution yet. I will try to address this in upcoming fixes. I believe that ultimately we should have max one e2e test describing the happy path, and the rest of the features should be tested with unit tests. Our e2e tests are very inconvenient, which results in no one running them (because we hijack the input), so they should be kept to a minimum. The rest of the e2e tests were already disabled (I assume I know why :D), so I didn't touch them. Fun fact: with the removal of the standard library, we managed to remove all unnecessary dependencies except for Sentry and PostHog (which are included for obvious reasons).
This commit is contained in:
parent
076ff3efc1
commit
5f647afdf2
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
<option name="version" value="1.8.0" />
|
||||
</component>
|
||||
</project>
|
|
@ -110,6 +110,23 @@ You can selectively increase the log granularity (e.g., debug-level logs) as fol
|
|||
|
||||
You can find more information about this feature in [official docs](https://youtrack.jetbrains.com/articles/SUPPORT-A-43/How-to-enable-debug-logging-in-IntelliJ-IDEA).
|
||||
|
||||
### Developing `build.plugin.kts`
|
||||
|
||||
If in doubt, check out the
|
||||
official [IntelliJ Platform Plugin Template](https://github.com/JetBrains/intellij-platform-plugin-template).
|
||||
These templates are the most up-to-date examples of how to correctly customize the plugin build scripts.
|
||||
|
||||
Also, check out
|
||||
the [useful recipes](https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-recipes.html)
|
||||
for common problems.
|
||||
|
||||
### Adding new extensions in `plugin.xml`
|
||||
|
||||
There's a tool called [JetBrains Platform Explorer](https://plugins.jetbrains.com/intellij-platform-explorer) that
|
||||
aggregates plugin metadata and allows you to filter by specific
|
||||
extension points. If you're having trouble implementing a feature that's not officially documented,
|
||||
you can learn from other open source plugins.
|
||||
|
||||
### Reloading changes
|
||||
|
||||
- `extensions/intellij`: Attempt to reload changed classes by selecting
|
||||
|
|
|
@ -1,77 +1,75 @@
|
|||
import org.jetbrains.changelog.markdownToHTML
|
||||
|
||||
fun properties(key: String) = providers.gradleProperty(key)
|
||||
import org.jetbrains.intellij.platform.gradle.TestFrameworkType
|
||||
import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask
|
||||
|
||||
fun environment(key: String) = providers.environmentVariable(key)
|
||||
|
||||
fun Sync.prepareSandbox() {
|
||||
from("../../binary/bin") { into("${intellij.pluginName.get()}/core/") }
|
||||
}
|
||||
|
||||
val remoteRobotVersion = "0.11.23"
|
||||
val platformType: String by project
|
||||
val platformVersion: String by project
|
||||
val pluginGroup: String by project
|
||||
val pluginVersion: String by project
|
||||
val pluginSinceBuild: String by project
|
||||
val pluginRepositoryUrl: String by project
|
||||
val isEap get() = environment("RELEASE_CHANNEL").orNull == "eap"
|
||||
val pluginVersion = properties("pluginVersion").get()
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
kotlin("jvm") version "1.9.0"
|
||||
id("org.jetbrains.intellij") version "1.15.0"
|
||||
kotlin("jvm") version "1.8.0"
|
||||
id("org.jetbrains.intellij.platform") version "2.6.0"
|
||||
id("org.jetbrains.changelog") version "2.1.2"
|
||||
id("org.jetbrains.qodana") version "0.1.13"
|
||||
id("org.jetbrains.kotlinx.kover") version "0.7.3"
|
||||
kotlin("plugin.serialization") version "1.8.0"
|
||||
id("io.sentry.jvm.gradle") version "5.8.0"
|
||||
}
|
||||
|
||||
group = properties("pluginGroup").get()
|
||||
group = pluginGroup
|
||||
version = if (isEap) "$pluginVersion-eap" else pluginVersion
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = uri("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") }
|
||||
intellijPlatform {
|
||||
defaultRepositories()
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.32")
|
||||
implementation("com.posthog.java:posthog:1.+")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||
intellijPlatform {
|
||||
create(platformType, platformVersion)
|
||||
plugins(listOf("org.jetbrains.plugins.terminal:223.8214.6"))
|
||||
testFramework(TestFrameworkType.Platform)
|
||||
}
|
||||
implementation("com.posthog.java:posthog:1.2.0")
|
||||
|
||||
testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion")
|
||||
testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion")
|
||||
testImplementation("io.mockk:mockk:1.14.2")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2")
|
||||
testImplementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
|
||||
testImplementation("com.automation-remarks:video-recorder-junit5:2.0")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
|
||||
testImplementation(kotlin("test"))
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.0") // required to run both JUnit 5 and JUnit 3
|
||||
}
|
||||
|
||||
// Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for
|
||||
// 2022.2+.
|
||||
kotlin { jvmToolchain(17) }
|
||||
|
||||
// Configure Gradle IntelliJ Plugin - read more:
|
||||
// https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html
|
||||
intellij {
|
||||
pluginName = properties("pluginName")
|
||||
version = properties("platformVersion")
|
||||
type = properties("platformType")
|
||||
|
||||
// Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file.
|
||||
plugins =
|
||||
properties("platformPlugins").map {
|
||||
it.split(',').map(String::trim).filter(String::isNotEmpty)
|
||||
intellijPlatform {
|
||||
pluginConfiguration {
|
||||
ideaVersion {
|
||||
sinceBuild = pluginSinceBuild
|
||||
}
|
||||
}
|
||||
pluginVerification {
|
||||
ides {
|
||||
ide("IC", "2025.1.4")
|
||||
ide("IC", "2022.3.3")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure Gradle Changelog Plugin - read more:
|
||||
// https://github.com/JetBrains/gradle-changelog-plugin
|
||||
changelog {
|
||||
groups.empty()
|
||||
repositoryUrl = properties("pluginRepositoryUrl")
|
||||
repositoryUrl = pluginRepositoryUrl
|
||||
}
|
||||
|
||||
// Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin
|
||||
qodana {
|
||||
cachePath = provider { file(".qodana").canonicalPath }
|
||||
reportPath = provider { file("build/reports/inspections").canonicalPath }
|
||||
|
@ -79,75 +77,52 @@ qodana {
|
|||
showReport = environment("QODANA_SHOW_REPORT").map { it.toBoolean() }.getOrElse(false)
|
||||
}
|
||||
|
||||
// Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration
|
||||
koverReport { defaults { xml { onCheck = true } } }
|
||||
|
||||
intellijPlatformTesting {
|
||||
runIde {
|
||||
register("runIdeForUiTests") {
|
||||
task {
|
||||
environment(
|
||||
"CONTINUE_GLOBAL_DIR",
|
||||
"${rootProject.projectDir}/src/test/kotlin/com/github/continuedev/continueintellijextension/e2e/test-continue"
|
||||
)
|
||||
jvmArgumentProviders += CommandLineArgumentProvider {
|
||||
listOf(
|
||||
"-Drobot-server.port=8082",
|
||||
"-Dide.mac.message.dialogs.as.sheets=false",
|
||||
"-Djb.privacy.policy.text=<!--999.999-->",
|
||||
"-Djb.consents.confirmation.enabled=false",
|
||||
"-Dide.mac.file.chooser.native=false",
|
||||
"-DjbScreenMenuBar.enabled=false",
|
||||
"-Dapple.laf.useScreenMenuBar=false",
|
||||
"-Didea.trust.all.projects=true",
|
||||
"-Dide.show.tips.on.startup.default.value=false",
|
||||
"-Dide.browser.jcef.jsQueryPoolSize=10000",
|
||||
"-Dide.browser.jcef.contextMenu.devTools.enabled=true"
|
||||
)
|
||||
}
|
||||
}
|
||||
plugins {
|
||||
robotServerPlugin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks {
|
||||
downloadRobotServerPlugin {
|
||||
version = remoteRobotVersion
|
||||
}
|
||||
|
||||
prepareSandbox {
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
prepareTestingSandbox {
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
prepareUiTestingSandbox {
|
||||
prepareSandbox()
|
||||
}
|
||||
|
||||
wrapper {
|
||||
gradleVersion = properties("gradleVersion").get()
|
||||
withType<PrepareSandboxTask> {
|
||||
from("../../binary/bin") {
|
||||
into(pluginName.map { "$it/core" })
|
||||
}
|
||||
}
|
||||
|
||||
patchPluginXml {
|
||||
sinceBuild = properties("pluginSinceBuild")
|
||||
untilBuild = properties("pluginUntilBuild")
|
||||
pluginDescription =
|
||||
providers.fileContents(layout.projectDirectory.file("README.md")).asText.map {
|
||||
val start = "<!-- Plugin description -->"
|
||||
val end = "<!-- Plugin description end -->"
|
||||
|
||||
with(it.lines()) {
|
||||
if (!containsAll(listOf(start, end))) {
|
||||
throw GradleException(
|
||||
"Plugin description section not found in README.md:\n$start ... $end"
|
||||
)
|
||||
}
|
||||
subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure UI tests plugin
|
||||
// Read more: https://github.com/JetBrains/intellij-ui-test-robot
|
||||
runIdeForUiTests {
|
||||
environment(
|
||||
"CONTINUE_GLOBAL_DIR",
|
||||
"${rootProject.projectDir}/src/test/kotlin/com/github/continuedev/continueintellijextension/e2e/test-continue"
|
||||
)
|
||||
systemProperty("robot-server.port", "8082")
|
||||
systemProperty("ide.mac.message.dialogs.as.sheets", "false")
|
||||
systemProperty("jb.privacy.policy.text", "<!--999.999-->")
|
||||
systemProperty("jb.consents.confirmation.enabled", "false")
|
||||
systemProperty("ide.mac.file.chooser.native", "false")
|
||||
systemProperty("jbScreenMenuBar.enabled", "false")
|
||||
systemProperty("apple.laf.useScreenMenuBar", "false")
|
||||
systemProperty("idea.trust.all.projects", "true")
|
||||
systemProperty("ide.show.tips.on.startup.default.value", "false")
|
||||
systemProperty("ide.browser.jcef.jsQueryPoolSize", "10000")
|
||||
systemProperty("ide.browser.jcef.contextMenu.devTools.enabled", "true")
|
||||
|
||||
// This is to ensure we load the GUI with OSR enabled. We have logic that
|
||||
// renders with OSR disabled below a particular IDE version.
|
||||
// See ContinueExtensionSettingsService.kt for more info.
|
||||
// Currently commented out however since test fail in CI with this version
|
||||
// intellij {
|
||||
// version = "2024.1"
|
||||
// }
|
||||
pluginDescription = providers.fileContents(layout.projectDirectory.file("README.md")).asText.get()
|
||||
.substringAfter("<!-- Plugin description -->")
|
||||
.substringBefore("<!-- Plugin description end -->")
|
||||
.let(::markdownToHTML)
|
||||
check(pluginDescription.get().isNotEmpty()) { "Plugin description section not found in README.md" }
|
||||
}
|
||||
|
||||
signPlugin {
|
||||
|
@ -164,8 +139,10 @@ tasks {
|
|||
}
|
||||
|
||||
runIde {
|
||||
val dir = "${rootProject.projectDir.parentFile.parentFile.absolutePath}/manual-testing-sandbox"
|
||||
args = listOf(dir, "$dir/test.kt")
|
||||
val openProject = "$projectDir/../../manual-testing-sandbox"
|
||||
argumentProviders += CommandLineArgumentProvider {
|
||||
listOf(openProject, "$openProject/test.kt")
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
|
|
|
@ -1,26 +1,9 @@
|
|||
# IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html
|
||||
pluginGroup=com.github.continuedev.continueintellijextension
|
||||
pluginName=continue-intellij-extension
|
||||
pluginRepositoryUrl=https://github.com/continuedev/continue
|
||||
# SemVer format -> https://semver.org
|
||||
pluginVersion=1.0.33
|
||||
# Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
|
||||
pluginSinceBuild=223
|
||||
# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
|
||||
platformType=IC
|
||||
platformVersion=2022.3.3
|
||||
# org.gradle.java.home=/opt/homebrew/opt/openjdk@17
|
||||
#platformVersion = LATEST-EAP-SNAPSHOT
|
||||
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
|
||||
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
|
||||
platformPlugins=org.jetbrains.plugins.terminal
|
||||
# Gradle Releases -> https://github.com/gradle/gradle/releases
|
||||
gradleVersion=8.3
|
||||
# Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib
|
||||
kotlin.stdlib.default.dependency=false
|
||||
# Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html
|
||||
org.gradle.configuration-cache=true
|
||||
# Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html
|
||||
org.gradle.caching=true
|
||||
# Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment
|
||||
systemProp.org.gradle.unsafe.kotlin.assignment=true
|
||||
|
|
Binary file not shown.
|
@ -1,8 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
#org.gradle.java.home=/opt/homebrew/opt/openjdk@17
|
|
@ -145,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
|
@ -153,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
@ -202,11 +202,11 @@ fi
|
|||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.github.continuedev.continueintellijextension.`continue`
|
|||
import com.github.continuedev.continueintellijextension.*
|
||||
import com.github.continuedev.continueintellijextension.constants.ContinueConstants
|
||||
import com.github.continuedev.continueintellijextension.constants.getContinueGlobalPath
|
||||
import com.github.continuedev.continueintellijextension.`continue`.file.FileUtils
|
||||
import com.github.continuedev.continueintellijextension.error.ContinueErrorService
|
||||
import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings
|
||||
import com.github.continuedev.continueintellijextension.services.ContinuePluginService
|
||||
|
@ -38,9 +39,7 @@ import java.awt.Toolkit
|
|||
import java.awt.datatransfer.DataFlavor
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class IntelliJIDE(
|
||||
private val project: Project,
|
||||
|
@ -49,7 +48,7 @@ class IntelliJIDE(
|
|||
) : IDE {
|
||||
|
||||
private val gitService = GitService(project, continuePluginService)
|
||||
|
||||
private val fileUtils = FileUtils()
|
||||
private val ripgrep: String = getRipgrepPath()
|
||||
|
||||
init {
|
||||
|
@ -204,16 +203,11 @@ class IntelliJIDE(
|
|||
return configs as List<ContinueRcJson>
|
||||
}
|
||||
|
||||
override suspend fun fileExists(filepath: String): Boolean {
|
||||
val file = UriUtils.uriToFile(filepath)
|
||||
return file.exists()
|
||||
}
|
||||
override suspend fun fileExists(filepath: String): Boolean =
|
||||
fileUtils.fileExists(filepath)
|
||||
|
||||
override suspend fun writeFile(path: String, contents: String) {
|
||||
val file = UriUtils.uriToFile(path)
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeText(contents)
|
||||
}
|
||||
override suspend fun writeFile(path: String, contents: String) =
|
||||
fileUtils.writeFile(path, contents)
|
||||
|
||||
override suspend fun showVirtualFile(title: String, contents: String) {
|
||||
val virtualFile = LightVirtualFile(title, contents)
|
||||
|
@ -321,39 +315,8 @@ class IntelliJIDE(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun readFile(filepath: String): String {
|
||||
return try {
|
||||
val content = ApplicationManager.getApplication().runReadAction<String?> {
|
||||
val virtualFile = LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(filepath).path)
|
||||
if (virtualFile != null && FileDocumentManager.getInstance().isFileModified(virtualFile)) {
|
||||
return@runReadAction FileDocumentManager.getInstance().getDocument(virtualFile)?.text
|
||||
}
|
||||
return@runReadAction null
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
content
|
||||
} else {
|
||||
val file = UriUtils.uriToFile(filepath)
|
||||
if (!file.exists() || file.isDirectory) return ""
|
||||
withContext(Dispatchers.IO) {
|
||||
FileInputStream(file).use { fis ->
|
||||
val sizeToRead = minOf(100000, file.length()).toInt()
|
||||
val buffer = ByteArray(sizeToRead)
|
||||
val bytesRead = fis.read(buffer, 0, sizeToRead)
|
||||
if (bytesRead <= 0) return@use ""
|
||||
val content = String(buffer, 0, bytesRead, Charset.forName("UTF-8"))
|
||||
// Remove `\r` characters but preserve trailing newlines to prevent line count discrepancies
|
||||
val contentWithoutCR = content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
contentWithoutCR
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
}
|
||||
override suspend fun readFile(filepath: String): String =
|
||||
fileUtils.readFile(filepath)
|
||||
|
||||
override suspend fun readRangeInFile(filepath: String, range: Range): String {
|
||||
val fullContents = readFile(filepath)
|
||||
|
@ -662,13 +625,8 @@ class IntelliJIDE(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun listDir(dir: String): List<List<Any>> {
|
||||
val files = UriUtils.uriToFile(dir).listFiles()?.map {
|
||||
listOf(it.name, if (it.isDirectory) FileType.DIRECTORY.value else FileType.FILE.value)
|
||||
} ?: emptyList()
|
||||
|
||||
return files
|
||||
}
|
||||
override suspend fun listDir(dir: String): List<List<Any>> =
|
||||
fileUtils.listDir(dir)
|
||||
|
||||
override suspend fun getFileStats(files: List<String>): Map<String, FileStats> {
|
||||
return files.associateWith { file ->
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
package com.github.continuedev.continueintellijextension.`continue`.file
|
||||
|
||||
import com.github.continuedev.continueintellijextension.FileType
|
||||
import com.github.continuedev.continueintellijextension.`continue`.UriUtils
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.FileInputStream
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class FileUtils {
|
||||
// todo: use VFS (it's moved from IntellijIde)
|
||||
|
||||
fun fileExists(uri: String): Boolean {
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
return file.exists()
|
||||
}
|
||||
|
||||
fun writeFile(uri: String, contents: String) {
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
file.parentFile?.mkdirs()
|
||||
file.writeText(contents)
|
||||
}
|
||||
|
||||
fun listDir(dir: String): List<List<Any>> {
|
||||
val files = UriUtils.uriToFile(dir).listFiles()?.map {
|
||||
listOf(it.name, if (it.isDirectory) FileType.DIRECTORY.value else FileType.FILE.value)
|
||||
} ?: emptyList()
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
fun readFile(uri: String): String {
|
||||
return try {
|
||||
val content = ApplicationManager.getApplication().runReadAction<String?> {
|
||||
val virtualFile = LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(uri).path)
|
||||
if (virtualFile != null && FileDocumentManager.getInstance().isFileModified(virtualFile)) {
|
||||
return@runReadAction FileDocumentManager.getInstance().getDocument(virtualFile)?.text
|
||||
}
|
||||
return@runReadAction null
|
||||
}
|
||||
|
||||
if (content != null) {
|
||||
content
|
||||
} else {
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
if (!file.exists() || file.isDirectory) return ""
|
||||
FileInputStream(file).use { fis ->
|
||||
val sizeToRead = minOf(100000, file.length()).toInt()
|
||||
val buffer = ByteArray(sizeToRead)
|
||||
val bytesRead = fis.read(buffer, 0, sizeToRead)
|
||||
if (bytesRead <= 0) return@use ""
|
||||
val content = String(buffer, 0, bytesRead, Charset.forName("UTF-8"))
|
||||
// Remove `\r` characters but preserve trailing newlines to prevent line count discrepancies
|
||||
val contentWithoutCR = content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
contentWithoutCR
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -14,9 +14,6 @@
|
|||
com.intellij.modules.json
|
||||
</depends>
|
||||
|
||||
<!-- com.intellij.openapi.module.ModuleManager.Companion is only available since this build -->
|
||||
<idea-version since-build="223.7571.182"/>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<editorFactoryListener
|
||||
implementation="com.github.continuedev.continueintellijextension.autocomplete.AutocompleteEditorListener"/>
|
||||
|
|
|
@ -12,13 +12,12 @@ import com.github.continuedev.continueintellijextension.services.ContinuePluginS
|
|||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.project.Project
|
||||
import io.mockk.*
|
||||
import junit.framework.TestCase
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class ApplyToFileHandlerTest {
|
||||
class ApplyToFileHandlerTest : TestCase() {
|
||||
// Mock all dependencies
|
||||
private val mockProject = mockk<Project>(relaxed = true)
|
||||
private val mockContinuePluginService = mockk<ContinuePluginService>(relaxed = true)
|
||||
|
@ -39,8 +38,7 @@ class ApplyToFileHandlerTest {
|
|||
"tool-call-123"
|
||||
)
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
override fun setUp() {
|
||||
// Common setup
|
||||
every { mockEditorUtils.editor } returns mockEditor
|
||||
every { mockContinuePluginService.coreMessenger } returns mockCoreMessenger
|
||||
|
@ -56,8 +54,7 @@ class ApplyToFileHandlerTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should insert text directly when document is empty`() = runTest {
|
||||
fun `test should insert text directly when document is empty`() = runTest {
|
||||
// Given
|
||||
every { mockEditorUtils.isDocumentEmpty() } returns true
|
||||
|
||||
|
|
|
@ -1,48 +1,39 @@
|
|||
package com.github.continuedev.continueintellijextension.unit
|
||||
|
||||
import com.github.continuedev.continueintellijextension.FimResult
|
||||
import com.github.continuedev.continueintellijextension.`continue`.UriUtils
|
||||
import com.github.continuedev.continueintellijextension.utils.checkFim
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
import kotlin.test.assertEquals
|
||||
import junit.framework.TestCase
|
||||
|
||||
class CheckFimTest {
|
||||
@Test
|
||||
fun testFim() {
|
||||
class CheckFimTest : TestCase() {
|
||||
fun `test FIM`() {
|
||||
val oldEditRange = "hello world"
|
||||
val newEditRange = "hello this world"
|
||||
val cursorPosition = Pair(0, 6)
|
||||
assertEquals(checkFim(oldEditRange, newEditRange, cursorPosition), FimResult.FimEdit("this "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultilineFim() {
|
||||
fun `test multiline FIM`() {
|
||||
val oldEditRange = "hello {\n world\n}"
|
||||
val newEditRange = "hello {\n print()\n world\n}"
|
||||
val cursorPosition = Pair(1, 2)
|
||||
assertEquals(checkFim(oldEditRange, newEditRange, cursorPosition), FimResult.FimEdit("print()\n "))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNotFim() {
|
||||
fun `test not FIM`() {
|
||||
val oldEditRange = "hello world"
|
||||
val newEditRange = "hello this world"
|
||||
val cursorPosition = Pair(0, 8)
|
||||
assertEquals(checkFim(oldEditRange, newEditRange, cursorPosition), FimResult.NotFimEdit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultilineNotFim() {
|
||||
fun `test multiline not FIM`() {
|
||||
val oldEditRange = "hello {\n world\n}"
|
||||
val newEditRange = "hello {\n world\n}\nprint()"
|
||||
val cursorPosition = Pair(1, 2)
|
||||
assertEquals(checkFim(oldEditRange, newEditRange, cursorPosition), FimResult.NotFimEdit)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFimEdge() {
|
||||
fun `test FIM edge`() {
|
||||
val oldEditRange = ""
|
||||
val newEditRange = ""
|
||||
val cursorPosition = Pair(0, 0)
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package com.github.continuedev.continueintellijextension.unit
|
||||
|
||||
import com.github.continuedev.continueintellijextension.FileType
|
||||
import com.github.continuedev.continueintellijextension.`continue`.file.FileUtils
|
||||
import com.intellij.testFramework.UsefulTestCase
|
||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
||||
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
|
||||
|
||||
class FileUtilsTest : UsefulTestCase() {
|
||||
val fileUtils = FileUtils()
|
||||
val myFixture = setupRealFilesFixture()
|
||||
val tmpFixture get() = myFixture.tempDirFixture
|
||||
val tmpDir get() = myFixture.tempDirPath
|
||||
|
||||
override fun setUp() =
|
||||
myFixture.setUp()
|
||||
|
||||
override fun tearDown() =
|
||||
myFixture.tearDown()
|
||||
|
||||
fun `test fileExists`() {
|
||||
val file = tmpFixture.createFile("test.txt", "")
|
||||
val uri = "file://${file.path}"
|
||||
assertTrue(fileUtils.fileExists(uri))
|
||||
}
|
||||
|
||||
fun `test fileExists inside directory`() {
|
||||
val file = tmpFixture.createFile("dir/dir2/file.txt", "")
|
||||
val uri = "file://${file.path}"
|
||||
assertTrue(fileUtils.fileExists(uri))
|
||||
}
|
||||
|
||||
fun `test fileExists fails when file doesn't exists`() {
|
||||
val uri = "file:///tmp/aaaa.txt"
|
||||
assertFalse(fileUtils.fileExists(uri))
|
||||
}
|
||||
|
||||
fun `test readFile`() {
|
||||
val uri = "file://$tmpDir/bbbb.txt"
|
||||
tmpFixture.createFile("bbbb.txt", "contents")
|
||||
val text = fileUtils.readFile(uri)
|
||||
assertEquals("contents", text)
|
||||
}
|
||||
|
||||
fun `test listDir`() {
|
||||
tmpFixture.createFile("a.txt", "a")
|
||||
tmpFixture.createFile("b.txt", "b")
|
||||
tmpFixture.createFile("c.txt", "c")
|
||||
tmpFixture.createFile("dir/invisibleA.txt", "d")
|
||||
tmpFixture.createFile("dir/invisibleB.txt", "e")
|
||||
|
||||
val result = fileUtils.listDir("file://$tmpDir")
|
||||
|
||||
assertTrue(result.any { it[0] == "a.txt" && it[1] == FileType.FILE.value })
|
||||
assertTrue(result.any { it[0] == "b.txt" && it[1] == FileType.FILE.value })
|
||||
assertTrue(result.any { it[0] == "c.txt" && it[1] == FileType.FILE.value })
|
||||
assertTrue(result.any { it[0] == "dir" && it[1] == FileType.DIRECTORY.value })
|
||||
assertTrue(result.any { it[0] == "dir" && it[1] == FileType.DIRECTORY.value })
|
||||
assertFalse(result.any { it[0] == "invisibleA.txt" })
|
||||
assertFalse(result.any { it[0] == "invisibleB.txt" })
|
||||
assertEquals(4, result.size)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
// note: with this, files will actually exist inside /tmp/unitTest/ dir, otherwise it's virtual
|
||||
fun setupRealFilesFixture(): CodeInsightTestFixture {
|
||||
val factory = IdeaTestFixtureFactory.getFixtureFactory()
|
||||
return factory.createCodeInsightFixture(
|
||||
factory.createLightFixtureBuilder("my_continue_project").fixture,
|
||||
factory.createTempDirTestFixture()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,64 +1,48 @@
|
|||
package com.github.continuedev.continueintellijextension.unit
|
||||
|
||||
import com.github.continuedev.continueintellijextension.`continue`.UriUtils
|
||||
import org.junit.jupiter.api.Disabled
|
||||
import org.junit.jupiter.api.Test
|
||||
import junit.framework.TestCase
|
||||
import java.io.File
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class UriUtilsTest {
|
||||
@Test
|
||||
fun testUriToFile() {
|
||||
class UriUtilsTest : TestCase() {
|
||||
fun `test URI to File`() {
|
||||
val uri = "file:///path/to/file"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Disabled("Not working")
|
||||
@Test
|
||||
fun testUriToFileWithWindowsPath() {
|
||||
val uri = "file:///C:/path/to/file"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("C:/path/to/file"), file)
|
||||
}
|
||||
// fixme it's not working
|
||||
// fun `test Windows path`() {
|
||||
// val uri = "file:///C:/path/to/file"
|
||||
// val file = UriUtils.uriToFile(uri)
|
||||
// assertEquals(File("C:/path/to/file"), file)
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun shouldHandleAuthorityComponent() {
|
||||
val uri = "file://C:/path/to/file"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/C:/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithSpaces() {
|
||||
fun `test spaces`() {
|
||||
val uri = "file:///path/to/file%20with%20spaces"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file with spaces"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithSpecialCharacters() {
|
||||
fun `test special characters`() {
|
||||
val uri = "file:///path/to/file%23with%25special%26chars"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file#with%special&chars"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithQueryParams() {
|
||||
fun `test query params`() {
|
||||
val uri = "file:///path/to/file?param=value"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/path/to/file"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithWSLPath() {
|
||||
fun `test WSL path`() {
|
||||
val uri = "file:///wsl$/Ubuntu/home/user/file.txt"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/wsl$/Ubuntu/home/user/file.txt"), file)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUriToFileWithWSLLocalhostPath() {
|
||||
fun `test WSL localhost path`() {
|
||||
val uri = "file:///wsl.localhost/Ubuntu/home/user/file.txt"
|
||||
val file = UriUtils.uriToFile(uri)
|
||||
assertEquals(File("/wsl.localhost/Ubuntu/home/user/file.txt"), file)
|
||||
|
|
Loading…
Reference in New Issue