Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ What follows is how TuiCode implements it today.

## Help dialog (F1)

- `HelpView`'s left column, **Everywhere**, is onboarding, not a shortcut reference — the command palette (`Ctrl+E`) lists the commands that apply where you are with their keys, and Settings › Keyboard Shortcuts every one of them. It shows only how to find everything else (palette, mnemonics) plus the handful of keys needed in the first few minutes (open, new, save, close, find, quit). Don't add a row per feature; a new row has to displace one.
- The right column lists the keys of the place you pressed F1 (#406), captured in `WorkbenchHost.OpenHelp` before the dialog's scope is pushed. A diff has its own hand-picked rows (`DiffHelp`); the Explorer, the Find pane and the editor list every command in their scope (`ScopeHelp`), after a fixed list of keys that aren't commands (`ExplorerKeys`, `FindResultKeys`). The rows come from the live bindings and leave out an unbound or disabled command, as the palette does. The find bar has its own (`FindController.Help`), whose keys aren't rebindable, listing those of the field the keys were in. Review (`ReviewHelp`) and the tab strip (`TabsHelp`) list their fixed keys, then the commands among theirs that are bound; a new Review or tab-strip key adds a row to its column. The Usages tab has only fixed keys (`UsagesKeys`). A sidebar tab's column ends with the keys that show the other sidebar tabs and `Ctrl+PgUp`/`Ctrl+PgDn` (`SidebarTabKeys`), and a panel tab's with the panel's (`PanelTabKeys`). Below the width both need, it sits above Everywhere instead, and when the columns are taller than the screen they scroll with `↑ ↓ PgUp PgDn`.
- The status bar holds status, not key hints. Its default message (`TuiCode • F1 help`) and, while no file is open, the position slot's `Press F1 for help` both read the live `ShowHelp` binding (`WorkbenchHost.ApplyKeybindings`), so a rebind updates them and an unbound help key leaves just `TuiCode`.
- F1 (*Show keys*) opens the library's `KeysDialog`, titled *Help*, over a `KeySheet` TuiCode builds itself (`HelpSheet`), since the library's `KeySheet.For` reads its own command registry, not ours. It's hosted like any other modal: added to the workbench, its scope pushed through `LibraryScope`, which adapts the library's `IInputScope` to our stack. It closes on `Esc` only, and swallows every other key while it's open.
- **Everywhere** is every `Global` command that has a key and can run now (`IsEnabled`, as the palette filters), read from the live bindings when F1 opens, under its `CommandMenu.Layout` heading in menu order, then *Other* for bound commands with no menu item. A command with several keys is one row (`Ctrl+O, F5`). It shows the command's label, not its menu title. Editor-scoped commands are the editor column's, never Everywhere's. A key the place column already lists isn't repeated in Everywhere, so the find options, sidebar tab keys and tab-strip keys show once, beside the place. *Find in file* and *Replace in file* are listed only while there's a file to search (`CanRunNow`): with no tab their keys open the Find pane, which *Find globally* and *Replace globally* already list.
- The place column lists the keys of the place you pressed F1 (#406), captured in `WorkbenchHost.OpenHelp` before the dialog's scope is pushed. A diff has its own hand-picked rows (`DiffHelp`); the Explorer, the Find pane and the editor list every command in their scope (`ScopeHelp`), after a fixed list of keys that aren't commands (`ExplorerKeys`, `FindResultKeys`). The rows come from the live bindings and leave out an unbound or disabled command, as the palette does. The find bar has its own (`FindController.Help`), whose keys aren't rebindable, listing those of the field the keys were in. Review (`ReviewHelp`) and the tab strip (`TabsHelp`) list their fixed keys, then the commands among theirs that are bound; a new Review or tab-strip key adds a row to its column. The Usages tab has only fixed keys (`UsagesKeys`). A sidebar tab's column ends with the keys that show the other sidebar tabs and `Ctrl+PgUp`/`Ctrl+PgDn` (`SidebarTabKeys`), and a panel tab's with the panel's (`PanelTabKeys`). A place with none shows Everywhere alone. The library lays them out: side by side, Everywhere first; stacked with the place on top below the width both need; scrolling with `↑ ↓ PgUp PgDn` when taller than the screen.
- The status bar always ends with `Press F1 for help` (`StatusBarPart.SetHelpKey`), from the live `ShowHelp` binding (`WorkbenchHost.ApplyKeybindings`), so a rebind updates it and an unbound key leaves nothing there. The cursor position sits just before it, and the message on the left never replaces it; the default message is just `TuiCode`.

## Find & search (#33)

Expand Down
5 changes: 0 additions & 5 deletions src/TuiCode.Abstractions/CommandIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,6 @@ public static class CommandIds
public const string ScrollDiffPageLeft = "diff.action.scrollPageLeft";
public const string ScrollDiffPageRight = "diff.action.scrollPageRight";

public const string HelpClose = "help.action.close";
public const string HelpScrollUp = "help.action.scrollUp";
public const string HelpScrollDown = "help.action.scrollDown";
public const string HelpPageUp = "help.action.pageUp";
public const string HelpPageDown = "help.action.pageDown";
public const string DiagnosticsClose = "diagnostics.action.close";
public const string AboutClose = "about.action.close";
public const string DocumentInfoClose = "documentInfo.action.close";
Expand Down
9 changes: 5 additions & 4 deletions src/TuiCode.Workbench/Find/FindController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using MentalDesk.Tui.Keys;
using TuiCode.Abstractions;
using TuiCode.Editor;
using TuiCode.Search;
Expand Down Expand Up @@ -89,19 +90,19 @@ internal bool FocusInput() =>
_bar.ReplaceVisible && _bar.KeysInReplacement ? _bar.FocusReplacement() : _bar.FocusQuery();

/// <summary>The bar's keys for F1, for whichever field has them.</summary>
internal HelpColumn Help(IEnumerable<HelpRow> options)
internal KeyColumn Help(IEnumerable<KeyRow> options)
{
var inReplacement = _bar.ReplaceVisible && _bar.KeysInReplacement;
HelpRow[] replaceRows = [new("Ctrl+Enter", "Replace all"), new("Tab", inReplacement ? "Find field" : "Replace field")];
HelpRow[] rows =
KeyRow[] replaceRows = [new("Ctrl+Enter", "Replace all"), new("Tab", inReplacement ? "Find field" : "Replace field")];
KeyRow[] rows =
[
new("Enter", inReplacement ? "Replace and go to next" : "Next match"),
new("Shift+Enter", "Previous match"),
.. _bar.ReplaceVisible ? replaceRows : [],
.. options,
new("Esc", "Close"),
];
return new HelpColumn("Find bar", rows);
return HelpSheet.Place("Find bar", rows);
}

public void Close()
Expand Down
48 changes: 48 additions & 0 deletions src/TuiCode.Workbench/Help/HelpSheet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using MentalDesk.Tui.Keys;
using TuiCode.Abstractions;
using TuiCode.Workbench.Menus;

namespace TuiCode.Workbench.Help;

/// <summary>What F1 shows: the place's keys, and every other Global command that has a key and can run now under its menu's heading.</summary>
public static class HelpSheet
{
public static KeySheet For(KeyColumn? here, ICommandService commands, IKeybindingService keybindings, Func<string, bool>? canRun = null) =>
new(here, Everywhere(commands, keybindings, CommandMenu.Layout, canRun, here));

internal static KeyColumn Everywhere(ICommandService commands, IKeybindingService keybindings, IEnumerable<(string Title, string[] Ids)> menu,
Func<string, bool>? canRun = null, KeyColumn? here = null)
{
canRun ??= commands.IsEnabled;
var hereKeys = here is null ? [] : here.Rows().SelectMany(Keys).ToHashSet(StringComparer.Ordinal);
var bound = commands.Registered
.Where(command => command.Scope == CommandScope.Global && canRun(command.Id))
.Select(command => Row(command, keybindings))
.OfType<(string Id, KeyRow Row)>()
.Where(command => !Keys(command.Row).Any(hereKeys.Contains))
.ToList();
var rows = bound.ToDictionary(command => command.Id, command => command.Row, StringComparer.Ordinal);
var listed = new HashSet<string>(StringComparer.Ordinal);
var groups = new List<KeyGroup>();
foreach (var (title, ids) in menu)
{
KeyRow[] items = [.. ids.Where(id => rows.ContainsKey(id) && listed.Add(id)).Select(id => rows[id])];
if (items.Length > 0) groups.Add(new KeyGroup(title.Replace("_", "", StringComparison.Ordinal), items));
}
KeyRow[] other = [.. bound.Where(command => !listed.Contains(command.Id)).Select(command => command.Row)];
if (other.Length > 0) groups.Add(new KeyGroup(KeySheet.OtherHeading, other));
return new KeyColumn("Everywhere", groups);
}

public static KeyColumn Place(string heading, IEnumerable<KeyRow> rows) => new(heading, [new KeyGroup(null, [.. rows])]);

public static IReadOnlyList<KeyRow> Rows(this KeyColumn column) => [.. column.Groups.SelectMany(group => group.Rows)];

private static string[] Keys(KeyRow row) => row.Keys.Split(", ");

private static (string Id, KeyRow Row)? Row(CommandDescriptor command, IKeybindingService keybindings)
{
var keys = keybindings.Bindings.Where(binding => binding.CommandId == command.Id).Select(binding => binding.Display).ToList();
return keys.Count == 0 ? null : (command.Id, new KeyRow(string.Join(", ", keys), command.Label));
}
}
142 changes: 0 additions & 142 deletions src/TuiCode.Workbench/Help/HelpView.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/TuiCode.Workbench/Menus/CommandMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ .. Enumerable.Range(1, 9).Select(CommandIds.FocusEditorByIndex),
[CommandIds.OpenPullRequest] = 'o', [CommandIds.PullRequestOverview] = 'p', [CommandIds.CreateComment] = 'c',
[CommandIds.ToggleViewed] = 'v', [CommandIds.SubmitReview] = 's',

[CommandIds.ShowHelp] = 'g', [CommandIds.ShowActions] = 's', [CommandIds.ShowMnemonics] = 'm',
[CommandIds.ShowHelp] = 'k', [CommandIds.ShowActions] = 's', [CommandIds.ShowMnemonics] = 'm',
[CommandIds.WatchTutorials] = 't', [CommandIds.ShowDiagnostics] = 'd', [CommandIds.ShowLog] = 'l', [CommandIds.ShowAbout] = 'a',
};

Expand Down
37 changes: 19 additions & 18 deletions src/TuiCode.Workbench/Parts/StatusBarPart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public sealed class StatusBarPart : View
{
private readonly VerbatimLabel _label;
private readonly VerbatimLabel _position;
private readonly VerbatimLabel _help;
private readonly VerbatimLabel _focus;
private readonly Button _warnings;
private int _unseenWarnings;
Expand All @@ -20,7 +21,6 @@ public sealed class StatusBarPart : View
private string? _mode;
private (int Row, int Column)? _cursor;
private (int Carets, int? Selected) _selection = (1, null);
private string? _idleHint;

public StatusBarPart()
{
Expand All @@ -29,7 +29,9 @@ public StatusBarPart()
CanFocus = false;
SchemeName = "StatusBar";

_position = new VerbatimLabel { X = Pos.AnchorEnd() - 1, Y = 0 };
_help = new VerbatimLabel { X = Pos.AnchorEnd() - 1, Y = 0, Text = HelpHintFor("F1") };
_position = new VerbatimLabel { Y = 0 };
PlacePosition();
_focus = new VerbatimLabel { X = 1, Y = 0 };
_warnings = new Button
{
Expand Down Expand Up @@ -60,7 +62,7 @@ public StatusBarPart()
Width = Dim.Fill(2, _warnings),
Text = _message
};
Add(_focus, _label, _warnings, _position);
Add(_focus, _label, _warnings, _position, _help);
}

/// <summary>The <c>⚠ n</c> item was clicked.</summary>
Expand Down Expand Up @@ -106,18 +108,19 @@ public void SetError(string message)

internal string Message => _message;

/// <summary>What the bar says when nothing else has: the name and the help key, if it has one.</summary>
public string DefaultMessage { get; private set; } = DefaultMessageFor("F1");
/// <summary>What the bar says when nothing else has.</summary>
public string DefaultMessage => "TuiCode";

/// <summary>Name <paramref name="key"/> as the help key in the default message; null leaves the name alone.</summary>
/// <summary>Ends the bar with <c>Press {key} for help</c>; null leaves nothing there.</summary>
public void SetHelpKey(string? key)
{
var previous = DefaultMessage;
DefaultMessage = DefaultMessageFor(key);
if (_message == previous) SetMessage(DefaultMessage);
_help.Text = HelpHintFor(key);
PlacePosition();
}

private static string DefaultMessageFor(string? helpKey) => helpKey is null ? "TuiCode" : $"TuiCode • {helpKey} help";
private static string HelpHintFor(string? key) => key is null ? string.Empty : $"Press {key} for help";

internal string DisplayedHelp => _help.Text;

internal bool ShowsError => _label.SchemeName == "Error";

Expand Down Expand Up @@ -172,25 +175,23 @@ public void SetSelection(int carets, int? selected)
ShowPosition();
}

/// <summary>Shown in the position's place while no file is open; null for nothing.</summary>
public void SetIdleHint(string? hint)
{
_idleHint = hint;
ShowPosition();
}

private void ShowPosition()
{
if (_cursor is not var (row, column))
{
_position.Text = _idleHint ?? string.Empty;
_position.Text = string.Empty;
PlacePosition();
return;
}
var (carets, selected) = _selection;
var where = carets > 1 ? $"{carets} selections" : $"Ln {row + 1}, Col {column + 1}";
_position.Text = selected is { } count ? $"{where} ({count:N0} selected)" : where;
PlacePosition();
}

private void PlacePosition() =>
_position.X = Pos.Left(_help) - _position.Text.GetColumns(false) - (_help.Text.Length > 0 && _position.Text.Length > 0 ? 3 : 0);

internal string DisplayedText => _label.Text;

internal string DisplayedPosition => _position.Text;
Expand Down
Loading
Loading