Skip to content
Merged
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
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ What follows is how TuiCode implements it today.
- 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`.
- 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 resting text with no tab open is just `TuiCode`.

## Find & search (#33)

Expand Down Expand Up @@ -238,7 +238,7 @@ What follows is how TuiCode implements it today.
- **User grammar packages** live in `~/.tui/grammars/<name>/`, in VS Code extension layout: a `package.json` whose `contributes.languages` / `contributes.grammars` declare them, plus the JSON grammar files it points at. Copying a VS Code extension's folder works as-is (checked with Even Better TOML). `GrammarBundle.Load(fs, dir)` reads them through `IFileSystem` at startup (a restart picks up changes) **before** the bundled packages, and registration is first-wins, so a user package overrides a bundled grammar scope, language id or extension. Only JSON grammars load (TextMateSharp can't read plist `.tmLanguage` or YAML); `injectTo` grammars register but aren't injected, as with bundled ones. Anything unusable — no `package.json`, bad JSON or wrong types, a missing or non-JSON grammar — is skipped and described in `GrammarBundle.Problems`, which the Grammars settings pane shows (the first problem, and how many more). A user grammar is untrusted input to TextMateSharp, which throws for broken ones either on load (e.g. invalid JSON) or on the first lexed line (e.g. an invalid regex): `SyntaxHighlighter.CreateCache` and `LineTokenCache` catch both and leave the file plain rather than crash a draw.
- **Settings → Grammars** (`GrammarAssociationsView`) lists every association, built-in and user, filtered by pattern or grammar name. Enter opens `GrammarPickerView` for the row, Delete resets it to the default, and a filter that's a new pattern gets an `Add "…"` row first — for a bare word only when nothing else matches, since "Python" is likelier a grammar search than a file name. The row logic is the TG-free `GrammarAssociationRows`. Edits apply on Save: `WorkbenchHost.ApplyGrammarAssociations` updates the highlighter and re-infers every open tab.
- **`cg`** (Change grammar) opens the same picker for the active tab; `EditorTab.SetGrammar` pins that tab so later association changes leave it alone (`InferGrammar` skips it). It deliberately has **no default keybinding** — users who want one bind it themselves.
- The status bar shows the active tab's grammar after its path (`path • C#`, or `Plain Text`). `Workbench` refreshes both on `ActiveTabChanged` and `EditorGroup.GrammarChanged`, so switching tabs now also resets the message to that tab's path.
- The status bar shows the active tab's grammar at its right end (`C#`, or `Plain Text`). `Workbench` refreshes it and the resting path on `ActiveTabChanged` and `EditorGroup.GrammarChanged`.
- `EditorDrawBenchmarkTests.Syntax_colouring_keeps_drawing_a_large_file_fast` (Explicit): on a 50k-line C# file a frame costs the same ~14 ms with colours as without.

## About dialog (#107)
Expand Down Expand Up @@ -421,10 +421,11 @@ What follows is how TuiCode implements it today.
## Wiring

- `Program.cs` is the only DI consumer.
- `Workbench` (the root `Window`) wires cross-part events in its ctor: `explorer.FileActivated → editor.Open + tab.FocusContent + statusBar.SetMessage`, `editor.FileSaved → statusBar.SetMessage`. Add new cross-part wiring here.
- **Status messages (#592)** follow the style guide's rule through the library's `AppStatusBar`, which `StatusBarPart` hosts as `Bar`. Pick the kind at the call site: `ShowMessage` for a reply to what the user just did (goes at their next key or after 5 s), `ShowNews` for something that happened by itself (5 s; an error stays until a key after that), and `Severity.Error` for a failure, which never times out. Keep the `StatusMessage` news returns and `Withdraw` it when it stops being true (language server back, review refresh worked, cut cleared). Between messages the bar rests on the active tab's path (`StatusBarPart.SetResting`). `WorkbenchHost.OnAppKeyDown` calls `KeyPressed` after handling, skipping chord keys and keys whose command said something. *Show messages* (`sm`, Help menu, or clicking `⚠ n`) opens the library's `MessagesDialog` and clears `⚠ n`.
- `Workbench` (the root `Window`) wires cross-part events in its ctor: `explorer.FileActivated → editor.Open + tab.FocusContent`, `editor.FileSaved → statusBar.ShowMessage`. Add new cross-part wiring here.
- `WorkbenchHost` owns `IApplication`, the key intercept, and workbench-scoped command/keybinding registrations.
- Parts (`SidebarPart` / `EditorPart` / `StatusBarPart`) are thin layout slots over feature views.
- Logging is `Microsoft.Extensions.Logging`, with one provider, `LogFile` (#473). **Warning and above go to `~/.tui/logs/TuiCode.log` and count towards the status bar's `⚠ n`; Information and Debug aren't written anywhere.** So log a Warning only for something the user should know went wrong, naming the file and the reason. Each session starts a fresh log and keeps the last as `TuiCode.1.log`; the first write starts it, as does `Program.cs` before the UI. `sl` (*Show log*) opens it as an ordinary tab and clears the count, as does switching to that tab or clicking `⚠ n`; the host polls `LogFile.Unseen` every iteration, since warnings come from watcher threads too. The status bar is never used for diagnostics, with one exception: a `TuiCode.settings.json` that doesn't parse says so once at startup (`WorkbenchHost.SettingsErrorMessage`). A crash out of the main loop is logged, then the terminal restored, then the log's path printed (`Crash.Report`).
- Logging is `Microsoft.Extensions.Logging`, with one provider, `LogFile` (#473). **Warning and above go to `~/.tui/logs/TuiCode.log`, and into Messages and the status bar's `⚠ n` without showing on the bar; Information and Debug aren't written anywhere.** So log a Warning only for something the user should know went wrong, naming the file and the reason. Each session starts a fresh log and keeps the last as `TuiCode.1.log`; the first write starts it, as does `Program.cs` before the UI. `sl` (*Show log*) opens it as an ordinary tab. The host drains `LogFile.TakeWarnings` into `AppStatusBar.Record` every iteration, since warnings come from watcher threads too. The status bar is never used for diagnostics, with one exception: a `TuiCode.settings.json` that doesn't parse says so once at startup (`WorkbenchHost.SettingsErrorMessage`). A crash out of the main loop is logged, then the terminal restored, then the log's path printed (`Crash.Report`).

## Navigation

Expand Down Expand Up @@ -454,7 +455,7 @@ What follows is how TuiCode implements it today.

- `Languages/` is a hand-rolled LSP client: StreamJsonRpc is only partly AOT-safe. `JsonRpcConnection` is JSON-RPC over `Content-Length` framing, symmetric so the same class is the fake server in tests and in `--smoke-language-server`, which CI runs on every RID's AOT binary. Build JSON with `JsonNode`, and a `JsonArray` with its `params` constructor: a collection expression goes through the generic `Add<T>`, which only the AOT publish warns about.
- `LanguageServers` (owned by the host, read by `Workbench.Languages`) runs one `LanguageServer` per language for the open folder, or the file's own folder with none open. It starts on the first tab of that language, and stops on `Workbench.OpenFolder` and quit (`shutdown`, `exit`, then a kill after a second). A server that exits isn't restarted until the folder changes. Tabs are kept in step with full-text `didChange`s, debounced `ChangeDelay` and flushed before every request.
- Ready means initialized with no work-done progress in flight: csharp-ls reports loading the solution as progress. A server that reports none is ready after `QuietLoad`. The status bar shows the state after the language (`C# ◌ loading`, `C# ● ready`, `C# stopped`), and the install command in the message slot while the server isn't on `PATH`.
- Ready means initialized with no work-done progress in flight: csharp-ls reports loading the solution as progress. A server that reports none is ready after `QuietLoad`. The status bar shows the state after the language (`C# ◌ loading`, `C# ● ready`, `C# stopped`), and its trouble as news (`Workbench.LanguageTrouble`): the install command while the server isn't on `PATH`, or `… language server stopped: …` as an error. Each shows once and is withdrawn when the server recovers.
- **Settings › Language Servers** (#456, `LanguageServersView`) lists every highlighted language with its server and whether `CommandPath` finds it on `PATH`. Enter opens `LanguageServerDialog`: None, the language's `KnownLanguageServers`, or Custom, over the TG-free `LanguageServerForm`. Choices persist in `TuiCode.settings.json` under `LanguageServers`, keyed by grammar id, only where they differ from the default (`csharp-ls` for C#, none elsewhere). On Save, `LanguageServers.Configure` stops only the languages whose server changed and starts the new one for their open tabs. A server is picked by the tab's grammar, so `cg` changes it too.
- Host tests pass a `FakeLanguageServer` as the host's `languageServers`; with none (the default) no server ever starts, so other tests opening `.cs` files are unaffected.

Expand Down
1 change: 1 addition & 0 deletions src/TuiCode.Abstractions/CommandIds.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static class CommandIds
public const string ShowHelp = "workbench.action.showHelp";
public const string ShowDiagnostics = "workbench.action.showDiagnostics";
public const string ShowLog = "workbench.action.showLog";
public const string ShowMessages = "workbench.action.showMessages";
public const string ShowAbout = "workbench.action.showAbout";
public const string WatchTutorials = "workbench.action.watchTutorials";
public const string ShowDocumentInfo = "workbench.action.showDocumentInfo";
Expand Down
1 change: 1 addition & 0 deletions src/TuiCode.Abstractions/CommandMnemonics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ private static IReadOnlyDictionary<string, string> Build()
[CommandIds.SaveAll] = "saf",
[CommandIds.ShowDiagnostics] = "sd",
[CommandIds.ShowLog] = "sl",
[CommandIds.ShowMessages] = "sm",
[CommandIds.ShowAbout] = "tui",
[CommandIds.WatchTutorials] = "wt",
[CommandIds.New] = "nf",
Expand Down
18 changes: 11 additions & 7 deletions src/TuiCode.Workbench/Logging/LogFile.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Logging;
Expand All @@ -6,7 +7,7 @@ namespace TuiCode.Workbench.Logging;

/// <summary>
/// Writes Warning and above to <c>~/.tui/logs/TuiCode.log</c>, one file per session with the previous kept as
/// <c>TuiCode.1.log</c> (#473), and counts the warnings written since the log was last opened.
/// <c>TuiCode.1.log</c> (#473), and keeps each warning for the status bar's Messages.
/// </summary>
public sealed class LogFile : ILoggerProvider
{
Expand All @@ -18,7 +19,7 @@ public sealed class LogFile : ILoggerProvider
private readonly Func<DateTimeOffset> _now;
private readonly Lock _lock = new();
private bool _started;
private int _unseen;
private readonly ConcurrentQueue<string> _warnings = new();

public LogFile(IFileSystem fs, string directory, Func<DateTimeOffset>? now = null)
{
Expand All @@ -45,10 +46,13 @@ public string DisplayPath
}
}

/// <summary>Warnings written since the log was last opened.</summary>
public int Unseen => Volatile.Read(ref _unseen);

public void MarkSeen() => Interlocked.Exchange(ref _unseen, 0);
/// <summary>The warnings written since the last call, oldest first.</summary>
public IReadOnlyList<string> TakeWarnings()
{
var taken = new List<string>();
while (_warnings.TryDequeue(out var warning)) taken.Add(warning);
return taken;
}

/// <summary>Start this session's log, keeping the last one as <see cref="PreviousFileName"/>. The first write starts it too.</summary>
public void Start()
Expand Down Expand Up @@ -84,7 +88,7 @@ public void Write(LogLevel level, string message, Exception? exception)
{
}
}
Interlocked.Increment(ref _unseen);
_warnings.Enqueue(exception is null ? message : $"{message}: {exception.Message}");
}

internal static string Format(DateTimeOffset time, LogLevel level, string message, Exception? exception)
Expand Down
4 changes: 2 additions & 2 deletions src/TuiCode.Workbench/Menus/CommandMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ .. Enumerable.Range(1, 9).Select(CommandIds.FocusEditorByIndex),
[CommandIds.ToggleViewed] = 'v', [CommandIds.SubmitReview] = 's',

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

public static readonly (string Title, string[] Ids)[] Layout =
Expand Down Expand Up @@ -143,7 +143,7 @@ public static readonly (string Title, string[] Ids)[] Layout =
[
CommandIds.ShowHelp, CommandIds.ShowActions, CommandIds.ShowMnemonics,
Separator,
CommandIds.WatchTutorials, CommandIds.ShowDiagnostics, CommandIds.ShowLog, CommandIds.ShowAbout,
CommandIds.WatchTutorials, CommandIds.ShowDiagnostics, CommandIds.ShowLog, CommandIds.ShowMessages, CommandIds.ShowAbout,
]),
];

Expand Down
Loading
Loading