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
9 changes: 9 additions & 0 deletions examples/ted/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"profiles": {
"ted": {
"commandName": "Project",
"commandLineArgs": "examples/ted/TedApp.cs",
"workingDirectory": "../.."
}
}
}
15 changes: 15 additions & 0 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public TedApp (bool readOnly = false)
new ([
new Shortcut { Title = "Language", CommandView = LanguageShortcut },
new Shortcut { Title = "Theme", CommandView = ThemeDropDown },
OverwriteShortcut = new Shortcut (Key.Empty, "INS", null)
{ MouseHighlightStates = MouseState.None },
LocShortcut = new Shortcut (Key.Empty, FormatLoc (1, 1), null)
{ MouseHighlightStates = MouseState.None }
])
Expand Down Expand Up @@ -264,6 +266,7 @@ [new MenuItem ("_About", "About ted", ShowAboutDialog)]),
// Editor.CaretChanged covers both user-driven movement and document edits that shift the
// caret (insert/remove). Initial render seeds the value before any movement happens.
Editor.CaretChanged += (_, _) => UpdateLocShortcut ();
Editor.OverwriteModeChanged += (_, _) => UpdateOverwriteShortcut ();
Editor.FindRequested += (_, _) => ShowFindReplaceDialog (false);
Editor.ReplaceRequested += (_, _) => ShowFindReplaceDialog (true);
UpdateLocShortcut ();
Expand Down Expand Up @@ -293,6 +296,12 @@ [new MenuItem ("_About", "About ted", ShowAboutDialog)]),
/// </summary>
public Shortcut LocShortcut { get; }

/// <summary>
/// The status-bar shortcut that shows whether the editor is in insert (INS) or overwrite (OVR)
/// mode. Updated whenever <see cref="Editor.OverwriteModeChanged" /> fires.
/// </summary>
public Shortcut OverwriteShortcut { get; }

/// <summary>
/// Resolves the key shortcut for <paramref name="command" /> by asking the <see cref="Editor" />'s
/// <see cref="View.KeyBindings" /> first; falls back to <see cref="Application.GetDefaultKey" /> for
Expand Down Expand Up @@ -382,6 +391,12 @@ private void UpdateLocShortcut ()
LocShortcut.SetNeedsDraw ();
}

private void UpdateOverwriteShortcut ()
{
OverwriteShortcut.Title = Editor.OverwriteMode ? "OVR" : "INS";
OverwriteShortcut.SetNeedsDraw ();
}

private static string FormatLoc (int line, int column)
{
return $"Ln {line}, Col {column}";
Expand Down
67 changes: 67 additions & 0 deletions specs/overwrite-mode/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Overwrite (Insert-Replace) Mode

**Status**: Implemented
**Issue**: [#146](https://github.com/gui-cs/Editor/issues/146)
**Updated**: 2026-05-17

## Summary

`Editor` supports an overwrite mode: when active, typed characters replace the grapheme under
the caret instead of inserting before it. At line-end or when a selection is active, typing
still inserts. The mode is toggled via the Insert key and can be controlled programmatically.

## Public API

```csharp
public partial class Editor : View
{
/// Gets or sets whether the editor is in overwrite mode.
public bool OverwriteMode { get; set; }

/// Raised whenever OverwriteMode changes.
public event EventHandler? OverwriteModeChanged;
}
```

## Commands & Key Bindings

| Command | Default Key | Behaviour |
|----------------------------|-------------|------------------------------|
| `Command.ToggleOverwrite` | Insert | Toggles `OverwriteMode` |
| `Command.EnableOverwrite` | *(none)* | Sets `OverwriteMode = true` |
| `Command.DisableOverwrite` | *(none)* | Sets `OverwriteMode = false` |

All three are wired through `AddCommand` and the `ToggleOverwrite` binding lives in
`Editor.DefaultKeyBindings` (user-overridable via `[ConfigurationProperty]`).

## Typing Behaviour

- **Overwrite on, no selection, caret not at line-end**: the grapheme cluster at the caret
is replaced by the typed character. Uses `RemoveAndInsert` offset mapping so the caret
anchor advances past the inserted text. Wide-rune safe (uses
`StringInfo.GetNextTextElementLength`).
- **Overwrite on, selection active**: selection is replaced (same as insert mode).
- **Overwrite on, caret at line-end**: plain insert (newline is never consumed).
- **Multi-caret**: each additional caret follows the same overwrite logic.
- **Undo**: each overwrite is a single undo step.

## Caret Rendering

While `OverwriteMode` is active, the cursor style is forced to `CursorStyle.SteadyBlock`
(solid block), distinct from the default bar/underline style used in insert mode.

## ted Integration

The `ted` demo shows an **INS** / **OVR** indicator in the status bar, updated whenever
`OverwriteModeChanged` fires.

## Files Changed

- `src/Terminal.Gui.Editor/Editor.cs` — `OverwriteMode` property + `OverwriteModeChanged` event
- `src/Terminal.Gui.Editor/Editor.Commands.cs` — commands, key binding, `OverwriteAtOffset` helper
- `src/Terminal.Gui.Editor/Editor.Keyboard.cs` — overwrite path in `OnKeyDownNotHandled`
- `src/Terminal.Gui.Editor/Editor.Drawing.cs` — `SteadyBlock` cursor in overwrite mode
- `src/Terminal.Gui.Editor/Editor.MultiCaret.cs` — overwrite in multi-caret insert
- `examples/ted/TedApp.cs` — INS/OVR status bar indicator
- `specs/public-api.md` — updated with new property and event
- `tests/Terminal.Gui.Editor.IntegrationTests/EditorOverwriteTests.cs` — integration tests
2 changes: 2 additions & 0 deletions specs/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class Editor : View
public bool ShowLineNumbers { get; set; } // exists
public bool WordWrap { get; set; } // word-wrap-toggle (needs word-wrap)
public bool ReadOnly { get; set; } // exists (read-only ✅)
public bool OverwriteMode { get; set; } // exists (overwrite-mode ✅)
public event EventHandler? OverwriteModeChanged; // exists (overwrite-mode ✅)

// --- Indentation (tab-handling ✅ + auto-indent) ---
public int IndentationSize { get; set; } = 4; // exists (codex merge)
Expand Down
64 changes: 63 additions & 1 deletion src/Terminal.Gui.Editor/Editor.Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public partial class Editor
[Command.WordLeftExtend] = Bind.All (Key.CursorLeft.WithCtrl.WithShift),
[Command.WordRightExtend] = Bind.All (Key.CursorRight.WithCtrl.WithShift),
[Command.KillWordLeft] = Bind.All (Key.Backspace.WithCtrl),
[Command.KillWordRight] = Bind.All (Key.Delete.WithCtrl)
[Command.KillWordRight] = Bind.All (Key.Delete.WithCtrl),
[Command.ToggleOverwrite] = Bind.All (Key.InsertChar)
};

private void CreateCommandsAndBindings ()
Expand Down Expand Up @@ -244,6 +245,26 @@ private void CreateCommandsAndBindings ()
return true;
});

// Overwrite mode
AddCommand (Command.ToggleOverwrite, () =>
{
OverwriteMode = !OverwriteMode;

return true;
});
AddCommand (Command.EnableOverwrite, () =>
{
OverwriteMode = true;

return true;
});
AddCommand (Command.DisableOverwrite, () =>
{
OverwriteMode = false;

return true;
});

ApplyKeyBindings (View.DefaultKeyBindings, DefaultKeyBindings);

// Reclaim Tab / Shift+Tab from the framework's default focus-cycling bindings so our
Expand Down Expand Up @@ -350,6 +371,10 @@ private void CreateCommandsAndBindings ()
{
ReplaceSelection (text);
}
else if (OverwriteMode && _document is not null)
{
OverwriteAtCaret (text);
}
else
{
_document!.Insert (CaretOffset, text);
Expand All @@ -358,6 +383,43 @@ private void CreateCommandsAndBindings ()
return true;
}

/// <summary>
/// Overwrites the grapheme at the caret with <paramref name="text" />. If the caret is at
/// line-end, falls back to a plain insert so the newline is not consumed.
/// </summary>
private void OverwriteAtCaret (string text)
{
OverwriteAtOffset (CaretOffset, text);
}

/// <summary>
/// Overwrites the grapheme at the given <paramref name="offset" /> with <paramref name="text" />.
/// If the offset is at line-end, falls back to a plain insert so the newline is not consumed.
/// </summary>
private void OverwriteAtOffset (int offset, string text)
{
DocumentLine line = _document!.GetLineByOffset (offset);
var lineEnd = line.Offset + line.Length;

if (offset >= lineEnd)
{
// At or past end-of-line content — just insert.
_document.Insert (offset, text);

return;
}

// Determine the length of the grapheme cluster under the caret so wide runes are
// replaced atomically. StringInfo.GetNextTextElementLength gives cluster length in chars.
var remaining = _document.GetText (offset, lineEnd - offset);
var graphemeLength = System.Globalization.StringInfo.GetNextTextElementLength (remaining);

// Use RemoveAndInsert so that the caret anchor (AfterInsertion) moves past the
// inserted text. The default same-length Replace uses CharacterReplace mode which
// does not move anchors at all.
_document.Replace (offset, graphemeLength, text, OffsetChangeMappingType.RemoveAndInsert);
}

private bool? DeleteLeft ()
{
if (ReadOnly)
Expand Down
4 changes: 3 additions & 1 deletion src/Terminal.Gui.Editor/Editor.Drawing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,12 @@ private void UpdateCursor ()
}

Point screen = ViewportToScreen (new Point (col, row));
CursorStyle style = OverwriteMode ? CursorStyle.SteadyBlock :
Cursor.Style == CursorStyle.Hidden ? CursorStyle.Default : Cursor.Style;
Comment thread
tig marked this conversation as resolved.
Cursor = Cursor with
{
Position = screen,
Style = Cursor.Style == CursorStyle.Hidden ? CursorStyle.Default : Cursor.Style
Style = style
};
}
}
4 changes: 4 additions & 0 deletions src/Terminal.Gui.Editor/Editor.Keyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ protected override bool OnKeyDownNotHandled (Key key)
{
ReplaceSelection (rune.ToString ());
}
else if (OverwriteMode && _document is not null)
{
OverwriteAtCaret (rune.ToString ());
}
else
{
_document!.Insert (CaretOffset, rune.ToString ());
Expand Down
13 changes: 12 additions & 1 deletion src/Terminal.Gui.Editor/Editor.MultiCaret.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,10 @@ private void MultiCaretInsert (string text)
{
ReplaceSelection (text);
}
else if (OverwriteMode)
{
OverwriteAtCaret (text);
}
else
{
_document.Insert (CaretOffset, text);
Expand All @@ -329,7 +333,14 @@ private void MultiCaretInsert (string text)
}
}

_document.Insert (caret.Offset, text);
if (OverwriteMode)
{
OverwriteAtOffset (caret.Offset, text);
}
else
{
_document.Insert (caret.Offset, text);
}
}
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/Terminal.Gui.Editor/Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,30 @@ private void EnsureCaretNotInFold ()
}
}

/// <summary>
/// Gets or sets whether the editor is in overwrite mode. When <see langword="true" />, typed
/// characters replace the grapheme under the caret instead of inserting before it. At line-end
/// or when a selection is active, the insertion still inserts. Defaults to <see langword="false" />.
/// </summary>
public bool OverwriteMode
{
get;
set
{
if (field == value)
{
return;
}

field = value;
SetNeedsDraw ();
OverwriteModeChanged?.Invoke (this, EventArgs.Empty);
}
}

/// <summary>Raised whenever <see cref="OverwriteMode" /> changes.</summary>
public event EventHandler? OverwriteModeChanged;

/// <summary>Raised whenever <see cref="CaretOffset" /> changes.</summary>
public event EventHandler? CaretChanged;

Expand Down
Loading
Loading