Repository: microsoft/terminal (language: cpp)

Issue:
Scrollbar marks for shell integration do not clear when you use clear/cls commands
### Windows Terminal version

1.18.1421.0

### Windows build number

Version 10.0.22621.1702

### Other Software

PowerShell 7.3.4

### Steps to reproduce

1. Enable shell integration as documented here: https://learn.microsoft.com/en-us/windows/terminal/tutorials/shell-integration
2. Enable PWSH and CMD support (for PWSH I added it to my Documents\PowerShell\Profile.ps1)
3. Open new PWSH and CMD shells and press enter a bunch of times to generate a bunch of marks.
4. Type clear in PWSH or cls in CMD
6. Type commands that generate output like dir 
7. Press Ctrl+Up/Down a bunch

### Expected Behavior

The scrollbar marks should disappear on clear/cls and the Ctrl+Up/Down should navigate only through the visible commands

### Actual Behavior

The marks remain on the scrollbar and they will still be jumped to even though they are in the middle of command output.


Discussion / hints:
Sure don't. I think this is noted in #11000 but I'll make sure #15057 links this specifically. Thanks!

Fix patch:
diff --git a/src/buffer/out/textBuffer.cpp b/src/buffer/out/textBuffer.cpp
index f41a1442721..c9269f08c43 100644
--- a/src/buffer/out/textBuffer.cpp
+++ b/src/buffer/out/textBuffer.cpp
@@ -2660,6 +2660,9 @@ try
     // Set size back to real size as it will be taking over the rendering duties.
     newCursor.SetSize(ulSize);
 
+    newBuffer._marks = oldBuffer._marks;
+    newBuffer._trimMarksOutsideBuffer();
+
     return S_OK;
 }
 CATCH_RETURN()
@@ -2869,3 +2872,114 @@ PointTree TextBuffer::GetPatterns(const til::CoordType firstRow, const til::Coor
     PointTree result(std::move(intervals));
     return result;
 }
+
+const std::vector<ScrollMark>& TextBuffer::GetMarks() const noexcept
+{
+    return _marks;
+}
+
+// Remove all marks between `start` & `end`, inclusive.
+void TextBuffer::ClearMarksInRange(
+    const til::point start,
+    const til::point end)
+{
+    auto inRange = [&start, &end](const ScrollMark& m) {
+        return (m.start >= start && m.start <= end) ||
+               (m.end >= start && m.end <= end);
+    };
+
+    _marks.erase(std::remove_if(_marks.begin(),
+                                _marks.end(),
+                                inRange),
+                 _marks.end());
+}
+void TextBuffer::ClearAllMarks() noexcept
+{
+    _marks.clear();
+}
+
+// Adjust all the marks in the y-direction by `delta`. Positive values move the
+// marks down (the positive y direction). Negative values move up. This will
+// trim marks that are no longer have a start in the bounds of the buffer
+void TextBuffer::ScrollMarks(const int delta)
+{
+    for (auto& mark : _marks)
+    {
+        mark.start.y += delta;
+
+        // If the mark had sub-regions, then move those pointers too
+        if (mark.commandEnd.has_value())
+        {
+            (*mark.commandEnd).y += delta;
+        }
+        if (mark.outputEnd.has_value())
+        {
+            (*mark.outputEnd).y += delta;
+        }
+    }
+    _trimMarksOutsideBuffer();
+}
+
+// Method Description:
+// - Add a mark to our list of marks, and treat it as the active "prompt". For
+//   the sake of shell integration, we need to know which mark represents the
+//   current prompt/command/output. Internally, we'll always treat the _last_
+//   mark in the list as the current prompt.
+// Arguments:
+// - m: the mark to add.
+void TextBuffer::StartPromptMark(const ScrollMark& m)
+{
+    _marks.push_back(m);
+}
+// Method Description:
+// - Add a mark to our list of marks. Don't treat this as the active prompt.
+//   This should be used for marks created by the UI or from other user input.
+//   By inserting at the start of the list, we can separate out marks that were
+//   generated by client programs vs ones created by the user.
+// Arguments:
+// - m: the mark to add.
+void TextBuffer::AddMark(const ScrollMark& m)
+{
+    _marks.insert(_marks.begin(), m);
+}
+
+void TextBuffer::_trimMarksOutsideBuffer()
+{
+    const auto height = GetSize().Height();
+    _marks.erase(std::remove_if(_marks.begin(),
+                                _marks.end(),
+                                [height](const auto& m) {
+                                    return (m.start.y < 0) ||
+                                           (m.start.y >= height);
+                                }),
+                 _marks.end());
+}
+
+void TextBuffer::SetCurrentPromptEnd(const til::point pos) noexcept
+{
+    if (_marks.empty())
+    {
+        return;
+    }
+    auto& curr{ _marks.back() };
+    curr.end = pos;
+}
+void TextBuffer::SetCurrentCommandEnd(const til::point pos) noexcept
+{
+    if (_marks.empty())
+    {
+        return;
+    }
+    auto& curr{ _marks.back() };
+    curr.commandEnd = pos;
+}
+void TextBuffer::SetCurrentOutputEnd(const til::point pos, ::MarkCategory category) noexcept
+{
+    if (_marks.empty())
+    {
+        return;
+    }
+    auto& curr{ _marks.back() };
+    curr.outputEnd = pos;
+    curr.category = category;
+}
diff --git a/src/buffer/out/textBuffer.hpp b/src/buffer/out/textBuffer.hpp
index 5efeaaa741d..49c8bc5f29e 100644
--- a/src/buffer/out/textBuffer.hpp
+++ b/src/buffer/out/textBuffer.hpp
@@ -64,6 +64,41 @@ namespace Microsoft::Console::Render
     class Renderer;
 }
 
+enum class MarkCategory
+{
+    Prompt = 0,
+    Error = 1,
+    Warning = 2,
+    Success = 3,
+    Info = 4
+};
+struct ScrollMark
+{
+    std::optional<til::color> color;
+    til::point start;
+    til::point end; // exclusive
+    std::optional<til::point> commandEnd;
+    std::optional<til::point> outputEnd;
+
+    MarkCategory category{ MarkCategory::Info };
+    // Other things we may want to think about in the future are listed in
+    // GH#11000
+
+    bool HasCommand() const noexcept
+    {
+        return commandEnd.has_value() && *commandEnd != end;
+    }
+    bool HasOutput() const noexcept
+    {
+        return outputEnd.has_value() && *outputEnd != *commandEnd;
+    }
+    std::pair<til::point, til::point> GetExtent() const
+    {
+        til::point realEnd{ til::coalesce_value(outputEnd, commandEnd, end) };
+        return std::make_pair(til::point{ start }, realEnd);
+    }
+};
+
 class TextBuffer final
 {
 public:
@@ -228,6 +263,16 @@ class TextBuffer final
     void CopyPatterns(const TextBuffer& OtherBuffer);
     interval_tree::IntervalTree<til::point, size_t> GetPatterns(const til::CoordType firstRow, const til::CoordType lastRow) const;
 
+    const std::vector<ScrollMark>& GetMarks() const noexcept;
+    void ClearMarksInRange(const til::point start, const til::point end);
+    void ClearAllMarks() noexcept;
+    void ScrollMarks(const int delta);
+    void StartPromptMark(const ScrollMark& m);
+    void AddMark(const ScrollMark& m);
+    void SetCurrentPromptEnd(const til::point pos) noexcept;
+    void SetCurrentCommandEnd(const til::point pos) noexcept;
+    void SetCurrentOutputEnd(const til::point pos, ::MarkCategory category) noexcept;
+
 private:
     void _reserve(til::size screenBufferSize, const TextAttribute& defaultAttributes);
     void _commit(const std::byte* row);
@@ -251,6 +296,7 @@ class TextBuffer final
     til::point _GetWordEndForAccessibility(const til::point target, const std::wstring_view wordDelimiters, const til::point limit) const;
     til::point _GetWordEndForSelection(const til::point target, const std::wstring_view wordDelimiters) const;
     void _PruneHyperlinks();
+    void _trimMarksOutsideBuffer();
 
     static void _AppendRTFText(std::ostringstream& contentBuilder, const std::wstring_view& text);
 
@@ -327,6 +373,8 @@ class TextBuffer final
 
     bool _isActiveBuffer = false;
 
+    std::vector<ScrollMark> _marks;
+
 #ifdef UNIT_TESTING
     friend class TextBufferTests;
     friend class UiaTextRangeTests;
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 4aabcfa1414..870b8f9fed5 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -2111,7 +2111,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
     void ControlCore::AddMark(const Control::ScrollMark& mark)
     {
-        ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark m{};
+        ::ScrollMark m{};
 
         if (mark.Color.HasValue)
         {
@@ -2140,7 +2140,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         const auto currentOffset = ScrollOffset();
         const auto& marks{ _terminal->GetScrollMarks() };
 
-        std::optional<DispatchTypes::ScrollMark> tgt;
+        std::optional<::ScrollMark> tgt;
 
         switch (direction)
         {
@@ -2243,7 +2243,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
                                                   _terminal->GetTextBuffer().GetCursor().GetPosition();
 
-        std::optional<DispatchTypes::ScrollMark> nearest{ std::nullopt };
+        std::optional<::ScrollMark> nearest{ std::nullopt };
         const auto& marks{ _terminal->GetScrollMarks() };
 
         // Early return so we don't have to check for the validity of `nearest` below after the loop exits.
@@ -2283,7 +2283,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         const til::point start = HasSelection() ? (goUp ? _terminal->GetSelectionAnchor() : _terminal->GetSelectionEnd()) :
                                                   _terminal->GetTextBuffer().GetCursor().GetPosition();
 
-        std::optional<DispatchTypes::ScrollMark> nearest{ std::nullopt };
+        std::optional<::ScrollMark> nearest{ std::nullopt };
         const auto& marks{ _terminal->GetScrollMarks() };
 
         static constexpr til::point worst{ til::CoordTypeMax, til::CoordTypeMax };
@@ -2357,8 +2357,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
     void ControlCore::_contextMenuSelectMark(
         const til::point& pos,
-        bool (*filter)(const DispatchTypes::ScrollMark&),
-        til::point_span (*getSpan)(const DispatchTypes::ScrollMark&))
+        bool (*filter)(const ::ScrollMark&),
+        til::point_span (*getSpan)(const ::ScrollMark&))
     {
         // Do nothing if the caller didn't give us a way to get the span to select for this mark.
         if (!getSpan)
@@ -2391,20 +2391,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     {
         _contextMenuSelectMark(
             _contextMenuBufferPosition,
-            [](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasCommand(); },
-            [](const DispatchTypes::ScrollMark& m) { return til::point_span{ m.end, *m.commandEnd }; });
+            [](const ::ScrollMark& m) -> bool { return !m.HasCommand(); },
+            [](const ::ScrollMark& m) { return til::point_span{ m.end, *m.commandEnd }; });
     }
     void ControlCore::ContextMenuSelectOutput()
     {
         _contextMenuSelectMark(
             _contextMenuBufferPosition,
-            [](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasOutput(); },
-            [](const DispatchTypes::ScrollMark& m) { return til::point_span{ *m.commandEnd, *m.outputEnd }; });
+            [](const ::ScrollMark& m) -> bool { return !m.HasOutput(); },
+            [](const ::ScrollMark& m) { return til::point_span{ *m.commandEnd, *m.outputEnd }; });
     }
 
     bool ControlCore::_clickedOnMark(
         const til::point& pos,
-        bool (*filter)(const DispatchTypes::ScrollMark&))
+        bool (*filter)(const ::ScrollMark&))
     {
         // Don't show this if the click was on the selection
         if (_terminal->IsSelectionActive() &&
@@ -2442,7 +2442,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     {
         // Relies on the anchor set in AnchorContextMenu
         return _clickedOnMark(_contextMenuBufferPosition,
-                              [](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasCommand(); });
+                              [](const ::ScrollMark& m) -> bool { return !m.HasCommand(); });
     }
 
     // Method Description:
@@ -2451,6 +2451,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     {
         // Relies on the anchor set in AnchorContextMenu
         return _clickedOnMark(_contextMenuBufferPosition,
-                              [](const DispatchTypes::ScrollMark& m) -> bool { return !m.HasOutput(); });
+                              [](const ::ScrollMark& m) -> bool { return !m.HasOutput(); });
     }
 }
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index dbb92c0f781..d26fb0ec541 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -372,10 +372,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         void _contextMenuSelectMark(
             const til::point& pos,
-            bool (*filter)(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark&),
-            til::point_span (*getSpan)(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark&));
+            bool (*filter)(const ::ScrollMark&),
+            til::point_span (*getSpan)(const ::ScrollMark&));
 
-        bool _clickedOnMark(const til::point& pos, bool (*filter)(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark&));
+        bool _clickedOnMark(const til::point& pos, bool (*filter)(const ::ScrollMark&));
 
         inline bool _IsClosing() const noexcept
         {
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index e21f726f451..d475966350e 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -1344,7 +1344,7 @@ void Terminal::_updateUrlDetection()
 }
 
 // NOTE: This is the version of AddMark that comes from the UI. The VT api call into this too.
-void Terminal::AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark,
+void Terminal::AddMark(const ScrollMark& mark,
                        const til::point& start,
                        const til::point& end,
                        const bool fromUi)
@@ -1354,17 +1354,18 @@ void Terminal::AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes:
         return;
     }
 
-    DispatchTypes::ScrollMark m = mark;
+    ScrollMark m = mark;
     m.start = start;
     m.end = end;
 
+    // If the mark came from the user adding a mark via the UI, don't make it the active prompt mark.
     if (fromUi)
     {
-        _scrollMarks.insert(_scrollMarks.begin(), m);
+        _activeBuffer().AddMark(m);
     }
     else
     {
-        _scrollMarks.push_back(m);
+        _activeBuffer().StartPromptMark(m);
     }
 
     // Tell the control that the scrollbar has somehow changed. Used as a
@@ -1389,15 +1390,7 @@ void Terminal::ClearMark()
         start = til::point{ GetSelectionAnchor() };
         end = til::point{ GetSelectionEnd() };
     }
-    auto inSelection = [&start, &end](const DispatchTypes::ScrollMark& m) {
-        return (m.start >= start && m.start <= end) ||
-               (m.end >= start && m.end <= end);
-    };
-
-    _scrollMarks.erase(std::remove_if(_scrollMarks.begin(),
-                                      _scrollMarks.end(),
-                                      inSelection),
-                       _scrollMarks.end());
+    _activeBuffer().ClearMarksInRange(start, end);
 
     // Tell the control that the scrollbar has somehow changed. Used as a
     // workaround to force the control to redraw any scrollbar marks
@@ -1405,23 +1398,22 @@ void Terminal::ClearMark()
 }
 void Terminal::ClearAllMarks() noexcept
 {
-    _scrollMarks.clear();
+    _activeBuffer().ClearAllMarks();
     // Tell the control that the scrollbar has somehow changed. Used as a
     // workaround to force the control to redraw any scrollbar marks
     _NotifyScrollEvent();
 }
 
-const std::vector<DispatchTypes::ScrollMark>& Terminal::GetScrollMarks() const noexcept
+const std::vector<ScrollMark>& Terminal::GetScrollMarks() const noexcept
 {
     // TODO: GH#11000 - when the marks are stored per-buffer, get rid of this.
     // We want to return _no_ marks when we're in the alt buffer, to effectively
     // hide them. We need to return a reference, so we can't just ctor an empty
     // list here just for when we're in the alt buffer.
-    static const std::vector<DispatchTypes::ScrollMark> _altBufferMarks{};
-    return _inAltBuffer() ? _altBufferMarks : _scrollMarks;
+    return _activeBuffer().GetMarks();
 }
 
-til::color Terminal::GetColorForMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) const
+til::color Terminal::GetColorForMark(const ScrollMark& mark) const
 {
     if (mark.color.has_value())
     {
@@ -1430,24 +1422,24 @@ til::color Terminal::GetColorForMark(const Microsoft::Console::VirtualTerminal::
 
     switch (mark.category)
     {
-    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Prompt:
+    case MarkCategory::Prompt:
     {
         return _renderSettings.GetColorAlias(ColorAlias::DefaultForeground);
     }
-    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Error:
+    case MarkCategory::Error:
     {
         return _renderSettings.GetColorTableEntry(TextColor::BRIGHT_RED);
     }
-    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Warning:
+    case MarkCategory::Warning:
     {
         return _renderSettings.GetColorTableEntry(TextColor::BRIGHT_YELLOW);
     }
-    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Success:
+    case MarkCategory::Success:
     {
         return _renderSettings.GetColorTableEntry(TextColor::BRIGHT_GREEN);
     }
     default:
-    case Microsoft::Console::VirtualTerminal::DispatchTypes::MarkCategory::Info:
+    case MarkCategory::Info:
     {
         return _renderSettings.GetColorAlias(ColorAlias::DefaultForeground);
     }
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index 7db645b2357..fc392bc7e0c 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -97,8 +97,8 @@ class Microsoft::Terminal::Core::Terminal final :
     RenderSettings& GetRenderSettings() noexcept { return _renderSettings; };
     const RenderSettings& GetRenderSettings() const noexcept { return _renderSettings; };
 
-    const std::vector<Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark>& GetScrollMarks() const noexcept;
-    void AddMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark,
+    const std::vector<ScrollMark>& GetScrollMarks() const noexcept;
+    void AddMark(const ScrollMark& mark,
                  const til::point& start,
                  const til::point& end,
                  const bool fromUi);
@@ -127,7 +127,7 @@ class Microsoft::Terminal::Core::Terminal final :
     void UseAlternateScreenBuffer(const TextAttribute& attrs) override;
     void UseMainScreenBuffer() override;
 
-    void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override;
+    void MarkPrompt(const ScrollMark& mark) override;
     void MarkCommandStart() override;
     void MarkOutputStart() override;
     void MarkCommandFinish(std::optional<unsigned int> error) override;
@@ -140,7 +140,7 @@ class Microsoft::Terminal::Core::Terminal final :
 
     void ClearMark();
     void ClearAllMarks() noexcept;
-    til::color GetColorForMark(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) const;
+    til::color GetColorForMark(const ScrollMark& mark) const;
 
 #pragma region ITerminalInput
     // These methods are defined in Terminal.cpp
@@ -393,7 +393,6 @@ class Microsoft::Terminal::Core::Terminal final :
     };
     std::optional<KeyEventCodes> _lastKeyEventCodes;
 
-    std::vector<Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark> _scrollMarks;
     enum class PromptState : uint32_t
     {
         None = 0,
diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp
index f8f9191bae5..a32a1ddb496 100644
--- a/src/cascadia/TerminalCore/TerminalApi.cpp
+++ b/src/cascadia/TerminalCore/TerminalApi.cpp
@@ -291,7 +291,7 @@ void Terminal::UseMainScreenBuffer()
 }
 
 // NOTE: This is the version of AddMark that comes from VT
-void Terminal::MarkPrompt(const DispatchTypes::ScrollMark& mark)
+void Terminal::MarkPrompt(const ScrollMark& mark)
 {
     static bool logged = false;
     if (!logged)
@@ -309,7 +309,7 @@ void Terminal::MarkPrompt(const DispatchTypes::ScrollMark& mark)
     const til::point cursorPos{ _activeBuffer().GetCursor().GetPosition() };
     AddMark(mark, cursorPos, cursorPos, false);
 
-    if (mark.category == DispatchTypes::MarkCategory::Prompt)
+    if (mark.category == MarkCategory::Prompt)
     {
         _currentPromptState = PromptState::Prompt;
     }
@@ -320,7 +320,7 @@ void Terminal::MarkCommandStart()
     const til::point cursorPos{ _activeBuffer().GetCursor().GetPosition() };
 
     if ((_currentPromptState == PromptState::Prompt) &&
-        (_scrollMarks.size() > 0))
+        (_activeBuffer().GetMarks().size() > 0))
     {
         // We were in the right state, and there's a previous mark to work
         // with.
@@ -333,11 +333,11 @@ void Terminal::MarkCommandStart()
         // then abandon the current state, and just insert a new Prompt mark that
         // start's & ends here, and got to State::Command.
 
-        DispatchTypes::ScrollMark mark;
-        mark.category = DispatchTypes::MarkCategory::Prompt;
+        ScrollMark mark;
+        mark.category = MarkCategory::Prompt;
         AddMark(mark, cursorPos, cursorPos, false);
     }
-    _scrollMarks.back().end = cursorPos;
+    _activeBuffer().SetCurrentPromptEnd(cursorPos);
     _currentPromptState = PromptState::Command;
 }
 
@@ -346,7 +346,7 @@ void Terminal::MarkOutputStart()
     const til::point cursorPos{ _activeBuffer().GetCursor().GetPosition() };
 
     if ((_currentPromptState == PromptState::Command) &&
-        (_scrollMarks.size() > 0))
+        (_activeBuffer().GetMarks().size() > 0))
     {
         // We were in the right state, and there's a previous mark to work
         // with.
@@ -359,27 +359,27 @@ void Terminal::MarkOutputStart()
         // then abandon the current state, and just insert a new Prompt mark that
         // start's & ends here, and the command ends here, and go to State::Output.
 
-        DispatchTypes::ScrollMark mark;
-        mark.category = DispatchTypes::MarkCategory::Prompt;
+        ScrollMark mark;
+        mark.category = MarkCategory::Prompt;
         AddMark(mark, cursorPos, cursorPos, false);
     }
-    _scrollMarks.back().commandEnd = cursorPos;
+    _activeBuffer().SetCurrentCommandEnd(cursorPos);
     _currentPromptState = PromptState::Output;
 }
 
 void Terminal::MarkCommandFinish(std::optional<unsigned int> error)
 {
     const til::point cursorPos{ _activeBuffer().GetCursor().GetPosition() };
-    auto category = DispatchTypes::MarkCategory::Prompt;
+    auto category = MarkCategory::Prompt;
     if (error.has_value())
     {
         category = *error == 0u ?
-                       DispatchTypes::MarkCategory::Success :
-                       DispatchTypes::MarkCategory::Error;
+                       MarkCategory::Success :
+                       MarkCategory::Error;
     }
 
     if ((_currentPromptState == PromptState::Output) &&
-        (_scrollMarks.size() > 0))
+        (_activeBuffer().GetMarks().size() > 0))
     {
         // We were in the right state, and there's a previous mark to work
         // with.
@@ -393,13 +393,12 @@ void Terminal::MarkCommandFinish(std::optional<unsigned int> error)
         // ends here, and the command ends here, AND the output ends here. and
         // go to State::Output.
 
-        DispatchTypes::ScrollMark mark;
-        mark.category = DispatchTypes::MarkCategory::Prompt;
+        ScrollMark mark;
+        mark.category = MarkCategory::Prompt;
         AddMark(mark, cursorPos, cursorPos, false);
-        _scrollMarks.back().commandEnd = cursorPos;
+        _activeBuffer().SetCurrentCommandEnd(cursorPos);
     }
-    _scrollMarks.back().outputEnd = cursorPos;
-    _scrollMarks.back().category = category;
+    _activeBuffer().SetCurrentOutputEnd(cursorPos, category);
     _currentPromptState = PromptState::None;
 }
 
@@ -458,29 +457,11 @@ void Terminal::NotifyBufferRotation(const int delta)
     // manually erase our pattern intervals since the locations have changed now
     _patternIntervalTree = {};
 
-    const auto hasScrollMarks = _scrollMarks.size() > 0;
+    auto& marks{ _activeBuffer().GetMarks() };
+    const auto hasScrollMarks = marks.size() > 0;
     if (hasScrollMarks)
     {
-        for (auto& mark : _scrollMarks)
-        {
-            // Move the mark up
-            mark.start.y -= delta;
-
-            // If the mark had sub-regions, then move those pointers too
-            if (mark.commandEnd.has_value())
-            {
-                (*mark.commandEnd).y -= delta;
-            }
-            if (mark.outputEnd.has_value())
-            {
-                (*mark.outputEnd).y -= delta;
-            }
-        }
-
-        _scrollMarks.erase(std::remove_if(_scrollMarks.begin(),
-                                          _scrollMarks.end(),
-                                          [](const auto& m) { return m.start.y < 0; }),
-                           _scrollMarks.end());
+        _activeBuffer().ScrollMarks(-delta);
     }
 
     const auto oldScrollOffset = _scrollOffset;
diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp
index 01145bd413a..e20f1e73f99 100644
--- a/src/host/outputStream.cpp
+++ b/src/host/outputStream.cpp
@@ -435,7 +435,7 @@ void ConhostInternalGetSet::NotifyBufferRotation(const int delta)
     }
 }
 
-void ConhostInternalGetSet::MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/)
+void ConhostInternalGetSet::MarkPrompt(const ::ScrollMark& /*mark*/)
 {
     // Not implemented for conhost.
 }
diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp
index 612f179c700..4a271685541 100644
--- a/src/host/outputStream.hpp
+++ b/src/host/outputStream.hpp
@@ -69,7 +69,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal::
     void NotifyAccessibilityChange(const til::rect& changedRect) override;
     void NotifyBufferRotation(const int delta) override;
 
-    void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) override;
+    void MarkPrompt(const ScrollMark& mark) override;
     void MarkCommandStart() override;
     void MarkOutputStart() override;
     void MarkCommandFinish(std::optional<unsigned int> error) override;
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 81864c0c503..3673aa09d0a 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -654,39 +654,4 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
     constexpr VTInt s_sDECCOLMSetColumns = 132;
     constexpr VTInt s_sDECCOLMResetColumns = 80;
 
-    enum class MarkCategory : size_t
-    {
-        Prompt = 0,
-        Error = 1,
-        Warning = 2,
-        Success = 3,
-        Info = 4
-    };
-
-    struct ScrollMark
-    {
-        std::optional<til::color> color;
-        til::point start;
-        til::point end; // exclusive
-        std::optional<til::point> commandEnd;
-        std::optional<til::point> outputEnd;
-
-        MarkCategory category{ MarkCategory::Info };
-        // Other things we may want to think about in the future are listed in
-        // GH#11000
-
-        bool HasCommand() const noexcept
-        {
-            return commandEnd.has_value() && *commandEnd != end;
-        }
-        bool HasOutput() const noexcept
-        {
-            return outputEnd.has_value() && *outputEnd != *commandEnd;
-        }
-        std::pair<til::point, til::point> GetExtent() const
-        {
-            til::point realEnd{ til::coalesce_value(outputEnd, commandEnd, end) };
-            return std::make_pair(til::point{ start }, realEnd);
-        }
-    };
 }
diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp
index e2faff39616..1d81c5a182c 100644
--- a/src/terminal/adapter/ITerminalApi.hpp
+++ b/src/terminal/adapter/ITerminalApi.hpp
@@ -81,7 +81,7 @@ namespace Microsoft::Console::VirtualTerminal
         virtual void NotifyAccessibilityChange(const til::rect& changedRect) = 0;
         virtual void NotifyBufferRotation(const int delta) = 0;
 
-        virtual void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& mark) = 0;
+        virtual void MarkPrompt(const ScrollMark& mark) = 0;
         virtual void MarkCommandStart() = 0;
         virtual void MarkOutputStart() = 0;
         virtual void MarkCommandFinish(std::optional<unsigned int> error) = 0;
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 16aa57f6d83..3f80931a3d7 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -3145,6 +3145,12 @@ bool AdaptDispatch::_EraseScrollback()
     auto& cursor = textBuffer.GetCursor();
     const auto row = cursor.GetPosition().y;
 
+    // Clear all the marks below the new viewport position.
+    textBuffer.ClearMarksInRange(til::point{ 0, height },
+                                 til::point{ bufferSize.width, bufferSize.height });
+    // Then scroll all the remaining marks up. This will trim ones that are now "outside" the buffer
+    textBuffer.ScrollMarks(-top);
+
     // Scroll the viewport content to the top of the buffer.
     textBuffer.ScrollRows(top, height, -top);
     // Clear everything after the viewport.
@@ -3227,6 +3233,10 @@ bool AdaptDispatch::_EraseAll()
     // Also reset the line rendition for the erased rows.
     textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom);
 
+    // Clear any marks that remain below the start of the
+    textBuffer.ClearMarksInRange(til::point{ 0, newViewportTop },
+                                 til::point{ bufferSize.Width(), bufferSize.Height() });
+
     // GH#5683 - If this succeeded, but we're in a conpty, return `false` to
     // make the state machine propagate this ED sequence to the connected
     // terminal application. While we're in conpty mode, when the client
@@ -3633,8 +3643,8 @@ bool AdaptDispatch::DoITerm2Action(const std::wstring_view string)
 
     if (action == L"SetMark")
     {
-        DispatchTypes::ScrollMark mark;
-        mark.category = DispatchTypes::MarkCategory::Prompt;
+        ScrollMark mark;
+        mark.category = MarkCategory::Prompt;
         _api.MarkPrompt(mark);
         return true;
     }
@@ -3681,8 +3691,8 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string)
         case L'A': // FTCS_PROMPT
         {
             // Simply just mark this line as a prompt line.
-            DispatchTypes::ScrollMark mark;
-            mark.category = DispatchTypes::MarkCategory::Prompt;
+            ScrollMark mark;
+            mark.category = MarkCategory::Prompt;
             _api.MarkPrompt(mark);
             return true;
         }
diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp
index 64f7378a6f4..c0be3a74e0f 100644
--- a/src/terminal/adapter/ut_adapter/adapterTest.cpp
+++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp
@@ -215,7 +215,7 @@ class TestGetSet final : public ITerminalApi
         Log::Comment(L"NotifyBufferRotation MOCK called...");
     }
 
-    void MarkPrompt(const Microsoft::Console::VirtualTerminal::DispatchTypes::ScrollMark& /*mark*/) override
+    void MarkPrompt(const ScrollMark& /*mark*/) override
     {
         Log::Comment(L"MarkPrompt MOCK called...");
     }


Test patch: