diff --git a/Terminal.Gui.Editor.slnx b/Terminal.Gui.Editor.slnx
index 5f3c90a..9442a9d 100644
--- a/Terminal.Gui.Editor.slnx
+++ b/Terminal.Gui.Editor.slnx
@@ -29,6 +29,7 @@
+
diff --git a/examples/prompt/Program.cs b/examples/prompt/Program.cs
new file mode 100644
index 0000000..e56a657
--- /dev/null
+++ b/examples/prompt/Program.cs
@@ -0,0 +1,61 @@
+// Claude - claude-sonnet-4-5
+// prompt — single-line Editor example. Captures command-line text, lets the user edit it,
+// outputs to stdout on Enter, exits silently on Esc.
+
+using Terminal.Gui.App;
+using Terminal.Gui.Document;
+using Terminal.Gui.Editor;
+using Terminal.Gui.Input;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
+
+var initialText = string.Join (' ', args);
+string? result = null;
+
+using IApplication app = Application.Create ();
+app.AppModel = AppModel.Inline;
+app.Init ();
+
+Window window = new ()
+{
+ Title = "prompt",
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+};
+
+Editor editor = new ()
+{
+ Multiline = false,
+ X = 0,
+ Y = 0,
+ Width = Dim.Fill (),
+ Document = new TextDocument (initialText),
+ CaretOffset = initialText.Length
+};
+
+editor.Accepting += (_, e) =>
+{
+ result = editor.Document.Text;
+ window.RequestStop ();
+ e.Handled = true;
+};
+
+editor.KeyDown += (_, key) =>
+{
+ if (key != Key.Esc)
+ {
+ return;
+ }
+
+ window.RequestStop ();
+ key.Handled = true;
+};
+
+window.Add (editor);
+
+app.Run (window);
+
+if (result is not null)
+{
+ Console.WriteLine (result);
+}
diff --git a/examples/prompt/Properties/launchSettings.json b/examples/prompt/Properties/launchSettings.json
new file mode 100644
index 0000000..c9b6688
--- /dev/null
+++ b/examples/prompt/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "prompt": {
+ "commandName": "Project",
+ "commandLineArgs": "this is line 1\r\n\r\nthis is line 2"
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/prompt/prompt.csproj b/examples/prompt/prompt.csproj
new file mode 100644
index 0000000..ce9f324
--- /dev/null
+++ b/examples/prompt/prompt.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ Prompt
+ prompt
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/ted/ted.csproj b/examples/ted/ted.csproj
index 9086d64..d4728cc 100644
--- a/examples/ted/ted.csproj
+++ b/examples/ted/ted.csproj
@@ -7,6 +7,11 @@
false
+
+
+
+
+
diff --git a/specs/decisions.md b/specs/decisions.md
index 23c56a8..3377740 100644
--- a/specs/decisions.md
+++ b/specs/decisions.md
@@ -70,6 +70,18 @@ need to know the document construction details.
---
+### DEC-008: Single-line / embeddable-input mode (resolves former OPEN-006)
+
+**Decision**: **Yes** — `Editor` adds a `Multiline` property (default `true`) that enables a single-line / fixed-height input mode. When `false`, newlines are suppressed (Enter is a no-op), vertical navigation is constrained, WordWrap is forced off, and multi-caret is disabled. Follow-up properties `EnterKeyAddsLine` and `TabKeyAddsTab` (Enter raises `Accepting`, Tab traverses focus) are tracked separately in [#147](https://github.com/gui-cs/Editor/issues/147) and not yet implemented. Defaults preserve today's multi-line behavior exactly.
+
+**Rationale**: The earlier "tension" rested on the CLAUDE.md non-goal *"`Editor` ships beside `TextView`, not as a replacement."* Maintainer direction (2026-05-17): `Editor` **will** functionally replace `TextView` — just **not** in a source/API- or UI-compatible way. For *feature* purposes that dissolves the tension: a code-aware single-/few-line input (highlighted expression field, REPL line) is a capability `TextView` serves and `Editor` must therefore serve. The behavior is mostly binding-shaped (Enter/Tab semantics + an `Accepting` event + a height/scroll constraint), so the cost is low and the defaults are non-breaking.
+
+**Affected features**: see [`textview-parity-gap/spec.md`](textview-parity-gap/spec.md) Gap 3 (#147). Note: this "functionally replaces `TextView`" framing also reclassifies `IDesignable` (#151) from non-goal to a tracked gap and keeps single-line Enter/Tab as a real feature (not mere rebinding).
+
+**Date**: 2026-05-17
+
+---
+
### DEC-009: Completion item shape & provider interface
**Decision**: Use a fresh LSP-flavored `IEditorCompletionProvider` interface and `CompletionItem` type (`Terminal.Gui.Editor.Completion` namespace), **not** Terminal.Gui's existing `IAutocomplete` / `PopupAutocomplete`.
@@ -114,18 +126,6 @@ Resolved — see DEC-009.
---
-### DEC-008: Single-line / embeddable-input mode (resolves former OPEN-006)
-
-**Decision**: **Yes** — `Editor` adds a single-line / fixed-height input mode: `Multiline` (default `true`), `EnterKeyAddsLine` (default `true`; when `false`, Enter raises `Accepting` instead of inserting a newline), `TabKeyAddsTab` (default `true`; when `false`, Tab traverses focus). Defaults preserve today's multi-line behavior exactly. Tracked in [#147](https://github.com/gui-cs/Editor/issues/147).
-
-**Rationale**: The earlier "tension" rested on the CLAUDE.md non-goal *"`Editor` ships beside `TextView`, not as a replacement."* Maintainer direction (2026-05-17): `Editor` **will** functionally replace `TextView` — just **not** in a source/API- or UI-compatible way. For *feature* purposes that dissolves the tension: a code-aware single-/few-line input (highlighted expression field, REPL line) is a capability `TextView` serves and `Editor` must therefore serve. The behavior is mostly binding-shaped (Enter/Tab semantics + an `Accepting` event + a height/scroll constraint), so the cost is low and the defaults are non-breaking.
-
-**Affected features**: see [`textview-parity-gap/spec.md`](textview-parity-gap/spec.md) Gap 3 (#147). Note: this "functionally replaces `TextView`" framing also reclassifies `IDesignable` (#151) from non-goal to a tracked gap and keeps single-line Enter/Tab as a real feature (not mere rebinding).
-
-**Date**: 2026-05-17
-
----
-
### DEC-005: Word-wrap continuation-line indent policy
**Decision**: Continuation lines render flush at column 0 for v1 (no leading indent).
diff --git a/specs/public-api.md b/specs/public-api.md
index 7e27356..2becb5a 100644
--- a/specs/public-api.md
+++ b/specs/public-api.md
@@ -44,6 +44,7 @@ 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 Multiline { get; set; } = true; // single-line-mode (single-line ✅)
public bool OverwriteMode { get; set; } // exists (overwrite-mode ✅)
public event EventHandler? OverwriteModeChanged; // exists (overwrite-mode ✅)
@@ -175,6 +176,7 @@ public readonly record struct TextDocumentProgress (
| 2026-05-11 | ReadOnly property landed on Editor | read-only |
| 2026-05-12 | `ISearchStrategy?` `SearchStrategy { get; set; }` landed on Editor; string-based FindNext/FindPrevious/ReplaceNext/ReplaceAll overloads retained as convenience wrappers | find-and-replace |
| 2026-05-16 | Vertical multi-caret keybindings (`Ctrl+Alt+CursorUp/Down`, `Alt+Drag`) added via `Editor.DefaultKeyBindings`; no new public Editor API (R8) | vertical-multi-caret |
+| 2026-05-17 | `Multiline` property added (default `true`); single-line mode suppresses newlines, constrains vertical nav/scroll, forces WordWrap off, disables multi-caret | single-line-mode |
| 2026-05-17 | Column selection during `Alt+Drag` and `Ctrl+Shift+Alt+Arrow/Page` added without new public Editor API | vertical-multi-caret |
| 2026-05-17 | `IEditorCompletionProvider?` `CompletionProvider` + `bool IsCompletionActive` landed; `CompletionItem` sealed class; `Popover`-based popup; DEC-009 resolves OPEN-002 | completion |
| 2026-05-17 | Streaming `TextDocument.LoadAsync` / `TextDocument.SaveAsync`, `TextDocumentProgress`, `TextDocument.Encoding`, and delegating `Editor.LoadAsync` / `Editor.SaveAsync` landed | file-io |
diff --git a/specs/single-line-mode/spec.md b/specs/single-line-mode/spec.md
new file mode 100644
index 0000000..92c0162
--- /dev/null
+++ b/specs/single-line-mode/spec.md
@@ -0,0 +1,49 @@
+# Single-Line / Embeddable-Input Mode
+
+**Status**: Implemented
+**Issue**: [#147](https://github.com/gui-cs/Editor/issues/147)
+**Decision**: [DEC-008](../decisions.md#dec-008-single-line--embeddable-input-mode-resolves-former-open-006)
+
+## Summary
+
+`Editor` supports a single-line input mode via the `Multiline` property (default `true`).
+When `Multiline` is `false`, `Editor` behaves as a one-line text input suitable for embedding
+in dialogs, forms, and tool bars — with syntax highlighting, selection, and all horizontal
+editing intact.
+
+## Behavior when `Multiline == false`
+
+| Aspect | Behavior |
+|--------|----------|
+| **Newline insertion** | `Command.NewLine` (Enter) is a no-op; prevents adding new newlines. |
+| **Newline display** | Existing newlines in the document are preserved (no data loss) and rendered as visible glyphs (`⏎`). |
+| **Word wrap** | Forced off; setting `WordWrap = true` is silently ignored. |
+| **Vertical navigation** | `Up`, `Down`, `PageUp`, `PageDown` and their `*Extend` (Shift) variants are no-ops. |
+| **Vertical scroll** | `ScrollUp` / `ScrollDown` are no-ops. Content height is always 1. |
+| **Multi-caret** | `ToggleCaretAt`, `AddCaretVertically`, `SetVerticalCaretsFromViewRows` are no-ops. Existing additional carets are cleared on transition to `Multiline = false`. |
+| **Paste** | Newlines (`\r\n`, `\r`, `\n`) are stripped from pasted content before insertion. |
+| **Selection** | Works normally (horizontal). `SelectAll`, `Shift+Left/Right`, `Shift+Home/End` all function. |
+| **Editing** | Insert, delete, backspace, undo/redo, cut/copy/paste all work. |
+| **Horizontal navigation** | `Left`, `Right`, `Home`, `End`, word-left/right all work. |
+
+## Property
+
+```csharp
+///
+/// Gets or sets whether the editor supports multiple lines. Default is .
+///
+public bool Multiline { get; set; } = true;
+```
+
+Setting `Multiline` to `false`:
+1. Forces `WordWrap = false`.
+2. Clears any additional carets (`ClearAdditionalCarets`).
+3. Clears visual-line caches and recomputes content size (height = 1).
+4. Preserves existing document content (no data loss) — newlines are rendered as `⏎` glyphs.
+5. Home/End navigate to document start/end (since the visual representation is one row).
+
+## Not in scope (this phase)
+
+- `EnterKeyAddsLine` — when `false`, Enter raises `Accepting` instead of `Command.NewLine`.
+- `TabKeyAddsTab` — when `false`, Tab falls through to focus traversal.
+- These are tracked in [#147](https://github.com/gui-cs/Editor/issues/147) as follow-up work.
diff --git a/src/Terminal.Gui.Editor/Editor.Commands.cs b/src/Terminal.Gui.Editor/Editor.Commands.cs
index c720fd7..af00a5d 100644
--- a/src/Terminal.Gui.Editor/Editor.Commands.cs
+++ b/src/Terminal.Gui.Editor/Editor.Commands.cs
@@ -82,23 +82,31 @@ private void CreateCommandsAndBindings ()
// Selection-extending movement
AddCommand (Command.LeftExtend, () => ExtendCommand (() => ExtendCaretBy (-1)));
AddCommand (Command.RightExtend, () => ExtendCommand (() => ExtendCaretBy (1)));
- AddCommand (Command.UpExtend, () => ExtendCommand (() => ExtendCaretVertically (-1)));
- AddCommand (Command.DownExtend, () => ExtendCommand (() => ExtendCaretVertically (1)));
+ AddCommand (Command.UpExtend, () => Multiline ? ExtendCommand (() => ExtendCaretVertically (-1)) : true);
+ AddCommand (Command.DownExtend, () => Multiline ? ExtendCommand (() => ExtendCaretVertically (1)) : true);
AddCommand (Command.LeftStartExtend,
- () => ExtendCommand (() => ExtendCaretTo (_document!.GetLineByOffset (CaretOffset).Offset)));
+ () => ExtendCommand (() =>
+ ExtendCaretTo (!Multiline ? 0 : _document!.GetLineByOffset (CaretOffset).Offset)));
AddCommand (Command.RightEndExtend, () => ExtendCommand (() =>
{
- DocumentLine line = _document!.GetLineByOffset (CaretOffset);
- ExtendCaretTo (line.Offset + line.Length);
+ if (!Multiline)
+ {
+ ExtendCaretTo (_document!.TextLength);
+ }
+ else
+ {
+ DocumentLine line = _document!.GetLineByOffset (CaretOffset);
+ ExtendCaretTo (line.Offset + line.Length);
+ }
}));
AddCommand (Command.StartExtend, () => ExtendCommand (() => ExtendCaretTo (0)));
AddCommand (Command.EndExtend, () => ExtendCommand (() => ExtendCaretTo (_document!.TextLength)));
AddCommand (Command.PageUpExtend,
- () => ExtendCommand (() => ExtendCaretVertically (-Math.Max (1, Viewport.Height))));
+ () => Multiline ? ExtendCommand (() => ExtendCaretVertically (-Math.Max (1, Viewport.Height))) : true);
AddCommand (Command.PageDownExtend,
- () => ExtendCommand (() => ExtendCaretVertically (Math.Max (1, Viewport.Height))));
+ () => Multiline ? ExtendCommand (() => ExtendCaretVertically (Math.Max (1, Viewport.Height))) : true);
// Selection ops
AddCommand (Command.SelectAll, () =>
@@ -156,6 +164,13 @@ private void CreateCommandsAndBindings ()
return true;
}
+ // In single-line mode, strip newlines from pasted content so the document
+ // stays on one line.
+ if (!Multiline)
+ {
+ contents = contents.ReplaceLineEndings (string.Empty);
+ }
+
using (_document!.RunUpdate ())
{
if (HasSelection)
@@ -327,6 +342,11 @@ private void CreateCommandsAndBindings ()
private bool? MoveCaretVerticallyCollapsing (int delta)
{
+ if (!Multiline)
+ {
+ return true;
+ }
+
MoveCaretVerticallyCollapsingSelection (delta);
return true;
@@ -334,6 +354,13 @@ private void CreateCommandsAndBindings ()
private bool? ScrollVerticalCommand (int delta)
{
+ // In single-line mode, vertical scroll is a no-op but must return true (handled)
+ // to prevent the event from bubbling to parent containers.
+ if (!Multiline)
+ {
+ return true;
+ }
+
if (_document is null || ScrollVertical (delta) != true)
{
return false;
@@ -358,7 +385,7 @@ private void CreateCommandsAndBindings ()
private bool? InsertNewLineWithAutoIndent ()
{
- if (ReadOnly)
+ if (ReadOnly || !Multiline)
{
return true;
}
@@ -499,6 +526,13 @@ private void OverwriteAtOffset (int offset, string text)
private bool? MoveCaretToLineStart ()
{
+ if (!Multiline)
+ {
+ CaretOffset = 0;
+
+ return true;
+ }
+
DocumentLine line = _document!.GetLineByOffset (CaretOffset);
CaretOffset = line.Offset;
@@ -507,6 +541,13 @@ private void OverwriteAtOffset (int offset, string text)
private bool? MoveCaretToLineEnd ()
{
+ if (!Multiline)
+ {
+ CaretOffset = _document!.TextLength;
+
+ return true;
+ }
+
DocumentLine line = _document!.GetLineByOffset (CaretOffset);
CaretOffset = line.Offset + line.Length;
diff --git a/src/Terminal.Gui.Editor/Editor.Drawing.cs b/src/Terminal.Gui.Editor/Editor.Drawing.cs
index c88a27a..c21feaa 100644
--- a/src/Terminal.Gui.Editor/Editor.Drawing.cs
+++ b/src/Terminal.Gui.Editor/Editor.Drawing.cs
@@ -55,6 +55,13 @@ private void DrawVisibleLines (Rectangle viewport, Attribute normal, Attribute s
var visibleStart = viewport.X;
var visibleEnd = viewport.X + viewport.Width;
+ if (!Multiline)
+ {
+ DrawSingleLineFlat (viewport, normal, selected, selStart, selEnd, visibleStart, visibleEnd);
+
+ return;
+ }
+
if (WordWrap)
{
DrawWrappedLines (viewport, normal, selected, selStart, selEnd);
@@ -130,6 +137,111 @@ private void DrawWrappedLines (Rectangle viewport, Attribute normal, Attribute s
}
}
+ ///
+ /// Draws all document content on a single visual row with newline characters rendered as
+ /// visible glyphs. Used when is .
+ ///
+ private void DrawSingleLineFlat (
+ Rectangle viewport,
+ Attribute normal,
+ Attribute selected,
+ int selStart,
+ int selEnd,
+ int visibleStart,
+ int visibleEnd)
+ {
+ // Build a composite visual line from visible document lines (respecting folds),
+ // inserting newline glyph elements between them. Uses the first visible line as the owner.
+ List visibleLines = GetVisibleLineNumbers ();
+
+ if (visibleLines.Count == 0)
+ {
+ return;
+ }
+
+ DocumentLine firstLine = _document!.GetLineByNumber (visibleLines[0]);
+ CellVisualLine composite = new (firstLine);
+ var flatColumn = 0;
+
+ for (var idx = 0; idx < visibleLines.Count; idx++)
+ {
+ var lineNum = visibleLines[idx];
+ DocumentLine line = _document.GetLineByNumber (lineNum);
+ CellVisualLine lineVisual = GetOrBuildDrawVisualLine (line, null, normal, selected, selStart, selEnd);
+
+ foreach (CellVisualLineElement element in lineVisual.Elements)
+ {
+ CellVisualLineElement shifted = ShiftElement (element, flatColumn);
+ composite.AddElement (shifted);
+ }
+
+ flatColumn += lineVisual.VisualLength;
+
+ if (idx < visibleLines.Count - 1)
+ {
+ // The newline delimiter occupies document offsets at the end of the line.
+ var nlOffset = line.Offset + line.Length;
+ var nlLength = line.DelimiterLength;
+
+ // Determine the newline glyph attribute: use selection attribute if within selection.
+ Attribute nlAttr = selStart < selEnd && nlOffset < selEnd && nlOffset + nlLength > selStart
+ ? selected
+ : normal;
+
+ composite.AddElement (new NewlineGlyphElement (nlOffset, nlLength, flatColumn, nlAttr));
+ flatColumn += 1;
+ }
+ }
+
+ foreach (IBackgroundRenderer renderer in BackgroundRenderers)
+ {
+ renderer.Draw (this, composite, 0, viewport);
+ }
+
+ foreach (CellVisualLineElement element in composite.Elements)
+ {
+ if (element.VisualColumn >= visibleEnd)
+ {
+ break;
+ }
+
+ if (element.VisualEndColumn <= visibleStart)
+ {
+ continue;
+ }
+
+ element.Draw (this, 0, 0, visibleStart, visibleEnd);
+ }
+
+ foreach (IOverlayRenderer renderer in OverlayRenderers)
+ {
+ renderer.Draw (this, composite, 0, viewport);
+ }
+ }
+
+ ///
+ /// Creates a copy of an element shifted by a flat column offset. Used to compose
+ /// elements from multiple document lines onto a single visual row.
+ ///
+ private static CellVisualLineElement ShiftElement (CellVisualLineElement element, int columnOffset)
+ {
+ if (columnOffset == 0)
+ {
+ return element;
+ }
+
+ return element switch
+ {
+ TabElement tab => new TabElement (
+ tab.DocumentOffset, tab.VisualColumn + columnOffset, tab.VisualLength, tab.ShowTabs, tab.Attribute),
+ FoldingMarkerElement fold => new FoldingMarkerElement (
+ fold.DocumentOffset, fold.DocumentLength, fold.VisualColumn + columnOffset, fold.Attribute, fold.Title),
+ TextRunElement text => new TextRunElement (
+ text.DocumentOffset, text.DocumentLength, text.VisualColumn + columnOffset, text.Text, text.Attribute),
+ _ => element // Unknown element type — leave as-is (shouldn't happen).
+ };
+ }
+
private CellVisualLine BuildWrappedSegmentVisualLine (
DocumentLine documentLine,
int segmentStartOffset,
diff --git a/src/Terminal.Gui.Editor/Editor.MultiCaret.cs b/src/Terminal.Gui.Editor/Editor.MultiCaret.cs
index 422974b..a5d838f 100644
--- a/src/Terminal.Gui.Editor/Editor.MultiCaret.cs
+++ b/src/Terminal.Gui.Editor/Editor.MultiCaret.cs
@@ -36,7 +36,7 @@ public partial class Editor
///
public void ToggleCaretAt (int offset)
{
- if (_document is null)
+ if (_document is null || !Multiline)
{
return;
}
@@ -168,7 +168,7 @@ private void NormalizeAdditionalCarets ()
///
private bool? AddCaretVertically (int delta)
{
- if (_document is null)
+ if (_document is null || !Multiline)
{
return true;
}
@@ -222,7 +222,7 @@ private void NormalizeAdditionalCarets ()
private void SetVerticalCaretsFromViewRows (int anchorViewRow, int activeViewRow, int anchorViewColumn,
int activeViewColumn)
{
- if (_document is null)
+ if (_document is null || !Multiline)
{
return;
}
@@ -604,7 +604,7 @@ private void MultiCaretInsert (string text)
///
private bool? MultiCaretNewLine ()
{
- if (ReadOnly || _document is null)
+ if (ReadOnly || !Multiline || _document is null)
{
return true;
}
@@ -747,7 +747,7 @@ private bool MultiCaretInsertTab ()
foreach (CaretEditInfo caret in GetAllCaretsDescending ())
{
- if (TryGetCaretSelectionRange (caret, out int selStart, out int selEnd))
+ if (TryGetCaretSelectionRange (caret, out var selStart, out var selEnd))
{
foreach (DocumentLine line in LinesInRange (selStart, selEnd))
{
@@ -801,9 +801,11 @@ private bool MultiCaretUnindent ()
foreach (CaretEditInfo caret in GetAllCaretsDescending ())
{
- List lines = TryGetCaretSelectionRange (caret, out int selStart, out int selEnd) && RangeSpansMultipleLines (selStart, selEnd)
- ? LinesInRange (selStart, selEnd)
- : [_document!.GetLineByOffset (caret.Offset)];
+ List lines =
+ TryGetCaretSelectionRange (caret, out var selStart, out var selEnd) &&
+ RangeSpansMultipleLines (selStart, selEnd)
+ ? LinesInRange (selStart, selEnd)
+ : [_document!.GetLineByOffset (caret.Offset)];
foreach (DocumentLine line in lines)
{
diff --git a/src/Terminal.Gui.Editor/Editor.cs b/src/Terminal.Gui.Editor/Editor.cs
index 91b2bb2..fd1eeca 100644
--- a/src/Terminal.Gui.Editor/Editor.cs
+++ b/src/Terminal.Gui.Editor/Editor.cs
@@ -6,6 +6,7 @@
using Terminal.Gui.Drawing;
using Terminal.Gui.Editor.Rendering;
using Terminal.Gui.Highlighting;
+using Terminal.Gui.Input;
using Terminal.Gui.Text;
using Terminal.Gui.Text.Indentation;
using Terminal.Gui.ViewBase;
@@ -193,6 +194,56 @@ public GutterOptions GutterOptions
///
public bool ReadOnly { get; set; }
+ ///
+ /// Gets or sets whether the editor supports multiple lines. When (default),
+ /// Enter inserts a newline, vertical navigation moves across lines, and content height reflects the
+ /// full document. When , newline insertion is suppressed, vertical
+ /// navigation and scroll are constrained to a single row, is forced off,
+ /// and multi-caret operations are disabled. Existing newlines in the document are preserved (no
+ /// data loss) and rendered as visible ⏎ glyphs. Selection and horizontal editing still work.
+ ///
+ public bool Multiline
+ {
+ get;
+ set
+ {
+ if (field == value)
+ {
+ return;
+ }
+
+ field = value;
+
+ if (!value)
+ {
+ // Force off WordWrap — meaningless for a single-line input.
+ WordWrap = false;
+
+ // Collapse additional carets — vertical multi-caret is a multi-line concept.
+ ClearAdditionalCarets ();
+
+ // Auto-size to ContentSize (height = 1) so callers don't need explicit Height = 1.
+ Height = Dim.Auto (DimAutoStyle.Content);
+
+ // Rebind Enter to Accept (like TextField) — raises Accepting event.
+ KeyBindings.Remove (Key.Enter);
+ KeyBindings.Add (Key.Enter, Command.Accept);
+ }
+ else
+ {
+ // Restore Enter → NewLine binding for multiline mode.
+ KeyBindings.Remove (Key.Enter);
+ KeyBindings.Add (Key.Enter, Command.NewLine);
+ }
+
+ ClearVisualLineCaches ();
+ _maxWidthDirty = true;
+ UpdateContentSize ();
+ EnsureCaretVisible ();
+ SetNeedsDraw ();
+ }
+ } = true;
+
///
/// Gets or sets the highlighting definition used for syntax coloring. When set, a
/// is automatically added to
@@ -282,6 +333,12 @@ public bool WordWrap
get;
set
{
+ // Word wrap is meaningless in single-line mode.
+ if (!Multiline && value)
+ {
+ return;
+ }
+
if (field == value)
{
return;
@@ -519,6 +576,27 @@ private void UpdateContentSize ()
return;
}
+ if (!Multiline)
+ {
+ // Single-line mode: one row, width = sum of visible line visual lengths + newline glyphs.
+ List visibleLines = GetVisibleLineNumbers ();
+ var width = 0;
+
+ for (var idx = 0; idx < visibleLines.Count; idx++)
+ {
+ width += GetOrBuildDefaultVisualLine (_document.GetLineByNumber (visibleLines[idx])).VisualLength;
+
+ if (idx < visibleLines.Count - 1)
+ {
+ width += 1; // newline glyph
+ }
+ }
+
+ SetContentSize (new Size (width + 1, 1));
+
+ return;
+ }
+
if (WordWrap)
{
// In word-wrap mode, the content height is the total number of visual rows
@@ -535,8 +613,8 @@ private void UpdateContentSize ()
}
// +1 column lets the caret sit just past the end-of-line.
- var visibleLines = _document.LineCount - (FoldingManager?.GetHiddenLineCount () ?? 0);
- SetContentSize (new Size (_maxVisualWidth + 1, Math.Max (1, visibleLines)));
+ var visibleLineCount = _document.LineCount - (FoldingManager?.GetHiddenLineCount () ?? 0);
+ SetContentSize (new Size (_maxVisualWidth + 1, Math.Max (1, visibleLineCount)));
}
///
@@ -888,6 +966,11 @@ private int GetVisualColumnForOffset (int caretOffset)
return 0;
}
+ if (!Multiline)
+ {
+ return GetFlatVisualColumn (line, caretOffset - line.Offset);
+ }
+
if (!WordWrap)
{
return GetOrBuildDefaultVisualLine (line).GetVisualColumn (caretOffset - line.Offset);
@@ -919,6 +1002,32 @@ private int GetCaretLineIndex ()
return _document?.GetLineByOffset (CaretOffset).LineNumber - 1 ?? 0;
}
+ ///
+ /// Returns the visual column of an offset in a flattened single-line view. Sums the visual
+ /// lengths of all visible preceding lines (plus 1-cell newline glyphs) and adds the column
+ /// within the target line. Respects folding by using .
+ ///
+ private int GetFlatVisualColumn (DocumentLine targetLine, int offsetInLine)
+ {
+ List visibleLines = GetVisibleLineNumbers ();
+ var flatColumn = 0;
+
+ foreach (var lineNum in visibleLines)
+ {
+ if (lineNum == targetLine.LineNumber)
+ {
+ break;
+ }
+
+ flatColumn += GetOrBuildDefaultVisualLine (_document!.GetLineByNumber (lineNum)).VisualLength;
+ flatColumn += 1; // newline glyph
+ }
+
+ flatColumn += GetOrBuildDefaultVisualLine (targetLine).GetVisualColumn (offsetInLine);
+
+ return flatColumn;
+ }
+
///
/// Returns the caret's position as an index into the visible-line list (i.e. the coordinate
/// system used by Viewport.Y). Falls back to when
@@ -926,6 +1035,11 @@ private int GetCaretLineIndex ()
///
private int GetCaretVisibleLineIndex ()
{
+ if (!Multiline)
+ {
+ return 0;
+ }
+
if (WordWrap)
{
return GetCaretWrapRow ();
diff --git a/src/Terminal.Gui.Editor/Rendering/NewlineGlyphElement.cs b/src/Terminal.Gui.Editor/Rendering/NewlineGlyphElement.cs
new file mode 100644
index 0000000..ce60bcb
--- /dev/null
+++ b/src/Terminal.Gui.Editor/Rendering/NewlineGlyphElement.cs
@@ -0,0 +1,27 @@
+using Terminal.Gui.ViewBase;
+using Attribute = Terminal.Gui.Drawing.Attribute;
+
+namespace Terminal.Gui.Editor.Rendering;
+
+/// A newline character rendered as a visible glyph in single-line mode.
+public sealed class NewlineGlyphElement (
+ int documentOffset,
+ int documentLength,
+ int visualColumn,
+ Attribute attribute)
+ : CellVisualLineElement (documentOffset, documentLength, visualColumn, 1, attribute)
+{
+ /// The glyph used to represent a newline character.
+ internal const string NewlineGlyph = "⏎";
+
+ public override void Draw (View host, int x, int y, int visibleStart, int visibleEnd)
+ {
+ if (VisualEndColumn <= visibleStart || VisualColumn >= visibleEnd)
+ {
+ return;
+ }
+
+ host.SetAttribute (Attribute);
+ host.AddStr (x + VisualColumn - visibleStart, y, NewlineGlyph);
+ }
+}
diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/EditorSingleLineTests.cs b/tests/Terminal.Gui.Editor.IntegrationTests/EditorSingleLineTests.cs
new file mode 100644
index 0000000..fd1955d
--- /dev/null
+++ b/tests/Terminal.Gui.Editor.IntegrationTests/EditorSingleLineTests.cs
@@ -0,0 +1,341 @@
+// Copilot - claude-sonnet-4
+
+using Terminal.Gui.Document.Folding;
+using Terminal.Gui.Drivers;
+using Terminal.Gui.Editor.IntegrationTests.Testing;
+using Terminal.Gui.Input;
+using Terminal.Gui.Testing;
+using Xunit;
+
+namespace Terminal.Gui.Editor.IntegrationTests;
+
+///
+/// Integration tests for = (single-line mode).
+/// Verifies that newline insertion is suppressed, vertical navigation is constrained,
+/// word-wrap is forced off, multi-caret is disabled, and selection + editing still work.
+///
+public class EditorSingleLineTests
+{
+ private static readonly InputInjectionOptions Direct = new () { Mode = InputInjectionMode.Direct };
+
+ [Fact]
+ public async Task SingleLine_Enter_Does_Not_Insert_Newline ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("ab"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 1;
+
+ fx.Injector.InjectKey (Key.Enter, Direct);
+
+ Assert.Equal ("ab", fx.Top.Editor.Document!.Text);
+ Assert.Equal (1, fx.Top.Editor.Document.LineCount);
+ }
+
+ [Fact]
+ public async Task SingleLine_Renders_Newlines_As_Glyphs ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("ab\ncd\nef"), 12, 3);
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Render ();
+
+ // The document keeps its newlines; single-line mode flattens every visible line onto one
+ // row, rendering each newline as a visible ⏎ glyph (DrawSingleLineFlat). The ANSI golden
+ // locks that exact look — cat __snapshots__/SingleLine_Renders_Newlines_As_Glyphs.ans.
+ Assert.Equal ("ab\ncd\nef", fx.Top.Editor.Document!.Text);
+ Assert.Equal (3, fx.Top.Editor.Document.LineCount);
+ Assert.Equal (1, fx.Top.Editor.Viewport.Height);
+
+ AnsiSnapshot.Verify (fx.Driver, nameof (SingleLine_Renders_Newlines_As_Glyphs));
+ }
+
+ [Fact]
+ public async Task SingleLine_Up_Down_Are_NoOps ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 2;
+
+ fx.Injector.InjectKey (Key.CursorUp, Direct);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+
+ fx.Injector.InjectKey (Key.CursorDown, Direct);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+ }
+
+ [Fact]
+ public async Task SingleLine_WordWrap_Cannot_Be_Enabled ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello world"));
+ fx.Top.Editor.Multiline = false;
+
+ fx.Top.Editor.WordWrap = true;
+
+ Assert.False (fx.Top.Editor.WordWrap);
+ }
+
+ [Fact]
+ public async Task SingleLine_Setting_Multiline_False_Disables_WordWrap ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello world"));
+ fx.Top.Editor.WordWrap = true;
+ Assert.True (fx.Top.Editor.WordWrap);
+
+ fx.Top.Editor.Multiline = false;
+
+ Assert.False (fx.Top.Editor.WordWrap);
+ }
+
+ [Fact]
+ public async Task SingleLine_MultiCaret_Toggle_Is_NoOp ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("abcdef"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 0;
+
+ fx.Top.Editor.ToggleCaretAt (3);
+
+ Assert.False (fx.Top.Editor.HasMultipleCarets);
+ }
+
+ [Fact]
+ public async Task SingleLine_Selection_Still_Works ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 0;
+
+ fx.Injector.InjectKey (Key.A.WithCtrl, Direct);
+
+ Assert.True (fx.Top.Editor.HasSelection);
+ Assert.Equal ("hello", fx.Top.Editor.SelectedText);
+ }
+
+ [Fact]
+ public async Task SingleLine_Editing_Insert_Delete_Still_Works ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("abc"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 3;
+
+ fx.Injector.InjectKey (Key.Backspace, Direct);
+
+ Assert.Equal ("ab", fx.Top.Editor.Document!.Text);
+ }
+
+ [Fact]
+ public async Task SingleLine_ContentSize_Height_Is_One ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Render ();
+
+ Assert.Equal (1, fx.Top.Editor.GetContentSize ().Height);
+ }
+
+ [Fact]
+ public async Task SingleLine_Height_Defaults_To_DimAuto ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Render ();
+
+ // Multiline=false sets Height to Dim.Auto(Content), so no explicit Height=1 needed.
+ Assert.Equal (1, fx.Top.Editor.Frame.Height);
+ }
+
+ [Fact]
+ public async Task SingleLine_Enter_Raises_Accept ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+
+ var accepted = false;
+ fx.Top.Editor.Accepting += (_, _) => accepted = true;
+
+ fx.Injector.InjectKey (Key.Enter, Direct);
+
+ Assert.True (accepted);
+ // Document is still unchanged (no newline inserted).
+ Assert.Equal ("hello", fx.Top.Editor.Document!.Text);
+ }
+
+ [Fact]
+ public async Task SingleLine_PageUp_PageDown_Are_NoOps ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 2;
+
+ fx.Injector.InjectKey (Key.PageUp, Direct);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+
+ fx.Injector.InjectKey (Key.PageDown, Direct);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+ }
+
+ [Fact]
+ public async Task SingleLine_Multiline_Default_Is_True ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+
+ Assert.True (fx.Top.Editor.Multiline);
+ }
+
+ [Fact]
+ public async Task SingleLine_Existing_Carets_Cleared_When_Setting_Multiline_False ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("abcdef"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 0;
+ fx.Top.Editor.ToggleCaretAt (3);
+ Assert.True (fx.Top.Editor.HasMultipleCarets);
+
+ fx.Top.Editor.Multiline = false;
+
+ Assert.False (fx.Top.Editor.HasMultipleCarets);
+ }
+
+ [Fact]
+ public async Task SingleLine_Home_End_Still_Navigate ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello world"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 5;
+
+ fx.Injector.InjectKey (Key.Home, Direct);
+ Assert.Equal (0, fx.Top.Editor.CaretOffset);
+
+ fx.Injector.InjectKey (Key.End, Direct);
+ Assert.Equal (11, fx.Top.Editor.CaretOffset);
+ }
+
+ [Fact]
+ public async Task SingleLine_VerticalExtend_NoOp ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 2;
+
+ fx.Injector.InjectKey (Key.CursorUp.WithShift, Direct);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+ Assert.False (fx.Top.Editor.HasSelection);
+
+ fx.Injector.InjectKey (Key.CursorDown.WithShift, Direct);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+ Assert.False (fx.Top.Editor.HasSelection);
+ }
+
+ [Fact]
+ public async Task SingleLine_Paste_Strips_Newlines ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("ab"));
+ fx.Driver.Clipboard = new FakeClipboard (false, false);
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 2;
+
+ fx.App.Clipboard!.TrySetClipboardData ("cd\nef\r\ngh");
+ fx.Injector.InjectKey (Key.V.WithCtrl, Direct);
+
+ // Paste still strips newlines so new content stays on one logical line.
+ Assert.Equal ("abcdefgh", fx.Top.Editor.Document!.Text);
+ Assert.Equal (1, fx.Top.Editor.Document.LineCount);
+ }
+
+ [Fact]
+ public async Task SingleLine_Transition_Preserves_Newlines ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("line1\nline2\nline3"));
+ Assert.Equal (3, fx.Top.Editor.Document!.LineCount);
+
+ fx.Top.Editor.Multiline = false;
+
+ // Newlines are preserved — no data loss.
+ Assert.Equal ("line1\nline2\nline3", fx.Top.Editor.Document.Text);
+ Assert.Equal (3, fx.Top.Editor.Document.LineCount);
+
+ // Content size height is still 1 (single visual row).
+ fx.Render ();
+ Assert.Equal (1, fx.Top.Editor.GetContentSize ().Height);
+ }
+
+ [Fact]
+ public async Task SingleLine_Home_End_Navigate_To_Document_Bounds ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("ab\ncd\nef"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+
+ // Place caret in the middle of line 2.
+ fx.Top.Editor.CaretOffset = 4; // 'c' on line 2
+
+ fx.Injector.InjectKey (Key.Home, Direct);
+ Assert.Equal (0, fx.Top.Editor.CaretOffset);
+
+ fx.Top.Editor.CaretOffset = 4;
+ fx.Injector.InjectKey (Key.End, Direct);
+ Assert.Equal (8, fx.Top.Editor.CaretOffset); // end of "ef"
+ }
+
+ ///
+ /// CR1: Vertical scroll command should return true (handled) in single-line mode so the
+ /// event doesn't bubble to parent containers.
+ ///
+ [Fact]
+ public async Task SingleLine_ScrollVertical_Is_Handled_NoOp ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("hello"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+
+ // Scroll commands should not change caret position and should be handled (not bubble).
+ // Viewport.Y should remain 0 since there's only one row.
+ fx.Top.Editor.CaretOffset = 2;
+ fx.Injector.InjectKey (Key.CursorUp.WithCtrl, Direct); // typically triggers scroll
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+ Assert.Equal (0, fx.Top.Editor.Viewport.Y);
+ }
+
+ ///
+ /// CR2/CR3/CR5: Folded lines should be excluded from the single-line flat view. When lines
+ /// are hidden by folding, the flat visual width and rendering should skip them.
+ ///
+ [Fact]
+ public async Task SingleLine_Folding_Hides_Lines_From_Flat_View ()
+ {
+ await using AppFixture fx = new (() => new EditorTestHost ("ab\ncd\nef"));
+ fx.Top.Editor.Multiline = false;
+ fx.Top.Editor.SetFocus ();
+
+ // Install a FoldingManager so we can fold lines.
+ fx.Top.Editor.FoldingManager = new FoldingManager (fx.Top.Editor.Document!);
+ fx.Render ();
+
+ // Full flat width: "ab" (2) + ⏎ (1) + "cd" (2) + ⏎ (1) + "ef" (2) = 8 plus +1 for caret-past-end = 9
+ Assert.Equal (9, fx.Top.Editor.GetContentSize ().Width);
+
+ // CreateFolding spanning from start of "ab" to end of "cd\n" hides line 2.
+ FoldingSection fold = fx.Top.Editor.FoldingManager!.CreateFolding (
+ 0, fx.Top.Editor.Document!.GetLineByNumber (2).EndOffset);
+ fold.IsFolded = true;
+
+ // Toggling a property that clears caches to force re-computation
+ fx.Top.Editor.SetNeedsDraw ();
+ fx.Render ();
+
+ // After folding, UpdateContentSize skips hidden lines, reducing the width.
+ int widthAfterFold = fx.Top.Editor.GetContentSize ().Width;
+ Assert.True (widthAfterFold < 9,
+ $"Content width after fold should decrease from 9, was {widthAfterFold}");
+ }
+}
diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/__snapshots__/SingleLine_Renders_Newlines_As_Glyphs.ans b/tests/Terminal.Gui.Editor.IntegrationTests/__snapshots__/SingleLine_Renders_Newlines_As_Glyphs.ans
new file mode 100644
index 0000000..8205921
--- /dev/null
+++ b/tests/Terminal.Gui.Editor.IntegrationTests/__snapshots__/SingleLine_Renders_Newlines_As_Glyphs.ans
@@ -0,0 +1,3 @@
+[39m[49mab⏎cd⏎ef
+
+