Skip to content

Commit 73bdf59

Browse files
authored
Merge pull request #491 from LogExperts/missing_characters_in_log_view
Missing characters in log view
2 parents 73c1ff6 + 3796da4 commit 73bdf59

25 files changed

Lines changed: 381 additions & 170 deletions

.github/copilot-instructions.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
- Serilog.Formatting.Compact format support
1818

1919
### Technology Stack
20-
- **Primary Language**: C# (.NET 8.0-windows target framework)
20+
- **Primary Language**: C# (.NET 10.0-windows target framework)
2121
- **UI Framework**: Windows Forms
2222
- **Build System**: Nuke Build System with MSBuild
2323
- **Target Platform**: Windows (requires Windows-specific dependencies)
@@ -39,9 +39,9 @@
3939
**CRITICAL**: This project requires Windows development environment and .NET 9.0.301 SDK or compatible.
4040

4141
### Environment Setup
42-
1. **Install .NET SDK**: Project requires .NET 9.0.301 SDK (specified in `global.json`)
43-
2. **Windows Environment**: Build targets `net8.0-windows` and uses Windows Forms
44-
3. **Visual Studio**: Recommended Visual Studio 2017+ or Visual Studio Code with C# extension
42+
1. **Install .NET SDK**: Project requires .NET 10.0.100 SDK (specified in `global.json`)
43+
2. **Windows Environment**: Build targets `net10.0-windows` and uses Windows Forms
44+
3. **Visual Studio**: Recommended Visual Studio 2026+ or Visual Studio Code with C# extension
4545
4. **Optional Dependencies**:
4646
- Chocolatey (for packaging)
4747
- Inno Setup 5 or 6 (for setup creation)
@@ -88,7 +88,7 @@ dotnet test --no-build --verbosity normal
8888
- **Workaround**: Use Windows environment or Windows Subsystem for Linux with proper .NET Windows SDK
8989

9090
2. **.NET Version Mismatch**:
91-
- Project requires .NET 9.0.301 but may encounter .NET 8.0 environments
91+
- Project requires .NET 10.0.100 but may encounter .NET 8.0 environments
9292
- **Workaround**: Nuke build system automatically downloads correct SDK version
9393

9494
3. **Build Timing**:
@@ -176,7 +176,7 @@ LogExpert/
176176
2. **`.github/workflows/test_dotnet.yml`**:
177177
- Runs on push to Development branch
178178
- Executes unit tests
179-
- Uses .NET 9.0.x SDK
179+
- Uses .NET 10.0.x SDK
180180

181181
#### AppVeyor Integration
182182
- **`appveyor.yml`**: Legacy CI configuration
@@ -224,7 +224,7 @@ Key external dependencies (managed via Directory.Packages.props):
224224
#### Adding Dependencies
225225
1. Update `src/Directory.Packages.props` for version management
226226
2. Add `<PackageReference>` in specific project files
227-
3. Ensure compatibility with .NET 8.0 target framework
227+
3. Ensure compatibility with .NET 10.0 target framework
228228

229229
### Build Troubleshooting
230230
- **Missing Windows SDK**: Ensure Windows development environment

src/ColumnizerLib.UnitTests/ColumnTests.cs

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,74 @@ namespace ColumnizerLib.UnitTests;
1010
[TestFixture]
1111
public class ColumnTests
1212
{
13+
[SetUp]
14+
public void SetUp()
15+
{
16+
// Reset to default before each test
17+
Column.SetMaxDisplayLength(20_000);
18+
}
19+
20+
[Test]
21+
public void Column_DisplayMaxLength_DefaultIs20000()
22+
{
23+
Assert.That(Column.GetMaxDisplayLength(), Is.EqualTo(20_000));
24+
}
25+
1326
[Test]
14-
public void Column_LineCutOff ()
27+
public void Column_DisplayMaxLength_CanBeConfigured()
1528
{
16-
var expectedFullValue = new StringBuilder().Append('6', 4675).Append("1234").ToString();
17-
var expectedDisplayValue = expectedFullValue[..4675] + "..."; // Using substring shorthand
29+
Column.SetMaxDisplayLength(50_000);
30+
Assert.That(Column.GetMaxDisplayLength(), Is.EqualTo(50_000));
31+
32+
// Reset for other tests
33+
Column.SetMaxDisplayLength(20_000);
34+
}
1835

36+
[Test]
37+
public void Column_DisplayMaxLength_EnforcesMinimum()
38+
{
39+
Assert.Throws<ArgumentOutOfRangeException>(() => Column.SetMaxDisplayLength(500));
40+
}
41+
42+
[Test]
43+
public void Column_TruncatesAtConfiguredDisplayLength()
44+
{
45+
Column.SetMaxDisplayLength(10_000);
46+
47+
// Create a line longer than the display max length
48+
var longValue = new StringBuilder().Append('X', 15_000).ToString();
49+
1950
Column column = new()
2051
{
21-
FullValue = expectedFullValue
52+
FullValue = longValue
2253
};
2354

24-
Assert.That(column.DisplayValue, Is.EqualTo(expectedDisplayValue));
25-
Assert.That(column.FullValue, Is.EqualTo(expectedFullValue));
55+
// FullValue should contain the full line
56+
Assert.That(column.FullValue, Is.EqualTo(longValue));
57+
Assert.That(column.FullValue.Length, Is.EqualTo(15_000));
58+
59+
// DisplayValue should be truncated at 10,000 with "..." appended
60+
Assert.That(column.DisplayValue.Length, Is.EqualTo(10_003)); // 10000 + "..."
61+
Assert.That(column.DisplayValue.EndsWith("..."), Is.True);
62+
Assert.That(column.DisplayValue.StartsWith("XXX"), Is.True);
63+
64+
// Reset for other tests
65+
Column.SetMaxDisplayLength(20_000);
2666
}
2767

2868
[Test]
29-
public void Column_NoLineCutOff ()
69+
public void Column_NoTruncationWhenBelowLimit()
3070
{
31-
var expected = new StringBuilder().Append('6', 4675).ToString();
71+
Column.SetMaxDisplayLength(20_000);
72+
73+
var normalValue = new StringBuilder().Append('Y', 5_000).ToString();
3274
Column column = new()
3375
{
34-
FullValue = expected
76+
FullValue = normalValue
3577
};
3678

3779
Assert.That(column.DisplayValue, Is.EqualTo(column.FullValue));
80+
Assert.That(column.DisplayValue.Length, Is.EqualTo(5_000));
3881
}
3982

4083
[Test]

src/ColumnizerLib/Column.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
using System;
2-
using System.Collections.Generic;
3-
41
using LogExpert;
52

63
namespace ColumnizerLib;
@@ -9,21 +6,22 @@ public class Column : IColumn
96
{
107
#region Fields
118

12-
private const int MAXLENGTH = 4678 - 3;
139
private const string REPLACEMENT = "...";
1410

11+
// Display-level maximum line length (separate from reader-level limit)
12+
// Can be configured via SetMaxDisplayLength()
13+
private static int _maxDisplayLength = 20_000;
14+
1515
private static readonly List<Func<string, string>> _replacements = [
1616
//replace tab with 3 spaces, from old coding. Needed???
1717
input => input.Replace("\t", " ", StringComparison.Ordinal),
1818

1919
//shorten string if it exceeds maxLength
20-
input => input.Length > MAXLENGTH
21-
? string.Concat(input.AsSpan(0, MAXLENGTH), REPLACEMENT)
20+
input => input.Length > _maxDisplayLength
21+
? string.Concat(input.AsSpan(0, _maxDisplayLength), REPLACEMENT)
2222
: input
2323
];
2424

25-
private string _fullValue;
26-
2725
#endregion
2826

2927
#region cTor
@@ -40,6 +38,9 @@ static Column ()
4038
{
4139
//Everything below Win8 the installed fonts seems to not to support reliabel
4240
//Replace null char with space
41+
//.net 10 does no longer support windows lower then windows 10
42+
//TODO: remove if with one of the next releases
43+
//https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md
4344
_replacements.Add(input => input.Replace("\0", " ", StringComparison.Ordinal));
4445
}
4546

@@ -56,10 +57,10 @@ static Column ()
5657

5758
public string FullValue
5859
{
59-
get => _fullValue;
60+
get;
6061
set
6162
{
62-
_fullValue = value;
63+
field = value;
6364

6465
var temp = FullValue;
6566

@@ -80,6 +81,26 @@ public string FullValue
8081

8182
#region Public methods
8283

84+
/// <summary>
85+
/// Configures the maximum display length for all Column instances.
86+
/// This is separate from the reader-level MaxLineLength.
87+
/// </summary>
88+
/// <param name="maxLength">Maximum length for displayed content. Must be at least 1000.</param>
89+
public static void SetMaxDisplayLength (int maxLength)
90+
{
91+
if (maxLength < 1000)
92+
{
93+
throw new ArgumentOutOfRangeException(nameof(maxLength), "Maximum display length must be at least 1000 characters.");
94+
}
95+
96+
_maxDisplayLength = maxLength;
97+
}
98+
99+
/// <summary>
100+
/// Gets the current maximum display length setting.
101+
/// </summary>
102+
public static int GetMaxDisplayLength () => _maxDisplayLength;
103+
83104
public static Column[] CreateColumns (int count, IColumnizedLogLine parent)
84105
{
85106
return CreateColumns(count, parent, string.Empty);

src/LogExpert.Core/Classes/Log/LogfileReader.cs

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ public class LogfileReader : IAutoLogLineColumnizerCallback, IDisposable
2929
private ReaderWriterLock _bufferListLock;
3030
private bool _contentDeleted;
3131
private int _currLineCount;
32+
33+
private readonly int _maximumLineLength;
34+
3235
private ReaderWriterLock _disposeLock;
36+
3337
private EncodingOptions _encodingOptions;
38+
3439
private long _fileLength;
3540
private Task _garbageCollectorTask;
3641
private Task _monitorTask;
@@ -50,27 +55,34 @@ public class LogfileReader : IAutoLogLineColumnizerCallback, IDisposable
5055
#region cTor
5156

5257
/// Public constructor for single file.
53-
public LogfileReader (string fileName, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry)
54-
: this([fileName], encodingOptions, multiFile, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry)
58+
public LogfileReader (string fileName, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry, int maximumLineLength)
59+
: this([fileName], encodingOptions, multiFile, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry, maximumLineLength)
5560
{
5661
}
5762

5863
/// Public constructor for multiple files.
59-
public LogfileReader (string[] fileNames, EncodingOptions encodingOptions, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry)
60-
: this(fileNames, encodingOptions, true, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry)
64+
public LogfileReader (string[] fileNames, EncodingOptions encodingOptions, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry, int maximumLineLength)
65+
: this(fileNames, encodingOptions, true, bufferCount, linesPerBuffer, multiFileOptions, useNewReader, pluginRegistry, maximumLineLength)
6166
{
6267
// In this overload, we assume multiFile is always true.
6368
}
6469

6570
// Single private constructor that contains the common initialization logic.
66-
private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry)
71+
private LogfileReader (string[] fileNames, EncodingOptions encodingOptions, bool multiFile, int bufferCount, int linesPerBuffer, MultiFileOptions multiFileOptions, bool useNewReader, IPluginRegistry pluginRegistry, int maximumLineLength)
6772
{
6873
// Validate input: at least one file must be provided.
6974
if (fileNames == null || fileNames.Length < 1)
7075
{
7176
throw new ArgumentException("Must provide at least one file.", nameof(fileNames));
7277
}
7378

79+
//Set default maximum line length if invalid value provided.
80+
if (maximumLineLength <= 0)
81+
{
82+
maximumLineLength = 500;
83+
}
84+
85+
_maximumLineLength = maximumLineLength;
7486
_useNewReader = useNewReader;
7587
EncodingOptions = encodingOptions;
7688
_max_buffers = bufferCount;
@@ -1535,9 +1547,12 @@ private void FileChanged ()
15351547

15361548
private void FireChangeEvent ()
15371549
{
1538-
LogEventArgs args = new();
1539-
args.PrevFileSize = FileSize;
1540-
args.PrevLineCount = LineCount;
1550+
LogEventArgs args = new()
1551+
{
1552+
PrevFileSize = FileSize,
1553+
PrevLineCount = LineCount
1554+
};
1555+
15411556
var newSize = _fileLength;
15421557
if (newSize < FileSize || _isDeleted)
15431558
{
@@ -1604,12 +1619,9 @@ private ILogStreamReader GetLogStreamReader (Stream stream, EncodingOptions enco
16041619

16051620
private ILogStreamReader CreateLogStreamReader (Stream stream, EncodingOptions encodingOptions, bool useSystemReader)
16061621
{
1607-
if (useSystemReader)
1608-
{
1609-
return new PositionAwareStreamReaderSystem(stream, encodingOptions);
1610-
}
1611-
1612-
return new PositionAwareStreamReaderLegacy(stream, encodingOptions);
1622+
return useSystemReader
1623+
? new PositionAwareStreamReaderSystem(stream, encodingOptions, _maximumLineLength)
1624+
: new PositionAwareStreamReaderLegacy(stream, encodingOptions, _maximumLineLength);
16131625
}
16141626

16151627
private bool ReadLine (ILogStreamReader reader, int lineNum, int realLineNum, out string outLine)

src/LogExpert.Core/Classes/Log/PositionAwareStreamReaderBase.cs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ public abstract class PositionAwareStreamReaderBase : LogStreamReaderBase
88
{
99
#region Fields
1010

11-
private const int MAX_LINE_LEN = 20000;
12-
1311
private static readonly Encoding[] _preambleEncodings = [Encoding.UTF8, Encoding.Unicode, Encoding.BigEndianUnicode, Encoding.UTF32];
1412

1513
private readonly BufferedStream _stream;
@@ -24,10 +22,12 @@ public abstract class PositionAwareStreamReaderBase : LogStreamReaderBase
2422

2523
#region cTor
2624

27-
protected PositionAwareStreamReaderBase (Stream stream, EncodingOptions encodingOptions)
25+
protected PositionAwareStreamReaderBase (Stream stream, EncodingOptions encodingOptions, int maximumLineLength)
2826
{
2927
_stream = new BufferedStream(stream);
3028

29+
MaximumLineLength = maximumLineLength;
30+
3131
_preambleLength = DetectPreambleLengthAndEncoding(out var detectedEncoding);
3232

3333
var usedEncoding = GetUsedEncoding(encodingOptions, detectedEncoding);
@@ -61,7 +61,7 @@ public sealed override long Position
6161
_position = value; // +Encoding.GetPreamble().Length; // 1
6262
//stream.Seek(pos, SeekOrigin.Begin); // 2
6363
//stream.Seek(pos + Encoding.GetPreamble().Length, SeekOrigin.Begin); // 3
64-
_stream.Seek(_position + _preambleLength, SeekOrigin.Begin); // 4
64+
_ = _stream.Seek(_position + _preambleLength, SeekOrigin.Begin); // 4
6565

6666
ResetReader();
6767
}
@@ -71,8 +71,11 @@ public sealed override long Position
7171

7272
public sealed override bool IsBufferComplete => true;
7373

74-
//Refactor this needs to be given and should not be added like this
75-
protected static int MaxLineLen => 500;//ConfigManager.Settings.Preferences.MaxLineLength;
74+
protected static int MaximumLineLength
75+
{
76+
get => field;
77+
private set => field = value;
78+
}
7679

7780
#endregion
7881

@@ -89,7 +92,7 @@ protected override void Dispose (bool disposing)
8992
_stream.Dispose();
9093
_reader.Dispose();
9194
IsDisposed = true;
92-
}
95+
}
9396
}
9497

9598
//TODO This is unsafe and should be refactored
@@ -188,21 +191,14 @@ private int DetectPreambleLengthAndEncoding (out Encoding detectedEncoding)
188191
return 0;
189192
}
190193

191-
private Encoding GetUsedEncoding (EncodingOptions encodingOptions, Encoding detectedEncoding)
194+
private static Encoding GetUsedEncoding (EncodingOptions encodingOptions, Encoding detectedEncoding)
192195
{
193-
if (encodingOptions.Encoding != null)
194-
{
195-
return encodingOptions.Encoding;
196-
}
197-
198-
if (detectedEncoding != null)
199-
{
200-
return detectedEncoding;
201-
}
202-
203-
return encodingOptions.DefaultEncoding ?? Encoding.Default;
196+
return encodingOptions.Encoding ??
197+
detectedEncoding ??
198+
encodingOptions.DefaultEncoding ??
199+
Encoding.Default;
204200
}
205-
private int GetPosIncPrecomputed (Encoding usedEncoding)
201+
private static int GetPosIncPrecomputed (Encoding usedEncoding)
206202
{
207203
switch (usedEncoding)
208204
{

0 commit comments

Comments
 (0)