context: using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Diagnostics.CodeAnalysis;
  using System.Drawing;
  using System.IO;
  using System.Linq;
  using System.Reactive.Linq;
  using System.Reactive.Subjects;
  using System.Runtime.InteropServices;
  using System.Text;
  using System.Text.RegularExpressions;
  using System.Threading;
  using System.Threading.Tasks;
  using System.Windows.Forms;
  using GitCommands;
  using GitCommands.Config;
  using GitCommands.Git;
  using GitCommands.Git.Commands;
  using GitExtUtils;
  using GitExtUtils.GitUI;
  using GitExtUtils.GitUI.Theming;
  using GitUI.AutoCompletion;
  using GitUI.CommandsDialogs.CommitDialog;
  using GitUI.Editor;
  using GitUI.HelperDialogs;
  using GitUI.Hotkey;
  using GitUI.Properties;
  using GitUI.Script;
  using GitUI.SpellChecker;
  using GitUI.UserControls;
  using GitUIPluginInterfaces;
  using Microsoft;
  using Microsoft.VisualStudio.Threading;
  using ResourceManager;
  
  namespace GitUI.CommandsDialogs
  {
      public sealed partial class FormCommit : GitModuleForm
      {
          private const string fixupPrefix = "fixup!";
          private const string squashPrefix = "squash!";
  
          #region Translation
  
          private readonly TranslationString _amendCommit =
              new("You are about to rewrite history." + Environment.NewLine +
                                    "Only use amend if the commit is not published yet!" + Environment.NewLine +
                                    Environment.NewLine + "Do you want to continue?");
  
          private readonly TranslationString _amendCommitCaption = new("Amend commit");
  
          private readonly TranslationString _commitAndPush = new("Commit && &push");
  
          private readonly TranslationString _deleteFailed = new("Delete file failed");
  
          private readonly TranslationString _deleteSelectedFiles =
              new("Are you sure you want to delete the selected file(s)?");
  
          private readonly TranslationString _deleteSelectedFilesCaption = new("Delete");
  
          private readonly TranslationString _deleteUntrackedFiles =
              new("Are you sure you want to delete all untracked files?");
  
          private readonly TranslationString _deleteUntrackedFilesCaption =
              new("Delete untracked files.");
  
          private readonly TranslationString _enterCommitMessage = new("Please enter commit message");
          private readonly TranslationString _enterCommitMessageCaption = new("Commit message");
          private readonly TranslationString _commitMessageDisabled = new("Commit Message is requested during commit");
  
          private readonly TranslationString _enterCommitMessageHint = new("Enter commit message");
  
          private readonly TranslationString _mergeConflicts =
              new("There are unresolved merge conflicts, solve merge conflicts before committing.");
  
          private readonly TranslationString _mergeConflictsCaption = new("Merge conflicts");
  
          private readonly TranslationString _noFilesStagedAndConfirmAnEmptyMergeCommit =
              new("There are no files staged for this commit.\nAre you sure you want to commit?");
          private readonly TranslationString _noFilesStagedCommitAllFilteredUnstagedOption =
              new("Stage and commit the unstaged files that match your filter");
          private readonly TranslationString _noFilesStagedCommitAllUnstagedOption =
              new("Stage and commit all unstaged files");
          private readonly TranslationString _noFilesStagedMakeEmptyCommitOption =
              new("Make an empty commit");
          private readonly TranslationString _noFilesStagedCommitCaption =
              new("Confirm commit");
          private readonly TranslationString _noFilesStagedCommitInstructions =
              new("There aren't any changes in the staging area.\nHow do you want to proceed?");
  
          private readonly TranslationString _noStagedChanges = new("There are no staged changes");
          private readonly TranslationString _noUnstagedChanges = new("There are no unstaged changes");
  
          private readonly TranslationString _notOnBranch =
              new("This commit will be unreferenced when switching to another branch and can be lost." +
                                    Environment.NewLine + Environment.NewLine + "Do you want to continue?");
  
          private readonly TranslationString _onlyStageChunkOfSingleFileError =
              new("You can only use this option when selecting a single file");
  
          private readonly TranslationString _resetSelectedChangesText =
              new("Are you sure you want to reset all selected files?");
  
          private readonly TranslationString _resetStageChunkOfFileCaption = new("Unstage chunk of file");
          private readonly TranslationString _stageDetails = new("Stage Details");
          private readonly TranslationString _stageFiles = new("Stage {0} files");
          private readonly TranslationString _selectOnlyOneFile = new("You must have only one file selected.");
  
          private readonly TranslationString _stageAll = new("Stage all");
          private readonly TranslationString _stageFiltered = new("Stage filtered");
          private readonly TranslationString _unstageAll = new("Unstage all");
          private readonly TranslationString _unstageFiltered = new("Unstage filtered");
  
          private readonly TranslationString _addSelectionToCommitMessage = new("Add selection to commit message");
          private readonly TranslationString _formTitle = new("Commit to {0} ({1})");
  
          private readonly TranslationString _selectionFilterToolTip = new("Enter a regular expression to select unstaged files.");
          private readonly TranslationString _selectionFilterErrorToolTip = new("Error {0}");
  
          private readonly TranslationString _commitMsgFirstLineInvalid = new("First line of commit message contains too many characters."
              + Environment.NewLine + "Do you want to continue?");
  
          private readonly TranslationString _commitMsgLineInvalid = new("The following line of commit message contains too many characters:"
              + Environment.NewLine + Environment.NewLine + "{0}" + Environment.NewLine + Environment.NewLine + "Do you want to continue?");
  
          private readonly TranslationString _commitMsgSecondLineNotEmpty = new("Second line of commit message is not empty." + Environment.NewLine + "Do you want to continue?");
  
          private readonly TranslationString _commitMsgRegExNotMatched = new("Commit message does not match RegEx." + Environment.NewLine + "Do you want to continue?");
  
          private readonly TranslationString _commitValidationCaption = new("Commit validation");
  
          private readonly TranslationString _commitMessageSettings = new("Edit commit message templates and settings...");
  
          private readonly TranslationString _commitAuthorInfo = new("Author");
          private readonly TranslationString _commitCommitterInfo = new("Committer");
          private readonly TranslationString _commitCommitterToolTip = new("Click to change committer information.");
  
          private readonly TranslationString _modifyCommitMessageButtonToolTip
              = new("If you change the first line of the commit message, git will treat this commit as an ordinary commit," + Environment.NewLine
                                      + "i.e. it may no longer be a fixup or an autosquash commit.");
  
          private readonly TranslationString _templateNotFoundCaption = new("Template Error");
          private readonly TranslationString _templateNotFound = new($"Template not found: {{0}}.{Environment.NewLine}{Environment.NewLine}You can set your template:{Environment.NewLine}\t$ git config commit.template ./.git_commit_msg.txt{Environment.NewLine}You can unset the template:{Environment.NewLine}\t$ git config --unset commit.template");
          private readonly TranslationString _templateLoadErrorCaption = new("Template could not be loaded");
  
          private readonly TranslationString _skipWorktreeToolTip = new("Hide already tracked files that will change but that you don\'t want to commit."
              + Environment.NewLine + "Suitable for some config files modified locally.");
          private readonly TranslationString _assumeUnchangedToolTip = new("Tell git to not check the status of this file for performance benefits."
              + Environment.NewLine + "Use this feature when a file is big and never change."
              + Environment.NewLine + "Git will never check if the file has changed that will improve status check performance.");
          private readonly TranslationString _stopTrackingFail = new("Fail to stop tracking the file '{0}'.");
  
          private readonly TranslationString _statusBarBranchWithoutRemote = new("(remote not configured)");
          private readonly TranslationString _untrackedRemote = new("(untracked)");
          #endregion
  
          private event Action? OnStageAreaLoaded;
  
          private readonly ICommitTemplateManager _commitTemplateManager;
          private readonly GitRevision? _editedCommit;
          private readonly ToolStripMenuItem _addSelectionToCommitMessageToolStripMenuItem;
          private readonly AsyncLoader _unstagedLoader = new();
          private readonly bool _useFormCommitMessage = AppSettings.UseFormCommitMessage;
          private readonly CancellationTokenSequence _customDiffToolsSequence = new();
          private readonly CancellationTokenSequence _interactiveAddSequence = new();
          private readonly CancellationTokenSequence _viewChangesSequence = new();
          private readonly SplitterManager _splitterManager = new(new AppSettingsPath("CommitDialog"));
          private readonly Subject<string> _selectionFilterSubject = new();
          private readonly IFullPathResolver _fullPathResolver;
          private readonly List<string> _formattedLines = new();
  
          private CommitKind _commitKind;
          private FileStatusList? _currentFilesList;
          private bool _skipUpdate;
          private FileStatusItem? _currentItem;
          private bool _currentItemStaged;
          private readonly ICommitMessageManager _commitMessageManager;
          private string? _commitTemplate;
          private bool _isMergeCommit;
          private bool _shouldRescanChanges = true;
          private bool _shouldReloadCommitTemplates = true;
          private bool _bypassActivatedEventHandler;
          private bool _loadUnstagedOutputFirstTime = true;
          private bool _initialized;
          private IReadOnlyList<GitItemStatus>? _currentSelection;
          private int _alreadyLoadedTemplatesCount = -1;
          private EventHandler? _branchNameLabelOnClick;
  
          private CommitKind CommitKind
          {
              get => _commitKind;
              set
              {
                  _commitKind = value;
  
                  modifyCommitMessageButton.Visible = _useFormCommitMessage && CommitKind != CommitKind.Normal;
                  bool messageCanBeChanged = _useFormCommitMessage && CommitKind == CommitKind.Normal;
                  Message.Enabled = messageCanBeChanged;
                  commitMessageToolStripMenuItem.Enabled = messageCanBeChanged;
                  commitTemplatesToolStripMenuItem.Enabled = messageCanBeChanged;
              }
          }
  
          [Obsolete("For VS designer and translation test only. Do not remove.")]
  #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
          private FormCommit()
  #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
          {
              InitializeComponent();
          }
  
          public FormCommit(GitUICommands commands, CommitKind commitKind = CommitKind.Normal, GitRevision? editedCommit = null, string? commitMessage = null)
              : base(commands)
          {
              ThreadHelper.ThrowIfNotOnUIThread();
  
              _editedCommit = editedCommit;
  
              InitializeComponent();
  
              CommitAndPush.Text = _commitAndPush.Text;
  
              splitRight.Panel2MinSize = DpiUtil.Scale(100);
  
              _commitMessageManager = new CommitMessageManager(Module.WorkingDirGitDir, Module.CommitEncoding, commitMessage);
  
              Message.TextChanged += Message_TextChanged;
              Message.TextAssigned += Message_TextAssigned;
              Message.AddAutoCompleteProvider(new CommitAutoCompleteProvider(() => Module));
              _commitTemplateManager = new CommitTemplateManager(() => Module);
  
              SolveMergeconflicts.Font = new Font(SolveMergeconflicts.Font, FontStyle.Bold);
  
              StageInSuperproject.Visible = Module.SuperprojectModule is not null;
              StageInSuperproject.Checked = AppSettings.StageInSuperprojectAfterCommit;
              closeDialogAfterEachCommitToolStripMenuItem.Checked = AppSettings.CloseCommitDialogAfterCommit;
              closeDialogAfterAllFilesCommittedToolStripMenuItem.Checked = AppSettings.CloseCommitDialogAfterLastCommit;
              refreshDialogOnFormFocusToolStripMenuItem.Checked = AppSettings.RefreshArtificialCommitOnApplicationActivated;
              ShowOnlyMyMessagesToolStripMenuItem.Checked = AppSettings.CommitDialogShowOnlyMyMessages;
  
              Unstaged.SetNoFilesText(_noUnstagedChanges.Text);
              Unstaged.DisableSubmoduleMenuItemBold = true;
              Unstaged.FilterChanged += Unstaged_FilterChanged;
              Staged.FilterChanged += Staged_FilterChanged;
              Staged.SetNoFilesText(_noStagedChanges.Text);
              Staged.DisableSubmoduleMenuItemBold = true;
  
              ConfigureMessageBox();
  
              HotkeysEnabled = true;
              Hotkeys = HotkeySettingsManager.LoadHotkeys(HotkeySettingsName);
  
              stageToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.StageSelectedFile);
              openToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.OpenFile);
              openWithToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.OpenFileWith);
              editFileToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.EditFile);
              openWithDifftoolToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.OpenWithDifftool);
              stagedOpenDifftoolToolStripMenuItem9.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.OpenWithDifftool);
  
              stageSubmoduleToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.StageSelectedFile);
  
              stagedUnstageToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.UnStageSelectedFile);
              stagedOpenToolStripMenuItem7.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.OpenFile);
              stagedOpenWithToolStripMenuItem8.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.OpenFileWith);
              stagedEditFileToolStripMenuItem11.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.EditFile);
  
              SelectedDiff.AddContextMenuSeparator();
              _addSelectionToCommitMessageToolStripMenuItem = SelectedDiff.AddContextMenuEntry(_addSelectionToCommitMessage.Text, (s, e) => AddSelectionToCommitMessage());
              _addSelectionToCommitMessageToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.AddSelectionToCommitMessage);
              resetChanges.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.ResetSelectedFiles);
              stagedResetChanges.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.ResetSelectedFiles);
              deleteFileToolStripMenuItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.DeleteSelectedFiles);
              viewFileHistoryToolStripItem.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.ShowHistory);
              stagedFileHistoryToolStripMenuItem6.ShortcutKeyDisplayString = GetShortcutKeyDisplayString(Command.ShowHistory);
              fileTooltip.SetToolTip(modifyCommitMessageButton, _modifyCommitMessageButtonToolTip.Text);
              commitAuthorStatus.ToolTipText = _commitCommitterToolTip.Text;
              skipWorktreeToolStripMenuItem.ToolTipText = _skipWorktreeToolTip.Text;
              assumeUnchangedToolStripMenuItem.ToolTipText = _assumeUnchangedToolTip.Text;
              toolStageAllItem.ToolTipText = _stageAll.Text;
              toolUnstageAllItem.ToolTipText = _unstageAll.Text;
              stageToolStripMenuItem.Text = toolStageItem.Text;
              stageSubmoduleToolStripMenuItem.Text = toolStageItem.Text;
              stagedUnstageToolStripMenuItem.Text = toolUnstageItem.Text;
  
              toolAuthor.Control.PreviewKeyDown += (_, e) =>
              {
                  if (e.Alt)
                  {
                      e.IsInputKey = true;
                  }
              };
              _fullPathResolver = new FullPathResolver(() => Module.WorkingDir);
  
              /* If not changed, by default show "no sign commit" */
              if (gpgSignCommitToolStripComboBox.SelectedIndex == -1)
              {
                  gpgSignCommitToolStripComboBox.SelectedIndex = 0;
              }
  
              ((ToolStripDropDownMenu)commitTemplatesToolStripMenuItem.DropDown).ShowImageMargin = true;
              ((ToolStripDropDownMenu)commitTemplatesToolStripMenuItem.DropDown).ShowCheckMargin = false;
  
              ((ToolStripDropDownMenu)commitMessageToolStripMenuItem.DropDown).ShowImageMargin = false;
              ((ToolStripDropDownMenu)commitMessageToolStripMenuItem.DropDown).ShowCheckMargin = true;
  
              selectionFilter.Size = DpiUtil.Scale(selectionFilter.Size);
              toolStripStatusBranchIcon.Width = DpiUtil.Scale(toolStripStatusBranchIcon.Width);
  
              if (!Module.GitVersion.SupportStashStaged)
              {
                  flowCommitButtons.Controls.Remove(StashStaged);
              }
  
              SetVisibilityOfSelectionFilter(AppSettings.CommitDialogSelectionFilter);
              Reset.Visible = AppSettings.ShowResetAllChanges;
              ResetUnStaged.Visible = AppSettings.ShowResetWorkTreeChanges;
              CommitAndPush.Visible = AppSettings.ShowCommitAndPush;
              splitRight.Panel2MinSize = Math.Max(splitRight.Panel2MinSize, flowCommitButtons.PreferredSize.Height);
              splitRight.SplitterDistance = Math.Min(splitRight.SplitterDistance, splitRight.Height - splitRight.Panel2MinSize);
  
              SelectedDiff.EscapePressed += () => DialogResult = DialogResult.Cancel;
              SelectedDiff.TopScrollReached += FileViewer_TopScrollReached;
              SelectedDiff.BottomScrollReached += FileViewer_BottomScrollReached;
              SelectedDiff.LinePatchingBlocksUntilReload = true;
  
              SolveMergeconflicts.BackColor = OtherColors.MergeConflictsColor;
              SolveMergeconflicts.SetForeColorForBackColor();
  
              toolStripStatusBranchIcon.AdaptImageLightness();
  
              splitLeft.Panel1.BackColor = OtherColors.PanelBorderColor;
              splitLeft.Panel2.BackColor = OtherColors.PanelBorderColor;
              splitRight.Panel1.BackColor = OtherColors.PanelBorderColor;
              splitRight.Panel2.BackColor = OtherColors.PanelBorderColor;
  
              BackColor = OtherColors.BackgroundColor;
  
              WorkaroundPaddingIncreaseBug();
  
              InitializeComplete();
  
              // By calling this in the constructor, we prevent flickering caused by resizing the
              // form, for example when it is restored to maximised, but first drawn as a smaller window.
              RestorePosition();
  
              // TODO this code is very similar to code in FileStatusList
              _selectionFilterSubject
                  .Throttle(TimeSpan.FromMilliseconds(250))
                  .ObserveOn(SynchronizationContext.Current)
                  .Subscribe(
                      filterText =>
                      {
                          ThreadHelper.AssertOnUIThread();
  
                          var matchCount = 0;
                          try
                          {
                              matchCount = Unstaged.SetSelectionFilter(filterText);
                              selectionFilter.ToolTipText = _selectionFilterToolTip.Text;
                          }
                          catch (ArgumentException ae)
                          {
                              selectionFilter.ToolTipText = string.Format(_selectionFilterErrorToolTip.Text, ae.Message);
                          }
  
                          if (matchCount > 0)
                          {
                              AddToSelectionFilter();
                          }
  
                          void AddToSelectionFilter()
                          {
                              if (selectionFilter.Items.Cast<string>().Any(s => s == filterText))
                              {
                                  // Item is already in the list
                                  return;
                              }
  
                              const int SelectionFilterMaxLength = 10;
  
                              while (selectionFilter.Items.Count >= SelectionFilterMaxLength)
                              {
                                  // Remove the last item
                                  selectionFilter.Items.RemoveAt(SelectionFilterMaxLength - 1);
                              }
  
                              // Insert the next term at the start of the filter control
                              selectionFilter.Items.Insert(0, filterText);
                          }
                      });
  
              return;
  
              void ConfigureMessageBox()
              {
                  CommitKind = commitKind;
  
                  Message.WatermarkText = _useFormCommitMessage
                      ? _enterCommitMessageHint.Text
                      : _commitMessageDisabled.Text;
              }
  
              void WorkaroundPaddingIncreaseBug()
              {
                  Padding padding = new(1);
  
                  splitLeft.Panel1.Padding = padding;
                  splitLeft.Panel2.Padding = padding;
                  splitRight.Panel1.Padding = padding;
                  splitRight.Panel2.Padding = padding;
              }
          }
  
          protected override void Dispose(bool disposing)
          {
              if (disposing)
              {
                  _unstagedLoader.Dispose();
                  _customDiffToolsSequence.Dispose();
                  _interactiveAddSequence.Dispose();
                  _viewChangesSequence.Dispose();
                  components?.Dispose();
              }
  
              base.Dispose(disposing);
          }
  
          protected override void OnHandleCreated(EventArgs e)
          {
              const int TVM_SETEXTENDEDSTYLE = 0x1100 + 44;
              const int TVS_EX_DOUBLEBUFFER = 0x0004;
  
              SendMessage(Handle, TVM_SETEXTENDEDSTYLE, (IntPtr)TVS_EX_DOUBLEBUFFER, (IntPtr)TVS_EX_DOUBLEBUFFER);
              base.OnHandleCreated(e);
          }
  
          [DllImport("user32.dll")]
          private static extern IntPtr SendMessage(IntPtr hwnd, int msg, IntPtr wp, IntPtr lp);
  
          protected override void OnApplicationActivated()
          {
              if (!_bypassActivatedEventHandler && AppSettings.RefreshArtificialCommitOnApplicationActivated)
              {
                  RescanChanges();
              }
  
              base.OnApplicationActivated();
          }
  
          protected override void OnActivated(EventArgs e)
          {
              if (!_bypassActivatedEventHandler)
              {
                  UpdateAuthorInfo();
              }
  
              base.OnActivated(e);
          }
  
          protected override void OnFormClosed(FormClosedEventArgs e)
          {
              // Do not attempt to store again if the form has already been closed. Unfortunately, OnFormClosed is always called by Close.
              if (Visible)
              {
                  // Do not remember commit message of fixup or squash commits, since they have
                  // a special meaning, and can be dangerous if used inappropriately.
                  if (CommitKind == CommitKind.Normal)
                  {
                      _commitMessageManager.MergeOrCommitMessage = Message.Text;
                      _commitMessageManager.AmendState = Amend.Checked;
                  }
  
                  _splitterManager.SaveSplitters();
              }
  
              base.OnFormClosed(e);
          }
  
          protected override void OnLoad(EventArgs e)
          {
              showUntrackedFilesToolStripMenuItem.Checked = Module.EffectiveConfigFile.GetValue("status.showUntrackedFiles") != "no";
              MinimizeBox = Owner is null;
              LoadCustomDifftools();
  
              base.OnLoad(e);
  
              _splitterManager.AddSplitter(splitMain, nameof(splitMain));
              _splitterManager.AddSplitter(splitRight, nameof(splitRight));
              _splitterManager.AddSplitter(splitLeft, nameof(splitLeft));
              _splitterManager.RestoreSplitters();
  
              // Since #8849 and #8557 we have a geometry bug, which pushes the splitter up by 6px.
              // Account for this shift. This is a workaround at best.
              //
              // The problem is likely caused by 'splitRight.FixedPanel = FixedPanel.Panel2' fact, but other forms
              // have the same setting, and don't appear to suffer from the same bug.
              splitRight.SplitterDistance -= 6;
          }
  
          protected override void OnShown(EventArgs e)
          {
              if (!_initialized)
              {
                  Initialize();
              }
  
              UpdateAuthorInfo();
  
              string message;
  
              switch (CommitKind)
              {
                  case CommitKind.Fixup:
                      Validates.NotNull(_editedCommit);
                      message = TryAddPrefix(fixupPrefix, _editedCommit.Subject);
                      break;
                  case CommitKind.Squash:
                      Validates.NotNull(_editedCommit);
                      message = TryAddPrefix(squashPrefix, _editedCommit.Subject);
                      break;
                  default:
                      message = _commitMessageManager.MergeOrCommitMessage;
                      Amend.Checked = !_commitMessageManager.IsMergeCommit && _commitMessageManager.AmendState;
                      break;
              }
  
              if (_useFormCommitMessage && !string.IsNullOrEmpty(message))
              {
                  Message.Text = message; // initial assignment
              }
              else
              {
                  AssignCommitMessageFromTemplate();
              }
  
              base.OnShown(e);
  
              return;
  
              string TryAddPrefix(string prefix, string suffix)
              {
                  return suffix.StartsWith(prefix) ? suffix : $"{prefix} {suffix}";
              }
  
              void AssignCommitMessageFromTemplate()
              {
                  var text = "";
                  try
                  {
                      text = _commitTemplateManager.LoadGitCommitTemplate() ?? "";
                  }
                  catch (FileNotFoundException ex)
                  {
                      MessageBox.Show(this, string.Format(_templateNotFound.Text, ex.FileName),
                          _templateNotFoundCaption.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
                  }
                  catch (Exception ex)
                  {
                      MessageBox.Show(this, ex.Message,
                          _templateLoadErrorCaption.Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
                  }
  
                  Message.Text = text; // initial assignment
                  _commitTemplate = text;
              }
          }
  
          protected override void OnKeyUp(KeyEventArgs e)
          {
              switch (e.KeyData)
              {
                  case Keys.Control | Keys.Enter when !Message.ContainsFocus:
                      {
                          FocusCommitMessage();
                          e.Handled = true;
                          break;
                      }
  
                  case Keys.Control | Keys.P:
                  case Keys.Alt | Keys.Up:
                  case Keys.Alt | Keys.Left:
                      {
                          MoveSelection(-1);
                          e.Handled = true;
                          break;
                      }
  
                  case Keys.Control | Keys.N:
                  case Keys.Alt | Keys.Down:
                  case Keys.Alt | Keys.Right:
                      {
                          MoveSelection(+1);
                          e.Handled = true;
                          break;
                      }
              }
  
              base.OnKeyUp(e);
  
              return;
  
              void MoveSelection(int direction)
              {
                  var list = Message.Focused ? Staged : _currentFilesList;
                  if (list is null)
                  {
                      // If a user is keyboard-happy, we may receive KeyUp event before we have selected a file list control.
                      return;
                  }
  
                  _currentFilesList = list;
                  var itemsCount = list.AllItemsCount;
                  if (itemsCount != 0)
                  {
                      list.SelectedIndex = (list.SelectedIndex + direction + itemsCount) % itemsCount;
                  }
              }
          }
  
          protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
          {
              if (keyData == Keys.F5)
              {
                  RescanChanges();
                  return true;
              }
  
              return base.ProcessCmdKey(ref msg, keyData) // generic handling of this form's hotkeys (upstream)
                  || (!IsDesignMode && HotkeysEnabled && !GitExtensionsControl.IsTextEditKey(keyData, multiLine: true) // downstream
                      && SelectedDiff.ProcessHotkey(keyData));
          }
  
          public void LoadCustomDifftools()
          {
              List<CustomDiffMergeTool> menus = new()
              {
                  new(openWithDifftoolToolStripMenuItem, openWithDifftoolToolStripMenuItem_Click),
                  new(stagedOpenDifftoolToolStripMenuItem9, stagedOpenDifftoolToolStripMenuItem9_Click),
              };
  
              new CustomDiffMergeToolProvider().LoadCustomDiffMergeTools(Module, menus, components, isDiff: true, cancellationToken: _customDiffToolsSequence.Next());
          }
  
          private void FileViewer_TopScrollReached(object sender, EventArgs e)
          {
              var fileStatus = _currentItemStaged ? Staged : Unstaged;
              fileStatus.SelectPreviousVisibleItem();
              SelectedDiff.ScrollToBottom();
          }
  
          private void FileViewer_BottomScrollReached(object sender, EventArgs e)
          {
              var fileStatus = _currentItemStaged ? Staged : Unstaged;
              fileStatus.SelectNextVisibleItem();
              SelectedDiff.ScrollToTop();
          }
  
          #region Hotkey commands
  
          public static readonly string HotkeySettingsName = "Commit";
  
          internal enum Command
          {
              AddToGitIgnore = 0,
              DeleteSelectedFiles = 1,
              FocusUnstagedFiles = 2,
              FocusSelectedDiff = 3,
              FocusStagedFiles = 4,
              FocusCommitMessage = 5,
              ResetSelectedFiles = 6,
              StageSelectedFile = 7,
              UnStageSelectedFile = 8,
              ShowHistory = 9,
              ToggleSelectionFilter = 10,
              StageAll = 11,
              OpenWithDifftool = 12,
              OpenFile = 13,
              OpenFileWith = 14,
              EditFile = 15,
              AddSelectionToCommitMessage = 16,
              CreateBranch = 17
          }
  
          private string GetShortcutKeyDisplayString(Command cmd)
          {
              return GetShortcutKeys((int)cmd).ToShortcutKeyDisplayString();
          }
  
          private bool AddSelectionToCommitMessage()
          {
              if (!SelectedDiff.ContainsFocus)
              {
                  return false;
              }
  
              var selectedText = SelectedDiff.GetSelectedText();
              if (string.IsNullOrEmpty(selectedText))
              {
                  return false;
              }
  
              if (Message.SelectionLength == 0)
              {
                  selectedText += '\n';
              }
  
              int selectionStart = Message.SelectionStart;
  
              Message.SelectedText = selectedText;
  
              Message.SelectionStart = selectionStart + selectedText.Length;
  
              return true;
          }
  
          private bool AddToGitIgnore()
          {
              if (!Unstaged.Focused)
              {
                  return false;
              }
  
              AddFileToGitIgnoreToolStripMenuItemClick(this, EventArgs.Empty);
              return true;
          }
  
          private bool DeleteSelectedFiles()
          {
              if (!Unstaged.Focused)
              {
                  return false;
              }
  
              DeleteFileToolStripMenuItemClick(this, EventArgs.Empty);
              return true;
          }
  
          private bool FocusStagedFiles()
          {
              Staged.Focus();
              return true;
          }
  
          private bool FocusUnstagedFiles()
          {
              Unstaged.Focus();
              return true;
          }
  
          private bool FocusSelectedDiff()
          {
              SelectedDiff.Focus();
              return true;
          }
  
          private bool FocusCommitMessage()
          {
              Message.Focus();
              return true;
          }
  
          private bool ResetSelectedFiles()
          {
              if (!Unstaged.Focused && !Staged.Focused)
              {
                  return false;
              }
  
              ResetSoftClick(this, EventArgs.Empty);
              return true;
          }
  
          private bool StageSelectedFile()
          {
              if (!Unstaged.Focused)
              {
                  return false;
              }
  
              StageClick(this, EventArgs.Empty);
              return true;
          }
  
          private bool UnStageSelectedFile()
          {
              if (!Staged.Focused)
              {
                  return false;
              }
  
              UnstageFilesClick(this, EventArgs.Empty);
              return true;
          }
  
          private bool StageAllFiles()
          {
              if (Unstaged.IsEmpty)
              {
                  return false;
              }
  
              StageAllAccordingToFilter();
              return true;
          }
  
          private bool StartFileHistoryDialog()
          {
              if (!Unstaged.Focused && !Staged.Focused)
              {
                  return false;
              }
  
              Validates.NotNull(_currentFilesList);
  
              if (_currentFilesList.SelectedItem?.Item is not null)
              {
                  if ((!_currentFilesList.SelectedItem.Item.IsNew) && (!_currentFilesList.SelectedItem.Item.IsRenamed))
                  {
                      UICommands.StartFileHistoryDialog(this, _currentFilesList.SelectedItem.Item.Name);
                  }
              }
  
              return true;
          }
  
          private bool ToggleSelectionFilter()
          {
              var visible = !selectionFilterToolStripMenuItem.Checked;
              SetVisibilityOfSelectionFilter(visible);
              if (visible)
              {
                  selectionFilter.Focus();
              }
              else if (selectionFilter.Focused)
              {
                  Unstaged.Focus();
              }
  
              return true;
          }
  
          private void ShowOnlyMyMessagesToolStripMenuItem_CheckedChanged(object sender, EventArgs e)
          {
              AppSettings.CommitDialogShowOnlyMyMessages = ShowOnlyMyMessagesToolStripMenuItem.Checked;
          }
  
          protected override CommandStatus ExecuteCommand(int cmd)
          {
              switch ((Command)cmd)
              {
                  case Command.AddToGitIgnore: return AddToGitIgnore();
                  case Command.DeleteSelectedFiles: return DeleteSelectedFiles();
                  case Command.FocusStagedFiles: return FocusStagedFiles();
                  case Command.FocusUnstagedFiles: return FocusUnstagedFiles();
                  case Command.FocusSelectedDiff: return FocusSelectedDiff();
                  case Command.FocusCommitMessage: return FocusCommitMessage();
                  case Command.ResetSelectedFiles: return ResetSelectedFiles();
                  case Command.StageSelectedFile: return StageSelectedFile();
                  case Command.UnStageSelectedFile: return UnStageSelectedFile();
                  case Command.ShowHistory: return StartFileHistoryDialog();
                  case Command.ToggleSelectionFilter: return ToggleSelectionFilter();
                  case Command.StageAll: return StageAllFiles();
                  case Command.OpenWithDifftool: OpenWithDiffTool(); return true;
                  case Command.OpenFile: openToolStripMenuItem.PerformClick(); return true;
                  case Command.OpenFileWith: openWithToolStripMenuItem.PerformClick(); return true;
                  case Command.EditFile: editFileToolStripMenuItem.PerformClick(); return true;
                  case Command.AddSelectionToCommitMessage: return AddSelectionToCommitMessage();
                  case Command.CreateBranch: createBranchToolStripButton.PerformClick(); return true;
                  default: return base.ExecuteCommand(cmd);
              }
          }
  
          #endregion
  
          private void ComputeUnstagedFiles(Action<IReadOnlyList<GitItemStatus>> onComputed, bool doAsync)
          {
              IReadOnlyList<GitItemStatus> GetAllChangedFilesWithSubmodulesStatus()
              {
                  return Module.GetAllChangedFilesWithSubmodulesStatus(
                      !showIgnoredFilesToolStripMenuItem.Checked,
                      !showAssumeUnchangedFilesToolStripMenuItem.Checked,
                      !showSkipWorktreeFilesToolStripMenuItem.Checked,
                      showUntrackedFilesToolStripMenuItem.Checked ? UntrackedFilesMode.Default : UntrackedFilesMode.No);
              }
  
              if (doAsync)
              {
                  ThreadHelper.JoinableTaskFactory.RunAsync(() =>
                  {
                      return _unstagedLoader.LoadAsync(GetAllChangedFilesWithSubmodulesStatus, onComputed);
                  });
              }
              else
              {
                  _unstagedLoader.Cancel();
                  onComputed(GetAllChangedFilesWithSubmodulesStatus());
              }
          }
  
          public void ShowDialogWhenChanges(IWin32Window? owner = null)
          {
              ComputeUnstagedFiles(allChangedFiles =>
                  {
                      if (allChangedFiles.Count > 0)
                      {
                          LoadUnstagedOutput(allChangedFiles);
                          Initialize(loadUnstaged: false);
                          ShowDialog(owner);
                      }
                      else
                      {
                          Close();
                      }
  
                      Loading.IsAnimating = false;
                  }, false);
          }
  
          private void EnableStageButtons(bool enable)
          {
              toolUnstageItem.Enabled = enable;
              toolUnstageAllItem.Enabled = enable;
              toolStageItem.Enabled = enable;
              toolStageAllItem.Enabled = enable;
              workingToolStripMenuItem.Enabled = enable;
              ResetUnStaged.Enabled = Unstaged.AllItems.Any();
          }
  
          private async Task UpdateBranchNameDisplayAsync()
          {
              await TaskScheduler.Default;
  
  
gt:             var currentBranchName = Module.GetSelectedBranch();