diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/UriUtils.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/UriUtils.kt index d2030280f28..b7adf31a535 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/UriUtils.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/UriUtils.kt @@ -23,10 +23,16 @@ object UriUtils { // Remove query parameters if present val uriStr = uri.substringBefore("?") - // Handle Windows file paths with authority component + // Handle Windows file paths with authority component. + // URI(scheme, authority, path, query) treats path as decoded and re-encodes `%`, + // so already-encoded paths (Unity%20Projects) must be parsed as a URI string. if (uriStr.startsWith("file://") && !uriStr.startsWith("file:///")) { val path = uriStr.substringAfter("file://") - return URI("file", "", "/$path", null) + return try { + URI("file:///$path") + } catch (e: Exception) { + URI("file", "", "/$path", null) + } } return try { diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/file/FileUtils.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/file/FileUtils.kt index 6aac270cdfb..73603046101 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/file/FileUtils.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/file/FileUtils.kt @@ -2,6 +2,7 @@ package com.github.continuedev.continueintellijextension.`continue`.file import com.github.continuedev.continueintellijextension.FileStats import com.github.continuedev.continueintellijextension.FileType +import com.github.continuedev.continueintellijextension.`continue`.UriUtils import com.intellij.openapi.application.runReadAction import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.diagnostic.Logger @@ -23,7 +24,8 @@ class FileUtils( findFile(fileUri) != null fun writeFile(fileUri: String, content: String) { - val path = VfsUtilCore.urlToPath(fileUri) + val path = toLocalPath(fileUri) + ?: return val pathDirectory = VfsUtil.getParentDir(path) ?: return LOG.warn("Parent directory is null for $path") val vfsDirectory = VfsUtil.createDirectories(pathDirectory) @@ -91,12 +93,22 @@ class FileUtils( }.toMap() private fun findFile(fileUri: String): VirtualFile? { - val noParams = fileUri.substringBefore("?") - val normalizedAuthority = normalizeWindowsAuthority(noParams) + val path = toLocalPath(fileUri) + ?: return null return VirtualFileManager.getInstance() - .refreshAndFindFileByUrl(normalizedAuthority) + .refreshAndFindFileByUrl(VfsUtilCore.pathToUrl(path)) } + // VfsUtilCore.urlToPath does not percent-decode, so file://.../Unity%20Projects + // would create/find a literal "%20" directory. UriUtils.uriToFile uses java.net.URI. + private fun toLocalPath(fileUri: String): String? = + try { + UriUtils.uriToFile(fileUri).path.replace('\\', '/') + } catch (e: Exception) { + LOG.warn("Could not resolve path for $fileUri", e) + null + } + private fun readDocument(file: VirtualFile, maxLength: Int): String? { val document = FileDocumentManager.getInstance().getDocument(file) ?: return null @@ -108,16 +120,6 @@ class FileUtils( text.replace("\r\n", "\n") .replace("\r", "\n") - private fun normalizeWindowsAuthority(fileUri: String): String { - val authorityPrefix = "file://" - val noAuthorityPrefix = "file:///" - if (fileUri.startsWith(authorityPrefix) && !fileUri.startsWith(noAuthorityPrefix)) { - val path = fileUri.substringAfter(authorityPrefix) - return "$noAuthorityPrefix$path" - } - return fileUri - } - private companion object { private val LOG = Logger.getInstance(FileUtils::class.java) } diff --git a/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/FileUtilsTest.kt b/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/FileUtilsTest.kt index 338cb6fdafd..80da5c61125 100644 --- a/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/FileUtilsTest.kt +++ b/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/FileUtilsTest.kt @@ -41,6 +41,11 @@ class FileUtilsTest : UsefulTestCase() { assertFalse(fileUtils.fileExists("file://dir/missing.txt")) } + fun `test fileExists decodes percent-encoded path`() { + myFixture.createTempFile("Unity Projects/foo.txt") + assertTrue(fileUtils.fileExists("file://$tmp/Unity%20Projects/foo.txt")) + } + fun `test writeFile creates missing file`() { fileUtils.writeFile("file://$tmp/file.txt", "text") assertEquals("text", myFixture.readTempFile("file.txt")) @@ -57,6 +62,12 @@ class FileUtilsTest : UsefulTestCase() { assertEquals("new_text", myFixture.readTempFile("overwrite.txt")) } + fun `test writeFile decodes percent-encoded path`() { + fileUtils.writeFile("file://$tmp/Unity%20Projects/foo.txt", "text") + assertEquals("text", myFixture.readTempFile("Unity Projects/foo.txt")) + assertNull(myFixture.tempDirFixture.getFile("Unity%20Projects/foo.txt")) + } + fun `test readFile`() { myFixture.createTempFile("file.txt", "text") assertEquals("text", fileUtils.readFile("file://$tmp/file.txt")) @@ -77,6 +88,11 @@ class FileUtilsTest : UsefulTestCase() { assertEmpty(fileUtils.readFile("file://missing.txt")) } + fun `test readFile decodes percent-encoded path`() { + myFixture.createTempFile("Unity Projects/foo.txt", "text") + assertEquals("text", fileUtils.readFile("file://$tmp/Unity%20Projects/foo.txt")) + } + fun `test readFile normalizes line endings`() { myFixture.createTempFile("file.txt", "line\r\nline\rline\nline") val normalized = fileUtils.readFile("file://$tmp/file.txt") diff --git a/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/UriUtilsTest.kt b/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/UriUtilsTest.kt index 8295abfe607..4ba0b07a573 100644 --- a/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/UriUtilsTest.kt +++ b/extensions/intellij/src/test/kotlin/com/github/continuedev/continueintellijextension/unit/UriUtilsTest.kt @@ -75,4 +75,12 @@ class UriUtilsTest : TestCase() { assertEquals("/C:/Users/user/projects/[gamemode]/file.lua", parsed.path) assertEquals("file:///C:/Users/user/projects/%5Bgamemode%5D/file.lua", parsed.toString()) } + + // file://C:/... (two slashes). URI(scheme, authority, path, query) re-encodes `%`. + fun `test Windows path with percent-encoded spaces`() { + val uri = "file://C:/Unity%20Projects/My%20Project/foo.txt" + val parsed = UriUtils.parseUri(uri) + assertEquals("file", parsed.scheme) + assertEquals("/C:/Unity Projects/My Project/foo.txt", parsed.path) + } }