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 9873b286808e..18f1731d8acf 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 @@ -1,10 +1,7 @@ // Copyright 2000-2024 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 -import com.intellij.terminal.completion.spec.ShellCommandSpec -import com.intellij.terminal.completion.spec.ShellCompletionSuggestion -import com.intellij.terminal.completion.spec.ShellRuntimeDataGenerator -import com.intellij.terminal.completion.spec.ShellSuggestionType +import com.intellij.terminal.completion.spec.* import org.jetbrains.annotations.ApiStatus import org.jetbrains.plugins.terminal.TerminalBundle import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellCommandSpecImpl @@ -32,9 +29,9 @@ object ShellDataGenerators { val key = if (onlyDirectories) "directories" else "files" return ShellRuntimeDataGenerator( debugName = key, - getCacheKey = { "$key:${getParentPath(it.typedPrefix)}" } + getCacheKey = { "$key:${getParentPath(it.typedPrefix, it.shellName)}" } ) { context -> - val path = getParentPath(context.typedPrefix) + val path = getParentPath(context.typedPrefix, context.shellName) val files: List = context.getChildFiles(path, onlyDirectories) val prefixReplacementIndex = path.length + if (context.typedPrefix.startsWith('"')) 1 else 0 val suggestions = files.flatMap { @@ -116,14 +113,22 @@ object ShellDataGenerators { * 1. `file.txt` -> `` * 2. `src/file.txt` -> `src/` * 3. `/usr/b` -> `/usr/` + * + * If [shellName] is PowerShell, then this function considers both slash `/` and backslash `\` as path separators. */ - fun getParentPath(typedPrefix: String): String { + fun getParentPath(typedPrefix: String, shellName: ShellName): String { val separator = File.separatorChar + val pathSeparators = if (shellName.isPowerShell()) { + val oppositeSeparator = if (separator == '/') '\\' else '/' + charArrayOf(separator, oppositeSeparator) + } + else charArrayOf(separator) // Remove possible quotes before and after // TODO: quotes should not be handled there, typed prefix should already contain no quotes. val adjustedPrefix = typedPrefix.removePrefix("\"").removeSuffix("'") - return if (adjustedPrefix.contains(separator)) { - adjustedPrefix.substringBeforeLast(separator) + separator + val lastSeparatorIndex = adjustedPrefix.lastIndexOfAny(pathSeparators) + return if (lastSeparatorIndex != -1) { + adjustedPrefix.substring(0, lastSeparatorIndex + 1) } else "" } 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..60716c5164b8 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 @@ -18,9 +18,9 @@ internal fun cdCommandSpec() = ShellCommandSpec("cd") { val generator = ShellRuntimeDataGenerator( debugName = "cd suggestions", - getCacheKey = { "cd suggestions:${getParentPath(it.typedPrefix)}" } + getCacheKey = { "cd suggestions:${getParentPath(it.typedPrefix, it.shellName)}" } ) { context -> - val path = getParentPath(context.typedPrefix) + val path = getParentPath(context.typedPrefix, context.shellName) val directories = context.getChildFiles(path, onlyDirectories = true) val prefixReplacementIndex = path.length + if (context.typedPrefix.startsWith('"')) 1 else 0 val suggestions = directories.flatMap { diff --git a/plugins/terminal/src/org/jetbrains/plugins/terminal/exp/completion/TerminalCommandSpecCompletionContributor.kt b/plugins/terminal/src/org/jetbrains/plugins/terminal/exp/completion/TerminalCommandSpecCompletionContributor.kt index d52905babe4c..72411d9112be 100644 --- a/plugins/terminal/src/org/jetbrains/plugins/terminal/exp/completion/TerminalCommandSpecCompletionContributor.kt +++ b/plugins/terminal/src/org/jetbrains/plugins/terminal/exp/completion/TerminalCommandSpecCompletionContributor.kt @@ -11,12 +11,13 @@ import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil import com.intellij.terminal.completion.ShellCommandSpecCompletion import com.intellij.terminal.completion.spec.ShellCompletionSuggestion +import com.intellij.terminal.completion.spec.ShellSuggestionType import org.jetbrains.plugins.terminal.action.TerminalCommandCompletionAction import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGenerators.availableCommandsGenerator import org.jetbrains.plugins.terminal.block.completion.spec.ShellDataGenerators.fileSuggestionsGenerator import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellDataGeneratorsExecutorImpl -import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellRuntimeContextProviderImpl import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellEnvBasedGenerators.aliasesGenerator +import org.jetbrains.plugins.terminal.block.completion.spec.impl.ShellRuntimeContextProviderImpl import org.jetbrains.plugins.terminal.exp.BlockTerminalSession import org.jetbrains.plugins.terminal.exp.TerminalDataContextUtils.terminalPromptModel import org.jetbrains.plugins.terminal.exp.completion.TerminalCompletionUtil.findIconForSuggestion @@ -66,7 +67,7 @@ internal class TerminalCommandSpecCompletionContributor : CompletionContributor( val prefix = allTokens.last().substring(prefixReplacementIndex) val resultSet = result.withPrefixMatcher(PlainPrefixMatcher(prefix, true)) - val elements = suggestions.map { it.toLookupElement() } + val elements = suggestions.map { it.toLookupElement(session.shellIntegration.shellType) } resultSet.addAllElements(elements) if (elements.isNotEmpty()) { @@ -131,7 +132,7 @@ internal class TerminalCommandSpecCompletionContributor : CompletionContributor( return (expandedTokens ?: completeTokens) + tokens.last() // add incomplete token to the end } - private fun ShellCompletionSuggestion.toLookupElement(): LookupElement { + private fun ShellCompletionSuggestion.toLookupElement(shellType: ShellType): LookupElement { val actualIcon = icon ?: findIconForSuggestion(name, type) val realInsertValue = insertValue?.replace("{cursor}", "") val nextSuggestions = getNextSuggestionsString(this).takeIf { it.isNotEmpty() } @@ -149,14 +150,27 @@ internal class TerminalCommandSpecCompletionContributor : CompletionContributor( .withPresentableText(displayName ?: name) .withTailText(nextSuggestions, true) .withIcon(actualIcon) - .withInsertHandler(MyInsertHandler(this, appendPathSeparator)) + .withInsertHandler(MyInsertHandler(this, appendPathSeparator, shellType)) val adjustedPriority = priority.coerceIn(0, 100) return PrioritizedLookupElement.withPriority(element, adjustedPriority / 100.0) } - private class MyInsertHandler(private val suggestion: ShellCompletionSuggestion, - private val appendPathSeparator: Boolean) : InsertHandler { + private class MyInsertHandler( + private val suggestion: ShellCompletionSuggestion, + private val appendPathSeparator: Boolean, + private val shellType: ShellType + ) : InsertHandler { override fun handleInsert(context: InsertionContext, item: LookupElement) { + // PowerShell consider both slash and backslash as valid path separators. + // But after suggestion insertion, it is replacing wrong path separators with OS path separators. + // Here we are emulating the same behavior. + if (shellType == ShellType.POWERSHELL && (suggestion.type == ShellSuggestionType.FOLDER || suggestion.type == ShellSuggestionType.FILE)) { + val pathStartOffset = context.startOffset - suggestion.prefixReplacementIndex + val pathText = context.document.immutableCharSequence.substring(pathStartOffset, context.tailOffset) + val wrongSeparator = if (File.separatorChar == '/') '\\' else '/' + val adjustedPathText = pathText.replace(wrongSeparator, File.separatorChar) + context.document.replaceString(pathStartOffset, context.tailOffset, adjustedPathText) + } if (appendPathSeparator) { val tailOffset = context.tailOffset context.document.insertString(tailOffset, File.separator)