diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a30d25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,398 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
diff --git a/App.config b/App.config
deleted file mode 100644
index fad249e..0000000
--- a/App.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/App.xaml b/App.xaml
new file mode 100644
index 0000000..bb46346
--- /dev/null
+++ b/App.xaml
@@ -0,0 +1,4 @@
+
diff --git a/App.xaml.cs b/App.xaml.cs
new file mode 100644
index 0000000..0bc4dd6
--- /dev/null
+++ b/App.xaml.cs
@@ -0,0 +1,62 @@
+using Autofac;
+using charposition.Services;
+using System;
+using System.Data;
+using System.Windows;
+
+namespace charposition;
+
+///
+/// Interaction logic for App.xaml
+///
+public partial class App : Application
+{
+ private void Application_Startup(object sender, StartupEventArgs e)
+ {
+ // Default: dummy data
+ string text = DummyData.Text;
+ string title = DummyData.Title;
+ string semantics = DummyData.Semantics;
+ string error = string.Empty;
+
+ // Services
+ ContainerBuilder services = new();
+ services.RegisterType().As();
+ services.RegisterType().As();
+ services.RegisterType().SingleInstance();
+ services.RegisterType();
+
+ var container = services.Build();
+
+ var model = container.Resolve();
+
+ // read args
+ var fileReader = container.Resolve();
+ if (e.Args.Length > 0)
+ {
+ text = string.Empty;
+ semantics = string.Empty;
+ try
+ {
+ title = e.Args[0];
+ text = fileReader.ReadAllText(e.Args[0]);
+ semantics = e.Args.Length == 1 ? string.Empty : fileReader.ReadAllText(e.Args[1]);
+ }
+ catch (Exception ex)
+ {
+ error = ex.Message;
+ }
+ }
+
+ model.LoadData(text, semantics);
+
+ if (error != string.Empty)
+ {
+ model.ErrorMessage = error;
+ }
+
+ var window = container.Resolve();
+ window.Title = title;
+ window.Show();
+ }
+}
diff --git a/Controls/Character.cs b/Controls/Character.cs
new file mode 100644
index 0000000..ee99508
--- /dev/null
+++ b/Controls/Character.cs
@@ -0,0 +1,62 @@
+using System.Windows.Controls;
+using System.Windows.Media;
+
+namespace charposition.Controls;
+
+public class Character : Canvas
+{
+ public int Line { get; set; }
+ public int Column { get; set; }
+
+ public int CharIndex
+ {
+ set
+ {
+ this.Index.Text = value.ToString();
+ }
+ }
+ public char CharCode
+ {
+ set
+ {
+ string display = DisplayCharacter(value);
+ this.Label.Text = display;
+ this.Label.Foreground = display != value.ToString() ? Brushes.Gray : Brushes.Black;
+ }
+ }
+
+ private TextBlock Label { get; }
+ private TextBlock Index { get; }
+
+ public Character()
+ {
+ this.Label = new()
+ {
+ HorizontalAlignment = System.Windows.HorizontalAlignment.Left,
+ VerticalAlignment = System.Windows.VerticalAlignment.Top,
+ };
+ this.Children.Add(this.Label);
+ Canvas.SetTop(this.Label, 2);
+ Canvas.SetLeft(this.Label, 8);
+
+ this.Index = new()
+ {
+ Foreground = Brushes.Gray,
+ HorizontalAlignment = System.Windows.HorizontalAlignment.Right,
+ VerticalAlignment = System.Windows.VerticalAlignment.Bottom,
+ FontSize = 10
+ };
+ this.Children.Add(this.Index);
+ Canvas.SetBottom(this.Index, 2);
+ Canvas.SetRight(this.Index, 2);
+ }
+
+ private static string DisplayCharacter(char character) =>
+ character switch
+ {
+ '\r' => "\\r",
+ '\n' => "\\n",
+ '\t' => "\\t",
+ _ => character.ToString(),
+ };
+}
diff --git a/Controls/CharsCanvas.cs b/Controls/CharsCanvas.cs
new file mode 100644
index 0000000..e9c7e21
--- /dev/null
+++ b/Controls/CharsCanvas.cs
@@ -0,0 +1,355 @@
+using charposition.ParserModel;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace charposition.Controls;
+
+public class CharsCanvas : UserControl
+{
+ public event EventHandler? HoveredCharChanged;
+
+ public int LineCount
+ {
+ get => (int)GetValue(LineCountProperty);
+ set => SetValue(LineCountProperty, value);
+ }
+ public static readonly DependencyProperty LineCountProperty =
+ DependencyProperty.Register("LineCount", typeof(int), typeof(CharsCanvas), new PropertyMetadata(1, Redraw));
+
+ public int MaxLineLength
+ {
+ get => (int)GetValue(MaxLineLengthProperty);
+ set => SetValue(MaxLineLengthProperty, value);
+ }
+ public static readonly DependencyProperty MaxLineLengthProperty =
+ DependencyProperty.Register("MaxLineLength", typeof(int), typeof(CharsCanvas), new PropertyMetadata(1, Redraw));
+
+ public IEnumerable> LineChars
+ {
+ get => (IEnumerable>)GetValue(LineCharsProperty);
+ set => SetValue(LineCharsProperty, value);
+ }
+ public static readonly DependencyProperty LineCharsProperty =
+ DependencyProperty.Register("LineChars", typeof(IEnumerable>), typeof(CharsCanvas), new PropertyMetadata(null, OnLineCharsChanged));
+
+ static void OnLineCharsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.NewValue is ObservableCollection> coll && d is CharsCanvas ctrl)
+ {
+ coll.CollectionChanged += (object? sender, NotifyCollectionChangedEventArgs e) => ctrl.Draw();
+ }
+ }
+
+ public LocationSpan? SelectedSpan
+ {
+ get => (LocationSpan?)GetValue(SelectedSpanProperty);
+ set => SetValue(SelectedSpanProperty, value);
+ }
+ public static readonly DependencyProperty SelectedSpanProperty =
+ DependencyProperty.Register("SelectedSpan", typeof(LocationSpan), typeof(CharsCanvas), new PropertyMetadata(null, Redraw));
+
+ private readonly ScrollViewer scrollViewer;
+
+ public CharsCanvas(ScrollViewer scrollViewer)
+ {
+ this.Background = Brushes.Transparent;
+
+ this.Canvas = new Canvas();
+ this.Content = this.Canvas;
+
+ Canvas.SetBinding(WidthProperty,
+ new Binding { Path = new PropertyPath(ActualWidthProperty), Source = this });
+ Canvas.SetBinding(HeightProperty,
+ new Binding { Path = new PropertyPath(ActualHeightProperty), Source = this });
+
+ this.SizeChanged += (s, e) => this.Draw();
+
+ this.HighlightColumn = new()
+ {
+ Width = CharsView.CellWidth,
+ Fill = CharsView.HighlightBrush
+ };
+ this.HighlightLine = new()
+ {
+ Height = CharsView.CellHeight,
+ Fill = CharsView.HighlightBrush
+ };
+
+ this.HighlightLine.SetBinding(WidthProperty,
+ new Binding { Path = new PropertyPath(ActualWidthProperty), Source = this });
+ this.HighlightColumn.SetBinding(HeightProperty,
+ new Binding { Path = new PropertyPath(ActualHeightProperty), Source = this });
+
+ this.scrollViewer = scrollViewer;
+ this.scrollViewer.ScrollChanged += (s, e) => this.Draw();
+ }
+
+ protected override Size MeasureOverride(Size constraint) =>
+ new(this.MaxLineLength * CharsView.CellWidth, this.LineCount * CharsView.CellHeight);
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ var mouseOverChar = this.Characters.Find(c => c.IsMouseOver);
+ this.HighlightCharacter(mouseOverChar);
+ }
+
+ internal void HighlightCharacter(Character? character)
+ {
+ if (this.HighlightedCharacter == character)
+ {
+ return;
+ }
+
+ this.HoveredCharChanged?.Invoke(this, new CharChangedArgs
+ {
+ Column = character?.Column,
+ Line = character?.Line
+ });
+
+ if (this.HighlightedCharacter != null)
+ {
+ this.UpdateCharacterSelection(this.HighlightedCharacter);
+ }
+
+ this.HighlightedCharacter = character;
+ if (character == null)
+ {
+ this.Canvas.Children.Remove(this.HighlightColumn);
+ this.Canvas.Children.Remove(this.HighlightLine);
+ return;
+ }
+
+ character.Background = Brushes.Lime;
+ Canvas.SetLeft(this.HighlightColumn, character.Column * CharsView.CellWidth);
+ Canvas.SetTop(this.HighlightLine, character.Line * CharsView.CellHeight);
+ if (!this.Canvas.Children.Contains(this.HighlightColumn))
+ {
+ this.Canvas.Children.Insert(0, this.HighlightColumn);
+ }
+ if (!this.Canvas.Children.Contains(this.HighlightLine))
+ {
+ this.Canvas.Children.Insert(0, this.HighlightLine);
+ }
+ }
+
+ private static void Redraw(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
+ ((CharsCanvas)d).Draw();
+
+ private void Draw()
+ {
+ // Calculate visible columns
+ this.visibleLines = (int)Math.Ceiling(this.scrollViewer.ViewportHeight / CharsView.CellHeight) + 2;
+ this.visibleColumns = (int)Math.Ceiling(this.scrollViewer.ViewportWidth / CharsView.CellWidth) + 2;
+
+ // Update columns
+ this.startRow = (int)Math.Max(0, Math.Floor(this.scrollViewer.VerticalOffset / CharsView.CellHeight) - 1);
+ this.startCol = (int)Math.Max(0, Math.Floor(this.scrollViewer.HorizontalOffset / CharsView.CellWidth) - 1);
+
+ this.UpdateRowLines();
+ this.UpdateColumnLines();
+ this.UpdateCharacters();
+ }
+
+ private void UpdateCharacters()
+ {
+ int charIndex = 0;
+ int rowIndex = 0;
+ foreach (var lineChars in this.LineChars.Skip(this.startRow).Take(this.visibleLines))
+ {
+ var endCol = Math.Min(lineChars.Count, this.startCol + this.visibleColumns);
+ var col = startCol;
+ foreach(var c in lineChars.Skip(this.startCol).Take(endCol))
+ {
+ if (this.Characters.Count <= charIndex)
+ {
+ var @char = new Character()
+ {
+ Width = CharsView.CellWidth,
+ Height = CharsView.CellHeight,
+ Background = Brushes.Transparent,
+ };
+ this.Canvas.Children.Add(@char);
+ this.Characters.Add(@char);
+ }
+
+ var character = this.Characters[charIndex];
+ character.CharIndex = c.Key;
+ character.CharCode = c.Value;
+ character.Line = rowIndex + this.startRow;
+ character.Column = col;
+ this.UpdateCharacterSelection(character);
+ Canvas.SetTop(character, character.Line * CharsView.CellHeight);
+ Canvas.SetLeft(character, col * CharsView.CellWidth);
+
+ charIndex++;
+ col++;
+ }
+ rowIndex++;
+ }
+
+ // Cleanup unneeded chars
+ if (this.Characters.Count > charIndex + 100)
+ {
+ while (this.Characters.Count > charIndex)
+ {
+ this.Canvas.Children.Remove(this.Characters[^1]);
+ this.Characters.RemoveAt(this.Characters.Count - 1);
+ }
+ }
+ }
+
+ private void UpdateCharacterSelection(Character character)
+ {
+ bool isSelected = false;
+ if (this.SelectedSpan?.Start != null && this.SelectedSpan?.End != null)
+ {
+ int row = character.Line + 1;
+ if (row == this.SelectedSpan.Start[0])
+ {
+ isSelected = character.Column + 1 >= this.SelectedSpan.Start[1];
+ }
+ else if (row > this.SelectedSpan.Start[0] && character.Line < this.SelectedSpan.End[0])
+ {
+ isSelected = true;
+ }
+ else if (row == this.SelectedSpan.End[0])
+ {
+ isSelected = character.Column + 1 <= this.SelectedSpan.End[1];
+ }
+ }
+
+ character.Background = isSelected ? CharsView.SelectionBrush : Brushes.Transparent;
+ }
+
+ private void UpdateColumnLines()
+ {
+ int rowIndex = 0;
+ int colIndex = 0;
+ foreach (var lineChars in this.LineChars.Skip(this.startRow).Take(this.visibleLines))
+ {
+ var endCol = Math.Min(lineChars.Count, this.startCol + this.visibleColumns);
+ for (int i = this.startCol; i <= endCol; i++)
+ {
+ if (this.ColumnLines.Count <= colIndex)
+ {
+ var line = new Line
+ {
+ Stroke = CharsView.LineBrush,
+ StrokeThickness = 0.5,
+ };
+ this.Canvas.Children.Add(line);
+ this.ColumnLines.Add(line);
+ }
+ var columnLine = this.ColumnLines[colIndex++];
+
+ columnLine.X1 = columnLine.X2 = (i * CharsView.CellWidth);
+
+ int row = this.startRow + rowIndex;
+ columnLine.Y1 = (row * CharsView.CellHeight);
+ columnLine.Y2 = ((row + 1) * CharsView.CellHeight);
+ }
+
+ rowIndex++;
+ }
+
+ // Cleanup unneeded columns
+ if (this.ColumnLines.Count > colIndex + 100)
+ {
+ while (this.ColumnLines.Count > colIndex)
+ {
+ this.Canvas.Children.Remove(this.ColumnLines[^1]);
+ this.ColumnLines.RemoveAt(this.ColumnLines.Count - 1);
+ }
+ }
+ }
+
+ private void UpdateRowLines()
+ {
+ // Cleanup unneeded lines
+ if (this.RowLines.Count > this.visibleLines + 25)
+ {
+ while (this.RowLines.Count > this.visibleLines)
+ {
+ this.Canvas.Children.Remove(this.RowLines[^1]);
+ this.RowLines.RemoveAt(this.RowLines.Count - 1);
+ }
+ }
+
+ // Create new lines
+ while (this.RowLines.Count < this.visibleLines)
+ {
+ var line = new Line
+ {
+ Stroke = CharsView.LineBrush,
+ StrokeThickness = 0.5,
+ X1 = 0
+ };
+ this.Canvas.Children.Add(line);
+ this.RowLines.Add(line);
+ }
+
+ // Adjust line lengths
+ int lastLineLength = 0;
+ int rowIndex = 0;
+ foreach (var lineChars in this.LineChars.Skip(this.startRow).Take(this.visibleLines))
+ {
+ if (lastLineLength == 0)
+ {
+ rowIndex++;
+ lastLineLength = lineChars.Count;
+ continue;
+ }
+
+ int length = Math.Max(lastLineLength, lineChars.Count);
+ UpdateRowLineLength(rowIndex, this.startRow + rowIndex, length);
+ lastLineLength = lineChars.Count;
+ rowIndex++;
+ }
+
+ UpdateRowLineLength(rowIndex, this.startRow + rowIndex, lastLineLength);
+ }
+
+ private void UpdateRowLineLength(int rowIndex, int lineNo, int length)
+ {
+ if (rowIndex < 1 || rowIndex >= this.RowLines.Count)
+ {
+ return;
+ }
+
+ var line = this.RowLines[rowIndex - 1];
+ line.Y1 = lineNo * CharsView.CellHeight;
+ line.Y2 = lineNo * CharsView.CellHeight;
+ line.X2 = length * CharsView.CellWidth;
+ }
+
+ private Canvas Canvas { get; }
+
+ private List RowLines { get; } = new();
+ private List ColumnLines { get; } = new();
+
+ private List Characters { get; } = new();
+ private Character? HighlightedCharacter { get; set; }
+
+ private Rectangle HighlightLine { get; }
+ private Rectangle HighlightColumn { get; }
+
+ private int visibleLines;
+ private int visibleColumns;
+ private int startRow;
+ private int startCol;
+
+ public class CharChangedArgs : EventArgs
+ {
+ public int? Line { get; set; }
+ public int? Column { get; set; }
+ }
+}
diff --git a/Controls/CharsView.cs b/Controls/CharsView.cs
new file mode 100644
index 0000000..d522bd1
--- /dev/null
+++ b/Controls/CharsView.cs
@@ -0,0 +1,269 @@
+using charposition.Converters;
+using charposition.ParserModel;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Media;
+using System.Windows.Shapes;
+
+namespace charposition.Controls;
+
+public class CharsView : UserControl
+{
+ internal const int CellWidth = 24;
+ internal const int CellHeight = 32;
+
+ internal static readonly Brush LineBrush = Brushes.Gray;
+ internal static readonly Brush LabelBrush = Brushes.Gray;
+ internal static readonly Brush HighlightBrush = new SolidColorBrush(Color.FromRgb(230, 255, 236));
+ internal static readonly Brush SelectionBrush = new SolidColorBrush(Color.FromArgb(100, 192, 232, 250));
+
+ public int LineCount
+ {
+ get => (int)GetValue(LineCountProperty);
+ set => SetValue(LineCountProperty, value);
+ }
+ public static readonly DependencyProperty LineCountProperty =
+ DependencyProperty.Register("LineCount", typeof(int), typeof(CharsView), new PropertyMetadata(1, Redraw));
+
+ public int MaxLineLength
+ {
+ get => (int)GetValue(MaxLineLengthProperty);
+ set => SetValue(MaxLineLengthProperty, value);
+ }
+ public static readonly DependencyProperty MaxLineLengthProperty =
+ DependencyProperty.Register("MaxLineLength", typeof(int), typeof(CharsView), new PropertyMetadata(1, Redraw));
+
+ public IEnumerable LineChars
+ {
+ get => (IEnumerable)GetValue(LineCharsProperty);
+ set => SetValue(LineCharsProperty, value);
+ }
+ public static readonly DependencyProperty LineCharsProperty =
+ DependencyProperty.Register("LineChars", typeof(IEnumerable), typeof(CharsView), new PropertyMetadata(null, OnLineCharsChanged));
+
+ static void OnLineCharsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.NewValue is ObservableCollection coll && d is CharsView ctrl)
+ {
+ coll.CollectionChanged += (object? sender, NotifyCollectionChangedEventArgs e) => ctrl.Draw();
+ }
+ }
+
+ public LocationSpan? SelectedSpan
+ {
+ get => (LocationSpan?)GetValue(SelectedSpanProperty);
+ set => SetValue(SelectedSpanProperty, value);
+ }
+ public static readonly DependencyProperty SelectedSpanProperty =
+ DependencyProperty.Register("SelectedSpan", typeof(LocationSpan), typeof(CharsView), new PropertyMetadata(null));
+
+ public CharsView()
+ {
+ this.Background = Brushes.Transparent;
+
+ this.Canvas = new Canvas();
+ this.Content = this.Canvas;
+
+ Canvas.SetBinding(WidthProperty,
+ new Binding { Path = new PropertyPath(ActualWidthProperty), Source = this });
+ Canvas.SetBinding(HeightProperty,
+ new Binding { Path = new PropertyPath(ActualHeightProperty), Source = this });
+
+ this.SizeChanged += (s, e) => this.Draw();
+
+ this.HorizontalLine = new Line
+ {
+ Stroke = LineBrush,
+ StrokeThickness = 1,
+ X1 = CellWidth,
+ Y1 = CellWidth,
+ Y2 = CellWidth
+ };
+ this.Canvas.Children.Add(this.HorizontalLine);
+
+ this.VerticalLine = new Line
+ {
+ Stroke = LineBrush,
+ StrokeThickness = 1,
+ X1 = CellWidth,
+ X2 = CellWidth,
+ Y1 = CellWidth
+ };
+ this.Canvas.Children.Add(this.VerticalLine);
+
+ this.ScrollViewer = new()
+ {
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Visible,
+ VerticalScrollBarVisibility = ScrollBarVisibility.Visible
+ };
+ this.ScrollViewer.ScrollChanged += (s, e) => this.Draw();
+ Canvas.SetTop(this.ScrollViewer, CellWidth + 1);
+ Canvas.SetLeft(this.ScrollViewer, CellWidth + 1);
+ ScrollViewer.SetBinding(WidthProperty,
+ new Binding
+ {
+ Path = new PropertyPath(ActualWidthProperty),
+ Source = this,
+ Converter = new SizeAdjustmentConverter { Adjustment = -1 * CellWidth }
+ });
+ ScrollViewer.SetBinding(HeightProperty,
+ new Binding
+ {
+ Path = new PropertyPath(ActualHeightProperty),
+ Source = this,
+ Converter = new SizeAdjustmentConverter { Adjustment = -1 * CellWidth }
+ });
+ this.Canvas.Children.Add(this.ScrollViewer);
+
+ this.CharsCanvas = new(this.ScrollViewer);
+ this.CharsCanvas.HoveredCharChanged += (s, e) => HighlightCharacter(e.Line, e.Column);
+ this.ScrollViewer.Content = this.CharsCanvas;
+
+ CharsCanvas.SetBinding(CharsCanvas.LineCharsProperty,
+ new Binding { Path = new PropertyPath(LineCharsProperty), Source = this });
+ CharsCanvas.SetBinding(CharsCanvas.MaxLineLengthProperty,
+ new Binding { Path = new PropertyPath(MaxLineLengthProperty), Source = this });
+ CharsCanvas.SetBinding(CharsCanvas.LineCountProperty,
+ new Binding { Path = new PropertyPath(LineCountProperty), Source = this });
+ CharsCanvas.SetBinding(CharsCanvas.SelectedSpanProperty,
+ new Binding { Path = new PropertyPath(SelectedSpanProperty), Source = this });
+
+ this.HighlightColumn = new()
+ {
+ Width = CellWidth,
+ Height = CellWidth,
+ Fill = HighlightBrush,
+ Visibility = Visibility.Collapsed
+ };
+ this.Canvas.Children.Add(this.HighlightColumn);
+ this.HighlightLine = new()
+ {
+ Width = CellWidth,
+ Height = CellHeight,
+ Fill = HighlightBrush,
+ Visibility = Visibility.Collapsed
+ };
+ this.Canvas.Children.Add(this.HighlightLine);
+ }
+
+ private void HighlightCharacter(int? line, int? column)
+ {
+ if (line.HasValue)
+ {
+ this.HighlightLine.Visibility = Visibility.Visible;
+ Canvas.SetTop(this.HighlightLine, CellWidth + (line.Value * CellHeight) - this.ScrollViewer.VerticalOffset);
+ }
+ else
+ {
+ this.HighlightLine.Visibility = Visibility.Collapsed;
+ }
+
+ if (column.HasValue)
+ {
+ this.HighlightColumn.Visibility = Visibility.Visible;
+ Canvas.SetLeft(this.HighlightColumn, ((column.Value + 1) * CellWidth) - this.ScrollViewer.HorizontalOffset);
+ }
+ else
+ {
+ this.HighlightColumn.Visibility = Visibility.Collapsed;
+ }
+ }
+
+ private static void Redraw(DependencyObject d, DependencyPropertyChangedEventArgs e) =>
+ ((CharsView)d).Draw();
+
+ private void Draw()
+ {
+ this.UpdateBorder();
+ this.UpdateLineNumbers();
+ this.UpdateColumnNumbers();
+ }
+
+ private void UpdateColumnNumbers()
+ {
+ while (this.ColumnLabels.Count > this.MaxLineLength)
+ {
+ this.Canvas.Children.Remove(this.ColumnLabels[^1]);
+ this.ColumnLabels.RemoveAt(this.ColumnLabels.Count - 1);
+ }
+ while (this.ColumnLabels.Count < this.MaxLineLength)
+ {
+ var label = new TextBlock
+ {
+ Text = (this.ColumnLabels.Count + 1).ToString(),
+ Foreground = LabelBrush,
+ FontSize = 12,
+ Width = CellWidth,
+ Height = CellHeight,
+ TextAlignment = TextAlignment.Center
+ };
+ this.Canvas.Children.Add(label);
+ this.ColumnLabels.Add(label);
+ Canvas.SetTop(label, 0);
+ }
+
+ for (int i = 0; i < this.ColumnLabels.Count; i++)
+ {
+ var label = this.ColumnLabels[i];
+ double left = ((i + 1) * CellWidth) - this.ScrollViewer.HorizontalOffset;
+ Canvas.SetLeft(label, left);
+ label.Visibility = left >= CellWidth / 2 ? Visibility.Visible : Visibility.Hidden;
+ }
+ }
+
+ private void UpdateLineNumbers()
+ {
+ while (this.LineNoLabels.Count > this.LineCount)
+ {
+ this.Canvas.Children.Remove(this.LineNoLabels[^1]);
+ this.LineNoLabels.RemoveAt(this.LineNoLabels.Count - 1);
+ }
+ while (this.LineNoLabels.Count < this.LineCount)
+ {
+ var label = new TextBlock
+ {
+ Text = (this.LineNoLabels.Count + 1).ToString(),
+ Foreground = LabelBrush,
+ FontSize = 12,
+ Width = CellWidth,
+ Height = CellHeight,
+ TextAlignment = TextAlignment.Center
+ };
+ this.Canvas.Children.Add(label);
+ this.LineNoLabels.Add(label);
+ Canvas.SetLeft(label, 0);
+ }
+
+ for (int i = 0; i < this.LineNoLabels.Count; i++)
+ {
+ var label = this.LineNoLabels[i];
+ double top = ((i + 1) * CellHeight) - this.ScrollViewer.VerticalOffset;
+ Canvas.SetTop(label, top);
+ label.Visibility = top >= CellWidth / 2 ? Visibility.Visible : Visibility.Hidden;
+ }
+ }
+
+ private void UpdateBorder()
+ {
+ this.HorizontalLine.X2 = this.ActualWidth;
+ this.VerticalLine.Y2 = this.ActualHeight;
+ }
+
+ private Canvas Canvas { get; }
+ private Line HorizontalLine { get; }
+ private Line VerticalLine { get; }
+
+ private List LineNoLabels { get; } = new();
+ private List ColumnLabels { get; } = new();
+
+ private ScrollViewer ScrollViewer { get; }
+ private CharsCanvas CharsCanvas { get; }
+
+ private Rectangle HighlightLine { get; }
+ private Rectangle HighlightColumn { get; }
+}
diff --git a/Converters/NotNullVisibilityConverter.cs b/Converters/NotNullVisibilityConverter.cs
new file mode 100644
index 0000000..f50201e
--- /dev/null
+++ b/Converters/NotNullVisibilityConverter.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace charposition.Converters;
+
+public class NotNullVisibilityConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
+ value != null ? Visibility.Visible : Visibility.Collapsed;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
+ throw new NotImplementedException();
+}
diff --git a/Converters/SizeAdjustmentConverter.cs b/Converters/SizeAdjustmentConverter.cs
new file mode 100644
index 0000000..e1f98a2
--- /dev/null
+++ b/Converters/SizeAdjustmentConverter.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Globalization;
+using System.Windows.Data;
+
+namespace charposition.Converters;
+
+public class SizeAdjustmentConverter : IValueConverter
+{
+ public double Adjustment { get; set; }
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
+ value is double size ? size + Adjustment : value;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
+ value is double size ? size - Adjustment : value;
+}
diff --git a/DummyData.cs b/DummyData.cs
new file mode 100644
index 0000000..6d0ba49
--- /dev/null
+++ b/DummyData.cs
@@ -0,0 +1,46 @@
+namespace charposition;
+
+internal static class DummyData
+{
+ public const string Title = "Sample code";
+
+ public const string Text =
+@"class Socket
+{
+ void Connect(string server)
+ {
+ SocketLibrary.Connect(mSocket, server);
+ }
+
+ void Disconnect()
+ {
+ SocketLibrary.Disconnect(mSocket);
+ }
+}";
+
+ public const string Semantics =
+@"---
+type: file
+name: FooSocket.csharp
+locationSpan : {start: [1, 0], end: [12, 1]}
+footerSpan : [0,-1]
+parsingErrorsDetected : false
+children:
+
+ - type : class
+ name : Socket
+ locationSpan : {start: [1, 0], end: [12, 1]}
+ headerSpan : [0, 16]
+ footerSpan : [186, 186]
+ children :
+
+ - type : method
+ name : Connect
+ locationSpan : {start: [3, 0], end: [7,2]}
+ span : [17, 109]
+
+ - type : method
+ name : Disconnect
+ locationSpan : {start: [8,0], end: [11,6]}
+ span : [110, 185]";
+}
diff --git a/Form1.Designer.cs b/Form1.Designer.cs
deleted file mode 100644
index 463bbef..0000000
--- a/Form1.Designer.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-namespace charposition
-{
- partial class Form1
- {
- ///
- /// Required designer variable.
- ///
- private System.ComponentModel.IContainer components = null;
-
- ///
- /// Clean up any resources being used.
- ///
- /// true if managed resources should be disposed; otherwise, false.
- protected override void Dispose(bool disposing)
- {
- if (disposing && (components != null))
- {
- components.Dispose();
- }
- base.Dispose(disposing);
- }
-
- #region Windows Form Designer generated code
-
- ///
- /// Required method for Designer support - do not modify
- /// the contents of this method with the code editor.
- ///
- private void InitializeComponent()
- {
- this.pictureBox1 = new System.Windows.Forms.PictureBox();
- ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
- this.SuspendLayout();
- //
- // pictureBox1
- //
- this.pictureBox1.Location = new System.Drawing.Point(0, 0);
- this.pictureBox1.Name = "pictureBox1";
- this.pictureBox1.Size = new System.Drawing.Size(100, 50);
- this.pictureBox1.TabIndex = 0;
- this.pictureBox1.TabStop = false;
- //
- // Form1
- //
- this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
- this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.AutoScroll = true;
- this.BackColor = System.Drawing.Color.White;
- this.ClientSize = new System.Drawing.Size(862, 385);
- this.Controls.Add(this.pictureBox1);
- this.Name = "Form1";
- this.Text = "Form1";
- ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
- this.ResumeLayout(false);
-
- }
-
- #endregion
-
- private System.Windows.Forms.PictureBox pictureBox1;
-
- }
-}
-
diff --git a/Form1.cs b/Form1.cs
deleted file mode 100644
index d3637ce..0000000
--- a/Form1.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Drawing;
-using System.Windows.Forms;
-
-namespace charposition
-{
- public partial class Form1 : Form
- {
- public Form1(string title, string text)
- {
- InitializeComponent();
-
- this.Text = title;
-
- Bitmap bmp = RenderFile.Draw(text);
-
- pictureBox1.Width = bmp.Width;
- pictureBox1.Height = bmp.Height;
- pictureBox1.Left = 0;
- pictureBox1.Top = 0;
- pictureBox1.Image = bmp;
- }
- }
-}
diff --git a/Form1.resx b/Form1.resx
deleted file mode 100644
index 29dcb1b..0000000
--- a/Form1.resx
+++ /dev/null
@@ -1,120 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- text/microsoft-resx
-
-
- 2.0
-
-
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
-
-
\ No newline at end of file
diff --git a/MainWindow.xaml b/MainWindow.xaml
new file mode 100644
index 0000000..51d8a75
--- /dev/null
+++ b/MainWindow.xaml
@@ -0,0 +1,186 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
new file mode 100644
index 0000000..3425421
--- /dev/null
+++ b/MainWindow.xaml.cs
@@ -0,0 +1,32 @@
+using charposition.ParserModel;
+using System.Windows;
+using System.Windows.Controls;
+
+namespace charposition;
+
+///
+/// Interaction logic for MainWindow.xaml
+///
+public partial class MainWindow : Window
+{
+ public MainWindow(MainWindowModel model)
+ {
+ DataContext = model;
+ InitializeComponent();
+ }
+
+ private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs