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(); } } }
/// <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)); }
public SelectionSpan(ConsoleCoordinate start, ConsoleCoordinate end, SelectionDirection direction) { Debug.Assert(start >= ConsoleCoordinate.Zero); Debug.Assert(start < end); Start = start; End = end; Direction = direction; }
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); }
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); } }
/// <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); }
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); }
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) ); } }
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); }
/// <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()); }
public SelectionSpan WithStart(ConsoleCoordinate start) => new(start, End, Direction);