fix(source): map original offsets to native CR text
Hosted source selections exposed a two-character drift after two newlines. Preserve canonical offsets and normalize only the native projection to one CR per break; retain exact viewport, fold and find assertions. Refs #10 Co-Authored-By: Codex (GPT-6) <noreply@openai.com> Signed-off-by: James Maes <jmaes@dmdbrands.com>
This commit is contained in:
6 files changed
+34
-23
No files matched your search
+15
-1
@@ -21,7 +21,7 @@ zero failures/skips, at artifacts/source-anchors-verified-installed-38113042618.
|
||||
Fixtures remain synthetic/unsigned; all prior producers are terminal.
|
||||
|
||||
The current increment implements literal nested source folding and bidirectional
|
||||
heading-anchor scroll mapping. Canonical snapshots remain immutable; native CRLF
|
||||
heading-anchor scroll mapping. Canonical snapshots remain immutable; native CR
|
||||
and folded offsets retain exact find selections. Browser geometry is bounded,
|
||||
validated and current-page/query guarded; feedback holds preserve explicit find
|
||||
viewports. Microsoft TextBoxView content coordinates do not receive a duplicate
|
||||
@@ -375,3 +375,17 @@ quadratic full-remainder scan for adversarial long raw HTML lines. The source
|
||||
snapshot remains immutable; no new capability/dependency is introduced. Update
|
||||
PRODUCT/TODO with actual vim proof; source synchronization UI stays pending and
|
||||
full scope remains active. No local GUI input, Azure attempt, merge/tag/release.
|
||||
|
||||
## Current native newline failure and correction
|
||||
|
||||
ddb98ac / CI 38114492408 native job 114396569276 finished FAILURE: 42/46,
|
||||
zero skips. Existing paired source selections shift by two characters after two
|
||||
line breaks; actual selections are pha** and *[x] followed by CR. WinUI's text
|
||||
provider uses one CR per line break, not CRLF. The new model now normalizes display
|
||||
to CR, subtracts only original CRLF extra characters and retains canonical source
|
||||
positions. This also corrects heading geometry indices. The fold fixture compares
|
||||
literal Setext text after newline normalization; all hide/reveal/find/reload and
|
||||
actual viewport/physical-wheel assertions remain. Exact native log retained at
|
||||
artifacts/source-sync-ui-38114492408.log. Build/core/format, CodeQL, scripts,
|
||||
security and PR gates passed; installed producer is still observed before a push.
|
||||
No local desktop input or weakening of coverage/selection/viewport checks.
|
||||
@@ -97,7 +97,7 @@ capability. These parser tests do not prove browser behavior. ADR status remains
|
||||
|
||||
## Literal source folding and scroll synchronization
|
||||
|
||||
A pure source projection normalizes native CRLF display offsets, retains heading
|
||||
A pure source projection normalizes native CR display offsets, retains heading
|
||||
lines and replaces folded bodies with plain ellipses. Nested choices remain
|
||||
independent; search expands the projection without changing the original snapshot.
|
||||
Native selections map original UTF-16 query ranges into the visible literal text.
|
||||
|
||||
@@ -25,7 +25,7 @@ physical directory. It does not authorize native commands or remote resource acc
|
||||
| File change notifications | Directory subscription scoped to selected path; 350 ms quiet-period debounce; replacement/deletion recovery; bounded transient retries; subscriptions released on window close |
|
||||
| Theme JSON / CSS | Native editor and saved user overrides use the same bounded validation; corrupt theme settings are discarded before startup; restoration keeps separately named custom themes; embedded original palettes; bounded schema-version-one parser; validated hex colors and finite sizes; code-point escaped font names; less-than signs escaped before style embedding; no network capability |
|
||||
| Browser viewport | Fixed origin-checked native query; validate two finite fractions; invariant numeric restore only; authored scripts/messages stay disabled |
|
||||
| Source pane | Bounded immutable snapshot plus literal native fold projection; original UTF-16 find positions map to visible CRLF text; HTML/CSS/script text never enters a renderer or command interpreter; hidden pane removed from accessibility surface |
|
||||
| Source pane | Bounded immutable snapshot plus literal native fold projection; original UTF-16 find positions map to visible native CR text; HTML/CSS/script text never enters a renderer or command interpreter; hidden pane removed from accessibility surface |
|
||||
| Find | Bounded literal query; pure source positions and native WebView2 finder, with duplicate browser UI suppressed; no query enters executable code or activity logs. Serialized queries and page/navigation checks reject stale completions; existing numeric section operations reveal/restore content |
|
||||
| Fixture input diagnostics | Explicit opt-in environment switch; observes handled native pointer/drag events without changing routing; logs owned control names, coordinates and button state only, never document text or keystrokes |
|
||||
| Browser surface | Scripts, messages, host objects, permissions, downloads, frames and popups disabled; restrictive CSP |
|
||||
|
||||
@@ -12,10 +12,10 @@ namespace MarkdownViewr.Windows.Core.Documents;
|
||||
/// <summary>Mirrors section folds in a literal native display while preserving the original snapshot and source identities.</summary>
|
||||
public sealed class DocumentSourceText
|
||||
{
|
||||
private const string Ellipsis = "⋯\r\n";
|
||||
private const string Ellipsis = "⋯\r";
|
||||
private readonly string _source;
|
||||
private readonly string _native;
|
||||
private readonly int[] _singleBreaks;
|
||||
private readonly int[] _crlfBreaks;
|
||||
private readonly int[] _lineStarts;
|
||||
private readonly DocumentSourceHeading[] _originalHeadings;
|
||||
private readonly (int Index, int Start, int End)[] _sections;
|
||||
@@ -40,8 +40,8 @@ public sealed class DocumentSourceText
|
||||
throw new ArgumentException("Source positions must belong to the committed snapshot.", nameof(outline));
|
||||
}
|
||||
_source = source;
|
||||
_native = source.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n').Replace("\n", "\r\n", StringComparison.Ordinal);
|
||||
var singleBreaks = new List<int>();
|
||||
_native = source.Replace("\r\n", "\r", StringComparison.Ordinal).Replace('\n', '\r');
|
||||
var crlfBreaks = new List<int>();
|
||||
var lineStarts = new List<int> { 0 };
|
||||
for(int index = 0; index < source.Length; index++)
|
||||
{
|
||||
@@ -50,15 +50,12 @@ public sealed class DocumentSourceText
|
||||
if(source[index] == '\r' && index + 1 < source.Length && source[index + 1] == '\n')
|
||||
{
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
singleBreaks.Add(index);
|
||||
crlfBreaks.Add(index);
|
||||
}
|
||||
lineStarts.Add(index + 1);
|
||||
}
|
||||
}
|
||||
_singleBreaks = singleBreaks.ToArray();
|
||||
_crlfBreaks = crlfBreaks.ToArray();
|
||||
_lineStarts = lineStarts.ToArray();
|
||||
_originalHeadings = outline.SourceHeadings.OrderBy(heading => heading.Start).ToArray();
|
||||
_sections = new (int, int, int)[_originalHeadings.Length];
|
||||
@@ -81,7 +78,7 @@ public sealed class DocumentSourceText
|
||||
|
||||
|
||||
|
||||
/// <summary>Visible literal text with Windows newlines and a plain ellipsis for omitted bodies, never HTML or rich text.</summary>
|
||||
/// <summary>Visible literal text with native CR newlines and a plain ellipsis for omitted bodies, never HTML or rich text.</summary>
|
||||
public string Text { get; private set; } = "";
|
||||
|
||||
|
||||
@@ -163,11 +160,11 @@ public sealed class DocumentSourceText
|
||||
|
||||
|
||||
|
||||
/// <summary>Each original lone CR/LF expands by one native character; existing CRLF and Unicode text keep their lengths.</summary>
|
||||
/// <summary>Each original CRLF loses its LF in WinUI text; lone CR/LF and Unicode retain their lengths.</summary>
|
||||
private int ToNative(int position)
|
||||
{
|
||||
int index = Array.BinarySearch(_singleBreaks, position);
|
||||
return position + (index >= 0 ? index : ~index);
|
||||
int index = Array.BinarySearch(_crlfBreaks, position);
|
||||
return position - (index >= 0 ? index : ~index);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -20,15 +20,15 @@ public sealed class DocumentSourceTextTests
|
||||
{
|
||||
string original = string.Join(newline, "😀 Intro", "", "Parent", "======", "Parent body", "", "## Child", "Child body", "```md", "# Fake", "```", "", "> ## Peer", "> Peer body", "", "# Last", "Tail");
|
||||
var source = new DocumentSourceText(original, new DocumentOutline(original));
|
||||
Assert.Equal(original.ReplaceLineEndings("\r\n"), source.Text);
|
||||
Assert.Equal(original.ReplaceLineEndings("\r"), source.Text);
|
||||
Assert.Equal(4, source.Headings.Count);
|
||||
Assert.True(source.SetCollapsed([1]));
|
||||
Assert.Contains("Parent body", source.Text, StringComparison.Ordinal);
|
||||
Assert.Contains("## Child\r\n⋯\r\n> ## Peer", source.Text, StringComparison.Ordinal);
|
||||
Assert.Contains("## Child\r⋯\r> ## Peer", source.Text, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("# Fake", source.Text, StringComparison.Ordinal);
|
||||
Assert.Null(source.GetDisplayRange(original.IndexOf("Child body", StringComparison.Ordinal), 5));
|
||||
Assert.True(source.SetCollapsed([0, 1]));
|
||||
Assert.Contains("Parent\r\n======\r\n⋯\r\n# Last", source.Text, StringComparison.Ordinal);
|
||||
Assert.Contains("Parent\r======\r⋯\r# Last", source.Text, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Child", source.Text, StringComparison.Ordinal);
|
||||
Assert.Equal(new[] { 0, 3 }, source.Headings.Select(heading => heading.Index));
|
||||
Assert.True(source.SetCollapsed([1]));
|
||||
@@ -36,7 +36,7 @@ public sealed class DocumentSourceTextTests
|
||||
Assert.DoesNotContain("Child body", source.Text, StringComparison.Ordinal);
|
||||
Assert.False(source.SetCollapsed([1, 1]));
|
||||
Assert.True(source.SetCollapsed([]));
|
||||
Assert.Equal(original.ReplaceLineEndings("\r\n"), source.Text);
|
||||
Assert.Equal(original.ReplaceLineEndings("\r"), source.Text);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class DocumentSourceTextTests
|
||||
{
|
||||
var source = new DocumentSourceText(original, new DocumentOutline(original));
|
||||
source.SetCollapsed([-1, 0, 99]);
|
||||
Assert.Equal(original.ReplaceLineEndings("\r\n"), source.Text);
|
||||
Assert.Equal(original.ReplaceLineEndings("\r"), source.Text);
|
||||
Assert.Throws<ArgumentNullException>(() => source.SetCollapsed(null!));
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public sealed class DocumentSourceTextTests
|
||||
source.SetCollapsed([0, 1]);
|
||||
Assert.Contains("<h2>One</h2><h2>Two</h2>", source.Text, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Body", source.Text, StringComparison.Ordinal);
|
||||
Assert.Contains("# Last\r\nTail", source.Text, StringComparison.Ordinal);
|
||||
Assert.Contains("# Last\rTail", source.Text, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -389,7 +389,7 @@ public class AppTests
|
||||
WaitForSourceValue(session, source, value => !value.Contains("Grand body", StringComparison.Ordinal) && !value.Contains("# Fence pseudo", StringComparison.Ordinal) && value.Contains("### Source fold grandchild", StringComparison.Ordinal), "the folded native grandchild body and fence");
|
||||
session.ClickElement("OutlineHeading:1");
|
||||
session.ClickElement("ToggleSection");
|
||||
WaitForSourceValue(session, source, value => !value.Contains("### Source fold grandchild", StringComparison.Ordinal) && value.Contains("Source fold peer\r\n----------------", StringComparison.Ordinal), "the folded parent and retained Setext peer");
|
||||
WaitForSourceValue(session, source, value => !value.Contains("### Source fold grandchild", StringComparison.Ordinal) && value.ReplaceLineEndings("\n").Contains("Source fold peer\n----------------", StringComparison.Ordinal), "the folded parent and retained Setext peer");
|
||||
session.InvokeMenuItem("Edit", "FindMenuItem");
|
||||
session.EnterText("FindText", "Grand body");
|
||||
WaitForFindCount(session, "1 of 1");
|
||||
|
||||
Reference in new issue
Block a user