[terminal] IJPL-148594 Allow using both slash and backslash for file paths completion in PowerShell

This part fixes command spec based completion of file paths.

GitOrigin-RevId: 67a170fce03b729cc34cc33bd8bff49e0241924c
This commit is contained in:
Konstantin Hudyakov
2024-06-07 17:39:44 +00:00
committed by intellij-monorepo-bot
parent 08b258a65d
commit 3849073765
3 changed files with 36 additions and 17 deletions
@@ -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<String> = 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` -> `<empty>`
* 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 ""
}
@@ -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 {
@@ -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<LookupElement> {
private class MyInsertHandler(
private val suggestion: ShellCompletionSuggestion,
private val appendPathSeparator: Boolean,
private val shellType: ShellType
) : InsertHandler<LookupElement> {
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)