// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "AttrRow.hpp" // Routine Description: // - constructor // Arguments: // - cchRowWidth - the length of the default text attribute // - attr - the default text attribute // Return Value: // - constructed object // Note: will throw exception if unable to allocate memory for text attribute storage ATTR_ROW::ATTR_ROW(const UINT cchRowWidth, const TextAttribute attr) { _list.push_back(TextAttributeRun(cchRowWidth, attr)); _cchRowWidth = cchRowWidth; } // Routine Description: // - Sets all properties of the ATTR_ROW to default values // Arguments: // - attr - The default text attributes to use on text in this row. void ATTR_ROW::Reset(const TextAttribute attr) { _list.clear(); _list.push_back(TextAttributeRun(_cchRowWidth, attr)); } // Routine Description: // - Takes an existing row of attributes, and changes the length so that it fills the NewWidth. // If the new size is bigger, then the last attr is extended to fill the NewWidth. // If the new size is smaller, the runs are cut off to fit. // Arguments: // - oldWidth - The original width of the row. // - newWidth - The new width of the row. // Return Value: // - , throws exceptions on failures. void ATTR_ROW::Resize(const size_t newWidth) { THROW_HR_IF(E_INVALIDARG, 0 == newWidth); // Easy case. If the new row is longer, increase the length of the last run by how much new space there is. if (newWidth > _cchRowWidth) { // Get the attribute that covers the final column of old width. const auto runPos = FindAttrIndex(_cchRowWidth - 1, nullptr); auto& run = _list[runPos]; // Extend its length by the additional columns we're adding. run.SetLength(run.GetLength() + newWidth - _cchRowWidth); // Store that the new total width we represent is the new width. _cchRowWidth = newWidth; } // harder case: new row is shorter. else { // Get the attribute that covers the final column of the new width size_t CountOfAttr = 0; const auto runPos = FindAttrIndex(newWidth - 1, &CountOfAttr); auto& run = _list[runPos]; // CountOfAttr was given to us as "how many columns left from this point forward are covered by the returned run" // So if the original run was B5 covering a 5 size OldWidth and we have a NewWidth of 3 // then when we called FindAttrIndex, it returned the B5 as the pIndexedRun and a 2 for how many more segments it covers // after and including the 3rd column. // B5-2 = B3, which is what we desire to cover the new 3 size buffer. run.SetLength(run.GetLength() - CountOfAttr + 1); // Store that the new total width we represent is the new width. _cchRowWidth = newWidth; // Erase segments after the one we just updated. _list.erase(_list.cbegin() + runPos + 1, _list.cend()); // NOTE: Under some circumstances here, we have leftover run segments in memory or blank run segments // in memory. We're not going to waste time redimensioning the array in the heap. We're just noting that the useful // portions of it have changed. } } // Routine Description: // - returns a copy of the TextAttribute at the specified column // Arguments: // - column - the column to get the attribute for // Return Value: // - the text attribute at column // Note: // - will throw on error TextAttribute ATTR_ROW::GetAttrByColumn(const size_t column) const { return GetAttrByColumn(column, nullptr); } // Routine Description: // - returns a copy of the TextAttribute at the specified column // Arguments: // - column - the column to get the attribute for // - pApplies - if given, fills how long this attribute will apply for // Return Value: // - the text attribute at column // Note: // - will throw on error TextAttribute ATTR_ROW::GetAttrByColumn(const size_t column, size_t* const pApplies) const { THROW_HR_IF(E_INVALIDARG, column >= _cchRowWidth); const auto runPos = FindAttrIndex(column, pApplies); return _list[runPos].GetAttributes(); } // Routine Description: // - reports how many runs we have stored (to be used for some optimizations // Return Value: // - Count of runs. 1 means we have 1 color to represent the entire row. size_t ATTR_ROW::GetNumberOfRuns() const noexcept { return _list.size(); } // Routine Description: // - This routine finds the nth attribute in this ATTR_ROW. // Arguments: // - index - which attribute to find // - applies - on output, contains corrected length of indexed attr. // for example, if the attribute string was { 5, BLUE } and the requested // index was 3, CountOfAttr would be 2. // Return Value: // - const reference to attribute run object size_t ATTR_ROW::FindAttrIndex(const size_t index, size_t* const pApplies) const { FAIL_FAST_IF(!(index < _cchRowWidth)); // The requested index cannot be longer than the total length described by this set of Attrs. size_t cTotalLength = 0; FAIL_FAST_IF(!(_list.size() > 0)); // There should be a non-zero and positive number of items in the array. // Scan through the internal array from position 0 adding up the lengths that each attribute applies to auto runPos = _list.cbegin(); do { cTotalLength += runPos->GetLength(); if (cTotalLength > index) { // If we've just passed up the requested index with the length we added, break early break; } runPos++; } while (runPos < _list.cend()); // we should have broken before falling out the while case. // if we didn't break, then this ATTR_ROW wasn't filled with enough attributes for the entire row of characters FAIL_FAST_IF(runPos >= _list.cend()); // The remaining iterator position is the position of the attribute that is applicable at the position requested (index) // Calculate its remaining applicability if requested // The length on which the found attribute applies is the total length seen so far minus the index we were searching for. FAIL_FAST_IF(!(cTotalLength > index)); // The length of all attributes we counted up so far should be longer than the index requested or we'll underflow. if (nullptr != pApplies) { const auto attrApplies = cTotalLength - index; FAIL_FAST_IF(!(attrApplies > 0)); // An attribute applies for >0 characters // MSFT: 17130145 - will restore this and add a better assert to catch the real issue. //FAIL_FAST_IF(!(attrApplies <= _cchRowWidth)); // An attribute applies for a maximum of the total length available to us *pApplies = attrApplies; } return runPos - _list.cbegin(); } // Routine Description: // - Sets the attributes (colors) of all character positions from the given position through the end of the row. // Arguments: // - iStart - Starting index position within the row // - attr - Attribute (color) to fill remaining characters with // Return Value: // - bool ATTR_ROW::SetAttrToEnd(const UINT iStart, const TextAttribute attr) { size_t const length = _cchRowWidth - iStart; const TextAttributeRun run(length, attr); return SUCCEEDED(InsertAttrRuns({ &run, 1 }, iStart, _cchRowWidth - 1, _cchRowWidth)); } // Routine Description: // - Replaces all runs in the row with the given wToBeReplacedAttr with the new // attribute wReplaceWith. This method is used for replacing specifically // legacy attributes. // Arguments: // - wToBeReplacedAttr - the legacy attribute to replace in this row. // - wReplaceWith - the new value for the matching runs' attributes. // Return Value: // void ATTR_ROW::ReplaceLegacyAttrs(_In_ WORD wToBeReplacedAttr, _In_ WORD wReplaceWith) noexcept { TextAttribute ToBeReplaced; ToBeReplaced.SetFromLegacy(wToBeReplacedAttr); TextAttribute ReplaceWith; ReplaceWith.SetFromLegacy(wReplaceWith); ReplaceAttrs(ToBeReplaced, ReplaceWith); } // Method Description: // - Replaces all runs in the row with the given toBeReplacedAttr with the new // attribute replaceWith. // Arguments: // - toBeReplacedAttr - the attribute to replace in this row. // - replaceWith - the new value for the matching runs' attributes. // Return Value: // - void ATTR_ROW::ReplaceAttrs(const TextAttribute& toBeReplacedAttr, const TextAttribute& replaceWith) noexcept { for (auto& run : _list) { if (run.GetAttributes() == toBeReplacedAttr) { run.SetAttributes(replaceWith); } } } // Routine Description: // - Takes a array of attribute runs, and inserts them into this row from startIndex to endIndex. // - For example, if the current row was was [{4, BLUE}], the merge string // was [{ 2, RED }], with (StartIndex, EndIndex) = (1, 2), // then the row would modified to be = [{ 1, BLUE}, {2, RED}, {1, BLUE}]. // Arguments: // - rgInsertAttrs - The array of attrRuns to merge into this row. // - cInsertAttrs - The number of elements in rgInsertAttrs // - iStart - The index in the row to place the array of runs. // - iEnd - the final index of the merge runs // - BufferWidth - the width of the row. // Return Value: // - STATUS_NO_MEMORY if there wasn't enough memory to insert the runs // otherwise STATUS_SUCCESS if we were successful. [[nodiscard]] HRESULT ATTR_ROW::InsertAttrRuns(const std::basic_string_view newAttrs, const size_t iStart, const size_t iEnd, const size_t cBufferWidth) { // Definitions: // Existing Run = The run length encoded color array we're already storing in memory before this was called. // Insert Run = The run length encoded color array that someone is asking us to inject into our stored memory run. // New Run = The run length encoded color array that we have to allocate and rebuild to store internally // which will replace Existing Run at the end of this function. // Example: // cBufferWidth = 10. // Existing Run: R3 -> G5 -> B2 // Insert Run: Y1 -> N1 at iStart = 5 and iEnd = 6 // (rgInsertAttrs is a 2 length array with Y1->N1 in it and cInsertAttrs = 2) // Final Run: R3 -> G2 -> Y1 -> N1 -> G1 -> B2 // We'll need to know what the last valid column is for some calculations versus iEnd // because iEnd is specified to us as an inclusive index value. // Do the -1 math here now so we don't have to have -1s scattered all over this function. const size_t iLastBufferCol = cBufferWidth - 1; // If the insertion size is 1, do some pre-processing to // see if we can get this done quickly. if (newAttrs.size() == 1) { // Get the new color attribute we're trying to apply const TextAttribute NewAttr = newAttrs.at(0).GetAttributes(); // If the existing run was only 1 element... // ...and the new color is the same as the old, we don't have to do anything and can exit quick. if (_list.size() == 1 && _list.at(0).GetAttributes() == NewAttr) { return S_OK; } // .. otherwise if we internally have a list of 2 and we're about to insert a single color // it's probable that we're just walking left-to-right through the row and changing each // cell one at a time. // e.g. // AAAAABBBBBBB // AAAAAABBBBBB // AAAAAAABBBBB // Check for that circumstance by seeing if we're inserting a single run of the // left side color right at the boundary and just adjust the counts in the existing // two elements in our internal list. else if (_list.size() == 2 && newAttrs.at(0).GetLength() == 1) { auto left = _list.begin(); if (iStart == left->GetLength() && NewAttr == left->GetAttributes()) { auto right = left + 1; left->IncrementLength(); right->DecrementLength(); // If we just reduced the right half to zero, just erase it out of the list. if (right->GetLength() == 0) { _list.erase(right); } return S_OK; } } } // If we're about to cover the entire existing run with a new one, we can also make an optimization. if (iStart == 0 && iEnd == iLastBufferCol) { // Just dump what we're given over what we have and call it a day. _list.assign(newAttrs.cbegin(), newAttrs.cend()); return S_OK; } // In the worst case scenario, we will need a new run that is the length of // The existing run in memory + The new run in memory + 1. // This worst case occurs when we inject a new item in the middle of an existing run like so // Existing R3->B5->G2, Insertion Y2 starting at 5 (in the middle of the B5) // becomes R3->B2->Y2->B1->G2. // The original run was 3 long. The insertion run was 1 long. We need 1 more for the // fact that an existing piece of the run was split in half (to hold the latter half). const size_t cNewRun = _list.size() + newAttrs.size() + 1; std::vector newRun; newRun.resize(cNewRun); // We will start analyzing from the beginning of our existing run. // Use some pointers to keep track of where we are in walking through our runs. // Get the existing run that we'll be updating/manipulating. const auto existingRun = _list.begin(); auto pExistingRunPos = existingRun; const auto pExistingRunEnd = existingRun + _list.size(); auto pInsertRunPos = newAttrs.begin(); size_t cInsertRunRemaining = newAttrs.size(); auto pNewRunPos = newRun.begin(); size_t iExistingRunCoverage = 0; // Copy the existing run into the new buffer up to the "start index" where the new run will be injected. // If the new run starts at 0, we have nothing to copy from the beginning. if (iStart != 0) { // While we're less than the desired insertion position... while (iExistingRunCoverage < iStart) { // Add up how much length we can cover by copying an item from the existing run. iExistingRunCoverage += pExistingRunPos->GetLength(); // Copy it to the new run buffer and advance both pointers. *pNewRunPos++ = *pExistingRunPos++; } // When we get to this point, we've copied full segments from the original existing run // into our new run buffer. We will have 1 or more full segments of color attributes and // we MIGHT have to cut the last copied segment's length back depending on where the inserted // attributes will fall in the final/new run. // Some examples: // - Starting with the original string R3 -> G5 -> B2 // - 1. If the insertion is Y5 at start index 3 // We are trying to get a result/final/new run of R3 -> Y5 -> B2. // We just copied R3 to the new destination buffer and we cang skip down and start inserting the new attrs. // - 2. If the insertion is Y3 at start index 5 // We are trying to get a result/final/new run of R3 -> G2 -> Y3 -> B2. // We just copied R3 -> G5 to the new destination buffer with the code above. // But the insertion is going to cut out some of the length of the G5. // We need to fix this up below so it says G2 instead to leave room for the Y3 to fit in // the new/final run. // Copying above advanced the pointer to an empty cell beyond what we copied. // Back up one cell so we can manipulate the final item we copied from the existing run to the new run. pNewRunPos--; // Fetch out the length so we can fix it up based on the below conditions. size_t length = pNewRunPos->GetLength(); // If we've covered more cells already than the start of the attributes to be inserted... if (iExistingRunCoverage > iStart) { // ..then subtract some of the length of the final cell we copied. // We want to take remove the difference in distance between the cells we've covered in the new // run and the insertion point. // (This turns G5 into G2 from Example 2 just above) length -= (iExistingRunCoverage - iStart); } // Now we're still on that "last cell copied" into the new run. // If the color of that existing copied cell matches the color of the first segment // of the run we're about to insert, we can just increment the length to extend the coverage. if (pNewRunPos->GetAttributes() == pInsertRunPos->GetAttributes()) { length += pInsertRunPos->GetLength(); // Since the color matched, we have already "used up" part of the insert run // and can skip it in our big "memcopy" step below that will copy the bulk of the insert run. cInsertRunRemaining--; pInsertRunPos++; } // We're done manipulating the length. Store it back. pNewRunPos->SetLength(length); // Now that we're done adjusting the last copied item, advance the pointer into a fresh/blank // part of the new run array. pNewRunPos++; } // Bulk copy the majority (or all, depending on circumstance) of the insert run into the final run buffer. std::copy_n(pInsertRunPos, cInsertRunRemaining, pNewRunPos); // Advance the new run pointer into the position just after everything we copied. pNewRunPos += cInsertRunRemaining; // We're technically done with the insert run now and have 0 remaining, but won't bother updating its pointers // and counts any further because we won't use them. // Now we need to move our pointer for the original existing run forward and update our counts // on how many cells we could have copied from the source before finishing off the new run. while (iExistingRunCoverage <= iEnd) { FAIL_FAST_IF(!(pExistingRunPos != pExistingRunEnd)); iExistingRunCoverage += pExistingRunPos->GetLength(); pExistingRunPos++; } // If we still have original existing run cells remaining, copy them into the final new run. if (pExistingRunPos != pExistingRunEnd || iExistingRunCoverage != (iEnd + 1)) { // Back up one cell so we can inspect the most recent item copied into the new run for optimizations. pNewRunPos--; // We advanced the existing run pointer and its count to on or past the end of what the insertion run filled in. // If this ended up being past the end of what the insertion run covers, we have to account for the cells after // the insertion run but before the next piece of the original existing run. // The example in this case is if we had... // Existing Run = R3 -> G5 -> B2 -> X5 // Insert Run = Y2 @ iStart = 7 and iEnd = 8 // ... then at this point in time, our states would look like... // New Run so far = R3 -> G4 -> Y2 // Existing Run Pointer is at X5 // Existing run coverage count at 3 + 5 + 2 = 10. // However, in order to get the final desired New Run // (which is R3 -> G4 -> Y2 -> B1 -> X5) // we would need to grab a piece of that B2 we already skipped past. // iExistingRunCoverage = 10. iEnd = 8. iEnd+1 = 9. 10 > 9. So we skipped something. if (iExistingRunCoverage > (iEnd + 1)) { // Back up the existing run pointer so we can grab the piece we skipped. pExistingRunPos--; // If the color matches what's already in our run, just increment the count value. // This case is slightly off from the example above. This case is for if the B2 above was actually Y2. // That Y2 from the existing run is the same color as the Y2 we just filled a few columns left in the final run // so we can just adjust the final run's column count instead of adding another segment here. if (pNewRunPos->GetAttributes() == pExistingRunPos->GetAttributes()) { size_t length = pNewRunPos->GetLength(); length += (iExistingRunCoverage - (iEnd + 1)); pNewRunPos->SetLength(length); } else { // If the color didn't match, then we just need to copy the piece we skipped and adjust // its length for the discrepency in columns not yet covered by the final/new run. // Move forward to a blank spot in the new run pNewRunPos++; // Copy the existing run's color information to the new run pNewRunPos->SetAttributes(pExistingRunPos->GetAttributes()); // Adjust the length of that copied color to cover only the reduced number of columns needed // now that some have been replaced by the insert run. pNewRunPos->SetLength(iExistingRunCoverage - (iEnd + 1)); } // Now that we're done recovering a piece of the existing run we skipped, move the pointer forward again. pExistingRunPos++; } // OK. In this case, we didn't skip anything. The end of the insert run fell right at a boundary // in columns that was in the original existing run. // However, the next piece of the original existing run might happen to have the same color attribute // as the final piece of what we just copied. // As an example... // Existing Run = R3 -> G5 -> B2. // Insert Run = B5 @ iStart = 3 and iEnd = 7 // New Run so far = R3 -> B5 // New Run desired when done = R3 -> B7 // Existing run pointer is on B2. // We want to merge the 2 from the B2 into the B5 so we get B7. else if (pNewRunPos->GetAttributes() == pExistingRunPos->GetAttributes()) { // Add the value from the existing run into the current new run position. size_t length = pNewRunPos->GetLength(); length += pExistingRunPos->GetLength(); pNewRunPos->SetLength(length); // Advance the existing run position since we consumed its value and merged it in. pExistingRunPos++; } // OK. We're done inspecting the most recently copied cell for optimizations. pNewRunPos++; // Now bulk copy any segments left in the original existing run if (pExistingRunPos < pExistingRunEnd) { std::copy_n(pExistingRunPos, (pExistingRunEnd - pExistingRunPos), pNewRunPos); // Fix up the end pointer so we know where we are for counting how much of the new run's memory space we used. pNewRunPos += (pExistingRunEnd - pExistingRunPos); } } // OK, phew. We're done. Now we just need to free the existing run, store the new run in its place, // and update the count for the correct length of the new run now that we've filled it up. newRun.erase(pNewRunPos, newRun.end()); _list.swap(newRun); return S_OK; } // Routine Description: // - packs a vector of TextAttribute into a vector of TextAttrbuteRun // Arguments: // - attrs - text attributes to pack // Return Value: // - packed text attribute run std::vector ATTR_ROW::PackAttrs(const std::vector& attrs) { std::vector runs; if (attrs.empty()) { return runs; } for (auto attr : attrs) { if (runs.empty() || runs.back().GetAttributes() != attr) { const TextAttributeRun run(1, attr); runs.push_back(run); } else { runs.back().SetLength(runs.back().GetLength() + 1); } } return runs; } ATTR_ROW::const_iterator ATTR_ROW::begin() const noexcept { return AttrRowIterator(this); } ATTR_ROW::const_iterator ATTR_ROW::end() const noexcept { return AttrRowIterator::CreateEndIterator(this); } ATTR_ROW::const_iterator ATTR_ROW::cbegin() const noexcept { return AttrRowIterator(this); } ATTR_ROW::const_iterator ATTR_ROW::cend() const noexcept { return AttrRowIterator::CreateEndIterator(this); } bool operator==(const ATTR_ROW& a, const ATTR_ROW& b) noexcept { return (a._list.size() == b._list.size() && a._list.data() == b._list.data() && a._cchRowWidth == b._cchRowWidth); }