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
37 changes: 32 additions & 5 deletions Terminal.Gui/Views/Markdown/MarkdownView.Drawing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,25 @@ private void DrawSelectionOverlayOnSubViewRows ()
Rectangle viewportScreen = ViewportToScreen (new Rectangle (Point.Empty, Viewport.Size));
SetClip (new Region (viewportScreen));

SetAttribute (selAttr);
// Popovers draw before the MarkdownView in the application draw loop, so their menu
// items are already written to the screen buffer when we run. The SetClip call above
// resets the clip to allow drawing over SubView areas, but it also undoes the clip
// exclusion that the popover's DoDrawComplete registered for its drawn cells. Without
// a guard, we would overwrite those cells with stale ScreenContents graphemes, erasing
// the popover. (Paragraph-text selection is drawn in DrawRenderedLine / OnDrawingSubViews
// before the clip reset, so it naturally inherits the popover's exclusion and is safe.)
// Compute the popover's content rect (screen-relative) and skip any cells inside it.
Rectangle? popoverScreenRect = null;

if (App?.Popovers?.GetActivePopover () is View { Visible: true } popoverView)
{
View? popoverContent = popoverView.SubViews.FirstOrDefault (v => v.Visible);

if (popoverContent is { })
{
popoverScreenRect = popoverContent.Frame;
}
}

for (int lineIdx = startRow; lineIdx <= Math.Min (endRow, _renderedLines.Count - 1); lineIdx++)
{
Expand All @@ -112,7 +130,7 @@ private void DrawSelectionOverlayOnSubViewRows ()
}

int drawRow = lineIdx - Viewport.Y;
Point screenOrigin = ContentToScreen (new Point (0, drawRow));
Point screenOrigin = ContentToScreen (new Point (0, lineIdx));
int screenRow = screenOrigin.Y;
int screenStartCol = screenOrigin.X;
int cols = Viewport.Width;
Expand All @@ -126,13 +144,22 @@ private void DrawSelectionOverlayOnSubViewRows ()
continue;
}

string grapheme = contents [screenRow, sc].Grapheme;
if (popoverScreenRect is { } psr && psr.Contains (new Point (sc, screenRow)))
{
continue;
}

int contentX = col + Viewport.X;

if (string.IsNullOrEmpty (grapheme))
if (!IsInSelection (lineIdx, contentX))
{
grapheme = " ";
continue;
}

Cell cell = contents [screenRow, sc];
string grapheme = string.IsNullOrEmpty (cell.Grapheme) ? " " : cell.Grapheme;

SetAttribute (selAttr);
AddStr (col, drawRow, grapheme);
}
}
Expand Down
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
Loading
Loading