| 1 | #nullable enable |
| 2 | using System.Collections.ObjectModel; |
| 3 | using CommunityToolkit.Mvvm.ComponentModel; |
| 4 | |
| 5 | namespace CascadeIDE.Features.Chat; |
| 6 | |
| 7 | public partial class ChatPanelViewModel |
| 8 | { |
| 9 | public ObservableCollection<ChatSlashSuggestionItem> ChatSlashSuggestions { get; } = []; |
| 10 | |
| 11 | [ObservableProperty] |
| 12 | private bool _isChatSlashAutocompleteVisible; |
| 13 | |
| 14 | [ObservableProperty] |
| 15 | private int _selectedChatSlashSuggestionIndex = -1; |
| 16 | |
| 17 | [ObservableProperty] |
| 18 | private string? _chatSlashPathPrefix; |
| 19 | |
| 20 | [ObservableProperty] |
| 21 | private string? _chatSlashNextStepLabel; |
| 22 | |
| 23 | [ObservableProperty] |
| 24 | private string? _chatSlashBreadcrumb; |
| 25 | |
| 26 | partial void OnChatInputChanged(string value) |
| 27 | { |
| 28 | RefreshComposerAutocomplete(); |
| 29 | RefreshComposerSlashPreview(); |
| 30 | } |
| 31 | |
| 32 | /// <param name="inputOverride">Текст из TextBox при <c>TextChanged</c> (биндинг может отставать на один тик).</param> |
| 33 | /// <param name="caretOverride">Каретка из Skia composer (приоритет над VM, если биндинг отстаёт).</param> |
| 34 | public void RefreshChatSlashAutocomplete(string? inputOverride = null, int? caretOverride = null) |
| 35 | { |
| 36 | var text = inputOverride ?? ChatInput; |
| 37 | var caret = Math.Clamp(caretOverride ?? ChatComposerCaretIndex, 0, text.Length); |
| 38 | var suggestions = ChatSlashAutocomplete.GetSuggestions( |
| 39 | text, |
| 40 | _workspaceFileSlashCompletion, |
| 41 | _sessionTopicSlashCompletion, |
| 42 | _messageAnchorSlashCompletion, |
| 43 | caretIndex: caret); |
| 44 | ChatSlashSuggestions.Clear(); |
| 45 | foreach (var s in suggestions) |
| 46 | ChatSlashSuggestions.Add(new ChatSlashSuggestionItem(s)); |
| 47 | |
| 48 | var visible = ChatSlashSuggestions.Count > 0; |
| 49 | if (IsChatSlashAutocompleteVisible != visible) |
| 50 | IsChatSlashAutocompleteVisible = visible; |
| 51 | else |
| 52 | OnPropertyChanged(nameof(IsChatSlashAutocompleteVisible)); |
| 53 | |
| 54 | SelectedChatSlashSuggestionIndex = visible ? 0 : -1; |
| 55 | |
| 56 | var hierarchy = ChatSlashAutocomplete.GetHierarchyContext(text, caret); |
| 57 | ChatSlashPathPrefix = hierarchy?.PathPrefix; |
| 58 | ChatSlashNextStepLabel = hierarchy?.NextStepLabel; |
| 59 | ChatSlashBreadcrumb = hierarchy?.Breadcrumb; |
| 60 | |
| 61 | rebuildComposerPopup(); |
| 62 | } |
| 63 | |
| 64 | public void MoveChatSlashSuggestionSelection(int delta) |
| 65 | { |
| 66 | if (ChatSlashSuggestions.Count == 0) |
| 67 | return; |
| 68 | |
| 69 | if (SelectedChatSlashSuggestionIndex < 0) |
| 70 | SelectedChatSlashSuggestionIndex = 0; |
| 71 | else |
| 72 | { |
| 73 | var next = SelectedChatSlashSuggestionIndex + delta; |
| 74 | if (next < 0) |
| 75 | next = ChatSlashSuggestions.Count - 1; |
| 76 | else if (next >= ChatSlashSuggestions.Count) |
| 77 | next = 0; |
| 78 | SelectedChatSlashSuggestionIndex = next; |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | public bool TryApplySelectedChatSlashSuggestion() => |
| 83 | TryCommitSelectedChatSlashSuggestion(out _); |
| 84 | |
| 85 | /// <summary>Подставить выбранную подсказку; <paramref name="shouldAutoExecute"/> — сразу отправить open/load.</summary> |
| 86 | public bool TryCommitSelectedChatSlashSuggestion(out bool shouldAutoExecute) |
| 87 | { |
| 88 | shouldAutoExecute = false; |
| 89 | if (ChatSlashSuggestions.Count == 0) |
| 90 | return false; |
| 91 | |
| 92 | var idx = SelectedChatSlashSuggestionIndex < 0 ? 0 : SelectedChatSlashSuggestionIndex; |
| 93 | if (idx >= ChatSlashSuggestions.Count) |
| 94 | idx = 0; |
| 95 | |
| 96 | var insertText = ChatSlashSuggestions[idx].InsertText; |
| 97 | if (IsCockpitCommandLineOpen) |
| 98 | { |
| 99 | CockpitCommandLineText = insertText; |
| 100 | CockpitCommandLineCaretIndex = insertText.Length; |
| 101 | } |
| 102 | else if (ChatSlashAutocomplete.TryReplaceSlashLineAtCaret( |
| 103 | ChatInput, |
| 104 | ChatComposerCaretIndex, |
| 105 | insertText, |
| 106 | out var newText, |
| 107 | out var newCaret)) |
| 108 | { |
| 109 | ChatInput = newText; |
| 110 | ChatComposerCaretIndex = newCaret; |
| 111 | } |
| 112 | else |
| 113 | { |
| 114 | ChatInput = insertText; |
| 115 | ChatComposerCaretIndex = insertText.Length; |
| 116 | } |
| 117 | |
| 118 | shouldAutoExecute = ChatSlashCommandParser.ShouldAutoExecuteAfterAutocompleteCommit(insertText); |
| 119 | if (shouldAutoExecute) |
| 120 | { |
| 121 | IsChatSlashAutocompleteVisible = false; |
| 122 | } |
| 123 | else |
| 124 | { |
| 125 | // Не гасить popup после сегмента: иначе следующий Enter уходит в Send вместо «start» (ADR 0119 §6). |
| 126 | RefreshComposerAutocomplete( |
| 127 | inputOverride: IsCockpitCommandLineOpen ? CockpitCommandLineText : ChatInput, |
| 128 | caretOverride: IsCockpitCommandLineOpen ? CockpitCommandLineCaretIndex : ChatComposerCaretIndex); |
| 129 | } |
| 130 | |
| 131 | return true; |
| 132 | } |
| 133 | |
| 134 | public void DismissChatSlashAutocomplete() |
| 135 | { |
| 136 | IsChatSlashAutocompleteVisible = false; |
| 137 | ChatSlashPathPrefix = null; |
| 138 | ChatSlashNextStepLabel = null; |
| 139 | ChatSlashBreadcrumb = null; |
| 140 | } |
| 141 | } |
| 142 | |