diff --git a/src/PowerShellRun/Application/InternalEntry.cs b/src/PowerShellRun/Application/InternalEntry.cs index d51ab30..566165d 100644 --- a/src/PowerShellRun/Application/InternalEntry.cs +++ b/src/PowerShellRun/Application/InternalEntry.cs @@ -31,8 +31,10 @@ internal class InternalEntry public BackgroundRunspace.Task? PreviewTask { get; set; } = null; private int _previewTaskExecutionCount = 0; private string[]? _previewLines = null; - private readonly object? _previewLinesLock = null; + private readonly object? _previewTaskLock = null; private bool _previewLinesUpdatedByTask = false; + private TextBox.WrappedText? _previewWrappedTextCache = null; + private int _previewWrappedTextCacheWidth = 0; public InternalEntry(SelectorEntry selectorEntry) { @@ -71,7 +73,7 @@ public InternalEntry(SelectorEntry selectorEntry) if (selectorEntry.PreviewAsyncScript is not null) { - _previewLinesLock = new object(); + _previewTaskLock = new object(); } } @@ -89,10 +91,10 @@ public ActionKey[] GetActionKeysMultiSelection() public void CompletePreviewTask(System.Collections.ObjectModel.Collection taskResult) { - if (_previewLinesLock is null) + if (_previewTaskLock is null) return; - lock (_previewLinesLock) + lock (_previewTaskLock) { if (taskResult.Count > 0 && taskResult[0] is not null) { @@ -106,22 +108,39 @@ public void CompletePreviewTask(System.Collections.ObjectModel.Collection= maxWidth) break; - bool escaped = false; - var textElementIndexes = System.Globalization.StringInfo.ParseCombiningCharacters(word.String); - for (int i = 0; i < textElementIndexes.Length; ++i) + var textElement = new TextElementEnumerator(word.String); + while (textElement.MoveNext()) { if (cellIndex >= maxWidth) break; - var elementStartIndex = textElementIndexes[i]; - var elementStartIndexNext = (i + 1 == textElementIndexes.Length) ? word.String.Length : textElementIndexes[i + 1]; - - var elementCharCount = elementStartIndexNext - elementStartIndex; - bool isBaseCharacter = elementCharCount == 1; - var charIndex = elementStartIndex; + if (textElement.IsEscapeSequence) + { + AddEscapeSequence(textElement.Character); + continue; + } - if (isBaseCharacter) + int charIndex = textElement.ElementStartCharIndex; + if (textElement.IsTab) { - // basic 2 byte characters - char character = word.String[charIndex]; - if (escaped) - { - if (character == 'm') - { - escaped = false; - } - AddEscapeSequence(character); - } - else - if (character == '\x1b') - { - escaped = true; - AddEscapeSequence(character); - } - else - if (character == '\t') - { - int spaces = tabSize - cellIndex % tabSize; - for (int s = 0; s < spaces; ++s) - { - SetCell(word, charIndex, character: ' '); - if (cellIndex >= maxWidth) - break; - } - } - else + int spaces = tabSize - cellIndex % tabSize; + for (int s = 0; s < spaces; ++s) { - int displayWidth = Unicode.GetDisplayWidth(character); - if (displayWidth <= 0) - continue; - - if (cellIndex + displayWidth > maxWidth) - continue; - - SetCell(word, charIndex, character: character); - if (displayWidth == 2) - { - SetCell(word, charIndex, character: '\0', isSecondCellOfWideCharacter: true); - } + SetCell(word, charIndex, character: ' '); + if (cellIndex >= maxWidth) + break; } + continue; + } + + if (textElement.DisplayWidth <= 0) + continue; + + if (cellIndex + textElement.DisplayWidth > maxWidth) + continue; + + if (textElement.IsBaseCharacter) + { + SetCell(word, charIndex, character: textElement.Character); } else { - int displayWidth = GetDisplayWidthOfComplexTextElement(word.String, elementStartIndex, elementCharCount); - if (displayWidth <= 0) - continue; - - if (cellIndex + displayWidth > maxWidth) - continue; + SetCell(word, charIndex, textElement: textElement.Element); + } - var textElement = word.String.Substring(elementStartIndex, elementCharCount); - SetCell(word, charIndex, textElement: textElement); - if (displayWidth == 2) - { - SetCell(word, charIndex, character: '\0', isSecondCellOfWideCharacter: true); - } + if (textElement.DisplayWidth == 2) + { + SetCell(word, charIndex, character: '\0', isSecondCellOfWideCharacter: true); } } @@ -253,7 +224,110 @@ void SetCell(Word word, int charIndex, char character = '\0', string? textElemen } } - public static int GetDisplayWidthOfComplexTextElement(string str, int elementCharOffset, int elementCharCount) + public static int GetDisplayWidth(string str) + { + int tabSize = SelectorOptionHolder.GetInstance().Option.Theme.TabSize; + int cellCount = 0; + + var textElement = new TextElementEnumerator(str); + while (textElement.MoveNext()) + { + if (textElement.IsTab) + { + cellCount += tabSize - cellCount % tabSize; + continue; + } + + if (textElement.DisplayWidth <= 0) + continue; + + cellCount += textElement.DisplayWidth; + } + + return cellCount; + } + + public class TextElementEnumerator + { + private readonly string _str; + private readonly int[] _elementIndexes; + private int _currentElementIndex = -1; + private bool _isEscaped; + + public bool IsBaseCharacter => ElementCharCount == 1; + public bool IsEscapeSequence { get; set; } + public bool IsTab { get; set; } + public int DisplayWidth { get; set; } + public char Character {get; set; } = '\0'; + public string Element {get; set; } = ""; + public int ElementStartCharIndex {get; set;} + public int ElementCharCount {get; set;} + + public TextElementEnumerator(string str) + { + _str = str; + _elementIndexes = System.Globalization.StringInfo.ParseCombiningCharacters(str); + } + + public bool MoveNext() + { + if (_currentElementIndex >= _elementIndexes.Length) + return false; + + ++_currentElementIndex; + + if (_currentElementIndex >= _elementIndexes.Length) + return false; + + ElementStartCharIndex = _elementIndexes[_currentElementIndex]; + var elementStartIndexNext = (_currentElementIndex + 1 == _elementIndexes.Length) ? _str.Length : _elementIndexes[_currentElementIndex + 1]; + ElementCharCount = elementStartIndexNext - ElementStartCharIndex; + + IsEscapeSequence = false; + IsTab = false; + DisplayWidth = 0; + Character = '\0'; + Element = ""; + + if (IsBaseCharacter) + { + // basic 2 byte characters + Character = _str[ElementStartCharIndex]; + if (_isEscaped) + { + if (Character == 'm') + { + _isEscaped = false; + } + IsEscapeSequence = true; + } + else + if (Character == '\x1b') + { + _isEscaped = true; + IsEscapeSequence = true; + } + else + if (Character == '\t') + { + IsTab = true; + } + else + { + DisplayWidth = Unicode.GetDisplayWidth(Character); + } + } + else + { + DisplayWidth = GetDisplayWidthOfComplexTextElement(_str, ElementStartCharIndex, ElementCharCount); + Element = _str.Substring(ElementStartCharIndex, ElementCharCount); + } + + return true; + } + } + + private static int GetDisplayWidthOfComplexTextElement(string str, int elementCharOffset, int elementCharCount) { Debug.Assert(elementCharCount > 1); @@ -285,60 +359,73 @@ public static int GetDisplayWidthOfComplexTextElement(string str, int elementCha return displayWidth; } - public static int GetDisplayWidth(string str) + public class WrappedText { - int tabSize = SelectorOptionHolder.GetInstance().Option.Theme.TabSize; - int count = 0; - bool escaped = false; + private int _originalLineCount; + private int[] _originalLineIndexes; + public string[] Lines; - var textElementIndexes = System.Globalization.StringInfo.ParseCombiningCharacters(str); - for (int i = 0; i < textElementIndexes.Length; ++i) + public WrappedText(string[] lines, int maxWidth) { - var elementStartIndex = textElementIndexes[i]; - var elementStartIndexNext = (i + 1 == textElementIndexes.Length) ? str.Length : textElementIndexes[i + 1]; + _originalLineCount = lines.Length; - var elementCharCount = elementStartIndexNext - elementStartIndex; - bool isBaseCharacter = elementCharCount == 1; - var charIndex = elementStartIndex; + List newLines = new(lines.Length); + List originalLineIndexes = new(lines.Length); + int tabSize = SelectorOptionHolder.GetInstance().Option.Theme.TabSize; - if (isBaseCharacter) + for (int i = 0; i < lines.Length; ++i) { - char character = str[charIndex]; - if (escaped) + string line = lines[i]; + int cellCount = 0; + var textElement = new TextElementEnumerator(line); + + int copyStartCharIndex = 0; + int copyCharCount = 0; + while (textElement.MoveNext()) { - if (character == 'm') + int displayWidth = Math.Max(textElement.DisplayWidth, 0); + if (textElement.IsTab) { - escaped = false; + displayWidth = tabSize - cellCount % tabSize; } + + if (cellCount + displayWidth <= maxWidth || copyCharCount == 0) + { + copyCharCount += textElement.ElementCharCount; + } + else + { + newLines.Add(line.Substring(copyStartCharIndex, copyCharCount)); + originalLineIndexes.Add(i); + + copyStartCharIndex = copyStartCharIndex + copyCharCount; + copyCharCount = textElement.ElementCharCount; + cellCount = 0; + } + cellCount += displayWidth; } - else - if (character == '\x1b') - { - escaped = true; - } - else - if (character == '\t') - { - count += tabSize - count % tabSize; - } - else - { - int displayWidth = Unicode.GetDisplayWidth(character); - if (displayWidth <= 0) - continue; - count += displayWidth; - } + + newLines.Add(line.Substring(copyStartCharIndex, copyCharCount)); + originalLineIndexes.Add(i); } - else + + Lines = newLines.ToArray(); + _originalLineIndexes = originalLineIndexes.ToArray(); + } + + // Returns the start line index after wrapping from the original (before wrapping) line index. + public int GetStartLineIndex(int originalLineIndex) + { + originalLineIndex = Math.Min(originalLineIndex, _originalLineCount - 1); + for (int i = 0; i < _originalLineIndexes.Length; ++i) { - int displayWidth = GetDisplayWidthOfComplexTextElement(str, elementStartIndex, elementCharCount); - if (displayWidth <= 0) - continue; - count += displayWidth; + if (_originalLineIndexes[i] == originalLineIndex) + { + return i; + } } + return -1; } - - return count; } private List _lines = new List(); diff --git a/src/PowerShellRun/UI/TextWrapMode.cs b/src/PowerShellRun/UI/TextWrapMode.cs new file mode 100644 index 0000000..011de59 --- /dev/null +++ b/src/PowerShellRun/UI/TextWrapMode.cs @@ -0,0 +1,9 @@ +namespace PowerShellRun; + +public enum TextWrapMode +{ + None, + + // Wrap anywhere without considering words. + Character, +}