Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
6b18c52
Initial plan
Copilot May 17, 2026
087d4fe
feat: add in-editor completion/autocomplete popup (IEditorCompletionP…
Copilot May 17, 2026
d42c176
fix: dispose PopoverMenu before recreating; fix DEC-009 reference in …
Copilot May 17, 2026
449334d
fix: disable autocomplete by default in ted; add Auto Complete checkb…
Copilot May 17, 2026
8f4d77d
fix: move Auto Complete checkbox from Tab Settings to Config tab in S…
Copilot May 17, 2026
f86ad3a
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 17, 2026
45a1e09
fix: CLAUDE.md compliance — remove dead Esc handler, fix Allman brace…
Copilot May 17, 2026
8762c34
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 17, 2026
dc8fcbd
code cleanup
tig May 17, 2026
499d0d5
refactor: replace PopoverMenu with Popover<ListView> for completion p…
Copilot May 17, 2026
47d3782
refactor: replace Popover<ListView> with DropDownList for completion …
Copilot May 17, 2026
324a24b
merge: resolve conflicts with origin/develop (overwrite-mode + IDesig…
Copilot May 17, 2026
9292447
Merge branch 'develop' into copilot/add-editor-autocomplete-functiona…
tig May 17, 2026
d34ba7c
refactor: replace DropDownList with Popover<ListView, CompletionItem?…
Copilot May 17, 2026
ee44968
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 17, 2026
828c17f
test: add failing tests for completion keyboard capture, Enter/Tab ro…
Copilot May 17, 2026
d12c713
fix: Popover keyboard capture, Enter/Tab routing, and provider-null d…
Copilot May 17, 2026
e1e9340
fix: guard against short text in Enter-accept integration test
Copilot May 17, 2026
d5f4c82
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 17, 2026
7008d94
Merge branch 'develop' into copilot/add-editor-autocomplete-functiona…
tig May 17, 2026
ebdb104
fix: arrow key navigation in completion popup — update selection inde…
Copilot May 17, 2026
8456288
docs: fix XML doc and comment per code review feedback
Copilot May 17, 2026
482394b
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 17, 2026
64356e4
fix: rewrite completion popup — Enabled=false for keyboard, direct ch…
Copilot May 17, 2026
d76c128
Merge develop: resolve conflicts (kill-ring + completion)
Copilot May 17, 2026
60bca61
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 17, 2026
c75404f
Merge origin/develop: resolve conflicts in TedApp.cs and public-api.md
Copilot May 18, 2026
32ccf3c
Merge branch 'copilot/add-editor-autocomplete-functionality' of https…
tig May 18, 2026
bd41bfa
Refactor completion popup key handling and tests
tig May 18, 2026
7ad560e
Refactor completion key handling to use key bindings
tig May 18, 2026
f77877e
Refine completion popup dismissal and selection logic
tig May 18, 2026
a4bb1a1
Refactor completion popup teardown and Esc handling
tig May 18, 2026
ba56970
Update completion popup dismissal to use app Quit key
tig May 18, 2026
8f07a31
Fix Enter double-accept; add real InjectMouse completion tests
tig May 18, 2026
d9f9fb9
Route completion popup edits through canonical insert/delete helpers
tig May 18, 2026
dd6553c
Measure completion popup width in display columns, not char count
tig May 18, 2026
600fe0c
Dedup ShowCompletion / NotifyCompletionAfterInsert
tig May 18, 2026
b93edaf
Remove unwired CompletionItem.Detail; test the real InsertText path
tig May 18, 2026
77fbae1
Codify the no-speculative-API rule in CLAUDE.md Non-goals
tig May 18, 2026
9277aca
Revert CLAUDE.md no-speculative-API bullet; constitution R9 already c…
tig May 19, 2026
19980c7
Refresh completion popup in place instead of rebuilding every keystroke
tig May 19, 2026
6f7922c
Strengthen filter/dismiss tests to drive the real key path
tig May 19, 2026
affe0fa
Fix flaky OpenFileAsync background-thread test (intrinsically racy th…
tig May 19, 2026
3de0c57
fix: completion-consumed keys break kill-ring consecutive run; dismis…
Copilot May 19, 2026
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ Don't accidentally do these — they were considered and rejected:
- Source/API compatibility with `Terminal.Gui.TextView`. `Editor` ships beside it, not as a replacement.
- RTL bidi or rich text shaping beyond grapheme width.
- Pixel/proportional font fidelity.
- Porting AvaloniaEdit's `Editing/`, `Rendering/`, or `CodeCompletion/` namespaces — those are Avalonia-UI-specific and replaced by TG-native equivalents (`Editor` partials, cell-grid `Rendering/`, `PopoverMenu` for completion).
- Porting AvaloniaEdit's `Editing/`, `Rendering/`, or `CodeCompletion/` namespaces — those are Avalonia-UI-specific and replaced by TG-native equivalents (`Editor` partials, cell-grid `Rendering/`, `Popover<ListView>` for completion).

## Open decisions

Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>

<!-- Pinned Terminal.Gui version. CI / release workflows can override via -p:TerminalGuiVersion=<x>. -->
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.1.1-develop.98</TerminalGuiVersion>
<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.2.0-rc.4</TerminalGuiVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
7 changes: 6 additions & 1 deletion examples/ted/EditorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal static class EditorSettings
[ConfigurationProperty (Scope = typeof (TedSettingsScope))]
public static bool AutoIndent { get; set; } = true;

[ConfigurationProperty (Scope = typeof (TedSettingsScope))]
public static bool AutoComplete { get; set; }

/// <summary>
/// Loads settings from the config file at <see cref="GetConfigPath" />.
/// Called once at startup before constructing <see cref="TedApp" />.
Expand Down Expand Up @@ -56,6 +59,7 @@ internal static void Load (string path)
IndentSize = ReadInt (text, "EditorSettings.IndentSize", IndentSize);
ConvertTabsToSpaces = ReadBool (text, "EditorSettings.ConvertTabsToSpaces", ConvertTabsToSpaces);
AutoIndent = ReadBool (text, "EditorSettings.AutoIndent", AutoIndent);
AutoComplete = ReadBool (text, "EditorSettings.AutoComplete", AutoComplete);
}
catch (Exception ex)
{
Expand All @@ -82,7 +86,8 @@ internal static void Save (string path)
["EditorSettings.ShowTabs"] = ToJson (ShowTabs),
["EditorSettings.IndentSize"] = IndentSize.ToString (),
["EditorSettings.ConvertTabsToSpaces"] = ToJson (ConvertTabsToSpaces),
["EditorSettings.AutoIndent"] = ToJson (AutoIndent)
["EditorSettings.AutoIndent"] = ToJson (AutoIndent),
["EditorSettings.AutoComplete"] = ToJson (AutoComplete)
};

List<string> toInsert = [];
Expand Down
16 changes: 14 additions & 2 deletions examples/ted/EditorSettingsDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Ted;

internal sealed class EditorSettingsDialog : Dialog
{
private readonly CheckBox _autoCompleteCheck;
private readonly CheckBox _autoIndentCheck;
private readonly CheckBox _convertTabsCheck;
private readonly NumericUpDown<int> _indentSize;
Expand All @@ -15,7 +16,7 @@ internal EditorSettingsDialog (Editor editor)
{
Title = "Settings";
Width = Dim.Percent (60);
Height = 16;
Height = 18;

View tabSettingsTab = new ()
{
Expand Down Expand Up @@ -61,14 +62,22 @@ internal EditorSettingsDialog (Editor editor)
_convertTabsCheck,
_autoIndentCheck);

_autoCompleteCheck = new CheckBox
{
X = 1,
Y = 1,
Title = "Auto _Complete (Ctrl+Space)",
Value = editor.CompletionProvider is not null ? CheckState.Checked : CheckState.UnChecked
};

View configTab = new ()
{
Title = "_Config",
Width = Dim.Fill (),
Height = Dim.Fill ()
};

configTab.Add (new Label { X = 1, Y = 1, Text = "No settings yet." });
configTab.Add (_autoCompleteCheck);

Tabs tabs = new ()
{
Expand Down Expand Up @@ -115,5 +124,8 @@ internal void ApplyTo (Editor editor)
editor.IndentationStrategy = _autoIndentCheck.Value == CheckState.Checked
? new DefaultIndentationStrategy ()
: null;
editor.CompletionProvider = _autoCompleteCheck.Value == CheckState.Checked
? new WordCompletionProvider ()
: null;
}
}
2 changes: 1 addition & 1 deletion examples/ted/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
"workingDirectory": "../.."
}
}
}
}
2 changes: 2 additions & 0 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public TedApp (bool readOnly = false, string? configPath = null)
WordWrap = EditorSettings.WordWrap,
ShowTabs = EditorSettings.ShowTabs,
ReadOnly = readOnly,
CompletionProvider = EditorSettings.AutoComplete ? new WordCompletionProvider () : null,
ViewportSettings = ViewportSettingsFlags.HasScrollBars
};

Expand Down Expand Up @@ -432,6 +433,7 @@ private void SaveViewSettings ()
EditorSettings.IndentSize = Editor.IndentationSize;
EditorSettings.ConvertTabsToSpaces = Editor.ConvertTabsToSpaces;
EditorSettings.AutoIndent = Editor.IndentationStrategy is not null;
EditorSettings.AutoComplete = Editor.CompletionProvider is not null;
EditorSettings.Save (_configPath);
}

Expand Down
74 changes: 74 additions & 0 deletions examples/ted/WordCompletionProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using Terminal.Gui.Document;
using Terminal.Gui.Editor.Completion;
using Terminal.Gui.Input;

namespace Ted;

/// <summary>
/// A trivial word-completion provider for the <c>ted</c> demo. Scans the document for unique
/// word tokens (letters, digits, underscores) and offers them as suggestions when the prefix
/// matches. Triggered by <c>Ctrl+Space</c>.
/// </summary>
internal sealed class WordCompletionProvider : IEditorCompletionProvider
{
/// <inheritdoc />
public IReadOnlyList<CompletionItem> GetCompletions (TextDocument document, int caretOffset, string prefix)
{
if (string.IsNullOrEmpty (prefix))
{
return [];
}

var text = document.Text;
HashSet<string> seen = new (StringComparer.OrdinalIgnoreCase);
List<CompletionItem> results = [];

// Walk the document text for word tokens.
var i = 0;

while (i < text.Length)
{
if (!IsWordChar (text[i]))
{
i++;

continue;
}

var start = i;

while (i < text.Length && IsWordChar (text[i]))
{
i++;
}

var word = text.Substring (start, i - start);

// Skip the exact prefix and short tokens.
if (word.Length <= prefix.Length || string.Equals (word, prefix, StringComparison.Ordinal))
{
continue;
}

if (word.StartsWith (prefix, StringComparison.OrdinalIgnoreCase) && seen.Add (word))
{
results.Add (new CompletionItem { Label = word });
}
}

results.Sort ((a, b) => string.Compare (a.Label, b.Label, StringComparison.OrdinalIgnoreCase));

return results;
}

/// <inheritdoc />
public bool ShouldTrigger (Key key)
{
return key == Key.Space.WithCtrl;
}

private static bool IsWordChar (char ch)
{
return char.IsLetterOrDigit (ch) || ch == '_';
}
}
89 changes: 89 additions & 0 deletions specs/completion/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Completion Spec

**Status**: Implemented
**Date**: 2026-05-17
**Resolves**: OPEN-002, `specs/textview-parity-gap/spec.md` Gap 1

---

## Summary

In-editor autocomplete popup for `Editor`, providing caret-anchored, filter-as-you-type completion
suggestions. Consumers implement `IEditorCompletionProvider` and assign it to `Editor.CompletionProvider`.
The popup renders via a `Popover<ListView, CompletionItem?>`,
keys are intercepted ahead of the editor, and accepted suggestions apply as a single undo step.

## Public API

```csharp
namespace Terminal.Gui.Editor.Completion;

public sealed class CompletionItem
{
public required string Label { get; init; }
public string? InsertText { get; init; } // defaults to Label
}

public interface IEditorCompletionProvider
{
IReadOnlyList<CompletionItem> GetCompletions (TextDocument document, int caretOffset, string prefix);
bool ShouldTrigger (Key key);
}
```

On `Editor`:

```csharp
public IEditorCompletionProvider? CompletionProvider { get; set; }
public bool IsCompletionActive { get; }
```

## Behavior

### Opening the popup

1. **Explicit trigger**: Provider's `ShouldTrigger(key)` returns `true` (e.g. `Ctrl+Space`).
2. **Filter-as-you-type**: After each character insert, the editor extracts the word-prefix before the caret
(letters/digits/underscores) and queries the provider. If items are returned, the popup opens or updates.

### Popup interaction

| Key | Action |
|-----|--------|
| `↑` / `↓` | Move selection |
| `Enter` / `Tab` | Accept selected item |
| `Esc` | Dismiss |
| Any printable char | Inserted into document, popup re-filters |
| `Backspace` | Deletes char, popup re-filters (dismissed when prefix becomes empty) |

### Accepting a completion

The word-prefix (`_completionPrefixStart` to `CaretOffset`) is replaced by `CompletionItem.TextToInsert`
inside a single `Document.RunUpdate()` scope, so one `Ctrl+Z` undoes the entire replacement.

### Dismissing

Pressing `Esc`, typing a non-word character that empties the prefix,
or the provider returning zero items — all dismiss the popup.
Pressing `Enter`/`Tab` accepts the selected item (see "Accepting" above), not dismiss.

## Positioning

The popup is anchored at the caret's screen position via `ViewportToScreen`, placed one row below the caret.
Uses `Popover<ListView, CompletionItem?>` positioned at the caret via `MakeVisible`.

## ted demo

`WordCompletionProvider` scans the document for unique word tokens and offers those starting with the
current prefix. Triggered by `Ctrl+Space`. Wired via `Editor.CompletionProvider = new WordCompletionProvider()`.

## Testing

- **Unit tests** (`EditorCompletionTests`): prefix extraction, accept/dismiss, single-undo-step, no-op cases.
- **Integration tests**: popup rendering requires `IApplication`; covered by ted integration tests.

## Design decisions

- **DEC-009**: Fresh LSP-flavored provider, not TG's `IAutocomplete` (see `specs/decisions.md`).
- **Popover<ListView>**: `Popover<ListView, CompletionItem?>` positioned at the caret (explicit non-goal: AvaloniaEdit `CodeCompletion/` lift).
- **Synchronous provider**: `GetCompletions` is synchronous; providers should pre-index for speed.
16 changes: 12 additions & 4 deletions specs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ need to know the document construction details.

---

### 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`.

**Rationale**: TG's `IAutocomplete` is tightly coupled to `TextView` (it assumes its own `PopupAutocomplete` rendering, owns selection state, and embeds key-handling that conflicts with `Editor`'s command architecture). A clean provider interface — `GetCompletions(document, caretOffset, prefix)` + `ShouldTrigger(key)` — keeps the completion *data* separate from the *UI*. `CompletionItem` follows a minimal LSP-flavored shape (Label, InsertText) rather than reusing `IAutocomplete`'s string list, which simplifies future LSP integration; richer fields (e.g. detail/documentation) are added when the popup actually renders them, not before. The popup uses a `Popover<ListView, CompletionItem?>` positioned at the caret. Accept applies inside a single `RunUpdate` scope so the entire replacement is one undo step.

**Date**: 2026-05-17

---

## Open

### OPEN-001: Independent `Terminal.Gui.Editor` NuGet from day one
Expand All @@ -80,11 +90,9 @@ need to know the document construction details.

---

### OPEN-002: Completion item shape

**Question**: Reuse Terminal.Gui's `IAutocomplete`-style types vs. a fresh LSP-flavored `IEditorCompletionProvider`?
### OPEN-002: Completion item shape → DEC-009

**Affected features**: Post-MLP.
Resolved — see DEC-009.

---

Expand Down
24 changes: 22 additions & 2 deletions specs/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,9 @@ public class Editor : View
// --- Search ---
public ISearchStrategy? SearchStrategy { get; set; } // find-and-replace (needs search + rendering-pipeline ✅)

// --- Completion (post-MLP) ---
public IEditorCompletionProvider? CompletionProvider { get; set; } // post-MLP
// --- Completion ---
public IEditorCompletionProvider? CompletionProvider { get; set; } // completion ✅
public bool IsCompletionActive { get; } // completion ✅

// --- Design-time support ---
public bool EnableForDesign (); // IDesignable (design-time ✅)
Expand Down Expand Up @@ -117,6 +118,24 @@ public interface IOverlayRenderer
}
```

## Completion Types (completion — landed)

```csharp
namespace Terminal.Gui.Editor.Completion;

public sealed class CompletionItem
{
public required string Label { get; init; }
public string? InsertText { get; init; }
}

public interface IEditorCompletionProvider
{
IReadOnlyList<CompletionItem> GetCompletions (TextDocument document, int caretOffset, string prefix);
bool ShouldTrigger (Key key);
}
```

## Document File I/O (file-io)

```csharp
Expand Down Expand Up @@ -157,5 +176,6 @@ 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 | `IEditorCompletionProvider?` `CompletionProvider` + `bool IsCompletionActive` landed; `CompletionItem` sealed class; `Popover<ListView>`-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 |
| 2026-05-17 | `Editor` implements `IDesignable`; `EnableForDesign()` seeds C# sample code with syntax highlighting and line numbers | design-time |
24 changes: 24 additions & 0 deletions src/Terminal.Gui.Editor/Completion/CompletionItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Terminal.Gui.Editor.Completion;

/// <summary>
/// A single completion suggestion returned by an <see cref="IEditorCompletionProvider" />.
/// Modelled after the LSP <c>CompletionItem</c> shape — label + insertText — but kept
/// minimal for the terminal context.
/// </summary>
public sealed class CompletionItem
{
/// <summary>
/// The display text shown in the completion popup. This is the primary string the user sees
/// when filtering suggestions.
/// </summary>
public required string Label { get; init; }

/// <summary>
/// The text inserted into the document when this item is accepted. When <see langword="null" />,
/// <see cref="Label" /> is inserted instead.
/// </summary>
public string? InsertText { get; init; }

/// <summary>The text that will actually be inserted: <see cref="InsertText" /> ?? <see cref="Label" />.</summary>
internal string TextToInsert => InsertText ?? Label;
}
Loading
Loading