diff --git a/plugins/git4idea/backend/src/actions/workingTree/GitWorkingTreeDialog.kt b/plugins/git4idea/backend/src/actions/workingTree/GitWorkingTreeDialog.kt index 212f327a5463..8fd6c9d81b91 100644 --- a/plugins/git4idea/backend/src/actions/workingTree/GitWorkingTreeDialog.kt +++ b/plugins/git4idea/backend/src/actions/workingTree/GitWorkingTreeDialog.kt @@ -1,9 +1,11 @@ // Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package git4idea.actions.workingTree +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.observable.properties.GraphProperty import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.whenDocumentChanged import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogWrapper @@ -32,6 +34,7 @@ import com.intellij.ui.layout.ValidationInfoBuilder import com.intellij.util.PathUtil.isValidFileName import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.containers.addIfNotNull +import com.intellij.util.textCompletion.TextFieldWithCompletion import com.intellij.util.ui.JBUI import com.intellij.vcs.git.ui.GitBranchesTreeIconProvider import com.intellij.vcsUtil.VcsUtil @@ -39,10 +42,13 @@ import git4idea.GitReference import git4idea.GitRemoteBranch import git4idea.GitStandardLocalBranch import git4idea.GitWorkingTree +import git4idea.branch.GitNewBranchDialog +import git4idea.branch.GitNewBranchDialog.Companion.cleanBranchNameAndAdjustCursorIfNeeded import git4idea.i18n.GitBundle import git4idea.repo.GitRepository import git4idea.validators.checkRefName import git4idea.repo.tags +import git4idea.validators.GitRefNameValidator import git4idea.workingTrees.GitWorkingTreesService import org.jetbrains.annotations.VisibleForTesting import java.awt.Dimension @@ -71,6 +77,7 @@ internal class GitWorkingTreeDialog( private lateinit var parentPathCell: Cell private lateinit var projectNameCell: Cell + private val validator = GitRefNameValidator.getInstance() private val existingRefWithWorkingTree: GraphProperty private val projectName: GraphProperty private val parentPath: GraphProperty @@ -109,6 +116,16 @@ internal class GitWorkingTreeDialog( } override fun createCenterPanel(): JComponent { + val newBranchNameField = TextFieldWithCompletion(data.project, createBranchNameCompletion(), "", true, true, false, false) + .apply { + minimumSize = JBUI.size(240, 0) + setupCleanBranchNameAndAdjustCursorIfNeeded() + whenDocumentChanged(disposable) { + newBranchName.set(text) + createNewBranch.set(text.isNotEmpty()) + } + } + return panel { row(GitBundle.message("working.tree.dialog.label.existing.branch")) { createRefComboBox() @@ -121,13 +138,11 @@ internal class GitWorkingTreeDialog( .bindSelected(createNewBranch) .gap(RightGap.SMALL) - textField() - .bindText(newBranchName) + cell(newBranchNameField) .align(Align.FILL) .validationRequestor(WHEN_GRAPH_PROPAGATION_FINISHED(propertyGraph)) .validationOnInput { validateNewBranchNameField() } .validationOnApply { validateNewBranchNameField() } - .enabledIf(createNewBranch) } .bottomGap(BottomGap.MEDIUM) .layout(RowLayout.LABEL_ALIGNED) @@ -294,6 +309,29 @@ internal class GitWorkingTreeDialog( } } + private fun createBranchNameCompletion(): GitNewBranchDialog.BranchNamesCompletion { + val branches = data.repository.branches + val localBranches = branches.localBranches.map { it.name } + val remoteBranches = branches.remoteBranches.map { it.nameForRemoteOperations } + val localDirectories = GitNewBranchDialog.collectDirectories(localBranches, true) + val remoteDirectories = GitNewBranchDialog.collectDirectories(remoteBranches, true) + val allSuggestions = buildSet { + addAll(localBranches) + addAll(remoteBranches) + addAll(localDirectories) + addAll(remoteDirectories) + } + return GitNewBranchDialog.BranchNamesCompletion(localDirectories.toList(), allSuggestions.toList()) + } + + private fun TextFieldWithCompletion.setupCleanBranchNameAndAdjustCursorIfNeeded() { + whenDocumentChanged(disposable) { + invokeLater { + cleanBranchNameAndAdjustCursorIfNeeded(validator) + } + } + } + private class RefWithTreeCellRenderer(project: Project, repository: GitRepository) : ColoredListCellRenderer() { diff --git a/plugins/git4idea/backend/src/branch/GitNewBranchDialog.kt b/plugins/git4idea/backend/src/branch/GitNewBranchDialog.kt index 5d4e7e949a86..54a700376507 100644 --- a/plugins/git4idea/backend/src/branch/GitNewBranchDialog.kt +++ b/plugins/git4idea/backend/src/branch/GitNewBranchDialog.kt @@ -106,6 +106,26 @@ internal class GitNewBranchDialog @JvmOverloads constructor( } return directories } + + internal fun TextFieldWithCompletion.cleanBranchNameAndAdjustCursorIfNeeded(validator: GitRefNameValidator) { + val caretModel = caretModel ?: return + + val initialText = text + val initialCaret = caretModel.offset + + val fixedText = validator.cleanUpBranchNameOnTyping(initialText) + + // if the text didn't change, there's no point in updating it or cursorPosition + if (fixedText == initialText) return + + val initialTextBeforeCaret = initialText.take(initialCaret) + val fixedTextBeforeCaret = validator.cleanUpBranchNameOnTyping(initialTextBeforeCaret) + + val fixedCaret = fixedTextBeforeCaret.length + + text = fixedText + caretModel.moveToOffset(fixedCaret) + } } private var checkout = true @@ -280,31 +300,11 @@ internal class GitNewBranchDialog @JvmOverloads constructor( whenDocumentChanged(disposable) { // Do not change Document inside DocumentListener callback invokeLater { - cleanBranchNameAndAdjustCursorIfNeeded() + cleanBranchNameAndAdjustCursorIfNeeded(validator) } } } - private fun TextFieldWithCompletion.cleanBranchNameAndAdjustCursorIfNeeded() { - if (isDisposed) return - - val initialText = text - val initialCaret = caretModel.offset - - val fixedText = validator.cleanUpBranchNameOnTyping(initialText) - - // if the text didn't change, there's no point in updating it or cursorPosition - if (fixedText == initialText) return - - val initialTextBeforeCaret = initialText.take(initialCaret) - val fixedTextBeforeCaret = validator.cleanUpBranchNameOnTyping(initialTextBeforeCaret) - - val fixedCaret = fixedTextBeforeCaret.length - - text = fixedText - caretModel.moveToOffset(fixedCaret) - } - internal class BranchNamesCompletion( val localDirectories: List, val allSuggestions: List)