diff --git a/.claude/hooks/cleanup-cs.ps1 b/.claude/hooks/cleanup-cs.ps1
index 0b3e1101..94ee20eb 100644
--- a/.claude/hooks/cleanup-cs.ps1
+++ b/.claude/hooks/cleanup-cs.ps1
@@ -36,7 +36,7 @@ if (-not $changed) { exit 0 }
# --include narrows the work to changed files.
$includes = ($changed | ForEach-Object { "--include=$_" }) -join ' '
if ($includes) {
- & dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="Built-in: Full Cleanup" $includes.Split(' ') --no-build *> $null
+ & dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="TG.Text Full Cleanup" $includes.Split(' ') --no-build *> $null
}
# Surface the net effect so the agent sees its own drift.
diff --git a/.editorconfig b/.editorconfig
index d1f412ba..db997d2b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -61,7 +61,7 @@ csharp_space_between_square_brackets = false
# discoverability, but don't block the build. Enforcement happens in two other places:
#
# 1. `dotnet format Terminal.Gui.Text.slnx --verify-no-changes` in CI, and
-# 2. `dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="Full Cleanup"` in CI
+# 2. `dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="TG.Text Full Cleanup"` in CI
# (and the Stop hook in .claude/settings.json before each agent turn ends).
#
# Both tools auto-fix what they can and surface a clean diff. The `:warning` posture
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c42ade9e..fc1d8ef8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,17 +52,17 @@ jobs:
- name: Verify code style — ReSharper cleanupcode
if: matrix.os == 'ubuntu-latest'
run: |
- # Use the built-in profile name (jb cleanupcode does not always discover custom
- # profile names from team-shared .DotSettings files reliably). Our .DotSettings
- # still ships its own profile + style settings (var, expression-bodied, etc.) for
- # Rider users; the CLI just runs the default-shaped cleanup using those settings.
+ # Use our custom profile name defined in the team-shared .DotSettings file.
+ # Previous attempts used "Built-in: Full Cleanup" which worked but didn't pick
+ # up our tweaks (CSUseAutoProperty OFF, etc.). The custom-named profile resolves
+ # reliably from the .DotSettings file.
dotnet jb cleanupcode Terminal.Gui.Text.slnx \
- --profile="Built-in: Full Cleanup" \
+ --profile="TG.Text Full Cleanup" \
--no-build \
--exclude="third_party/**/*" \
|| true
if ! git diff --exit-code; then
- echo "::error::ReSharper code cleanup found style drift. Run 'dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile=\"Built-in: Full Cleanup\"' locally and commit the result."
+ echo "::error::ReSharper code cleanup found style drift. Run 'dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile=\"TG.Text Full Cleanup\"' locally and commit the result."
exit 1
fi
diff --git a/CLAUDE.md b/CLAUDE.md
index 048feb5d..68f176ab 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -84,10 +84,10 @@ The fork is **hard** — re-syncs are manual and deliberate, triggered only by u
Adopts Terminal.Gui's house style. Three enforcement layers:
1. **`.editorconfig` + `dotnet format`** — formatting, var, expression-bodied, collection expressions, modern syntax preferences. CI runs `dotnet format --verify-no-changes`.
-2. **`Terminal.Gui.Text.slnx.DotSettings` + `dotnet jb cleanupcode`** — ReSharper-driven cleanup ("Full Cleanup" profile). Catches what `dotnet format` misses (XML doc spacing, using sorting, name qualifier removal, expression-bodied conversions). CI runs `dotnet jb cleanupcode` and fails on any diff.
+2. **`Terminal.Gui.Text.slnx.DotSettings` + `dotnet jb cleanupcode`** — ReSharper-driven cleanup ("TG.Text Full Cleanup" profile). Catches what `dotnet format` misses (XML doc spacing, using sorting, name qualifier removal, expression-bodied conversions). CI runs `dotnet jb cleanupcode` and fails on any diff.
3. **A Stop hook in `.claude/settings.json`** that runs both tools on .cs files modified during the session before the agent reports done. Output is suppressed unless the cleanup actually changed something.
-**Before declaring work complete, an agent must run `dotnet tool restore && dotnet format Terminal.Gui.Text.slnx --exclude third_party/ && dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="Full Cleanup"` (the Stop hook does this automatically). If the cleanup adjusts files, those changes are part of the work — re-stage and continue.**
+**Before declaring work complete, an agent must run `dotnet tool restore && dotnet format Terminal.Gui.Text.slnx --exclude third_party/ && dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="TG.Text Full Cleanup"` (the Stop hook does this automatically). If the cleanup adjusts files, those changes are part of the work — re-stage and continue.**
### Formatting and spacing
diff --git a/Terminal.Gui.Text.slnx.DotSettings b/Terminal.Gui.Text.slnx.DotSettings
index ca5c603b..5c7d8148 100644
--- a/Terminal.Gui.Text.slnx.DotSettings
+++ b/Terminal.Gui.Text.slnx.DotSettings
@@ -13,40 +13,45 @@
-->
-
-
+
+
+
+
+
+
+
- Full Cleanup
- Full Cleanup
+ TG.Text Full Cleanup
+ TG.Text Full Cleanup
- True
- gui-cs/Text house cleanup. Keep aligned with CLAUDE.md.
- Full Cleanup
+ True
+ gui-cs/Text house cleanup. Keep aligned with CLAUDE.md.
+ TG.Text Full Cleanup
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- True
- False
- True
- False
- True
- True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ True
+ False
+ True
+ False
+ True
+ True
diff --git a/examples/ted/TedApp.cs b/examples/ted/TedApp.cs
index 725d03cd..d48d40a3 100644
--- a/examples/ted/TedApp.cs
+++ b/examples/ted/TedApp.cs
@@ -246,7 +246,7 @@ private Key KeyFor (Command command)
private void ShowAboutDialog ()
{
Dialog dialog = new ()
- { Title = "About ted", Buttons = [new Button { Title = Strings.btnOk, IsDefault = true }] };
+ { Title = "About ted", Buttons = [new Button { Title = Strings.btnOk, IsDefault = true }] };
dialog.Border.Settings &= ~BorderSettings.Title;
diff --git a/src/Terminal.Gui.Editor/Editor.Drawing.cs b/src/Terminal.Gui.Editor/Editor.Drawing.cs
index 2428df3e..d27c4ddb 100644
--- a/src/Terminal.Gui.Editor/Editor.Drawing.cs
+++ b/src/Terminal.Gui.Editor/Editor.Drawing.cs
@@ -65,6 +65,11 @@ private void DrawVisibleLines (Rectangle viewport, Attribute normal, Attribute s
}
}
+ // Syntax highlighter state optimization: tracks how far we've prepared so incremental
+ // scrolling doesn't re-highlight from line 0 every frame.
+ private int _highlighterPreparedUpToLine = -1;
+ private ISyntaxHighlighter? _highlighterPreparedInstance;
+
private void PrepareSyntaxHighlighter (ISyntaxHighlighter? syntaxHighlighter, int firstVisibleLineIndex)
{
if (syntaxHighlighter is null || _document is null)
@@ -72,15 +77,25 @@ private void PrepareSyntaxHighlighter (ISyntaxHighlighter? syntaxHighlighter, in
return;
}
- syntaxHighlighter.ResetState ();
+ // If the highlighter instance changed or viewport scrolled backward, reset from scratch.
+ if (!ReferenceEquals (syntaxHighlighter, _highlighterPreparedInstance)
+ || firstVisibleLineIndex < _highlighterPreparedUpToLine)
+ {
+ syntaxHighlighter.ResetState ();
+ _highlighterPreparedInstance = syntaxHighlighter;
+ _highlighterPreparedUpToLine = 0;
+ }
- for (var lineIndex = 0; lineIndex < firstVisibleLineIndex && lineIndex < _document.LineCount; lineIndex++)
+ // Incrementally highlight from where we left off to the first visible line.
+ for (var lineIndex = _highlighterPreparedUpToLine; lineIndex < firstVisibleLineIndex && lineIndex < _document.LineCount; lineIndex++)
{
DocumentLine line = _document.GetLineByNumber (lineIndex + 1);
#pragma warning disable CS0618 // Type or member is obsolete — see note in OnDrawingContent.
syntaxHighlighter.Highlight (_document.GetText (line), SyntaxLanguage);
#pragma warning restore CS0618 // Type or member is obsolete
}
+
+ _highlighterPreparedUpToLine = firstVisibleLineIndex;
}
private void DrawVisualLine (
@@ -106,6 +121,18 @@ private void DrawVisualLine (
foreach (CellVisualLineElement element in visualLine.Elements)
{
+ // Elements are ordered by visual column. Once we pass the visible end,
+ // all remaining elements are off-screen — skip them entirely.
+ if (element.VisualColumn >= visibleEnd)
+ {
+ break;
+ }
+
+ if (element.VisualEndColumn <= visibleStart)
+ {
+ continue;
+ }
+
element.Draw (this, 0, row, visibleStart, visibleEnd);
}
}
diff --git a/src/Terminal.Gui.Editor/Editor.cs b/src/Terminal.Gui.Editor/Editor.cs
index c5f67a1e..1a446c2e 100644
--- a/src/Terminal.Gui.Editor/Editor.cs
+++ b/src/Terminal.Gui.Editor/Editor.cs
@@ -34,6 +34,14 @@ public partial class Editor : View
private readonly Dictionary _drawVisualLineCache = [];
private readonly VisualLineBuilder _visualLineBuilder = new ();
+ // Incremental max-width tracking: avoids the O(N) all-lines walk that UpdateContentSize
+ // used to do on every edit. _maxVisualWidth is the widest visual line seen; _maxWidthLineNumber
+ // tracks which line holds it so we can detect when that line is edited. _maxWidthDirty forces
+ // a full recompute (e.g. on Document swap or IndentationSize change).
+ private int _maxVisualWidth;
+ private int _maxWidthLineNumber;
+ private bool _maxWidthDirty = true;
+
private TextAnchor? _caretAnchor;
private TextDocument? _document;
private Gutter? _gutter;
@@ -84,6 +92,7 @@ public TextDocument? Document
_lastKnownCaretOffset = caretOffset;
_selectionAnchor = null;
ClearVisualLineCaches ();
+ _maxWidthDirty = true;
_virtualCaretColumn = GetCaretColumn ();
UpdateContentSize ();
@@ -211,6 +220,7 @@ public int IndentationSize
field = value;
ClearVisualLineCaches ();
+ _maxWidthDirty = true;
_virtualCaretColumn = GetCaretColumn ();
UpdateContentSize ();
EnsureCaretVisible ();
@@ -299,6 +309,8 @@ private void OnDocumentChanged (object? sender, DocumentChangeEventArgs e)
// line numbers downstream may also have shifted, so clear everything from that line on.
// Cheap: usually one or a handful of entries; correctness > saving a few cache hits.
InvalidateVisualLineCaches (e);
+ InvalidateHighlighterState (e);
+ UpdateMaxWidthIncremental (e);
UpdateContentSize ();
UpdateLineNumberPadding ();
@@ -323,20 +335,115 @@ private void UpdateContentSize ()
return;
}
- var maxWidth = 0;
+ if (_maxWidthDirty)
+ {
+ RecomputeMaxWidth ();
+ }
+
+ // +1 column lets the caret sit just past the end-of-line.
+ SetContentSize (new Size (_maxVisualWidth + 1, _document.LineCount));
+ }
+
+ /// Full O(N) recompute — only called on Document swap, IndentationSize change, etc.
+ private void RecomputeMaxWidth ()
+ {
+ _maxVisualWidth = 0;
+ _maxWidthLineNumber = 0;
+
+ if (_document is null)
+ {
+ _maxWidthDirty = false;
+
+ return;
+ }
foreach (DocumentLine line in _document.Lines)
{
var width = GetOrBuildDefaultVisualLine (line).VisualLength;
- if (width > maxWidth)
+ if (width > _maxVisualWidth)
{
- maxWidth = width;
+ _maxVisualWidth = width;
+ _maxWidthLineNumber = line.LineNumber;
}
}
- // +1 column lets the caret sit just past the end-of-line.
- SetContentSize (new Size (maxWidth + 1, _document.LineCount));
+ _maxWidthDirty = false;
+ }
+
+ ///
+ /// Incrementally updates max width after a document change. Only recomputes the affected
+ /// lines. If the edited line was the widest, does a full recompute since the max may have shrunk.
+ ///
+ private void UpdateMaxWidthIncremental (DocumentChangeEventArgs e)
+ {
+ if (_document is null)
+ {
+ return;
+ }
+
+ // Find which lines are affected by the change.
+ DocumentLine firstAffected = _document.GetLineByOffset (Math.Min (e.Offset, _document.TextLength));
+ var insertedText = e.InsertedText?.Text ?? "";
+ var newlineCount = insertedText.Count (c => c == '\n');
+ var removedText = e.RemovedText?.Text ?? "";
+ var removedNewlines = removedText.Count (c => c == '\n');
+
+ // If the widest line was deleted or its content changed, we must recompute.
+ if (_maxWidthLineNumber >= firstAffected.LineNumber
+ && (_maxWidthLineNumber <= firstAffected.LineNumber + Math.Max (removedNewlines, 0)
+ || removedNewlines > 0))
+ {
+ // The max-holder was touched or lines were removed — check affected lines first,
+ // and only fall back to full recompute if the old max shrank.
+ var newMax = 0;
+ var newMaxLine = _maxWidthLineNumber;
+
+ // Scan from firstAffected through the new lines that were inserted.
+ var scanEnd = Math.Min (firstAffected.LineNumber + newlineCount, _document.LineCount);
+
+ for (var lineNum = firstAffected.LineNumber; lineNum <= scanEnd; lineNum++)
+ {
+ DocumentLine line = _document.GetLineByNumber (lineNum);
+ var width = GetOrBuildDefaultVisualLine (line).VisualLength;
+
+ if (width >= newMax)
+ {
+ newMax = width;
+ newMaxLine = lineNum;
+ }
+ }
+
+ if (newMax >= _maxVisualWidth)
+ {
+ // The affected region has a line at least as wide — it's the new max.
+ _maxVisualWidth = newMax;
+ _maxWidthLineNumber = newMaxLine;
+ }
+ else
+ {
+ // The old widest line shrank and no scanned line is as wide — some unscanned
+ // line may be the new widest. Fall back to full recompute.
+ _maxWidthDirty = true;
+ }
+
+ return;
+ }
+
+ // The change didn't touch the widest line. Just check affected lines for a new max.
+ var endLine = Math.Min (firstAffected.LineNumber + newlineCount, _document.LineCount);
+
+ for (var lineNum = firstAffected.LineNumber; lineNum <= endLine; lineNum++)
+ {
+ DocumentLine line = _document.GetLineByNumber (lineNum);
+ var width = GetOrBuildDefaultVisualLine (line).VisualLength;
+
+ if (width > _maxVisualWidth)
+ {
+ _maxVisualWidth = width;
+ _maxWidthLineNumber = lineNum;
+ }
+ }
}
private void ClearVisualLineCaches ()
@@ -345,6 +452,31 @@ private void ClearVisualLineCaches ()
_drawVisualLineCache.Clear ();
}
+ ///
+ /// Resets the incremental highlighter state when a document change occurs at or before
+ /// the prepared-up-to line. Edits after the prepared region don't affect tokenizer state
+ /// for lines that have already been processed.
+ ///
+ private void InvalidateHighlighterState (DocumentChangeEventArgs e)
+ {
+ if (_highlighterPreparedUpToLine < 0 || _document is null)
+ {
+ return;
+ }
+
+ DocumentLine affectedLine = _document.GetLineByOffset (Math.Min (e.Offset, _document.TextLength));
+ var affectedLineIndex = affectedLine.LineNumber - 1;
+
+ if (affectedLineIndex < _highlighterPreparedUpToLine)
+ {
+ // Edit was before/within the prepared region — must re-prepare from line 0.
+ // Setting to 0 (not -1) so PrepareSyntaxHighlighter's comparison triggers a
+ // ResetState() call on the next draw frame.
+ _highlighterPreparedUpToLine = 0;
+ _highlighterPreparedInstance = null;
+ }
+ }
+
private void InvalidateVisualLineCaches (DocumentChangeEventArgs e)
{
if (_document is null || (_defaultVisualLineCache.Count == 0 && _drawVisualLineCache.Count == 0))
@@ -352,41 +484,75 @@ private void InvalidateVisualLineCaches (DocumentChangeEventArgs e)
return;
}
- // The change starts at e.Offset; anything before that line is unaffected. The cheapest
- // sound invalidation is "drop entries for line numbers ≥ the changed line's number" —
- // newline insert/delete renumbers everything downstream, so per-line keys past the edit
- // are stale even if their content is untouched.
DocumentLine firstAffected = _document.GetLineByOffset (Math.Min (e.Offset, _document.TextLength));
var threshold = firstAffected.LineNumber;
- RemoveFromCache (_defaultVisualLineCache, threshold);
- RemoveFromCache (_drawVisualLineCache, threshold);
+ // Count net newline delta: downstream line numbers shift by this amount.
+ var insertedText = e.InsertedText?.Text ?? "";
+ var insertedNewlines = insertedText.Count (c => c == '\n');
+ var removedText = e.RemovedText?.Text ?? "";
+ var removedNewlines = removedText.Count (c => c == '\n');
+ var lineDelta = insertedNewlines - removedNewlines;
- static void RemoveFromCache (Dictionary cache, int threshold)
+ RekeyCache (_defaultVisualLineCache, threshold, lineDelta, removedNewlines);
+ RekeyCache (_drawVisualLineCache, threshold, lineDelta, removedNewlines);
+
+ static void RekeyCache (Dictionary cache, int threshold, int lineDelta, int removedNewlines)
{
if (cache.Count == 0)
{
return;
}
+ // On newline removal, lines in [threshold, threshold + removedNewlines] were merged
+ // and their cached content is stale — invalidate, don't rekey.
+ var invalidateEnd = threshold + removedNewlines;
+
+ // Collect entries: invalidate the edited line(s), rekey downstream.
+ List>? toRekey = null;
List? toRemove = null;
- foreach (var lineNumber in cache.Keys)
+ foreach (KeyValuePair kvp in cache)
{
- if (lineNumber >= threshold)
+ if (kvp.Key >= threshold && kvp.Key <= invalidateEnd)
{
- (toRemove ??= []).Add (lineNumber);
+ // The edited/merged line(s) — content changed, must invalidate.
+ (toRemove ??= []).Add (kvp.Key);
+ }
+ else if (kvp.Key > invalidateEnd)
+ {
+ if (lineDelta == 0)
+ {
+ // No newline change — downstream entries are still valid as-is.
+ }
+ else
+ {
+ // Line numbers shifted — remove old key, re-add with shifted key.
+ (toRemove ??= []).Add (kvp.Key);
+ (toRekey ??= []).Add (kvp);
+ }
}
}
- if (toRemove is null)
+ if (toRemove is not null)
{
- return;
+ foreach (var key in toRemove)
+ {
+ cache.Remove (key);
+ }
}
- foreach (var lineNumber in toRemove)
+ if (toRekey is not null)
{
- cache.Remove (lineNumber);
+ foreach (KeyValuePair kvp in toRekey)
+ {
+ var newKey = kvp.Key + lineDelta;
+
+ if (newKey > 0)
+ {
+ cache[newKey] = kvp.Value;
+ }
+ }
}
}
}
diff --git a/src/Terminal.Gui.Editor/Rendering/VisualLineBuilder.cs b/src/Terminal.Gui.Editor/Rendering/VisualLineBuilder.cs
index a3471f4c..df1eae41 100644
--- a/src/Terminal.Gui.Editor/Rendering/VisualLineBuilder.cs
+++ b/src/Terminal.Gui.Editor/Rendering/VisualLineBuilder.cs
@@ -11,6 +11,80 @@ public CellVisualLine Build (DocumentLine documentLine, VisualLineBuildContext c
{
var text = context.Document.GetText (documentLine);
CellVisualLine visualLine = new (documentLine);
+
+ if (IsAsciiOnly (text))
+ {
+ BuildAsciiFastPath (documentLine, context, text, visualLine);
+ }
+ else
+ {
+ BuildGraphemePath (documentLine, context, text, visualLine);
+ }
+
+ foreach (IVisualLineTransformer transformer in context.LineTransformers)
+ {
+ transformer.Transform (visualLine);
+ }
+
+ return visualLine;
+ }
+
+ ///
+ /// Fast path for pure-ASCII lines. Avoids GraphemeHelper.GetGraphemes
+ /// allocation — each byte is one grapheme, one column (tabs expand as usual).
+ ///
+ private static void BuildAsciiFastPath (
+ DocumentLine documentLine,
+ VisualLineBuildContext context,
+ string text,
+ CellVisualLine visualLine)
+ {
+ var segmentIndex = 0;
+ var segmentEnd = GetSegmentEnd (context.StyledSegments, segmentIndex);
+ var visualColumn = 0;
+
+ for (var i = 0; i < text.Length; i++)
+ {
+ while (context.StyledSegments is { Count: > 0 }
+ && i >= segmentEnd
+ && segmentIndex + 1 < context.StyledSegments.Count)
+ {
+ segmentIndex++;
+ segmentEnd += context.StyledSegments[segmentIndex].Text.Length;
+ }
+
+ Attribute attribute = GetAttribute (context, segmentIndex);
+ var documentOffset = documentLine.Offset + i;
+
+ if (context.HasSelection
+ && documentOffset < context.SelectionEnd
+ && documentOffset + 1 > context.SelectionStart)
+ {
+ attribute = context.SelectedAttribute;
+ }
+
+ if (text[i] == '\t')
+ {
+ var width = GetTabExpansionWidth (visualColumn, context.IndentationSize);
+ visualLine.AddElement (
+ new TabElement (documentOffset, visualColumn, width, context.ShowTabs, attribute));
+ visualColumn += width;
+ }
+ else
+ {
+ TextRunElement element = new (documentOffset, 1, visualColumn, text.Substring (i, 1), attribute);
+ visualLine.AddElement (element);
+ visualColumn += 1;
+ }
+ }
+ }
+
+ private static void BuildGraphemePath (
+ DocumentLine documentLine,
+ VisualLineBuildContext context,
+ string text,
+ CellVisualLine visualLine)
+ {
var logicalColumn = 0;
var visualColumn = 0;
var segmentIndex = 0;
@@ -53,13 +127,20 @@ public CellVisualLine Build (DocumentLine documentLine, VisualLineBuildContext c
logicalColumn += documentLength;
}
+ }
- foreach (IVisualLineTransformer transformer in context.LineTransformers)
+ /// Checks if all characters are ASCII (no multi-byte graphemes, no surrogates).
+ private static bool IsAsciiOnly (string text)
+ {
+ foreach (var c in text)
{
- transformer.Transform (visualLine);
+ if (c > 127)
+ {
+ return false;
+ }
}
- return visualLine;
+ return true;
}
public static int GetTabExpansionWidth (int visualColumn, int indentationSize)
diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/Testing/TestEnvironment.cs b/tests/Terminal.Gui.Editor.IntegrationTests/Testing/TestEnvironment.cs
new file mode 100644
index 00000000..c905bc0a
--- /dev/null
+++ b/tests/Terminal.Gui.Editor.IntegrationTests/Testing/TestEnvironment.cs
@@ -0,0 +1,17 @@
+using System.Runtime.CompilerServices;
+
+namespace Terminal.Gui.Editor.IntegrationTests.Testing;
+
+///
+/// Sets DisableRealDriverIO=1 before any test runs so the ANSI driver does not attempt
+/// real console I/O. Without this, the full integration test suite hangs on local machines
+/// (the env var is set in CI via the workflow YAML but was missing for local runs).
+///
+internal static class TestEnvironment
+{
+ [ModuleInitializer]
+ internal static void Init ()
+ {
+ Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1");
+ }
+}