Beispiel #1
0
    private static void UpdateCoordinateFromCursorMove(Screen currentScreen, ConsoleCoordinate ansiCoordinate, StringBuilder diff, ref ConsoleCoordinate previousCoordinate, Cell?currentCell)
    {
        var characterWidth = currentCell is null ? 1 : currentCell.ElementWidth;
        // if we hit the edge of the screen, wrap
        bool hitRightEdgeOfScreen = previousCoordinate.Column + characterWidth == currentScreen.Width + ansiCoordinate.Column;

        if (hitRightEdgeOfScreen)
        {
            if (currentCell is not null && !currentCell.TruncateToScreenHeight)
            {
                diff.Append('\n');
                UpdateCoordinateFromNewLine(ref previousCoordinate);
                if (characterWidth == 2)
                {
                    previousCoordinate = previousCoordinate.MoveRight();
                }
            }
        }
        else
        {
            previousCoordinate = previousCoordinate.MoveRight();
            if (characterWidth == 2)
            {
                previousCoordinate = previousCoordinate.MoveRight();
            }
        }
    }
Beispiel #2
0
    /// <summary>
    /// We have our cursor coordinate, but its position represents the position in the input string.
    /// Normally, this is the same as the coordinate on screen, unless we've rendered CJK characters
    /// which are "full width" and take up two characters on screen.
    /// </summary>
    private static ConsoleCoordinate PositionCursor(Screen screen, ConsoleCoordinate cursor)
    {
        if (screen.CellBuffer.Length == 0)
        {
            return(cursor);
        }

        int row               = Math.Min(cursor.Row, screen.Height - 1);
        int column            = Math.Min(cursor.Column, screen.Width - 1);
        int rowStartIndex     = row * screen.Width;
        int rowCursorIndex    = rowStartIndex + column;
        int extraColumnOffset = 0;

        for (int i = row * screen.Width; i <= rowCursorIndex + extraColumnOffset; i++)
        {
            var cell = screen.CellBuffer[i];
            if (cell is not null && cell.IsContinuationOfPreviousCharacter)
            {
                Debug.Assert(i > 0);
                var previousCell = screen.CellBuffer[i - 1];
                Debug.Assert(previousCell.ElementWidth == 2);
                Debug.Assert(previousCell.Text is not null);

                //e.g. for '界' is previousCell.ElementWidth==2 and previousCell.Text.Length==1
                //e.g. for '😀' is previousCell.ElementWidth==2 and previousCell.Text.Length==2 (which means cursor is already moved by 2 because of Text length)
                extraColumnOffset += previousCell.ElementWidth - previousCell.Text.Length;
            }
        }
        int newColumn = column + extraColumnOffset;

        return(newColumn > screen.Width
            ? new ConsoleCoordinate(row + 1, newColumn - screen.Width)
            : new ConsoleCoordinate(row, newColumn));
    }
Beispiel #3
0
    public SelectionSpan(ConsoleCoordinate start, ConsoleCoordinate end, SelectionDirection direction)
    {
        Debug.Assert(start >= ConsoleCoordinate.Zero);
        Debug.Assert(start < end);

        Start     = start;
        End       = end;
        Direction = direction;
    }
Beispiel #4
0
    public Task OnKeyDown(KeyPress key, CancellationToken cancellationToken)
    {
        this.previousCursorLocation = codePane.Cursor;

        if (key.ObjectPattern is (Control, A))
        {
            codePane.Document.Caret = codePane.Document.Length;
        }
        return(Task.CompletedTask);
    }
Beispiel #5
0
 private static void UpdateCoordinateFromNewLine(ref ConsoleCoordinate previousCoordinate)
 {
     // for simplicity, we standardize all newlines to "\n" regardless of platform. However, that complicates our diff,
     // because "\n" on windows _only_ moves one line down, it does not change the column. Handle that here.
     previousCoordinate = previousCoordinate.MoveDown();
     if (!OperatingSystem.IsWindows())
     {
         previousCoordinate = previousCoordinate.WithColumn(1);
     }
 }
Beispiel #6
0
    /// <summary>
    /// Given a string, and a collection of highlighting instructions, create ANSI Escape Sequence instructions that will
    /// draw the highlighted text to the console.
    /// </summary>
    /// <param name="text">the text to print</param>
    /// <param name="formatting">the formatting instructions containing color information for the <paramref name="text"/></param>
    /// <param name="textWidth">the width of the console. This controls the word wrapping, and can usually be <see cref="Console.BufferWidth"/>.</param>
    /// <returns>A string of escape sequences that will draw the <paramref name="text"/></returns>
    /// <remarks>
    /// This function is different from most in that it involves drawing _output_ to the screen, rather than
    /// drawing typed user input. It's still useful because if users want syntax-highlighted input, chances are they
    /// also want syntax-highlighted output. It's sort of co-opting existing input functions for the purposes of output.
    /// </remarks>
    public static string RenderAnsiOutput(string text, IReadOnlyCollection <FormatSpan> formatting, int textWidth)
    {
        var rows          = CellRenderer.ApplyColorToCharacters(formatting, text, textWidth);
        var initialCursor = ConsoleCoordinate.Zero;
        var finalCursor   = new ConsoleCoordinate(rows.Length - 1, 0);
        var output        = IncrementalRendering.CalculateDiff(
            previousScreen: new Screen(textWidth, rows.Length, initialCursor),
            currentScreen: new Screen(textWidth, rows.Length, finalCursor, new ScreenArea(initialCursor, rows, TruncateToScreenHeight: false)),
            ansiCoordinate: initialCursor
            );

        return(output);
    }
Beispiel #7
0
 public Screen(int width, int height, ConsoleCoordinate cursor, params ScreenArea[] screenAreas)
 {
     this.screenAreas = screenAreas;
     this.Width       = width;
     this.Height      = screenAreas
                        .Select(area => area.TruncateToScreenHeight
             ? Math.Min(height, area.Start.Row + area.Rows.Length)
             : area.Start.Row + area.Rows.Length
                                )
                        .DefaultIfEmpty()
                        .Max();
     this.CellBuffer = new Cell[Width * Height];
     this.MaxIndex   = FillCharBuffer(screenAreas);
     this.Cursor     = PositionCursor(this, cursor);
 }
Beispiel #8
0
    private static void MoveCursorIfRequired(StringBuilder diff, ConsoleCoordinate fromCoordinate, ConsoleCoordinate toCoordinate)
    {
        // we only ever move the cursor relative to its current position.
        // this is because ansi escape sequences know nothing about the current scroll in the window,
        // they only operate on the current viewport. If we move to absolute positions, the display
        // is garbled if the user scrolls the window and then types.

        if (fromCoordinate.Row != toCoordinate.Row)
        {
            diff.Append(fromCoordinate.Row < toCoordinate.Row
                ? MoveCursorDown(toCoordinate.Row - fromCoordinate.Row)
                : MoveCursorUp(fromCoordinate.Row - toCoordinate.Row)
                        );
        }
        if (fromCoordinate.Column != toCoordinate.Column)
        {
            diff.Append(fromCoordinate.Column < toCoordinate.Column
                ? MoveCursorRight(toCoordinate.Column - fromCoordinate.Column)
                : MoveCursorLeft(fromCoordinate.Column - toCoordinate.Column)
                        );
        }
    }
Beispiel #9
0
    public static Row[] ApplyColorToCharacters(IReadOnlyCollection <FormatSpan> highlights, IReadOnlyList <WrappedLine> lines, SelectionSpan?selection, AnsiColor?selectedTextBackground)
    {
        var selectionStart = new ConsoleCoordinate(int.MaxValue, int.MaxValue); //invalid
        var selectionEnd   = new ConsoleCoordinate(int.MaxValue, int.MaxValue); //invalid

        if (selection.TryGet(out var selectionValue))
        {
            selectionStart = selectionValue.Start;
            selectionEnd   = selectionValue.End;
        }

        bool selectionHighlight = false;

        var highlightsLookup = highlights
                               .ToLookup(h => h.Start)
                               .ToDictionary(h => h.Key, conflictingHighlights => conflictingHighlights.OrderByDescending(h => h.Length).First());
        var        highlightedRows  = new Row[lines.Count];
        FormatSpan?currentHighlight = null;

        for (int lineIndex = 0; lineIndex < lines.Count; lineIndex++)
        {
            WrappedLine line = lines[lineIndex];
            int         lineFullWidthCharacterOffset = 0;
            var         cells = Cell.FromText(line.Content);
            for (int cellIndex = 0; cellIndex < cells.Count; cellIndex++)
            {
                var cell = cells[cellIndex];
                if (cell.IsContinuationOfPreviousCharacter)
                {
                    lineFullWidthCharacterOffset++;
                }

                // syntax highlight wrapped lines
                if (currentHighlight.TryGet(out var previousLineHighlight) &&
                    cellIndex == 0)
                {
                    currentHighlight = HighlightSpan(previousLineHighlight, cells, cellIndex, previousLineHighlight.Start - line.StartIndex);
                }

                // get current syntaxt highlight start
                int characterPosition = line.StartIndex + cellIndex - lineFullWidthCharacterOffset;
                currentHighlight ??= highlightsLookup.TryGetValue(characterPosition, out var lookupHighlight) ? lookupHighlight : null;

                // syntax highlight based on start
                if (currentHighlight.TryGet(out var highlight) &&
                    highlight.Contains(characterPosition))
                {
                    currentHighlight = HighlightSpan(highlight, cells, cellIndex, cellIndex);
                }

                // if there's text selected, invert colors to represent the highlight of the selected text.
                if (selectionStart.Equals(lineIndex, cellIndex - lineFullWidthCharacterOffset)) //start is inclusive
                {
                    selectionHighlight = true;
                }
                if (selectionEnd.Equals(lineIndex, cellIndex - lineFullWidthCharacterOffset)) //end is exclusive
                {
                    selectionHighlight = false;
                }
                if (selectionHighlight)
                {
                    if (selectedTextBackground.TryGet(out var background))
                    {
                        cell.Formatting = cell.Formatting with {
                            Background = background
                        };
                    }
                    else
                    {
                        cell.Formatting = new ConsoleFormat {
                            Inverted = true
                        };
                    }
                }
            }
            highlightedRows[lineIndex] = new Row(cells);
        }
        return(highlightedRows);
    }
Beispiel #10
0
    /// <summary>
    /// Given a new screen and the previously rendered screen,
    /// returns the minimum required ansi escape sequences to
    /// render the new screen.
    ///
    /// In the simple case, where the user typed a single character, we should only return that character (e.g. the returned string will be of length 1).
    /// A more complicated case, like finishing a word that triggers syntax highlighting, we should redraw just that word in the new color.
    /// An even more complicated case, like opening the autocomplete menu, should draw the autocomplete menu, and return the cursor to the correct position.
    /// </summary>
    public static string CalculateDiff(Screen currentScreen, Screen previousScreen, ConsoleCoordinate ansiCoordinate)
    {
        var diff = new StringBuilder();

        // if there are multiple characters with the same formatting, don't output formatting
        // instructions per character; instead output one instruction at the beginning for all
        // characters that share the same formatting.
        var currentFormatRun   = ConsoleFormat.None;
        var previousCoordinate = new ConsoleCoordinate(
            row: ansiCoordinate.Row + previousScreen.Cursor.Row,
            column: ansiCoordinate.Column + previousScreen.Cursor.Column
            );

        foreach (var(i, currentCell, previousCell) in currentScreen.CellBuffer.ZipLongest(previousScreen.CellBuffer))
        {
            if (currentCell is not null && currentCell.IsContinuationOfPreviousCharacter)
            {
                continue;
            }

            if (currentCell == previousCell)
            {
                continue;
            }

            var cellCoordinate = ansiCoordinate.Offset(i / currentScreen.Width, i % currentScreen.Width);

            MoveCursorIfRequired(diff, previousCoordinate, cellCoordinate);
            previousCoordinate = cellCoordinate;

            // handle when we're erasing characters/formatting from the previously rendered screen.
            if (currentCell is null || currentCell.Formatting == ConsoleFormat.None)
            {
                if (currentFormatRun != ConsoleFormat.None)
                {
                    diff.Append(Reset);
                    currentFormatRun = ConsoleFormat.None;
                }

                if (currentCell?.Text is null || currentCell.Text == "\n")
                {
                    diff.Append(' ');
                    UpdateCoordinateFromCursorMove(previousScreen, ansiCoordinate, diff, ref previousCoordinate, currentCell);

                    if (currentCell is null)
                    {
                        continue;
                    }
                }
            }

            // write out current character, with any formatting
            if (currentCell.Formatting != currentFormatRun)
            {
                // text selection is implemented by inverting colors. Reset inverted colors if required.
                if (currentFormatRun != ConsoleFormat.None && currentCell.Formatting.Inverted != currentFormatRun.Inverted)
                {
                    diff.Append(Reset);
                }
                diff.Append(
                    ToAnsiEscapeSequence(currentCell.Formatting)
                    + currentCell.Text
                    );
                currentFormatRun = currentCell.Formatting;
            }
            else
            {
                diff.Append(currentCell.Text);
            }

            // writing to the console will automatically move the cursor.
            // update our internal tracking so we calculate the least
            // amount of movement required for the next character.
            if (currentCell.Text == "\n")
            {
                UpdateCoordinateFromNewLine(ref previousCoordinate);
            }
            else
            {
                UpdateCoordinateFromCursorMove(currentScreen, ansiCoordinate, diff, ref previousCoordinate, currentCell);
            }
        }

        if (currentFormatRun != ConsoleFormat.None)
        {
            diff.Append(Reset);
        }

        // all done rendering, update the cursor position if we need to. If we rendered the
        // autocomplete menu, or if the cursor is manually positioned in the middle of
        // the text, the cursor won't be in the correct position.
        MoveCursorIfRequired(
            diff,
            fromCoordinate: previousCoordinate,
            toCoordinate: new ConsoleCoordinate(
                currentScreen.Cursor.Row + ansiCoordinate.Row,
                currentScreen.Cursor.Column + ansiCoordinate.Column
                )
            );

        return(diff.ToString());
    }
Beispiel #11
0
 public SelectionSpan WithStart(ConsoleCoordinate start) => new(start, End, Direction);