Skip to content

Commit 9b77b52

Browse files
authored
fix: migrate FileUtils to VFS (#6866)
* fix: migrate FileUtils to VFS I replicated the original behavior, for example, writeFile should automatically create dirs + overwrite the file (if exists), readText normalizes line endings + limits the output to 10k chars, etc. All those details should be covered by unit test. Also, I removed some usages of UriUtils because if we treat "file URIs" as URLs, we can use refreshAndFindFileByUrl. It works, but we should keep that in mind. ### Extra: Manual testing (I did it myself on top of the unit tests, but I’m posting the instructions here in case someone wants to reproduce it) * writeFile - run Continue in agentic mode and ask smth like: "create scss/test.scss" * listDir (+ getFileStats) - ask: "list files in project root" * openFile/showFile - edit a rule using the toolbar (pen icon) * fileExists - is invoked on startup multiple times * readFile - is invoked on startup multiple times (put some files into .continue/*) * fix: remove params in FileUtils * fix: read from documents if possible
1 parent 4752d7a commit 9b77b52

File tree

3 files changed

+225
-110
lines changed

3 files changed

+225
-110
lines changed

extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/continue/IntelliJIde.kt

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class IntelliJIDE(
4747
) : IDE {
4848

4949
private val gitService = GitService(project, continuePluginService)
50-
private val fileUtils = FileUtils()
50+
private val fileUtils = FileUtils(project)
5151
private val ripgrep: String = getRipgrepPath()
5252

5353
init {
@@ -183,7 +183,9 @@ class IntelliJIDE(
183183
fileUtils.fileExists(filepath)
184184

185185
override suspend fun writeFile(path: String, contents: String) =
186-
fileUtils.writeFile(path, contents)
186+
withContext(Dispatchers.EDT) {
187+
fileUtils.writeFile(path, contents)
188+
}
187189

188190
override suspend fun showVirtualFile(title: String, contents: String) {
189191
val virtualFile = LightVirtualFile(title, contents)
@@ -196,20 +198,8 @@ class IntelliJIDE(
196198
return getContinueGlobalPath()
197199
}
198200

199-
override suspend fun openFile(path: String) {
200-
// Convert URI path to absolute file path
201-
val filePath = UriUtils.uriToFile(path).absolutePath
202-
// Find the file using the absolute path
203-
val file = withContext(Dispatchers.IO) {
204-
LocalFileSystem.getInstance().refreshAndFindFileByPath(filePath)
205-
}
206-
207-
file?.let {
208-
ApplicationManager.getApplication().invokeLater {
209-
FileEditorManager.getInstance(project).openFile(it, true)
210-
}
211-
}
212-
}
201+
override suspend fun openFile(path: String) =
202+
fileUtils.openFile(path)
213203

214204
override suspend fun openUrl(url: String) {
215205
withContext(Dispatchers.IO) {
@@ -278,18 +268,10 @@ class IntelliJIDE(
278268
}
279269
}
280270

281-
override suspend fun saveFile(filepath: String) {
282-
ApplicationManager.getApplication().invokeLater {
283-
val file =
284-
LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(filepath).path) ?: return@invokeLater
285-
val fileDocumentManager = FileDocumentManager.getInstance()
286-
val document = fileDocumentManager.getDocument(file)
287-
288-
document?.let {
289-
fileDocumentManager.saveDocument(it)
290-
}
271+
override suspend fun saveFile(filepath: String) =
272+
withContext(Dispatchers.EDT) {
273+
fileUtils.saveFile(filepath)
291274
}
292-
}
293275

294276
override suspend fun readFile(filepath: String): String =
295277
fileUtils.readFile(filepath)
@@ -604,11 +586,8 @@ class IntelliJIDE(
604586
override suspend fun listDir(dir: String): List<List<Any>> =
605587
fileUtils.listDir(dir)
606588

607-
override suspend fun getFileStats(files: List<String>): Map<String, FileStats> {
608-
return files.associateWith { file ->
609-
FileStats(UriUtils.uriToFile(file).lastModified(), UriUtils.uriToFile(file).length())
610-
}
611-
}
589+
override suspend fun getFileStats(files: List<String>): Map<String, FileStats> =
590+
fileUtils.getFileStats(files)
612591

613592
override suspend fun gotoDefinition(location: Location): List<RangeInFile> {
614593
throw NotImplementedError("gotoDefinition not implemented yet")
Lines changed: 91 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,110 @@
11
package com.github.continuedev.continueintellijextension.`continue`.file
22

3+
import com.github.continuedev.continueintellijextension.FileStats
34
import com.github.continuedev.continueintellijextension.FileType
4-
import com.github.continuedev.continueintellijextension.`continue`.UriUtils
5-
import com.intellij.openapi.application.ApplicationManager
5+
import com.intellij.openapi.application.runReadAction
6+
import com.intellij.openapi.application.runWriteAction
67
import com.intellij.openapi.fileEditor.FileDocumentManager
7-
import com.intellij.openapi.vfs.LocalFileSystem
8-
import kotlinx.coroutines.Dispatchers
9-
import kotlinx.coroutines.withContext
10-
import java.io.FileInputStream
11-
import java.nio.charset.Charset
8+
import com.intellij.openapi.fileEditor.FileEditorManager
9+
import com.intellij.openapi.project.Project
10+
import com.intellij.openapi.util.TextRange
11+
import com.intellij.openapi.vfs.VfsUtil
12+
import com.intellij.openapi.vfs.VfsUtilCore
13+
import com.intellij.openapi.vfs.VirtualFile
14+
import com.intellij.openapi.vfs.VirtualFileManager
15+
import kotlin.math.min
1216

13-
class FileUtils {
14-
// todo: use VFS (it's moved from IntellijIde)
1517

16-
fun fileExists(uri: String): Boolean {
17-
val file = UriUtils.uriToFile(uri)
18-
return file.exists()
18+
class FileUtils(
19+
private val project: Project,
20+
) {
21+
fun fileExists(fileUri: String): Boolean =
22+
findFile(fileUri) != null
23+
24+
fun writeFile(fileUri: String, content: String) {
25+
val path = VfsUtilCore.urlToPath(fileUri)
26+
val pathDirectory = VfsUtil.getParentDir(path)
27+
?: return
28+
val vfsDirectory = VfsUtil.createDirectories(pathDirectory)
29+
val pathFilename = VfsUtil.extractFileName(path)
30+
?: return
31+
runWriteAction {
32+
val newFile = vfsDirectory.createChildData(this, pathFilename)
33+
VfsUtil.saveText(newFile, content)
34+
}
1935
}
2036

21-
fun writeFile(uri: String, contents: String) {
22-
val file = UriUtils.uriToFile(uri)
23-
file.parentFile?.mkdirs()
24-
file.writeText(contents)
37+
fun listDir(fileUri: String): List<List<Any>> {
38+
val found = findFile(fileUri)
39+
?: return emptyList()
40+
if (!found.isDirectory)
41+
return emptyList()
42+
return found.children.map { file ->
43+
val fileType = if (file.isDirectory)
44+
FileType.DIRECTORY.value
45+
else
46+
FileType.FILE.value
47+
listOf(file.name, fileType)
48+
}
2549
}
2650

27-
fun listDir(dir: String): List<List<Any>> {
28-
val files = UriUtils.uriToFile(dir).listFiles()?.map {
29-
listOf(it.name, if (it.isDirectory) FileType.DIRECTORY.value else FileType.FILE.value)
30-
} ?: emptyList()
51+
fun readFile(fileUri: String, maxLength: Int = 100_000): String {
52+
val found = findFile(fileUri)
53+
?: return ""
54+
val text = runReadAction {
55+
// note: document (if exists) is more up-to-date than VFS
56+
readDocument(found, maxLength) ?: VfsUtil.loadText(found, maxLength)
57+
}
58+
return normalizeLineEndings(text)
59+
}
3160

32-
return files
61+
fun openFile(fileUri: String) {
62+
val found = findFile(fileUri)
63+
?: return
64+
FileEditorManager.getInstance(project).openFile(found, true)
3365
}
3466

35-
fun readFile(uri: String): String {
36-
return try {
37-
val content = ApplicationManager.getApplication().runReadAction<String?> {
38-
val virtualFile = LocalFileSystem.getInstance().findFileByPath(UriUtils.parseUri(uri).path)
39-
if (virtualFile != null && FileDocumentManager.getInstance().isFileModified(virtualFile)) {
40-
return@runReadAction FileDocumentManager.getInstance().getDocument(virtualFile)?.text
41-
}
42-
return@runReadAction null
43-
}
67+
fun saveFile(fileUri: String) {
68+
val found = findFile(fileUri)
69+
?: return
70+
val manager = FileDocumentManager.getInstance()
71+
val document = manager.getDocument(found)
72+
?: return
73+
manager.saveDocument(document)
74+
}
4475

45-
if (content != null) {
46-
content
47-
} else {
48-
val file = UriUtils.uriToFile(uri)
49-
if (!file.exists() || file.isDirectory) return ""
50-
FileInputStream(file).use { fis ->
51-
val sizeToRead = minOf(100000, file.length()).toInt()
52-
val buffer = ByteArray(sizeToRead)
53-
val bytesRead = fis.read(buffer, 0, sizeToRead)
54-
if (bytesRead <= 0) return@use ""
55-
val content = String(buffer, 0, bytesRead, Charset.forName("UTF-8"))
56-
// Remove `\r` characters but preserve trailing newlines to prevent line count discrepancies
57-
val contentWithoutCR = content.replace("\r\n", "\n").replace("\r", "\n")
58-
contentWithoutCR
59-
}
60-
}
61-
} catch (e: Exception) {
62-
e.printStackTrace()
63-
""
64-
}
76+
fun getFileStats(fileUris: List<String>): Map<String, FileStats> =
77+
fileUris.mapNotNull { fileUri ->
78+
val file = findFile(fileUri)
79+
?: return@mapNotNull null
80+
fileUri to FileStats(file.timeStamp, file.length)
81+
}.toMap()
82+
83+
private fun findFile(fileUri: String): VirtualFile? {
84+
val noParams = fileUri.substringBefore("?")
85+
val normalizedAuthority = normalizeWindowsAuthority(noParams)
86+
return VirtualFileManager.getInstance()
87+
.refreshAndFindFileByUrl(normalizedAuthority)
88+
}
6589

90+
private fun readDocument(file: VirtualFile, maxLength: Int): String? {
91+
val document = FileDocumentManager.getInstance().getDocument(file)
92+
?: return ""
93+
val length = min(document.textLength, maxLength)
94+
return document.getText(TextRange(0, length))
6695
}
6796

97+
private fun normalizeLineEndings(text: String) =
98+
text.replace("\r\n", "\n")
99+
.replace("\r", "\n")
68100

101+
private fun normalizeWindowsAuthority(fileUri: String): String {
102+
val authorityPrefix = "file://"
103+
val noAuthorityPrefix = "file:///"
104+
if (fileUri.startsWith(authorityPrefix) && !fileUri.startsWith(noAuthorityPrefix)) {
105+
val path = fileUri.substringAfter(authorityPrefix)
106+
return "$noAuthorityPrefix$path"
107+
}
108+
return fileUri
109+
}
69110
}

0 commit comments

Comments
 (0)