Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,21 @@ object UriUtils {
*/
fun parseUri(uri: String): URI {
// Remove query parameters if present
val uriStr = uri.substringBefore("?")
val noParams = uri.substringBefore("?")

// Handle Windows file paths with authority component
if (uriStr.startsWith("file://") && !uriStr.startsWith("file:///")) {
val path = uriStr.substringAfter("file://")
return URI("file", "", "/$path", null)
}
// Normalize Windows file paths with authority component, so that the path
// is percent-decoded by URI just like for authority-less file URIs
val uriStr = if (noParams.startsWith("file://") && !noParams.startsWith("file:///"))
"file:///" + noParams.removePrefix("file://")
else
noParams

return try {
URI(uriStr)
} catch (e: Exception) {
// Handle unencoded file URIs (e.g. spaces in path from VirtualFile.toUriOrNull())
if (uriStr.startsWith("file:///")) {
val path = uriStr.removePrefix("file://")
val file = File(path)
file.toURI()
URI("file", "", uriStr.removePrefix("file://"), null)
} else {
throw Exception("Invalid URI: $uri ${e.message}")
}
Expand All @@ -54,4 +53,14 @@ object UriUtils {
val parsedUri = parseUri(uri)
return File(parsedUri)
}

/**
* Converts a URI string to a filesystem path with forward slashes, the
* format expected by the IntelliJ virtual file system.
*
* @param uri The URI string to convert to a file path
* @return A percent-decoded path, e.g. "C:/My Project/file.txt"
*/
fun uriToPath(uri: String): String =
uriToFile(uri).path.replace('\\', '/')
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -91,12 +93,25 @@ 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))
}

/**
* Resolves a file URI to a filesystem path, percent-decoding it so that
* paths containing spaces (e.g. "C:/My Project") are not mistaken for
* literal "%20" segments.
*/
private fun toLocalPath(fileUri: String): String? =
try {
UriUtils.uriToPath(fileUri)
} 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
Expand All @@ -108,16 +123,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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class FileUtilsTest : UsefulTestCase() {
assertEquals("new_text", myFixture.readTempFile("overwrite.txt"))
}

// Regression test for #13134 — encoded URIs must not create %20 directories
fun `test writeFile decodes percent-encoded path`() {
fileUtils.writeFile("file://$tmp/Unity%20Projects/My%20Project/file.txt", "text")
assertEquals("text", myFixture.readTempFile("Unity Projects/My Project/file.txt"))
}

fun `test readFile`() {
myFixture.createTempFile("file.txt", "text")
assertEquals("text", fileUtils.readFile("file://$tmp/file.txt"))
Expand All @@ -77,6 +83,16 @@ class FileUtilsTest : UsefulTestCase() {
assertEmpty(fileUtils.readFile("file://missing.txt"))
}

fun `test readFile decodes percent-encoded path`() {
myFixture.createTempFile("Unity Projects/My Project/file.txt", "text")
assertEquals("text", fileUtils.readFile("file://$tmp/Unity%20Projects/My%20Project/file.txt"))
}

fun `test fileExists decodes percent-encoded path`() {
myFixture.createTempFile("Unity Projects/My Project/file.txt")
assertTrue(fileUtils.fileExists("file://$tmp/Unity%20Projects/My%20Project/file.txt"))
}

fun `test readFile normalizes line endings`() {
myFixture.createTempFile("file.txt", "line\r\nline\rline\nline")
val normalized = fileUtils.readFile("file://$tmp/file.txt")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ class UriUtilsTest : TestCase() {
assertEquals(File("/path/to/[gamemode]/file.lua"), result)
}

// Regression test for #13134 — Windows paths with spaces from JetBrains IDEs
fun `test Windows path with authority and encoded spaces`() {
val uri = "file://C:/Unity%20Projects/My%20Project/file.txt"
val parsed = UriUtils.parseUri(uri)
assertEquals("/C:/Unity Projects/My Project/file.txt", parsed.path)
}

fun `test Windows path with square brackets`() {
val uri = "file://C:/Users/user/projects/[gamemode]/file.lua"
val parsed = UriUtils.parseUri(uri)
Expand Down
Loading