diff --git a/FileDiff/AppSettings.cs b/FileDiff/AppSettings.cs index bdbc5b8..e9a4684 100644 --- a/FileDiff/AppSettings.cs +++ b/FileDiff/AppSettings.cs @@ -462,6 +462,18 @@ public static SolidColorBrush MovedToBackground } } + private static SolidColorBrush whiteSpaceForeground; + public static SolidColorBrush WhiteSpaceForeground + { + get { return whiteSpaceForeground; } + set + { + whiteSpaceForeground = value; + whiteSpaceForeground.Freeze(); + CurrentTheme.WhiteSpaceForeground = value.Color.ToString(); + } + } + // Editor colors private static SolidColorBrush lineNumberColor; @@ -881,6 +893,8 @@ private static void UpdateCachedSettings() MovedFromBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(CurrentTheme.MovedFromBackground)); MovedToBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(CurrentTheme.MovedToBackground)); + WhiteSpaceForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString(CurrentTheme.WhiteSpaceForeground)); + // Editor colors LineNumberColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString(CurrentTheme.LineNumberColor)); CurrentDiffColor = new SolidColorBrush((Color)ColorConverter.ConvertFromString(CurrentTheme.CurrentDiffColor)); diff --git a/FileDiff/BackgroundCompare.cs b/FileDiff/BackgroundCompare.cs index a910d44..77727d8 100644 --- a/FileDiff/BackgroundCompare.cs +++ b/FileDiff/BackgroundCompare.cs @@ -31,7 +31,7 @@ public static void Cancel() CompareCancelled = true; } - public static Tuple, List, TimeSpan> MatchFiles(List leftLines, List rightLines) + public static Tuple, List, TimeSpan> MatchFiles(List leftLines, List rightLines, FileEncoding leftFileEncoding, FileEncoding rightFileEncoding) { Debug.WriteLine(" - MatchFiles"); @@ -44,7 +44,7 @@ public static Tuple, List, TimeSpan> MatchFiles(List left MatchLines(leftLines, rightLines); - AddFillerLines(ref leftLines, ref rightLines); + AddFillerLines(ref leftLines, ref rightLines, leftFileEncoding.SaveNewline, rightFileEncoding.SaveNewline); ShiftDown(leftLines, rightLines); @@ -721,7 +721,7 @@ private static int CountMatchingCharacters(List leftRange, List righ return matchLength; } - private static void AddFillerLines(ref List leftLines, ref List rightLines) + private static void AddFillerLines(ref List leftLines, ref List rightLines, NewlineMode leftNewline, NewlineMode rightNewline) { int rightIndex = 0; @@ -733,13 +733,13 @@ private static void AddFillerLines(ref List leftLines, ref List righ if (leftLines[leftIndex].MatchingLineIndex == null) { newLeft.Add(leftLines[leftIndex]); - newRight.Add(new Line() { Type = TextState.Filler }); + newRight.Add(new Line() { Type = TextState.Filler, Newline = rightNewline }); } else { while (rightIndex < leftLines[leftIndex].MatchingLineIndex) { - newLeft.Add(new Line() { Type = TextState.Filler }); + newLeft.Add(new Line() { Type = TextState.Filler, Newline = leftNewline }); newRight.Add(rightLines[rightIndex]); rightIndex++; } @@ -750,7 +750,7 @@ private static void AddFillerLines(ref List leftLines, ref List righ } while (rightIndex < rightLines.Count) { - newLeft.Add(new Line() { Type = TextState.Filler }); + newLeft.Add(new Line() { Type = TextState.Filler, Newline = leftNewline }); newRight.Add(rightLines[rightIndex]); rightIndex++; } diff --git a/FileDiff/ColorTheme.cs b/FileDiff/ColorTheme.cs index f7398a9..9e8974d 100644 --- a/FileDiff/ColorTheme.cs +++ b/FileDiff/ColorTheme.cs @@ -42,6 +42,8 @@ public class ColorTheme public required string MovedFromBackground { get; set; } public required string MovedToBackground { get; set; } + public required string WhiteSpaceForeground { get; set; } + // Editor colors public required string LineNumberColor { get; set; } public required string CurrentDiffColor { get; set; } diff --git a/FileDiff/DefaultSettings.cs b/FileDiff/DefaultSettings.cs index d2b984b..62cd2a5 100644 --- a/FileDiff/DefaultSettings.cs +++ b/FileDiff/DefaultSettings.cs @@ -44,6 +44,8 @@ public static class DefaultSettings MovedFromBackground = "#FF2D1B1B", MovedToBackground = "#FF1B261B", + WhiteSpaceForeground = "#FF3871A7", + // Editor colors LineNumberColor = "#FF797979", CurrentDiffColor = "#FF252525", @@ -106,6 +108,8 @@ public static class DefaultSettings MovedFromBackground = "#FFFFF0F0", MovedToBackground = "#FFF0FFF0", + WhiteSpaceForeground = "#FF2E8CB5", + // Editor colors LineNumberColor = "#FF585858", CurrentDiffColor = "#FFB7B7B7", diff --git a/FileDiff/DiffControl.cs b/FileDiff/DiffControl.cs index a7ca80a..3734ef7 100644 --- a/FileDiff/DiffControl.cs +++ b/FileDiff/DiffControl.cs @@ -31,6 +31,8 @@ public class DiffControl : Control private Typeface typeface; + private NewlineMode documentNewline; + private readonly DispatcherTimer blinkTimer = new(DispatcherPriority.Render); private readonly Stopwatch stopwatch = new(); @@ -109,6 +111,9 @@ protected override void OnRender(DrawingContext drawingContext) TextUtils.CreateGlyphRun("W", typeface, this.FontSize, dpiScale, 0, out characterWidth); characterHeight = Math.Ceiling(TextUtils.FontHeight(typeface, this.FontSize, dpiScale) / dpiScale) * dpiScale; + GlyphRun crNewline = TextUtils.CreateGlyphRun("CR", typeface, this.FontSize, dpiScale, 0, out double crNewlineWidth); + GlyphRun lfNewline = TextUtils.CreateGlyphRun("LF", typeface, this.FontSize, dpiScale, 0, out double lfNewlineWidth); + //Color semiTransparent = Color.FromArgb(100, 0, 0, 0); //Brush currentDiffBrush = new LinearGradientBrush(semiTransparent, Colors.Transparent, 0); @@ -129,6 +134,11 @@ protected override void OnRender(DrawingContext drawingContext) currentDiffPen.Freeze(); GuidelineSet currentDiffGuide = CreateGuidelineSet(currentDiffPen); + Pen whiteSpacePen = new(AppSettings.WhiteSpaceForeground, RoundToWholePixels(characterHeight * .06)) { StartLineCap = PenLineCap.Flat, EndLineCap = PenLineCap.Square }; + whiteSpacePen.Freeze(); + GuidelineSet whiteSpacePenGuide = CreateGuidelineSet(whiteSpacePen); + double penMargin = whiteSpacePen.Thickness * 1.7; + textMargin = RoundToWholePixels(4); lineNumberMargin = RoundToWholePixels(characterWidth * Lines.Count.ToString().Length) + (2 * textMargin) + borderPen.Thickness; @@ -216,14 +226,14 @@ protected override void OnRender(DrawingContext drawingContext) drawingContext.PushTransform(new TranslateTransform(lineNumberMargin + textMargin - HorizontalOffset, 0)); { // Draw line - if (line.Text != "") + if (line.Text != "" || AppSettings.ShowWhiteSpaceCharacters) { double nextPosition = 0; foreach (TextSegment textSegment in line.TextSegments) { drawingContext.PushTransform(new TranslateTransform(nextPosition, 0)); - GlyphRun segmentRun = textSegment.GetRenderedText(typeface, this.FontSize, dpiScale, AppSettings.ShowWhiteSpaceCharacters, AppSettings.TabSize, nextPosition, out double runWidth); + GlyphRun segmentRun = textSegment.GetRenderedText(typeface, this.FontSize, dpiScale, AppSettings.TabSize, nextPosition, out double runWidth); if (nextPosition - HorizontalOffset < ActualWidth && nextPosition + runWidth - HorizontalOffset > 0) { @@ -233,12 +243,62 @@ protected override void OnRender(DrawingContext drawingContext) } drawingContext.DrawGlyphRun(AppSettings.ShowLineChanges ? textSegment.ForegroundBrush : line.ForegroundBrush, segmentRun); + + // Draw white space characters + if (AppSettings.ShowWhiteSpaceCharacters) + { + drawingContext.PushGuidelineSet(whiteSpacePenGuide); + { + double offset = 0; + for (int characterIndex = 0; characterIndex < textSegment.Text.Length; characterIndex++) + { + char character = textSegment.Text[characterIndex]; + double characterWidth = segmentRun.AdvanceWidths[characterIndex]; + double arrowSize = characterHeight * .2; + double centerY = RoundToWholePixels(characterHeight / 2); + + if (character.In([' ', '\t'])) + { + //drawingContext.DrawRectangle(new SolidColorBrush(Color.FromArgb(50, 128, 128, 128)), null, new Rect(offset, 0, characterWidth, characterHeight)); + + if (character == ' ') + { + drawingContext.DrawEllipse(AppSettings.WhiteSpaceForeground, null, new Point(offset + characterWidth / 2, centerY), arrowSize * .4, arrowSize * .4); + } + if (character == '\t') + { + drawingContext.DrawLine(whiteSpacePen, new Point(offset + characterWidth - penMargin, centerY), new Point(offset + penMargin, centerY)); + + drawingContext.DrawLine(whiteSpacePen, new Point(offset + characterWidth - arrowSize - penMargin, centerY - arrowSize), new Point(offset + characterWidth - penMargin, centerY)); + drawingContext.DrawLine(whiteSpacePen, new Point(offset + characterWidth - arrowSize - penMargin, centerY + arrowSize), new Point(offset + characterWidth - penMargin, centerY)); + } + } + + offset += characterWidth; + } + } + drawingContext.Pop(); + } } nextPosition += runWidth; drawingContext.Pop(); } maxTextWidth = Math.Max(maxTextWidth, nextPosition); + + // Draw newline characters + if (AppSettings.ShowWhiteSpaceCharacters && !line.IsFiller) + { + if (line.Newline == NewlineMode.Windows || line.Newline == NewlineMode.Mac) + { + nextPosition = DrawNewlineCharacter(crNewline, crNewlineWidth, nextPosition); + } + + if (line.Newline == NewlineMode.Windows || line.Newline == NewlineMode.Unix) + { + nextPosition = DrawNewlineCharacter(lfNewline, lfNewlineWidth, nextPosition); + } + } } // Draw cursor @@ -341,13 +401,51 @@ protected override void OnRender(DrawingContext drawingContext) #if DEBUG ReportRenderTime(); #endif + + double DrawNewlineCharacter(GlyphRun NewlineText, double crNewlineWidth, double nextPosition) + { + Rect nlRect = NewlineText.ComputeAlignmentBox(); + //r.Top -= crNewline.BaselineOrigin.Y; + drawingContext.PushTransform(new TranslateTransform(nextPosition + penMargin * 3, 0)); + { + + // drawingContext.DrawRoundedRectangle(Brushes.Yellow, null, nlRect, penMargin, penMargin); + + + drawingContext.PushGuidelineSet(whiteSpacePenGuide); + { + Rect r = new Rect( + 0, + RoundToWholePixels((characterHeight - nlRect.Height) / 2), + RoundToWholePixels(nlRect.Width), + RoundToWholePixels(nlRect.Height) + ); + + drawingContext.DrawRoundedRectangle(null, whiteSpacePen, r, penMargin, penMargin); + // drawingContext.DrawRoundedRectangle(null, whiteSpacePen, new Rect(0, RoundToWholePixels(whiteSpacePen.Thickness), RoundToWholePixels(crNewlineWidth + whiteSpacePen.Thickness * 3), RoundToWholePixels(characterHeight - whiteSpacePen.Thickness * 2)), penMargin, penMargin); + + } + drawingContext.Pop(); + + drawingContext.PushTransform(new TranslateTransform(0, 0)); + { + drawingContext.DrawGlyphRun(AppSettings.WhiteSpaceForeground, NewlineText); + } + drawingContext.Pop(); + + } + drawingContext.Pop(); + nextPosition += penMargin * 3 + crNewlineWidth; + return nextPosition; + } + } protected override void OnTextInput(TextCompositionEventArgs e) { if (EditMode && e.Text.Length > 0 && Lines.Count > 0) { - if (e.Text == "\b") + if (e.Text == "\b") // Backspace key pressed { if (Selection != null) { @@ -362,6 +460,10 @@ protected override void OnTextInput(TextCompositionEventArgs e) cursorCharacter = Lines[cursorLine - 1].Text.Length; SetLineText(cursorLine - 1, Lines[cursorLine - 1].Text + Lines[cursorLine].Text); RemoveLine(cursorLine); + if (cursorLine == Lines.Count - 1) // Backspace on last line, remove newline character from line above + { + Lines[cursorLine - 1].Newline = null; + } cursorLine--; } } @@ -373,12 +475,15 @@ protected override void OnTextInput(TextCompositionEventArgs e) } } } - else if (e.Text == "\r") + else if (e.Text == "\r") // Enter key pressed { if (Selection != null) { DeleteSelection(); } + + Lines[cursorLine].Newline = documentNewline; + InsertNewLine(cursorLine + 1, Lines[cursorLine].Text[cursorCharacter..]); SetLineText(cursorLine, Lines[cursorLine].Text[..cursorCharacter]); cursorLine++; @@ -438,6 +543,11 @@ protected override void OnKeyDown(KeyEventArgs e) SetLineText(cursorLine, Lines[cursorLine].Text + Lines[cursorLine + 1].Text); RemoveLine(cursorLine + 1); } + else if (cursorLine == Lines.Count - 1 && Lines[cursorLine].Newline != null) // Delete newline character on last line + { + Lines[cursorLine].Newline = null; + Edited = true; + } } else { @@ -457,7 +567,7 @@ protected override void OnKeyDown(KeyEventArgs e) { if (Lines[i].Text.Length > 0 && Lines[i].Text[0] == '\t') { - Lines[i].Text = Lines[i].Text.Remove(0, 1); + Lines[i].Text = Lines[i].Text[1..]; } } else @@ -528,7 +638,7 @@ protected override void OnKeyDown(KeyEventArgs e) DeleteSelection(); } - string[] pastedRows = Clipboard.GetText().Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None); + string[] pastedRows = Clipboard.GetText().Split(["\r\n", "\n"], StringSplitOptions.None); string leftOfCursor = Lines[cursorLine].Text[..cursorCharacter]; string rightOfCursor = Lines[cursorLine].Text[cursorCharacter..]; @@ -861,7 +971,14 @@ protected override void OnToolTipOpening(ToolTipEventArgs e) public ObservableCollection Lines { get { return (ObservableCollection)GetValue(LinesProperty); } - set { SetValue(LinesProperty, value); } + set + { + SetValue(LinesProperty, value); + if (value.Count > 0) + { + this.documentNewline = value[0].Newline ?? NewlineMode.Windows; + } + } } @@ -1026,7 +1143,7 @@ private void InsertNewLine(int index, string newText) { this.CurrentDiff = null; - Lines.Insert(index, new Line() { Text = newText, LineIndex = -1 }); + Lines.Insert(index, new Line() { Text = newText, LineIndex = -1, Newline = cursorLine == Lines.Count - 1 ? null : documentNewline }); Edited = true; } @@ -1146,7 +1263,7 @@ private double CharacterPosition(int lineIndex, int characterIndex) foreach (TextSegment textSegment in Lines[lineIndex].TextSegments) { - if (textSegment.GetRenderedText(typeface, this.FontSize, dpiScale, AppSettings.ShowWhiteSpaceCharacters, AppSettings.TabSize, startPosition, out double runWidth) != null) + if (textSegment.GetRenderedText(typeface, this.FontSize, dpiScale, AppSettings.TabSize, startPosition, out double runWidth) != null) { foreach (double x in textSegment.RenderedText.AdvanceWidths) { diff --git a/FileDiff/FileEncoding.cs b/FileDiff/FileEncoding.cs index 07c794a..c10ef83 100644 --- a/FileDiff/FileEncoding.cs +++ b/FileDiff/FileEncoding.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.IO; +using System.Text; namespace FileDiff; @@ -7,12 +8,50 @@ public class FileEncoding #region Constructor - public FileEncoding(Encoding type, bool bom, NewlineMode newline, bool endOfFileNewline) + public FileEncoding(string path) { - this.Type = type; - this.Bom = bom; - this.Newline = newline; - this.EndOfFileNewline = endOfFileNewline; + byte[] bytes = File.ReadAllBytes(path); + + // Check if the file has a BOM + if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) + { + Type = Encoding.UTF8; + HasBom = true; + } + else if (bytes[0] == 0xFF && bytes[1] == 0xFE) + { + Type = Encoding.Unicode; + HasBom = true; + } + else if (bytes[0] == 0xFE && bytes[1] == 0xFF) + { + Type = Encoding.BigEndianUnicode; + HasBom = true; + } + else if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xFE && bytes[3] == 0xFF) + { + Type = new UTF32Encoding(true, true); + HasBom = true; + } + + // No bom found, check if data passes as a bom-less UTF-8 file + else if (CheckValidUtf8(bytes)) + { + Type = Encoding.UTF8; + } + + // Check if the file has null bytes, if so we assume it is a bom-less UTF-16 file + else + { + for (int i = 0; i < bytes.Length; i++) + { + if (bytes[i] == 0) + { + Type = Encoding.Unicode; + break; + } + } + } } #endregion @@ -44,7 +83,7 @@ public override string ToString() name = Type.WebName; } - if (Bom) + if (HasBom) { name += " BOM"; } @@ -58,13 +97,13 @@ public override string ToString() #region Properties - public Encoding Type { get; private set; } + public Encoding Type { get; private set; } = Encoding.Default; - public bool Bom { get; private set; } + public bool HasBom { get; private set; } - public NewlineMode Newline { get; private set; } + public NewlineMode Newline { get; set; } = NewlineMode.Windows; - public bool EndOfFileNewline { get; private set; } + public NewlineMode SaveNewline { get { return Newline == NewlineMode.Mixed ? NewlineMode.Windows : Newline; } } public Encoding GetEncoding { @@ -72,26 +111,26 @@ public Encoding GetEncoding { if (Type == Encoding.UTF8) { - return new UTF8Encoding(Bom); + return new UTF8Encoding(HasBom); } else if (Type == Encoding.Unicode) { - return new UnicodeEncoding(false, Bom); + return new UnicodeEncoding(false, HasBom); } else if (Type == Encoding.BigEndianUnicode) { - return new UnicodeEncoding(true, Bom); + return new UnicodeEncoding(true, HasBom); } else if (Type == Encoding.UTF32) { - return new UTF32Encoding(false, Bom); + return new UTF32Encoding(false, HasBom); } return Encoding.Default; } } - public string GetNewLineString + public string NewlineString { get { @@ -107,6 +146,142 @@ public string GetNewLineString } } + public static NewlineMode GetNewlineMode(string newlineString) + { + return newlineString switch + { + "\n" => NewlineMode.Unix, + "\r" => NewlineMode.Mac, + _ => NewlineMode.Windows, + }; + } + + #endregion + + #region Methods + + private static bool CheckValidUtf8(byte[] bytes) + { + int i = 0; + while (i < bytes.Length - 4) + { + // 1 byte character + if (bytes[i] >= 0x00 && bytes[i] <= 0x7F) + { + i++; + continue; + } + + // 2 byte character + if (bytes[i] >= 0xC2 && bytes[i] <= 0xDF) + { + if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) + { + i += 2; + continue; + } + } + + // 3 byte character + if (bytes[i] == 0xE0) + { + if (bytes[i + 1] >= 0xA0 && bytes[i + 1] <= 0xBF) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + i += 3; + continue; + } + } + } + + if (bytes[i] >= 0xE1 && bytes[i] <= 0xEC) + { + if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + i += 3; + continue; + } + } + } + + if (bytes[i] == 0xED) + { + if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0x9F) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + i += 3; + continue; + } + } + } + + if (bytes[i] >= 0xEE && bytes[i] <= 0xEF) + { + if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + i += 3; + continue; + } + } + } + + // 4 byte character + if (bytes[i] == 0xF0) + { + if (bytes[i + 1] >= 0x90 && bytes[i + 1] <= 0xBF) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + if (bytes[i + 3] >= 0x80 && bytes[i + 3] <= 0xBF) + { + i += 4; + continue; + } + } + } + } + + if (bytes[i] >= 0xF1 && bytes[i] <= 0xF3) + { + if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + if (bytes[i + 3] >= 0x80 && bytes[i + 3] <= 0xBF) + { + i += 4; + continue; + } + } + } + } + + if (bytes[i] == 0xF4) + { + if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0x8F) + { + if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) + { + if (bytes[i + 3] >= 0x80 && bytes[i + 3] <= 0xBF) + { + i += 4; + continue; + } + } + } + } + + return false; + } + return true; + } + #endregion } diff --git a/FileDiff/Line.cs b/FileDiff/Line.cs index 97e1b83..2b4857b 100644 --- a/FileDiff/Line.cs +++ b/FileDiff/Line.cs @@ -26,7 +26,7 @@ public Line() public override string ToString() { - return $"{LineIndex}-{MatchingLineIndex} ({DiffId})".PadRight(12, ' ') + $" {Type.ToString().PadRight(10, ' ')} {Text}"; + return $" {LineIndex}-{MatchingLineIndex} ({DiffId})".PadRight(12, ' ') + $" {Type.ToString().PadRight(10, ' ')} {Text} {Newline} "; } public override int GetHashCode() @@ -114,6 +114,8 @@ public TextState Type } } + public NewlineMode? Newline { get; set; } = null; + public bool IsFiller { get diff --git a/FileDiff/TextSegment.cs b/FileDiff/TextSegment.cs index 3626fb9..5f05a8c 100644 --- a/FileDiff/TextSegment.cs +++ b/FileDiff/TextSegment.cs @@ -33,7 +33,7 @@ public override string ToString() public TextState Type { get; set; } - private string Text { get; set; } + public string Text { get; set; } public bool IsWhiteSpace { @@ -69,19 +69,17 @@ public SolidColorBrush ForegroundBrush private Typeface renderedTypeface; private double renderedFontSize; private double renderedDpiScale; - private bool renderedWhiteSpace; private int renderedTabSize; - public GlyphRun GetRenderedText(Typeface typeface, double fontSize, double dpiScale, bool whiteSpace, int tabSize, double startPosition, out double runWidth) + public GlyphRun GetRenderedText(Typeface typeface, double fontSize, double dpiScale, int tabSize, double startPosition, out double runWidth) { - if (!typeface.Equals(renderedTypeface) || fontSize != renderedFontSize || dpiScale != renderedDpiScale || whiteSpace != renderedWhiteSpace || tabSize != renderedTabSize) + if (!typeface.Equals(renderedTypeface) || fontSize != renderedFontSize || dpiScale != renderedDpiScale || tabSize != renderedTabSize) { RenderedText = TextUtils.CreateGlyphRun(Text, typeface, fontSize, dpiScale, startPosition, out renderedTextWidth); renderedTypeface = typeface; renderedFontSize = fontSize; renderedDpiScale = dpiScale; - renderedWhiteSpace = whiteSpace; renderedTabSize = tabSize; } diff --git a/FileDiff/TextUtils.cs b/FileDiff/TextUtils.cs index 7b9b1d3..ee6111c 100644 --- a/FileDiff/TextUtils.cs +++ b/FileDiff/TextUtils.cs @@ -128,9 +128,11 @@ private static ushort ReplaceGlyph(int codePoint, GlyphTypeface glyphTypeface, d if (codePoint == '\t') { - displayCodePoint = AppSettings.ShowWhiteSpaceCharacters ? '›' : ' '; + displayCodePoint = ' '; glyphTypeface.CharacterToGlyphMap.TryGetValue(displayCodePoint, out glyphIndex); + + // Tab width varies depending on the position of the tab character in the line. double tabCharacterWidth = AppSettings.TabSize * characterWidth; width = tabCharacterWidth - ((characterStartPosition + tabCharacterWidth) % tabCharacterWidth); @@ -141,16 +143,8 @@ private static ushort ReplaceGlyph(int codePoint, GlyphTypeface glyphTypeface, d return glyphIndex; } - else if (codePoint == ' ') - { - displayCodePoint = AppSettings.ShowWhiteSpaceCharacters ? '·' : ' '; - - glyphTypeface.CharacterToGlyphMap.TryGetValue(displayCodePoint, out glyphIndex); - width = Math.Ceiling(glyphTypeface.AdvanceWidths[glyphIndex] * fontSize / dpiScale) * dpiScale; - return glyphIndex; - } - // Most fixed width fonts don't have a glyph for zero width space, use the space glyph but set width to 0 - // to not get black squares when painting. + // Most fixed width fonts don't have a glyph for zero width space, use the space glyph + // but set width to 0 to not get black squares when painting. else if (codePoint == '\u200B') { displayCodePoint = ' '; diff --git a/FileDiff/Unicode.cs b/FileDiff/Unicode.cs deleted file mode 100644 index 124fd26..0000000 --- a/FileDiff/Unicode.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System.IO; -using System.Text; -using System.Text.RegularExpressions; - -namespace FileDiff; - -static class Unicode -{ - - public static FileEncoding GetEncoding(string path) - { - Encoding encoding = Encoding.Default; - bool bom = false; - NewlineMode newlineMode = NewlineMode.Windows; - bool endOfFileNewline = false; - - var bytes = new byte[10000]; - int bytesRead = 0; - - // Check if the file ends with a newline character - using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) - { - bytesRead = fileStream.Read(bytes, 0, bytes.Length); - - if (bytesRead > 0) - { - byte[] lastByte = new byte[1]; - fileStream.Seek(-1, SeekOrigin.End); - fileStream.Read(lastByte, 0, 1); - - if (lastByte[0] == '\n' || lastByte[0] == '\r') - { - endOfFileNewline = true; - } - } - } - - // Check if the file has a BOM - if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) - { - encoding = Encoding.UTF8; - bom = true; - } - else if (bytes[0] == 0xFF && bytes[1] == 0xFE) - { - encoding = Encoding.Unicode; - bom = true; - } - else if (bytes[0] == 0xFE && bytes[1] == 0xFF) - { - encoding = Encoding.BigEndianUnicode; - bom = true; - } - else if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0xFE && bytes[3] == 0xFF) - { - encoding = new UTF32Encoding(true, true); - bom = true; - } - - // No bom found, check if data passes as a bom-less UTF-8 file - else if (ValidUtf8(bytes, bytesRead)) - { - encoding = Encoding.UTF8; - } - - // Check if the file has null bytes, if so we assume it's a bom-less UTF-16 - else - { - for (int i = 0; i < bytesRead; i++) - { - if (bytes[i] == 0) - { - encoding = Encoding.Unicode; - break; - } - } - } - - // Check what newline characters are used - MatchCollection allNewLines = Regex.Matches(File.ReadAllText(path, encoding), "(\r\n|\r|\n)"); - - HashSet distinctNewLines = []; - - foreach (Match match in allNewLines) - { - distinctNewLines.Add(match.Value); - } - - if (distinctNewLines.Count > 1) - { - newlineMode = NewlineMode.Mixed; - } - else if (distinctNewLines.Count == 1) - { - newlineMode = distinctNewLines.ToArray()[0] switch - { - "\n" => NewlineMode.Unix, - "\r" => NewlineMode.Mac, - _ => NewlineMode.Windows, - }; - } - - return new FileEncoding(encoding, bom, newlineMode, endOfFileNewline); - } - - public static bool ValidUtf8(byte[] bytes, int length) - { - int i = 0; - while (i < length - 4) - { - // 1 byte character - if (bytes[i] >= 0x00 && bytes[i] <= 0x7F) - { - i++; - continue; - } - - // 2 byte character - if (bytes[i] >= 0xC2 && bytes[i] <= 0xDF) - { - if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) - { - i += 2; - continue; - } - } - - // 3 byte character - if (bytes[i] == 0xE0) - { - if (bytes[i + 1] >= 0xA0 && bytes[i + 1] <= 0xBF) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - i += 3; - continue; - } - } - } - - if (bytes[i] >= 0xE1 && bytes[i] <= 0xEC) - { - if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - i += 3; - continue; - } - } - } - - if (bytes[i] == 0xED) - { - if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0x9F) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - i += 3; - continue; - } - } - } - - if (bytes[i] >= 0xEE && bytes[i] <= 0xEF) - { - if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - i += 3; - continue; - } - } - } - - // 4 byte character - if (bytes[i] == 0xF0) - { - if (bytes[i + 1] >= 0x90 && bytes[i + 1] <= 0xBF) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - if (bytes[i + 3] >= 0x80 && bytes[i + 3] <= 0xBF) - { - i += 4; - continue; - } - } - } - } - - if (bytes[i] >= 0xF1 && bytes[i] <= 0xF3) - { - if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0xBF) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - if (bytes[i + 3] >= 0x80 && bytes[i + 3] <= 0xBF) - { - i += 4; - continue; - } - } - } - } - - if (bytes[i] == 0xF4) - { - if (bytes[i + 1] >= 0x80 && bytes[i + 1] <= 0x8F) - { - if (bytes[i + 2] >= 0x80 && bytes[i + 2] <= 0xBF) - { - if (bytes[i + 3] >= 0x80 && bytes[i + 3] <= 0xBF) - { - i += 4; - continue; - } - } - } - } - - return false; - } - return true; - } - -} diff --git a/FileDiff/Utils.cs b/FileDiff/Utils.cs index 15249b1..7084119 100644 --- a/FileDiff/Utils.cs +++ b/FileDiff/Utils.cs @@ -66,4 +66,18 @@ public static string FixRootPath(string path) return path; } + #region Extention Methods + + public static bool In(this T item, params T[] list) + { + return list.Contains(item); + } + + public static bool NotIn(this T item, params T[] list) + { + return !list.Contains(item); + } + + #endregion + } diff --git a/FileDiff/Windows/MainWindow.xaml b/FileDiff/Windows/MainWindow.xaml index 3b1bb15..8fbc95b 100644 --- a/FileDiff/Windows/MainWindow.xaml +++ b/FileDiff/Windows/MainWindow.xaml @@ -433,7 +433,7 @@ - + @@ -446,7 +446,7 @@ - + diff --git a/FileDiff/Windows/MainWindow.xaml.cs b/FileDiff/Windows/MainWindow.xaml.cs index 0f0fd42..a2ff25f 100644 --- a/FileDiff/Windows/MainWindow.xaml.cs +++ b/FileDiff/Windows/MainWindow.xaml.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.IO; using System.Net.Http; +using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -167,29 +168,29 @@ private void CompareFiles() { if (File.Exists(leftPath)) { - leftLines = []; leftSelection = leftPath; - ViewModel.LeftFileEncoding = Unicode.GetEncoding(leftPath); + + leftLines = GetFileContent(leftPath, out FileEncoding encoding); + ViewModel.LeftFileEncoding = encoding; ViewModel.LeftFileDirty = false; - int i = 0; - foreach (string s in File.ReadAllLines(leftPath, ViewModel.LeftFileEncoding.Type)) + foreach (Line line in leftLines) { - leftLines.Add(new Line() { Type = TextState.Deleted, Text = s, LineIndex = i++ }); + line.Type = TextState.Deleted; } } if (File.Exists(rightPath)) { - rightLines = []; rightSelection = rightPath; - ViewModel.RightFileEncoding = Unicode.GetEncoding(rightPath); + + rightLines = GetFileContent(rightPath, out FileEncoding encoding); + ViewModel.RightFileEncoding = encoding; ViewModel.RightFileDirty = false; - int i = 0; - foreach (string s in File.ReadAllLines(rightPath, ViewModel.RightFileEncoding.Type)) + foreach (Line line in rightLines) { - rightLines.Add(new Line() { Type = TextState.New, Text = s, LineIndex = i++ }); + line.Type = TextState.New; } } } @@ -210,7 +211,7 @@ private void CompareFiles() canceledRightLines = rightLines; BackgroundCompare.progressHandler = new Progress(CompareStatusUpdate); - Task.Run(() => BackgroundCompare.MatchFiles(leftLines, rightLines)).ContinueWith(CompareFilesFinished, TaskScheduler.FromCurrentSynchronizationContext()); + Task.Run(() => BackgroundCompare.MatchFiles(leftLines, rightLines, ViewModel.LeftFileEncoding, ViewModel.RightFileEncoding)).ContinueWith(CompareFilesFinished, TaskScheduler.FromCurrentSynchronizationContext()); progressTimer.Start(); } @@ -309,6 +310,58 @@ private void CompareDirectoriesFinished(Task GetFileContent(string path, out FileEncoding fileEncoding) + { + fileEncoding = new FileEncoding(path); + + List lines = []; + + string allText = File.ReadAllText(path, fileEncoding.Type); + + // Check what newline characters are used + MatchCollection allNewlines = Regex.Matches(allText, "(\r\n|\r|\n)"); + HashSet distinctNewLines = []; + + int offset = 0; + int lineIndex = 0; + + foreach (Match match in allNewlines) + { + distinctNewLines.Add(match.Value); + + lines.Add(new Line() + { + Text = allText[offset..match.Index], + Newline = FileEncoding.GetNewlineMode(match.Value), + LineIndex = lineIndex++ + }); + + offset = match.Index + match.Length; + } + + // If the last line (or entirety) of the file has no newline character + if (offset < allText.Length) + { + lines.Add(new Line() + { + Text = allText[offset..], + Newline = null, + LineIndex = lineIndex++ + }); + } + + if (distinctNewLines.Count > 1) + { + fileEncoding.Newline = NewlineMode.Mixed; + } + else if (distinctNewLines.Count == 1) + { + fileEncoding.Newline = FileEncoding.GetNewlineMode(distinctNewLines.ToArray()[0]); + } + + return lines; + } + private void InitNavigationState() { ViewModel.EditMode = false; @@ -554,6 +607,34 @@ private async void CheckForNewVersion(bool forced = false) } } + private static void SaveFile(string savePath, ObservableCollection lines, FileEncoding fileEncoding) + { + try + { + using StreamWriter sw = new(savePath, false, fileEncoding.GetEncoding); + sw.NewLine = fileEncoding.NewlineString; + + foreach (Line l in lines) + { + if (!l.IsFiller) + { + if (l.Newline == null) + { + sw.Write(l.Text); + } + else + { + sw.WriteLine(l.Text); + } + } + } + } + catch (Exception exception) + { + MessageBox.Show(exception.Message, "Error Saving File", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + #endregion #region Events @@ -873,26 +954,9 @@ private void CommandSaveLeftFile_Executed(object sender, ExecutedRoutedEventArgs if (File.Exists(leftPath) && ViewModel.LeftFileDirty) { - try - { - using (StreamWriter sw = new(leftPath, false, ViewModel.LeftFileEncoding.GetEncoding)) - { - sw.NewLine = ViewModel.LeftFileEncoding.GetNewLineString; - foreach (Line l in ViewModel.LeftFile) - { - if (!l.IsFiller) - { - sw.WriteLine(l.Text); - } - } - } - ViewModel.LeftFileDirty = false; - ViewModel.LeftFileEdited = false; - } - catch (Exception exception) - { - MessageBox.Show(exception.Message, "Error Saving File", MessageBoxButton.OK, MessageBoxImage.Error); - } + SaveFile(leftPath, ViewModel.LeftFile, ViewModel.LeftFileEncoding); + ViewModel.LeftFileDirty = false; + ViewModel.LeftFileEdited = false; } } @@ -907,26 +971,9 @@ private void CommandSaveRightFile_Executed(object sender, ExecutedRoutedEventArg if (File.Exists(rightPath) && ViewModel.RightFileDirty) { - try - { - using (StreamWriter sw = new(rightPath, false, ViewModel.RightFileEncoding.GetEncoding)) - { - sw.NewLine = ViewModel.RightFileEncoding.GetNewLineString; - foreach (Line l in ViewModel.RightFile) - { - if (!l.IsFiller) - { - sw.WriteLine(l.Text); - } - } - } - ViewModel.RightFileDirty = false; - ViewModel.RightFileEdited = false; - } - catch (Exception exception) - { - MessageBox.Show(exception.Message, "Error Saving File", MessageBoxButton.OK, MessageBoxImage.Error); - } + SaveFile(rightPath, ViewModel.RightFile, ViewModel.RightFileEncoding); + ViewModel.RightFileDirty = false; + ViewModel.RightFileEdited = false; } } diff --git a/FileDiff/Windows/MainWindowViewModel.cs b/FileDiff/Windows/MainWindowViewModel.cs index 606a22d..26f9c19 100644 --- a/FileDiff/Windows/MainWindowViewModel.cs +++ b/FileDiff/Windows/MainWindowViewModel.cs @@ -545,6 +545,12 @@ public Brush MovedToBackground set { AppSettings.MovedToBackground = value as SolidColorBrush; OnPropertyChangedRepaint(nameof(MovedToBackground)); } } + public Brush WhiteSpaceForeground + { + get { return AppSettings.WhiteSpaceForeground; } + set { AppSettings.WhiteSpaceForeground = value as SolidColorBrush; OnPropertyChangedRepaint(nameof(WhiteSpaceForeground)); } + } + // Editor colors public Brush SelectionBackground diff --git a/FileDiff/Windows/OptionsWindow.xaml b/FileDiff/Windows/OptionsWindow.xaml index 4e61535..3364bd7 100644 --- a/FileDiff/Windows/OptionsWindow.xaml +++ b/FileDiff/Windows/OptionsWindow.xaml @@ -123,6 +123,7 @@ + @@ -197,20 +198,23 @@ -