bool Selection::HandleKeyboardLineSelectionEvent()

in src/host/selectionInput.cpp [286:604]


bool Selection::HandleKeyboardLineSelectionEvent(const INPUT_KEY_INFO* const pInputKeyInfo)
{
    const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
    const WORD wVirtualKeyCode = pInputKeyInfo->GetVirtualKey();

    // if this isn't a valid key combination for this function, exit quickly.
    if (!s_IsValidKeyboardLineSelection(pInputKeyInfo))
    {
        return false;
    }

    Telemetry::Instance().SetKeyboardTextSelectionUsed();

    // if we're not currently selecting anything, start a new mouse selection
    if (!IsInSelectingState())
    {
        InitializeMouseSelection(gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().GetPosition());

        // force that this is a line selection
        _AlignAlternateSelection(true);

        ShowSelection();

        // if we did shift+left/right, then just exit
        if (pInputKeyInfo->IsShiftOnly())
        {
            switch (wVirtualKeyCode)
            {
            case VK_LEFT:
            case VK_RIGHT:
                return true;
            }
        }
    }

    // anchor is the first clicked position
    const COORD coordAnchor = _coordSelectionAnchor;

    // rect covers the entire selection
    const SMALL_RECT rectSelection = _srSelectionRect;

    // the selection point is the other corner of the rectangle from the anchor that we're about to manipulate
    COORD coordSelPoint;
    coordSelPoint.X = coordAnchor.X == rectSelection.Left ? rectSelection.Right : rectSelection.Left;
    coordSelPoint.Y = coordAnchor.Y == rectSelection.Top ? rectSelection.Bottom : rectSelection.Top;

    // this is the maximum size of the buffer
    const auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize();

    const SHORT sWindowHeight = gci.GetActiveOutputBuffer().GetViewport().Height();

    FAIL_FAST_IF(!bufferSize.IsInBounds(coordSelPoint));

    // retrieve input line information. If we are selecting from within the input line, we need
    // to bound ourselves within the input data first and not move into the back buffer.

    COORD coordInputLineStart;
    COORD coordInputLineEnd;
    bool fHaveInputLine = s_GetInputLineBoundaries(&coordInputLineStart, &coordInputLineEnd);

    if (pInputKeyInfo->IsShiftOnly())
    {
        switch (wVirtualKeyCode)
        {
            // shift + left/right extends the selection by one character, wrapping at screen edge
        case VK_LEFT:
        {
            bufferSize.DecrementInBounds(coordSelPoint);
            break;
        }
        case VK_RIGHT:
        {
            bufferSize.IncrementInBounds(coordSelPoint);

            // if we're about to split a character in half, keep moving right
            try
            {
                const auto attr = gci.GetActiveOutputBuffer().GetCellDataAt(coordSelPoint)->DbcsAttr();
                if (attr.IsTrailing())
                {
                    bufferSize.IncrementInBounds(coordSelPoint);
                }
            }
            CATCH_LOG();

            break;
        }
            // shift + up/down extends the selection by one row, stopping at top or bottom of screen
        case VK_UP:
        {
            if (coordSelPoint.Y > bufferSize.Top())
            {
                coordSelPoint.Y--;
            }
            break;
        }
        case VK_DOWN:
        {
            if (coordSelPoint.Y < bufferSize.BottomInclusive())
            {
                coordSelPoint.Y++;
            }
            break;
        }
            // shift + pgup/pgdn extends selection up or down one full screen
        case VK_NEXT:
        {
            coordSelPoint.Y = base::CheckAdd(coordSelPoint.Y, sWindowHeight).ValueOrDefault(bufferSize.BottomInclusive());
            if (coordSelPoint.Y > bufferSize.BottomInclusive())
            {
                coordSelPoint.Y = bufferSize.BottomInclusive();
            }
            break;
        }
        case VK_PRIOR:
        {
            coordSelPoint.Y = base::CheckSub(coordSelPoint.Y, sWindowHeight).ValueOrDefault(bufferSize.Top());
            if (coordSelPoint.Y < bufferSize.Top())
            {
                coordSelPoint.Y = bufferSize.Top();
            }
            break;
        }
            // shift + home/end extends selection to beginning or end of line
        case VK_HOME:
        {
            /*
            Prompt sample:
                qwertyuiopasdfg
                C:\>dir /p /w C
                :\windows\syste
                m32

                The input area runs from the d in "dir" to the space after the 2 in "32"

                We want to stop the HOME command from running to the beginning of the line only
                if we're on the first input line because then it would capture the prompt.

                So if the selection point we're manipulating is currently anywhere in the
                "dir /p /w C" area, then pressing home should only move it on top of the "d" in "dir".

                But if it's already at the "d" in dir, pressing HOME again should move us to the
                beginning of the line anyway to collect up the prompt as well.
            */

            // if we're in the input line
            if (fHaveInputLine)
            {
                // and the selection point is inside the input line area
                if (Utils::s_CompareCoords(coordSelPoint, coordInputLineStart) > 0)
                {
                    // and we're on the same line as the beginning of the input
                    if (coordInputLineStart.Y == coordSelPoint.Y)
                    {
                        // then only back up to the start of the input
                        coordSelPoint.X = coordInputLineStart.X;
                        break;
                    }
                }
            }

            // otherwise, fall through and select to the head of the line.
            coordSelPoint.X = 0;
            break;
        }
        case VK_END:
        {
            /*
            Prompt sample:
                qwertyuiopasdfg
                C:\>dir /p /w C
                :\windows\syste
                m32

                The input area runs from the d in "dir" to the space after the 2 in "32"

                We want to stop the END command from running to the space after the "32" because
                that's just where the cursor lies to let more text get entered and not actually
                a valid selection area.

                So if the selection point is anywhere on the "m32", pressing end should move it
                to on top of the "2".

                Additionally, if we're starting within the output buffer (qwerty, etc. and C:\>), then
                pressing END should stop us before we enter the input line the first time.

                So if we're anywhere on "C:\", we should select up to the ">" character and no further
                until a subsequent press of END.

                At the subsequent press of END when we're on the ">", we should move to the end of the input
                line or the end of the screen, whichever comes first.
            */

            // if we're in the input line
            if (fHaveInputLine)
            {
                // and the selection point is inside the input area
                if (Utils::s_CompareCoords(coordSelPoint, coordInputLineStart) >= 0)
                {
                    // and we're on the same line as the end of the input
                    if (coordInputLineEnd.Y == coordSelPoint.Y)
                    {
                        // and we're not already on the end of the input...
                        if (coordSelPoint.X < coordInputLineEnd.X)
                        {
                            // then only use end to the end of the input
                            coordSelPoint.X = coordInputLineEnd.X;
                            break;
                        }
                    }
                }
                else
                {
                    // otherwise if we're outside and on the same line as the start of the input
                    if (coordInputLineStart.Y == coordSelPoint.Y)
                    {
                        // calculate the end of the outside/output buffer position
                        const short sEndOfOutputPos = coordInputLineStart.X - 1;

                        // if we're not already on the very last character...
                        if (coordSelPoint.X < sEndOfOutputPos)
                        {
                            // then only move to just before the beginning of the input
                            coordSelPoint.X = sEndOfOutputPos;
                            break;
                        }
                        else if (coordSelPoint.X == sEndOfOutputPos)
                        {
                            // if we were on the last character,
                            // then if the end of the input line is also on this current line,
                            // move to that.
                            if (coordSelPoint.Y == coordInputLineEnd.Y)
                            {
                                coordSelPoint.X = coordInputLineEnd.X;
                                break;
                            }
                        }
                    }
                }
            }

            // otherwise, fall through and go to selecting the whole line to the end.
            coordSelPoint.X = bufferSize.RightInclusive();
            break;
        }
        }
    }
    else if (pInputKeyInfo->IsShiftAndCtrlOnly())
    {
        switch (wVirtualKeyCode)
        {
            // shift + ctrl + left/right extends selection to next/prev word boundary
        case VK_LEFT:
        {
            coordSelPoint = WordByWordSelection(true, bufferSize, coordAnchor, coordSelPoint);
            break;
        }
        case VK_RIGHT:
        {
            coordSelPoint = WordByWordSelection(false, bufferSize, coordAnchor, coordSelPoint);
            break;
        }
            // shift + ctrl + up/down does the same thing that shift + up/down does
        case VK_UP:
        {
            if (coordSelPoint.Y > bufferSize.Top())
            {
                coordSelPoint.Y--;
            }
            break;
        }
        case VK_DOWN:
        {
            if (coordSelPoint.Y < bufferSize.BottomInclusive())
            {
                coordSelPoint.Y++;
            }
            break;
        }
            // shift + ctrl + home/end extends selection to top or bottom of buffer from selection
        case VK_HOME:
        {
            COORD coordValidStart;
            GetValidAreaBoundaries(&coordValidStart, nullptr);
            coordSelPoint = coordValidStart;
            break;
        }
        case VK_END:
        {
            COORD coordValidEnd;
            GetValidAreaBoundaries(nullptr, &coordValidEnd);
            coordSelPoint = coordValidEnd;
            break;
        }
        }
    }

    // ensure we're not planting the cursor in the middle of a double-wide character.
    try
    {
        const auto attr = gci.GetActiveOutputBuffer().GetCellDataAt(coordSelPoint)->DbcsAttr();
        if (attr.IsTrailing())
        {
            // try to move off by highlighting the lead half too.
            bool fSuccess = bufferSize.DecrementInBounds(coordSelPoint);

            // if that fails, move off to the next character
            if (!fSuccess)
            {
                bufferSize.IncrementInBounds(coordSelPoint);
            }
        }
    }
    CATCH_LOG();

    ExtendSelection(coordSelPoint);

    return true;
}