Skip to content
Closed
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
56 changes: 44 additions & 12 deletions Terminal.Gui/Views/Markdown/MarkdownView.Selection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,22 @@ private string GetSelectedText ()
(Point start, Point end) = GetNormalizedSelection ();
List<string> outputLines = [];
var inCodeBlock = false;

string? currentCodeLanguage = null;

// Fences are injected only when the selection crosses a code-block boundary:
// • Opening fence: emitted when entering a code block after selected non-code
// content, and also when transitioning directly to an adjacent selected code
// block whose opening fence has not yet been written.
// • Closing fence: always when the selection crosses out of the code block into
// non-code content — regardless of whether an opening fence was emitted.
// • No trailing fence: when the selection ends inside a code block, no closing
// fence is added; the selection ends mid-block.
// This produces no fences for a selection entirely within a code block, matching
// the behaviour of the copy-button on MarkdownCodeBlock. codeOpenFenceEmitted
// tracks whether the current selected code block already has its opening fence.
var selectionHasNonCodeContent = false;
var codeOpenFenceEmitted = false;

// Track the last table instance that was output. All placeholder rows for the
// same table share the same TableData reference, so we use ReferenceEquals to
// emit the reconstructed table markdown exactly once even when the selection
Expand All @@ -150,26 +163,50 @@ private string GetSelectedText ()

if (!inCodeBlock)
{
// Entering a code block: inject the opening fence with optional language tag
outputLines.Add ($"```{nextCodeLanguage ?? string.Empty}");
inCodeBlock = true;
currentCodeLanguage = nextCodeLanguage;

// Only inject the opening fence when non-code content has already been
// output — that is, the selection crosses from outside into this code block.
if (selectionHasNonCodeContent)
{
outputLines.Add ($"```{nextCodeLanguage ?? string.Empty}");
codeOpenFenceEmitted = true;
}
else
{
codeOpenFenceEmitted = false;
}
}
else if (!string.Equals (currentCodeLanguage, nextCodeLanguage, StringComparison.Ordinal))
{
// Transitioning directly between two code blocks: close the current fence
// and open the next one so adjacent fenced blocks are preserved.
outputLines.Add ("```");
// Transitioning directly between two adjacent code blocks of different
// languages: close the current fence (if opened) and open the next one.
if (codeOpenFenceEmitted)
{
outputLines.Add ("```");
}

outputLines.Add ($"```{nextCodeLanguage ?? string.Empty}");
codeOpenFenceEmitted = true;
currentCodeLanguage = nextCodeLanguage;
}
}
else if (inCodeBlock)
{
// Leaving a code block: inject the closing fence
// Leaving a code block into non-code content: always inject the closing fence.
// The selection crosses the block's end boundary regardless of whether the
// opening fence was emitted (e.g., when the selection started inside the block).
outputLines.Add ("```");

inCodeBlock = false;
codeOpenFenceEmitted = false;
currentCodeLanguage = null;
selectionHasNonCodeContent = true;
}
else
{
selectionHasNonCodeContent = true;
}

if (line is { IsTable: true, TableData: { } tableData })
Expand Down Expand Up @@ -197,11 +234,6 @@ private string GetSelectedText ()
outputLines.Add (lineSb.ToString ());
}

if (inCodeBlock)
{
outputLines.Add ("```");
}

return string.Join ("\n", outputLines);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,20 +462,23 @@ public void PartialSelection_TaskList_SelectedText_Preserves_Markdown_Markers ()
}

// Copilot - partial drag selection spanning lines inside a csharp code block (with 🌍)
// The selection covers all code lines but no non-code content, so no fence delimiters
// should appear in the output (mirrors the copy-button behaviour on MarkdownCodeBlock).
Comment thread
YourRobotOverlord marked this conversation as resolved.
[Fact]
public void PartialSelection_FencedCodeBlock_SelectedText_Preserves_Fence_Context ()
public void PartialSelection_FencedCodeBlock_SelectedText_DoesNotIncludeFence ()
{
string md = "```csharp\nConsole.WriteLine (\"Hello, Terminal.Gui! 🌍\");\nvar x = 42;\n```";
(IApplication app, Runnable window, Terminal.Gui.Views.Markdown mv) = CreateMv (md, width: 60, height: 10);

// Select both code lines (rendered as lines 0 and 1 — fence lines are not in _renderedLines)
// Select both code lines (rendered as lines 0 and 1 — fence lines are not in _renderedLines).
// End at x=10 (one short of "var x = 42;" width=11) so IsFullDocumentSelected() returns false.
mv.NewMouseEvent (new Mouse { Position = new Point (0, 0), Flags = MouseFlags.LeftButtonPressed });
mv.NewMouseEvent (new Mouse { Position = new Point (12, 1), Flags = MouseFlags.LeftButtonPressed | MouseFlags.PositionReport });
mv.NewMouseEvent (new Mouse { Position = new Point (10, 1), Flags = MouseFlags.LeftButtonPressed | MouseFlags.PositionReport });

string? selected = mv.SelectedText;

Assert.NotNull (selected);
Assert.Contains ("```csharp", selected);
Assert.DoesNotContain ("```", selected);
Assert.Contains ("Console.WriteLine", selected);
Assert.Contains ("🌍", selected);

Expand Down Expand Up @@ -764,6 +767,122 @@ public void PartialSelection_DocEndingWithTable_NotTreatedAsFullDocument ()
app.Dispose ();
}

// --- Issue #5273: partial selection inside a code block must not include fence delimiters ---

// Copilot - Claude Sonnet 4.6
// Selecting only middle lines of a multi-line code block should not produce fence delimiters.
[Fact]
public void PartialSelection_InsideCodeBlock_DoesNotIncludeFenceDelimiters ()
{
// 4 code lines; select only the middle two (lines 1 and 2)
string md = "```csharp\nline A\nline B\nline C\nline D\n```";
(IApplication app, Runnable window, Terminal.Gui.Views.Markdown mv) = CreateMv (md, width: 60, height: 10);

// Rendered lines 0-3 are the four code lines (fence lines are stripped during parse).
// Press on line 1, drag to end of line 2.
mv.NewMouseEvent (new Mouse { Position = new Point (0, 1), Flags = MouseFlags.LeftButtonPressed });
mv.NewMouseEvent (new Mouse { Position = new Point (60, 2), Flags = MouseFlags.LeftButtonPressed | MouseFlags.PositionReport });

string? selected = mv.SelectedText;

Assert.NotNull (selected);
Assert.Contains ("line B", selected);
Assert.Contains ("line C", selected);
Assert.DoesNotContain ("```", selected);

window.Dispose ();
app.Dispose ();
}

// Copilot - Claude Sonnet 4.6
// Selection starts before the code block (on a paragraph line) and ends inside it.
// Only the opening fence should be present; the closing fence must be omitted.
[Fact]
public void PartialSelection_StartBeforeCodeBlock_EndInside_HasOpeningFenceOnly ()
{
string md = "Before\n```csharp\nline A\nline B\nline C\n```";
(IApplication app, Runnable window, Terminal.Gui.Views.Markdown mv) = CreateMv (md, width: 60, height: 10);

// Rendered line 0 = "Before", lines 1-3 = code lines.
// Press on line 0, drag to line 2 (mid-block).
mv.NewMouseEvent (new Mouse { Position = new Point (0, 0), Flags = MouseFlags.LeftButtonPressed });
mv.NewMouseEvent (new Mouse { Position = new Point (60, 2), Flags = MouseFlags.LeftButtonPressed | MouseFlags.PositionReport });

string? selected = mv.SelectedText;

Assert.NotNull (selected);
Assert.Contains ("Before", selected);
Assert.Contains ("```csharp", selected);
Assert.Contains ("line A", selected);
Assert.Contains ("line B", selected);
Assert.DoesNotContain ("line C", selected);

// Opening fence present; closing fence absent because selection ends mid-block.
int fenceCount = CountOccurrences (selected, "```");
Assert.Equal (1, fenceCount);

window.Dispose ();
app.Dispose ();
}

// Copilot - Claude Sonnet 4.6
// Selection starts inside a code block and ends after it (on a paragraph line).
// A closing fence is expected because the selection crosses the block's end, even
// though no opening fence was emitted (the selection began inside the block).
[Fact]
public void PartialSelection_StartInsideCodeBlock_EndAfter_HasClosingFenceOnly ()
{
string md = "```csharp\nline A\nline B\nline C\n```\nAfter";
(IApplication app, Runnable window, Terminal.Gui.Views.Markdown mv) = CreateMv (md, width: 60, height: 10);

// Rendered lines 0-2 = code lines, line 3 = "After".
// Press on line 1 (mid-block), drag to line 3 (after block).
mv.NewMouseEvent (new Mouse { Position = new Point (0, 1), Flags = MouseFlags.LeftButtonPressed });
mv.NewMouseEvent (new Mouse { Position = new Point (60, 3), Flags = MouseFlags.LeftButtonPressed | MouseFlags.PositionReport });

string? selected = mv.SelectedText;

Assert.NotNull (selected);
Assert.Contains ("line B", selected);
Assert.Contains ("line C", selected);
Assert.Contains ("After", selected);
Assert.DoesNotContain ("line A", selected);

// Closing fence present because selection crosses out of the code block; no opening fence.
int fenceCount = CountOccurrences (selected, "```");
Assert.Equal (1, fenceCount);

window.Dispose ();
app.Dispose ();
}

// Copilot - Claude Sonnet 4.6
// Regression guard: selecting all lines of a code block starting from its first line should
// produce NO fence delimiters — the selection is entirely within the fenced region.
[Fact]
public void PartialSelection_AllLinesOfCodeBlock_FromFirstLine_NoFences ()
{
// Three code lines; select only the first two to avoid triggering IsFullDocumentSelected().
string md = "```csharp\nline A\nline B\nline C\n```";
(IApplication app, Runnable window, Terminal.Gui.Views.Markdown mv) = CreateMv (md, width: 60, height: 10);

Comment on lines +860 to +868
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

// Press on line 0 (first code line), drag to end of line 1.
// end.Y=1 < lastLine=2, so IsFullDocumentSelected() returns false and partial-selection runs.
mv.NewMouseEvent (new Mouse { Position = new Point (0, 0), Flags = MouseFlags.LeftButtonPressed });
mv.NewMouseEvent (new Mouse { Position = new Point (6, 1), Flags = MouseFlags.LeftButtonPressed | MouseFlags.PositionReport });

string? selected = mv.SelectedText;

Assert.NotNull (selected);
Assert.Contains ("line A", selected);
Assert.Contains ("line B", selected);
Assert.DoesNotContain ("line C", selected);
Assert.DoesNotContain ("```", selected);

window.Dispose ();
app.Dispose ();
}

private static int CountOccurrences (string text, string pattern)
{
int count = 0;
Expand Down
Loading