diff --git a/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellFileInfo.kt b/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellFileInfo.kt new file mode 100644 index 000000000000..c9ff65c28c43 --- /dev/null +++ b/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellFileInfo.kt @@ -0,0 +1,15 @@ +package com.intellij.terminal.completion.spec + +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Experimental +@ApiStatus.NonExtendable +interface ShellFileInfo { + val name: String + val type: Type + + @ApiStatus.Experimental + enum class Type { + FILE, DIRECTORY, OTHER + } +} \ No newline at end of file diff --git a/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellRuntimeContext.kt b/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellRuntimeContext.kt index b4e19ffed077..b31116684822 100644 --- a/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellRuntimeContext.kt +++ b/plugins/terminal/completion/src/com/intellij/terminal/completion/spec/ShellRuntimeContext.kt @@ -27,6 +27,11 @@ interface ShellRuntimeContext { */ suspend fun runShellCommand(@Language("ShellScript") command: String): ShellCommandResult + /** + * @param path absolute os-dependent path to the directory. + */ + suspend fun listDirectoryFiles(path: String): List + /** * Used to implement custom extensions of [ShellRuntimeContext]. * See the extensions in this [file][org.jetbrains.plugins.terminal.block.completion.spec.getFileSuggestions] diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellCommandExecutorReworked.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellCommandExecutorReworked.kt index d239f459e33c..c1144c4c8319 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellCommandExecutorReworked.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellCommandExecutorReworked.kt @@ -15,9 +15,11 @@ import com.intellij.terminal.completion.spec.ShellCommandExecutor import com.intellij.terminal.completion.spec.ShellCommandResult import com.intellij.util.execution.ParametersListUtil import kotlinx.coroutines.coroutineScope +import org.jetbrains.annotations.ApiStatus import kotlin.coroutines.cancellation.CancellationException -internal class ShellCommandExecutorReworked(private val eelDescriptor: EelDescriptor) : ShellCommandExecutor { +@ApiStatus.Internal +class ShellCommandExecutorReworked(private val eelDescriptor: EelDescriptor) : ShellCommandExecutor { override suspend fun runShellCommand(directory: String, command: String): ShellCommandResult { val commandList = ParametersListUtil.parse(command) val commandName = commandList.firstOrNull() ?: return emptyResult() diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellFileSystemSupportImpl.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellFileSystemSupportImpl.kt new file mode 100644 index 000000000000..36351b891792 --- /dev/null +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellFileSystemSupportImpl.kt @@ -0,0 +1,54 @@ +package com.intellij.terminal.frontend.completion + +import com.intellij.openapi.diagnostic.logger +import com.intellij.platform.eel.EelDescriptor +import com.intellij.platform.eel.fs.EelFileInfo +import com.intellij.platform.eel.fs.listDirectoryWithAttrs +import com.intellij.platform.eel.getOrNull +import com.intellij.platform.eel.path.EelPath +import com.intellij.platform.eel.path.EelPathException +import com.intellij.terminal.completion.spec.ShellFileInfo +import kotlinx.coroutines.CancellationException +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellFileInfoImpl +import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellFileSystemSupport + +@ApiStatus.Internal +class ShellFileSystemSupportImpl(private val eelDescriptor: EelDescriptor) : ShellFileSystemSupport { + override suspend fun listDirectoryFiles(path: String): List { + val eelPath = try { + EelPath.parse(path, eelDescriptor) + } + catch (_: EelPathException) { + return emptyList() + } + + return try { + val eelApi = eelDescriptor.toEelApi() + val result = eelApi.fs.listDirectoryWithAttrs(eelPath).doNotResolve().eelIt() + val files = result.getOrNull() ?: emptyList() + files.map { (name, info) -> + ShellFileInfoImpl(name, info.type.toShellFileType()) + } + } + catch (e: CancellationException) { + throw e + } + catch (e: Exception) { + LOG.error("Failed to get child files for path: $path", e) + emptyList() + } + } + + private fun EelFileInfo.Type.toShellFileType(): ShellFileInfo.Type { + return when (this) { + is EelFileInfo.Type.Regular -> ShellFileInfo.Type.FILE + is EelFileInfo.Type.Directory -> ShellFileInfo.Type.DIRECTORY + else -> ShellFileInfo.Type.OTHER + } + } + + companion object { + private val LOG = logger() + } +} \ No newline at end of file diff --git a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellRuntimeContextProviderReworkedImpl.kt b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellRuntimeContextProviderReworkedImpl.kt index 3862bca0f80a..af152c449e93 100644 --- a/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellRuntimeContextProviderReworkedImpl.kt +++ b/plugins/terminal/frontend/src/com/intellij/terminal/frontend/completion/ShellRuntimeContextProviderReworkedImpl.kt @@ -5,7 +5,6 @@ import com.intellij.openapi.project.Project import com.intellij.platform.eel.EelDescriptor import com.intellij.terminal.completion.ShellRuntimeContextProvider import com.intellij.terminal.completion.spec.ShellRuntimeContext -import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.terminal.block.completion.TerminalCompletionUtil.toShellName import org.jetbrains.plugins.terminal.block.completion.spec.IS_REWORKED_KEY import org.jetbrains.plugins.terminal.block.completion.spec.PROJECT_KEY @@ -19,6 +18,7 @@ internal class ShellRuntimeContextProviderReworkedImpl( eelDescriptor: EelDescriptor, ) : ShellRuntimeContextProvider { private val shellCommandExecutor = ShellCommandExecutorReworked(eelDescriptor) + private val fileSystemSupport = ShellFileSystemSupportImpl(eelDescriptor) override fun getContext(typedPrefix: String): ShellRuntimeContext { return ShellRuntimeContextImpl( @@ -26,6 +26,7 @@ internal class ShellRuntimeContextProviderReworkedImpl( typedPrefix, ShellType.ZSH.toShellName(), shellCommandExecutor, + fileSystemSupport, ).apply { putUserData(PROJECT_KEY, project) putUserData(IS_REWORKED_KEY, true) diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellDataGenerators.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellDataGenerators.kt index 038b240e1169..7c9b74b9e36e 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellDataGenerators.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellDataGenerators.kt @@ -51,11 +51,12 @@ object ShellDataGenerators { replacementIndexDelta: Int = 0, ): List { val path = getParentPath(pathPrefix) - val files: List = context.getChildFiles(path, onlyDirectories) + val files: List = context.getChildFiles(path, onlyDirectories) val prefixReplacementIndex = path.length + (if (isStartWithQuote(context.typedPrefix)) 1 else 0) + replacementIndexDelta val suggestions = files.flatMap { - val type = if (it.endsWith(File.separatorChar)) ShellSuggestionType.FOLDER else ShellSuggestionType.FILE - val suggestion = ShellCompletionSuggestion(name = it, type = type, prefixReplacementIndex = prefixReplacementIndex) + val type = if (it.type == ShellFileInfo.Type.DIRECTORY) ShellSuggestionType.FOLDER else ShellSuggestionType.FILE + val name = it.name + if (type == ShellSuggestionType.FOLDER) File.separator else "" + val suggestion = ShellCompletionSuggestion(name, type = type, prefixReplacementIndex = prefixReplacementIndex) if (type == ShellSuggestionType.FILE) { listOf(suggestion) } @@ -63,7 +64,7 @@ object ShellDataGenerators { // Directory suggestion has a trailing file separator, but suggestion without it is also valid. // It is needed for the parser to consider it as a valid suggestion and not mark it as something unknown. val hiddenSuggestion = ShellCompletionSuggestion( - name = it.removeSuffix(File.separator), + name = it.name, type = ShellSuggestionType.FOLDER, prefixReplacementIndex = prefixReplacementIndex, isHidden = true diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellRuntimeContextExtensions.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellRuntimeContextExtensions.kt index 1e1b381d5bff..fe5826827de8 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellRuntimeContextExtensions.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/ShellRuntimeContextExtensions.kt @@ -5,10 +5,14 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.util.io.FileUtil +import com.intellij.terminal.completion.spec.ShellFileInfo import com.intellij.terminal.completion.spec.ShellRuntimeContext import org.jetbrains.annotations.ApiStatus +import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellFileInfoImpl import org.jetbrains.plugins.terminal.block.session.ShellIntegrationFunctions.GET_DIRECTORY_FILES import java.io.File +import java.nio.file.InvalidPathException +import kotlin.io.path.Path @get:ApiStatus.Experimental val ShellRuntimeContext.project: Project @@ -26,8 +30,8 @@ val IS_REWORKED_KEY: Key = Key.create("isReworked") /** * Returns the list of [path] child file names. - * [path] can be either an absolute path or relative path. - * In case of relative path, it is related to [ShellRuntimeContext.currentDirectory]. + * [path] can be either an absolute path, relative path or home-relative path like `~/abc`. + * In case of the relative path, it is related to [ShellRuntimeContext.currentDirectory]. * * Use [ShellDataGenerators.getParentPath] utility to get the right [path] from the user typed prefix. */ @@ -35,7 +39,7 @@ val IS_REWORKED_KEY: Key = Key.create("isReworked") suspend fun ShellRuntimeContext.getChildFiles( path: String, onlyDirectories: Boolean = false, -): List { +): List { if (isReworkedTerminal) { return getChildFilesReworked(path, onlyDirectories) } @@ -44,26 +48,30 @@ suspend fun ShellRuntimeContext.getChildFiles( } } -private suspend fun ShellRuntimeContext.getChildFilesReworked(path: String, onlyDirectories: Boolean): List { +private suspend fun ShellRuntimeContext.getChildFilesReworked(path: String, onlyDirectories: Boolean): List { check(isReworkedTerminal) - val adjustedPath = FileUtil.expandUserHome(path.ifEmpty { "." }) - val command = "ls -1ap $adjustedPath" - val result = runShellCommand(command) - if (result.exitCode != 0) { - // it is a regular case: the user entered an invalid path + val adjustedPath = FileUtil.expandUserHome(path) + val absolutePath = try { + val nioPath = Path(adjustedPath) + if (nioPath.isAbsolute) { + nioPath.toString() + } + else { + Path(currentDirectory, path).toString() + } + } + catch (_: InvalidPathException) { return emptyList() } - val separator = File.separatorChar - return result.output.splitToSequence("\n") - .filter { it.isNotBlank() } - .filter { !onlyDirectories || it.endsWith(separator) } - // do not suggest './' and '../' choices, there is no need to show them in the completion popup. - .filter { it != ".$separator" && it != "..$separator" } + + val files = listDirectoryFiles(absolutePath) + return files.asSequence() + .filter { !onlyDirectories || it.type == ShellFileInfo.Type.DIRECTORY } .toList() } -private suspend fun ShellRuntimeContext.getChildFilesExp(path: String, onlyDirectories: Boolean): List { +private suspend fun ShellRuntimeContext.getChildFilesExp(path: String, onlyDirectories: Boolean): List { check(!isReworkedTerminal) val adjustedPath = path.ifEmpty { "." } @@ -79,5 +87,9 @@ private suspend fun ShellRuntimeContext.getChildFilesExp(path: String, onlyDirec .filter { !onlyDirectories || it.endsWith(separator) } // do not suggest './' and '../' directories if the user already typed some path .filter { path.isEmpty() || (it != ".$separator" && it != "..$separator") } + .map { + val type = if (it.endsWith(separator)) ShellFileInfo.Type.DIRECTORY else ShellFileInfo.Type.FILE + ShellFileInfoImpl(it.removeSuffix(separator.toString()), type) + } .toList() } \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellFileInfoImpl.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellFileInfoImpl.kt new file mode 100644 index 000000000000..5f359e71c13a --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellFileInfoImpl.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.terminal.block.completion.spec.impl + +import com.intellij.terminal.completion.spec.ShellFileInfo +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +data class ShellFileInfoImpl( + override val name: String, + override val type: ShellFileInfo.Type, +) : ShellFileInfo \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellFileSystemSupport.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellFileSystemSupport.kt new file mode 100644 index 000000000000..a1570f60941f --- /dev/null +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellFileSystemSupport.kt @@ -0,0 +1,13 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.plugins.terminal.block.completion.spec.impl + +import com.intellij.terminal.completion.spec.ShellFileInfo +import org.jetbrains.annotations.ApiStatus + +@ApiStatus.Internal +interface ShellFileSystemSupport { + /** + * @param path absolute os-dependent path to the directory. + */ + suspend fun listDirectoryFiles(path: String): List +} \ No newline at end of file diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellRuntimeContextImpl.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellRuntimeContextImpl.kt index 5a9776503741..5faf947a6949 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellRuntimeContextImpl.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/impl/ShellRuntimeContextImpl.kt @@ -2,10 +2,7 @@ package org.jetbrains.plugins.terminal.block.completion.spec.impl import com.intellij.openapi.util.UserDataHolderBase -import com.intellij.terminal.completion.spec.ShellCommandExecutor -import com.intellij.terminal.completion.spec.ShellCommandResult -import com.intellij.terminal.completion.spec.ShellName -import com.intellij.terminal.completion.spec.ShellRuntimeContext +import com.intellij.terminal.completion.spec.* import org.jetbrains.annotations.ApiStatus @ApiStatus.Internal @@ -14,12 +11,18 @@ class ShellRuntimeContextImpl( override val typedPrefix: String, override val shellName: ShellName, private val generatorCommandsRunner: ShellCommandExecutor, + private val fileSystemSupport: ShellFileSystemSupport? = null, ) : ShellRuntimeContext, UserDataHolderBase() { override suspend fun runShellCommand(command: String): ShellCommandResult { return generatorCommandsRunner.runShellCommand(currentDirectory, command) } + override suspend fun listDirectoryFiles(path: String): List { + return fileSystemSupport?.listDirectoryFiles(path) + ?: error("Supported only in Reworked Terminal") + } + override fun toString(): String { return "ShellRuntimeContextImpl(currentDirectory='$currentDirectory', typedPrefix='$typedPrefix')" } diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/specs/ShellCdCommandSpec.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/specs/ShellCdCommandSpec.kt index 3fa5e9f6dc3a..b996280bb203 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/specs/ShellCdCommandSpec.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/block/completion/spec/specs/ShellCdCommandSpec.kt @@ -24,9 +24,9 @@ internal fun cdCommandSpec() = ShellCommandSpec("cd") { val directories = context.getChildFiles(path, onlyDirectories = true) val prefixReplacementIndex = path.length + if (context.typedPrefix.startsWith('"')) 1 else 0 val suggestions = directories.flatMap { - val suggestion = ShellCompletionSuggestion(name = it, type = ShellSuggestionType.FOLDER, prefixReplacementIndex = prefixReplacementIndex) + val suggestion = ShellCompletionSuggestion(name = it.name + File.separator, type = ShellSuggestionType.FOLDER, prefixReplacementIndex = prefixReplacementIndex) val hiddenSuggestion = ShellCompletionSuggestion( - name = it.removeSuffix(File.separator), + name = it.name, type = ShellSuggestionType.FOLDER, prefixReplacementIndex = prefixReplacementIndex, isHidden = true diff --git a/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/completion/TerminalCompletionFilesCalculationTest.kt b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/completion/TerminalCompletionFilesCalculationTest.kt new file mode 100644 index 000000000000..e39f6a216f21 --- /dev/null +++ b/plugins/terminal/tests/src/com/intellij/terminal/tests/reworked/frontend/completion/TerminalCompletionFilesCalculationTest.kt @@ -0,0 +1,152 @@ +package com.intellij.terminal.tests.reworked.frontend.completion + +import com.intellij.openapi.util.Disposer +import com.intellij.platform.eel.provider.LocalEelDescriptor +import com.intellij.terminal.completion.spec.ShellFileInfo +import com.intellij.terminal.completion.spec.ShellFileInfo.Type.DIRECTORY +import com.intellij.terminal.completion.spec.ShellFileInfo.Type.FILE +import com.intellij.terminal.completion.spec.ShellName +import com.intellij.terminal.frontend.completion.ShellCommandExecutorReworked +import com.intellij.terminal.frontend.completion.ShellFileSystemSupportImpl +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.intellij.testFramework.utils.io.createDirectory +import com.intellij.testFramework.utils.io.createFile +import com.intellij.testFramework.utils.io.deleteRecursively +import com.intellij.util.SystemProperties +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.plugins.terminal.block.completion.spec.IS_REWORKED_KEY +import org.jetbrains.plugins.terminal.block.completion.spec.getChildFiles +import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellFileInfoImpl +import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellRuntimeContextImpl +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.io.File +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.createTempDirectory +import kotlin.io.path.deleteIfExists +import kotlin.io.path.name + +/** + * Test for [org.jetbrains.plugins.terminal.block.completion.spec.getChildFiles] in Reworked Terminal. + */ +@RunWith(JUnit4::class) +internal class TerminalCompletionFilesCalculationTest : BasePlatformTestCase() { + @Test + fun `get child files by absolute path`() { + val root = createTempDirStructure() + val actualFiles = getChildFiles(root.toString(), root.toString()) + val expectedFiles = listOf( + fileInfo("file1.txt", FILE), + fileInfo(".file2", FILE), + fileInfo("file with spaces", FILE), + fileInfo("dir1", DIRECTORY), + fileInfo("dir with spaces", DIRECTORY), + fileInfo("dir2", DIRECTORY), + ) + + assertThat(actualFiles).hasSameElementsAs(expectedFiles) + } + + @Test + fun `get child files by relative path`() { + val root = createTempDirStructure() + val actualFiles = getChildFiles(root.toString(), "") + val expectedFiles = listOf( + fileInfo("file1.txt", FILE), + fileInfo(".file2", FILE), + fileInfo("file with spaces", FILE), + fileInfo("dir1", DIRECTORY), + fileInfo("dir with spaces", DIRECTORY), + fileInfo("dir2", DIRECTORY), + ) + + assertThat(actualFiles).hasSameElementsAs(expectedFiles) + } + + @Test + fun `get child files from nested dir by a relative path`() { + val root = createTempDirStructure() + val actualFiles = getChildFiles(root.toString(), "dir2") + val expectedFiles = listOf( + fileInfo("nestedFile1.txt", FILE), + fileInfo(".nestedFile2", FILE), + fileInfo("nestedFile with spaces", FILE), + fileInfo("nestedDir", DIRECTORY), + ) + + assertThat(actualFiles).hasSameElementsAs(expectedFiles) + } + + @Test + fun `get child files from nested dir by a relative path with trailing path separator`() { + val root = createTempDirStructure() + val actualFiles = getChildFiles(root.toString(), "dir2" + File.separator) + val expectedFiles = listOf( + fileInfo("nestedFile1.txt", FILE), + fileInfo(".nestedFile2", FILE), + fileInfo("nestedFile with spaces", FILE), + fileInfo("nestedDir", DIRECTORY), + ) + + assertThat(actualFiles).hasSameElementsAs(expectedFiles) + } + + @Test + fun `get child files from home related path`() { + val tempFile = createTempFileInUserHome() + val actualFiles = getChildFiles("/", "~") + assertThat(actualFiles).contains(fileInfo(tempFile.name, FILE)) + } + + @Test + fun `get child files from home related path with trailing path separator`() { + val tempFile = createTempFileInUserHome() + val actualFiles = getChildFiles("/", "~" + File.separator) + assertThat(actualFiles).contains(fileInfo(tempFile.name, FILE)) + } + + private fun getChildFiles(currentDirectory: String, path: String): List = runBlocking { + val eelDescriptor = LocalEelDescriptor + val context = ShellRuntimeContextImpl( + currentDirectory, + typedPrefix = path, + ShellName("test"), + ShellCommandExecutorReworked(eelDescriptor), + ShellFileSystemSupportImpl(eelDescriptor) + ) + context.putUserData(IS_REWORKED_KEY, true) + context.getChildFiles(path) + } + + private fun createTempDirStructure(): Path { + val root = createTempDirectory("terminal-completion") + Disposer.register(testRootDisposable) { root.deleteRecursively() } + return root.apply { + createFile("file1.txt") + createFile(".file2") + createFile("file with spaces") + createDirectory("dir1") + createDirectory("dir with spaces") + createDirectory("dir2").apply { + createFile("nestedFile1.txt") + createFile(".nestedFile2") + createFile("nestedFile with spaces") + createDirectory("nestedDir") + } + } + } + + private fun createTempFileInUserHome(): Path { + val userHome = Path(SystemProperties.getUserHome()) + val tempFile = userHome.createFile("terminal-completion-test-file") + Disposer.register(testRootDisposable) { tempFile.deleteIfExists() } + return tempFile + } + + private fun fileInfo(name: String, type: ShellFileInfo.Type): ShellFileInfo { + return ShellFileInfoImpl(name, type) + } +} \ No newline at end of file